Merge branch 'stable-2.16' into stable-3.0

* stable-2.16:
  Bazel: Remove deprecated tls_enabled option

Change-Id: I2806152042bd597c8a5bf2c61d15da7c203d7a20
diff --git a/.bazelproject b/.bazelproject
index 8a726eb..e14c108 100644
--- a/.bazelproject
+++ b/.bazelproject
@@ -1,6 +1,6 @@
 # The project view file (.bazelproject) is used to import Gerrit Bazel packages into the IDE.
 #
-# See: https://ij.bazel.io/docs/project-views.html
+# See: https://ij.bazel.build/docs/project-views.html
 
 directories:
   .
diff --git a/.gitignore b/.gitignore
index ac244a6..93c1339 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,28 +6,42 @@
 *.swp
 *~
 .DS_Store
-.gwt_work_dir
 /.apt_generated
 /.apt_generated_tests
 /.bazel_path
 /.classpath
 /.factorypath
 /.idea
+/.ijwb
 /.metadata
 /.project
 /.settings/org.eclipse.ltk.core.refactoring.prefs
 /.settings/org.eclipse.m2e.core.prefs
 /.settings/org.maven.ide.eclipse.prefs
+/.vscode
 /bazel-*
 /bin/
+/bower_components/
 /eclipse-out
 /extras
 /gerrit-package-plugins
-/gwt-unitCache
 /infer-out
 /local.properties
-/plugins/cookbook-plugin/
+/node_modules/
+/package-lock.json
+/plugins/*
+!/plugins/BUILD
+!/plugins/codemirror-editor
+!/plugins/commit-message-length-validator
+!/plugins/delete-project
+!/plugins/download-commands
+!/plugins/external_plugin_deps.bzl
+!/plugins/gitiles
+!/plugins/hooks
+!/plugins/plugin-manager
+!/plugins/replication
+!/plugins/reviewnotes
+!/plugins/singleusergroup
+!/plugins/webhooks
 /test_site
 /tools/format
-/.vscode
-/.ijwb
diff --git a/.gitmodules b/.gitmodules
index 8d75bcc..6844f6a 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -8,16 +8,31 @@
 	url = ../plugins/commit-message-length-validator
 	branch = .
 
+[submodule "plugins/delete-project"]
+	path = plugins/delete-project
+	url = ../plugins/delete-project
+	branch = .
+
 [submodule "plugins/download-commands"]
 	path = plugins/download-commands
 	url = ../plugins/download-commands
 	branch = .
 
+[submodule "plugins/gitiles"]
+	path = plugins/gitiles
+	url = ../plugins/gitiles
+	branch = .
+
 [submodule "plugins/hooks"]
 	path = plugins/hooks
 	url = ../plugins/hooks
 	branch = .
 
+[submodule "plugins/plugin-manager"]
+	path = plugins/plugin-manager
+	url = ../plugins/plugin-manager
+	branch = .
+
 [submodule "plugins/replication"]
 	path = plugins/replication
 	url = ../plugins/replication
@@ -32,3 +47,8 @@
 	path = plugins/singleusergroup
 	url = ../plugins/singleusergroup
 	branch = .
+
+[submodule "plugins/webhooks"]
+	path = plugins/webhooks
+	url = ../plugins/webhooks
+	branch = .
diff --git a/.gitreview b/.gitreview
index 663037e..b3c37ad 100644
--- a/.gitreview
+++ b/.gitreview
@@ -2,4 +2,4 @@
 host=gerrit-review.googlesource.com
 scheme=https
 project=gerrit.git
-defaultbranch=stable-2.16
+defaultbranch=stable-3.0
diff --git a/BUILD b/BUILD
index 10162be..3989a75 100644
--- a/BUILD
+++ b/BUILD
@@ -43,15 +43,9 @@
 )
 
 pkg_war(
-    name = "polygerrit",
-    ui = "polygerrit",
-)
-
-pkg_war(
     name = "release",
     context = ["//plugins:core"],
     doc = True,
-    ui = "ui_optdbg_r",
 )
 
 pkg_war(
@@ -69,9 +63,6 @@
     "//plugins:plugin-api_deploy.jar",
     "//plugins:plugin-api-sources_deploy.jar",
     "//plugins:plugin-api-javadoc",
-    "//gerrit-plugin-gwtui:gwtui-api_deploy.jar",
-    "//gerrit-plugin-gwtui:gwtui-api-source_deploy.jar",
-    "//gerrit-plugin-gwtui:gwtui-api-javadoc",
 ]
 
 genrule2(
diff --git a/Documentation/BUILD b/Documentation/BUILD
index 79b9e51..52ab7a8 100644
--- a/Documentation/BUILD
+++ b/Documentation/BUILD
@@ -44,7 +44,6 @@
     name = "licenses",
     opts = ["--asciidoctor"],
     targets = [
-        "//gerrit-gwtui:ui_module",
         "//polygerrit-ui/app:polygerrit_ui",
         "//java/com/google/gerrit/pgm",
     ],
@@ -53,14 +52,24 @@
 license_map(
     name = "js_licenses",
     targets = [
-        "//gerrit-gwtui:ui_module",
         "//polygerrit-ui/app:polygerrit_ui",
     ],
 )
 
+sh_test(
+    name = "check_licenses",
+    srcs = ["check_licenses_test.sh"],
+    data = [
+        "js_licenses.gen.txt",
+        "js_licenses.txt",
+        "licenses.gen.txt",
+        "licenses.txt",
+    ],
+)
+
 DOC_DIR = "Documentation"
 
-SRCS = glob(["*.txt"]) + [":licenses.txt"]
+SRCS = glob(["*.txt"])
 
 genrule(
     name = "index",
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 370a891..cdf6b30 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -5,6 +5,12 @@
 to those groups.  Access rights cannot be granted to individual
 users.
 
+To view/edit the access controls for a specific project, first
+navigate to the projects page: for example,
+https://gerrit-review.googlesource.com/admin/repos/ . Then click on
+the individual project, and then click Access. This will bring you
+to a url that looks like
+https://gerrit-review.googlesource.com/admin/repos/gerrit,access
 
 [[system_groups]]
 == System Groups
@@ -828,7 +834,7 @@
 [[category_view_private_changes]]
 === View Private Changes
 
-This category permits users to view all private changes.
+This category permits users to view all private changes and all change edit refs.
 
 The change owner and any explicitly added reviewers can always see
 private changes (even without having the `View Private Changes` access
@@ -1083,21 +1089,43 @@
 [[block]]
 === 'BLOCK' access rule
 
-The 'BLOCK' rule blocks a permission globally. An inherited 'BLOCK'
-rule cannot be overridden in the inheriting project. Any 'ALLOW' rule
-from an inheriting project, which conflicts with an inherited 'BLOCK'
-rule will not be honored. Searching for 'BLOCK' rules, in the chain
-of parent projects, ignores the Exclusive flag, unless the rule with
-the Exclusive flag is defined on the same project as the 'BLOCK'
-rule. This means within the same project a 'BLOCK' rule can be
-overruled by 'ALLOW' rules on the same access section and 'ALLOW'
-rules with Exclusive flag on access section for more specific refs.
+The 'BLOCK' rule can be used to take away rights from users. The BLOCK rule
+works across project inheritance, from the top down, so an administrator can
+use 'BLOCK' rules to enforce site-wide restrictions.
+
+For example, if a user in the 'Foo Users' group tries to push to
+'refs/heads/mater' with the permissions below, that user will be blocked
+
+[options="header"]
+|=========================================================================
+|Project      | Inherits From    |Reference Name |Permissions            |
+|All-Projects | -                |refs/*         |push = block Foo Users |
+|Foo          | All-Projects     |refs/heads/*   |push = Foo Users       |
+|=========================================================================
+
+'BLOCK' rules are evaluated starting from the parent project, and after a 'BLOCK'
+rule is found to apply, further rules are ignored. Hence, in this example, the
+permissions on child-project is ignored.
+
+----
+All-Projects: project.config
+  [access "refs/heads/*"]
+    push = block group X
+
+child-project: project.config
+  [access "refs/heads/*"]
+    exclusiveGroupPermissions = push
+    push = group X
+----
+
+In this case push for group 'X' will be blocked, even though the Exclusive
+flag was set for the child-project.
 
 A 'BLOCK' rule that blocks the 'push' permission blocks any type of push,
 force or not. A blocking force push rule blocks only force pushes, but
 allows non-forced pushes if an 'ALLOW' rule would have permitted it.
 
-It is also possible to block label ranges.  To block a group 'X' from voting
+It is also possible to block label ranges. To block a group 'X' from voting
 '-2' and '+2', but keep their existing voting permissions for the '-1..+1'
 range intact we would define:
 
@@ -1124,6 +1152,24 @@
 In this case a user which is a member of the group 'Y' will still be allowed to
 push to 'refs/heads/*' even if it is a member of the group 'X'.
 
+=== 'BLOCK' and 'ALLOW' rules in the same project with the Exclusive flag
+
+When a project contains a 'BLOCK' and 'ALLOW' that uses the Exclusive flag in a
+more specific reference, the 'ALLOW' rule with the Exclusive flag will override
+the 'BLOCK' rule:
+
+----
+  [access "refs/*"]
+    read = block group X
+
+  [access "refs/heads/*"]
+    exclusiveGroupPermissions = read
+    read = group X
+----
+
+In this case a user which is a member of the group 'X' will still be allowed to
+read 'refs/heads/*'.
+
 [NOTE]
 An 'ALLOW' rule overrides a 'BLOCK' rule only when both of them are
 inside the same access section of the same project. An 'ALLOW' rule in a
@@ -1198,8 +1244,7 @@
 [[capability_accessDatabase]]
 === Access Database
 
-Allow users to access the database using the `gsql` command, and view code
-review metadata refs in repositories.
+Allow users to view code review metadata refs in repositories.
 
 
 [[capability_administrateServer]]
diff --git a/Documentation/check_licenses_test.sh b/Documentation/check_licenses_test.sh
new file mode 100755
index 0000000..52e27f2
--- /dev/null
+++ b/Documentation/check_licenses_test.sh
@@ -0,0 +1,15 @@
+#!/bin/sh
+
+hook=$(pwd)/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
+
+for f in  js_licenses licenses ; do
+  if ! diff -u Documentation/${f}.txt Documentation/${f}.gen.txt  ; then
+     echo ""
+     echo "FAIL: ${f}.txt out of date"
+     echo "to fix: "
+     echo ""
+     echo "  cp bazel-bin/Documentation/${f}.gen.txt Documentation/${f}.txt"
+     echo ""
+     exit 1
+  fi
+done
diff --git a/Documentation/cmd-create-project.txt b/Documentation/cmd-create-project.txt
index 026d7b1..9e3d70b 100644
--- a/Documentation/cmd-create-project.txt
+++ b/Documentation/cmd-create-project.txt
@@ -28,9 +28,8 @@
 == DESCRIPTION
 Creates a new bare Git repository under `gerrit.basePath`, using
 the project name supplied.  The newly created repository is empty
-(has no commits), but is registered in the Gerrit database so that
-the initial commit may be uploaded for review, or initial content
-can be pushed directly into a branch.
+(has no commits), and the initial content may either be uploaded for
+review, or pushed directly to a branch.
 
 If replication is enabled, this command also connects to each of
 the configured remote systems over SSH and uses command line git
@@ -119,7 +118,7 @@
 Defaults to MERGE_IF_NECESSARY unless
 link:config-gerrit.html#repository.name.defaultSubmitType[
 repository.<name>.defaultSubmitType] is set to a different value.
-For more details see link:project-configuration.html#submit_type[
+For more details see link:config-project-config.html#submit-type[
 Submit Types].
 
 --use-content-merge::
diff --git a/Documentation/cmd-flush-caches.txt b/Documentation/cmd-flush-caches.txt
index 55d9083..5a84b9d 100644
--- a/Documentation/cmd-flush-caches.txt
+++ b/Documentation/cmd-flush-caches.txt
@@ -16,7 +16,7 @@
 truth when it needs the information again.
 
 Flushing a cache may be necessary if an administrator modifies
-database records directly in the database, rather than going through
+NoteDb metadata directly in a repository, rather than going through
 the Gerrit web interface.
 
 If no options are supplied, defaults to `--all`.
diff --git a/Documentation/cmd-gsql.txt b/Documentation/cmd-gsql.txt
deleted file mode 100644
index 7f2aaf7..0000000
--- a/Documentation/cmd-gsql.txt
+++ /dev/null
@@ -1,64 +0,0 @@
-= gerrit gsql
-
-== NAME
-gerrit gsql - Administrative interface to active database.
-
-== SYNOPSIS
-[verse]
---
-_ssh_ -p <port> <host> _gerrit gsql_
-  [--format {PRETTY | JSON | JSON_SINGLE}]
-  [-c QUERY]
---
-
-== DESCRIPTION
-Provides interactive query support directly against the underlying
-SQL database used by the host Gerrit server.  All SQL statements
-are supported, including SELECT, UPDATE, INSERT, DELETE and ALTER.
-
-== OPTIONS
---format::
-	Set the format records are output in.  In PRETTY (the
-	default) records are displayed in a tabular output suitable
-	for reading by a human on a sufficiently wide terminal.
-	In JSON mode records are output as JSON objects using the
-	column names as the property names, one object per line.
-	In JSON_SINGLE mode the whole result set is output as a
-	single JSON object.
-
--c::
-	Execute the single query statement supplied, and then exit.
-
-== ACCESS
-Caller must have been granted the
-link:access-control.html#capability_accessDatabase[Access Database]
-global capability.
-
-== SCRIPTING
-Intended for interactive use only, unless format is JSON, or
-JSON_SINGLE.
-
-== EXAMPLES
-To manually correct a user's SSH user name:
-
-----
-$ ssh -p 29418 review.example.com gerrit gsql
-Welcome to Gerrit Code Review v2.0.25
-(PostgreSQL 8.3.8)
-
-Type '\h' for help.  Type '\r' to clear the buffer.
-
-gerrit> update accounts set ssh_user_name = 'alice' where account_id=1;
-UPDATE 1; 1 ms
-gerrit> \q
-Bye
-
-$ ssh -p 29418 review.example.com gerrit flush-caches --cache sshkeys --cache accounts
-----
-
-GERRIT
-------
-Part of link:index.html[Gerrit Code Review]
-
-SEARCHBOX
----------
diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt
index 25099fa..edb54b5 100644
--- a/Documentation/cmd-index.txt
+++ b/Documentation/cmd-index.txt
@@ -71,7 +71,7 @@
 	List projects visible to the caller.
 
 link:cmd-query.html[gerrit query]::
-	Query the change database.
+	Query the change search index.
 
 'gerrit receive-pack'::
 	'Deprecated alias for `git receive-pack`.'
@@ -130,9 +130,6 @@
 link:cmd-gc.html[gerrit gc]::
 	Run the Git garbage collection.
 
-link:cmd-gsql.html[gerrit gsql]::
-	Administrative interface to active database.
-
 link:cmd-index-activate.html[gerrit index activate]::
 	Activate the latest index version available.
 
diff --git a/Documentation/cmd-ls-user-refs.txt b/Documentation/cmd-ls-user-refs.txt
index cba7d1b..0363f60 100644
--- a/Documentation/cmd-ls-user-refs.txt
+++ b/Documentation/cmd-ls-user-refs.txt
@@ -32,8 +32,8 @@
 --user::
 -u::
 	Required; User for which the visible refs should be listed. Gerrit
-	will query the database to find matching users, so the
-	full identity/name does not need to be specified.
+	will query the index to find matching users, so the full
+	identity/name does not need to be specified.
 
 --only-refs-heads::
 	Only list the refs found under refs/heads/*
diff --git a/Documentation/cmd-query.txt b/Documentation/cmd-query.txt
index 79723c5..d0419d7 100644
--- a/Documentation/cmd-query.txt
+++ b/Documentation/cmd-query.txt
@@ -1,7 +1,7 @@
 = gerrit query
 
 == NAME
-gerrit query - Query the change database
+gerrit query - Query the change search index
 
 == SYNOPSIS
 [verse]
@@ -17,6 +17,7 @@
   [--submit-records]
   [--all-reviewers]
   [--start <n> | -S <n>]
+  [--no-limit]
   [--]
   <query>
   [limit:<n>]
@@ -24,7 +25,7 @@
 
 == DESCRIPTION
 
-Queries the change database and returns results describing changes
+Queries the change search index and returns results describing changes
 that match the input query.  More recently updated changes appear
 before older changes, which is the same order presented in the
 web interface.  For each matching change, the result contains data
@@ -101,6 +102,9 @@
 -S::
 	Number of changes to skip.
 
+--no-limit::
+	Return all results, overriding the default limit.
+
 limit:<n>::
 	Maximum number of results to return.  This is actually a
 	query operator, and not a command line option.	If more
diff --git a/Documentation/cmd-review.txt b/Documentation/cmd-review.txt
index 27a39f4..eef47fc 100644
--- a/Documentation/cmd-review.txt
+++ b/Documentation/cmd-review.txt
@@ -25,7 +25,7 @@
 == DESCRIPTION
 Updates the current user's approval status of the specified patch
 sets and/or submits them for merging, sending out email
-notifications and updating the database.
+notifications and updating code review metadata.
 
 Patch sets may be specified in 'CHANGEID,PATCHSET' format, such as
 '8242,2', or 'COMMIT' format.
@@ -142,7 +142,7 @@
 
 Approve the change with commit c0ff33 as "Verified +1"
 ----
-$ ssh -p 29418 review.example.com gerrit review --verified +1 c0ff33
+$ ssh -p 29418 review.example.com gerrit review --verified +1 8242,2
 ----
 
 Approve the change with change number 8242 and patch set 2 as "Code-Review +2"
diff --git a/Documentation/cmd-set-project.txt b/Documentation/cmd-set-project.txt
index 45b31ff..9686230 100644
--- a/Documentation/cmd-set-project.txt
+++ b/Documentation/cmd-set-project.txt
@@ -59,7 +59,7 @@
 
 +
 For more details see
-link:project-configuration.html#submit_type[Submit Types].
+link:config-project-config.html#submit-type[Submit Types].
 
 --content-merge::
     If enabled, Gerrit will try to perform a 3-way merge of text
diff --git a/Documentation/cmd-stream-events.txt b/Documentation/cmd-stream-events.txt
index e8c5213..f0ad460 100644
--- a/Documentation/cmd-stream-events.txt
+++ b/Documentation/cmd-stream-events.txt
@@ -234,6 +234,8 @@
 
 reviewer:: link:json.html#account[account attribute]
 
+adder:: user that added the reviewer as link:json.html#account[account attribute]
+
 eventCreatedOn:: Time in seconds since the UNIX epoch when this event was
 created.
 
diff --git a/Documentation/config-auto-site-initialization.txt b/Documentation/config-auto-site-initialization.txt
index 1be0af9..2253ed0 100644
--- a/Documentation/config-auto-site-initialization.txt
+++ b/Documentation/config-auto-site-initialization.txt
@@ -2,74 +2,41 @@
 
 == Description
 
-Gerrit supports automatic site initialization on server startup
-when Gerrit runs in a servlet container. Both creation of a new site
-and upgrade of an existing site are supported. By default, all packaged
-plugins will be installed when Gerrit is deployed in a servlet container
-and the location of the Gerrit distribution can be determined at
-runtime. It is also possible to install only a subset of packaged
-plugins or not install any plugins.
+Gerrit supports automatic site initialization on server startup when Gerrit runs
+in a servlet container. Both creation of a new site and upgrade of an existing
+site are supported. By default, all packaged plugins will be installed when
+Gerrit is deployed in a servlet container and the location of the Gerrit
+distribution can be determined at runtime. It is also possible to install only a
+subset of packaged plugins or not install any plugins.
 
-This feature may be useful for such setups where Gerrit administrators
-don't have direct access to the database and the file system of the
-server where Gerrit should be deployed and, therefore, cannot perform
-the init from their local machine prior to deploying Gerrit on such a
-server. It may also make deployment and testing in a local servlet
-container faster to set up as the init step could be skipped.
+This feature may be useful for such setups where Gerrit administrators don't
+have direct access to the file system of the server where Gerrit should be
+deployed and, therefore, cannot perform the init from their local machine prior
+to deploying Gerrit on such a server. It may also make deployment and testing in
+a local servlet container faster to set up as the init step could be skipped.
 
 == Gerrit Configuration
 
-The site initialization will be performed only if the `gerrit.init`
-system property exists. The value of the property is not used; only the
-existence of the property matters.
+In order to perform site initialization, define `gerrit.site_path` with the path
+to your site. If the site already exists, this is the only required property.
+If your site does not yet exist, set the `gerrit.init` system property to
+automatically initialize the site.
 
-If the `gerrit.site_path` system property is defined then the init is
-run for that site. The database connectivity, in that case, is defined
-in the `etc/gerrit.config`.
+During initialization, if the `gerrit.install_plugins` property is not defined,
+then all packaged plugins will be installed. If it is defined, then it is parsed
+as a comma-separated list of plugin names to install. If the value is an empty
+string then no plugins will be installed.
 
-`gerrit.site_path` system property must be defined to run the init for
-that site.
+=== Example
 
-[WARNING]
-Defining the `jdbc/ReviewDb` JNDI property for an H2 database under the
-path defined by `gerrit.site_path` will cause an incomplete auto
-initialization and Gerrit will fail to start.
-
-Opening a connection to such a database will create a subfolder under the
-site path folder (in order to create the H2 database) and Gerrit will
-no longer consider that site path to be new and, because of that,
-skip some required initialization steps (for example, Lucene index
-creation). In order to auto initialize Gerrit with an embedded H2
-database use the `gerrit.site_path` to define the location of the review
-site and don't define a JNDI resource with a URL under that path.
-
-If the `gerrit.install_plugins` property is not defined then all packaged
-plugins will be installed. If it is defined then it is parsed as a
-comma-separated list of plugin names to install. If the value is an
-empty string then no plugin will be installed.
-
-=== Example 1
-
-Prepare Tomcat so that a site is initialized at a given path using
-the H2 database (if the site doesn't exist yet) or using whatever
-database is defined in `etc/gerrit.config` of that site:
+Prepare Tomcat so that a site is initialized at a given path (if the site
+doesn't exist yet), installing all packaged plugins.
 
 ----
   $ export CATALINA_OPTS='-Dgerrit.init -Dgerrit.site_path=/path/to/site'
   $ catalina.sh start
 ----
 
-=== Example 2
-
-Assuming the database schema doesn't exist in the database defined
-via the `jdbc/ReviewDb` JNDI property, initialize a new site using that
-database and a given path:
-
-----
-  $ export CATALINA_OPTS='-Dgerrit.init -Dgerrit.init_path=/path/to/site'
-  $ catalina.sh start
-----
-
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/config-cla.txt b/Documentation/config-cla.txt
index 2234808..2c7b194 100644
--- a/Documentation/config-cla.txt
+++ b/Documentation/config-cla.txt
@@ -25,13 +25,15 @@
 ----
 
 Contributor agreements are defined as contributor-agreement sections in
-`project.config`:
+`project.config` of `All-Projects`:
 ----
   [contributor-agreement "Individual"]
     description = If you are going to be contributing code on your own, this is the one you want. You can sign this one online.
     agreementUrl = static/cla_individual.html
     autoVerify = group CLA Accepted - Individual
     accepted = group CLA Accepted - Individual
+    matchProjects = ^/.*$
+    excludeProjects = ^/not/my/project/
 ----
 
 Each `contributor-agreement` section within the `project.config` file must
@@ -75,6 +77,16 @@
 contributor agreement has been accepted. The groups' UUID must also
 appear in the `groups` file.
 
+[[contributor-agreement.name.matchProjects]]contributor-agreement.<name>.matchProjects::
++
+List of project regular expressions identifying projects where the
+agreement is required. Defaults to every project when omitted.
+
+[[contributor-agreement.name.excludeProjects]]contributor-agreement.<name>.excludeProjects::
++
+List of project regular expressions identifying projects where the
+agreement does not apply. Defaults to empty. i.e. no projects excluded.
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 53d976f..f42e9be 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -32,7 +32,7 @@
 === Section accountPatchReviewDb
 
 The AccountPatchReviewDb is a database used to store the user file reviewed
-flags. It co-exists with <<database,ReviewDb>> and link:note-db.html[NoteDb].
+flags.
 
 [[accountPatchReviewDb.url]]accountPatchReviewDb.url::
 +
@@ -46,8 +46,8 @@
 link:pgm-MigrateAccountPatchReviewDb.html[MigrateAccountPatchReviewDb] program.
 Migration cannot be done while the server is running.
 +
-Also note that the db_name has to be a new db and not reusing gerrit's own review database,
-otherwise gerrit's init will remove the table.
+Also note that the db_name has to be a new db and not reusing an old ReviewDb
+database from a former 2.x site, otherwise gerrit's init will remove the table.
 
 ----
 [accountPatchReviewDb]
@@ -273,10 +273,7 @@
 `Become` appears in the top right corner of the page, taking the
 user to a form where they can enter the username of any existing
 user account, and immediately login as that account, without any
-authentication taking place.  This form of authentication is only
-useful for the GWT hosted mode shell, where OpenID authentication
-redirects might be risky to the developer's host computer, and HTTP
-authentication is not possible.
+authentication taking place.
 
 +
 By default, OpenID.
@@ -485,13 +482,12 @@
 +
 When `auth.type` does not normally enable this URL administrators may
 set this to `login/`, allowing users to begin a new web session. This value
-is used as an href in PolyGerrit and the GWT UI, so absolute URLs like
+is used as an href in PolyGerrit, so absolute URLs like
 `https://someotherhost/login` work as well.
 +
 If a ${path} parameter is included, then PolyGerrit will substitute the
 currently viewed path in the link. Be aware that this path will include
 a leading slash, so a value like this might be appropriate: `/login${path}`.
-Note: in the GWT UI this substitution for ${path} is *always* `/`.
 
 [[auth.cookiePath]]auth.cookiePath::
 +
@@ -697,7 +693,9 @@
 allows to limit the memory used by H2 and thus prevent out-of-memory
 caused by the H2 database using too much memory.
 +
-See <<database.h2.cacheSize,database.h2.cacheSize>> for a detailed discussion.
+Technically the H2 cache size is configured using the CACHE_SIZE parameter in
+the H2 JDBC connection URL, as described
+link:http://www.h2database.com/html/features.html#cache_settings[here]
 +
 Default is unset, using up to half of the available memory.
 +
@@ -807,11 +805,10 @@
 +
 Cache entries contain important details of an active user, including
 their display name, preferences, and known email addresses. Entry
-information is obtained from the `accounts` database table.
+information is obtained from NoteDb data in the `All-Users` repo.
 
 +
-If direct updates are made to any of these database tables, this
-cache should be flushed.
+If direct updates are made to `All-Users`, this cache should be flushed.
 
 cache `"adv_bases"`::
 +
@@ -982,6 +979,11 @@
 Caches parsed `rules.pl` contents for each project. This cache uses the same
 size as the `projects` cache, and cannot be configured independently.
 
+cache `"pure_revert"`::
++
+Result of checking if one change or commit is a pure/clean revert of
+another.
+
 cache `"sshkeys"`::
 +
 Caches unpacked versions of user SSH keys, so the internal SSH daemon
@@ -1143,43 +1145,23 @@
 [[change]]
 === Section change
 
-[[change.largeChange]]change.largeChange::
-+
-Number of changed lines from which on a change is considered as a large
-change. The number of changed lines of a change is the sum of the lines
-that were inserted and deleted in the change.
-+
-The specified value is used to visualize the change sizes in the Web UI
-in change tables and user dashboards.
-+
-By default 500.
-
-[[change.updateDelay]]change.updateDelay::
-+
-How often in seconds the web interface should poll for updates to the
-currently open change.  The poller relies on the client's browser
-cache to use If-Modified-Since and respect `304 Not Modified` HTTP
-responses.  This allows for fast polls, often under 8 milliseconds.
-+
-With a configured 30 second delay a server with 4900 active users will
-typically need to dedicate 1 CPU to the update check.  4900 users
-divided by an average delay of 30 seconds is 163 requests arriving per
-second.  If requests are served at \~6 ms response time, 1 CPU is
-necessary to keep up with the update request traffic.  On a smaller
-user base of 500 active users, the default 30 second delay is only 17
-requests per second and requires ~10% CPU.
-+
-If 0 the update polling is disabled.
-+
-Default is 5 minutes.
-
 [[change.allowBlame]]change.allowBlame::
 +
-Allow blame on side by side diff in the GWT UI. If set to false, blame cannot be
-used.
+Allow blame on side by side diff. If set to false, blame cannot be used.
 +
 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.allowedIdentifier]]change.api.allowedIdentifier::
 +
 Change identifier(s) that are allowed on the API. See
@@ -1192,14 +1174,12 @@
 +
 Default is `ALL`.
 
-[[change.allowDrafts]]change.allowDrafts::
+[[change.api.excludeMergeableInChangeInfo]]change.api.excludeMergeableInChangeInfo::
 +
-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.
+If true, the mergeability bit in
+link:rest-api-changes.html#change-info[ChangeInfo] will never be set. It can
+be requested separately through the
+link:rest-api-changes.html#get-mergeable[get-mergeable] endpoint.
 +
 Default is false.
 
@@ -1218,6 +1198,65 @@
 +
 Default is true.
 
+[[change.disablePrivateChanges]]change.disablePrivateChanges::
++
+If set to true, users are not allowed to create private changes.
++
+The default is false.
+
+[[change.largeChange]]change.largeChange::
++
+Number of changed lines from which on a change is considered as a large
+change. The number of changed lines of a change is the sum of the lines
+that were inserted and deleted in the change.
++
+The specified value is used to visualize the change sizes in the Web UI
+in change tables and user dashboards.
++
+By default 500.
+
+[[change.move]]change.move::
++
+Whether the link:rest-api-changes.html#move-change[Move Change] REST
+endpoint is enabled.
++
+The move change functionality has some corner cases with undesired side
+effects. Hence administrators may decide to disable this functionality.
+In particular, if a change that has dependencies on other changes is
+moved to a new branch, and the moved change gets submitted to the new
+branch, the changes on which the change depends are silently merged
+into the new branch, although these changes have not been moved to that
+branch (see details in
+link:https://bugs.chromium.org/p/gerrit/issues/detail?id=9877[issue
+9877]).
++
+By default true.
+
+[[change.replyLabel]]change.replyLabel::
++
+Label name for the reply button. In the user interface an ellipsis (…)
+is appended.
++
+Default is "Reply". In the user interface it becomes "Reply…".
+
+[[change.replyTooltip]]change.replyTooltip::
++
+Tooltip for the reply button. In the user interface a note about the
+keyboard shortcut is appended.
++
+Default is "Reply and score". In the user interface it becomes "Reply
+and score (Shortcut: a)".
+
+[[change.robotCommentSizeLimit]]change.robotCommentSizeLimit::
++
+Maximum allowed size of a robot comment that will be accepted. Robot comments
+which exceed the indicated size will be rejected on addition. The specified
+value is interpreted as the maximum size in bytes of the JSON representation of
+the robot comment. Common unit suffixes of 'k', 'm', or 'g' are supported.
+Zero or negative values allow robot comments of unlimited size.
++
+The default limit is 1024kB.
+
 [[change.showAssigneeInChangesTable]]change.showAssigneeInChangesTable::
 +
 Show assignee field in changes table. If set to false, assignees will
@@ -1225,6 +1264,14 @@
 +
 Default is false.
 
+[[change.strictLabels]]change.strictLabels::
++
+Reject invalid label votes: invalid labels or invalid values. This
+configuration option is provided for backwards compatibility and may
+be removed in future gerrit versions.
++
+Default is false.
+
 [[change.submitLabel]]change.submitLabel::
 +
 Label name for the submit button.
@@ -1258,13 +1305,6 @@
 Default is "Submit all ${topicSize} changes of the same topic (${submitSize}
 changes including ancestors and other changes related by topic)".
 
-[[change.submitWholeTopic]]change.submitWholeTopic::
-+
-Determines if the submit button submits the whole topic instead of
-just the current change.
-+
-Default is false.
-
 [[change.submitTopicLabel]]change.submitTopicLabel::
 +
 If `change.submitWholeTopic` is set and a change has a topic,
@@ -1285,44 +1325,31 @@
 (${submitSize} changes including ancestors and other
 changes related by topic)".
 
-[[change.replyLabel]]change.replyLabel::
+[[change.submitWholeTopic]]change.submitWholeTopic::
 +
-Label name for the reply button. In the user interface an ellipsis (…)
-is appended.
-+
-Default is "Reply". In the user interface it becomes "Reply…".
-
-[[change.replyTooltip]]change.replyTooltip::
-+
-Tooltip for the reply button. In the user interface a note about the
-keyboard shortcut is appended.
-+
-Default is "Reply and score". In the user interface it becomes "Reply
-and score (Shortcut: a)".
-
-[[change.robotCommentSizeLimit]]change.robotCommentSizeLimit::
-+
-Maximum allowed size of a robot comment that will be accepted. Robot comments
-which exceed the indicated size will be rejected on addition. The specified
-value is interpreted as the maximum size in bytes of the JSON representation of
-the robot comment. Common unit suffixes of 'k', 'm', or 'g' are supported.
-Zero or negative values allow robot comments of unlimited size.
-+
-The default limit is 1024kB.
-
-[[change.strictLabels]]change.strictLabels::
-+
-Reject invalid label votes: invalid labels or invalid values. This
-configuration option is provided for backwards compatibility and may
-be removed in future gerrit versions.
+Determines if the submit button submits the whole topic instead of
+just the current change.
 +
 Default is false.
 
-[[change.disablePrivateChanges]]change.disablePrivateChanges::
+[[change.updateDelay]]change.updateDelay::
 +
-If set to true, users are not allowed to create private changes.
+How often in seconds the web interface should poll for updates to the
+currently open change.  The poller relies on the client's browser
+cache to use If-Modified-Since and respect `304 Not Modified` HTTP
+responses.  This allows for fast polls, often under 8 milliseconds.
 +
-The default is false.
+With a configured 30 second delay a server with 4900 active users will
+typically need to dedicate 1 CPU to the update check.  4900 users
+divided by an average delay of 30 seconds is 163 requests arriving per
+second.  If requests are served at \~6 ms response time, 1 CPU is
+necessary to keep up with the update request traffic.  On a smaller
+user base of 500 active users, the default 30 second delay is only 17
+requests per second and requires ~10% CPU.
++
+If 0 the update polling is disabled.
++
+Default is 5 minutes.
 
 [[changeCleanup]]
 === Section changeCleanup
@@ -1354,6 +1381,13 @@
 +
 By default `true`.
 
+[[changeCleanup.cleanupAccountPatchReview]]changeCleanup.cleanupAccountPatchReview::
++
+Whether accountPatchReview data should be also removed when change
+gets auto-abandoned.
++
+By default `false`.
+
 [[changeCleanup.abandonMessage]]changeCleanup.abandonMessage::
 +
 Change message that should be posted when a change is abandoned.
@@ -1433,13 +1467,10 @@
 example, to match the string `bug` in a case insensitive way the match
 pattern `[bB][uU][gG]` needs to be used.
 +
-Between the GWT UI and PolyGerrit, the commentlink.name.match regular
-expressions are applied differently. Whereas in the GWT UI the
-expressions are applied to the formatted and escaped HTML result, the
-PolyGerrit UI applies them only to the raw, unformatted and unescaped
-text form. PolyGerrit does not support regex matching against HTML.
-Comment link patterns that are written in this style should be updated
-to match text formats.
+The commentlink.name.match regular expressions are applied to the raw,
+unformatted and unescaped text form. Regex matching against HTML is not
+supported. Comment link patterns that are written in this style should
+be updated to match text formats.
 +
 A common pattern to match is `bug\\s+(\\d+)`.
 
@@ -1685,215 +1716,6 @@
 +
 Default is 1 hour.
 
-[[database]]
-=== Section database
-
-The database section configures ReviewDb, where Gerrit stores its metadata
-records about account groups and change reviews. Starting from 2.15, accounts
-are always stored in NoteDb and, optionally, changes too. See the
-link:note-db.html[NoteDb documentation] for more information.
-
-Note that user file reviewed flags are stored in a separate database. See the
-<<accountPatchReviewDb,accountPatchReviewDb>> section for more information.
-
-----
-[database]
-  type = POSTGRESQL
-  hostname = localhost
-  database = reviewdb
-  username = gerrit
-  password = s3kr3t
-----
-
-[[database.type]]database.type::
-+
-Type of database server to connect to.  If set this value will be
-used to automatically create correct database.driver and database.url
-values to open the connection.
-+
-* `DB2`
-+
-Connect to a DB2 database server.
-+
-* `DERBY`
-+
-Connect to an Apache Derby database server.
-+
-* `H2`
-+
-Connect to a local embedded H2 database.
-+
-* `JDBC`
-+
-Connect using a JDBC driver class name and URL.
-+
-* `MAXDB`
-+
-Connect to an SAP MaxDB database server.
-+
-* `MYSQL`
-+
-Connect to a MySQL database server.
-+
-* `MARIADB`
-+
-Connect to a MariaDB database server.
-+
-* `ORACLE`
-+
-Connect to an Oracle database server.
-+
-* `POSTGRESQL`
-+
-Connect to a PostgreSQL database server.
-
-+
-If not specified, database.driver and database.url are used as-is,
-and if they are also not specified, defaults to H2.
-
-[[database.hostname]]database.hostname::
-+
-Hostname of the database server.  Defaults to 'localhost'.
-
-[[database.port]]database.port::
-+
-Port number of the database server.  Defaults to the default port
-of the server named by database.type.
-
-[[database.database]]database.database::
-+
-For POSTGRESQL or MYSQL, the name of the database on the server.
-+
-For H2, this is the path to the database, and if not absolute is
-relative to `'$site_path'`.
-
-[[database.username]]database.username::
-+
-Username to connect to the database server as.
-
-[[database.password]]database.password::
-+
-Password to authenticate to the database server with.
-
-[[database.driver]]database.driver::
-+
-Name of the JDBC driver class to connect to the database with.
-Setting this usually isn't necessary as it can be derived from
-database.type or database.url for any supported database.
-
-[[database.url]]database.url::
-+
-'jdbc:' URL for the database.  Setting this variable usually
-isn't necessary as it can be constructed from the all of the
-above properties.
-
-[[database.connectionPool]]database.connectionPool::
-+
-If true, use connection pooling for database connections. Otherwise, a
-new database connection is opened for each request.
-+
-Default is false for MySQL, and true for other database backends.
-
-[[database.poolLimit]]database.poolLimit::
-+
-Maximum number of open database connections.  If the server needs
-more than this number, request processing threads will wait up
-to <<database.poolMaxWait, poolMaxWait>> seconds for a
-connection to be released before they abort with an exception.
-This limit must be several units higher than the total number of
-httpd and sshd threads as some request processing code paths may
-need multiple connections.
-+
-Default is <<sshd.threads, sshd.threads>>
- + <<httpd.maxThreads, httpd.maxThreads>> + 2.
-+
-This setting only applies if
-<<database.connectionPool,database.connectionPool>> is true.
-
-[[database.poolMinIdle]]database.poolMinIdle::
-+
-Minimum number of connections to keep idle in the pool.
-Default is 4.
-+
-This setting only applies if
-<<database.connectionPool,database.connectionPool>> is true.
-
-[[database.poolMaxIdle]]database.poolMaxIdle::
-+
-Maximum number of connections to keep idle in the pool.  If there
-are more idle connections, connections will be closed instead of
-being returned back to the pool.
-Default is min(<<database.poolLimit, database.poolLimit>>, 16).
-+
-This setting only applies if
-<<database.connectionPool,database.connectionPool>> is true.
-
-[[database.poolMaxWait]]database.poolMaxWait::
-+
-Maximum amount of time a request processing thread will wait to
-acquire a database connection from the pool.  If no connection is
-released within this time period, the processing thread will abort
-its current operations and return an error to the client.
-Values should use common unit suffixes to express their setting:
-+
-* ms, milliseconds
-* s, sec, second, seconds
-* m, min, minute, minutes
-* h, hr, hour, hours
-
-+
---
-If a unit suffix is not specified, `milliseconds` is assumed.
-
-Default is `30 seconds`.
-
-This setting only applies if
-<<database.connectionPool,database.connectionPool>> is true.
---
-
-[[database.dataSourceInterceptorClass]]database.dataSourceInterceptorClass::
-
-Class that implements DataSourceInterceptor interface to monitor SQL activity.
-This class must have default constructor and be available on Gerrit's bootstrap
-classpath, e. g. in `$gerrit_site/lib` directory. Example implementation of
-SQL monitoring can be found in javamelody-plugin.
-
-[[database.h2]]database.h2::
-+
-The settings in this section are used for the reviewdb if the
-<<database.type,database.type>> is H2.
-+
-Additionally gerrit uses H2 for storing reviewed flags on changes.
-
-[[database.h2.cacheSize]]database.h2.cacheSize::
-+
-The size of the H2 internal database cache, in bytes. The H2 internal cache for
-persistent H2-backed caches is controlled by
-<<cache.h2CacheSize,cache.h2CacheSize>>.
-+
-H2 uses memory to cache its database content. The parameter `cacheSize`
-allows to limit the memory used by H2 and thus prevent out-of-memory
-caused by the H2 database using too much memory.
-+
-Technically the H2 cache size is configured using the CACHE_SIZE parameter in
-the H2 JDBC connection URL, as described
-link:http://www.h2database.com/html/features.html#cache_settings[here]
-+
-Default is unset, using up to half of the available memory.
-+
-H2 will persist this value in the database, so to unset explicitly specify 0.
-+
-Common unit suffixes of 'k', 'm', or 'g' are supported.
-
-[[database.h2.autoServer]]database.h2.autoServer::
-+
-If `true` enable the automatic mixed mode
-(see link:http://www.h2database.com/html/features.html#auto_mixed_mode[Automatic Mixed Mode]).
-This enables concurrent access to the embedded H2 database from command line
-utils (e.g. MigrateToNoteDb).
-+
-Default is `false`.
-
 [[download]]
 === Section download
 
@@ -2157,10 +1979,20 @@
 by the system administrator, and might not even be running on the
 same host as Gerrit.
 
+[[gerrit.installDbModule]]gerrit.installDbModule::
++
+Repeatable list of class name of additional Guice modules to load at
+Gerrit startup as part of the dbInjector and during the init phases.
+Classes are resolved using the primary Gerrit class loader, hence the
+class needs to be either declared in Gerrit or an additional JAR
+located under the `/lib` directory.
++
+By default unset.
+
 [[gerrit.installModule]]gerrit.installModule::
 +
 Repeatable list of class name of additional Guice modules to load at
-Gerrit startup and init phases.
+Gerrit startup as part of the sysInjector and during the init phases.
 Classes are resolved using the primary Gerrit class loader, hence the
 class needs to be either declared in Gerrit or an additional JAR
 located under the `/lib` directory.
@@ -2172,6 +2004,7 @@
 [gerrit]
   installModule = com.googlesource.gerrit.libmodule.MyModule
   installModule = com.example.abc.OurSpecialSauceModule
+  installDbModule = com.example.def.OurCustomProvider
 ----
 
 [[gerrit.listProjectsFromIndex]]gerrit.listProjectsFromIndex::
@@ -2214,19 +2047,16 @@
 By default unset, meaning no bug report URL will be displayed. Administrators
 should set this to the URL of their issue tracker, if necessary.
 
-[[gerrit.reportBugText]]gerrit.reportBugText::
+[[gerrit.enableReverseDnsLookup]]gerrit.enableReverseDnsLookup::
 +
-Text to be displayed in the link to the bug report URL.
+Enable reverse DNS lookup during computing ref log entry for identified user,
+to record the actual hostname of the user's host in the ref log.
 +
-Only used when `gerrit.reportBugUrl` is set and only supported in GWT (Old UI).
+Enabling reverse DNS lookup can cause performance issues on git push when
+the reverse DNS lookup is slow.
 +
-Defaults to "Report Bug".
-
-[[gerrit.disableReverseDnsLookup]]gerrit.disableReverseDnsLookup::
-+
-Disables reverse DNS lookup during computing ref log entry for identified user.
-+
-Defaults to false.
+Defaults to false, reverse DNS lookup is disabled. The user's IP address
+will be recorded in the ref log rather than their hostname.
 
 [[gerrit.secureStoreClass]]gerrit.secureStoreClass::
 +
@@ -2276,12 +2106,6 @@
 used to create the NoteDb will show in the resulting exception message in case
 the value differs.
 
-[[gerrit.ui]]gerrit.ui::
-+
-Configure which UI is the default. Valid values are `polygerrit` and `gwt`.
-+
-Defaults to `polygerrit`.
-
 [[gitweb]]
 === Section gitweb
 
@@ -2937,8 +2761,7 @@
 +
 Maximum number of leaf terms to allow in a query. Too-large queries may
 perform poorly, so setting this option causes query parsing to fail fast
-before attempting to send them to the secondary index. Should this limit
-be reached, database is used instead of index as applicable.
+before attempting to send them to the secondary index.
 +
 When the index type is `LUCENE`, also sets the maximum number of clauses
 permitted per BooleanQuery. This is so that all enforced query limits
@@ -3680,9 +3503,9 @@
 [[note-db]]
 === Section noteDb
 
-NoteDb is the next generation of Gerrit storage backend, currently powering
-`googlesource.com`. For more information, including how to migrate your data,
-see the link:note-db.html[documentation].
+NoteDb is the Git-based database storage backend for Gerrit. For more
+information, including how to migrate data from an older Gerrit version, see the
+link:note-db.html[documentation].
 
 [[notedb.accounts.sequenceBatchSize]]notedb.accounts.sequenceBatchSize::
 +
@@ -4035,9 +3858,9 @@
 are `INHERIT`, `MERGE_IF_NECESSARY`, `FAST_FORWARD_ONLY`, `REBASE_IF_NECESSARY`,
 `REBASE_ALWAYS`, `MERGE_ALWAYS` and `CHERRY_PICK`.
 +
-For more details see link:project-configuration.html#submit_type[Submit Types].
+For more details see link:config-project-config.html#submit-type[Submit Types].
 +
-Default is link:project-configuration.html#submit_type_inherit[`INHERIT`].
+Default is link:config-project-config.html#submit_type_inherit[`INHERIT`].
 +
 This submit type is only applied at project creation time if a submit type is
 omitted from the link:rest-api-projects.html#project-input[ProjectInput]. If the
@@ -4830,103 +4653,6 @@
 +
 By default 0.
 
-[[theme]]
-=== Section theme
-
-[[theme.backgroundColor]]theme.backgroundColor::
-+
-_(GWT UI only)_ Background color for the page, and major data tables like the all
-open changes table or the account dashboard. The value must be a
-valid HTML hex color code, or standard color name.
-+
-By default white, `FFFFFF`.
-
-[[theme.topMenuColor]]theme.topMenuColor::
-+
-_(GWT UI only)_ This is the color of the main menu bar at the top of the page.
-The value must be a valid HTML hex color code, or standard color
-name.
-+
-By default white, `FFFFFF`.
-
-[[theme.textColor]]theme.textColor::
-+
-_(GWT UI only)_ Text color for the page, and major data tables like the all open
-changes table or the account dashboard. The value must be a valid HTML hex color
-code, or standard color name.
-+
-By default dark grey, `353535`.
-
-[[theme.trimColor]]theme.trimColor::
-+
-_(GWT UI only)_ Primary color used as a background color behind text.  This is
-the color of the main menu bar at the top, of table headers, and of major UI
-areas that we want to offset from other portions of the page.  The value must be
-a valid HTML hex color code, or standard color name.
-+
-By default a light grey, `EEEEEE`.
-
-[[theme.selectionColor]]theme.selectionColor::
-+
-_(GWT UI only)_ Background color used within a trimColor area to denote the
-currently selected tab, or the background color used in a table to denote the
-currently selected row.  The value must be a valid HTML hex color code, or
-standard color name.
-+
-By default a pale blue, `D8EDF9`.
-
-[[theme.changeTableOutdatedColor]]theme.changeTableOutdatedColor::
-+
-_(GWT UI only)_ Background color used for patch outdated messages.  The value
-must be a valid HTML hex color code, or standard color name.
-+
-By default a shade of red, `F08080`.
-
-[[theme.tableOddRowColor]]theme.tableOddRowColor::
-+
-_(GWT UI only)_ Background color for tables such as lists of open reviews for
-odd rows.  This is so you can have a different color for odd and even rows of
-the table.  The value must be a valid HTML hex color code, or standard color
-name.
-+
-By default transparent.
-
-[[theme.tableEvenRowColor]]theme.tableEvenRowColor::
-+
-_(GWT UI only)_ Background color for tables such as lists of open reviews for
-even rows.  This is so you can have a different color for odd and even rows of
-the table.  The value must be a valid HTML hex color code, or standard color
-name.
-+
-By default transparent.
-
-A different theme may be used for signed-in vs. signed-out user status
-by using the "signed-in" and "signed-out" theme sections. Variables
-not specified in a section are inherited from the default theme.
-
-----
-[theme]
-  backgroundColor = FFFFFF
-[theme "signed-in"]
-  backgroundColor = C0C0C0
-[theme "signed-out"]
-  backgroundColor = 00FFFF
-----
-
-As example, here is the theme configuration to have the old green look:
-
-----
-[theme]
-  backgroundColor = FCFEEF
-  textColor = 000000
-  trimColor = D4E9A9
-  selectionColor = FFFFCC
-  topMenuColor = D4E9A9
-  changeTableOutdatedColor = F08080
-[theme "signed-in"]
-  backgroundColor = FFFFFF
-----
-
 [[trackingid]]
 === Section trackingid
 
@@ -5043,54 +4769,6 @@
 link:#schedule-configuration-examples[Schedule examples] can be found
 in the link:#schedule-configuration[Schedule Configuration] section.
 
-[[urlAlias]]
-=== Section urlAlias
-
-[NOTE]
-urlAlias settings are only supported in GWT (Old UI) and only effective
-in GWT pages. Plugins that serve their own pages (e.g. gitiles) may
-ignore these settings. An alternative is serving Gerrit behind a
-link:config-reverseproxy.html[reverse proxy] and configure URL rewriting
-in the proxy configuration.
-
-URL aliases define regular expressions for URL tokens that are mapped
-to target URL tokens.
-
-Each URL alias must be specified in its own subsection. The subsection
-name should be a descriptive name. It must be unique, but is not
-interpreted in any way.
-
-The URL aliases are applied in no particular order. The first matching
-URL alias is used and further matches are ignored.
-
-URL aliases can be used to map plugin screens into the Gerrit URL
-namespace, or to replace Gerrit screens by plugin screens.
-
-Example:
-
-----
-[urlAlias "MyPluginScreen"]
-  match = /myscreen/(.*)
-  token = /x/myplugin/myscreen/$1
-[urlAlias "MyChangeScreen"]
-  match = /c/(.*)
-  token = /x/myplugin/c/$1
-----
-
-[[urlAlias.match]]urlAlias.match::
-+
-A regular expression for a URL token.
-+
-The matched URL token is replaced by `urlAlias.token`.
-
-[[urlAlias.token]]urlAlias.token::
-+
-The target URL token.
-+
-It can contain placeholders for the groups matched by the
-`urlAlias.match` regular expression: `$1` for the first matched group,
-`$2` for the second matched group, etc.
-
 [[submodule]]
 === Section submodule
 
@@ -5239,6 +4917,38 @@
 Assuming that the server is started on `Mon 07:00` then this yields the
 first run on Tuesday at 06:00 and a repetition interval of 1 day.
 
+[[All-Projects-project.config]]
+== File `etc/All-Projects/project.config`
+
+The optional file `'$site_path'/etc/All-Projects/project.config` provides
+defaults for configuration read from
+link:config-project-config.html[`project.config`] in the
+`All-Projects` repo. Unlike `gerrit.config`, this file contains project-type
+configuration rather than server-type configuration.
+
+Most administrators will not need this file, and should instead make commits to
+`All-Projects` to modify global config. However, a separate file can be useful
+when managing multiple Gerrit servers, since pushing changes to defaults using
+Puppet or a similar tool can be easier than scripting git updates to
+`All-Projects`.
+
+The contents of the file are loaded each time the `All-Projects` project is
+reloaded. Updating the file requires either evicting the project cache or
+restarting the server.
+
+Caveats:
+
+* The path from which the file is read corresponds to the name of the repo,
+  which is link:#gerrit.allProjects[configurable].
+* Although the file lives in a directory that shares a name with a repository,
+  this directory is not a Git repository.
+* Only the file `project.config` is read from this directory to provide
+  defaults; any other files in this directory, such as `rules.pl`, are ignored.
+  (This behavior may change in the future.)
+* Group names listed in the access config in this file are resolved to UUIDs
+  using the `groups` file in the repository, not in the config directory. As a
+  result, setting ACLs in this file is not recommended.
+
 [[secure.config]]
 == File `etc/secure.config`
 
diff --git a/Documentation/config-plugins.txt b/Documentation/config-plugins.txt
index 9f7634a..edeec54 100644
--- a/Documentation/config-plugins.txt
+++ b/Documentation/config-plugins.txt
@@ -9,6 +9,10 @@
 link:config-gerrit.html#plugins.checkFrequency[a few minutes] until
 the server picks up new and updated plugins.
 
+Due to caching, you might need to flush your browser cache after
+installing a plugin. Users will usually see the result within
+several minutes.
+
 Plugins can also be installed via
 link:rest-api-plugins.html#install-plugin[REST] and
 link:cmd-plugin-install.html[SSH].
@@ -65,6 +69,18 @@
 link:https://gerrit.googlesource.com/plugins/commit-message-length-validator/+doc/master/src/main/resources/Documentation/config.md[
 Configuration]
 
+[[delete-project]]
+=== delete-project
+
+Provides the ability to delete a project.
+
+link:https://gerrit-review.googlesource.com/admin/repos/plugins/delete-project[
+Project] |
+link:https://gerrit.googlesource.com/plugins/delete-project/+doc/master/src/main/resources/Documentation/about.md[
+Documentation] |
+link:https://gerrit.googlesource.com/plugins/delete-project/+doc/master/src/main/resources/Documentation/config.md[
+Configuration]
+
 [[download-commands]]
 === download-commands
 
@@ -78,6 +94,14 @@
 link:https://gerrit.googlesource.com/plugins/download-commands/+doc/master/src/main/resources/Documentation/config.md[
 Configuration]
 
+[[gitiles]]
+=== gitiles
+
+Plugin running Gitiles alongside a Gerrit server.
+
+link:https://gerrit-review.googlesource.com/admin/repos/plugins/gitiles[
+Project]
+
 [[hooks]]
 === hooks
 
@@ -90,6 +114,20 @@
 link:https://gerrit.googlesource.com/plugins/hooks/+doc/master/src/main/resources/Documentation/config.md[
 Configuration]
 
+[[plugin-manager]]
+=== plugin-manager
+
+This plugins provides an initial wizard to discover and install Gerrit plugins.
+Per default GerritForge CI is used to download the plugin artifacts from, but
+this can be changed per plugin configuration.
+
+link:https://gerrit-review.googlesource.com/admin/repos/plugins/plugin-manager[
+Project]
+link:https://gerrit.googlesource.com/plugins/plugin-manager/+doc/master/src/main/resources/Documentation/about.md[
+Documentation]
+link:https://gerrit.googlesource.com/plugins/plugin-manager/+doc/master/src/main/resources/Documentation/config.md[
+Configuration]
+
 [[replication]]
 === replication
 
@@ -123,6 +161,18 @@
 rights directly to a single user, since in Gerrit access rights can
 only be assigned to groups.
 
+[[webhooks]]
+=== webhooks
+
+This plugin allows to propagate Gerrit events to remote http endpoints.
+
+link:https://gerrit-review.googlesource.com/admin/repos/plugins/webhooks[
+Project] |
+link:https://gerrit.googlesource.com/plugins/webhooks/+doc/master/src/main/resources/Documentation/about.md[
+Documentation] |
+link:https://gerrit.googlesource.com/plugins/webhooks/+doc/master/src/main/resources/Documentation/config.md[
+Configuration]
+
 [[other-plugins]]
 == Other Plugins
 
@@ -199,8 +249,7 @@
 
 This plugin allows the rendering of Git repository branch network in a
 graphical HTML5 Canvas. It is mainly intended to be used as a
-"project link" in a gitweb configuration or by other Gerrit GWT UI
-plugins to be plugged elsewhere in Gerrit.
+"project link" in a gitweb configuration.
 
 link:https://gerrit-review.googlesource.com/admin/repos/plugins/branch-network[
 Project] |
@@ -221,15 +270,16 @@
 link:https://gerrit.googlesource.com/plugins/changemessage/+doc/master/src/main/resources/Documentation/config.md[
 Configuration]
 
-[[delete-project]]
-=== delete-project
+[[checks]]
+=== checks
 
-Provides the ability to delete a project.
+The checks plugin provides a REST API and UI extensions for integrating
+CI systems with Gerrit.
 
-link:https://gerrit-review.googlesource.com/admin/repos/plugins/delete-project[
+link:https://gerrit-review.googlesource.com/admin/repos/plugins/checks[
 Project] |
-link:https://gerrit.googlesource.com/plugins/delete-project/+doc/master/src/main/resources/Documentation/about.md[
-Documentation]
+link:https://gerrit.googlesource.com/plugins/checks/+doc/master/README.md[
+Plugin Documentation]
 
 [[egit]]
 === egit
@@ -257,6 +307,17 @@
 link:https://gerrit.googlesource.com/plugins/emoticons/+doc/master/src/main/resources/Documentation/config.md[
 Configuration]
 
+[[find-owners]]
+=== find-owners
+This plugin provides (1) a change review action button `[FIND OWNERS]`
+that shows owners of changed files to be included as code reviewers, and
+(2) Prolog predicates to make sure that a CL is submittable
+only with owner Code-Review +1 votes.
+
+link:https://gerrit-review.googlesource.com/admin/repos/plugins/find-owners[Project] |
+link:https://gerrit.googlesource.com/plugins/find-owners/+doc/master/src/main/resources/Documentation/about.md[Documentation] |
+link:https://gerrit.googlesource.com/plugins/find-owners/+doc/master/src/main/resources/Documentation/config.md[Configuration]
+
 [[gitblit]]
 === gitblit
 
@@ -273,14 +334,6 @@
 link:https://gerrit-review.googlesource.com/admin/repos/plugins/github[
 Project]
 
-[[gitiles]]
-=== gitiles
-
-Plugin running Gitiles alongside a Gerrit server.
-
-link:https://gerrit-review.googlesource.com/admin/repos/plugins/gitiles[
-Project]
-
 [[healthcheck]]
 === healthcheck
 
@@ -499,13 +552,21 @@
 [[oauth-authentication-provider]]
 === OAuth authentication provider
 This plugin enables Gerrit to use OAuth2 protocol for authentication.
-Two different OAuth providers are supported:
+Several OAuth2 providers are supported:
 
+* AirVantage
+* Bitbucket
+* CAS
+* CoreOS Dex
+* Facebook
 * GitHub
+* GitLab
 * Google
+* Keycloak
+* Office365
 
-https://github.com/davido/gerrit-oauth-provider[Project] |
-https://github.com/davido/gerrit-oauth-provider/wiki/Getting-Started[Configuration]
+link:https://gerrit-review.googlesource.com/admin/repos/plugins/oauth[Project] |
+link:https://gerrit.googlesource.com/plugins/oauth/+doc/master/src/main/resources/Documentation/config.md[Configuration]
 
 [[owners]]
 === owners
@@ -643,6 +704,13 @@
 link:https://gerrit.googlesource.com/plugins/scripting/groovy-provider/+doc/master/src/main/resources/Documentation/about.md[
 Documentation]
 
+[[saml-authentication-provider]]
+=== SAML2 authentication provider
+
+This plugin enables Gerrit to use SAML2 protocol for authentication.
+
+link:https://gerrit-review.googlesource.com/admin/repos/plugins/saml[Project]
+
 [[scala-provider]]
 === scripting/scala-provider
 
diff --git a/Documentation/config-project-config.txt b/Documentation/config-project-config.txt
index f2c7965..71af331 100644
--- a/Documentation/config-project-config.txt
+++ b/Documentation/config-project-config.txt
@@ -96,7 +96,41 @@
 
 These are the keys:
 
-- Description
+[[description]]description::
++
+A description for the project.
+
+[[state]]state::
++
+This setting defines the state of the project. A project can have the
+following states:
+
+- `Active`:
++
+The project is active and users can see and modify the project according
+to their access rights on the project.
+
+- `Read Only`:
++
+The project is read only and all modifying operations on it are
+disabled. E.g. this means that pushing to this project fails for all
+users even if they have push permissions assigned on it.
++
+Setting a project to this state is an easy way to temporary close a
+project, as you can keep all write access rights in place and they will
+become active again as soon as the project state is set back to
+`Active`.
++
+This state also makes sense if a project was moved to another location.
+In this case all new development should happen in the new project and
+you want to prevent that somebody accidentally works on the old
+project, while keeping the old project around for old references.
+
+- `Hidden`:
++
+The project is hidden and only visible to project owners. Other users
+are not able to see the project even if they have read permissions
+granted on the project.
 
 
 [[receive-section]]
@@ -125,11 +159,27 @@
 
 [[receive.requireChangeId]]receive.requireChangeId::
 +
-Controls whether or not the Change-Id must be included in the commit message
-in the last paragraph. Default is `INHERIT`, which means that this property
-is inherited from the parent project. The global default for new hosts
-is `true`
-+
+The `Require Change-Id in commit message` option defines whether a
+link:user-changeid.html[Change-Id] in the commit message is required
+for pushing a commit for review. If this option is set, trying to push
+a commit for review that doesn't contain a Change-Id in the commit
+message fails with link:error-missing-changeid.html[missing Change-Id
+in commit message footer].
+
+It is recommended to set this option and use a
+link:user-changeid.html#create[commit-msg hook] (or other client side
+tooling like EGit) to automatically generate Change-Id's for new
+commits. This way the Change-Id is automatically in place when changes
+are reworked or rebased and uploading new patch sets gets easy.
+
+If this option is not set, commits can be uploaded without a Change-Id,
+but then users have to remember to copy the assigned Change-Id from the
+change screen and insert it manually into the commit message when they
+want to upload a second patch set.
+
+Default is `INHERIT`, which means that this property is inherited from
+the parent project. The global default for new hosts is `true`
+
 This option is deprecated and future releases will behave as if this
 is always `true`.
 
@@ -210,6 +260,25 @@
 Default is `INHERIT`, which means that this property is inherited from
 the parent project.
 
+[[receive.createNewChangeForAllNotInTarget]]receive.createNewChangeForAllNotInTarget::
++
+The `create-new-change-for-all-not-in-target` option provides a
+convenience for selecting link:user-upload.html#base[the merge base]
+by setting it automatically to the target branch's tip so you can
+create new changes for all commits not in the target branch.
+
+This option is disabled if the tip of the push is a merge commit.
+
+This option also only works if there are no merge commits in the
+commit chain, in such cases it fails warning the user that such
+pushes can only be performed by manually specifying
+link:user-upload.html#base[bases]
+
+This option is useful if you want to push a change to your personal
+branch first and for review to another branch for example. Or in cases
+where a commit is already merged into a branch and you want to create
+a new open change for that commit on another branch.
+
 [[change-section]]
 === Change section
 
@@ -250,7 +319,7 @@
 - 'mergeContent': Defines whether to automatically merge changes.  Valid values
 are 'true', 'false', or 'INHERIT'.  Default is 'INHERIT'.
 
-- 'action': defines the link:project-configuration.html#submit_type[submit type].  Valid
+- 'action': defines the link:#submit-type[submit type].  Valid
 values are 'fast forward only', 'merge if necessary', 'rebase if necessary',
 'rebase always', 'merge always' and 'cherry pick'.  The default is 'merge if necessary'.
 
@@ -404,6 +473,100 @@
 You can read more about the +rules.pl+ file and the prolog rules on
 link:prolog-cookbook.html[the Prolog cookbook page].
 
+[[submit-type]]
+=== Submit Type
+
+The method Gerrit uses to submit a change to a project can be
+modified by any project owner through the project console, `Projects` >
+`List` > my/project. In general, a submitted change is only merged if all
+its dependencies are also submitted, with exceptions documented below.
+The following submit types are supported:
+
+[[submit_type_inherit]]
+* Inherit
++
+This is the default for new projects, unless overridden by a global
+link:config-gerrit.html#repository.name.defaultSubmitType[`defaultSubmitType` option].
++
+Inherit the submit type from the parent project. In `All-Projects`, this
+is equivalent to link:#merge_if_necessary[Merge If Necessary].
+
+[[fast_forward_only]]
+* Fast Forward Only
++
+With this method Gerrit does not create merge commits on submitting a
+change. Merge commits may still be submitted, but they must be created
+on the client prior to uploading to Gerrit for review.
++
+To submit a change, the change must be a strict superset of the
+destination branch.  That is, the change must already contain the
+tip of the destination branch at submit time.
+
+[[merge_if_necessary]]
+* Merge If Necessary
++
+If the change being submitted is a strict superset of the destination
+branch, then the branch is fast-forwarded to the change.  If not,
+then a merge commit is automatically created.  This is identical
+to the classical `git merge` behavior, or `git merge --ff`.
+
+[[always_merge]]
+* Always Merge
++
+Always produce a merge commit, even if the change is a strict
+superset of the destination branch.  This is identical to the
+behavior of `git merge --no-ff`, and may be useful if the
+project needs to follow submits with `git log --first-parent`.
+
+[[cherry_pick]]
+* Cherry Pick
++
+Always cherry pick the patch set, ignoring the parent lineage
+and instead creating a brand new commit on top of the current
+branch head.
++
+When cherry picking a change, Gerrit automatically appends onto the
+end of the commit message a short summary of the change's approvals,
+and a URL link back to the change on the web.  The committer header
+is also set to the submitter, while the author header retains the
+original patch set author.
++
+Note that Gerrit ignores dependencies between changes when using this
+submit type unless
+link:config-gerrit.html#change.submitWholeTopic[`change.submitWholeTopic`]
+is enabled and depending changes share the same topic. So generally
+submitters must remember to submit changes in the right order when using this
+submit type. If all you want is extra information in the commit message,
+consider using the Rebase Always submit strategy.
+
+[[rebase_if_necessary]]
+* Rebase If Necessary
++
+If the change being submitted is a strict superset of the destination
+branch, then the branch is fast-forwarded to the change.  If not,
+then the change is automatically rebased and then the branch is
+fast-forwarded to the change.
++
+When Gerrit tries to do a merge, by default the merge will only
+succeed if there is no path conflict.  A path conflict occurs when
+the same file has also been changed on the other side of the merge.
+
+[[rebase_always]]
+* Rebase Always
++
+Basically, the same as Rebase If Necessary, but it creates a new patchset even
+if fast forward is possible AND like Cherry Pick it ensures footers such as
+Change-Id, Reviewed-On, and others are present in resulting commit that is
+merged.
++
+Thus, Rebase Always can be considered similar to Cherry Pick, but with
+the important distinction that Rebase Always does not ignore dependencies.
+
+[[content_merge]]
+=== Allow content merges
+If `Allow content merges` is enabled, Gerrit will try
+to do a content merge when a path conflict occurs.
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/config-robot-comments.txt b/Documentation/config-robot-comments.txt
index cf5de10..0077697 100644
--- a/Documentation/config-robot-comments.txt
+++ b/Documentation/config-robot-comments.txt
@@ -36,7 +36,6 @@
 
 == Limitations
 
-* Robot comments are only supported with NoteDb, but not with ReviewDb.
 * 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-themes.txt b/Documentation/config-themes.txt
index dcfd711..a83c747 100644
--- a/Documentation/config-themes.txt
+++ b/Documentation/config-themes.txt
@@ -4,34 +4,28 @@
 the browser, allowing organizations to alter the look and
 feel of the application to fit with their general scheme.
 
-Configuration can either be sitewide or per-project. Projects without a
-specified theme inherit from their parents, or from the sitewide theme
-for `All-Projects`.
+== HTML Header/Footer and CSS
 
-Sitewide themes are stored in `'$site_path'/etc`, and per-project
-themes are stored in `'$site_path'/themes/{project-name}`. Files are
-only served from a single theme directory; if you want to modify or
-extend an inherited theme, you must copy it into the appropriate
-per-project directory.
-
-== HTML Header/Footer
+The HTML header, footer and CSS may be customized for login
+screens (LDAP, OAuth, OpenId) and the internally managed
+Gitweb servlet.
 
 At startup Gerrit reads the following files (if they exist) and
 uses them to customize the HTML page it sends to clients:
 
-* `<theme-dir>/GerritSiteHeader.html`
+* `etc/GerritSiteHeader.html`
 +
 HTML is inserted below the menu bar, but above any page content.
 This is a good location for an organizational logo, or links to
 other systems like bug tracking.
 
-* `<theme-dir>/GerritSiteFooter.html`
+* `etc/GerritSiteFooter.html`
 +
 HTML is inserted at the bottom of the page, below all other content,
 but just above the footer rule and the "Powered by Gerrit Code
 Review (v....)" message shown at the extreme bottom.
 
-* `<theme-dir>/GerritSite.css`
+* `etc/GerritSite.css`
 +
 The CSS rules are inlined into the top of the HTML page, inside
 of a `<style>` tag.  These rules can be used to support styling
@@ -129,9 +123,7 @@
 
 The `window.onload` callback is necessary to ensure that the
 `Gerrit.on()` function has actually been defined by the
-page.  Because GWT loads the module asynchronously any `<script>`
-block in the header or footer will execute before Gerrit has defined
-the function and is ready to register the hook callback.
+page.
 
 GERRIT
 ------
diff --git a/Documentation/database-setup.txt b/Documentation/database-setup.txt
deleted file mode 100644
index de4fb20..0000000
--- a/Documentation/database-setup.txt
+++ /dev/null
@@ -1,280 +0,0 @@
-[[createdb]]
-== Database Setup
-
-During the init phase of Gerrit you will need to specify which database to use.
-
-[[createdb_h2]]
-=== H2
-
-If you choose H2, Gerrit will automatically set up the embedded H2 database as
-backend so no set up or configuration is necessary.
-
-Using the embedded H2 database is the easiest way to get a Gerrit
-site up and running, making it ideal for proof of concepts or small team
-servers.  On the flip side, H2 is not the recommended option for large
-corporate installations when using ReviewDb. This is because there is no easy way to interact
-with the database while Gerrit is offline, it's not easy to backup the data,
-and it's not possible to set up H2 in a load balanced/hotswap configuration.
-
-If this option interests you, you might want to consider
-link:linux-quickstart.html[the quick guide].
-
-[[createdb_derby]]
-=== Apache Derby
-
-If Derby is selected, Gerrit will automatically set up the embedded Derby
-database as backend so no set up or configuration is necessary.
-
-Currently only support for embedded mode is added. There are two other
-deployment options for Apache Derby that can be added later:
-
-* link:http://db.apache.org/derby/papers/DerbyTut/ns_intro.html#Network+Server+Options[
-Derby Network Server (standalone mode)]
-
-* link:http://db.apache.org/derby/papers/DerbyTut/ns_intro.html#Embedded+Server[
-Embedded Server (hybrid mode)]
-
-[[createdb_postgres]]
-=== PostgreSQL
-
-This option is more complicated than the H2 option but is recommended
-for larger installations. It's the database backend with the largest userbase
-in the Gerrit community.
-
-Create a user for the web application within PostgreSQL, assign it a
-password, create a database to store the metadata, and grant the user
-full rights on the newly created database:
-
-----
-  $ createuser --username=postgres -RDIElPS gerrit
-  $ createdb --username=postgres -E UTF-8 -O gerrit reviewdb
-----
-
-Visit PostgreSQL's link:http://www.postgresql.org/docs/9.1/interactive/index.html[documentation] for further information regarding
-using PostgreSQL.
-
-[[createdb_mysql]]
-=== MySQL
-
-Requirements: MySQL version 5.1 or later.
-
-This option is also more complicated than the H2 option. Just as with
-PostgreSQL it's also recommended for larger installations.
-
-Create a user for the web application within the database, assign it a
-password, create a database, and give the newly created user full
-rights on it:
-
-----
-  mysql
-
-  CREATE USER 'gerrit'@'localhost' IDENTIFIED BY 'secret';
-  CREATE DATABASE reviewdb DEFAULT CHARACTER SET 'utf8';
-  GRANT ALL ON reviewdb.* TO 'gerrit'@'localhost';
-  FLUSH PRIVILEGES;
-----
-
-Visit MySQL's link:http://dev.mysql.com/doc/[documentation] for further
-information regarding using MySQL.
-
-[[createdb_mariadb]]
-=== MariaDB
-
-Requirements: MariaDB version 5.5 or later.
-
-Refer to MySQL section above how to create MariaDB database.
-
-Visit MariaDB's link:https://mariadb.com/kb/en/mariadb/[documentation] for further
-information regarding using MariaDB.
-
-[[createdb_oracle]]
-=== Oracle
-
-PostgreSQL or H2 is the recommended database for Gerrit Code Review.
-Oracle is supported for environments where running on an existing Oracle
-installation simplifies administrative overheads, such as database backups.
-
-Create a user for the web application within sqlplus, assign it a
-password, and grant the user full rights on the newly created database:
-
-----
-  SQL> create user gerrit identified by secret_password default tablespace users;
-  SQL> grant connect, resources to gerrit;
-----
-
-JDBC driver ojdbc6.jar must be obtained from your Oracle distribution. Gerrit
-initialization process tries to copy it from a known location:
-
-----
-/u01/app/oracle/product/11.2.0/xe/jdbc/lib/ojdbc6.jar
-----
-
-If this file can not be located at this place, then the alternative location
-can be provided.
-
-Instance name is the Oracle SID. Sample database section in
-$site_path/etc/gerrit.config:
-
-----
-[database]
-        type = oracle
-        instance = xe
-        hostname = localhost
-        username = gerrit
-        port = 1521
-----
-
-Sample database section in $site_path/etc/secure.config:
-
-----
-[database]
-        password = secret_password
-----
-
-[[createdb_maxdb]]
-=== SAP MaxDB
-
-SAP MaxDB is a supported database for running Gerrit Code Review. However it is
-recommended only for environments where you intend to run Gerrit on an existing
-MaxDB installation to reduce administrative overhead.
-
-In the MaxDB studio or using the SQLCLI command line interface create a user
-'gerrit' with the user class 'RESOURCE' and a password <secret password>. This
-will also create an associated schema on the database.
-
-To run Gerrit on MaxDB, you need to obtain the MaxDB JDBC driver. It can be
-found in your MaxDB installation at the following location:
-
-- on Windows 64bit at "C:\Program Files\sdb\MaxDB\runtime\jar\sapdbc.jar"
-- on Linux at "/opt/sdb/MaxDB/runtime/jar/sapdbc.jar"
-
-It needs to be stored in the 'lib' folder of the review site.
-
-In the following sample database section it is assumed that the database name is
-'reviewdb' and the database is installed on localhost:
-
-In $site_path/etc/gerrit.config:
-
-----
-[database]
-        type = maxdb
-        database = reviewdb
-        hostname = localhost
-        username = gerrit
-
-----
-
-In $site_path/etc/secure.config:
-
-----
-[database]
-        password = <secret password>
-----
-
-Visit SAP MaxDB's link:http://maxdb.sap.com/documentation/[documentation] for further
-information regarding using SAP MaxDB.
-
-[[createdb_db2]]
-=== DB2
-
-IBM DB2 is a supported database for running Gerrit Code Review. However it is
-recommended only for environments where you intend to run Gerrit on an existing
-DB2 installation to reduce administrative overhead.
-
-Create a system wide user for the Gerrit application, and grant the user
-full rights on the newly created database:
-
-----
-  db2 => create database gerrit
-  db2 => connect to gerrit
-  db2 => grant connect,accessctrl,dataaccess,dbadm,secadm on database to gerrit;
-----
-
-JDBC driver db2jcc4.jar and db2jcc_license_cu.jar must be obtained
-from your DB2 distribution. Gerrit initialization process tries to copy
-it from a known location:
-
-----
-/opt/ibm/db2/V10.5/java/db2jcc4.jar
-/opt/ibm/db2/V10.5/java/db2jcc_license_cu.jar
-----
-
-If these files cannot be located at this place, then an alternative location
-can be provided during init step execution.
-
-Sample database section in $site_path/etc/gerrit.config:
-
-----
-[database]
-        type = db2
-        database = gerrit
-        hostname = localhost
-        username = gerrit
-        port = 50001
-----
-
-Sample database section in $site_path/etc/secure.config:
-
-----
-[database]
-        password = secret_password
-----
-
-[[createdb_hana]]
-=== SAP HANA
-
-SAP HANA is a supported database for running Gerrit Code Review. However it is
-recommended only for environments where you intend to run Gerrit on an existing
-HANA installation to reduce administrative overhead.
-
-In the HANA studio or the SAP HANA Web-based Development Workbench create a user
-'GERRIT2' with the role 'RESTRICTED_USER_JDBC_ACCESS' and a password
-<secret password>. This will also create an associated schema on the database.
-As this user would be required to change the password upon first login you might
-want to to disable the password lifetime check by executing
-'ALTER USER GERRIT2 DISABLE PASSWORD LIFETIME'.
-
-To run Gerrit on HANA, you need to obtain the HANA JDBC driver. It can be found
-as described
-link:http://help.sap.com/saphelp_hanaplatform/helpdata/en/ff/15928cf5594d78b841fbbe649f04b4/frameset.htm[here].
-It needs to be stored in the 'lib' folder of the review site.
-
-In the following sample database section it is assumed that HANA is running on
-the host 'hana.host' and listening on port '4242' where a schema/user GERRIT2
-was created:
-
-In $site_path/etc/gerrit.config:
-
-----
-[database]
-        type = hana
-        hostname = hana.host
-        port = 4242
-        username = GERRIT2
-
-----
-
-In order to configure a specific database in a multi-database environment (MDC)
-the database name has to be specified additionally:
-
-In $site_path/etc/gerrit.config:
-
-----
-[database]
-        type = hana
-        hostname = hana.host
-        database = tdb1
-        port = 4242
-        username = GERRIT2
-
-----
-
-In $site_path/etc/secure.config:
-
-----
-[database]
-        password = <secret password>
-----
-
-Visit SAP HANA's link:http://help.sap.com/hana_appliance/[documentation] for
-further information regarding using SAP HANA.
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index b62a0a4d6..7d3dd07 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -9,7 +9,7 @@
 * A JDK for Java 8|9|10|11|...
 * Python 2 or 3
 * Node.js
-* link:https://www.bazel.io/versions/master/docs/install.html[Bazel] directly
+* link:https://docs.bazel.build/versions/master/install.html[Bazel] directly
 or through link:https://github.com/bazelbuild/bazelisk[Bazelisk]
 * Maven
 * zip, unzip
@@ -21,7 +21,7 @@
 [[java-10]]
 ==== Java 10 support
 
-Java 10 is supported through vanilla java toolchain
+Java 10 (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
 provide the path to JDK home:
@@ -100,7 +100,7 @@
 
 === Gerrit Development WAR File
 
-To build the Gerrit web application that includes the PolyGerrit UI:
+To build the Gerrit web application:
 
 ----
   bazel build gerrit
@@ -119,8 +119,8 @@
 [[release]]
 === Gerrit Release WAR File
 
-To build the Gerrit web application that includes the GWT UI, the
-PolyGerrit UI, core plugins and documentation:
+To build the Gerrit web application that includes the PolyGerrit UI,
+core plugins and documentation:
 
 ----
   bazel build release
@@ -134,7 +134,7 @@
 
 === Headless Mode
 
-To build Gerrit in headless mode, i.e. without the PolyGerrit and GWT
+To build Gerrit in headless mode, i.e. without the PolyGerrit UI:
 Web UI:
 
 ----
@@ -149,7 +149,7 @@
 
 === Extension and Plugin API JAR Files
 
-To build the extension, plugin and GWT API JAR files:
+To build the extension, plugin and acceptance-framework JAR files:
 
 ----
   bazel build api
@@ -162,7 +162,8 @@
   bazel-bin/api.zip
 ----
 
-Install {extension,plugin,gwt}-api to the local maven repository:
+Install {extension,plugin,acceptance-framework}-api to the local
+maven repository:
 
 ----
   tools/maven/api.sh install
@@ -219,7 +220,7 @@
 
 === IntelliJ
 
-The Gerrit build works with Bazel's link:https://ij.bazel.io[IntelliJ plugin].
+The Gerrit build works with Bazel's link:https://ij.bazel.build[IntelliJ plugin].
 Please follow the instructions on <<dev-intellij#,IntelliJ Setup>>.
 
 === Eclipse
@@ -239,7 +240,7 @@
 If an updated classpath is needed, the Eclipse project can be
 refreshed and missing dependency JARs can be downloaded by running
 `project.py` again. For IntelliJ, you need to click the `Sync Project
-with BUILD Files` button of link:https://ij.bazel.io[IntelliJ plugin].
+with BUILD Files` button of link:https://ij.bazel.build[Bazel plugin].
 
 [[documentation]]
 === Documentation
@@ -293,31 +294,6 @@
   bazel test //javatests/com/google/gerrit/acceptance/rest/account:rest_account
 ----
 
-To run the tests against NoteDb backend with write
-to NoteDb, but not read from it:
-
-----
-  bazel test --test_env=GERRIT_NOTEDB=WRITE //...
-----
-
-Write and read from NoteDb:
-
-----
-  bazel test --test_env=GERRIT_NOTEDB=READ_WRITE //...
-----
-
-Primary storage NoteDb:
-
-----
-  bazel test --test_env=GERRIT_NOTEDB=PRIMARY //...
-----
-
-Primary storage NoteDb and ReviewDb disabled:
-
-----
-  bazel test --test_env=GERRIT_NOTEDB=ON //...
-----
-
 To run only tests that do not use SSH:
 
 ----
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index 42a7f1b..5033d39 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -255,7 +255,6 @@
 Here are some design level objectives that you should keep in mind
 when coding:
 
-  * ORM entity objects should match exactly one row in the database.
   * 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
@@ -274,11 +273,11 @@
   * 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 the DB.  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.
-  * GWT EventBus is the new way forward.
+    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).
 
 
@@ -327,31 +326,44 @@
 [[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:
+
+  * 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.
+
 === 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
-patchsets to these branches, even if no new release is planned.
+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.  It will then be included in
-the master branch when the stable branch is merged back.
-
-=== Updating to new version of GWT
-
-When updating to a new version of GWT, there are several things that also need
-to be updated or at least checked.
-
-* Update common and plugin dependencies in `tools/gwt-constants.defs`.
-* Update to the same GWT version in the cookbook plugin and optionally in other
-plugins that have a dependency on GWT.
-* Update the GWT version in the archetype metadata in the
-`gerrit-plugin-gwt-archetype`.
-* Update the version of `gwt-maven-plugin` in the example pom.xml file in
-link:dev-plugins.html[dev-plugins].
-* Update to the same GWT version in the `gwtjsonrpc` project, and release a
-new version.
+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.
 
 === Finding starter projects to work on
 
@@ -384,20 +396,21 @@
 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.
+
+  * 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
diff --git a/Documentation/dev-design.txt b/Documentation/dev-design.txt
index bdd2a68..69af18d 100644
--- a/Documentation/dev-design.txt
+++ b/Documentation/dev-design.txt
@@ -65,6 +65,9 @@
 changing the implementation from Python on Google App Engine, to Java
 on a J2EE servlet container and an SQL database.
 
+Since Gerrit 3.x link:note-db.html[NoteDb] replaced the SQL database
+and all metadata is now stored in Git.
+
 * link:http://video.google.com/videoplay?docid=-8502904076440714866[Mondrian Code Review On The Web]
 * link:https://github.com/rietveld-codereview/rietveld[Rietveld - Code Review for Subversion]
 * link:http://eagain.net/gitweb/?p=gitosis.git;a=blob;f=README.rst;hb=HEAD[Gitosis README]
@@ -83,9 +86,7 @@
 
 Each Git commit created on the client desktop system is converted
 into a unique change record which can be reviewed independently.
-Change records are stored in a database: PostgreSQL, MySQL, or the
-built-in H2, where they can be queried to present customized user
-dashboards, enumerating any pending changes.
+Change records are stored in NoteDb.
 
 A summary of each newly uploaded change is automatically emailed
 to reviewers, so they receive a direct hyperlink to review the
@@ -121,9 +122,9 @@
 
 End-user web browsers make HTTP requests directly to Gerrit's
 HTTP server.  As nearly all of the user interface is implemented
-through Google Web Toolkit (GWT), the majority of these requests
-are transmitting compressed JSON payloads, with all HTML being
-generated within the browser.  Most responses are under 1 KB.
+through PolyGerrit, the majority of these requests are transmitting
+compressed JSON payloads, with all HTML being generated within the
+browser.  Most responses are under 1 KB.
 
 Gerrit's HTTP server side component is implemented as a standard
 Java servlet, and thus runs within any J2EE servlet container.
@@ -166,9 +167,7 @@
 requires that the OpenID provider selected by a user must be
 online and operating in order to authenticate that user.
 
-* link:http://www.gwtproject.org/[Google Web Toolkit (GWT)]
 * link:http://www.kernel.org/pub/software/scm/git/docs/gitrepository-layout.html[Git Repository Format]
-* link:http://www.postgresql.org/about/[About PostgreSQL]
 * link:http://openid.net/developers/specs/[OpenID Specifications]
 
 *1  Although an effort is underway to eliminate the use of the
@@ -200,11 +199,6 @@
 and comments in English, and therefore an English user interface
 is usable by the target user base.
 
-Gerrit uses GWT's i18n support to externalize all constant strings
-and messages shown to the user, so that in the future someone who
-really needed a translated version of the UI could contribute new
-string files for their locale(s).
-
 Right-to-left (RTL) support is only barely considered within the
 Gerrit code base.  Some portions of the code have tried to take
 RTL into consideration, while others probably need to be modified
@@ -235,20 +229,11 @@
 
 Supporting non-JavaScript enabled browsers is a non-goal for Gerrit.
 
-As Gerrit is a pure-GWT application with no server side rendering
-fallbacks, the browser must support modern JavaScript semantics in
-order to access the Gerrit web application.  Dumb clients such as
-`lynx`, `wget`, `curl`, or even many search engine spiders are not
-able to access Gerrit content.
-
-As Google Web Toolkit (GWT) is used to generate the browser
-specific versions of the client-side JavaScript code, Gerrit works
-on any JavaScript enabled browser which GWT can produce code for.
-This covers the majority of the popular browsers.
-
-The Gerrit project does not have the development resources necessary
-to support two parallel UI implementations (GWT based JavaScript
-and server-side rendering).  Consequently only one is implemented.
+As Gerrit is a pure JavaScript application on the client side, with
+no server side rendering fallbacks, the browser must support modern
+JavaScript semantics in order to access the Gerrit web application.
+Dumb clients such as `lynx`, `wget`, `curl`, or even many search engine
+spiders are not able to access Gerrit content.
 
 There are number of web browsers available with full JavaScript
 support, and nearly every operating system (including any PDA-like
@@ -317,34 +302,6 @@
 Gerrit does not integrate with any Google service, or any other
 services other than those listed above.
 
-
-== Standards / Developer APIs
-
-Gerrit uses an XSRF protected variant of JSON-RPC 1.1 to communicate
-between the browser client and the server.
-
-As the protocol is not the GWT-RPC protocol, but is instead a
-self-describing standard JSON format it is easily implemented by
-any 3rd party client application, provided the client has a JSON
-parser and HTTP client library available.
-
-As the entire command set necessary for the standard web browser
-based UI is exposed through JSON-RPC over HTTP, there are no other
-data feeds or command interfaces to the server.
-
-Commands requiring user authentication may require the user agent to
-complete a sign-in cycle through the user's OpenID provider in order
-to establish the HTTP cookie Gerrit uses to track user identity.
-Automating this sign-in process for non-web browser agents is
-outside of the scope of Gerrit, as each OpenID provider uses its own
-sign-in sequence.  Use of OpenID providers which have difficult to
-automate interfaces may make it impossible for non-browser agents
-to be used with the JSON-RPC interface.
-
-* link:http://json-rpc.org/wd/JSON-RPC-1-1-WD-20060807.html[JSON-RPC 1.1]
-* link:https://gerrit.googlesource.com/gwtjsonrpc/+/master/README[XSRF JSON-RPC]
-
-
 == Privacy Considerations
 
 Gerrit stores the following information per user account:
@@ -378,7 +335,7 @@
 project's mailing list archives.
 
 The user's name and email address is stored unencrypted in the
-Gerrit metadata store, typically a PostgreSQL database.
+link:config-accounts.html#all-users[All-Users] repository.
 
 The snail-mail mailing address, country, and phone and fax numbers
 are gathered to help project leads contact the user should there
@@ -648,12 +605,6 @@
 
 === Backups
 
-PostgreSQL and MySQL can be configured to replicate their data to
-other systems, where they are applied to a warm-standby backup in
-real time.  Gerrit instances which care about redundancy will setup
-this feature of PostgreSQL or MySQL to ensure the warm-standby is
-reasonably current should the master go offline.
-
 Using the standard replication plugin, Gerrit can be configured
 to replicate changes made to the local Git repositories over any
 standard Git transports. After the plugin is installed, remote
diff --git a/Documentation/dev-eclipse.txt b/Documentation/dev-eclipse.txt
index daabbf3..d038700 100644
--- a/Documentation/dev-eclipse.txt
+++ b/Documentation/dev-eclipse.txt
@@ -1,11 +1,9 @@
 = Gerrit Code Review - Eclipse Setup
 
 This document is about configuring Gerrit Code Review into an
-Eclipse workspace for development and debugging with GWT.
+Eclipse workspace for development.
 
-Java 8 or later SDK is also required to run GWT's compiler and
-runtime debugging environment.
-
+Java 8 or later SDK is required.
 
 [[setup]]
 == Project Setup
@@ -104,61 +102,6 @@
 * Change Save as to be Local file.
 * Close the Debug Configurations dialog and save the changes when prompted.
 
-
-=== Running GWT Debug Mode
-
-The `gerrit_gwt_debug` launch configuration uses GWT's
-link:http://www.gwtproject.org/articles/superdevmode.html[Super Dev Mode].
-
-* Make a local copy of the `gerrit_gwt_debug` configuration, using the
-process described for `gerrit_daemon` above.
-* Launch the local copy of `gerrit_gwt_debug` from the Eclipse debug menu.
-* If debugging GWT for the first time:
-
-** Open the link:http://localhost:9876/[codeserver URL] and add the `Dev Mode On`
-and `Dev Mode Off` bookmarklet to your bookmark bar.
-
-** Activate the source maps feature in your browser. Refer to the
-link:https://developer.chrome.com/devtools/docs/javascript-debugging#source-maps[
-Chrome] and
-link:https://developer.mozilla.org/en-US/docs/Tools/Debugger#Use_a_source_map[
-Firefox] developer documentation.
-
-* Load the link:http://localhost:8080[Gerrit page].
-* Open the source tab in developer tools.
-* Click the `Dev Mode On` bookmark to incrementally recompile changed files.
-* Select the `gerrit_ui` module to compile (the `Compile` button can also be used
-as a bookmarklet).
-* In the developer tools source tab, open a file and set a breakpoint.
-* Navigate to the UI and confirm that the breakpoint is hit.
-* To end the debugging session, click the `Dev Mode Off` bookmark.
-
-.After changing the client side code:
-
-* Hitting `F5` in the browser only reloads the last compile output, without
-recompiling.
-* To reflect your changes in the debug session, click `Dev Mode On` then `Compile`.
-
-
-=== Running GWT Debug Mode for Gerrit plugins
-
-A Gerrit plugin can expose GWT module and its implementation can be inspected
-in the SDM debug session.
-
-`codeserver` needs two additional inputs to expose the plugin module in the SDM
-debug session: the module name and the source folder location. For example the
-module name and source folder of any GWT plugin should be added in the local
-copy of the `gerrit_gwt_debug` configuration:
-
-----
-  com.googlesource.gerrit.plugins.myplugin.HelloForm \
-  -src ${resource_loc:/gerrit}/plugins/myplugin/src/main/java \
-  -- --console-log [...]
-----
-
-After doing that, both the Gerrit core and plugin GWT modules can be activated
-during SDM (debug session).
-
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-inspector.txt b/Documentation/dev-inspector.txt
index 2134f2f..b1559ca 100644
--- a/Documentation/dev-inspector.txt
+++ b/Documentation/dev-inspector.txt
@@ -54,8 +54,6 @@
 ----
 "Shell" is "com.google.gerrit.pgm.shell.JythonShell@61644f2d"
 "m" is "com.google.gerrit.lifecycle.LifecycleManager@6f03b248"
-"ds" is "com.google.gerrit.server.schema.DataSourceProvider@6b3592c"
-"schk" is "com.google.gerrit.server.schema.SchemaVersionCheck@5e8cb9bd"
 
 Welcome to the Gerrit Inspector
 Enter help() to see the above again, EOF to quit and stop Gerrit
@@ -109,71 +107,11 @@
 'registerNatives', 'toString', 'wait']
 ----
 
-Startup script provides some convenient variables to access some global Gerrit components,
-for example a connection to the review database is kept open:
-
-----
->>> ds
-org.apache.commons.dbcp.BasicDataSource@61db2215
->>> ds.driverClassName
-u'org.postgresql.Driver'
->>> ds.dataSource
-org.apache.commons.dbcp.PoolingDataSource@23226fe1
->>> ds.dataSource.connection
-jdbc:postgresql://localhost/reviewdb, UserName=rv, PostgreSQL Native Driver
-----
-
-It is also possible to interact with the ORM layer:
-
-----
->>> db = schk.schema.open()
->>> db
-com.google.gerrit.reviewdb.server.ReviewDb_Schema_GwtOrm$$28@24cbbdf3
->>> db.getDialect()
-com.google.gwtorm.schema.sql.DialectPostgreSQL@4de07d3e
->>> for x in db.patchSets().iterateAllEntities():
-...     print x
-...
-[PatchSet 1,1]
-[PatchSet 2,1]
-[PatchSet 3,1]
-[PatchSet 4,1]
-[PatchSet 5,1]
-[PatchSet 6,1]
-[PatchSet 7,1]
-[PatchSet 8,1]
-[PatchSet 6,2]
->>> for x in db.patchComments().iterateAllEntities():
-...     print x
-com.google.gerrit.reviewdb.client.PatchLineComment@5381298a
-com.google.gerrit.reviewdb.client.PatchLineComment@44ce4dda
-com.google.gerrit.reviewdb.client.PatchLineComment@44594680
->>> dir(com.google.gerrit.reviewdb.client.PatchLineComment)
-['Key', 'STATUS_DRAFT', 'STATUS_PUBLISHED', 'Status', '__class__',
-'__copy__', '__deepcopy__', '__delattr__', '__doc__', '__eq__',
-'__getattribute__', '__hash__', '__init__', '__ne__', '__new__',
-'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__str__',
-'__unicode__', 'author', 'class', 'clone', 'equals', 'finalize',
-'getAuthor', 'getClass', 'getKey', 'getLine', 'getMessage',
-'getParentUuid', 'getSide', 'getStatus', 'getWrittenOn', 'hashCode',
-'key', 'line', 'lineNbr', 'message', 'notify', 'notifyAll',
-'parentUuid', 'registerNatives', 'setMessage', 'setSide', 'setStatus',
-'side', 'status', 'toString', 'updated', 'wait', 'writtenOn']
->>> for x in db.patchComments().iterateAllEntities():
-...     print x.status, x.line, x.message
-...
-P 2 I like it!
-P 2 more
-P 1 better
-----
-
 A built-in *help()* function provides values of global variables
 defined in the interpreter:
 
 ----
 >>> help()
-"schk" is "com.google.gerrit.server.schema.SchemaVersionCheck@5e8cb9bd"
-"ds" is "com.google.gerrit.server.schema.DataSourceProvider@6b3592c"
 "m" is "com.google.gerrit.lifecycle.LifecycleManager@6f03b248"
 "Shell" is "com.google.gerrit.pgm.shell.JythonShell@61644f2d"
 "d" is "com.google.gerrit.pgm.Daemon@28a3f689"
diff --git a/Documentation/dev-intellij.txt b/Documentation/dev-intellij.txt
index 8bedd08..5077079 100644
--- a/Documentation/dev-intellij.txt
+++ b/Documentation/dev-intellij.txt
@@ -1,10 +1,24 @@
-= Gerrit Code Review - IntelliJ Setup
+= Gerrit Code Review - IntelliJ IDEA Setup
 
 == Prerequisites
-You need an installation of IntelliJ version 2016.2 or later. The latest version
-might not yet be in-sync with the Bazel plugin for IntelliJ. It usually becomes
-so quite quickly after new IDEA versions get released, though. It should then be
-possible to use the fairly latest IntelliJ release with an updated Bazel plugin.
+
+=== Bazel
+
+Bazel must be installed as described by
+<<dev-bazel#installation,Building with Bazel - Installation>>.
+
+It's strongly recommended to verify you can build your Gerrit tree with Bazel
+for Java 8 from the command line first. Ensure that at least
+`bazel build gerrit` runs successfully before you proceed.
+
+=== IntelliJ version and Bazel plugin
+
+Before downloading IntelliJ, look at the
+link:https://plugins.jetbrains.com/plugin/8609-bazel/versions[JetBrains plugin repository page of the Bazel plugin]
+to see what version of the IntelliJ IDEA it is actually compatible with.
+
+Also note that the version of the Bazel plugin used in turn may or may not be
+compatible with the Bazel version used.
 
 In addition, Java 8 must be specified on your path or via `JAVA_HOME` so that
 building with Bazel via the Bazel plugin is possible.
@@ -13,39 +27,64 @@
 plugin fails and IntelliJ reports the error **Could not get Bazel roots**, this
 indicates that the Bazel plugin couldn't find Java 8.
 
-Bazel must be installed as described by
-<<dev-bazel#installation,Building with Bazel - Installation>>.
+=== Installation of IntelliJ IDEA
+
+Please refer to the
+link:https://www.jetbrains.com/help/idea/installation-guide.html[installation guide provided by Jetbrains]
+to install it on your platform. Make sure to install a version compatible with
+the Bazel plugin as mentioned above.
 
 == Installation of the Bazel plugin
 
-. Go to *File -> Settings -> Plugins*.
-. Click on *Browse Repositories*.
-. Search for the plugin `IntelliJ with Bazel`.
-. Install it.
-. Restart IntelliJ.
+The plugin is usually installed using the Jetbrains plugin repository as shown
+in the steps below, but it's also possible to
+link:https://github.com/bazelbuild/intellij[build it from source].
 
-== Creation of IntelliJ project
+. Go to *File -> Settings -> Plugins*.
++
+(Or, from the welcome screen, *Configure -> Plugins*)
+. Activate the *Marketplace* tab.
+. Search for the plugin `Bazel` (by Google).
++
+TIP: In case the Bazel plugin is not listed, or if it shows an outdated version,
+verify the compatibility between the Bazel plugin and IntelliJ IDEA on link:https://plugins.jetbrains.com/plugin/8609-bazel/versions[the JetBrains plugin page].
+. Install it.
+. Restart IntelliJ IDEA.
+
+[TIP]
+====
+If your project's Bazel build fails with **Cannot run program "bazel": No such
+file or directory**, then you may have to set the binary location in the Bazel
+plugin settings:
+
+. Go to *Preferences -> Other Settings -> Bazel Settings*.
+. Set the *Bazel binary location*.
+====
+
+== Creation of the project
 
 . Go to *File -> Import Bazel Project*.
++
+(Or, from the welcome screen, *Import Bazel Project* should already be shown in
+there.)
 . For *Use existing bazel workspace -> Workspace*, select the directory
 containing the Gerrit source code.
 . Choose *Import from workspace* and select the `.bazelproject` file which is
 located in the top directory of the Gerrit source code.
 . Adjust the path of the project data directory and the name of the project if
 desired.
+. Finish the creation of the project.
+. Verify that you can now build the project. Hit the button with the Bazel icon
+(located on the top-right by default) to synchronize the project. Note that
+warnings may be present in the build.
+
+At this point all the basic functionality should be working such as Java class
+inspection and running <<unit-tests,unit tests>>.
 
 TIP: The project data directory can be separate from the source code. One
 advantage of this is that project files don't need to be excluded from version
 control.
 
-Unfortunately, the created project seems to have a broken output path. To fix
-it, please complete the following steps:
-
-. Go to *File -> Project Structure -> Project Settings -> Modules*.
-. Switch to the tab *Paths*.
-. Click on *Inherit project compile output path*.
-. Click on *Use module compile output path*.
-
 == Recommended settings
 
 === Code style
@@ -54,17 +93,20 @@
 Install the `google-java-format` plugin by following these steps:
 
 . Go to *File -> Settings -> Plugins*.
-. Click on *Browse Repositories*.
-. Search for the plugin `google-java-format`.
+. Activate the *Marketplace* tab.
+. Search for the plugin `google-java-format` by Google.
 . Install it.
-. Restart IntelliJ.
+. Restart IntelliJ IDEA.
 
-Every time you start IntelliJ, make sure to use *Code -> Reformat with
+Every time you start IntelliJ IDEA, make sure to use *Code -> Reformat with
 google-java-format* on an arbitrary line of code. This replaces the default
 CodeStyleManager with a custom one. Thus, uses of *Reformat Code* either via
 *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>>
+for which version of `google-java-format` is used with Gerrit.
+
 ==== Code style settings
 The `google-java-format` plugin is the preferred way to format the code. As it
 only kicks in on demand, it's also recommended to have code style settings
@@ -77,11 +119,10 @@
 https://raw.githubusercontent.com/google/styleguide/gh-pages/intellij-java-google-style.xml[
 intellij-java-google-style.xml].
 . Go to *File -> Settings -> Editor -> Code Style*.
-. Click on *Manage*.
-. Click on *Import*.
-. Choose `IntelliJ IDEA Code Style XML`.
+. Click on the wrench icon with the tooltip _Show Scheme Actions_.
+. Click on *Import Scheme*.
 . Select the previously downloaded file `intellij-java-google-style.xml`.
-. Make sure that `Google Style` is chosen as *Scheme*.
+. Make sure that `GoogleStyle` is chosen as the current *Scheme*.
 
 In addition, the EditorConfig settings (which ensure a consistent style between
 Eclipse, IntelliJ, and other editors) should be applied on top of that. Those
@@ -90,7 +131,7 @@
 and configured correctly as can be verified by:
 
 . Go to *File -> Settings -> Plugins*.
-. Ensure that the EditorConfig plugin is enabled.
+. Ensure that the *EditorConfig* plugin (by JetBrains) is enabled.
 . Go to *File -> Settings -> Editor -> Code Style*.
 . Ensure that *Enable EditorConfig support* is checked.
 
@@ -98,33 +139,42 @@
 the code style settings, simply confirm that.
 
 === Copyright
-Copy the folder `$(gerrit_source_code)/tools/intellij/copyright` (not just the
+
+. Copy the folder `$(gerrit_source_code)/tools/intellij/copyright` (not just the
 contents) to `$(project_data_directory)/.idea`. If it already exists, replace
-it.
+it. If you didn't select a custom data directory the command could look like
+this, as run from the Gerrit source tree checkout as working directory:
++
+----
+cp -r tools/intellij/copyright .ijwb/.idea/
+----
+. Go to *File -> Settings -> Editor -> Copyright -> Copyright Profiles*.
+. Verify that the *Gerrit Copyright* is now present there.
++
+Only in case it hasn't picked up the copyright profile automatically, import
+the `Gerrit_Copyright.xml` from that folder manually.
 
-=== File header
-By default, IntelliJ adds a file header containing the name of the author and
-the current date to new files. To disable that, follow these steps:
+=== Git integration
+This section is only relevant in case you want to use the Git integration
+plugin in IntelliJ IDEA.
 
-. Go to *File -> Settings -> Editor -> File and Code Templates*.
-. Select the tab *Includes*.
-. Select *File Header*.
-. Remove the template code in the right editor.
-
-=== Commit message
 To simplify the creation of commit messages which are compliant with the
 <<dev-contributing#commit-message,Commit Message>> format, do the following:
 
-. Go to *File -> Settings -> Version Control*.
-. Check *Commit message right margin (columns)*.
-. Make sure that 72 is specified as value.
-. Check *Wrap when typing reaches right margin*.
+. Go to *File -> Settings -> Version Control -> Commit Dialog*.
+. In the *Commit message inspections*, activate the three inspections:
+* *Blank line between subject and body*,
+* *Limit body line* and
+* *Limit subject line*.
+. For the limit line inspections, make sure that 72 is specified as value.
+. For *Limit body line*, tick *Show right margin* and *Wrap when typing reaches
+right margin*.
 
 In addition, you should follow the instructions of
 <<dev-contributing#git_commit_settings,this section>> (if you haven't
 done so already):
 
-* Install the Git hook for the `Change-Id` line.
+* Install the Git commit message hook for the `Change-Id` line.
 * Set up the HTTP access.
 
 Setting up the HTTP access will allow you to commit changes via IntelliJ without
@@ -136,29 +186,19 @@
 choose *Edit Configurations* on the drop-down list of the run configurations
 or go to *Run -> Edit Configurations*.
 
-=== Pre-configured run configurations
+[[runconfigurations-daemon]]
+=== Gerrit Daemon
 
-In order to be able to use the pre-configured run configurations, the following
-steps are necessary:
-
-. Make sure that the folder `runConfigurations` exists within
-`$(project_data_directory)/.idea`. If it doesn't exist, create it.
-. Specify the IntelliJ path variable `GERRIT_TESTSITE`. (This configuration is
-shared among all IntelliJ projects.)
-.. Go to *Settings -> Appearance & Behavior -> Path Variables*.
-.. Click on the *+* to add a new path variable.
-.. Specify `GERRIT_TESTSITE` as name and the path to your local test site as
-value.
-
-The copied run configurations will be added automatically to the available run
-configurations of the IntelliJ project.
-
-==== Gerrit Daemon
-WARNING: At the moment running this configuration results in a
+[WARNING]
+====
+At the moment running this (local) configuration results in a
 `java.io.FileNotFoundException`. To debug a local Gerrit server with IntelliJ,
 use the instructions of <<dev-readme#run_daemon,Running the Daemon>> in
 combination with <<remote-debug,Debugging a remote Gerrit server>>.
 
+(link:https://bugs.chromium.org/p/gerrit/issues/detail?id=11360[Issue 11360])
+====
+
 Copy `$(gerrit_source_code)/tools/intellij/gerrit_daemon.xml` to
 `$(project_data_directory)/.idea/runConfigurations/`.
 
@@ -168,19 +208,17 @@
 NOTE: The <<dev-readme#init,Site Initialization>> has to be completed
 before this run configuration works properly.
 
+[[unit-tests]]
 === Unit tests
 To create run configurations for unit tests, run or debug them via a right-click
 on a method, class, file, or package. The created run configuration is a
-temporary one and can be saved to make it permanent.
+temporary one and can be saved to make it permanent by selecting *Create
+'Bazel test [...]'...* from the context menu.
 
 Normally, this approach generates JUnit run configurations. When the Bazel
 plugin manages a project, it intercepts the creation and creates a Bazel test
 run configuration instead, which can be used just like the standard ones.
 
-TIP: If you would like to execute a test in NoteDb mode, add
-`--test_env=GERRIT_NOTEDB=READ_WRITE` to the *Bazel flags* of your run
-configuration.
-
 [[remote-debug]]
 === Debugging a remote Gerrit server
 If a remote Gerrit server is running and has opened a debug port, you can attach
@@ -188,10 +226,15 @@
 
 . Go to *Run -> Edit Configurations*.
 . Click on the *+* to add a new configuration.
-. Choose *Remote*.
+. Choose *Remote* from the *Templates*.
 . Adjust *Configuration -> Settings -> Host* and *Port*.
 . Start this configuration in `Debug` mode.
 
+TIP: This run configuration dialog also shows the line for the JVM as startup
+flag that you can copy to include in your
+`$(gerrit_test_site)/etc/gerrit.config` in the `[container]` section in order
+to work-around the <<runconfigurations-daemon,local run configuration issue>>.
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 974e370..e481d40 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -322,10 +322,10 @@
 
 Plugins' InitSteps are executed during the "Gerrit Plugin init" phase, after
 the extraction of the plugins embedded in the distribution .war file into
-`$GERRIT_SITE/plugins` and before the DB Schema initialization or upgrade.
+`$GERRIT_SITE/plugins` and before the site initialization or upgrade.
 
-A plugin's InitStep cannot refer to Gerrit's DB Schema or any other Gerrit
-runtime objects injected at startup.
+A plugin's InitStep cannot refer to any Gerrit runtime objects injected at
+startup.
 
 [source,java]
 ----
@@ -451,8 +451,8 @@
 [source,java]
 ----
 import com.google.gerrit.common.EventDispatcher;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.registration.DynamicItem;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
 class MyPlugin {
@@ -466,7 +466,7 @@
   private void postEvent(MyPluginEvent event) {
     try {
       eventDispatcher.get().postEvent(event);
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       // error handling
     }
   }
@@ -715,10 +715,9 @@
 
 [source,java]
 ----
-@Singleton
 public class SampleOperator
     implements ChangeQueryBuilder.ChangeOperatorFactory {
-  public static class MyPredicate extends OperatorChangePredicate<ChangeData> {
+  public static class MyPredicate extends PostFilterPredicate<ChangeData> {
     ...
   }
 
@@ -748,7 +747,6 @@
 new `has:sample_pluginName` operand is shown below:
 
 ====
-  @Singleton
   public class SampleHasOperand implements ChangeHasOperandFactory {
     public static class Module extends AbstractModule {
       @Override
@@ -798,56 +796,112 @@
   }
 ----
 
+=== Calling Command Options ===
+
+Within an OptionHandler, during the processing of an option, plugins can
+provide and call extra parameters on the current command during parsing
+simulating as if they had been passed from the command line originally.
+
+To call additional parameters from within an option handler, instantiate
+the com.google.gerrit.util.cli.CmdLineParser.Parameters class with the
+existing parameters, and then call callParameters() with the additional
+parameters to be parsed. OptionHandlers may optionally pass this class to
+other methods which may then both parse/consume more parameters and call
+additional parameters.
+
+When calling command options not provided by your plugin, there is always
+a risk that the options may not exist, perhaps because the options being
+called are to be provided by another plugin, and said plugin is not
+currently installed. To protect againt this situation, it is possible to
+define an option as being dependent on other options using the
+@RequiresOptions() annotation. If the required options are not all not
+currently present, then the dependent option will not be available or
+visible in the help.
+
+The example below shows a plugin that adds a "--special" option (perhaps
+for use with the Query command) that calls (and requires) the
+"--format json" option.
+
+[source, java]
+----
+public class JsonOutputOptionHandler<T> extends OptionHandler<T> {
+  protected com.google.gerrit.util.cli.CmdLineParser.MyParser myParser;
+
+  public JsonOutputOptionHandler(CmdLineParser parser, OptionDef option, Setter<? super T> setter) {
+    super(parser, option, setter);
+    myParser = (com.google.gerrit.util.cli.CmdLineParser.MyParser) owner;
+  }
+
+  @Override
+  public int parseArguments(org.kohsuke.args4j.spi.Parameters params) throws CmdLineException {
+    new Parameters(params, myParser).callParameters("--format", "json");
+    setter.addValue(true);
+    return 0; // we didn't consume any additional args
+  }
+
+  @Override
+  public String getDefaultMetaVariable() {
+   ...
+  }
+}
+
+@RequiresOptions("--format")
+@Option(
+  name = "--special",
+  usage = "ouptut results using json",
+  handler = JsonOutputOptionHandler.class
+)
+boolean json;
+----
+
 [[query_attributes]]
-=== Query Attributes ===
+=== Change Attributes ===
 
-Plugins can provide additional attributes to be returned in Gerrit queries by
-implementing the ChangeAttributeFactory interface and registering it to the
-ChangeQueryProcessor.ChangeAttributeFactory class in the plugin module's
-'configure()' method. The new attribute(s) will be output under a "plugin"
-attribute in the change query output. This can be further controlled with an
-option registered in the Http and Ssh modules' 'configure*()' methods.
+Plugins can provide additional attributes to be returned from the Get Change and
+Query Change APIs by implementing implementing the `ChangeAttributeFactory`
+interface and adding it to the `DynamicSet` in the plugin module's `configure()`
+method. The new attribute(s) will be output under a `plugin` attribute in the
+change output. This can be further controlled by registering a class containing
+@Option declarations as a `DynamicBean`, annotated with the with HTTP/SSH
+commands on which the options should be available.
 
-The example below shows a plugin that adds two attributes ('exampleName' and
-'changeValue'), to the change query output, when the query command is provided
-the --myplugin-name--all option.
+The example below shows a plugin that adds two attributes (`exampleName` and
+`changeValue`), to the change query output, when the query command is provided
+the `--myplugin-name--all` option.
 
 [source, java]
 ----
 public class Module extends AbstractModule {
   @Override
   protected void configure() {
-    bind(ChangeAttributeFactory.class)
-        .annotatedWith(Exports.named("example"))
+    // Register attribute factory.
+    DynamicSet.bind(binder(), ChangeAttributeFactory.class)
         .to(AttributeFactory.class);
+
+    // Register options for GET /changes/X/change and /changes/X/detail.
+    bind(DynamicBean.class)
+        .annotatedWith(Exports.named(GetChange.class))
+        .to(MyChangeOptions.class);
+
+    // Register options for GET /changes/?q=...
+    bind(DynamicBean.class)
+        .annotatedWith(Exports.named(QueryChanges.class))
+        .to(MyChangeOptions.class);
+
+    // Register options for ssh gerrit query.
+    bind(DynamicBean.class)
+        .annotatedWith(Exports.named(Query.class))
+        .to(MyChangeOptions.class);
   }
 }
 
-public class MyQueryOptions implements DynamicBean {
+public class MyChangeOptions implements DynamicBean {
   @Option(name = "--all", usage = "Include plugin output")
   public boolean all = false;
 }
 
-public static class HttpModule extends HttpPluginModule {
-  @Override
-  protected void configureServlets() {
-    bind(DynamicBean.class)
-        .annotatedWith(Exports.named(QueryChanges.class))
-        .to(MyQueryOptions.class);
-  }
-}
-
-public static class SshModule extends PluginCommandModule {
-  @Override
-  protected void configureCommands() {
-    bind(DynamicBean.class)
-        .annotatedWith(Exports.named(Query.class))
-        .to(MyQueryOptions.class);
-  }
-}
-
 public class AttributeFactory implements ChangeAttributeFactory {
-  protected MyQueryOptions options;
+  protected MyChangeOptions options;
 
   public class PluginAttribute extends PluginDefinedInfo {
     public String exampleName;
@@ -860,9 +914,9 @@
   }
 
   @Override
-  public PluginDefinedInfo create(ChangeData c, ChangeQueryProcessor qp, String plugin) {
+  public PluginDefinedInfo create(ChangeData c, BeanProvider bp, String plugin) {
     if (options == null) {
-      options = (MyQueryOptions) qp.getDynamicBean(plugin);
+      options = (MyChangeOptions) bp.getDynamicBean(plugin);
     }
     if (options.all) {
       return new PluginAttribute(c);
@@ -890,6 +944,23 @@
    ],
     ...
 }
+
+curl http://localhost:8080/changes/1?myplugin-name--all
+
+Output:
+
+{
+  "_number": 1,
+  ...
+  "plugins": [
+    {
+      "name": "myplugin-name",
+      "example_name": "Attribute Example",
+      "change_value": "1"
+    }
+  ],
+  ...
+}
 ----
 
 [[simple-configuration]]
@@ -1299,7 +1370,7 @@
 [[panels]]
 === Panels
 
-GWT plugins can contribute panels to Gerrit screens.
+UI plugins can contribute panels to Gerrit screens.
 
 Gerrit screens define extension points where plugins can add GWT
 panels with custom controls:
@@ -1512,9 +1583,8 @@
   // schedule a build
   [...]
   // update change
-  ReviewDb db = dbProvider.get();
   try (BatchUpdate bu = batchUpdateFactory.create(
-      db, project.getNameKey(), user, TimeUtil.nowTs())) {
+      project.getNameKey(), user, TimeUtil.nowTs())) {
     bu.addOp(change.getId(), new BatchUpdate.Op() {
       @Override
       public boolean updateChange(ChangeContext ctx) {
@@ -1875,317 +1945,6 @@
 ----
 
 
-[[gwt_ui_extension]]
-== GWT UI Extension
-Plugins can extend the Gerrit UI with own GWT code.
-
-A GWT plugin must contain a GWT module file, e.g. `HelloPlugin.gwt.xml`,
-that bundles together all the configuration settings of the GWT plugin:
-
-[source,xml]
-----
-<?xml version="1.0" encoding="UTF-8"?>
-<module rename-to="hello_gwt_plugin">
-  <!-- Inherit the core Web Toolkit stuff. -->
-  <inherits name="com.google.gwt.user.User"/>
-  <!-- Other module inherits -->
-  <inherits name="com.google.gerrit.Plugin"/>
-  <inherits name="com.google.gwt.http.HTTP"/>
-  <!-- Using GWT built-in themes adds a number of static -->
-  <!-- resources to the plugin. No theme inherits lines were -->
-  <!-- added in order to make this plugin as simple as possible -->
-  <!-- Specify the app entry point class. -->
-  <entry-point class="${package}.client.HelloPlugin"/>
-  <stylesheet src="hello.css"/>
-</module>
-----
-
-The GWT module must inherit `com.google.gerrit.Plugin` and
-`com.google.gwt.http.HTTP`.
-
-To register the GWT module a `GwtPlugin` needs to be bound.
-
-If no Guice modules are declared in the manifest, the GWT plugin may
-use auto-registration by using the `@Listen` annotation:
-
-[source,java]
-----
-@Listen
-public class MyExtension extends GwtPlugin {
-  public MyExtension() {
-    super("hello_gwt_plugin");
-  }
-}
-----
-
-Otherwise the binding must be done in an `HttpModule`:
-
-[source,java]
-----
-public class HttpModule extends ServletModule {
-
-  @Override
-  protected void configureServlets() {
-    DynamicSet.bind(binder(), WebUiPlugin.class)
-        .toInstance(new GwtPlugin("hello_gwt_plugin"));
-  }
-}
-----
-
-The HTTP module above must be declared in the `pom.xml` for Maven
-driven plugins:
-
-[source,xml]
-----
-<manifestEntries>
-  <Gerrit-HttpModule>com.googlesource.gerrit.plugins.myplugin.HttpModule</Gerrit-HttpModule>
-</manifestEntries>
-----
-
-The name that is provided to the `GwtPlugin` must match the GWT
-module name compiled into the plugin. The name of the GWT module
-can be explicitly set in the GWT module XML file by specifying
-the `rename-to` attribute on the module. It is important that the
-module name be unique across all plugins installed on the server,
-as the module name determines the JavaScript namespace used by the
-compiled plugin code.
-
-[source,xml]
-----
-<module rename-to="hello_gwt_plugin">
-----
-
-The actual GWT code must be implemented in a class that extends
-`com.google.gerrit.plugin.client.PluginEntryPoint`:
-
-[source,java]
-----
-public class HelloPlugin extends PluginEntryPoint {
-
-  @Override
-  public void onPluginLoad() {
-    // Create the dialog box
-    final DialogBox dialogBox = new DialogBox();
-
-    // The content of the dialog comes from a User specified Preference
-    dialogBox.setText("Hello from GWT Gerrit UI plugin");
-    dialogBox.setAnimationEnabled(true);
-    Button closeButton = new Button("Close");
-    VerticalPanel dialogVPanel = new VerticalPanel();
-    dialogVPanel.setWidth("100%");
-    dialogVPanel.setHorizontalAlignment(VerticalPanel.ALIGN_CENTER);
-    dialogVPanel.add(closeButton);
-
-    closeButton.addClickHandler(new ClickHandler() {
-      public void onClick(ClickEvent event) {
-        dialogBox.hide();
-      }
-    });
-
-    // Set the contents of the Widget
-    dialogBox.setWidget(dialogVPanel);
-
-    RootPanel rootPanel = RootPanel.get(HelloMenu.MENU_ID);
-    rootPanel.getElement().removeAttribute("href");
-    rootPanel.addDomHandler(new ClickHandler() {
-        @Override
-        public void onClick(ClickEvent event) {
-          dialogBox.center();
-          dialogBox.show();
-        }
-    }, ClickEvent.getType());
-  }
-}
-----
-
-This class must be set as entry point in the GWT module:
-
-[source,xml]
-----
-<entry-point class="${package}.client.HelloPlugin"/>
-----
-
-In addition this class must be defined as module in the `pom.xml` for the
-`gwt-maven-plugin` and the `webappDirectory` option of `gwt-maven-plugin`
-must be set to `${project.build.directory}/classes/static`:
-
-[source,xml]
-----
-<plugin>
-  <groupId>org.codehaus.mojo</groupId>
-  <artifactId>gwt-maven-plugin</artifactId>
-  <version>2.7.0</version>
-  <configuration>
-    <module>com.googlesource.gerrit.plugins.myplugin.HelloPlugin</module>
-    <disableClassMetadata>true</disableClassMetadata>
-    <disableCastChecking>true</disableCastChecking>
-    <webappDirectory>${project.build.directory}/classes/static</webappDirectory>
-  </configuration>
-  <executions>
-    <execution>
-      <goals>
-        <goal>compile</goal>
-      </goals>
-    </execution>
-  </executions>
-</plugin>
-----
-
-To attach a GWT widget defined by the plugin to the Gerrit core UI
-`com.google.gwt.user.client.ui.RootPanel` can be used to manipulate the
-Gerrit core widgets:
-
-[source,java]
-----
-RootPanel rootPanel = RootPanel.get(HelloMenu.MENU_ID);
-rootPanel.getElement().removeAttribute("href");
-rootPanel.addDomHandler(new ClickHandler() {
-  @Override
-  public void onClick(ClickEvent event) {
-    dialogBox.center();
-    dialogBox.show();
-  }
-}, ClickEvent.getType());
-----
-
-GWT plugins can come with their own css file. This css file must have a
-unique name and must be registered in the GWT module:
-
-[source,xml]
-----
-<stylesheet src="hello.css"/>
-----
-
-If a GWT plugin wants to invoke the Gerrit REST API it can use
-`com.google.gerrit.plugin.client.rpc.RestApi` to construct the URL
-path and to trigger the REST calls.
-
-Example for invoking a Gerrit core REST endpoint:
-
-[source,java]
-----
-new RestApi("projects").id(projectName).view("description")
-    .put("new description", new AsyncCallback<JavaScriptObject>() {
-
-  @Override
-  public void onSuccess(JavaScriptObject result) {
-    // TODO
-  }
-
-  @Override
-  public void onFailure(Throwable caught) {
-    // never invoked
-  }
-});
-----
-
-Example for invoking a REST endpoint defined by a plugin:
-
-[source,java]
-----
-new RestApi("projects").id(projectName).view("myplugin", "myview")
-    .get(new AsyncCallback<JavaScriptObject>() {
-
-  @Override
-  public void onSuccess(JavaScriptObject result) {
-    // TODO
-  }
-
-  @Override
-  public void onFailure(Throwable caught) {
-    // never invoked
-  }
-});
-----
-
-The `onFailure(Throwable)` of the provided callback is never invoked.
-If an error occurs, it is shown in an error dialog.
-
-In order to be able to do REST calls the GWT module must inherit
-`com.google.gwt.json.JSON`:
-
-[source,xml]
-----
-<inherits name="com.google.gwt.json.JSON"/>
-----
-
-[[screen]]
-== Add Screen
-A link:#gwt_ui_extension[GWT plugin] can link:#top-menu-extensions[add
-a menu item] that opens a screen that is implemented by the plugin.
-This way plugin screens can be fully integrated into the Gerrit UI.
-
-Example menu item:
-[source,java]
-----
-public class MyMenu implements TopMenu {
-  private final List<MenuEntry> menuEntries;
-
-  @Inject
-  public MyMenu(@PluginName String name) {
-    menuEntries = new ArrayList<>();
-    menuEntries.add(new MenuEntry("My Menu", Collections.singletonList(
-      new MenuItem("My Screen", "#/x/" + name + "/my-screen", ""))));
-  }
-
-  @Override
-  public List<MenuEntry> getEntries() {
-    return menuEntries;
-  }
-}
-----
-
-Example screen:
-[source,java]
-----
-public class MyPlugin extends PluginEntryPoint {
-  @Override
-  public void onPluginLoad() {
-    Plugin.get().screen("my-screen", new Screen.EntryPoint() {
-      @Override
-      public void onLoad(Screen screen) {
-        screen.add(new InlineLabel("My Screen");
-        screen.show();
-      }
-    });
-  }
-}
-----
-
-[[user-settings-screen]]
-== Add User Settings Screen
-
-A link:#gwt_ui_extension[GWT plugin] can implement a user settings
-screen that is integrated into the Gerrit user settings menu.
-
-Example settings screen:
-[source,java]
-----
-public class MyPlugin extends PluginEntryPoint {
-  @Override
-  public void onPluginLoad() {
-    Plugin.get().settingsScreen("my-preferences", "My Preferences",
-        new Screen.EntryPoint() {
-          @Override
-          public void onLoad(Screen screen) {
-            screen.setPageTitle("Settings");
-            screen.add(new InlineLabel("My Preferences"));
-            screen.show();
-          }
-    });
-  }
-}
-----
-
-By defining an link:config-gerrit.html#urlAlias[urlAlias] Gerrit
-administrators can map plugin screens into the Gerrit URL namespace or
-even replace Gerrit screens by plugin screens.
-
-Plugins may also programatically add URL aliases in the preferences of
-of a user. This way certain screens can be replaced for certain users.
-E.g. the plugin may offer a user preferences setting for choosing a
-screen that then sets/unsets a URL alias for the user.
-
 [[settings-screen]]
 == Plugin Settings Screen
 
@@ -2686,40 +2445,6 @@
 Disabled plugins can be re-enabled using the
 link:cmd-plugin-enable.html[plugin enable] command.
 
-== Known issues and bugs
-
-=== Error handling in UI when using the REST API
-
-When a plugin invokes a REST endpoint in the UI, it provides an
-`AsyncCallback` to handle the result. At the moment the
-`onFailure(Throwable)` of the callback is never invoked, even if there
-is an error. Errors are always handled by the Gerrit core UI which
-shows the error dialog. This means currently plugins cannot do any
-error handling and e.g. ignore expected errors.
-
-In the following example the REST endpoint would return '404 Not
-Found' if the user has no username and the Gerrit core UI would
-display an error dialog for this. However having no username is
-not an error and the plugin may like to handle this case.
-
-[source,java]
-----
-new RestApi("accounts").id("self").view("username")
-    .get(new AsyncCallback<NativeString>() {
-
-  @Override
-  public void onSuccess(NativeString username) {
-    // TODO
-  }
-
-  @Override
-  public void onFailure(Throwable caught) {
-    // never invoked
-  }
-});
-----
-
-
 [[reviewer-suggestion]]
 == Reviewer Suggestion Plugins
 
@@ -2800,6 +2525,39 @@
     return pluginName + " mycommand";
 ----
 
+[[ssh-command-execution-interception]]
+== SSH Command Execution Interception
+Gerrit provides an extension point that enables plugins to check and
+prevent an SSH command from being run.
+
+[source, java]
+----
+import com.google.gerrit.sshd.SshExecuteCommandInterceptor;
+
+@Singleton
+public class SshExecuteCommandInterceptorImpl implements SshExecuteCommandInterceptor {
+  private final Provider<SshSession> sessionProvider;
+
+  @Inject
+  SshExecuteCommandInterceptorImpl(Provider<SshSession> sessionProvider) {
+    this.sessionProvider = sessionProvider;
+  }
+
+  @Override
+  public boolean accept(String command, List<String> arguments) {
+    if (command.startsWith("gerrit") && !"10.1.2.3".equals(sessionProvider.get().getRemoteAddressAsString())) {
+      return false;
+    }
+    return true;
+  }
+}
+----
+
+And then declare it in your SSH module:
+[source, java]
+----
+  DynamicSet.bind(binder(), SshExecuteCommandInterceptor.class).to(SshExecuteCommandInterceptorImpl.class);
+----
 
 [[pre-submit-evaluator]]
 == Pre-submit Validation Plugins
@@ -2907,40 +2665,6 @@
 }
 ----
 
-[[ssh-command-execution-interception]]
-== SSH Command Execution Interception
-Gerrit provides an extension point that enables plugins to check and
-prevent an SSH command from being run.
-
-[source, java]
-----
-import com.google.gerrit.sshd.SshExecuteCommandInterceptor;
-
-@Singleton
-public class SshExecuteCommandInterceptorImpl implements SshExecuteCommandInterceptor {
-  private final Provider<SshSession> sessionProvider;
-
-  @Inject
-  SshExecuteCommandInterceptorImpl(Provider<SshSession> sessionProvider) {
-    this.sessionProvider = sessionProvider;
-  }
-
-  @Override
-  public boolean accept(String command, List<String> arguments) {
-    if (command.startsWith("gerrit") && !"10.1.2.3".equals(sessionProvider.get().getRemoteAddressAsString())) {
-      return false;
-    }
-    return true;
-  }
-}
-----
-
-And then declare it in your SSH module:
-[source, java]
-----
-  DynamicSet.bind(binder(), SshExecuteCommandInterceptor.class).to(SshExecuteCommandInterceptorImpl.class);
-----
-
 Plugin authors should also consider binding their SubmitRule using a `Gerrit-BatchModule`.
 See link:dev-plugins.html[Batch runtime] for more informations.
 
@@ -2954,6 +2678,94 @@
 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.
 
+[[quota-enforcer]]
+== Quota Enforcer
+
+Gerrit provides an extension point that allows a plugin to enforce quota.
+link:quota.html[This documentation page] has a list of all quota requests that
+Gerrit core issues. Plugins can choose to respond to all or just a subset of
+requests. Some implementations might want to keep track of user quota in buckets,
+others might just check against instance or project state to enforce limits on how
+many projects can be created or how large a repository can become.
+
+Checking against instance state can be racy for concurrent requests as the server does not
+refill tokens if the action fails in a later stage (e.g. database failure). If
+plugins want to guarantee an absolute maximum on a resource, they have to do their own
+book-keeping.
+
+[source, java]
+----
+import com.google.server.quota.QuotaEnforcer;
+
+class ProjectLimiter implements QuotaEnforcer {
+  private final long maxNumberOfProjects = 100;
+  @Override
+  QuotaResponse requestTokens(String quotaGroup, QuotaRequestContext ctx, long numTokens) {
+    if (!"/projects/create".equals(quotaGroup)) {
+      return QuotaResponse.noOp();
+    }
+    // No deduction because we always check against the instance state (racy but fine for
+    // this plugin)
+    if (currentNumberOfProjects() + numTokens > maxNumberOfProjects) {
+      return QuotaResponse.error("too many projects");
+    }
+    return QuotaResponse.ok();
+  }
+
+  @Override
+  QuotaResponse dryRun(String quotaGroup, QuotaRequestContext ctx, long numTokens) {
+    // Since we are not keeping any state in this enforcer, we can simply call requestTokens().
+    return requestTokens(quotaGroup, ctx, numTokens);
+  }
+
+  void refill(String quotaGroup, QuotaRequestContext ctx, long numTokens) {
+    // No-op
+  }
+}
+----
+
+[source, java]
+----
+import com.google.server.quota.QuotaEnforcer;
+
+class ApiQpsEnforcer implements QuotaEnforcer {
+  // AutoRefillingPerUserBuckets is a imaginary bucket implementation that could be based on
+  // a loading cache or a commonly used bucketing algorithm.
+  private final AutoRefillingPerUserBuckets<CurrentUser, Long> buckets;
+  @Override
+  QuotaResponse requestTokens(String quotaGroup, QuotaRequestContext ctx, long numTokens) {
+    if (!quotaGroup.startsWith("/restapi/")) {
+      return QuotaResponse.noOp();
+    }
+    boolean success = buckets.deduct(ctx.user(), numTokens);
+    if (!success) {
+      return QuotaResponse.error("user sent too many qps, please wait for 5 minutes");
+    }
+    return QuotaResponse.ok();
+  }
+
+  @Override
+  QuotaResponse dryRun(String quotaGroup, QuotaRequestContext ctx, long numTokens) {
+    if (!quotaGroup.startsWith("/restapi/")) {
+      return QuotaResponse.noOp();
+    }
+    boolean success = buckets.checkOnly(ctx.user(), numTokens);
+    if (!success) {
+      return QuotaResponse.error("user sent too many qps, please wait for 5 minutes");
+    }
+    return QuotaResponse.ok();
+  }
+
+  @Override
+  void refill(String quotaGroup, QuotaRequestContext ctx, long numTokens) {
+    if (!quotaGroup.startsWith("/restapi/")) {
+      return;
+    }
+    buckets.add(ctx.user(), numTokens);
+  }
+}
+----
+
 
 == SEE ALSO
 
diff --git a/Documentation/dev-readme.txt b/Documentation/dev-readme.txt
index 92d080b..8bf4814 100644
--- a/Documentation/dev-readme.txt
+++ b/Documentation/dev-readme.txt
@@ -93,6 +93,7 @@
 .  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
@@ -187,27 +188,6 @@
 CAUTION: When using the Inspector, be careful not to modify the internal state
 of the system.
 
-=== Querying the database
-
-The embedded H2 database can be queried and updated from the command line. If
-the daemon is not running, run:
-
-----
-  $(bazel info output_base)/external/local_jdk/bin/java \
-     -jar bazel-bin/gerrit.war gsql -d ../gerrit_testsite -s
-----
-
-NOTE: To learn why using `java -jar` isn't sufficient, see
-<<special_bazel_java_version,this explanation>>.
-
-Alternatively, if the daemon is running and the database is in use, use an
-administrator user account to connect over SSH:
-
-----
-  ssh -p 29418 user@localhost gerrit gsql
-----
-
-
 == Switching between branches
 
 When using `git checkout` without `--recurse-submodules` to switch between
diff --git a/Documentation/dev-release-deploy-config.txt b/Documentation/dev-release-deploy-config.txt
index 5f95cb3..5411927 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. `gwtjsonrpc` etc.).
+Bucket to store Gerrit Subproject Artifacts (e.g. `gwtorm` etc.).
 
 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-subproject.txt b/Documentation/dev-release-subproject.txt
index 4886849..c9369b9 100644
--- a/Documentation/dev-release-subproject.txt
+++ b/Documentation/dev-release-subproject.txt
@@ -73,7 +73,7 @@
 * Deploy the new release:
 +
 ----
-  mvn deploy
+  mvn deploy -DperformRelease=true
 ----
 
 * Push the pom change(s) to the project's repository
diff --git a/Documentation/error-changeid-above-footer.txt b/Documentation/error-changeid-above-footer.txt
new file mode 100644
index 0000000..abc0186
--- /dev/null
+++ b/Documentation/error-changeid-above-footer.txt
@@ -0,0 +1,31 @@
+= commit xxxxxxx: Change-Id must be in message footer
+
+With this error message, Gerrit rejects a push of a commit to a project
+if the commit message of the pushed commit contains a Change-Id line that
+is not in the footer (the last paragraph).
+
+To be picked up by Gerrit, a Change-Id must be in the last paragraph
+of a commit message. For details, see link:user-changeid.html[Change-Id Lines].
+
+You can see the commit messages for existing commits in the history
+by doing a link:http://www.kernel.org/pub/software/scm/git/docs/git-log.html[git log].
+
+
+== Change-Id is contained in the commit message but not in the last paragraph
+
+If the Change-Id is contained in the commit message but not in its
+last paragraph, you have to update the commit message and move the
+Change-Id into the last paragraph. How to update the commit message
+is explained link:error-push-fails-due-to-commit-message.html[here].
+
+To avoid confusion due to a Change-Id that was meant to be picked up by
+Gerrit not being picked up, this is an error whether or not the project
+is configured to always require a Change-Id in the commit message.
+
+
+GERRIT
+------
+Part of link:error-messages.html[Gerrit Error Messages]
+
+SEARCHBOX
+---------
diff --git a/Documentation/error-messages.txt b/Documentation/error-messages.txt
index 37eb1f6..b523663 100644
--- a/Documentation/error-messages.txt
+++ b/Documentation/error-messages.txt
@@ -18,6 +18,7 @@
 * link:error-invalid-changeid-line.html[invalid Change-Id line format in commit message footer]
 * link:error-invalid-committer.html[invalid committer]
 * link:error-missing-changeid.html[missing Change-Id in commit message footer]
+* link:error-changeid-above-footer.html[Change-Id must be in commit message footer]
 * link:error-missing-subject.html[missing subject; Change-Id must be in commit message footer]
 * link:error-multiple-changeid-lines.html[multiple Change-Id lines in commit message footer]
 * link:error-no-common-ancestry.html[no common ancestry]
diff --git a/Documentation/error-missing-changeid.txt b/Documentation/error-missing-changeid.txt
index 08f2c09..27bfea5 100644
--- a/Documentation/error-missing-changeid.txt
+++ b/Documentation/error-missing-changeid.txt
@@ -3,13 +3,7 @@
 With this error message Gerrit rejects to push a commit to a project
 which is configured to always require a Change-Id in the commit
 message if the commit message of the pushed commit does not contain
-a Change-Id in the footer (the last paragraph).
-
-This error may happen for different reasons:
-
-. missing Change-Id in the commit message
-. Change-Id is contained in the commit message but not in the last
-  paragraph
+a Change-Id.
 
 You can see the commit messages for existing commits in the history
 by doing a link:http://www.kernel.org/pub/software/scm/git/docs/git-log.html[git log].
@@ -38,17 +32,6 @@
 is explained link:error-push-fails-due-to-commit-message.html[here].
 
 
-== Change-Id is contained in the commit message but not in the last paragraph
-
-To be picked up by Gerrit, a Change-Id must be in the last paragraph
-of a commit message, for details, see link:user-changeid.html[Change-Id Lines].
-
-If the Change-Id is contained in the commit message but not in its
-last paragraph you have to update the commit message and move the
-Change-Id into the last paragraph. How to update the commit message
-is explained link:error-push-fails-due-to-commit-message.html[here].
-
-
 GERRIT
 ------
 Part of link:error-messages.html[Gerrit Error Messages]
diff --git a/Documentation/index.txt b/Documentation/index.txt
index 6011158..557cf90 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -85,6 +85,7 @@
 .. 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]
 
diff --git a/Documentation/install-j2ee.txt b/Documentation/install-j2ee.txt
index 91d73cc..48751b7 100644
--- a/Documentation/install-j2ee.txt
+++ b/Documentation/install-j2ee.txt
@@ -14,9 +14,8 @@
 
 == Installation
 
-* Complete the link:install.html#createdb[database setup] and
-  link:install.html#init[site initialization] tasks described
-  in the standard installation documentation.
+* Complete the link:install.html#init[site initialization] task
+  described in the standard installation documentation.
 
 * Stop the embedded daemon that was automatically started by 'init':
 +
@@ -24,13 +23,6 @@
   review_site/bin/gerrit.sh stop
 ----
 
-* Configure JNDI DataSource 'jdbc/ReviewDb'.
-+
-This DataSource must point to the database you created above.
-Don't forget to ensure your JNDI configuration can load the
-necessary JDBC drivers.  You may wish to ensure connection pooling
-is configured and enabled within the DataSource.
-
 * Deploy the 'gerrit.war' file to your application server.
 +
 The deployment process differs between servers, but typically this
@@ -70,20 +62,8 @@
   java -jar webapps/gerrit.war cat extra/jetty7/gerrit.xml >contexts/gerrit.xml
 ----
 
-Install the required additional libraries by copying them into the
-`'$JETTY_HOME'/lib/ext` directory:
-
-----
-  cp ../review_db/lib/* lib/ext/
-  java -jar webapps/gerrit.war cat lib/commons-dbcp-1.2.2.jar >lib/ext/commons-dbcp-1.2.2.jar
-  java -jar webapps/gerrit.war cat lib/commons-pool-1.5.4.jar >lib/ext/commons-pool-1.5.4.jar
-  java -jar webapps/gerrit.war cat lib/h2-1.2.128.jar >lib/ext/h2-1.2.128.jar
-  java -jar webapps/gerrit.war cat lib/postgresql-8.4-701.jdbc4.jar >lib/ext/postgresql-8.4-701.jdbc4.jar
-----
-
 Edit `'$JETTY_HOME'/contexts/gerrit.xml` to correctly configure
-the database and outgoing SMTP connections, especially the user
-and password fields.
+outgoing SMTP connections.
 
 If OpenID authentication (or certain enterprise single-sign-on
 solutions) is being used, you may need to increase the
diff --git a/Documentation/install.txt b/Documentation/install.txt
index 2f00492..aaefc86f 100644
--- a/Documentation/install.txt
+++ b/Documentation/install.txt
@@ -9,10 +9,6 @@
 +
 Gerrit is not yet compatible with Java 13 or newer at this time.
 
-By default, Gerrit uses link:note-db.html[NoteDB] as the storage backend. (If
-desired, you can _optionally_ use an external database such as MySQL or
-PostgreSQL.)
-
 [[cryptography]]
 == Configure Java for Strong Cryptography
 
@@ -60,15 +56,12 @@
 If you would prefer to build Gerrit directly from source, review
 the notes under link:dev-readme.html[developer setup].
 
-include::database-setup.txt[]
-
 [[init]]
 == Initialize the Site
 
 Gerrit stores configuration files, the server's SSH keys, and the
 managed Git repositories under a local directory, typically referred
-to as `'$site_path'`.  If the embedded H2 database is being used,
-its data files will also be stored under this directory.
+to as `'$site_path'`.
 
 You also have to decide where to store your server side git repositories. This
 can either be a relative path under `'$site_path'` or an absolute path
@@ -93,11 +86,10 @@
 then give ownership of that location to the `'gerrit'` user.
 
 If run from an interactive terminal, the init command will prompt through a
-series of configuration questions, including gathering information
-about the database created above.  If the terminal is not interactive,
-running the init command will choose some reasonable default selections,
-and will use the embedded H2 database. Once the init phase is complete,
-you can review your settings in the file `'$site_path/etc/gerrit.config'`.
+series of configuration questions.  If the terminal is not interactive,
+running the init command will choose some reasonable default selections.
+Once the init phase is complete, you can review your settings in the file
+`'$site_path/etc/gerrit.config'`.
 
 When running the init command, additional JARs might be downloaded to
 support optional selected functionality.  If a download fails a URL will
@@ -217,8 +209,7 @@
         --StartPath=C:\MY\GERRIT\SITE ^
         --StartMode=jvm --StopMode=jvm ^
         --StartClass=com.google.gerrit.launcher.GerritLauncher --StartMethod=daemonStart ^
-        --StopClass=com.google.gerrit.launcher.GerritLauncher --StopMethod=daemonStop ^
-        ++DependsOn=postgresql-x64-9.4
+        --StopClass=com.google.gerrit.launcher.GerritLauncher --StopMethod=daemonStop
 ====
 
 [[customize]]
@@ -255,8 +246,6 @@
 
 == External Documentation Links
 
-* http://www.postgresql.org/docs/[PostgreSQL Documentation]
-* http://dev.mysql.com/doc/[MySQL Documentation]
 * http://www.kernel.org/pub/software/scm/git/docs/git-daemon.html[git-daemon]
 
 
diff --git a/Documentation/intro-project-owner.txt b/Documentation/intro-project-owner.txt
index a5895c5..1f98291 100644
--- a/Documentation/intro-project-owner.txt
+++ b/Documentation/intro-project-owner.txt
@@ -123,6 +123,13 @@
 are a number of link:access-control.html#references_special[special refs]
 and link:access-control.html#references_magic[magic refs].
 
+Gerrit only supports tags that are reachable by any ref not owned by
+Gerrit. This includes branches (refs/heads/*) or custom ref namespaces
+(refs/my-company/*). Tagging a change ref is not supported.
+When filtering tags by visibility, Gerrit performs a reachability check
+and will present the user ony with tags that are reachable by any ref
+they can see.
+
 Access rights can be assigned on a concrete ref, e.g.
 `refs/heads/master` but also on ref patterns and regular expressions
 for ref names.
@@ -209,7 +216,7 @@
 addition you can benefit from Gerrit's merge strategies that can
 automatically merge/rebase commits on server side if necessary. You can
 control the merge strategy by configuring the
-link:project-configuration.html#submit_type[submit type] on the project. If you
+link:config-project-config.html#submit-type[submit type] on the project. If you
 bypass code review you always need to merge/rebase manually if the tip
 of the destination branch has moved. Please keep this in mind if you
 choose to not work with code review because you think it's easier to
@@ -239,7 +246,7 @@
 
 An important decision for a project is the choice of the submit type
 and the content merge setting (see the `Allow content merges` option).
-The link:project-configuration.html#submit_type[submit type] is the method
+The link:config-project-config.html#submit-type[submit type] is the method
 Gerrit uses to submit a change to the project. The submit type defines
 what Gerrit should do on submit of a change if the destination branch
 has moved while the change was in review. The
@@ -281,7 +288,7 @@
 types for different branches.
 
 Please note that there are other submit types available; they are
-described in the link:project-configuration.html#submit_type[Submit Type]
+described in the link:config-project-config.html#submit-type[Submit Type]
 section.
 
 [[labels]]
@@ -607,18 +614,6 @@
 are inherited by the child projects. A child project can overwrite an
 inherited download command, or remove it by assigning no value to it.
 
-[[theme]]
-== Theme
-
-Gerrit supports project-specific themes for customizing the appearance
-of the change screen and the diff screens. It is possible to define an
-HTML header and footer and to adapt Gerrit's CSS. Details about themes
-are explained in the link:config-themes.html[Themes] section.
-
-Project-specific themes can only be installed by Gerrit administrators
-since the theme files must be copied into the Gerrit installation
-folder.
-
 [[tool-integration]]
 == Integration with other tools
 
diff --git a/Documentation/intro-user.txt b/Documentation/intro-user.txt
index 2a877ec..f5aed8d 100644
--- a/Documentation/intro-user.txt
+++ b/Documentation/intro-user.txt
@@ -383,7 +383,7 @@
 
 How the code modification is applied to the target branch when a change
 is submitted is controlled by the
-link:project-configuration.html#submit_type[submit type] which can be
+link:config-project-config.html#submit-type[submit type] which can be
 link:intro-project-owner.html#submit-type[configured on project-level].
 
 Submitting a change may fail with conflicts. In this case you need to
@@ -538,7 +538,9 @@
 ----
   $ git push origin HEAD:refs/for/master%wip
 ----
-Alternatively, click *WIP* from the Change screen.
+Alternatively, click *WIP* from the *More* menu on the Change screen.
+The Change screen updates with a yellow header, indicating that
+the change is in a Work-in-Progress state.
 
 To mark the change as ready for review, append `%ready` to your push
 request.
@@ -546,17 +548,12 @@
 ----
   $ git push origin HEAD:refs/for/master%ready
 ----
-Alternatively, click *Ready* from the Change screen.
+Alternatively, click *Start Review* from the Change screen.
 
-Only change owners, project owners and site administrators can mark changes as
+Change owners, project owners, site administrators and members of a group that
+was granted "Toggle Work In Progress state" permission can mark changes as
 `work-in-progress` and `ready`.
 
-[[wip-polygerrit]]
-In the new PolyGerrit UI, you can mark a change as WIP, by selecting *WIP* from
-the *More* menu. The Change screen updates with a yellow header, indicating that
-the change is in a Work-in-Progress state. To mark a change as ready for review,
-click *Start Review*.
-
 [[private-changes]]
 == Private Changes
 
@@ -578,10 +575,9 @@
   creating a private throwaway change without reviewers, you can push
   from one device, and fetch to another device.
 
-* You want to do code review on a change that has sensitive
-  aspects. By reviewing a security fix in a private change,
-  outsiders can't discover the fix before it is pushed out. Even after
-  merging the change, the review can be kept private.
+Do *not* use private changes for making security fixes (see
+link:#private-changes-pitfalls[pitfalls] below). How to make security
+fixes is explained link:#security-fixes[below].
 
 To create a private change, you push it with the `private` option.
 
@@ -605,6 +601,26 @@
 In that case, care should be taken to prevent the CI system from
 exposing secret details.
 
+[[private-changes-pitfalls]]
+Pitfalls
+===
+
+If private changes are used, be aware of the following pitfalls:
+
+* If a private change gets merged the corresponding commit gets visible
+  for all users that can access the target branch and the private flag
+  from the change is automatically removed. This makes private changes
+  a bad choice for security fixes, as the security fix will be
+  accessible as soon as the change was merged, but for security issues
+  you want to keep an embargo until new releases have been made
+  available.
+* If you push a non-private change on top of a private change the
+  commit of the private change gets implicitly visible through the
+  parent relationship of the follow-up change.
+* If you have a series of private changes and share one with reviewers,
+  the reviewers can also see the commits of the predecessor private
+  changes through the commit parent relationship.
+
 [[ignore]]
 == Ignoring Or Marking Changes As 'Reviewed'
 
@@ -942,6 +958,29 @@
 a file comment ("Rename this file to File.java") as well as a reply to an
 inline comment ("Yeah, I see why, let me try again.").
 
+[[security-fixes]]
+-- Security Fixes
+
+If a security vulnerability is discovered you normally want to have an
+embargo about it until fixed releases have been made available. This
+means you want to develop and review security fixes in private.
+
+If your repository is public or grants broad read access it is
+recommended to fix security issues in a copy of your repository which
+has very restricted read permissions (e.g. `myproject-security-fixes`).
+You can then implement, review and submit the security fix in this
+repository, make and publish a new release and only then integrate the
+security fix back into the normal (public) repository.
+
+Alternatively you can do the security fix in your normal repository in
+a branch with restricted read permissions. We don't recommend this
+because there is a risk of configuring the access rights wrongly and
+unintentionally granting read access to the wrong people.
+
+Using private changes for security fixes is *not* recommended due to
+the link:#private-changes-pitfalls[pitfalls] discussed above.
+Especially you don't want the fix to become visible after submit and
+before you had a chance to make and publish a new release.
 
 GERRIT
 ------
diff --git a/Documentation/js-api.txt b/Documentation/js-api.txt
index 96b5107..4ef2a6c 100644
--- a/Documentation/js-api.txt
+++ b/Documentation/js-api.txt
@@ -188,6 +188,12 @@
   comments, file-level comments and summary comments, and it may change
   with new Gerrit versions.
 
+* `highlightjs-loaded`: Invoked when the highlight.js library has
+  finished loading. The global `hljs` object (also now accessible via
+  `window.hljs`) is passed as an argument to the callback function.
+  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
diff --git a/Documentation/js_licenses.txt b/Documentation/js_licenses.txt
new file mode 100644
index 0000000..bb1399d
--- /dev/null
+++ b/Documentation/js_licenses.txt
@@ -0,0 +1,509 @@
+
+[[Apache2_0]]
+Apache2.0
+
+* fonts:robotofonts
+* js:web-animations-js
+* polymer_externs:polymer_closure
+
+[[Apache2_0_license]]
+----
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+
+----
+
+
+[[ba-linkify]]
+ba-linkify
+
+* js:ba-linkify
+
+[[ba-linkify_license]]
+----
+Copyright (c) 2009 "Cowboy" Ben Alman
+
+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.
+
+----
+
+
+[[es6-promise]]
+es6-promise
+
+* js:es6-promise
+
+[[es6-promise_license]]
+----
+Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+----
+
+
+[[fetch]]
+fetch
+
+* js:fetch
+
+[[fetch_license]]
+----
+Copyright (c) 2014-2016 GitHub, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+----
+
+
+[[highlightjs]]
+highlightjs
+
+* js:highlightjs
+* js:highlightjs_files
+
+[[highlightjs_license]]
+----
+Copyright (c) 2006, Ivan Sagalaev
+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 highlight.js 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 REGENTS 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 REGENTS AND 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.
+----
+
+
+[[moment]]
+moment
+
+* js:moment
+
+[[moment_license]]
+----
+Copyright (c) 2011-2016 Tim Wood, Iskren Chernev, Moment.js contributors
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+----
+
+
+[[page_js]]
+page.js
+
+* js:page
+
+[[page_js_license]]
+----
+(The MIT License)
+
+Copyright (c) 2012 TJ Holowaychuk <tj@vision-media.ca>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the 'Software'), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+----
+
+
+[[polymer]]
+polymer
+
+* js:font-roboto-local
+* js:iron-a11y-announcer
+* js:iron-a11y-keys-behavior
+* js:iron-autogrow-textarea
+* js:iron-behaviors
+* js:iron-checked-element-behavior
+* js:iron-dropdown
+* js:iron-fit-behavior
+* js:iron-flex-layout
+* js:iron-form-element-behavior
+* js:iron-icon
+* js:iron-iconset-svg
+* js:iron-input
+* js:iron-menu-behavior
+* js:iron-meta
+* js:iron-overlay-behavior
+* js:iron-resizable-behavior
+* js:iron-selector
+* js:iron-validatable-behavior
+* js:neon-animation
+* js:paper-behaviors
+* js:paper-button
+* js:paper-icon-button
+* js:paper-input
+* js:paper-item
+* js:paper-listbox
+* js:paper-ripple
+* js:paper-styles
+* js:paper-tabs
+* js:paper-toggle-button
+* js:polymer
+* js:polymer-resin
+* js:webcomponentsjs
+
+[[polymer_license]]
+----
+Copyright (c) 2014 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.
+
+----
+
+
+[[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.
+
+----
+
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
new file mode 100644
index 0000000..7cf48c0
--- /dev/null
+++ b/Documentation/licenses.txt
@@ -0,0 +1,3470 @@
+= Gerrit Code Review - Licenses
+
+// DO NOT EDIT - GENERATED AUTOMATICALLY.
+
+Gerrit open source software is licensed under the <<Apache2_0,Apache
+License 2.0>>.  Executable distributions also include other software
+components that are provided under additional licenses.
+
+[[cryptography]]
+== Cryptography Notice
+
+This distribution includes cryptographic software.  The country
+in which you currently reside may have restrictions on the import,
+possession, use, and/or re-export to another country, of encryption
+software.  BEFORE using any encryption software, please check
+your country's laws, regulations and policies concerning the
+import, possession, or use, and re-export of encryption software,
+to see if this is permitted.  See the
+link:http://www.wassenaar.org/[Wassenaar Arrangement]
+for more information.
+
+The U.S. Government Department of Commerce, Bureau of Industry
+and Security (BIS), has classified this software as Export
+Commodity Control Number (ECCN) 5D002.C.1, which includes
+information security software using or performing cryptographic
+functions with asymmetric algorithms.  The form and manner of
+this distribution makes it eligible for export under the License
+Exception ENC Technology Software Unrestricted (TSU) exception
+(see the BIS Export Administration Regulations, Section 740.13)
+for both object code and source code.
+
+Gerrit includes an SSH daemon (Apache SSHD), to support authenticated
+uploads of changes directly from `git push` command line clients.
+
+Gerrit includes an SSH client (JSch), to support authenticated
+replication of changes to remote systems, such as for automatic
+updates of mirror servers, or realtime backups.
+
+== Licenses
+
+
+[[Apache2_0]]
+Apache2.0
+
+* auto:auto-value
+* auto:auto-value-annotations
+* commons:codec
+* commons:compress
+* commons:dbcp
+* commons:lang
+* commons:net
+* commons:pool
+* commons:validator
+* dropwizard:dropwizard-core
+* flogger:api
+* fonts:robotofonts
+* guice:guice
+* guice:guice-assistedinject
+* guice:guice-library
+* guice:guice-servlet
+* guice:javax_inject
+* httpcomponents:httpasyncclient
+* httpcomponents:httpclient
+* httpcomponents:httpcore
+* httpcomponents:httpcore-nio
+* jackson:jackson-core
+* jetty:continuation
+* jetty:http
+* jetty:io
+* jetty:jmx
+* jetty:security
+* jetty:server
+* jetty:servlet
+* jetty:util
+* jgit/org.eclipse.jgit:javaewah
+* js:web-animations-js
+* log:json-smart
+* log:jsonevent-layout
+* log:log4j
+* lucene:lucene-analyzers-common
+* lucene:lucene-core-and-backward-codecs-merged
+* lucene:lucene-misc
+* lucene:lucene-queryparser
+* mime4j:core
+* mime4j:dom
+* mina:core
+* mina:sshd
+* openid:consumer
+* openid:nekohtml
+* openid:xerces
+* polymer_externs:polymer_closure
+* blame-cache
+* caffeine
+* caffeine-guava
+* gson
+* guava
+* guava-failureaccess
+* guava-retrying
+* html-types
+* j2objc
+* jsr305
+* mime-util
+* servlet-api-3_1
+* servlet-api-3_1-without-neverlink
+* soy
+
+[[Apache2_0_license]]
+----
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+
+----
+
+
+[[CC0-1_0]]
+CC0-1.0
+
+* mina:eddsa
+
+[[CC0-1_0_license]]
+----
+Creative Commons Legal Code
+
+CC0 1.0 Universal
+
+    CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+    LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
+    ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+    INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+    REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
+    PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+    THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
+    HEREUNDER.
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator
+and subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for
+the purpose of contributing to a commons of creative, cultural and
+scientific works ("Commons") that the public can reliably and without fear
+of later claims of infringement build upon, modify, incorporate in other
+works, reuse and redistribute as freely as possible in any form whatsoever
+and for any purposes, including without limitation commercial purposes.
+These owners may contribute to the Commons to promote the ideal of a free
+culture and the further production of creative, cultural and scientific
+works, or to gain reputation or greater distribution for their Work in
+part through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any
+expectation of additional consideration or compensation, the person
+associating CC0 with a Work (the "Affirmer"), to the extent that he or she
+is an owner of Copyright and Related Rights in the Work, voluntarily
+elects to apply CC0 to the Work and publicly distribute the Work under its
+terms, with knowledge of his or her Copyright and Related Rights in the
+Work and the meaning and intended legal effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+protected by copyright and related or neighboring rights ("Copyright and
+Related Rights"). Copyright and Related Rights include, but are not
+limited to, the following:
+
+  i. the right to reproduce, adapt, distribute, perform, display,
+     communicate, and translate a Work;
+ ii. moral rights retained by the original author(s) and/or performer(s);
+iii. publicity and privacy rights pertaining to a person's image or
+     likeness depicted in a Work;
+ iv. rights protecting against unfair competition in regards to a Work,
+     subject to the limitations in paragraph 4(a), below;
+  v. rights protecting the extraction, dissemination, use and reuse of data
+     in a Work;
+ vi. database rights (such as those arising under Directive 96/9/EC of the
+     European Parliament and of the Council of 11 March 1996 on the legal
+     protection of databases, and under any national implementation
+     thereof, including any amended or successor version of such
+     directive); and
+vii. other similar, equivalent or corresponding rights throughout the
+     world based on applicable law or treaty, and any national
+     implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention
+of, applicable law, Affirmer hereby overtly, fully, permanently,
+irrevocably and unconditionally waives, abandons, and surrenders all of
+Affirmer's Copyright and Related Rights and associated claims and causes
+of action, whether now known or unknown (including existing as well as
+future claims and causes of action), in the Work (i) in all territories
+worldwide, (ii) for the maximum duration provided by applicable law or
+treaty (including future time extensions), (iii) in any current or future
+medium and for any number of copies, and (iv) for any purpose whatsoever,
+including without limitation commercial, advertising or promotional
+purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
+member of the public at large and to the detriment of Affirmer's heirs and
+successors, fully intending that such Waiver shall not be subject to
+revocation, rescission, cancellation, termination, or any other legal or
+equitable action to disrupt the quiet enjoyment of the Work by the public
+as contemplated by Affirmer's express Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason
+be judged legally invalid or ineffective under applicable law, then the
+Waiver shall be preserved to the maximum extent permitted taking into
+account Affirmer's express Statement of Purpose. In addition, to the
+extent the Waiver is so judged Affirmer hereby grants to each affected
+person a royalty-free, non transferable, non sublicensable, non exclusive,
+irrevocable and unconditional license to exercise Affirmer's Copyright and
+Related Rights in the Work (i) in all territories worldwide, (ii) for the
+maximum duration provided by applicable law or treaty (including future
+time extensions), (iii) in any current or future medium and for any number
+of copies, and (iv) for any purpose whatsoever, including without
+limitation commercial, advertising or promotional purposes (the
+"License"). The License shall be deemed effective as of the date CC0 was
+applied by Affirmer to the Work. Should any part of the License for any
+reason be judged legally invalid or ineffective under applicable law, such
+partial invalidity or ineffectiveness shall not invalidate the remainder
+of the License, and in such case Affirmer hereby affirms that he or she
+will not (i) exercise any of his or her remaining Copyright and Related
+Rights in the Work or (ii) assert any associated claims and causes of
+action with respect to the Work, in either case contrary to Affirmer's
+express Statement of Purpose.
+
+4. Limitations and Disclaimers.
+
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
+    surrendered, licensed or otherwise affected by this document.
+ b. Affirmer offers the Work as-is and makes no representations or
+    warranties of any kind concerning the Work, express, implied,
+    statutory or otherwise, including without limitation warranties of
+    title, merchantability, fitness for a particular purpose, non
+    infringement, or the absence of latent or other defects, accuracy, or
+    the present or absence of errors, whether or not discoverable, all to
+    the greatest extent permissible under applicable law.
+ c. Affirmer disclaims responsibility for clearing rights of other persons
+    that may apply to the Work or any use thereof, including without
+    limitation any person's Copyright and Related Rights in the Work.
+    Further, Affirmer disclaims responsibility for obtaining any necessary
+    consents, permissions or other rights required for any use of the
+    Work.
+ d. Affirmer understands and acknowledges that Creative Commons is not a
+    party to this document and has no duty or obligation with respect to
+    this CC0 or use of the Work.
+
+For more information, please see https://creativecommons.org/publicdomain/zero/1.0/
+
+----
+
+
+[[MPL1_1]]
+MPL1.1
+
+* juniversalchardet
+
+[[MPL1_1_license]]
+----
+                          MOZILLA PUBLIC LICENSE
+                                Version 1.1
+
+                              ---------------
+
+1. Definitions.
+
+     1.0.1. "Commercial Use" means distribution or otherwise making the
+     Covered Code available to a third party.
+
+     1.1. "Contributor" means each entity that creates or contributes to
+     the creation of Modifications.
+
+     1.2. "Contributor Version" means the combination of the Original
+     Code, prior Modifications used by a Contributor, and the Modifications
+     made by that particular Contributor.
+
+     1.3. "Covered Code" means the Original Code or Modifications or the
+     combination of the Original Code and Modifications, in each case
+     including portions thereof.
+
+     1.4. "Electronic Distribution Mechanism" means a mechanism generally
+     accepted in the software development community for the electronic
+     transfer of data.
+
+     1.5. "Executable" means Covered Code in any form other than Source
+     Code.
+
+     1.6. "Initial Developer" means the individual or entity identified
+     as the Initial Developer in the Source Code notice required by Exhibit
+     A.
+
+     1.7. "Larger Work" means a work which combines Covered Code or
+     portions thereof with code not governed by the terms of this License.
+
+     1.8. "License" means this document.
+
+     1.8.1. "Licensable" means having the right to grant, to the maximum
+     extent possible, whether at the time of the initial grant or
+     subsequently acquired, any and all of the rights conveyed herein.
+
+     1.9. "Modifications" means any addition to or deletion from the
+     substance or structure of either the Original Code or any previous
+     Modifications. When Covered Code is released as a series of files, a
+     Modification is:
+          A. Any addition to or deletion from the contents of a file
+          containing Original Code or previous Modifications.
+
+          B. Any new file that contains any part of the Original Code or
+          previous Modifications.
+
+     1.10. "Original Code" means Source Code of computer software code
+     which is described in the Source Code notice required by Exhibit A as
+     Original Code, and which, at the time of its release under this
+     License is not already Covered Code governed by this License.
+
+     1.10.1. "Patent Claims" means any patent claim(s), now owned or
+     hereafter acquired, including without limitation,  method, process,
+     and apparatus claims, in any patent Licensable by grantor.
+
+     1.11. "Source Code" means the preferred form of the Covered Code for
+     making modifications to it, including all modules it contains, plus
+     any associated interface definition files, scripts used to control
+     compilation and installation of an Executable, or source code
+     differential comparisons against either the Original Code or another
+     well known, available Covered Code of the Contributor's choice. The
+     Source Code can be in a compressed or archival form, provided the
+     appropriate decompression or de-archiving software is widely available
+     for no charge.
+
+     1.12. "You" (or "Your")  means an individual or a legal entity
+     exercising rights under, and complying with all of the terms of, this
+     License or a future version of this License issued under Section 6.1.
+     For legal entities, "You" includes any entity which controls, is
+     controlled by, or is under common control with You. For purposes of
+     this definition, "control" means (a) the power, direct or indirect,
+     to cause the direction or management of such entity, whether by
+     contract or otherwise, or (b) ownership of more than fifty percent
+     (50%) of the outstanding shares or beneficial ownership of such
+     entity.
+
+2. Source Code License.
+
+     2.1. The Initial Developer Grant.
+     The Initial Developer hereby grants You a world-wide, royalty-free,
+     non-exclusive license, subject to third party intellectual property
+     claims:
+          (a)  under intellectual property rights (other than patent or
+          trademark) Licensable by Initial Developer to use, reproduce,
+          modify, display, perform, sublicense and distribute the Original
+          Code (or portions thereof) with or without Modifications, and/or
+          as part of a Larger Work; and
+
+          (b) under Patents Claims infringed by the making, using or
+          selling of Original Code, to make, have made, use, practice,
+          sell, and offer for sale, and/or otherwise dispose of the
+          Original Code (or portions thereof).
+
+          (c) the licenses granted in this Section 2.1(a) and (b) are
+          effective on the date Initial Developer first distributes
+          Original Code under the terms of this License.
+
+          (d) Notwithstanding Section 2.1(b) above, no patent license is
+          granted: 1) for code that You delete from the Original Code; 2)
+          separate from the Original Code;  or 3) for infringements caused
+          by: i) the modification of the Original Code or ii) the
+          combination of the Original Code with other software or devices.
+
+     2.2. Contributor Grant.
+     Subject to third party intellectual property claims, each Contributor
+     hereby grants You a world-wide, royalty-free, non-exclusive license
+
+          (a)  under intellectual property rights (other than patent or
+          trademark) Licensable by Contributor, to use, reproduce, modify,
+          display, perform, sublicense and distribute the Modifications
+          created by such Contributor (or portions thereof) either on an
+          unmodified basis, with other Modifications, as Covered Code
+          and/or as part of a Larger Work; and
+
+          (b) under Patent Claims infringed by the making, using, or
+          selling of  Modifications made by that Contributor either alone
+          and/or in combination with its Contributor Version (or portions
+          of such combination), to make, use, sell, offer for sale, have
+          made, and/or otherwise dispose of: 1) Modifications made by that
+          Contributor (or portions thereof); and 2) the combination of
+          Modifications made by that Contributor with its Contributor
+          Version (or portions of such combination).
+
+          (c) the licenses granted in Sections 2.2(a) and 2.2(b) are
+          effective on the date Contributor first makes Commercial Use of
+          the Covered Code.
+
+          (d)    Notwithstanding Section 2.2(b) above, no patent license is
+          granted: 1) for any code that Contributor has deleted from the
+          Contributor Version; 2)  separate from the Contributor Version;
+          3)  for infringements caused by: i) third party modifications of
+          Contributor Version or ii)  the combination of Modifications made
+          by that Contributor with other software  (except as part of the
+          Contributor Version) or other devices; or 4) under Patent Claims
+          infringed by Covered Code in the absence of Modifications made by
+          that Contributor.
+
+3. Distribution Obligations.
+
+     3.1. Application of License.
+     The Modifications which You create or to which You contribute are
+     governed by the terms of this License, including without limitation
+     Section 2.2. The Source Code version of Covered Code may be
+     distributed only under the terms of this License or a future version
+     of this License released under Section 6.1, and You must include a
+     copy of this License with every copy of the Source Code You
+     distribute. You may not offer or impose any terms on any Source Code
+     version that alters or restricts the applicable version of this
+     License or the recipients' rights hereunder. However, You may include
+     an additional document offering the additional rights described in
+     Section 3.5.
+
+     3.2. Availability of Source Code.
+     Any Modification which You create or to which You contribute must be
+     made available in Source Code form under the terms of this License
+     either on the same media as an Executable version or via an accepted
+     Electronic Distribution Mechanism to anyone to whom you made an
+     Executable version available; and if made available via Electronic
+     Distribution Mechanism, must remain available for at least twelve (12)
+     months after the date it initially became available, or at least six
+     (6) months after a subsequent version of that particular Modification
+     has been made available to such recipients. You are responsible for
+     ensuring that the Source Code version remains available even if the
+     Electronic Distribution Mechanism is maintained by a third party.
+
+     3.3. Description of Modifications.
+     You must cause all Covered Code to which You contribute to contain a
+     file documenting the changes You made to create that Covered Code and
+     the date of any change. You must include a prominent statement that
+     the Modification is derived, directly or indirectly, from Original
+     Code provided by the Initial Developer and including the name of the
+     Initial Developer in (a) the Source Code, and (b) in any notice in an
+     Executable version or related documentation in which You describe the
+     origin or ownership of the Covered Code.
+
+     3.4. Intellectual Property Matters
+          (a) Third Party Claims.
+          If Contributor has knowledge that a license under a third party's
+          intellectual property rights is required to exercise the rights
+          granted by such Contributor under Sections 2.1 or 2.2,
+          Contributor must include a text file with the Source Code
+          distribution titled "LEGAL" which describes the claim and the
+          party making the claim in sufficient detail that a recipient will
+          know whom to contact. If Contributor obtains such knowledge after
+          the Modification is made available as described in Section 3.2,
+          Contributor shall promptly modify the LEGAL file in all copies
+          Contributor makes available thereafter and shall take other steps
+          (such as notifying appropriate mailing lists or newsgroups)
+          reasonably calculated to inform those who received the Covered
+          Code that new knowledge has been obtained.
+
+          (b) Contributor APIs.
+          If Contributor's Modifications include an application programming
+          interface and Contributor has knowledge of patent licenses which
+          are reasonably necessary to implement that API, Contributor must
+          also include this information in the LEGAL file.
+
+               (c)    Representations.
+          Contributor represents that, except as disclosed pursuant to
+          Section 3.4(a) above, Contributor believes that Contributor's
+          Modifications are Contributor's original creation(s) and/or
+          Contributor has sufficient rights to grant the rights conveyed by
+          this License.
+
+     3.5. Required Notices.
+     You must duplicate the notice in Exhibit A in each file of the Source
+     Code.  If it is not possible to put such notice in a particular Source
+     Code file due to its structure, then You must include such notice in a
+     location (such as a relevant directory) where a user would be likely
+     to look for such a notice.  If You created one or more Modification(s)
+     You may add your name as a Contributor to the notice described in
+     Exhibit A.  You must also duplicate this License in any documentation
+     for the Source Code where You describe recipients' rights or ownership
+     rights relating to Covered Code.  You may choose to offer, and to
+     charge a fee for, warranty, support, indemnity or liability
+     obligations to one or more recipients of Covered Code. However, You
+     may do so only on Your own behalf, and not on behalf of the Initial
+     Developer or any Contributor. You must make it absolutely clear than
+     any such warranty, support, indemnity or liability obligation is
+     offered by You alone, and You hereby agree to indemnify the Initial
+     Developer and every Contributor for any liability incurred by the
+     Initial Developer or such Contributor as a result of warranty,
+     support, indemnity or liability terms You offer.
+
+     3.6. Distribution of Executable Versions.
+     You may distribute Covered Code in Executable form only if the
+     requirements of Section 3.1-3.5 have been met for that Covered Code,
+     and if You include a notice stating that the Source Code version of
+     the Covered Code is available under the terms of this License,
+     including a description of how and where You have fulfilled the
+     obligations of Section 3.2. The notice must be conspicuously included
+     in any notice in an Executable version, related documentation or
+     collateral in which You describe recipients' rights relating to the
+     Covered Code. You may distribute the Executable version of Covered
+     Code or ownership rights under a license of Your choice, which may
+     contain terms different from this License, provided that You are in
+     compliance with the terms of this License and that the license for the
+     Executable version does not attempt to limit or alter the recipient's
+     rights in the Source Code version from the rights set forth in this
+     License. If You distribute the Executable version under a different
+     license You must make it absolutely clear that any terms which differ
+     from this License are offered by You alone, not by the Initial
+     Developer or any Contributor. You hereby agree to indemnify the
+     Initial Developer and every Contributor for any liability incurred by
+     the Initial Developer or such Contributor as a result of any such
+     terms You offer.
+
+     3.7. Larger Works.
+     You may create a Larger Work by combining Covered Code with other code
+     not governed by the terms of this License and distribute the Larger
+     Work as a single product. In such a case, You must make sure the
+     requirements of this License are fulfilled for the Covered Code.
+
+4. Inability to Comply Due to Statute or Regulation.
+
+     If it is impossible for You to comply with any of the terms of this
+     License with respect to some or all of the Covered Code due to
+     statute, judicial order, or regulation then You must: (a) comply with
+     the terms of this License to the maximum extent possible; and (b)
+     describe the limitations and the code they affect. Such description
+     must be included in the LEGAL file described in Section 3.4 and must
+     be included with all distributions of the Source Code. Except to the
+     extent prohibited by statute or regulation, such description must be
+     sufficiently detailed for a recipient of ordinary skill to be able to
+     understand it.
+
+5. Application of this License.
+
+     This License applies to code to which the Initial Developer has
+     attached the notice in Exhibit A and to related Covered Code.
+
+6. Versions of the License.
+
+     6.1. New Versions.
+     Netscape Communications Corporation ("Netscape") may publish revised
+     and/or new versions of the License from time to time. Each version
+     will be given a distinguishing version number.
+
+     6.2. Effect of New Versions.
+     Once Covered Code has been published under a particular version of the
+     License, You may always continue to use it under the terms of that
+     version. You may also choose to use such Covered Code under the terms
+     of any subsequent version of the License published by Netscape. No one
+     other than Netscape has the right to modify the terms applicable to
+     Covered Code created under this License.
+
+     6.3. Derivative Works.
+     If You create or use a modified version of this License (which you may
+     only do in order to apply it to code which is not already Covered Code
+     governed by this License), You must (a) rename Your license so that
+     the phrases "Mozilla", "MOZILLAPL", "MOZPL", "Netscape",
+     "MPL", "NPL" or any confusingly similar phrase do not appear in your
+     license (except to note that your license differs from this License)
+     and (b) otherwise make it clear that Your version of the license
+     contains terms which differ from the Mozilla Public License and
+     Netscape Public License. (Filling in the name of the Initial
+     Developer, Original Code or Contributor in the notice described in
+     Exhibit A shall not of themselves be deemed to be modifications of
+     this License.)
+
+7. DISCLAIMER OF WARRANTY.
+
+     COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS,
+     WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
+     WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF
+     DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING.
+     THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED CODE
+     IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT,
+     YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE
+     COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER
+     OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF
+     ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER.
+
+8. TERMINATION.
+
+     8.1.  This License and the rights granted hereunder will terminate
+     automatically if You fail to comply with terms herein and fail to cure
+     such breach within 30 days of becoming aware of the breach. All
+     sublicenses to the Covered Code which are properly granted shall
+     survive any termination of this License. Provisions which, by their
+     nature, must remain in effect beyond the termination of this License
+     shall survive.
+
+     8.2.  If You initiate litigation by asserting a patent infringement
+     claim (excluding declatory judgment actions) against Initial Developer
+     or a Contributor (the Initial Developer or Contributor against whom
+     You file such action is referred to as "Participant")  alleging that:
+
+     (a)  such Participant's Contributor Version directly or indirectly
+     infringes any patent, then any and all rights granted by such
+     Participant to You under Sections 2.1 and/or 2.2 of this License
+     shall, upon 60 days notice from Participant terminate prospectively,
+     unless if within 60 days after receipt of notice You either: (i)
+     agree in writing to pay Participant a mutually agreeable reasonable
+     royalty for Your past and future use of Modifications made by such
+     Participant, or (ii) withdraw Your litigation claim with respect to
+     the Contributor Version against such Participant.  If within 60 days
+     of notice, a reasonable royalty and payment arrangement are not
+     mutually agreed upon in writing by the parties or the litigation claim
+     is not withdrawn, the rights granted by Participant to You under
+     Sections 2.1 and/or 2.2 automatically terminate at the expiration of
+     the 60 day notice period specified above.
+
+     (b)  any software, hardware, or device, other than such Participant's
+     Contributor Version, directly or indirectly infringes any patent, then
+     any rights granted to You by such Participant under Sections 2.1(b)
+     and 2.2(b) are revoked effective as of the date You first made, used,
+     sold, distributed, or had made, Modifications made by that
+     Participant.
+
+     8.3.  If You assert a patent infringement claim against Participant
+     alleging that such Participant's Contributor Version directly or
+     indirectly infringes any patent where such claim is resolved (such as
+     by license or settlement) prior to the initiation of patent
+     infringement litigation, then the reasonable value of the licenses
+     granted by such Participant under Sections 2.1 or 2.2 shall be taken
+     into account in determining the amount or value of any payment or
+     license.
+
+     8.4.  In the event of termination under Sections 8.1 or 8.2 above,
+     all end user license agreements (excluding distributors and resellers)
+     which have been validly granted by You or any distributor hereunder
+     prior to termination shall survive termination.
+
+9. LIMITATION OF LIABILITY.
+
+     UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT
+     (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL
+     DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE,
+     OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR
+     ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY
+     CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL,
+     WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER
+     COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN
+     INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF
+     LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY
+     RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW
+     PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE
+     EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO
+     THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU.
+
+10. U.S. GOVERNMENT END USERS.
+
+     The Covered Code is a "commercial item," as that term is defined in
+     48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer
+     software" and "commercial computer software documentation," as such
+     terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48
+     C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995),
+     all U.S. Government End Users acquire Covered Code with only those
+     rights set forth herein.
+
+11. MISCELLANEOUS.
+
+     This License represents the complete agreement concerning subject
+     matter hereof. If any provision of this License is held to be
+     unenforceable, such provision shall be reformed only to the extent
+     necessary to make it enforceable. This License shall be governed by
+     California law provisions (except to the extent applicable law, if
+     any, provides otherwise), excluding its conflict-of-law provisions.
+     With respect to disputes in which at least one party is a citizen of,
+     or an entity chartered or registered to do business in the United
+     States of America, any litigation relating to this License shall be
+     subject to the jurisdiction of the Federal Courts of the Northern
+     District of California, with venue lying in Santa Clara County,
+     California, with the losing party responsible for costs, including
+     without limitation, court costs and reasonable attorneys' fees and
+     expenses. The application of the United Nations Convention on
+     Contracts for the International Sale of Goods is expressly excluded.
+     Any law or regulation which provides that the language of a contract
+     shall be construed against the drafter shall not apply to this
+     License.
+
+12. RESPONSIBILITY FOR CLAIMS.
+
+     As between Initial Developer and the Contributors, each party is
+     responsible for claims and damages arising, directly or indirectly,
+     out of its utilization of rights under this License and You agree to
+     work with Initial Developer and Contributors to distribute such
+     responsibility on an equitable basis. Nothing herein is intended or
+     shall be deemed to constitute any admission of liability.
+
+13. MULTIPLE-LICENSED CODE.
+
+     Initial Developer may designate portions of the Covered Code as
+     "Multiple-Licensed".  "Multiple-Licensed" means that the Initial
+     Developer permits you to utilize portions of the Covered Code under
+     Your choice of the NPL or the alternative licenses, if any, specified
+     by the Initial Developer in the file described in Exhibit A.
+
+EXHIBIT A -Mozilla Public License.
+
+     ``The contents of this file are subject to the Mozilla Public License
+     Version 1.1 (the "License"); you may not use this file except in
+     compliance with the License. You may obtain a copy of the License at
+     http://www.mozilla.org/MPL/
+
+     Software distributed under the License is distributed on an "AS IS"
+     basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
+     License for the specific language governing rights and limitations
+     under the License.
+
+     The Original Code is ______________________________________.
+
+     The Initial Developer of the Original Code is ________________________.
+     Portions created by ______________________ are Copyright (C) ______
+     _______________________. All Rights Reserved.
+
+     Contributor(s): ______________________________________.
+
+     Alternatively, the contents of this file may be used under the terms
+     of the _____ license (the  "[___] License"), in which case the
+     provisions of [______] License are applicable instead of those
+     above.  If you wish to allow use of your version of this file only
+     under the terms of the [____] License and not to allow others to use
+     your version of this file under the MPL, indicate your decision by
+     deleting  the provisions above and replace  them with the notice and
+     other provisions required by the [___] License.  If you do not delete
+     the provisions above, a recipient may use your version of this file
+     under either the MPL or the [___] License."
+
+     [NOTE: The text of this Exhibit A may differ slightly from the text of
+     the notices in the Source Code files of the Original Code. You should
+     use the text of this Exhibit A rather than the text found in the
+     Original Code Source Code for Your Modifications.]
+
+----
+
+
+[[PublicDomain]]
+PublicDomain
+
+* guice:aopalliance
+
+[[PublicDomain_license]]
+----
+This software has been placed in the public domain by its author(s).
+
+----
+
+
+[[antlr]]
+antlr
+
+* antlr:antlr27
+* antlr:java-runtime
+* antlr:stringtemplate
+* antlr:tool
+
+[[antlr_license]]
+----
+Copyright (c) 2003-2008, Terence Parr
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of the author 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.
+
+----
+
+
+[[args4j]]
+args4j
+
+* args4j
+
+[[args4j_license]]
+----
+Copyright (c) 2013 Kohsuke Kawaguchi and other contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+----
+
+
+[[autolink]]
+autolink
+
+* autolink
+
+[[autolink_license]]
+----
+The MIT License (MIT)
+
+Copyright (c) 2015 Robin Stocker
+
+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.
+
+----
+
+
+[[automaton]]
+automaton
+
+* automaton
+
+[[automaton_license]]
+----
+Copyright (c) 2001-2011 Anders Moeller
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name of the JSR305 expert group 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.
+
+----
+
+
+[[ba-linkify]]
+ba-linkify
+
+* js:ba-linkify
+
+[[ba-linkify_license]]
+----
+Copyright (c) 2009 "Cowboy" Ben Alman
+
+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.
+
+----
+
+
+[[bouncycastle]]
+bouncycastle
+
+* bouncycastle:bcpg-neverlink
+* bouncycastle:bcpkix-neverlink
+* bouncycastle:bcprov-neverlink
+
+[[bouncycastle_license]]
+----
+Copyright (c) 2000 - 2015 The Legion of the Bouncy Castle Inc.
+(http://www.bouncycastle.org)
+
+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.
+
+----
+
+
+[[elasticsearch]]
+elasticsearch
+
+* elasticsearch-rest-client:elasticsearch-rest-client
+
+[[elasticsearch_license]]
+----
+Elasticsearch
+Copyright 2009-2015 Elasticsearch
+
+This product includes software developed by The Apache Software
+Foundation (http://www.apache.org/).
+
+----
+
+
+[[es6-promise]]
+es6-promise
+
+* js:es6-promise
+
+[[es6-promise_license]]
+----
+Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+----
+
+
+[[fetch]]
+fetch
+
+* js:fetch
+
+[[fetch_license]]
+----
+Copyright (c) 2014-2016 GitHub, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+----
+
+
+[[flexmark]]
+flexmark
+
+* flexmark
+* flexmark-ext-abbreviation
+* flexmark-ext-anchorlink
+* flexmark-ext-autolink
+* flexmark-ext-definition
+* flexmark-ext-emoji
+* flexmark-ext-escaped-character
+* flexmark-ext-footnotes
+* flexmark-ext-gfm-issues
+* flexmark-ext-gfm-strikethrough
+* flexmark-ext-gfm-tables
+* flexmark-ext-gfm-tasklist
+* flexmark-ext-gfm-users
+* flexmark-ext-ins
+* flexmark-ext-jekyll-front-matter
+* flexmark-ext-superscript
+* flexmark-ext-tables
+* flexmark-ext-toc
+* flexmark-ext-typographic
+* flexmark-ext-wikilink
+* flexmark-ext-yaml-front-matter
+* flexmark-formatter
+* flexmark-html-parser
+* flexmark-profile-pegdown
+* flexmark-util
+
+[[flexmark_license]]
+----
+Copyright (c) 2015-2016, Atlassian Pty Ltd
+All rights reserved.
+
+Copyright (c) 2016, Vladimir Schneider,
+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.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+----
+
+
+[[h2]]
+h2
+
+* h2
+
+[[h2_license]]
+----
+H2 is dual licensed and available under a modified version of the
+MPL 1.1 (Mozilla Public License) or under the (unmodified) EPL 1.0.
+----
+
+link:http://www.h2database.com/html/license.html[H2 License]
+
+----
+H2 License - Version 1.0
+1. Definitions
+
+1.0.1. "Commercial Use" means distribution or otherwise making the
+       Covered Code available to a third party.
+
+1.1. "Contributor" means each entity that creates or contributes
+     to the creation of Modifications.
+
+1.2. "Contributor Version" means the combination of the Original
+     Code, prior Modifications used by a Contributor, and the
+     Modifications made by that particular Contributor.
+
+1.3. "Covered Code" means the Original Code or Modifications or
+     the combination of the Original Code and Modifications, in each
+     case including portions thereof.
+
+1.4. "Electronic Distribution Mechanism" means a mechanism generally
+     accepted in the software development community for the electronic
+     transfer of data.
+
+1.5. "Executable" means Covered Code in any form other than Source Code.
+
+1.6. "Initial Developer" means the individual or entity identified
+     as the Initial Developer in the Source Code notice required
+     by Exhibit A.
+
+1.7. "Larger Work" means a work which combines Covered Code or
+     portions thereof with code not governed by the terms of this
+     License.
+
+1.8. "License" means this document.
+
+1.8.1. "Licensable" means having the right to grant, to the maximum
+       extent possible, whether at the time of the initial grant
+       or subsequently acquired, any and all of the rights conveyed
+       herein.
+
+1.9. "Modifications" means any addition to or deletion from the
+     substance or structure of either the Original Code or any
+     previous Modifications. When Covered Code is released as a
+     series of files, a Modification is:
+
+1.9.a. Any addition to or deletion from the contents of a file
+       containing Original Code or previous Modifications.
+
+1.9.b. Any new file that contains any part of the Original Code or
+       previous Modifications.
+
+1.10. "Original Code" means Source Code of computer software
+      code which is described in the Source Code notice required
+      by Exhibit A as Original Code, and which, at the time of
+      its release under this License is not already Covered Code
+      governed by this License.
+
+1.10.1. "Patent Claims" means any patent claim(s), now owned or
+        hereafter acquired, including without limitation, method,
+        process, and apparatus claims, in any patent Licensable
+        by grantor.
+
+1.11. "Source Code" means the preferred form of the Covered Code
+      for making modifications to it, including all modules it
+      contains, plus any associated interface definition files,
+      scripts used to control compilation and installation of an
+      Executable, or source code differential comparisons against
+      either the Original Code or another well known, available
+      Covered Code of the Contributor's choice. The Source Code can
+      be in a compressed or archival form, provided the appropriate
+      decompression or de-archiving software is widely available
+      for no charge.
+
+1.12. "You" (or "Your") means an individual or a legal entity
+      exercising rights under, and complying with all of the terms
+      of, this License or a future version of this License issued
+      under Section 6.1. For legal entities, "You" includes any
+      entity which controls, is controlled by, or is under common
+      control with You. For purposes of this definition, "control"
+      means (a) the power, direct or indirect, to cause the direction
+      or management of such entity, whether by contract or otherwise,
+      or (b) ownership of more than fifty percent (50%) of the
+      outstanding shares or beneficial ownership of such entity.
+
+2. Source Code License
+
+2.1. The Initial Developer Grant
+
+The Initial Developer hereby grants You a world-wide, royalty-free,
+non-exclusive license, subject to third party intellectual property
+claims:
+
+2.1.a. under intellectual property rights (other than patent
+       or trademark) Licensable by Initial Developer to use,
+       reproduce, modify, display, perform, sublicense and distribute
+       the Original Code (or portions thereof) with or without
+       Modifications, and/or as part of a Larger Work; and
+
+2.1.b. under Patents Claims infringed by the making, using or selling
+       of Original Code, to make, have made, use, practice, sell,
+       and offer for sale, and/or otherwise dispose of the Original
+       Code (or portions thereof).
+
+2.1.c. the licenses granted in this Section 2.1 (a) and (b) are
+       effective on the date Initial Developer first distributes
+       Original Code under the terms of this License.
+
+2.1.d. Notwithstanding Section 2.1 (b) above, no patent license is
+       granted: 1) for code that You delete from the Original Code;
+       2) separate from the Original Code; or 3) for infringements
+       caused by: i) the modification of the Original Code or ii)
+       the combination of the Original Code with other software
+       or devices.
+
+2.2. Contributor Grant
+
+Subject to third party intellectual property claims, each Contributor
+hereby grants You a world-wide, royalty-free, non-exclusive license
+
+2.2.a. under intellectual property rights (other than patent or
+       trademark) Licensable by Contributor, to use, reproduce,
+       modify, display, perform, sublicense and distribute the
+       Modifications created by such Contributor (or portions
+       thereof) either on an unmodified basis, with other
+       Modifications, as Covered Code and/or as part of a Larger
+       Work; and
+
+2.2.b. under Patent Claims infringed by the making, using, or selling
+       of Modifications made by that Contributor either alone and/or
+       in combination with its Contributor Version (or portions
+       of such combination), to make, use, sell, offer for sale,
+       have made, and/or otherwise dispose of: 1) Modifications
+       made by that Contributor (or portions thereof); and 2) the
+       combination of Modifications made by that Contributor with
+       its Contributor Version (or portions of such combination).
+
+2.2.c. the licenses granted in Sections 2.2 (a) and 2.2 (b) are
+       effective on the date Contributor first makes Commercial
+       Use of the Covered Code.
+
+2.2.c. Notwithstanding Section 2.2 (b) above, no patent license is
+       granted: 1) for any code that Contributor has deleted from
+       the Contributor Version; 2) separate from the Contributor
+       Version; 3) for infringements caused by: i) third party
+       modifications of Contributor Version or ii) the combination
+       of Modifications made by that Contributor with other software
+       (except as part of the Contributor Version) or other devices;
+       or 4) under Patent Claims infringed by Covered Code in the
+       absence of Modifications made by that Contributor.
+
+3. Distribution Obligations
+
+3.1. Application of License
+
+The Modifications which You create or to which You contribute
+are governed by the terms of this License, including without
+limitation Section 2.2. The Source Code version of Covered Code may
+be distributed only under the terms of this License or a future
+version of this License released under Section 6.1, and You must
+include a copy of this License with every copy of the Source Code
+You distribute. You may not offer or impose any terms on any Source
+Code version that alters or restricts the applicable version of
+this License or the recipients' rights hereunder. However, You
+may include an additional document offering the additional rights
+described in Section 3.5.
+
+3.2. Availability of Source Code
+
+Any Modification which You create or to which You contribute must
+be made available in Source Code form under the terms of this
+License either on the same media as an Executable version or via
+an accepted Electronic Distribution Mechanism to anyone to whom
+you made an Executable version available; and if made available
+via Electronic Distribution Mechanism, must remain available for
+at least twelve (12) months after the date it initially became
+available, or at least six (6) months after a subsequent version
+of that particular Modification has been made available to such
+recipients. You are responsible for ensuring that the Source Code
+version remains available even if the Electronic Distribution
+Mechanism is maintained by a third party.
+
+3.3. Description of Modifications
+
+You must cause all Covered Code to which You contribute to contain
+a file documenting the changes You made to create that Covered
+Code and the date of any change. You must include a prominent
+statement that the Modification is derived, directly or indirectly,
+from Original Code provided by the Initial Developer and including
+the name of the Initial Developer in (a) the Source Code, and (b)
+in any notice in an Executable version or related documentation in
+which You describe the origin or ownership of the Covered Code.
+
+3.4. Intellectual Property Matters
+
+3.4.a. Third Party Claims: If Contributor has knowledge that
+       a license under a third party's intellectual property
+       rights is required to exercise the rights granted by such
+       Contributor under Sections 2.1 or 2.2, Contributor must
+       include a text file with the Source Code distribution titled
+       "LEGAL" which describes the claim and the party making the
+       claim in sufficient detail that a recipient will know whom
+       to contact. If Contributor obtains such knowledge after the
+       Modification is made available as described in Section 3.2,
+       Contributor shall promptly modify the LEGAL file in all
+       copies Contributor makes available thereafter and shall take
+       other steps (such as notifying appropriate mailing lists or
+       newsgroups) reasonably calculated to inform those who received
+       the Covered Code that new knowledge has been obtained.
+
+3.4.b. Contributor APIs: If Contributor's Modifications include
+       an application programming interface and Contributor has
+       knowledge of patent licenses which are reasonably necessary
+       to implement that API, Contributor must also include this
+       information in the legal file.
+
+3.4.c. Representations: Contributor represents that, except as
+       disclosed pursuant to Section 3.4 (a) above, Contributor
+       believes that Contributor's Modifications are Contributor's
+       original creation(s) and/or Contributor has sufficient rights
+       to grant the rights conveyed by this License.
+
+3.5. Required Notices
+
+You must duplicate the notice in Exhibit A in each file of
+the Source Code. If it is not possible to put such notice in a
+particular Source Code file due to its structure, then You must
+include such notice in a location (such as a relevant directory)
+where a user would be likely to look for such a notice. If You
+created one or more Modification(s) You may add your name as a
+Contributor to the notice described in Exhibit A. You must also
+duplicate this License in any documentation for the Source Code
+where You describe recipients' rights or ownership rights relating
+to Covered Code. You may choose to offer, and to charge a fee for,
+warranty, support, indemnity or liability obligations to one or
+more recipients of Covered Code. However, You may do so only on
+Your own behalf, and not on behalf of the Initial Developer or
+any Contributor. You must make it absolutely clear than any such
+warranty, support, indemnity or liability obligation is offered by
+You alone, and You hereby agree to indemnify the Initial Developer
+and every Contributor for any liability incurred by the Initial
+Developer or such Contributor as a result of warranty, support,
+indemnity or liability terms You offer.
+
+3.6. Distribution of Executable Versions
+
+You may distribute Covered Code in Executable form only if the
+requirements of Sections 3.1, 3.2, 3.3, 3.4 and 3.5 have been met
+for that Covered Code, and if You include a notice stating that
+the Source Code version of the Covered Code is available under the
+terms of this License, including a description of how and where
+You have fulfilled the obligations of Section 3.2. The notice
+must be conspicuously included in any notice in an Executable
+version, related documentation or collateral in which You describe
+recipients' rights relating to the Covered Code. You may distribute
+the Executable version of Covered Code or ownership rights under
+a license of Your choice, which may contain terms different from
+this License, provided that You are in compliance with the terms
+of this License and that the license for the Executable version
+does not attempt to limit or alter the recipient's rights in the
+Source Code version from the rights set forth in this License. If
+You distribute the Executable version under a different license You
+must make it absolutely clear that any terms which differ from this
+License are offered by You alone, not by the Initial Developer or any
+Contributor. You hereby agree to indemnify the Initial Developer and
+every Contributor for any liability incurred by the Initial Developer
+or such Contributor as a result of any such terms You offer.
+
+3.7. Larger Works
+
+You may create a Larger Work by combining Covered Code with other
+code not governed by the terms of this License and distribute the
+Larger Work as a single product. In such a case, You must make sure
+the requirements of this License are fulfilled for the Covered Code.
+
+4. Inability to Comply Due to Statute or Regulation.
+
+If it is impossible for You to comply with any of the terms of
+this License with respect to some or all of the Covered Code due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description
+must be included in the legal file described in Section 3.4 and
+must be included with all distributions of the Source Code. Except
+to the extent prohibited by statute or regulation, such description
+must be sufficiently detailed for a recipient of ordinary skill to
+be able to understand it.
+
+5. Application of this License.
+
+This License applies to code to which the Initial Developer has
+attached the notice in Exhibit A and to related Covered Code.
+
+6. Versions of the License.
+
+6.1. New Versions
+
+The H2 Group may publish revised and/or new versions of the License
+from time to time. Each version will be given a distinguishing
+version number.
+
+6.2. Effect of New Versions
+
+Once Covered Code has been published under a particular version of
+the License, You may always continue to use it under the terms of
+that version. You may also choose to use such Covered Code under the
+terms of any subsequent version of the License published by the H2
+Group. No one other than the H2 Group has the right to modify the
+terms applicable to Covered Code created under this License.
+
+6.3. Derivative Works
+
+If You create or use a modified version of this License (which you
+may only do in order to apply it to code which is not already Covered
+Code governed by this License), You must (a) rename Your license so
+that the phrases "H2 Group", "H2" or any confusingly similar phrase
+do not appear in your license (except to note that your license
+differs from this License) and (b) otherwise make it clear that
+Your version of the license contains terms which differ from the
+H2 License. (Filling in the name of the Initial Developer, Original
+Code or Contributor in the notice described in Exhibit A shall not
+of themselves be deemed to be modifications of this License.)
+
+7. Disclaimer of Warranty
+
+Covered code is provided under this license on an "as is" basis,
+without warranty of any kind, either expressed or implied,
+including, without limitation, warranties that the covered code
+is free of defects, merchantable, fit for a particular purpose or
+non-infringing. The entire risk as to the quality and performance
+of the covered code is with you. Should any covered code prove
+defective in any respect, you (not the initial developer or any
+other contributor) assume the cost of any necessary servicing,
+repair or correction. This disclaimer of warranty constitutes
+an essential part of this license. No use of any covered code is
+authorized hereunder except under this disclaimer.
+
+8. Termination
+
+8.1. This License and the rights granted hereunder will terminate
+     automatically if You fail to comply with terms herein and
+     fail to cure such breach within 30 days of becoming aware
+     of the breach. All sublicenses to the Covered Code which
+     are properly granted shall survive any termination of this
+     License. Provisions which, by their nature, must remain in
+     effect beyond the termination of this License shall survive.
+
+8.2. If You initiate litigation by asserting a patent infringement
+     claim (excluding declaratory judgment actions) against
+     Initial Developer or a Contributor (the Initial Developer or
+     Contributor against whom You file such action is referred to as
+     "Participant") alleging that:
+
+8.2.a. such Participant's Contributor Version directly or indirectly
+       infringes any patent, then any and all rights granted by
+       such Participant to You under Sections 2.1 and/or 2.2 of this
+       License shall, upon 60 days notice from Participant terminate
+       prospectively, unless if within 60 days after receipt of
+       notice You either: (i) agree in writing to pay Participant
+       a mutually agreeable reasonable royalty for Your past and
+       future use of Modifications made by such Participant, or (ii)
+       withdraw Your litigation claim with respect to the Contributor
+       Version against such Participant. If within 60 days of notice,
+       a reasonable royalty and payment arrangement are not mutually
+       agreed upon in writing by the parties or the litigation claim
+       is not withdrawn, the rights granted by Participant to You
+       under Sections 2.1 and/or 2.2 automatically terminate at
+       the expiration of the 60 day notice period specified above.
+
+8.2.b. any software, hardware, or device, other than such
+       Participant's Contributor Version, directly or indirectly
+       infringes any patent, then any rights granted to You by
+       such Participant under Sections 2.1(b) and 2.2(b) are
+       revoked effective as of the date You first made, used,
+       sold, distributed, or had made, Modifications made by that
+       Participant.
+
+8.3. If You assert a patent infringement claim against Participant
+     alleging that such Participant's Contributor Version directly
+     or indirectly infringes any patent where such claim is resolved
+     (such as by license or settlement) prior to the initiation of
+     patent infringement litigation, then the reasonable value of
+     the licenses granted by such Participant under Sections 2.1
+     or 2.2 shall be taken into account in determining the amount
+     or value of any payment or license.
+
+8.4. In the event of termination under Sections 8.1 or 8.2 above,
+     all end user license agreements (excluding distributors and
+     resellers) which have been validly granted by You or any
+     distributor hereunder prior to termination shall survive
+     termination.
+
+9. Limitation of Liability
+
+Under no circumstances and under no legal theory, whether tort
+(including negligence), contract, or otherwise, shall you, the
+initial developer, any other contributor, or any distributor of
+covered code, or any supplier of any of such parties, be liable to
+any person for any indirect, special, incidental, or consequential
+damages of any character including, without limitation, damages for
+loss of goodwill, work stoppage, computer failure or malfunction, or
+any and all other commercial damages or losses, even if such party
+shall have been informed of the possibility of such damages. This
+limitation of liability shall not apply to liability for death or
+personal injury resulting from such party's negligence to the extent
+applicable law prohibits such limitation. Some jurisdictions do not
+allow the exclusion or limitation of incidental or consequential
+damages, so this exclusion and limitation may not apply to you.
+
+10. United States Government End Users
+
+The Covered Code is a "commercial item", as that term is defined in
+48 C.F.R. 2.101 (October 1995), consisting of "commercial computer
+software" and "commercial computer software documentation", as such
+terms are used in 48 C.F.R. 12.212 (September 1995). Consistent
+with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4
+(June 1995), all U.S. Government End Users acquire Covered Code
+with only those rights set forth herein.
+
+11. Miscellaneous
+
+This License represents the complete agreement concerning subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. This License shall be governed
+by California law provisions (except to the extent applicable
+law, if any, provides otherwise), excluding its conflict-of-law
+provisions. With respect to disputes in which at least one party is
+a citizen of, or an entity chartered or registered to do business in
+United States of America, any litigation relating to this License
+shall be subject to the jurisdiction of the Federal Courts of the
+Northern District of California, with venue lying in Santa Clara
+County, California, with the losing party responsible for costs,
+including without limitation, court costs and reasonable attorneys'
+fees and expenses. The application of the United Nations Convention
+on Contracts for the International Sale of Goods is expressly
+excluded. Any law or regulation which provides that the language of
+a contract shall be construed against the drafter shall not apply
+to this License.
+
+12. Responsibility for Claims
+
+As between Initial Developer and the Contributors, each party is
+responsible for claims and damages arising, directly or indirectly,
+out of its utilization of rights under this License and You agree
+to work with Initial Developer and Contributors to distribute such
+responsibility on an equitable basis. Nothing herein is intended
+or shall be deemed to constitute any admission of liability.
+
+13. Multiple-Licensed Code
+
+Initial Developer may designate portions of the Covered Code as
+"Multiple-Licensed". "Multiple-Licensed" means that the Initial
+Developer permits you to utilize portions of the Covered Code under
+Your choice of this or the alternative licenses, if any, specified
+by the Initial Developer in the file described in Exhibit A.
+
+Exhibit A
+
+Multiple-Licensed under the H2 License, Version 1.0,
+and under the Eclipse Public License, Version 1.0
+(http://h2database.com/html/license.html).
+Initial Developer: H2 Group
+----
+
+----
+Eclipse Public License - v 1.0
+
+THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
+PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION
+OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
+
+1. DEFINITIONS
+
+"Contribution" means:
+
+a) in the case of the initial Contributor, the initial code and
+   documentation distributed under this Agreement, and
+b) in the case of each subsequent Contributor:
+
+i) changes to the Program, and
+
+ii) additions to the Program;
+
+where such changes and/or additions to the Program originate from
+and are distributed by that particular Contributor. A Contribution
+'originates' from a Contributor if it was added to the Program
+by such Contributor itself or anyone acting on such Contributor's
+behalf. Contributions do not include additions to the Program which:
+(i) are separate modules of software distributed in conjunction
+with the Program under their own license agreement, and (ii) are
+not derivative works of the Program.
+
+"Contributor" means any person or entity that distributes the Program.
+
+"Licensed Patents " mean patent claims licensable by a Contributor
+which are necessarily infringed by the use or sale of its
+Contribution alone or when combined with the Program.
+
+"Program" means the Contributions distributed in accordance with
+this Agreement.
+
+"Recipient" means anyone who receives the Program under this
+Agreement, including all Contributors.
+
+2. GRANT OF RIGHTS
+
+a) Subject to the terms of this Agreement, each Contributor hereby
+   grants Recipient a non-exclusive, worldwide, royalty-free copyright
+   license to reproduce, prepare derivative works of, publicly display,
+   publicly perform, distribute and sublicense the Contribution of such
+   Contributor, if any, and such derivative works, in source code and
+   object code form.
+
+b) Subject to the terms of this Agreement, each Contributor hereby
+   grants Recipient a non-exclusive, worldwide, royalty-free patent
+   license under Licensed Patents to make, use, sell, offer to sell,
+   import and otherwise transfer the Contribution of such Contributor,
+   if any, in source code and object code form. This patent license
+   shall apply to the combination of the Contribution and the Program
+   if, at the time the Contribution is added by the Contributor, such
+   addition of the Contribution causes such combination to be covered
+   by the Licensed Patents. The patent license shall not apply to any
+   other combinations which include the Contribution. No hardware per
+   se is licensed hereunder.
+
+c) Recipient understands that although each Contributor grants the
+   licenses to its Contributions set forth herein, no assurances are
+   provided by any Contributor that the Program does not infringe
+   the patent or other intellectual property rights of any other
+   entity. Each Contributor disclaims any liability to Recipient
+   for claims brought by any other entity based on infringement
+   of intellectual property rights or otherwise. As a condition to
+   exercising the rights and licenses granted hereunder, each Recipient
+   hereby assumes sole responsibility to secure any other intellectual
+   property rights needed, if any. For example, if a third party patent
+   license is required to allow Recipient to distribute the Program,
+   it is Recipient's responsibility to acquire that license before
+   distributing the Program.
+
+d) Each Contributor represents that to its knowledge it has
+   sufficient copyright rights in its Contribution, if any, to grant
+   the copyright license set forth in this Agreement.
+
+3. REQUIREMENTS
+
+A Contributor may choose to distribute the Program in object code
+  form under its own license agreement, provided that:
+
+a) it complies with the terms and conditions of this Agreement; and
+
+b) its license agreement:
+
+i) effectively disclaims on behalf of all Contributors all warranties
+   and conditions, express and implied, including warranties or
+   conditions of title and non-infringement, and implied warranties or
+   conditions of merchantability and fitness for a particular purpose;
+
+ii) effectively excludes on behalf of all Contributors all liability
+    for damages, including direct, indirect, special, incidental and
+    consequential damages, such as lost profits;
+
+iii) states that any provisions which differ from this Agreement
+     are offered by that Contributor alone and not by any other
+     party; and
+
+iv) states that source code for the Program is available from such
+    Contributor, and informs licensees how to obtain it in a reasonable
+    manner on or through a medium customarily used for software exchange.
+
+When the Program is made available in source code form:
+
+a) it must be made available under this Agreement; and
+
+b) a copy of this Agreement must be included with each copy of the Program.
+
+Contributors may not remove or alter any copyright notices contained
+within the Program.
+
+Each Contributor must identify itself as the originator of its
+Contribution, if any, in a manner that reasonably allows subsequent
+Recipients to identify the originator of the Contribution.
+
+4. COMMERCIAL DISTRIBUTION
+
+Commercial distributors of software may accept certain
+responsibilities with respect to end users, business partners and the
+like. While this license is intended to facilitate the commercial
+use of the Program, the Contributor who includes the Program in a
+commercial product offering should do so in a manner which does not
+create potential liability for other Contributors. Therefore, if a
+Contributor includes the Program in a commercial product offering,
+such Contributor ("Commercial Contributor") hereby agrees to defend
+and indemnify every other Contributor ("Indemnified Contributor")
+against any losses, damages and costs (collectively "Losses") arising
+from claims, lawsuits and other legal actions brought by a third
+party against the Indemnified Contributor to the extent caused by
+the acts or omissions of such Commercial Contributor in connection
+with its distribution of the Program in a commercial product
+offering. The obligations in this section do not apply to any claims
+or Losses relating to any actual or alleged intellectual property
+infringement. In order to qualify, an Indemnified Contributor must:
+a) promptly notify the Commercial Contributor in writing of such
+claim, and b) allow the Commercial Contributor to control, and
+cooperate with the Commercial Contributor in, the defense and any
+related settlement negotiations. The Indemnified Contributor may
+participate in any such claim at its own expense.
+
+For example, a Contributor might include the Program in a
+commercial product offering, Product X. That Contributor is then a
+Commercial Contributor. If that Commercial Contributor then makes
+performance claims, or offers warranties related to Product X, those
+performance claims and warranties are such Commercial Contributor's
+responsibility alone. Under this section, the Commercial Contributor
+would have to defend claims against the other Contributors related
+to those performance claims and warranties, and if a court requires
+any other Contributor to pay any damages as a result, the Commercial
+Contributor must pay those damages.
+
+5. NO WARRANTY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS
+PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY
+WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY
+OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely
+responsible for determining the appropriateness of using and
+distributing the Program and assumes all risks associated with
+its exercise of rights under this Agreement , including but not
+limited to the risks and costs of program errors, compliance with
+applicable laws, damage to or loss of data, programs or equipment,
+and unavailability or interruption of operations.
+
+6. DISCLAIMER OF LIABILITY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT
+NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING WITHOUT LIMITATION LOST PROFITS), 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 OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY
+RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+7. GENERAL
+
+If any provision of this Agreement is invalid or unenforceable under
+applicable law, it shall not affect the validity or enforceability of
+the remainder of the terms of this Agreement, and without further
+action by the parties hereto, such provision shall be reformed
+to the minimum extent necessary to make such provision valid and
+enforceable.
+
+If Recipient institutes patent litigation against any entity
+(including a cross-claim or counterclaim in a lawsuit) alleging
+that the Program itself (excluding combinations of the Program with
+other software or hardware) infringes such Recipient's patent(s),
+then such Recipient's rights granted under Section 2(b) shall
+terminate as of the date such litigation is filed.
+
+All Recipient's rights under this Agreement shall terminate if
+it fails to comply with any of the material terms or conditions
+of this Agreement and does not cure such failure in a reasonable
+period of time after becoming aware of such noncompliance. If all
+Recipient's rights under this Agreement terminate, Recipient agrees
+to cease use and distribution of the Program as soon as reasonably
+practicable. However, Recipient's obligations under this Agreement
+and any licenses granted by Recipient relating to the Program shall
+continue and survive.
+
+Everyone is permitted to copy and distribute copies of this
+Agreement, but in order to avoid inconsistency the Agreement is
+copyrighted and may only be modified in the following manner. The
+Agreement Steward reserves the right to publish new versions
+(including revisions) of this Agreement from time to time. No
+one other than the Agreement Steward has the right to modify
+this Agreement. The Eclipse Foundation is the initial Agreement
+Steward. The Eclipse Foundation may assign the responsibility to
+serve as the Agreement Steward to a suitable separate entity. Each
+new version of the Agreement will be given a distinguishing
+version number. The Program (including Contributions) may always be
+distributed subject to the version of the Agreement under which it
+was received. In addition, after a new version of the Agreement is
+published, Contributor may elect to distribute the Program (including
+its Contributions) under the new version. Except as expressly stated
+in Sections 2(a) and 2(b) above, Recipient receives no rights or
+licenses to the intellectual property of any Contributor under
+this Agreement, whether expressly, by implication, estoppel or
+otherwise. All rights in the Program not expressly granted under
+this Agreement are reserved.
+
+This Agreement is governed by the laws of the State of New York and
+the intellectual property laws of the United States of America. No
+party to this Agreement will bring a legal action under this
+Agreement more than one year after the cause of action arose. Each
+party waives its rights to a jury trial in any resulting litigation.
+----
+
+----
+Export Control Classification Number (ECCN)
+
+As far as we know, the U.S. Export Control Classification Number
+(ECCN) for this software is 5D002. However, for legal reasons, we
+can make no warranty that this information is correct. For details,
+see also the Apache Software Foundation Export Classifications page.
+
+----
+
+
+[[highlightjs]]
+highlightjs
+
+* js:highlightjs
+* js:highlightjs_files
+
+[[highlightjs_license]]
+----
+Copyright (c) 2006, Ivan Sagalaev
+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 highlight.js 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 REGENTS 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 REGENTS AND 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.
+----
+
+
+[[icu4j]]
+icu4j
+
+* icu4j
+
+[[icu4j_license]]
+----
+COPYRIGHT AND PERMISSION NOTICE (ICU 58 and later)
+
+Copyright © 1991-2016 Unicode, Inc. All rights reserved.
+Distributed under the Terms of Use in http://www.unicode.org/copyright.html
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Unicode data files and any associated documentation
+(the "Data Files") or Unicode software and any associated documentation
+(the "Software") to deal in the Data Files or Software
+without restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, and/or sell copies of
+the Data Files or Software, and to permit persons to whom the Data Files
+or Software are furnished to do so, provided that either
+(a) this copyright and permission notice appear with all copies
+of the Data Files or Software, or
+(b) this copyright and permission notice appear in associated
+Documentation.
+
+THE DATA FILES AND SOFTWARE ARE 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 OF THIRD PARTY RIGHTS.
+IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS
+NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL
+DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
+DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
+TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THE DATA FILES OR SOFTWARE.
+
+Except as contained in this notice, the name of a copyright holder
+shall not be used in advertising or otherwise to promote the sale,
+use or other dealings in these Data Files or Software without prior
+written authorization of the copyright holder.
+
+---------------------
+
+Third-Party Software Licenses
+
+This section contains third-party software notices and/or additional
+terms for licensed third-party software components included within ICU
+libraries.
+
+1. ICU License - ICU 1.8.1 to ICU 57.1
+
+COPYRIGHT AND PERMISSION NOTICE
+
+Copyright (c) 1995-2016 International Business Machines Corporation and others
+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, and/or sell copies of the Software, and to permit persons
+to whom the Software is furnished to do so, provided that the above
+copyright notice(s) and this permission notice appear in all copies of
+the Software and that both the above copyright notice(s) and this
+permission notice appear in supporting documentation.
+
+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
+OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY
+SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER
+RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
+CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+Except as contained in this notice, the name of a copyright holder
+shall not be used in advertising or otherwise to promote the sale, use
+or other dealings in this Software without prior written authorization
+of the copyright holder.
+
+All trademarks and registered trademarks mentioned herein are the
+property of their respective owners.
+
+2. Chinese/Japanese Word Break Dictionary Data (cjdict.txt)
+
+ #     The Google Chrome software developed by Google is licensed under
+ # the BSD license. Other software included in this distribution is
+ # provided under other licenses, as set forth below.
+ #
+ #  The BSD License
+ #  http://opensource.org/licenses/bsd-license.php
+ #  Copyright (C) 2006-2008, Google Inc.
+ #
+ #  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.
+ #
+ #
+ #  The word list in cjdict.txt are generated by combining three word lists
+ # listed below with further processing for compound word breaking. The
+ # frequency is generated with an iterative training against Google web
+ # corpora.
+ #
+ #  * Libtabe (Chinese)
+ #    - https://sourceforge.net/project/?group_id=1519
+ #    - Its license terms and conditions are shown below.
+ #
+ #  * IPADIC (Japanese)
+ #    - http://chasen.aist-nara.ac.jp/chasen/distribution.html
+ #    - Its license terms and conditions are shown below.
+ #
+ #  ---------COPYING.libtabe ---- BEGIN--------------------
+ #
+ #  /*
+ #   * Copyrighy (c) 1999 TaBE Project.
+ #   * Copyright (c) 1999 Pai-Hsiang Hsiao.
+ #   * All rights reserved.
+ #   *
+ #   * Redistribution and use in source and binary forms, with or without
+ #   * modification, are permitted provided that the following conditions
+ #   * are met:
+ #   *
+ #   * . Redistributions of source code must retain the above copyright
+ #   *   notice, this list of conditions and the following disclaimer.
+ #   * . Redistributions in binary form must reproduce the above copyright
+ #   *   notice, this list of conditions and the following disclaimer in
+ #   *   the documentation and/or other materials provided with the
+ #   *   distribution.
+ #   * . Neither the name of the TaBE Project 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
+ #   * REGENTS 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.
+ #   */
+ #
+ #  /*
+ #   * Copyright (c) 1999 Computer Systems and Communication Lab,
+ #   *                    Institute of Information Science, Academia
+ #       *                    Sinica. All rights reserved.
+ #   *
+ #   * Redistribution and use in source and binary forms, with or without
+ #   * modification, are permitted provided that the following conditions
+ #   * are met:
+ #   *
+ #   * . Redistributions of source code must retain the above copyright
+ #   *   notice, this list of conditions and the following disclaimer.
+ #   * . Redistributions in binary form must reproduce the above copyright
+ #   *   notice, this list of conditions and the following disclaimer in
+ #   *   the documentation and/or other materials provided with the
+ #   *   distribution.
+ #   * . Neither the name of the Computer Systems and Communication Lab
+ #   *   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
+ #   * REGENTS 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.
+ #   */
+ #
+ #  Copyright 1996 Chih-Hao Tsai @ Beckman Institute,
+ #      University of Illinois
+ #  c-tsai4@uiuc.edu  http://casper.beckman.uiuc.edu/~c-tsai4
+ #
+ #  ---------------COPYING.libtabe-----END--------------------------------
+ #
+ #
+ #  ---------------COPYING.ipadic-----BEGIN-------------------------------
+ #
+ #  Copyright 2000, 2001, 2002, 2003 Nara Institute of Science
+ #  and Technology.  All Rights Reserved.
+ #
+ #  Use, reproduction, and distribution of this software is permitted.
+ #  Any copy of this software, whether in its original form or modified,
+ #  must include both the above copyright notice and the following
+ #  paragraphs.
+ #
+ #  Nara Institute of Science and Technology (NAIST),
+ #  the copyright holders, disclaims all warranties with regard to this
+ #  software, including all implied warranties of merchantability and
+ #  fitness, in no event shall NAIST be liable for
+ #  any special, indirect or consequential damages or any damages
+ #  whatsoever resulting from loss of use, data or profits, whether in an
+ #  action of contract, negligence or other tortuous action, arising out
+ #  of or in connection with the use or performance of this software.
+ #
+ #  A large portion of the dictionary entries
+ #  originate from ICOT Free Software.  The following conditions for ICOT
+ #  Free Software applies to the current dictionary as well.
+ #
+ #  Each User may also freely distribute the Program, whether in its
+ #  original form or modified, to any third party or parties, PROVIDED
+ #  that the provisions of Section 3 ("NO WARRANTY") will ALWAYS appear
+ #  on, or be attached to, the Program, which is distributed substantially
+ #  in the same form as set out herein and that such intended
+ #  distribution, if actually made, will neither violate or otherwise
+ #  contravene any of the laws and regulations of the countries having
+ #  jurisdiction over the User or the intended distribution itself.
+ #
+ #  NO WARRANTY
+ #
+ #  The program was produced on an experimental basis in the course of the
+ #  research and development conducted during the project and is provided
+ #  to users as so produced on an experimental basis.  Accordingly, the
+ #  program is provided without any warranty whatsoever, whether express,
+ #  implied, statutory or otherwise.  The term "warranty" used herein
+ #  includes, but is not limited to, any warranty of the quality,
+ #  performance, merchantability and fitness for a particular purpose of
+ #  the program and the nonexistence of any infringement or violation of
+ #  any right of any third party.
+ #
+ #  Each user of the program will agree and understand, and be deemed to
+ #  have agreed and understood, that there is no warranty whatsoever for
+ #  the program and, accordingly, the entire risk arising from or
+ #  otherwise connected with the program is assumed by the user.
+ #
+ #  Therefore, neither ICOT, the copyright holder, or any other
+ #  organization that participated in or was otherwise related to the
+ #  development of the program and their respective officials, directors,
+ #  officers and other employees shall be held liable for any and all
+ #  damages, including, without limitation, general, special, incidental
+ #  and consequential damages, arising out of or otherwise in connection
+ #  with the use or inability to use the program or any product, material
+ #  or result produced or otherwise obtained by using the program,
+ #  regardless of whether they have been advised of, or otherwise had
+ #  knowledge of, the possibility of such damages at any time during the
+ #  project or thereafter.  Each user will be deemed to have agreed to the
+ #  foregoing by his or her commencement of use of the program.  The term
+ #  "use" as used herein includes, but is not limited to, the use,
+ #  modification, copying and distribution of the program and the
+ #  production of secondary products from the program.
+ #
+ #  In the case where the program, whether in its original form or
+ #  modified, was distributed or delivered to or received by a user from
+ #  any person, organization or entity other than ICOT, unless it makes or
+ #  grants independently of ICOT any specific warranty to the user in
+ #  writing, such person, organization or entity, will also be exempted
+ #  from and not be held liable to the user for any such damages as noted
+ #  above as far as the program is concerned.
+ #
+ #  ---------------COPYING.ipadic-----END----------------------------------
+
+3. Lao Word Break Dictionary Data (laodict.txt)
+
+ #  Copyright (c) 2013 International Business Machines Corporation
+ #  and others. All Rights Reserved.
+ #
+ # Project: http://code.google.com/p/lao-dictionary/
+ # Dictionary: http://lao-dictionary.googlecode.com/git/Lao-Dictionary.txt
+ # License: http://lao-dictionary.googlecode.com/git/Lao-Dictionary-LICENSE.txt
+ #              (copied below)
+ #
+ #  This file is derived from the above dictionary, with slight
+ #  modifications.
+ #  ----------------------------------------------------------------------
+ #  Copyright (C) 2013 Brian Eugene Wilson, Robert Martin Campbell.
+ #  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.
+ #
+ #
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ # OF THE POSSIBILITY OF SUCH DAMAGE.
+ #  --------------------------------------------------------------------------
+
+4. Burmese Word Break Dictionary Data (burmesedict.txt)
+
+ #  Copyright (c) 2014 International Business Machines Corporation
+ #  and others. All Rights Reserved.
+ #
+ #  This list is part of a project hosted at:
+ #    github.com/kanyawtech/myanmar-karen-word-lists
+ #
+ #  --------------------------------------------------------------------------
+ #  Copyright (c) 2013, LeRoy Benjamin Sharon
+ #  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 Myanmar Karen Word Lists, nor the names of its
+ #    contributors may be used to endorse or promote products derived
+ #    from this software without specific prior written permission.
+ #
+ #  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ #  CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ #  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ #  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ #  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
+ #  BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ #  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+ #  TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ #  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ #  ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ #  TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ #  THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ #  SUCH DAMAGE.
+ #  --------------------------------------------------------------------------
+
+5. Time Zone Database
+
+  ICU uses the public domain data and code derived from Time Zone
+Database for its time zone support. The ownership of the TZ database
+is explained in BCP 175: Procedure for Maintaining the Time Zone
+Database section 7.
+
+ # 7.  Database Ownership
+ #
+ #    The TZ database itself is not an IETF Contribution or an IETF
+ #    document.  Rather it is a pre-existing and regularly updated work
+ #    that is in the public domain, and is intended to remain in the
+ #    public domain.  Therefore, BCPs 78 [RFC5378] and 79 [RFC3979] do
+ #    not apply to the TZ Database or contributions that individuals make
+ #    to it.  Should any claims be made and substantiated against the TZ
+ #    Database, the organization that is providing the IANA
+ #    Considerations defined in this RFC, under the memorandum of
+ #    understanding with the IETF, currently ICANN, may act in accordance
+ #    with all competent court orders.  No ownership claims will be made
+ #    by ICANN or the IETF Trust on the database or the code.  Any person
+ #    making a contribution to the database or code waives all rights to
+ #    future claims in that contribution or in the TZ Database.
+
+----
+
+
+[[jgit]]
+jgit
+
+* jgit/org.eclipse.jgit.archive:jgit-archive
+* jgit/org.eclipse.jgit.http.server:jgit-servlet
+* jgit/org.eclipse.jgit:jgit
+
+[[jgit_license]]
+----
+This program and the accompanying materials are made available
+under the terms of the Eclipse Distribution License v1.0 which
+accompanies this distribution, is reproduced below, and is
+available at http://www.eclipse.org/org/documents/edl-v10.php
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or
+without modification, are permitted provided that the following
+conditions are met:
+
+- Redistributions of source code must retain the above copyright
+  notice, this list of conditions and the following disclaimer.
+
+- Redistributions in binary form must reproduce the above
+  copyright notice, this list of conditions and the following
+  disclaimer in the documentation and/or other materials provided
+  with the distribution.
+
+- Neither the name of the Eclipse Foundation, 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.
+
+----
+
+
+[[jsch]]
+jsch
+
+* jsch
+
+[[jsch_license]]
+----
+Copyright (c) 2002-2012 Atsuhiko Yamanaka, JCraft,Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+  1. Redistributions of source code must retain the above copyright notice,
+     this list of conditions and the following disclaimer.
+
+  2. Redistributions in binary form must reproduce the above copyright
+     notice, this list of conditions and the following disclaimer in
+     the documentation and/or other materials provided with the distribution.
+
+  3. The names of the authors may not be used to endorse or promote products
+     derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED 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 JCRAFT,
+INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE 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.
+
+----
+
+
+[[jsoup]]
+jsoup
+
+* jsoup:jsoup
+
+[[jsoup_license]]
+----
+The MIT License
+
+© 2009-2016, Jonathan Hedley <jonathan@hedley.net>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+----
+
+
+[[moment]]
+moment
+
+* js:moment
+
+[[moment_license]]
+----
+Copyright (c) 2011-2016 Tim Wood, Iskren Chernev, Moment.js contributors
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+----
+
+
+[[ow2]]
+ow2
+
+* ow2:ow2-asm
+* ow2:ow2-asm-analysis
+* ow2:ow2-asm-commons
+* ow2:ow2-asm-tree
+* ow2:ow2-asm-util
+
+[[ow2_license]]
+----
+Copyright (c) 2000-2011 INRIA, France Telecom
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holders 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.
+
+----
+
+
+[[page_js]]
+page.js
+
+* js:page
+
+[[page_js_license]]
+----
+(The MIT License)
+
+Copyright (c) 2012 TJ Holowaychuk <tj@vision-media.ca>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the 'Software'), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+----
+
+
+[[polymer]]
+polymer
+
+* js:font-roboto-local
+* js:iron-a11y-announcer
+* js:iron-a11y-keys-behavior
+* js:iron-autogrow-textarea
+* js:iron-behaviors
+* js:iron-checked-element-behavior
+* js:iron-dropdown
+* js:iron-fit-behavior
+* js:iron-flex-layout
+* js:iron-form-element-behavior
+* js:iron-icon
+* js:iron-iconset-svg
+* js:iron-input
+* js:iron-menu-behavior
+* js:iron-meta
+* js:iron-overlay-behavior
+* js:iron-resizable-behavior
+* js:iron-selector
+* js:iron-validatable-behavior
+* js:neon-animation
+* js:paper-behaviors
+* js:paper-button
+* js:paper-icon-button
+* js:paper-input
+* js:paper-item
+* js:paper-listbox
+* js:paper-ripple
+* js:paper-styles
+* js:paper-tabs
+* js:paper-toggle-button
+* js:polymer
+* js:polymer-resin
+* js:webcomponentsjs
+
+[[polymer_license]]
+----
+Copyright (c) 2014 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.
+
+----
+
+
+[[prologcafe]]
+prologcafe
+
+* prolog:cafeteria
+* prolog:compiler
+* prolog:io
+* prolog:runtime
+
+[[prologcafe_license]]
+----
+Prolog Cafe (A Prolog to Java Translator System)
+Copyright (C) 1997-2009 by Mutsunori Banbara and Naoyuki Tamura
+
+Prolog Cafe is free software; you can redistribute it and/or modify
+it under the terms of either:
+
+  * the GNU General Public License as published by the Free Software
+    Foundation; either version 2 of the License, or (at your option)
+    any later version, or
+
+  * the Eclipse Public License
+----
+
+In the context of Gerrit Code Review, Prolog Cafe is consumed under
+the <<prologcafe_EPL,EPL>>. Gerrit Code Review uses a fork derived
+from the 1.2.5 release and offers the corresponding source code at
+link:https://gerrit.googlesource.com/prolog-cafe[].
+
+----
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                            NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
+----
+
+[[prologcafe_EPL]]
+----
+Eclipse Public License - v 1.0
+
+THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
+PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION
+OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
+
+1. DEFINITIONS
+
+"Contribution" means:
+
+a) in the case of the initial Contributor, the initial code and
+   documentation distributed under this Agreement, and
+b) in the case of each subsequent Contributor:
+
+i) changes to the Program, and
+
+ii) additions to the Program;
+
+where such changes and/or additions to the Program originate from
+and are distributed by that particular Contributor. A Contribution
+'originates' from a Contributor if it was added to the Program
+by such Contributor itself or anyone acting on such Contributor's
+behalf. Contributions do not include additions to the Program which:
+(i) are separate modules of software distributed in conjunction
+with the Program under their own license agreement, and (ii) are
+not derivative works of the Program.
+
+"Contributor" means any person or entity that distributes the Program.
+
+"Licensed Patents " mean patent claims licensable by a Contributor
+which are necessarily infringed by the use or sale of its
+Contribution alone or when combined with the Program.
+
+"Program" means the Contributions distributed in accordance with
+this Agreement.
+
+"Recipient" means anyone who receives the Program under this
+Agreement, including all Contributors.
+
+2. GRANT OF RIGHTS
+
+a) Subject to the terms of this Agreement, each Contributor hereby
+   grants Recipient a non-exclusive, worldwide, royalty-free copyright
+   license to reproduce, prepare derivative works of, publicly display,
+   publicly perform, distribute and sublicense the Contribution of such
+   Contributor, if any, and such derivative works, in source code and
+   object code form.
+
+b) Subject to the terms of this Agreement, each Contributor hereby
+   grants Recipient a non-exclusive, worldwide, royalty-free patent
+   license under Licensed Patents to make, use, sell, offer to sell,
+   import and otherwise transfer the Contribution of such Contributor,
+   if any, in source code and object code form. This patent license
+   shall apply to the combination of the Contribution and the Program
+   if, at the time the Contribution is added by the Contributor, such
+   addition of the Contribution causes such combination to be covered
+   by the Licensed Patents. The patent license shall not apply to any
+   other combinations which include the Contribution. No hardware per
+   se is licensed hereunder.
+
+c) Recipient understands that although each Contributor grants the
+   licenses to its Contributions set forth herein, no assurances are
+   provided by any Contributor that the Program does not infringe
+   the patent or other intellectual property rights of any other
+   entity. Each Contributor disclaims any liability to Recipient
+   for claims brought by any other entity based on infringement
+   of intellectual property rights or otherwise. As a condition to
+   exercising the rights and licenses granted hereunder, each Recipient
+   hereby assumes sole responsibility to secure any other intellectual
+   property rights needed, if any. For example, if a third party patent
+   license is required to allow Recipient to distribute the Program,
+   it is Recipient's responsibility to acquire that license before
+   distributing the Program.
+
+d) Each Contributor represents that to its knowledge it has
+   sufficient copyright rights in its Contribution, if any, to grant
+   the copyright license set forth in this Agreement.
+
+3. REQUIREMENTS
+
+A Contributor may choose to distribute the Program in object code
+  form under its own license agreement, provided that:
+
+a) it complies with the terms and conditions of this Agreement; and
+
+b) its license agreement:
+
+i) effectively disclaims on behalf of all Contributors all warranties
+   and conditions, express and implied, including warranties or
+   conditions of title and non-infringement, and implied warranties or
+   conditions of merchantability and fitness for a particular purpose;
+
+ii) effectively excludes on behalf of all Contributors all liability
+    for damages, including direct, indirect, special, incidental and
+    consequential damages, such as lost profits;
+
+iii) states that any provisions which differ from this Agreement
+     are offered by that Contributor alone and not by any other
+     party; and
+
+iv) states that source code for the Program is available from such
+    Contributor, and informs licensees how to obtain it in a reasonable
+    manner on or through a medium customarily used for software exchange.
+
+When the Program is made available in source code form:
+
+a) it must be made available under this Agreement; and
+
+b) a copy of this Agreement must be included with each copy of the Program.
+
+Contributors may not remove or alter any copyright notices contained
+within the Program.
+
+Each Contributor must identify itself as the originator of its
+Contribution, if any, in a manner that reasonably allows subsequent
+Recipients to identify the originator of the Contribution.
+
+4. COMMERCIAL DISTRIBUTION
+
+Commercial distributors of software may accept certain
+responsibilities with respect to end users, business partners and the
+like. While this license is intended to facilitate the commercial
+use of the Program, the Contributor who includes the Program in a
+commercial product offering should do so in a manner which does not
+create potential liability for other Contributors. Therefore, if a
+Contributor includes the Program in a commercial product offering,
+such Contributor ("Commercial Contributor") hereby agrees to defend
+and indemnify every other Contributor ("Indemnified Contributor")
+against any losses, damages and costs (collectively "Losses") arising
+from claims, lawsuits and other legal actions brought by a third
+party against the Indemnified Contributor to the extent caused by
+the acts or omissions of such Commercial Contributor in connection
+with its distribution of the Program in a commercial product
+offering. The obligations in this section do not apply to any claims
+or Losses relating to any actual or alleged intellectual property
+infringement. In order to qualify, an Indemnified Contributor must:
+a) promptly notify the Commercial Contributor in writing of such
+claim, and b) allow the Commercial Contributor to control, and
+cooperate with the Commercial Contributor in, the defense and any
+related settlement negotiations. The Indemnified Contributor may
+participate in any such claim at its own expense.
+
+For example, a Contributor might include the Program in a
+commercial product offering, Product X. That Contributor is then a
+Commercial Contributor. If that Commercial Contributor then makes
+performance claims, or offers warranties related to Product X, those
+performance claims and warranties are such Commercial Contributor's
+responsibility alone. Under this section, the Commercial Contributor
+would have to defend claims against the other Contributors related
+to those performance claims and warranties, and if a court requires
+any other Contributor to pay any damages as a result, the Commercial
+Contributor must pay those damages.
+
+5. NO WARRANTY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS
+PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY
+WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY
+OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely
+responsible for determining the appropriateness of using and
+distributing the Program and assumes all risks associated with
+its exercise of rights under this Agreement , including but not
+limited to the risks and costs of program errors, compliance with
+applicable laws, damage to or loss of data, programs or equipment,
+and unavailability or interruption of operations.
+
+6. DISCLAIMER OF LIABILITY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT
+NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING WITHOUT LIMITATION LOST PROFITS), 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 OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY
+RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+7. GENERAL
+
+If any provision of this Agreement is invalid or unenforceable under
+applicable law, it shall not affect the validity or enforceability of
+the remainder of the terms of this Agreement, and without further
+action by the parties hereto, such provision shall be reformed
+to the minimum extent necessary to make such provision valid and
+enforceable.
+
+If Recipient institutes patent litigation against any entity
+(including a cross-claim or counterclaim in a lawsuit) alleging
+that the Program itself (excluding combinations of the Program with
+other software or hardware) infringes such Recipient's patent(s),
+then such Recipient's rights granted under Section 2(b) shall
+terminate as of the date such litigation is filed.
+
+All Recipient's rights under this Agreement shall terminate if
+it fails to comply with any of the material terms or conditions
+of this Agreement and does not cure such failure in a reasonable
+period of time after becoming aware of such noncompliance. If all
+Recipient's rights under this Agreement terminate, Recipient agrees
+to cease use and distribution of the Program as soon as reasonably
+practicable. However, Recipient's obligations under this Agreement
+and any licenses granted by Recipient relating to the Program shall
+continue and survive.
+
+Everyone is permitted to copy and distribute copies of this
+Agreement, but in order to avoid inconsistency the Agreement is
+copyrighted and may only be modified in the following manner. The
+Agreement Steward reserves the right to publish new versions
+(including revisions) of this Agreement from time to time. No
+one other than the Agreement Steward has the right to modify
+this Agreement. The Eclipse Foundation is the initial Agreement
+Steward. The Eclipse Foundation may assign the responsibility to
+serve as the Agreement Steward to a suitable separate entity. Each
+new version of the Agreement will be given a distinguishing
+version number. The Program (including Contributions) may always be
+distributed subject to the version of the Agreement under which it
+was received. In addition, after a new version of the Agreement is
+published, Contributor may elect to distribute the Program (including
+its Contributions) under the new version. Except as expressly stated
+in Sections 2(a) and 2(b) above, Recipient receives no rights or
+licenses to the intellectual property of any Contributor under
+this Agreement, whether expressly, by implication, estoppel or
+otherwise. All rights in the Program not expressly granted under
+this Agreement are reserved.
+
+This Agreement is governed by the laws of the State of New York and
+the intellectual property laws of the United States of America. No
+party to this Agreement will bring a legal action under this
+Agreement more than one year after the cause of action arose. Each
+party waives its rights to a jury trial in any resulting litigation.
+
+----
+
+
+[[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
+
+* protobuf
+
+[[protobuf_license]]
+----
+Copyright 2008, Google Inc.
+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.
+
+Code generated by the Protocol Buffer compiler is owned by the owner
+of the input file used when generating it.  This code is not
+standalone and requires a support library to be linked with it.  This
+support library is itself covered by the above license.
+
+----
+
+
+[[slf4j]]
+slf4j
+
+* log:api
+* log:jcl-over-slf4j
+
+[[slf4j_license]]
+----
+Copyright (c) 2004-2008 QOS.ch
+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.
+
+----
+
+
+[[xz]]
+xz
+
+* tukaani-xz
+
+[[xz_license]]
+----
+All the files in this package have been written by Lasse Collin
+and/or Igor Pavlov. All these files have been put into the
+public domain. You can do whatever you want with these files.
+This software is provided "as is", without any warranty.
+
+----
+
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index 37d6d01..064859d 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -23,10 +23,12 @@
 === Pushes
 
 * `receivecommits/changes`: histogram of number of changes processed
-in a single upload, split up by update type (new change created,
-existing changed updated, change autoclosed).
+in a single upload, split up by update type (change created/updated,
+change autoclosed).
 * `receivecommits/latency`: latency per change for processing a push,
 split up by update type (create+replace, and autoclose)
+* `receivecommits/push_latency`: total latency for processing a push,
+split up by update type (create+replace, autoclose, normal)
 * `receivecommits/timeout`: number of timeouts during push processing.
 
 === Process
@@ -106,10 +108,6 @@
 * `sshd/sessions/created`: Rate of new SSH sessions.
 * `sshd/sessions/authentication_failures`: Rate of SSH authentication failures.
 
-=== SQL connections
-
-* `sql/connection_pool/connections`: SQL database connections.
-
 === Topics
 
 * `topic/cross_project_submit`: number of cross-project topic submissions.
@@ -140,9 +138,6 @@
 * `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/auto_rebuild_latency`: NoteDb auto-rebuilding latency by table.
-* `notedb/auto_rebuild_failure_count`: NoteDb auto-rebuilding attempts that
-failed by table.
 * `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.
@@ -177,6 +172,10 @@
 * `plugin/latency`: Latency for plugin invocation.
 * `plugin/error_count`: Number of plugin errors.
 
+=== Group
+
+* `group/guess_relevant_groups_latency`: Latency for guessing relevant groups.
+
 === Replication Plugin
 
 * `plugins/replication/replication_latency`: Time spent pushing to remote
diff --git a/Documentation/note-db.txt b/Documentation/note-db.txt
index 555120a..8725cee 100644
--- a/Documentation/note-db.txt
+++ b/Documentation/note-db.txt
@@ -1,8 +1,8 @@
 = Gerrit Code Review - NoteDb Backend
 
 NoteDb is the next generation of Gerrit storage backend, which replaces the
-traditional SQL backend for change and account metadata with storing data in the
-same repository as code changes.
+traditional SQL backend for change, account and group metadata with storing
+data in the same repository as code changes.
 
 .Advantages
 - *Simplicity*: All data is stored in one location in the site directory, rather
@@ -32,12 +32,15 @@
   2.15 release. Account data is migrated automatically during the upgrade
   process by running `gerrit.war init`.
 - Storing link:config-groups.html[group metadata] is fully implemented
-  for the 2.16 release. Group data is migrated automatically during
+  in the 2.16 release. Group data is migrated automatically during
   the upgrade process by running `gerrit.war init`
 - Account, group and change metadata on the servers behind `googlesource.com` is fully
   migrated to NoteDb. In other words, if you use
   link:https://gerrit-review.googlesource.com/[gerrit-review], you're already
   using NoteDb.
+- NoteDb is the only database format supported by Gerrit 3.0. The change data
+  migration tools are only included in Gerrit 2.15 and 2.16; they are not
+  available in 3.0.
 
 For an example NoteDb change, poke around at this one:
 ----
@@ -45,12 +48,6 @@
       && git log -p FETCH_HEAD
 ----
 
-== Future Work ("Gerrit 3.0")
-
-- NoteDb will be the only database format supported by Gerrit 3.0. The offline
-  change data migration tool will be included in Gerrit 3.0, but online
-  migration will only be available in the 2.x line.
-
 [[migration]]
 == Migration
 
@@ -64,6 +61,9 @@
 [[online-migration]]
 === Online
 
+Note that online migration is only available in 2.x. To do the online migration
+from 2.14.x or 2.15.x to 3.0, it is necessary to first upgrade to 2.16.x.
+
 To start the online migration, set the `noteDb.changes.autoMigrate` option in
 `gerrit.config` and restart Gerrit:
 ----
@@ -87,7 +87,7 @@
 
 *Disadvantages*
 
-* Only available in 2.x; will not be available in Gerrit 3.0.
+* Only available in 2.x; not available in Gerrit 3.0.
 * Much slower than offline; uses only a single thread, to leave resources
   available for serving traffic.
 * Performance may be degraded, particularly of updates; data needs to be written
@@ -114,10 +114,10 @@
 * Much faster than online; can use all available CPUs, since no live traffic
   needs to be served.
 * No degraded performance of live servers due to writing data to 2 locations.
-* Available in both Gerrit 2.x and 3.0.
 
 *Disadvantages*
 
+* Available in Gerrit 2.15 and 2.16 only.
 * May require substantial downtime; takes about twice as long as an
   link:pgm-reindex.html[offline reindex]. (In fact, one of the migration steps is a
   full reindex, so it can't possibly take less time.)
diff --git a/Documentation/pg-plugin-admin-api.txt b/Documentation/pg-plugin-admin-api.txt
index 1c724d0..084fa2c 100644
--- a/Documentation/pg-plugin-admin-api.txt
+++ b/Documentation/pg-plugin-admin-api.txt
@@ -4,7 +4,7 @@
 and provides customization of the admin menu.
 
 == addMenuLink
-`adminApi.addMenuLink(text, url, opt_external)`
+`adminApi.addMenuLink(text, url, opt_external, opt_capabilities)`
 
 Add a new link to the end of the admin navigation menu.
 
diff --git a/Documentation/pg-plugin-dev.txt b/Documentation/pg-plugin-dev.txt
index c7aa57c..8fb5655 100644
--- a/Documentation/pg-plugin-dev.txt
+++ b/Documentation/pg-plugin-dev.txt
@@ -177,6 +177,14 @@
 
 Note: TODO
 
+=== registerDynamicCustomComponent
+`plugin.registerDynamicCustomComponent(dynamicEndpointName, opt_moduleName,
+opt_options)`
+
+See list of supported link:pg-plugin-endpoints.html[endpoints].
+
+Note: TODO
+
 === registerStyleModule
 `plugin.registerStyleModule(endpointName, moduleName)`
 
diff --git a/Documentation/pg-plugin-endpoints.txt b/Documentation/pg-plugin-endpoints.txt
index 14e62ea..9b5265b 100644
--- a/Documentation/pg-plugin-endpoints.txt
+++ b/Documentation/pg-plugin-endpoints.txt
@@ -150,3 +150,52 @@
 +
 The submit action, including the title and label, an instance of
 link:rest-api-changes.html#action-info[ActionInfo]
+
+== Dynamic Plugin endpoints
+
+The following endpoints are available to plugins.
+
+=== change-list-header
+The `change-list-header` extension point adds a header to the change list view.
+
+=== change-list-item-cell
+The `change-list-item-cell` extension point adds a cell to the change list item.
+
+In addition to default parameters, the following are available:
+
+* `change`
++
+current change of the row, an instance of
+link:rest-api-changes.html#change-info[ChangeInfo]
+
+=== change-view-tab-header
+The `change-view-tab-header` extension point adds a primary tab to the change
+view. This must be used in conjunction with `change-view-tab-content`.
+
+In addition to default parameters, the following are available:
+
+* `change`
++
+current change displayed, an instance of
+link:rest-api-changes.html#change-info[ChangeInfo]
+
+* `revision`
++
+current revision displayed, an instance of
+link:rest-api-changes.html#revision-info[RevisionInfo]
+
+=== change-view-tab-content
+The `change-view-tab-content` extension point adds primary tab content to
+the change view. This must be used in conjunction with `change-view-tab-header`.
+
+In addition to default parameters, the following are available:
+
+* `change`
++
+current change displayed, an instance of
+link:rest-api-changes.html#change-info[ChangeInfo]
+
+* `revision`
++
+current revision displayed, an instance of
+link:rest-api-changes.html#revision-info[RevisionInfo]
diff --git a/Documentation/pgm-LocalUsernamesToLowerCase.txt b/Documentation/pgm-LocalUsernamesToLowerCase.txt
index 4b50961..53081a1 100644
--- a/Documentation/pgm-LocalUsernamesToLowerCase.txt
+++ b/Documentation/pgm-LocalUsernamesToLowerCase.txt
@@ -47,7 +47,7 @@
 
 == CONTEXT
 This command can only be run on a server which has direct
-connectivity to the metadata database.
+connectivity to the managed Git repositories.
 
 == EXAMPLES
 To convert the local username of every account to lower case:
diff --git a/Documentation/pgm-daemon.txt b/Documentation/pgm-daemon.txt
index 0b1a3e5..ad07cfa 100644
--- a/Documentation/pgm-daemon.txt
+++ b/Documentation/pgm-daemon.txt
@@ -19,14 +19,8 @@
 
 == DESCRIPTION
 Runs the Gerrit network daemon on the local system, configured as
-per the local copy of link:config-gerrit.html[gerrit.config].
-
-The path to gerrit.config is read from the metadata database,
-which requires that all slaves (and master) reading from the same
-database must place gerrit.config at the same location on the local
-filesystem.  However, any option within gerrit.config, including
-link:config-gerrit.html#gerrit.basePath[gerrit.basePath] may be set
-to different values.
+per the local copy of link:config-gerrit.html[gerrit.config] located under
+`<SITE_PATH>/etc`.
 
 == OPTIONS
 
diff --git a/Documentation/pgm-gsql.txt b/Documentation/pgm-gsql.txt
deleted file mode 100644
index 4986522..0000000
--- a/Documentation/pgm-gsql.txt
+++ /dev/null
@@ -1,55 +0,0 @@
-= gsql
-
-== NAME
-gsql - Administrative interface to idle database
-
-== SYNOPSIS
-[verse]
---
-_java_ -jar gerrit.war _gsql_
-  -d <SITE_PATH>
---
-
-== DESCRIPTION
-Interactive query support against the configured SQL database.
-All SQL statements are supported, including SELECT, UPDATE, INSERT,
-DELETE and ALTER.
-
-This command is primarily intended to access a local H2 database
-which is not currently open by a Gerrit daemon.  To access an open
-database use link:cmd-gsql.html[gerrit gsql] over SSH.
-
-== OPTIONS
-
--d::
---site-path::
-	Location of the gerrit.config file, and all other per-site
-	configuration data, supporting libraries and log files.
-
-== CONTEXT
-This command can only be run on a server which has direct
-connectivity to the metadata database, and local access to the
-managed Git repositories.
-
-== EXAMPLES
-To manually correct a user's SSH user name:
-
-----
-	$ java -jar gerrit.war gsql
-	Welcome to Gerrit Code Review v2.0.25
-	(PostgreSQL 8.3.8)
-
-	Type '\h' for help.  Type '\r' to clear the buffer.
-
-	gerrit> update accounts set ssh_user_name = 'alice' where account_id=1;
-	UPDATE 1; 1 ms
-	gerrit> \q
-	Bye
-----
-
-GERRIT
-------
-Part of link:index.html[Gerrit Code Review]
-
-SEARCHBOX
----------
diff --git a/Documentation/pgm-index.txt b/Documentation/pgm-index.txt
index d61cc0b..dde0231 100644
--- a/Documentation/pgm-index.txt
+++ b/Documentation/pgm-index.txt
@@ -15,9 +15,6 @@
 link:pgm-daemon.html[daemon]::
 	Gerrit HTTP, SSH network server.
 
-link:pgm-gsql.html[gsql]::
-	Administrative interface to idle database.
-
 link:pgm-prolog-shell.html[prolog-shell]::
 	Simple interactive Prolog interpreter.
 
diff --git a/Documentation/pgm-init.txt b/Documentation/pgm-init.txt
index 9a16cdf..f6c3c85 100644
--- a/Documentation/pgm-init.txt
+++ b/Documentation/pgm-init.txt
@@ -28,7 +28,7 @@
 into a newly created `$site_path`.
 
 If run in an existing `$site_path`, init upgrades existing resources
-(e.g. DB schema, plugins) as necessary.
+(e.g. NoteDb schema, plugins) as necessary.
 
 == OPTIONS
 -b::
@@ -100,8 +100,7 @@
 	folder.
 
 == CONTEXT
-This command can only be run on a server which has direct
-connectivity to the metadata database, and local access to the
+This command can only be run on a server which has direct local access to the
 managed Git repositories.
 
 GERRIT
diff --git a/Documentation/pgm-prolog-shell.txt b/Documentation/pgm-prolog-shell.txt
index a669aa7..3566b8f 100644
--- a/Documentation/pgm-prolog-shell.txt
+++ b/Documentation/pgm-prolog-shell.txt
@@ -7,7 +7,7 @@
 [verse]
 --
 _java_ -jar gerrit.war _prolog-shell_
-  [-s FILE.pl ...]
+  [-q] [-s FILE.pl ...]
 --
 
 == DESCRIPTION
@@ -15,6 +15,8 @@
 and testing.
 
 == OPTIONS
+-q::
+	Do not display banner.
 -s::
 	Dynamically load the Prolog source code at startup,
 	as though the user had entered `['FILE.pl'].` into
diff --git a/Documentation/pgm-rulec.txt b/Documentation/pgm-rulec.txt
index 1b50812..2a987205 100644
--- a/Documentation/pgm-rulec.txt
+++ b/Documentation/pgm-rulec.txt
@@ -33,8 +33,7 @@
 	Compile rules for the specified project.
 
 == CONTEXT
-This command can only be run on a server which has direct
-connectivity to the metadata database, and local access to the
+This command can only be run on a server which has local access to the
 managed Git repositories.
 
 Caching needs to be enabled. See
diff --git a/Documentation/project-configuration.txt b/Documentation/project-configuration.txt
index 9fa21ff..23030a4 100644
--- a/Documentation/project-configuration.txt
+++ b/Documentation/project-configuration.txt
@@ -48,201 +48,7 @@
 [[project_options]]
 == Project Options
 
-[[submit_type]]
-=== Submit Type
-
-The method Gerrit uses to submit a change to a project can be
-modified by any project owner through the project console, `Projects` >
-`List` > my/project. In general, a submitted change is only merged if all
-its dependencies are also submitted, with exceptions documented below.
-The following submit types are supported:
-
-[[submit_type_inherit]]
-* Inherit
-+
-This is the default for new projects, unless overridden by a global
-link:config-gerrit.html#repository.name.defaultSubmitType[`defaultSubmitType` option].
-+
-Inherit the submit type from the parent project. In `All-Projects`, this
-is equivalent to link:#merge_if_necessary[Merge If Necessary].
-
-[[fast_forward_only]]
-* Fast Forward Only
-+
-With this method Gerrit does not create merge commits on submitting a
-change. Merge commits may still be submitted, but they must be created
-on the client prior to uploading to Gerrit for review.
-+
-To submit a change, the change must be a strict superset of the
-destination branch.  That is, the change must already contain the
-tip of the destination branch at submit time.
-
-[[merge_if_necessary]]
-* Merge If Necessary
-+
-If the change being submitted is a strict superset of the destination
-branch, then the branch is fast-forwarded to the change.  If not,
-then a merge commit is automatically created.  This is identical
-to the classical `git merge` behavior, or `git merge --ff`.
-
-[[always_merge]]
-* Always Merge
-+
-Always produce a merge commit, even if the change is a strict
-superset of the destination branch.  This is identical to the
-behavior of `git merge --no-ff`, and may be useful if the
-project needs to follow submits with `git log --first-parent`.
-
-[[cherry_pick]]
-* Cherry Pick
-+
-Always cherry pick the patch set, ignoring the parent lineage
-and instead creating a brand new commit on top of the current
-branch head.
-+
-When cherry picking a change, Gerrit automatically appends onto the
-end of the commit message a short summary of the change's approvals,
-and a URL link back to the change on the web.  The committer header
-is also set to the submitter, while the author header retains the
-original patch set author.
-+
-Note that Gerrit ignores dependencies between changes when using this
-submit type unless
-link:config-gerrit.html#change.submitWholeTopic[`change.submitWholeTopic`]
-is enabled and depending changes share the same topic. So generally
-submitters must remember to submit changes in the right order when using this
-submit type. If all you want is extra information in the commit message,
-consider using the Rebase Always submit strategy.
-
-[[rebase_if_necessary]]
-* Rebase If Necessary
-+
-If the change being submitted is a strict superset of the destination
-branch, then the branch is fast-forwarded to the change.  If not,
-then the change is automatically rebased and then the branch is
-fast-forwarded to the change.
-+
-When Gerrit tries to do a merge, by default the merge will only
-succeed if there is no path conflict.  A path conflict occurs when
-the same file has also been changed on the other side of the merge.
-
-[[rebase_always]]
-* Rebase Always
-+
-Basically, the same as Rebase If Necessary, but it creates a new patchset even
-if fast forward is possible AND like Cherry Pick it ensures footers such as
-Change-Id, Reviewed-On, and others are present in resulting commit that is
-merged.
-+
-Thus, Rebase Always can be considered similar to Cherry Pick, but with
-the important distinction that Rebase Always does not ignore dependencies.
-
-[[content_merge]]
-=== Allow content merges
-
-If `Allow content merges` is enabled, Gerrit will try
-to do a content merge when a path conflict occurs.
-
-[[project-state]]
-=== State
-
-This setting defines the state of the project. A project can have the
-following states:
-
-- `Active`:
-+
-The project is active and users can see and modify the project according
-to their access rights on the project.
-
-- `Read Only`:
-+
-The project is read only and all modifying operations on it are
-disabled. E.g. this means that pushing to this project fails for all
-users even if they have push permissions assigned on it.
-+
-Setting a project to this state is an easy way to temporary close a
-project, as you can keep all write access rights in place and they will
-become active again as soon as the project state is set back to
-`Active`.
-+
-This state also makes sense if a project was moved to another location.
-In this case all new development should happen in the new project and
-you want to prevent that somebody accidentally works on the old
-project, while keeping the old project around for old references.
-
-- `Hidden`:
-+
-The project is hidden and only visible to project owners. Other users
-are not able to see the project even if they have read permissions
-granted on the project.
-
-=== Use target branch when determining new changes to open
-
-The `create-new-change-for-all-not-in-target` option provides a
-convenience for selecting link:user-upload.html#base[the merge base]
-by setting it automatically to the target branch's tip so you can
-create new changes for all commits not in the target branch.
-
-This option is disabled if the tip of the push is a merge commit.
-
-This option also only works if there are no merge commits in the
-commit chain, in such cases it fails warning the user that such
-pushes can only be performed by manually specifying
-link:user-upload.html#base[bases]
-
-This option is useful if you want to push a change to your personal
-branch first and for review to another branch for example. Or in cases
-where a commit is already merged into a branch and you want to create
-a new open change for that commit on another branch.
-
-[[require-change-id]]
-=== Require Change-Id
-
-The `Require Change-Id in commit message` option defines whether a
-link:user-changeid.html[Change-Id] in the commit message is required
-for pushing a commit for review. If this option is set, trying to push
-a commit for review that doesn't contain a Change-Id in the commit
-message fails with link:error-missing-changeid.html[missing Change-Id
-in commit message footer].
-
-It is recommended to set this option and use a
-link:user-changeid.html#create[commit-msg hook] (or other client side
-tooling like EGit) to automatically generate Change-Id's for new
-commits. This way the Change-Id is automatically in place when changes
-are reworked or rebased and uploading new patch sets gets easy.
-
-If this option is not set, commits can be uploaded without a Change-Id,
-but then users have to remember to copy the assigned Change-Id from the
-change screen and insert it manually into the commit message when they
-want to upload a second patch set.
-
-=== Maximum Git Object Size Limit
-
-This option defines the maximum allowed Git object size that
-receive-pack will accept. If an object is larger than the given size
-the pack-parsing will abort and the push operation will fail.
-
-With this option users can be prevented from uploading commits that
-contain files which are too large.
-
-Normally the link:config-gerrit.html#receive.maxObjectSizeLimit[maximum
-Git object size limit] is configured globally for a Gerrit server. At
-the project level, the maximum Git object size limit can be further
-reduced, but not extended. The displayed effective limit shows the
-maximum Git object size limit that is actually used on the project.
-
-The defined maximum Git object size limit is inherited by any child
-project.
-
-[[require-signed-off-by]]
-=== Require Signed-off-by
-
-The `Require Signed-off-by in commit message` option defines whether a
-link:user-signedoffby.html[Signed-off-by] line in the commit message is
-required for pushing a commit. If this option is set, trying to push a
-commit that doesn't contain a Signed-off-by line in the commit message
-fails with link:error-not-signed-off-by.html[not Signed-off-by
-author/committer/uploader in commit message footer].
+See details at link:config-project-config.html#project-section[project section].
 
 [[branch-admin]]
 == Branch Administration
diff --git a/Documentation/prolog-cookbook.txt b/Documentation/prolog-cookbook.txt
index 19ed98a..9a23a27 100644
--- a/Documentation/prolog-cookbook.txt
+++ b/Documentation/prolog-cookbook.txt
@@ -725,6 +725,9 @@
 if the `cut` in the first rule is not reached and it only happens if a
 predicate before the `cut` fails.
 
+This fact can be bypassed by users who have
+link:access-control.html#category_forge_author[Forge Author] permission.
+
 ==== Don't use `gerrit:default_submit`
 Let's implement the same submit rule the other way, without reusing the
 `gerrit:default_submit`:
@@ -1044,10 +1047,7 @@
     gerrit:uploader(U),
     R = label('Is-Pure-Revert', ok(U)).
 
-submit_rule(submit(R)) :-
-    gerrit:pure_revert(U),
-    U /= 1,
-    R = label('Is-Pure-Revert', need(_)).
+submit_rule(submit(label('Is-Pure-Revert', need(_)))).
 ----
 
 Suppose currently a change is submittable if it gets `+2` for `Code-Review`
@@ -1058,21 +1058,20 @@
 [source,prolog]
 ----
 submit_rule(submit(CR, V, R)) :-
-    base(CR, V),
-    gerrit:pure_revert(1),
-    !,
-    gerrit:uploader(U),
-    R = label('Is-Pure-Revert', ok(U)).
-
-submit_rule(submit(CR, V, R)) :-
-    base(CR, V),
-    gerrit:pure_revert(U),
-    U /= 1,
-    R = label('Is-Pure-Revert', need(_)).
+  base(CR, V),
+  set_pure_revert_label(R).
 
 base(CR, V) :-
-    gerrit:max_with_block(-2, 2, 'Code-Review', CR),
-    gerrit:max_with_block(-1, 1, 'Verified', V).
+  gerrit:max_with_block(-2, 2, 'Code-Review', CR),
+  gerrit:max_with_block(-1, 1, 'Verified', V).
+
+set_pure_revert_label(R) :-
+  gerrit:pure_revert(1),
+  !,
+  gerrit:uploader(U),
+  R = label('Is-Pure-Revert', ok(U)).
+
+set_pure_revert_label(label('Is-Pure-Revert', need(_))).
 ----
 
 Note that a new label as `Is-Pure-Revert` should not be configured.
diff --git a/Documentation/quota.txt b/Documentation/quota.txt
new file mode 100644
index 0000000..a647e33
--- /dev/null
+++ b/Documentation/quota.txt
@@ -0,0 +1,50 @@
+= Gerrit Code Review - Quota
+
+Gerrit does not provide out of the box quota enforcement. However, it does
+support an extension mechanism for plugins to hook into to provide this
+functionality. The most prominent plugin is the
+link:https://gerrit.googlesource.com/plugins/quota/[Quota Plugin].
+
+This documentation is intended to be read by plugin developers. It contains all
+quota requests implemented in Gerrit-core as well as the metadata that they have
+associated.
+
+== Quota Groups
+
+The following quota groups are defined in core Gerrit:
+
+=== REST API
+[[rest-api]]
+
+The REST API enforces quota after the resource was parsed (if applicable) and before the
+endpoint's logic is executed. This enables quota enforcer implementations to throttle calls
+to specific endpoints while knowing the general context (user and top-level entity such as
+change, project or account).
+
+If the quota enforcer wants to throttle HTTP requests, they should use
+link:quota.html#http-requests[HTTP Requests] instead.
+
+The quota groups used for checking follow the exact definition of the endoint in the REST
+API, but remove all IDs. The schema is:
+
+/restapi/<ENDPOINT>:<HTTP-METHOD>
+
+Examples:
+
+[options="header",cols="1,6"]
+|=======================
+|HTTP call                                 |Quota Group                    |Metadata
+|GET /a/changes/1/revisions/current/detail |/changes/revisions/detail:GET  |CurrentUser, Change.Id, Project.NameKey
+|POST /a/changes/                          |/changes/:POST                 |CurrentUser
+|GET /a/accounts/self/detail               |/accounts/detail:GET           |CurrentUser, Account.Id
+|=======================
+
+The user provided in the check's metadata is always the calling user (having the
+impersonation bit and real user set in case the user is impersonating another user).
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/replace_macros.py b/Documentation/replace_macros.py
index 309a135..7d8ea23 100755
--- a/Documentation/replace_macros.py
+++ b/Documentation/replace_macros.py
@@ -87,6 +87,12 @@
   id="searchBox">
   Search
 </button>
+  %s
+</div>
+++++
+"""
+
+BUILTIN_SEARCH = """
 <script type="text/javascript">
 var f = function() {
   window.location = '../#/Documentation/q/' +
@@ -99,11 +105,25 @@
   }
 }
 </script>
-</div>
-++++
-
 """
 
+GOOGLE_SITE_SEARCH = """
+<script type="text/javascript">
+var f = function() {
+  window.location = 'https://www.google.com/search?q=' +
+     encodeURIComponent(document.getElementById("docSearch").value +
+     ' site:@SITE@');
+}
+document.getElementById("searchBox").onclick = f;
+document.getElementById("docSearch").onkeypress = function(e) {
+  if (13 == (e.keyCode ? e.keyCode : e.which)) {
+    f();
+  }
+}
+</script>
+"""
+
+
 LINK_SCRIPT = """
 
 ++++
@@ -227,8 +247,19 @@
                 help="generate the search boxes")
 opts.add_option('--no-searchbox', action="store_false", dest='searchbox',
                 help="don't generate the search boxes")
+opts.add_option('--site-search', action="store", metavar="SITE",
+                help=("generate the search box using google. SITE should " +
+                      "point to the domain/path of the site, eg. " +
+                      "gerrit-review.googlesource.com/Documentation"))
 options, _ = opts.parse_args()
 
+if options.site_search:
+  SEARCH_BOX = (SEARCH_BOX %
+                GOOGLE_SITE_SEARCH.replace("@SITE@", options.site_search))
+else:
+  SEARCH_BOX = SEARCH_BOX % BUILTIN_SEARCH
+
+
 try:
     try:
         out_file = open(options.out, 'w', errors='ignore')
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index 615accc..3ff3371 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -1252,7 +1252,6 @@
     "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",
@@ -1309,7 +1308,6 @@
     "show_site_header": true,
     "use_flash_clipboard": true,
     "expand_inline_diffs": true,
-    "download_command": "CHECKOUT",
     "date_format": "STD",
     "time_format": "HHMM_12",
     "size_bar_in_change_table": true,
@@ -1360,7 +1358,6 @@
     "show_site_header": true,
     "use_flash_clipboard": true,
     "expand_inline_diffs": true,
-    "download_command": "CHECKOUT",
     "date_format": "STD",
     "time_format": "HHMM_12",
     "size_bar_in_change_table": true,
@@ -1423,7 +1420,6 @@
   )]}'
   {
     "context": 10,
-    "theme": "DEFAULT",
     "ignore_whitespace": "IGNORE_ALL",
     "intraline_difference": true,
     "line_length": 100,
@@ -1454,7 +1450,6 @@
 
   {
     "context": 10,
-    "theme": "ECLIPSE",
     "ignore_whitespace": "IGNORE_ALL",
     "intraline_difference": true,
     "line_length": 100,
@@ -1480,7 +1475,6 @@
   )]}'
   {
     "context": 10,
-    "theme": "ECLIPSE",
     "ignore_whitespace": "IGNORE_ALL",
     "intraline_difference": true,
     "line_length": 100,
@@ -1517,8 +1511,6 @@
 
   )]}'
   {
-    "theme": "ECLIPSE",
-    "key_map_type": "VIM",
     "tab_size": 4,
     "line_length": 80,
     "indent_unit": 2,
@@ -1550,8 +1542,6 @@
   Content-Type: application/json;charset=UTF-8
 
   {
-    "theme": "ECLIPSE",
-    "key_map_type": "VIM",
     "tab_size": 4,
     "line_length": 80,
     "indent_unit": 2,
@@ -1578,8 +1568,6 @@
 
   )]}'
   {
-    "theme": "ECLIPSE",
-    "key_map_type": "VIM",
     "tab_size": 4,
     "line_length": 80,
     "cursor_blink_rate": 530,
@@ -2143,12 +2131,37 @@
 
 This can be:
 
+* `self` or `me` for the calling user
+* a bare account ID ("18419")
+* an account ID following a name in parentheses ("Full Name (18419)")
 * a string of the format "Full Name <email@example.com>"
 * just the email address ("email@example")
-* a full name if it is unique ("Full Name")
-* an account ID ("18419")
+* a full name ("Full Name")
 * a user name ("username")
-* `self` for the calling user
+
+In all cases, accounts that are not
+link:config-gerrit.html#accounts.visibility[visible] to the calling user are not
+considered.
+
+In all cases _except_ a bare account ID and `self`/`me`, inactive accounts are
+not considered. Inactive accounts should only be referenced by bare ID.
+
+If the input is a bare account ID, this will always resolve to exactly
+one account if there is a visible account with that ID, and zero accounts
+otherwise. (This is true even in corner cases like a user having a full name
+which is exactly a numeric account ID belonging to a different user; such a user
+cannot be identified by this number.)
+
+If the identifier is ambiguous or only refers to inactive accounts, the error
+message from the API should contain a human-readable description of how to
+disambiguate the request.
+
+*Note*: Except as noted above, callers should not rely on the particular
+priorities of any of the identifiers in the account resolution algorithm. Any
+other formats may be subject to future deprecation. If callers require specific
+searching semantics, they should use the link:#query-account[Query Account]
+endpoint to resolve a string to one or more accounts, then access the API using
+the account ID.
 
 [[capability-id]]
 === \{capability-id\}
@@ -2419,9 +2432,6 @@
 |Field Name                    ||Description
 |`context`                     ||
 The number of lines of context when viewing a patch.
-|`theme`                       ||
-The CodeMirror theme name in upper case, for example `DEFAULT`. All the themes
-from the CodeMirror release that Gerrit is using are available.
 |`expand_all_comments`         |not set if `false`|
 Whether all inline comments should be automatically expanded.
 |`ignore_whitespace`           ||
@@ -2543,12 +2553,6 @@
 [options="header",cols="1,^1,5"]
 |===========================================
 |Field Name                    ||Description
-|`theme`                       ||
-The CodeMirror theme name in upper case, for example `DEFAULT`. All the themes
-from the CodeMirror release that Gerrit is using are available.
-|`key_map_type`                ||
-The CodeMirror key map. Currently only a subset of key maps are
-supported: `DEFAULT`, `EMACS`, `SUBLIME`, `VIM`.
 |`tab_size`                    ||
 Number of spaces that should be used to display one tab.
 |`line_length`                 ||
@@ -2708,8 +2712,6 @@
 The type of download URL the user prefers to use. May be any key from
 the `schemes` map in
 link:rest-api-config.html#download-info[DownloadInfo].
-|`download_command`             ||
-The type of download command the user prefers to use.
 |`date_format`                  ||
 The format to display the date in.
 Allowed values are `STD`, `US`, `ISO`, `EURO`, `UK`.
@@ -2739,9 +2741,6 @@
 |`change_table`                           ||
 The columns to display in the change table (PolyGerrit only). The default is
 empty, which will default columns as determined by the frontend.
-|`url_aliases`                  |optional|
-A map of URL path pairs, where the first URL path is an alias for the
-second URL path.
 |`email_strategy`               ||
 The type of email strategy to use. On `ENABLED`, the user will receive emails
 from Gerrit. On `CC_ON_OWN_COMMENTS` the user will also receive emails for
@@ -2780,8 +2779,6 @@
 (PolyGerrit only).
 |`download_scheme`              |optional|
 The type of download URL the user prefers to use.
-|`download_command`             |optional|
-The type of download command the user prefers to use.
 |`date_format`                  |optional|
 The format to display the date in.
 Allowed values are `STD`, `US`, `ISO`, `EURO`, `UK`.
@@ -2811,9 +2808,6 @@
 |`change_table`                           ||
 The columns to display in the change table (PolyGerrit only). The default is
 empty, which will default columns as determined by the frontend.
-|`url_aliases`                  |optional|
-A map of URL path pairs, where the first URL path is an alias for the
-second URL path.
 |`email_strategy`               |optional|
 The type of email strategy to use. On `ENABLED`, the user will receive emails
 from Gerrit. On `CC_ON_OWN_COMMENTS` the user will also receive emails for
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index a7367f3..95c38a6 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -309,6 +309,14 @@
 * `SKIP_MERGEABLE`: skip the `mergeable` field in
 link:#change-info[ChangeInfo]. For fast moving projects, this field must
 be recomputed often, which is slow for projects with big trees.
++
+When link:config-gerrit.html#change.api.excludeMergeableInChangeInfo[
+`change.api.excludeMergeableInChangeInfo`] is set in the `gerrit.config`,
+the `mergeable` field will always be omitted and `SKIP_MERGEABLE` has no
+effect.
++
+A change's mergeability can be requested separately by calling the
+link:#get-mergeable[get-mergeable] endpoint.
 --
 
 [[submittable]]
@@ -350,6 +358,11 @@
   as link:#tracking-id-info[TrackingIdInfo].
 --
 
+[[no-limit]]
+--
+* `NO-LIMIT`: Return all results
+--
+
 .Request
 ----
   GET /changes/?q=97&o=CURRENT_REVISION&o=CURRENT_COMMIT&o=CURRENT_FILES&o=DOWNLOAD_COMMANDS HTTP/1.0
@@ -2200,8 +2213,8 @@
 'POST /changes/link:#change-id[\{change-id\}]/private'
 --
 
-Marks the change to be private. Changes may only be marked private by the
-owner or site administrators.
+Marks the change to be private. Only open changes can be marked private.
+Changes may only be marked private by the owner or site administrators.
 
 A message can be specified in the request body inside a
 link:#private-input[PrivateInput] entity.
@@ -5165,9 +5178,6 @@
 for supporting review of merge commits.  The value is the 1-based index of the
 parent's position in the commit object.
 
-[[weblinks-only]]
-If the `weblinks-only` parameter is specified, only the diff web links are returned.
-
 .Request
 ----
   GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/b6b9c10649b9041884046119ab794374470a1b45/files/gerrit-server%2Fsrc%2Fmain%2Fjava%2Fcom%2Fgoogle%2Fgerrit%2Fserver%2Fproject%2FRefControl.java/diff?base=2 HTTP/1.0
@@ -5770,12 +5780,14 @@
 Whether the change was reviewed by the calling user.
 Only set if link:#reviewed[reviewed] is requested.
 |`submit_type`        |optional|
-The link:project-configuration.html#submit_type[submit type] of the change. +
+The link:config-project-config.html#submit-type[submit type] of the change. +
 Not set for merged changes.
 |`mergeable`          |optional|
 Whether the change is mergeable. +
 Not set for merged changes, if the change has not yet been tested, or
-if the link:#skip_mergeable[skip_mergeable] option is set.
+if the link:#skip_mergeable[skip_mergeable] option is set or when
+link:config-gerrit.html#change.api.excludeMergeableInChangeInfo[change.api.excludeMergeableInChangeInfo]
+is set.
 |`submittable`        |optional|
 Whether the change has been approved by the project submit rules. +
 Only set if link:#submittable[requested].
@@ -5783,8 +5795,12 @@
 Number of inserted lines.
 |`deletions`          ||
 Number of deleted lines.
+|`total_comment_count`  |optional|
+Total number of inline comments across all patch sets. Not set if the current
+change index doesn't have the data.
 |`unresolved_comment_count`  |optional|
-Number of unresolved comments. Not set if the current change index doesn't have the data.
+Number of unresolved inline comment threads across all patch sets. Not set if
+the current change index doesn't have the data.
 |`_number`            ||The legacy numeric ID of the change.
 |`owner`              ||
 The owner of the change as an link:rest-api-accounts.html#account-info[
@@ -5892,9 +5908,13 @@
 created change. If set, it must be a merged commit on the destination branch.
 Mutually exclusive with `base_change`.
 |`new_branch`         |optional, default to `false`|
-Allow creating a new branch when set to `true`.
+Allow creating a new branch when set to `true`. Using this option is
+only possible for non-merge commits (if the `merge` field is not set).
 |`merge`              |optional|
 The detail of a merge commit as a link:#merge-input[MergeInput] entity.
+If set, the target branch (see  `branch` field) must exist (it is not
+possible to create it automatically by setting the `new_branch` field
+to `true`.
 |`notify`             |optional|
 Notify handling that defines to whom email notifications should be sent
 after the change is created. +
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 7be8c32..9359112 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -126,10 +126,7 @@
     "gerrit": {
       "all_projects": "All-Projects",
       "all_users": "All-Users"
-      "doc_search": true,
-      "web_uis": [
-        "gwt"
-      ]
+      "doc_search": true
     },
     "sshd": {},
     "suggest": {
@@ -1486,8 +1483,8 @@
 |`git_basic_auth_policy`      |optional|
 The link:config-gerrit.html#auth.gitBasicAuthPolicy[policy] to authenticate
 Git over HTTP and REST API requests when
-link:config-gerrit.html#auth.type[authentication type] is `LDAP`.
-Can be `HTTP`, `LDAP` or `HTTP_LDAP`.
+link:config-gerrit.html#auth.type[authentication type] is `LDAP`,
+`LDAP_BIND` or `OAUTH`. Can be `HTTP`, `LDAP`, `HTTP_LDAP` or `OAUTH`.
 |==========================================
 
 [[cache-info]]
@@ -1824,12 +1821,6 @@
 Whether to enable the web UI for editing GPG keys.
 |`report_bug_url`    |optional|
 link:config-gerrit.html#gerrit.reportBugUrl[URL to report bugs].
-|`report_bug_text`   |optional, not set if default|
-link:config-gerrit.html#gerrit.reportBugText[Display text for report
-bugs link].
-|`web_uis`           ||
-List of web UIs supported by the HTTP server. Possible values are `GWT`
-and `POLYGERRIT`.
 |=================================
 
 [[hit-ration-info]]
@@ -1974,11 +1965,6 @@
 Information about the configuration from the
 link:config-gerrit.html#suggest[suggest] section as link:#suggest-info[
 SuggestInfo] entity.
-|`url_aliases`             |optional|
-A map of URL aliases, where a regular expression for an URL token is
-mapped to a target URL token. The target URL token can contain
-placeholders for the groups matched by the regular expression: `$1` for
-the first matched group, `$2` for the second matched group, etc.
 |`user`                    ||
 Information about the configuration from the
 link:config-gerrit.html#user[user] section as link:#user-config-info[
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 5006b6d..76151b40 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -1356,25 +1356,6 @@
 Ref(ref)::
 The branch for which to check access. This must be given if `perm` is specified.
 
-[[check-access-post]]
-=== Check Access (POST)
-
-This endpoint can also be accessed as a POST request (deprecated). In
-this case, the input for the access checks must be provided in the
-request body inside a link:#access-check-input[AccessCheckInput]
-entity.
-
-.Request
-----
-  POST /projects/MyProject/check.access HTTP/1.0
-  Content-Type: application/json; charset=UTF-8
-
-  {
-    "account": "Kristen.Burns@gerritcodereview.com",
-    "ref": "refs/heads/secret/bla"
-  }
-----
-
 [[index]]
 === Index project
 
@@ -2934,21 +2915,6 @@
 |`message`                   |optional|A clarifying message if `status` is not 200.
 |=========================================
 
-[[access-check-input]]
-=== AccessCheckInput
-The `AccessCheckInput` entity is either an account or
-(account, ref) tuple for which we want to check access.
-
-[options="header",cols="1,^1,5"]
-|=========================================
-|Field Name                  ||Description
-|`account`                   ||The account for which to check access
-|`ref`                       |optional|The refname for which to check
-access
-|`permission`                |optional|The ref permission for which to
-check. This defaults to `read`. If given, it `ref` must be given too.
-|=========================================
-
 [[auto_closeable_changes_check_input]]
 === AutoCloseableChangesCheckInput
 The `AutoCloseableChangesCheckInput` entity contains options for running
@@ -3169,9 +3135,6 @@
 Map with the comment link configurations of the project. The name of
 the comment link configuration is mapped to a link:#commentlink-info[
 CommentlinkInfo] entity.
-|`theme`                                   |optional|
-The theme that is configured for the project as a link:#theme-info[
-ThemeInfo] entity.
 |`plugin_config`                           |optional|
 Plugin configuration as map which maps the plugin name to a map of
 parameter names to link:#config-parameter-info[ConfigParameterInfo]
@@ -3660,7 +3623,7 @@
 
 [[submit-type-info]]
 === SubmitTypeInfo
-Information about the link:project-configuration.html#submit_type[default submit
+Information about the link:config-project-config.html#submit-type[default submit
 type of a project], taking into account project inheritance.
 
 Valid values for each field are `MERGE_IF_NECESSARY`, `FAST_FORWARD_ONLY`,
@@ -3723,21 +3686,6 @@
 |=========================
 
 
-[[theme-info]]
-=== ThemeInfo
-The `ThemeInfo` entity describes a theme.
-
-[options="header",cols="1,^2,4"]
-|=============================
-|Field Name      ||Description
-|`css`           |optional|
-The path to the `GerritSite.css` file.
-|`header`        |optional|
-The path to the `GerritSiteHeader.html` file.
-|`footer`        |optional|
-The path to the `GerritSiteFooter.html` file.
-|=============================
-
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/rest-api.txt b/Documentation/rest-api.txt
index 8f6a47b..a8ab353 100644
--- a/Documentation/rest-api.txt
+++ b/Documentation/rest-api.txt
@@ -191,6 +191,11 @@
 "`422 Unprocessable Entity`" is returned if the ID of a resource that is
 specified in the request body cannot be resolved.
 
+==== 429 Too Many Requests
+"`429 Too Many Requests`" is returned if the request exhausted any set
+quota limits. Depending on the exhausted quota, the request may be retried
+with exponential backoff.
+
 [[tracing]]
 === Request Tracing
 For each REST endpoint tracing can be enabled by setting the
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 48d331b..cafd5ca 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -277,6 +277,8 @@
 ones using a bracket expression). For example, to match all XML
 files named like 'name1.xml', 'name2.xml', and 'name3.xml' use
 `file:"^name[1-3].xml"`.
++
+Slash ('/') is used path separator.
 
 [[file]]
 file:'NAME', f:'NAME'::
@@ -290,6 +292,46 @@
 Regular expression matching can be enabled by starting the string
 with `^`. In this mode `file:` is an alias of `path:` (see above).
 
+[[extension]]
+extension:'EXT', ext:'EXT'::
++
+Matches any change touching a file with extension 'EXT', case-insensitive. The
+extension is defined as the portion of the filename following the final `.`.
+Files with no `.` in their name have no extension and can be matched by an
+empty string.
+
+[[onlyextensions]]
+onlyextensions:'EXT_LIST', onlyexts:'EXT_LIST'::
++
+Matches any change touching only files with extensions that are listed in
+'EXT_LIST' (comma-separated list). The matching is done case-insensitive.
+An extension is defined as the portion of the filename following the final `.`.
+Files with no `.` in their name have no extension and can be matched by an
+empty string.
+
+[[directory]]
+directory:'DIR', dir:'DIR'::
++
+Matches any change where the current patch set touches a file in the directory
+'DIR'. The matching is done case-insensitive. 'DIR' can be a full directory
+name, a directory prefix or any combination of intermediate directory segments.
+E.g. a change that touches a file in the directory 'a/b/c' matches for 'a/b/c',
+'a', 'a/b', 'b', 'b/c' and 'c'.
++
+Slash ('/') is used path separator. Leading and trailing slashes are allowed
+but are not mandatory.
++
+If 'DIR' starts with `^` it matches directories and directory segments by
+regular expression. The link:http://www.brics.dk/automaton/[dk.brics.automaton
+library] is used for evaluation of such patterns.
+
+[[footer]]
+footer:'FOOTER'::
++
+Matches any change that has 'FOOTER' as footer in the commit message of the
+current patch set. 'FOOTER' can be specified verbatim ('<key>: <value>', must
+be quoted) or as '<key>=<value>'. The matching is done case-insensitive.
+
 [[star]]
 star:'LABEL'::
 +
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt
index 751e886..56602e2 100644
--- a/Documentation/user-upload.txt
+++ b/Documentation/user-upload.txt
@@ -732,7 +732,7 @@
 Gerrit to provide magical refs, such as `+refs/for/*+` for new
 change submission and `+refs/changes/*+` for change replacement.
 When a push request is received to create a ref in one of these
-namespaces Gerrit performs its own logic to update the database,
+namespaces Gerrit performs its own logic to update the review metadata,
 and then lies to the client about the result of the operation.
 A successful result causes the client to believe that Gerrit has
 created the ref, but in reality Gerrit hasn't created the ref at all.
diff --git a/Jenkinsfile b/Jenkinsfile
index f2d361e..7417bb3 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -111,7 +111,7 @@
 }
 
 def collectBuildModes() {
-    Builds.modes = ["reviewdb", "notedb"]
+    Builds.modes = ["notedb"]
     def changedFiles = queryChangedFiles(Globals.gerritUrl, Change.number, Change.sha1)
     def polygerritFiles = changedFiles.findAll { it.startsWith("polygerrit-ui") ||
         it.startsWith("lib/js") }
@@ -130,7 +130,7 @@
     }
 }
 
-def prepareBuildsForMode(buildName, mode="reviewdb", retryTimes = 1) {
+def prepareBuildsForMode(buildName, mode="notedb", retryTimes = 1) {
     return {
         stage("${buildName}/${mode}") {
             def slaveBuild = null
diff --git a/WORKSPACE b/WORKSPACE
index 6495faa..ec7676b 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -71,9 +71,6 @@
 
 rules_closure_toolchains()
 
-# This has to be done after loading of rules_closure, because it loads rules_java
-load("//lib/codemirror:cm.bzl", "CM_VERSION", "DIFF_MATCH_PATCH_VERSION")
-
 # Golang support for PolyGerrit local dev server.
 http_archive(
     name = "io_bazel_rules_go",
@@ -102,12 +99,6 @@
 
 # Dependencies for PolyGerrit local dev server.
 go_repository(
-    name = "com_github_robfig_soy",
-    commit = "82face14ebc0883b4ca9c901b5aaf3738b9f6a24",
-    importpath = "github.com/robfig/soy",
-)
-
-go_repository(
     name = "com_github_howeyc_fsnotify",
     commit = "441bbc86b167f3c1f4786afae9931403b99fdacf",
     importpath = "github.com/howeyc/fsnotify",
@@ -140,24 +131,24 @@
     sha1 = "83cd2cd674a217ade95a4bb83a8a14f351f48bd0",
 )
 
-GUICE_VERS = "4.2.1"
+GUICE_VERS = "4.2.2"
 
 maven_jar(
     name = "guice-library",
     artifact = "com.google.inject:guice:" + GUICE_VERS,
-    sha1 = "f77dfd89318fe3ff293bafceaa75fbf66e4e4b10",
+    sha1 = "6dacbe18e5eaa7f6c9c36db33b42e7985e94ce77",
 )
 
 maven_jar(
     name = "guice-assistedinject",
     artifact = "com.google.inject.extensions:guice-assistedinject:" + GUICE_VERS,
-    sha1 = "d327e4aee7c96f08cd657c17da231a1f4a8999ac",
+    sha1 = "c33fb10080d58446f752b4fcfff8a5fabb80a449",
 )
 
 maven_jar(
     name = "guice-servlet",
     artifact = "com.google.inject.extensions:guice-servlet:" + GUICE_VERS,
-    sha1 = "3927e462f923b0c672fdb045c5645bca4beab5c0",
+    sha1 = "0d0054bdd812224078357a9b11409e43d182a046",
 )
 
 maven_jar(
@@ -178,61 +169,6 @@
     sha1 = "021a212688ec94fe77aff74ab34cc74f6f940e60",
 )
 
-GWT_VERS = "2.8.2"
-
-maven_jar(
-    name = "user",
-    artifact = "com.google.gwt:gwt-user:" + GWT_VERS,
-    sha1 = "a2b9be2c996a658c4e009ba652a9c6a81c88a797",
-)
-
-maven_jar(
-    name = "dev",
-    artifact = "com.google.gwt:gwt-dev:" + GWT_VERS,
-    sha1 = "7a87e060bbf129386b7ae772459fb9f87297c332",
-)
-
-maven_jar(
-    name = "javax-validation",
-    artifact = "javax.validation:validation-api:1.0.0.GA",
-    sha1 = "b6bd7f9d78f6fdaa3c37dae18a4bd298915f328e",
-    src_sha1 = "7a561191db2203550fbfa40d534d4997624cd369",
-)
-
-maven_jar(
-    name = "jsinterop-annotations",
-    artifact = "com.google.jsinterop:jsinterop-annotations:1.0.2",
-    sha1 = "abd7319f53d018e11108a88f599bd16492448dd2",
-    src_sha1 = "33716f8aef043f2f02b78ab4a1acda6cd90a7602",
-)
-
-maven_jar(
-    name = "ant",
-    artifact = "ant:ant:1.6.5",
-    attach_source = False,
-    sha1 = "7d18faf23df1a5c3a43613952e0e8a182664564b",
-)
-
-maven_jar(
-    name = "colt",
-    artifact = "colt:colt:1.2.0",
-    attach_source = False,
-    sha1 = "0abc984f3adc760684d49e0f11ddf167ba516d4f",
-)
-
-maven_jar(
-    name = "tapestry",
-    artifact = "tapestry:tapestry:4.0.2",
-    attach_source = False,
-    sha1 = "e855a807425d522e958cbce8697f21e9d679b1f7",
-)
-
-maven_jar(
-    name = "w3c-css-sac",
-    artifact = "org.w3c.css:sac:1.3",
-    sha1 = "cdb2dcb4e22b83d6b32b93095f644c3462739e82",
-)
-
 load("//lib/jgit:jgit.bzl", "jgit_repos")
 
 jgit_repos()
@@ -265,25 +201,11 @@
 )
 
 maven_jar(
-    name = "gwtjsonrpc",
-    artifact = "com.google.gerrit:gwtjsonrpc:1.11",
-    sha1 = "0990e7eec9eec3a15661edcf9232acbac4aeacec",
-    src_sha1 = "a682afc46284fb58197a173cb5818770a1e7834a",
-)
-
-maven_jar(
     name = "gson",
     artifact = "com.google.code.gson:gson:2.8.5",
     sha1 = "f645ed69d595b24d4cf8b3fbb64cc505bede8829",
 )
 
-maven_jar(
-    name = "gwtorm-client",
-    artifact = "com.google.gerrit:gwtorm:1.18",
-    sha1 = "f326dec463439a92ccb32f05b38345e21d0b5ecf",
-    src_sha1 = "e0b973d5cafef3d145fa80cdf032fcead1186d29",
-)
-
 load("//lib:guava.bzl", "GUAVA_BIN_SHA1", "GUAVA_VERSION")
 
 maven_jar(
@@ -318,6 +240,12 @@
 )
 
 maven_jar(
+    name = "guava-failureaccess",
+    artifact = "com.google.guava:failureaccess:1.0.1",
+    sha1 = "1dcf1de382a0bf95a3d8b0849546c88bac1292c9",
+)
+
+maven_jar(
     name = "jsch",
     artifact = "com.jcraft:jsch:0.1.54",
     sha1 = "da3584329a263616e277e15462b387addd1b208d",
@@ -400,8 +328,14 @@
 
 maven_jar(
     name = "commons-lang3",
-    artifact = "org.apache.commons:commons-lang3:3.6",
-    sha1 = "9d28a6b23650e8a7e9063c04588ace6cf7012c17",
+    artifact = "org.apache.commons:commons-lang3:3.8.1",
+    sha1 = "6505a72a097d9270f7a9e7bf42c4238283247755",
+)
+
+maven_jar(
+    name = "commons-text",
+    artifact = "org.apache.commons:commons-text:1.2",
+    sha1 = "74acdec7237f576c4803fff0c1008ab8a3808b2b",
 )
 
 maven_jar(
@@ -436,6 +370,33 @@
     sha1 = "959a0c62f9a5c2309e0ad0b0589c74d69e101241",
 )
 
+COMMONMARK_VERS = "0.10.0"
+
+# commonmark must match the version used in Gitiles
+maven_jar(
+    name = "commonmark",
+    artifact = "com.atlassian.commonmark:commonmark:" + COMMONMARK_VERS,
+    sha1 = "119cb7bedc3570d9ecb64ec69ab7686b5c20559b",
+)
+
+maven_jar(
+    name = "cm-autolink",
+    artifact = "com.atlassian.commonmark:commonmark-ext-autolink:" + COMMONMARK_VERS,
+    sha1 = "a6056a5efbd68f57d420bc51bbc54b28a5d3c56b",
+)
+
+maven_jar(
+    name = "gfm-strikethrough",
+    artifact = "com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:" + COMMONMARK_VERS,
+    sha1 = "40837da951b421b545edddac57012e15fcc9e63c",
+)
+
+maven_jar(
+    name = "gfm-tables",
+    artifact = "com.atlassian.commonmark:commonmark-ext-gfm-tables:" + COMMONMARK_VERS,
+    sha1 = "c075db2a3301100cf70c7dced8ecf86b494458a2",
+)
+
 FLEXMARK_VERS = "0.34.18"
 
 maven_jar(
@@ -588,7 +549,7 @@
     sha1 = "31e2e1fbe8273d7c913506eafeb06b1a7badb062",
 )
 
-# Transitive dependency of flexmark
+# Transitive dependency of flexmark and gitiles
 maven_jar(
     name = "autolink",
     artifact = "org.nibor.autolink:autolink:0.7.0",
@@ -631,50 +592,50 @@
     sha1 = "5e3bda828a80c7a21dfbe2308d1755759c2fd7b4",
 )
 
-OW2_VERS = "6.2.1"
+OW2_VERS = "7.0"
 
 maven_jar(
     name = "ow2-asm",
     artifact = "org.ow2.asm:asm:" + OW2_VERS,
-    sha1 = "c01b6798f81b0fc2c5faa70cbe468c275d4b50c7",
+    sha1 = "d74d4ba0dee443f68fb2dcb7fcdb945a2cd89912",
 )
 
 maven_jar(
     name = "ow2-asm-analysis",
     artifact = "org.ow2.asm:asm-analysis:" + OW2_VERS,
-    sha1 = "e8b876c5ccf226cae2f44ed2c436ad3407d0ec1d",
+    sha1 = "4b310d20d6f1c6b7197a75f1b5d69f169bc8ac1f",
 )
 
 maven_jar(
     name = "ow2-asm-commons",
     artifact = "org.ow2.asm:asm-commons:" + OW2_VERS,
-    sha1 = "eaf31376d741a3e2017248a4c759209fe25c77d3",
+    sha1 = "478006d07b7c561ae3a92ddc1829bca81ae0cdd1",
 )
 
 maven_jar(
     name = "ow2-asm-tree",
     artifact = "org.ow2.asm:asm-tree:" + OW2_VERS,
-    sha1 = "332b022092ecec53cdb6272dc436884b2d940615",
+    sha1 = "29bc62dcb85573af6e62e5b2d735ef65966c4180",
 )
 
 maven_jar(
     name = "ow2-asm-util",
     artifact = "org.ow2.asm:asm-util:" + OW2_VERS,
-    sha1 = "400d664d7c92a659d988c00cb65150d1b30cf339",
+    sha1 = "18d4d07010c24405129a6dbb0e92057f8779fb9d",
 )
 
-AUTO_VALUE_VERSION = "1.6.2"
+AUTO_VALUE_VERSION = "1.6.5"
 
 maven_jar(
     name = "auto-value",
     artifact = "com.google.auto.value:auto-value:" + AUTO_VALUE_VERSION,
-    sha1 = "e7eae562942315a983eea3e191b72d755c153620",
+    sha1 = "816872c85048f36a67a276ef7a49cc2e4595711c",
 )
 
 maven_jar(
     name = "auto-value-annotations",
     artifact = "com.google.auto.value:auto-value-annotations:" + AUTO_VALUE_VERSION,
-    sha1 = "ed193d86e0af90cc2342aedbe73c5d86b03fa09b",
+    sha1 = "c3dad10377f0e2242c9a4b88e9704eaf79103679",
 )
 
 declare_nongoogle_deps()
@@ -718,7 +679,7 @@
     sha1 = "0c9cfae15c74f62491d4f28def0dff1dabe52a47",
 )
 
-PROLOG_VERS = "1.4.3"
+PROLOG_VERS = "1.4.4"
 
 PROLOG_REPO = GERRIT
 
@@ -727,7 +688,7 @@
     artifact = "com.googlecode.prolog-cafe:prolog-runtime:" + PROLOG_VERS,
     attach_source = False,
     repository = PROLOG_REPO,
-    sha1 = "d5206556cbc76ffeab21313ffc47b586a1efbcbb",
+    sha1 = "e9a364f4233481cce63239e8e68a6190c8f58acd",
 )
 
 maven_jar(
@@ -735,7 +696,7 @@
     artifact = "com.googlecode.prolog-cafe:prolog-compiler:" + PROLOG_VERS,
     attach_source = False,
     repository = PROLOG_REPO,
-    sha1 = "f37032cf1dec3e064427745bc59da5a12757a3b2",
+    sha1 = "570295026f6aa7b905e423d107cb2e081eecdc04",
 )
 
 maven_jar(
@@ -743,7 +704,7 @@
     artifact = "com.googlecode.prolog-cafe:prolog-io:" + PROLOG_VERS,
     attach_source = False,
     repository = PROLOG_REPO,
-    sha1 = "d02b2640b26f64036b6ba2b45e4acc79281cea17",
+    sha1 = "1f25c4e27d22bdbc31481ee0c962a2a2853e4428",
 )
 
 maven_jar(
@@ -751,7 +712,7 @@
     artifact = "com.googlecode.prolog-cafe:prolog-cafeteria:" + PROLOG_VERS,
     attach_source = False,
     repository = PROLOG_REPO,
-    sha1 = "e3b1860c63e57265e5435f890263ad82dafa724f",
+    sha1 = "0e6c2deeaf5054815a561cbd663566fd59b56c6c",
 )
 
 maven_jar(
@@ -766,25 +727,43 @@
     sha1 = "f7be08ec23c21485b9b5a1cf1654c2ec8c58168d",
 )
 
+GITILES_VERS = "0.2-11"
+
+GITILES_REPO = GERRIT
+
 maven_jar(
     name = "blame-cache",
-    artifact = "com/google/gitiles:blame-cache:0.2-7.1",
+    artifact = "com.google.gitiles:blame-cache:" + GITILES_VERS,
     attach_source = False,
-    repository = GERRIT,
-    sha1 = "73915991bb7472a730102ab01ca68776a52466fd",
+    repository = GITILES_REPO,
+    sha1 = "254c1e81184301feccbcd857013c88f4d4ee32d9",
+)
+
+maven_jar(
+    name = "gitiles-servlet",
+    artifact = "com.google.gitiles:gitiles-servlet:" + GITILES_VERS,
+    repository = GITILES_REPO,
+    sha1 = "0a3e6cbdb99d42459b4b6080ee9b47821444e99b",
+)
+
+# prettify must match the version used in Gitiles
+maven_jar(
+    name = "prettify",
+    artifact = "com.github.twalcari:java-prettify:1.2.2",
+    sha1 = "b8ba1c1eb8b2e45cfd465d01218c6060e887572e",
 )
 
 # Keep this version of Soy synchronized with the version used in Gitiles.
 maven_jar(
     name = "soy",
-    artifact = "com.google.template:soy:2018-03-14",
-    sha1 = "76a1322705ba5a6d6329ee26e7387417725ce4b3",
+    artifact = "com.google.template:soy:2019-03-11",
+    sha1 = "119ac4b3eb0e2c638526ca99374013965c727097",
 )
 
 maven_jar(
     name = "html-types",
-    artifact = "com.google.common.html.types:types:1.0.4",
-    sha1 = "2adf4c8bfccc0ff7346f9186ac5aa57d829ad065",
+    artifact = "com.google.common.html.types:types:1.0.8",
+    sha1 = "9e9cf7bc4b2a60efeb5f5581fe46d17c068e0777",
 )
 
 maven_jar(
@@ -863,30 +842,30 @@
     sha1 = "42a25dc3219429f0e5d060061f71acb49bf010a0",
 )
 
-TRUTH_VERS = "0.42"
+TRUTH_VERS = "0.44"
 
 maven_jar(
     name = "truth",
     artifact = "com.google.truth:truth:" + TRUTH_VERS,
-    sha1 = "b5768f644b114e6cf5c3962c2ebcb072f788dcbb",
+    sha1 = "11eff954c0c14da7d43276d7b3bcf71463105368",
 )
 
 maven_jar(
     name = "truth-java8-extension",
     artifact = "com.google.truth.extensions:truth-java8-extension:" + TRUTH_VERS,
-    sha1 = "4d01dfa5b3780632a3d109e14e101f01d10cce2c",
+    sha1 = "2081a0721d3101e1cf559f013e59c6129b4b10b0",
 )
 
 maven_jar(
     name = "truth-liteproto-extension",
     artifact = "com.google.truth.extensions:truth-liteproto-extension:" + TRUTH_VERS,
-    sha1 = "c231e6735aa6c133c7e411ae1c1c90b124900a8b",
+    sha1 = "64f47e4e3f79b0a582573098b9c3c6b73599f7c6",
 )
 
 maven_jar(
     name = "truth-proto-extension",
     artifact = "com.google.truth.extensions:truth-proto-extension:" + TRUTH_VERS,
-    sha1 = "c41d22e8b4a61b4171e57c44a2959ebee0091a14",
+    sha1 = "c03fbc16087d8cb3bf0f3265a04566d4beb88a6d",
 )
 
 maven_jar(
@@ -902,13 +881,6 @@
     sha1 = "3e127311a86fc2e8f550ef8ee4abe094bbcf7e7e",
 )
 
-maven_jar(
-    name = "derby",
-    artifact = "org.apache.derby:derby:10.12.1.1",
-    attach_source = False,
-    sha1 = "75070c744a8e52a7d17b8b476468580309d5cd09",
-)
-
 JETTY_VERS = "9.4.14.v20181114"
 
 maven_jar(
@@ -924,12 +896,6 @@
 )
 
 maven_jar(
-    name = "jetty-servlets",
-    artifact = "org.eclipse.jetty:jetty-servlets:" + JETTY_VERS,
-    sha1 = "38cfc07b53e5d285bb2fca78bb2531565ed9c9e5",
-)
-
-maven_jar(
     name = "jetty-server",
     artifact = "org.eclipse.jetty:jetty-server:" + JETTY_VERS,
     sha1 = "b36a3d52d78a1df6406f6fa236a6eeff48cbfef6",
@@ -966,31 +932,6 @@
 )
 
 maven_jar(
-    name = "postgresql",
-    artifact = "org.postgresql:postgresql:42.2.5",
-    sha1 = "951b7eda125f3137538a94e2cbdcf744088ad4c2",
-)
-
-maven_jar(
-    name = "codemirror-minified-gwt",
-    artifact = "org.webjars.npm:codemirror-minified:" + CM_VERSION,
-    sha1 = "36558ea3b8e30782e1e09c0e7bd781e09614f139",
-)
-
-maven_jar(
-    name = "codemirror-original-gwt",
-    artifact = "org.webjars.npm:codemirror:" + CM_VERSION,
-    sha1 = "f1f8fbbc3e2d224fdccc43d2f4180658a92320f9",
-)
-
-maven_jar(
-    name = "diff-match-patch",
-    artifact = "org.webjars:google-diff-match-patch:" + DIFF_MATCH_PATCH_VERSION,
-    attach_source = False,
-    sha1 = "0cf1782dbcb8359d95070da9176059a5a9d37709",
-)
-
-maven_jar(
     name = "commons-io",
     artifact = "commons-io:commons-io:2.2",
     sha1 = "83b5b8a7ba1c08f9e8c8ff2373724e33d3c1e22a",
@@ -1008,6 +949,32 @@
     sha1 = "485de3a253e23f645037828c07f1d7f1af40763a",
 )
 
+maven_jar(
+    name = "mockito",
+    artifact = "org.mockito:mockito-core:2.24.0",
+    sha1 = "969a7bcb6f16e076904336ebc7ca171d412cc1f9",
+)
+
+BYTE_BUDDY_VERSION = "1.9.7"
+
+maven_jar(
+    name = "byte-buddy",
+    artifact = "net.bytebuddy:byte-buddy:" + BYTE_BUDDY_VERSION,
+    sha1 = "8fea78fea6449e1738b675cb155ce8422661e237",
+)
+
+maven_jar(
+    name = "byte-buddy-agent",
+    artifact = "net.bytebuddy:byte-buddy-agent:" + BYTE_BUDDY_VERSION,
+    sha1 = "8e7d1b599f4943851ffea125fd9780e572727fc0",
+)
+
+maven_jar(
+    name = "objenesis",
+    artifact = "org.objenesis:objenesis:2.6",
+    sha1 = "639033469776fd37c08358c6b92a4761feb2af4b",
+)
+
 load("//tools/bzl:js.bzl", "bower_archive", "npm_binary")
 
 # NPM binaries bundled along with their dependencies.
@@ -1179,8 +1146,8 @@
 bower_archive(
     name = "codemirror-minified",
     package = "Dominator008/codemirror-minified",
-    sha1 = "1524e19087d8223edfe4a5b1ccf04c1e3707235d",
-    version = "5.37.0",
+    sha1 = "e6bda82afc7cf3493f4282c6f17265d40e1485e5",
+    version = "5.43.0",
 )
 
 # bower test stuff
diff --git a/antlr3/BUILD b/antlr3/BUILD
index 4af6006..549946a 100644
--- a/antlr3/BUILD
+++ b/antlr3/BUILD
@@ -21,7 +21,8 @@
     name = "query_parser",
     srcs = [":query"],
     visibility = [
-        "//java/com/google/gerrit/index:__pkg__",
+        "//java/com/google/gerrit/index:__subpackages__",
+        "//javatests/com/google/gerrit:__subpackages__",
         "//javatests/com/google/gerrit/index:__pkg__",
         "//plugins:__pkg__",
     ],
diff --git a/antlr3/com/google/gerrit/index/query/Query.g b/antlr3/com/google/gerrit/index/query/Query.g
index 953a473..1bf20aa 100644
--- a/antlr3/com/google/gerrit/index/query/Query.g
+++ b/antlr3/com/google/gerrit/index/query/Query.g
@@ -120,12 +120,24 @@
   ;
 conditionBase
   : '('! conditionOr ')'!
-  | (FIELD_NAME ':') => FIELD_NAME^ ':'! fieldValue
+  | (FIELD_NAME COLON) => FIELD_NAME^ COLON! fieldValue
   | fieldValue -> ^(DEFAULT_FIELD fieldValue)
   ;
 
 fieldValue
-  : n=FIELD_NAME   -> SINGLE_WORD[n]
+  // Rewrite by invoking SINGLE_WORD fragment lexer rule, passing the field name as an argument.
+  : n=FIELD_NAME -> SINGLE_WORD[n]
+
+  // Allow field values to contain a colon. We can't do this at the lexer level, because we need to
+  // emit a separate token for the field name. If we were to allow ':' in SINGLE_WORD, then
+  // everything would just lex as DEFAULT_FIELD.
+  //
+  // Field values with a colon may be lexed either as <field>:<rest> or <word>:<rest>, depending on
+  // whether the part before the colon looks like a field name.
+  // TODO(dborowitz): Field values ending in colon still don't work.
+  | (FIELD_NAME COLON) => n=FIELD_NAME COLON fieldValue -> SINGLE_WORD[n] COLON fieldValue
+  | (SINGLE_WORD COLON) => SINGLE_WORD COLON fieldValue
+
   | SINGLE_WORD
   | EXACT_PHRASE
   ;
@@ -134,6 +146,8 @@
 OR:  'OR'  ;
 NOT: 'NOT' ;
 
+COLON: ':' ;
+
 WS
   :  ( ' ' | '\r' | '\t' | '\n' ) { $channel=HIDDEN; }
   ;
@@ -172,7 +186,7 @@
      // '-'  permit
      // '.'  permit
      // '/'  permit
-     | ':'
+     | COLON
      | ';'
      // '<' permit
      // '=' permit
diff --git a/contrib/mitm-ui/README.md b/contrib/mitm-ui/README.md
new file mode 100644
index 0000000..1ec8dd4
--- /dev/null
+++ b/contrib/mitm-ui/README.md
@@ -0,0 +1,61 @@
+# Scripts for PolyGerrit local development against prod using MitmProxy.
+
+## Installation (OSX)
+
+1. Install Docker from http://docker.com
+2. Start the proxy and create a new proxied browser instance
+   ```
+   cd ~/gerrit
+   ~/mitm-gerrit/mitm-serve-app-dev.sh
+   ```
+3. Make sure that the browser uses the proxy provided by the command line,
+   e.g. if you are a Googler check that the BeyondCorp extension uses the
+   "System/Alternative" proxy.
+4. Install MITM certificates
+   - Open http://mitm.it in the proxied browser window
+   - Follow the instructions to install MITM certs
+
+## Usage
+
+### Add or replace a single plugin containing static content
+
+To develop unminified plugin that loads multiple files, use this.
+
+1. Create a new proxied browser window and start mitmproxy via Docker:
+   ```
+   ~/mitm-gerrit/mitm-single-plugin.sh ./path/to/static/plugin.html
+   ```
+2. Open any *.googlesource.com domain in proxied window
+3. plugin.html and ./path/to/static/* will be served
+
+### Add or replace a minified plugin for *.googlesource.com
+
+This flow assumes no additional .html/.js are needed, i.e. the plugin is a single file.
+
+1. Create a new proxied browser window and start mitmproxy via Docker:
+   ```
+   ~/mitm-gerrit/mitm-plugins.sh ./path/to/plugin.html,./maybe/one/more.js
+   ```
+2. Open any *.googlesource.com domain in proxied window
+3. plugin.html and more.js are served
+
+### Force or replace default site theme for *.googlesource.com
+
+1. Create a new proxied browser window and start mitmproxy via Docker:
+   ```
+   ~/mitm-gerrit/mitm-theme.sh ./path/to/theme.html
+   ```
+2. Open any *.googlesource.com domain in proxied window
+3. Default site themes are enabled.
+4. Local `theme.html` content replaces `/static/gerrit-theme.html`
+5. `/static/*` URLs are served from local theme directory, i.e. `./path/to/`
+
+### Serve uncompiled PolyGerrit
+
+1. Create a new proxied browser window and start mitmproxy via Docker:
+   ```
+   cd ~/gerrit
+   ~/mitm-gerrit/mitm-serve-app-dev.sh
+   ```
+2. Open any *.googlesource.com domain in proxied window
+3. Instead of prod UI (gr-app.html, gr-app.js), local source files will be served
diff --git a/contrib/mitm-ui/add-header.py b/contrib/mitm-ui/add-header.py
new file mode 100644
index 0000000..f9b2b12
--- /dev/null
+++ b/contrib/mitm-ui/add-header.py
@@ -0,0 +1,5 @@
+# mitmdump -s add-header.py
+def response(flow):
+    if flow.request.host == 'gerrit-review.googlesource.com' and flow.request.path == "/c/92000?1":
+        #flow.response.headers['any'] = '<meta.rdf>; rel=meta'
+        flow.response.headers['Link'] = '</changes/98000/detail?O=11640c>;rel="preload";crossorigin;'
diff --git a/contrib/mitm-ui/dev-chrome.sh b/contrib/mitm-ui/dev-chrome.sh
new file mode 100755
index 0000000..adcb296
--- /dev/null
+++ b/contrib/mitm-ui/dev-chrome.sh
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+if [[ "$OSTYPE" != "darwin"* ]]; then
+    echo Only works on OSX.
+    exit 1
+fi
+
+/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 --user-data-dir=${HOME}/devchrome --proxy-server="127.0.0.1:8888"
diff --git a/contrib/mitm-ui/force-version.py b/contrib/mitm-ui/force-version.py
new file mode 100644
index 0000000..a69c885
--- /dev/null
+++ b/contrib/mitm-ui/force-version.py
@@ -0,0 +1,22 @@
+# mitmdump -q -p 8888 -s "force-version.py --version $1"
+# Request URL is not changed, only the response context
+from mitmproxy import http
+import argparse
+import re
+
+class Server:
+    def __init__(self, version):
+        self.version = version
+
+    def request(self, flow: http.HTTPFlow) -> None:
+        if "gr-app." in flow.request.pretty_url:
+            flow.request.url = re.sub(
+                r"polygerrit_ui/([\d.]+)/elements",
+                "polygerrit_ui/" + self.version + "/elements",
+                flow.request.url)
+
+def start():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--version", type=str, help="Rapid release version, e.g. 432.0")
+    args = parser.parse_args()
+    return Server(args.version)
diff --git a/contrib/mitm-ui/mitm-docker.sh b/contrib/mitm-ui/mitm-docker.sh
new file mode 100755
index 0000000..a1206f7
--- /dev/null
+++ b/contrib/mitm-ui/mitm-docker.sh
@@ -0,0 +1,43 @@
+#!/bin/sh
+
+extra_volume='/tmp:/tmp'
+
+POSITIONAL=()
+while [[ $# -gt 0 ]]
+do
+key="$1"
+
+case $key in
+    -v|--volume)
+    extra_volume="$2"
+    shift # past argument
+    shift # past value
+    ;;
+    *)    # unknown option
+    POSITIONAL+=("$1") # save it in an array for later
+    shift # past argument
+    ;;
+esac
+done
+set -- "${POSITIONAL[@]}" # restore positional parameters
+
+if [[ -z "$1" ]]; then
+    echo This is a runner for higher-level scripts, e.g. mitm-serve-app-dev.sh
+    echo Alternatively, pass mitmproxy script from the same dir as a parameter, e.g. serve-app-dev.py
+    exit 1
+fi
+
+gerrit_dir=$(pwd)
+mitm_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
+
+CMD="${mitm_dir}/$1"
+
+docker run --rm -it \
+       -v ~/.mitmproxy:/home/mitmproxy/.mitmproxy \
+       -v ${mitm_dir}:${mitm_dir} \
+       -v ${gerrit_dir}:${gerrit_dir} \
+       -v ${gerrit_dir}/bazel-out:${gerrit_dir}/bazel-out \
+       -v ${extra_volume} \
+       -p 8888:8888 \
+       mitmproxy/mitmproxy:2.0.2 \
+       mitmdump -q -p 8888 -s "${CMD}"
diff --git a/contrib/mitm-ui/mitm-plugins.sh b/contrib/mitm-ui/mitm-plugins.sh
new file mode 100755
index 0000000..fc542bb
--- /dev/null
+++ b/contrib/mitm-ui/mitm-plugins.sh
@@ -0,0 +1,39 @@
+#!/bin/sh
+
+if [[ -z "$1" ]]; then
+    echo This script injects plugins for *.googlesource.com.
+    echo Provide plugin paths, comma-separated, as a parameter.
+    echo This script assumes files do not have dependencies, i.e. minified.
+    exit 1
+fi
+
+realpath() {
+    [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"
+}
+
+join () {
+  local IFS="$1"
+  shift
+  echo "$*"
+}
+
+plugins=$1
+plugin_paths=()
+for plugin in $(echo ${plugins} | sed "s/,/ /g")
+do
+    plugin_paths+=($(realpath ${plugin}))
+done
+
+absolute_plugin_paths=$(join , "${plugin_paths[@]}")
+
+mitm_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
+
+${mitm_dir}/dev-chrome.sh &
+
+bazel build //polygerrit-ui/app:test_components &
+
+${mitm_dir}/mitm-docker.sh \
+           "serve-app-dev.py \
+           --plugins ${absolute_plugin_paths} \
+           --strip_assets \
+           --components $(pwd)/bazel-bin/polygerrit-ui/app/"
diff --git a/contrib/mitm-ui/mitm-serve-app-dev.sh b/contrib/mitm-ui/mitm-serve-app-dev.sh
new file mode 100755
index 0000000..d4c72cc
--- /dev/null
+++ b/contrib/mitm-ui/mitm-serve-app-dev.sh
@@ -0,0 +1,15 @@
+#!/bin/sh
+
+workspace="./WORKSPACE"
+if [[ ! -f ${workspace} ]] || [[ ! $(head -n 1 ${workspace}) == *"gerrit"* ]]; then
+    echo Please change to cloned Gerrit repo from https://gerrit.googlesource.com/gerrit/
+    exit 1
+fi
+
+mitm_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
+
+bazel build //polygerrit-ui/app:test_components &
+
+${mitm_dir}/dev-chrome.sh &
+
+${mitm_dir}/mitm-docker.sh "serve-app-dev.py --app $(pwd)/polygerrit-ui/app/ --components $(pwd)/bazel-bin/polygerrit-ui/app/"
diff --git a/contrib/mitm-ui/mitm-single-plugin.sh b/contrib/mitm-ui/mitm-single-plugin.sh
new file mode 100755
index 0000000..8958229
--- /dev/null
+++ b/contrib/mitm-ui/mitm-single-plugin.sh
@@ -0,0 +1,38 @@
+#!/bin/sh
+
+if [[ -z "$1" ]]; then
+    echo This script serves one plugin with the rest of static content.
+    echo Provide path to index plugin file, e.g. buildbucket.html for buildbucket plugin
+    exit 1
+fi
+
+realpath() {
+  OURPWD=$PWD
+  cd "$(dirname "$1")"
+  LINK=$(basename "$1")
+  while [ -L "$LINK" ]; do
+      LINK=$(readlink "$LINK")
+      cd "$(dirname "$LINK")"
+      LINK="$(basename "$1")"
+  done
+  REAL_DIR=`pwd -P`
+  RESULT=$REAL_DIR/$LINK
+  cd "$OURPWD"
+  echo "$RESULT"
+}
+
+plugin=$(realpath $1)
+plugin_root=$(dirname ${plugin})
+
+mitm_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
+
+${mitm_dir}/dev-chrome.sh &
+
+bazel build //polygerrit-ui/app:test_components &
+
+${mitm_dir}/mitm-docker.sh -v ${plugin_root}:${plugin_root} \
+           "serve-app-dev.py \
+           --plugins ${plugin} \
+           --strip_assets \
+           --plugin_root ${plugin_root}  \
+           --components $(pwd)/bazel-bin/polygerrit-ui/app/"
diff --git a/contrib/mitm-ui/mitm-theme.sh b/contrib/mitm-ui/mitm-theme.sh
new file mode 100755
index 0000000..9290235
--- /dev/null
+++ b/contrib/mitm-ui/mitm-theme.sh
@@ -0,0 +1,31 @@
+#!/bin/sh
+
+if [[ -z "$1" ]]; then
+    echo This script forces or replaces default site theme on *.googlesource.com
+    echo Provide path to the theme.html as a parameter.
+    exit 1
+fi
+
+realpath() {
+  OURPWD=$PWD
+  cd "$(dirname "$1")"
+  LINK=$(basename "$1")
+  while [ -L "$LINK" ]; do
+      LINK=$(readlink "$LINK")
+      cd "$(dirname "$LINK")"
+      LINK="$(basename "$1")"
+  done
+  REAL_DIR=`pwd -P`
+  RESULT=$REAL_DIR/$LINK
+  cd "$OURPWD"
+  echo "$RESULT"
+}
+
+theme=$(realpath "$1")
+theme_dir=$(dirname "${theme}")
+
+mitm_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
+
+"${mitm_dir}"/dev-chrome.sh &
+
+"${mitm_dir}"/mitm-docker.sh -v "${theme_dir}":"${theme_dir}" "serve-app-dev.py --strip_assets --theme \"${theme}\""
diff --git a/contrib/mitm-ui/serve-app-dev.py b/contrib/mitm-ui/serve-app-dev.py
new file mode 100644
index 0000000..cdf7bfc
--- /dev/null
+++ b/contrib/mitm-ui/serve-app-dev.py
@@ -0,0 +1,169 @@
+# 1. install and setup mitmproxy v2.0.2: https://mitmproxy.readthedocs.io/en/v2.0.2/install.html
+#   (In case of python versions trouble, use https://www.anaconda.com/)
+# 2. mitmdump -q -s -p 8888 \
+#   "serve-app-dev.py --app /path/to/polygerrit-ui/app/"
+# 3. start Chrome with --proxy-server="127.0.0.1:8888" --user-data-dir=/tmp/devchrome
+# 4. open, say, gerrit-review.googlesource.com. Or chromium-review.googlesource.com. Any.
+# 5. uncompiled source files are served and you can log in, too.
+# 6. enjoy!
+#
+# P.S. For replacing plugins, use --plugins or --plugin_root
+#
+# --plugin takes comma-separated list of plugins to add or replace.
+#
+# Example: Adding a new plugin to the server response:
+# --plugins ~/gerrit-testsite/plugins/myplugin.html
+#
+# Example: Replace all matching plugins with local versions:
+# --plugins ~/gerrit-testsite/plugins/
+# Following files will be served if they exist for /plugins/tricium/static/tricium.html:
+#  ~/gerrit-testsite/plugins/tricium.html
+#  ~/gerrit-testsite/plugins/tricium/static/tricium.html
+#
+# --assets takes assets bundle.html, expecting rest of the assets files to be in the same folder
+#
+# Example:
+#  --assets ~/gerrit-testsite/assets/a3be19f.html
+#
+
+from mitmproxy import http
+from mitmproxy.script import concurrent
+import argparse
+import json
+import mimetypes
+import os.path
+import re
+import zipfile
+
+class Server:
+    def __init__(self, devpath, components, plugins, pluginroot, assets, strip_assets, theme):
+        if devpath:
+            print("Serving app from " + devpath)
+        if components:
+            print("Serving components from " + components)
+        if pluginroot:
+            print("Serving plugins from " + pluginroot)
+        if assets:
+            self.assets_root, self.assets_file = os.path.split(assets)
+            print("Assets: using " + self.assets_file + " from " + self.assets_root)
+        else:
+            self.assets_root = None
+        if plugins:
+            self.plugins = {path.split("/")[-1:][0]: path for path in map(expandpath, plugins.split(","))}
+            for filename, path in self.plugins.items():
+                print("Serving " + filename + " from " + path)
+        else:
+            self.plugins = {}
+        self.devpath = devpath
+        self.components = components
+        self.pluginroot = pluginroot
+        self.strip_assets = strip_assets
+        self.theme = theme
+
+    def readfile(self, path):
+        with open(path, 'rb') as contentfile:
+            return contentfile.read()
+
+@concurrent
+def response(flow: http.HTTPFlow) -> None:
+    if server.strip_assets:
+        assets_bundle = 'googlesource.com/polygerrit_assets'
+        assets_pos = flow.response.text.find(assets_bundle)
+        if assets_pos != -1:
+            t = flow.response.text
+            flow.response.text = t[:t.rfind('<', 0, assets_pos)] + t[t.find('>', assets_pos) + 1:]
+            return
+
+    if server.assets_root:
+        marker = 'webcomponents-lite.js"></script>'
+        pos = flow.response.text.find(marker)
+        if pos != -1:
+            pos += len(marker)
+            flow.response.text = ''.join([
+                flow.response.text[:pos],
+                '<link rel="import" href="/gerrit_assets/123.0/' + server.assets_file + '">',
+                flow.response.text[pos:]
+            ])
+
+        assets_prefix = "/gerrit_assets/123.0/"
+        if flow.request.path.startswith(assets_prefix):
+            assets_file = flow.request.path[len(assets_prefix):]
+            flow.response.content = server.readfile(server.assets_root + '/' + assets_file)
+            flow.response.status_code = 200
+            if assets_file.endswith('.js'):
+                flow.response.headers['Content-type'] = 'text/javascript'
+            return
+    m = re.match(".+polygerrit_ui/\d+\.\d+/(.+)", flow.request.path)
+    pluginmatch = re.match("^/plugins/(.+)", flow.request.path)
+    localfile = ""
+    content = ""
+    if flow.request.path == "/config/server/info":
+        config = json.loads(flow.response.content[5:].decode('utf8'))
+        if server.theme:
+            config['default_theme'] = '/static/gerrit-theme.html'
+        for filename, path in server.plugins.items():
+            pluginname = filename.split(".")[0]
+            payload = config["plugin"]["js_resource_paths" if filename.endswith(".js") else "html_resource_paths"]
+            if list(filter(lambda url: filename in url, payload)):
+                continue
+            payload.append("plugins/" + pluginname + "/static/" + filename)
+        flow.response.content = str.encode(")]}'\n" + json.dumps(config))
+    if m is not None:
+        filepath = m.groups()[0]
+        if (filepath.startswith("bower_components/")):
+            with zipfile.ZipFile(server.components + "test_components.zip") as bower_zip:
+                content = bower_zip.read(filepath)
+        localfile = server.devpath + filepath
+    elif pluginmatch is not None:
+        pluginfile = flow.request.path_components[-1]
+        if server.plugins and pluginfile in server.plugins:
+            if os.path.isfile(server.plugins[pluginfile]):
+                localfile = server.plugins[pluginfile]
+            else:
+                print("Can't find file " + server.plugins[pluginfile] + " for " + flow.request.path)
+        elif server.pluginroot:
+            pluginurl = pluginmatch.groups()[0]
+            if os.path.isfile(server.pluginroot + pluginfile):
+                localfile = server.pluginroot + pluginfile
+            elif os.path.isfile(server.pluginroot + pluginurl):
+                localfile = server.pluginroot + pluginurl
+
+    if server.theme:
+        if flow.request.path.endswith('/gerrit-theme.html'):
+            localfile = server.theme
+        else:
+            match = re.match("^/static(/[\w\.]+)$", flow.request.path)
+            if match is not None:
+                localfile = os.path.dirname(server.theme) + match.group(1)
+
+    if localfile and os.path.isfile(localfile):
+        if pluginmatch is not None:
+            print("Serving " + flow.request.path + " from " + localfile)
+        content = server.readfile(localfile)
+
+    if content:
+        flow.response.content = content
+        flow.response.status_code = 200
+        localtype = mimetypes.guess_type(localfile)
+        if localtype and localtype[0]:
+            flow.response.headers['Content-type'] = localtype[0]
+
+def expandpath(path):
+    return os.path.realpath(os.path.expanduser(path))
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--app", type=str, default="", help="Path to /polygerrit-ui/app/")
+parser.add_argument("--components", type=str, default="", help="Path to test_components.zip")
+parser.add_argument("--plugins", type=str, default="", help="Comma-separated list of plugin files to add/replace")
+parser.add_argument("--plugin_root", type=str, default="", help="Path containing individual plugin files to replace")
+parser.add_argument("--assets", type=str, default="", help="Path containing assets file to import.")
+parser.add_argument("--strip_assets", action="store_true", help="Strip plugin bundles from the response.")
+parser.add_argument("--theme", default="", type=str, help="Path to the default site theme to be used.")
+args = parser.parse_args()
+server = Server(expandpath(args.app) + '/',
+                expandpath(args.components) + '/',
+                args.plugins,
+                expandpath(args.plugin_root) + '/',
+                args.assets and expandpath(args.assets),
+                args.strip_assets,
+                expandpath(args.theme))
diff --git a/contrib/mitm-ui/serve-app-locally.py b/contrib/mitm-ui/serve-app-locally.py
new file mode 100644
index 0000000..636c684
--- /dev/null
+++ b/contrib/mitm-ui/serve-app-locally.py
@@ -0,0 +1,46 @@
+# bazel build polygerrit-ui/app:gr-app
+# mitmdump -s "serve-app-locally.py ~/gerrit/bazel-bin/polygerrit-ui/app"
+from mitmproxy import http
+import argparse
+import os
+import zipfile
+
+class Server:
+    def __init__(self, bundle):
+        self.bundle = bundle
+        self.bundlemtime = 0
+        self.files = {
+            'polygerrit_ui/elements/gr-app.js': '',
+            'polygerrit_ui/elements/gr-app.html': '',
+            'polygerrit_ui/styles/main.css': '',
+        }
+        self.read_files()
+
+    def read_files(self):
+        if not os.path.isfile(self.bundle):
+            print("bundle not found!")
+            return
+        mtime = os.stat(self.bundle).st_mtime
+        if mtime <= self.bundlemtime:
+            return
+        self.bundlemtime = mtime
+        with zipfile.ZipFile(self.bundle) as z:
+            for fname in self.files:
+                print('Reading new content for ' + fname)
+                with z.open(fname, 'r') as content_file:
+                    self.files[fname] = content_file.read()
+
+    def response(self, flow: http.HTTPFlow) -> None:
+        self.read_files()
+        for name in self.files:
+            if name.rsplit('/', 1)[1] in flow.request.pretty_url:
+                flow.response.content = self.files[name]
+
+def expandpath(path):
+    return os.path.expanduser(path)
+
+def start():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("bundle", type=str)
+    args = parser.parse_args()
+    return Server(expandpath(args.bundle))
diff --git a/contrib/themes/diffy/etc/GerritSite.css b/contrib/themes/diffy/etc/GerritSite.css
deleted file mode 100644
index 76c595a..0000000
--- a/contrib/themes/diffy/etc/GerritSite.css
+++ /dev/null
@@ -1,29 +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.
- */
-#gerrit_topmenu {
-  left: 60px;
-  margin-right: 60px;
-  position: relative;
-  margin-bottom: 5px;
-}
-
-#diffy_logo {
-  display: block !important;
-  margin-bottom: -55px;
-  padding-left: 10px;
-  position: relative;
-  top: -55px;
-  width: 60px;
-}
diff --git a/contrib/themes/diffy/etc/GerritSiteHeader.html b/contrib/themes/diffy/etc/GerritSiteHeader.html
deleted file mode 100644
index 89b4db5..0000000
--- a/contrib/themes/diffy/etc/GerritSiteHeader.html
+++ /dev/null
@@ -1,5 +0,0 @@
-<div>
-  <div id="diffy_logo">
-    <img width="50" height="46" src="static/logo.png"/>
-  </div>
-</div>
diff --git a/contrib/themes/diffy/static/diffy.svg b/contrib/themes/diffy/static/diffy.svg
deleted file mode 100644
index 3e6e6c2..0000000
--- a/contrib/themes/diffy/static/diffy.svg
+++ /dev/null
@@ -1,326 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
-   xmlns:dc="http://purl.org/dc/elements/1.1/"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   width="600.06732"
-   height="558.20709"
-   id="svg2"
-   version="1.1"
-   inkscape:version="0.48.3.1 r9886"
-   sodipodi:docname="diffy.svg"
-   inkscape:export-filename="/home/sarah/art/diffy.png"
-   inkscape:export-xdpi="500"
-   inkscape:export-ydpi="500">
-  <defs
-     id="defs4" />
-  <sodipodi:namedview
-     id="base"
-     pagecolor="#ffffff"
-     bordercolor="#666666"
-     borderopacity="1.0"
-     inkscape:pageopacity="0.0"
-     inkscape:pageshadow="2"
-     inkscape:zoom="0.66618777"
-     inkscape:cx="300.03365"
-     inkscape:cy="258.35521"
-     inkscape:document-units="px"
-     inkscape:current-layer="layer1"
-     showgrid="false"
-     inkscape:window-width="1366"
-     inkscape:window-height="744"
-     inkscape:window-x="0"
-     inkscape:window-y="24"
-     inkscape:window-maximized="1"
-     fit-margin-top="0"
-     fit-margin-left="0"
-     fit-margin-right="0"
-     fit-margin-bottom="0" />
-  <metadata
-     id="metadata7">
-    <rdf:RDF>
-      <cc:Work
-         rdf:about="">
-        <dc:format>image/svg+xml</dc:format>
-        <dc:type
-           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
-        <dc:title />
-      </cc:Work>
-    </rdf:RDF>
-  </metadata>
-  <g
-     inkscape:label="Layer 1"
-     inkscape:groupmode="layer"
-     id="layer1"
-     transform="translate(-3069.8852,2251.5084)">
-    <text
-       xml:space="preserve"
-       style="font-size:51.65934372px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:end;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:end;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeSans;-inkscape-font-specification:FreeSans Bold"
-       x="475.83173"
-       y="900.47076"
-       id="text4257"
-       sodipodi:linespacing="125%"
-       transform="scale(1.1411753,0.87628955)"><tspan
-         sodipodi:role="line"
-         x="475.83173"
-         y="900.47076"
-         style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:end;text-anchor:end;font-family:Acme;-inkscape-font-specification:Acme Bold"
-         id="tspan3008" /></text>
-    <flowRoot
-       xml:space="preserve"
-       id="flowRoot4263"
-       style="font-size:12px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><flowRegion
-         id="flowRegion4265"><rect
-           id="rect4267"
-           width="313.14728"
-           height="343.45187"
-           x="-335.37064"
-           y="205.85435" /></flowRegion><flowPara
-         id="flowPara4269" /></flowRoot>    <g
-       id="g10119"
-       transform="matrix(0.99201906,-0.12608805,0.12608805,0.99201906,273.85438,408.58042)">
-      <path
-         sodipodi:nodetypes="csccscac"
-         inkscape:connector-curvature="0"
-         id="path4130"
-         d="m 3378.9965,-2206.2972 c 0,0 13.4039,-8.8398 27.0022,-17.1718 13.5984,-8.3319 22.2322,-21.276 22.2322,-21.276 l 34.2799,28.1467 c 0,0 -16.0641,15.553 -24.7742,21.6893 -3.709,2.6129 -10.9463,9.1577 -10.9463,9.1577 0,0 -14.3617,-10.0769 -22.3623,-13.5225 -8.0773,-3.4786 -25.4315,-7.0234 -25.4315,-7.0234 z"
-         style="fill:#ffffff;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
-      <path
-         sodipodi:nodetypes="cac"
-         inkscape:connector-curvature="0"
-         d="m 3414.7387,-2204.9828 c 0,0 8.2233,-5.8571 11.9326,-9.2622 3.8224,-3.5089 10.5596,-11.437 10.5596,-11.437"
-         style="fill:#ffffff;stroke:#ff0000;stroke-width:8.97858429;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
-         id="path4185" />
-      <path
-         sodipodi:nodetypes="cascacsc"
-         inkscape:connector-curvature="0"
-         id="path3307"
-         d="m 3247.6516,-2171.4037 c 0,0 4.3348,-19.2835 10.7071,-26.2701 7.4284,-8.1446 19.072,-14.1542 29.3495,-15.24 27.8177,-2.9386 63.2371,6.1908 63.2371,6.1908 0,0 -30.8042,0.7262 -45.6022,4.5134 -5.1922,1.3289 -14.7941,6.2977 -14.7941,6.2977 0,0 -16.1166,0.8374 -29.8971,12.0927 -13.7804,11.2552 -3.0344,9.7831 -3.0344,9.7831"
-         style="fill:#ffffff;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
-      <path
-         sodipodi:nodetypes="ccsccscscccccscc"
-         inkscape:connector-curvature="0"
-         id="path3011"
-         d="m 3332.9985,-2012.4352 -20.0742,24.9886 c 0,0 -16.3479,-5.082 -40.3255,4.9725 -23.9776,10.0546 -39.2193,26.4042 -39.2193,26.4042 l 7.8897,-0.7975 c 0,0 20.4818,-14.4371 31.9614,-17.1294 11.4796,-2.6923 23.6197,-2.8388 23.6197,-2.8388 0,0 -25.492,10.0264 -30.7497,12.3759 -6.2584,2.7968 -15.0874,9.199 -15.0874,9.199 l 10.4761,-0.2965 c 17.6867,-7.2528 41.3221,-16.0187 59.9685,-15.047 15.5919,1.1274 33.9405,16.7543 33.9405,16.7543 l -3.8165,-9.9895 c 0,0 -10.8307,-8.1932 -16.3866,-13.7491 -5.5558,-5.5558 17.5848,-25.8193 17.5848,-25.8193 -14.3098,-2.8351 -2.1172,-7.9424 -19.7815,-9.0274 z"
-         style="fill:#ffff00;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
-      <path
-         style="fill:#ffff00;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
-         d="m 3397.9929,-1984.0461 -15.5774,33.0846 c 0,0 -14.4499,-2.2165 -39.5097,4.7139 -25.0597,6.9303 -44.9575,20.0476 -44.9575,20.0476 l 10.5706,1.822 c 0,0 19.7276,-11.1182 30.516,-14.1955 7.747,-2.2097 23.9934,-2.9008 23.9934,-2.9008 0,0 -20.2687,6.6869 -30.057,10.9033 -5.4067,2.329 -15.7817,7.9273 -15.7817,7.9273 l 10.8391,1.5269 c 0,0 31.056,-11.8625 47.4047,-13.3504 11.1103,-1.0112 11.8331,-2.962 33.324,3.1079 10.0509,2.8388 16.3928,6.252 16.3928,6.252 l -3.3122,-5.8696 c 0,0 -5.3909,-4.5711 -8.2712,-6.6111 -3.5982,-2.5484 -11.2202,-7.0059 -11.2202,-7.0059 l 15.4484,-30.858 c -13.3255,-5.9362 -2.8177,-3.6211 -19.8021,-8.5942 z"
-         id="path3013"
-         inkscape:connector-curvature="0"
-         sodipodi:nodetypes="ccsccacaccasccaccc" />
-      <path
-         sodipodi:nodetypes="aaczsccsssca"
-         inkscape:connector-curvature="0"
-         id="path3014"
-         d="m 3138.8926,-2124.2277 c 9.8375,-20.5358 27.6337,-38.9533 48.4705,-48.136 22.7321,-10.018 51.1582,-10.0879 74.4382,-3.5941 7.6944,6.5 10.3115,-17.1804 46.9059,-27.4208 36.5944,-10.2403 91.6429,-6.509 142.1505,22.7854 54.0494,31.3487 -23.7386,41.9213 -8.0812,94.4492 32.3844,113.578 -63.1345,101.5204 -63.1345,101.5204 0,0 -25.2539,-16.1625 -35.3554,-17.1726 -10.1015,-1.0102 -46.6429,-25.1251 -68.8662,-28.6607 -22.2234,-3.5355 -36.9003,18.6222 -107.991,-9.9282 -24.3178,-9.7661 -33.8401,-33.335 -33.8401,-33.335 0,0 -2.0102,-35.2405 5.3033,-50.5076 z"
-         style="fill:#0000ff;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
-      <path
-         sodipodi:nodetypes="cssscscsac"
-         inkscape:connector-curvature="0"
-         id="path3028"
-         d="m 3179.7636,-2035.4822 c 0,0 33.988,-7.9036 57.2215,-14.9746 23.2335,-7.0711 36.1637,-43.7474 71.519,-48.7981 35.3554,-5.0508 68.528,22.1624 80.6498,47.4162 12.1218,25.2539 50.6701,55.6193 50.6701,55.6193 0,0 1.0101,11.1117 -14.1422,18.1828 -15.1522,7.0711 -10.4989,7.6628 -10.4989,7.6628 0,0 -43.1103,-11.4139 -62.396,-21.4139 -19.2857,-10 -61.236,-23.044 -92.9377,-30.7963 -26.2653,-6.4228 -35.0334,2.5255 -80.0856,-12.8982 z"
-         style="fill:#ff6600;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
-      <path
-         sodipodi:nodetypes="cscccccscc"
-         inkscape:connector-curvature="0"
-         id="path3009"
-         d="m 3470.1209,-2002.8128 c 0,0 13.8937,99.0002 33.6829,157.8517 19.7893,58.8514 78.4464,180.4742 78.4464,180.4742 l 28.1031,-31.8521 -52.5625,-109.7203 60.5152,95.9016 26.0124,-35.7271 c 0,0 -70.663,-88.4878 -93.8954,-140.4597 -23.2324,-51.9718 -37.4945,-152.9082 -37.4945,-152.9082 z"
-         style="fill:#0000ff;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
-      <path
-         sodipodi:nodetypes="csscsccccccccccsc"
-         inkscape:connector-curvature="0"
-         id="path3016"
-         d="m 3359.2154,-2191.502 c 0,0 -40.7142,5 -57.1428,42.1428 -16.4286,37.1429 50.7143,115.7143 60.7143,132.1429 10,16.4286 24.2857,38.5714 24.2857,38.5714 0,0 47.6766,32.0335 82.9062,53.8444 32.6567,20.2179 65.2266,33.1109 65.2266,33.1109 0,0 1.1687,-0.4937 7.0265,-19.1963 -12.1218,-21.7183 -38.3216,-53.2543 -54.5036,-69.192 24.2792,14.1167 43.346,28.1254 60.1041,56.5482 0,0 4.9588,-21.3916 6.5659,-30.7894 -3.0357,-9.5763 -47.4771,-56.5685 -47.4771,-56.5685 20.5517,8.7613 33.7508,23.0255 50.0892,44.7732 0,0 3.3263,-21.5598 3.8366,-32.9472 -2.5253,-6.566 -33.7228,-35.0595 -33.7228,-35.0595 13.1654,6.6374 19.714,11.0661 34.4127,24.0476 0,0 -3.0357,-60.7142 -66.6072,-130.7142 -63.5714,-70 -135.7143,-50.7143 -135.7143,-50.7143 z"
-         style="fill:#000080;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
-      <g
-         transform="translate(2935.644,-2513.1499)"
-         id="g3138">
-        <path
-           style="fill:#ff0000;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
-           d="m 337.66259,394.0585 c 0,0 -28.86328,-30.61257 -60.78781,-25.80202 -31.92454,4.81054 -41.10831,33.67382 -39.79634,39.79633 1.31197,6.12252 13.557,11.80771 22.30345,15.74361 8.74644,3.9359 32.79917,-0.43732 45.48152,-6.55984 12.68235,-6.12251 32.79918,-23.17808 32.79918,-23.17808 z"
-           id="path3022"
-           inkscape:connector-curvature="0" />
-        <path
-           style="fill:#ffff00;fill-opacity:1;stroke:#000000;stroke-width:3;stroke-miterlimit:4"
-           d="m 286.65383,372.49887 c 12.50235,2.18615 20.86528,14.09354 18.67912,26.59589 -2.18615,12.50234 -14.09354,20.86527 -26.59589,18.67911 -12.50235,-2.18616 -20.86528,-14.09354 -18.67912,-26.59588 z"
-           id="path3024"
-           inkscape:connector-curvature="0"
-           sodipodi:nodetypes="csscc" />
-        <path
-           sodipodi:type="arc"
-           style="fill:#000000;fill-opacity:1;stroke:none"
-           id="path3026"
-           sodipodi:cx="229.64285"
-           sodipodi:cy="403.79074"
-           sodipodi:rx="8.2142859"
-           sodipodi:ry="8.2142859"
-           d="m 237.85714,403.79074 c 0,4.53663 -3.67766,8.21429 -8.21429,8.21429 -4.53662,0 -8.21428,-3.67766 -8.21428,-8.21429 0,-4.53662 3.67766,-8.21428 8.21428,-8.21428 4.53663,0 8.21429,3.67766 8.21429,8.21428 z"
-           transform="matrix(1.5694327,0,0,1.5694327,-85.566867,-233.30737)" />
-      </g>
-      <path
-         sodipodi:nodetypes="cssssssc"
-         style="fill:#ffff00;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
-         d="m 3110.2692,-2103.8575 c -32.3249,20.7081 -31.4522,56.0845 -27.4116,60.6301 4.0406,4.5457 8.0812,1.0102 15.6574,-1.5152 7.5761,-2.5254 26.2639,-10.1015 42.4264,-11.1117 16.1624,-1.0101 21.2132,3.0305 23.7386,-0.505 2.5254,-3.5356 4.0406,-15.1523 3.0304,-25.7589 -1.0101,-10.6066 -1.0101,-23.2335 -7.071,-25.2538 -6.061,-2.0203 -38.2484,-5.0718 -50.3702,3.5145 z"
-         id="path3296"
-         inkscape:connector-curvature="0" />
-      <path
-         inkscape:connector-curvature="0"
-         id="path3305"
-         d="m 3251.1404,-2178.2729 c 0,0 12.1428,21.0714 67.1428,32.5 55,11.4286 125.7143,-38.2143 125.7143,-38.2143 l 38.7857,38.6429 c 0,0 -113.0714,25.6428 -150.2143,21 -37.1428,-4.6429 -81.4285,-50.3572 -81.4285,-50.3572"
-         style="fill:#ffffff;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
-      <path
-         sodipodi:nodetypes="ccccc"
-         inkscape:connector-curvature="0"
-         id="path4099"
-         d="m 3453.1169,-2166.1191 -30.2186,14.3667 6.8378,5.5184 29.9689,-14.0733 z"
-         style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#008000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans" />
-      <path
-         sodipodi:nodetypes="cacscacsc"
-         inkscape:connector-curvature="0"
-         id="path3303"
-         d="m 3154.8351,-2149.0637 c 0,0 -6.1891,7.2925 -8.7928,11.2888 -2.8625,4.3936 -7.4472,13.8569 -7.4472,13.8569 0,0 49.8667,-24.0831 67.6065,-30.2972 18.2787,-6.4028 54.803,-21.7873 54.803,-21.7873 0,0 -3.8451,-1.8876 -5.9103,-2.3872 -3.1018,-0.7503 -9.5343,-0.868 -9.5343,-0.868 0,0 -25.7837,-0.1235 -42.8032,6.1343 -17.0196,6.2579 -47.9217,24.0597 -47.9217,24.0597 z"
-         style="fill:#ffffff;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
-      <path
-         style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#008000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans"
-         d="m 3433.0379,-2169.2212 24.4739,22.8165 -8.1844,3.1976 -24.3243,-22.4616 z"
-         id="path27241"
-         inkscape:connector-curvature="0"
-         sodipodi:nodetypes="ccccc" />
-    </g>
-    <g
-       id="g10229"
-       transform="translate(31.403024,6.4439689)">
-      <g
-         id="text4253"
-         style="font-size:140.50161743px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeSans;-inkscape-font-specification:FreeSans Bold"
-         transform="scale(1.066563,0.93759113)">
-        <path
-           id="path10219"
-           style="font-family:Acme;-inkscape-font-specification:Acme Bold"
-           d="m 2917.8887,-1930.8568 c -6.6505,0 -16.2514,-0.5152 -28.8029,-1.5455 l 1.6861,-34.1419 -1.6861,-62.8043 37.514,0 c 12.645,10e-5 22.527,3.7937 29.6458,11.3807 7.1187,7.5871 10.678,18.1247 10.6781,31.6128 -10e-5,17.0476 -4.4024,30.5826 -13.2071,40.605 -8.8049,9.9288 -20.7475,14.8932 -35.8279,14.8932 m 3.653,-83.3175 c -6.1821,10e-5 -10.5376,0.047 -13.0666,0.1405 l -1.4051,45.101 0.9836,22.7613 c 7.868,0.562 13.1602,0.843 15.8766,0.843 7.3997,0 13.2071,-2.9973 17.4222,-8.9921 4.3087,-5.9947 6.463,-14.7058 6.4631,-26.1333 -1e-4,-11.4274 -2.1076,-19.9043 -6.3226,-25.4308 -4.1214,-5.5263 -10.7718,-8.2895 -19.9512,-8.2896"
-           inkscape:connector-curvature="0" />
-        <path
-           id="path10221"
-           style="font-family:Acme;-inkscape-font-specification:Acme Bold"
-           d="m 3002.3389,-2030.051 -1.967,61.1182 1.686,36.5305 -19.6702,0 1.686,-34.1419 -1.686,-59.9942 19.9512,-3.5126"
-           inkscape:connector-curvature="0" />
-        <path
-           id="path10223"
-           style="font-family:Acme;-inkscape-font-specification:Acme Bold"
-           d="m 3074.2384,-2014.3148 -29.9268,0 -0.843,25.8523 26.8358,0 -1.5455,14.8932 -25.7118,0 -0.1405,4.6365 1.686,36.5305 -19.6703,0 1.6861,-34.1419 -1.6861,-62.8043 50.8616,0 -1.5455,15.0337"
-           inkscape:connector-curvature="0" />
-        <path
-           id="path10225"
-           style="font-family:Acme;-inkscape-font-specification:Acme Bold"
-           d="m 3136.2567,-2014.3148 -29.9268,0 -0.843,25.8523 26.8358,0 -1.5456,14.8932 -25.7117,0 -0.1405,4.6365 1.686,36.5305 -19.6703,0 1.6861,-34.1419 -1.6861,-62.8043 50.8616,0 -1.5455,15.0337"
-           inkscape:connector-curvature="0" />
-        <path
-           id="path10227"
-           style="font-family:Acme;-inkscape-font-specification:Acme Bold"
-           d="m 3177.4808,-1987.9005 1.686,0 10.3971,-19.1082 10.5376,-23.1828 17.1412,2.5291 -12.9261,23.4637 -18.2652,35.6874 1.5455,36.109 -19.6702,0 1.405,-33.7204 -17.7032,-35.9684 -13.4882,-24.5878 19.8107,-3.5126 9.1326,21.3563 10.3972,20.9347"
-           inkscape:connector-curvature="0" />
-      </g>
-      <g
-         id="text3010"
-         style="font-size:62.54807663px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Acme;-inkscape-font-specification:Acme"
-         transform="scale(1.1647576,0.85854772)">
-        <path
-           id="path10175"
-           d="m 2645.5222,-2090.4097 0.688,-7.1931 29.7104,0 -0.688,7.1931 -10.8834,-0.9382 -0.2502,20.6408 0.7506,16.2625 -8.7568,0 0.7506,-15.1992 -0.2502,-21.6416 -11.071,0.8757"
-           inkscape:connector-curvature="0" />
-        <path
-           id="path10177"
-           d="m 2697.8329,-2070.2692 c 0,-2.8772 -0.2502,-4.7954 -0.7505,-5.7545 -0.5005,-0.959 -1.668,-1.4385 -3.5027,-1.4386 -1.8348,1e-4 -3.9197,0.7298 -6.2548,2.1892 l 0,4.566 0.688,14.6988 -8.444,1.5637 0.7506,-15.1992 -0.7506,-31.6493 8.5691,-1.5637 -0.8131,16.2 0,7.3181 c 0.6254,-0.834 1.3343,-1.6471 2.1266,-2.4394 0.7923,-0.7922 2.0224,-1.6888 3.6903,-2.6896 1.668,-1.0007 3.4401,-1.5011 5.3166,-1.5011 1.9181,0 3.5235,0.7506 4.8162,2.2517 1.2926,1.4595 1.939,3.461 1.939,6.0046 l -0.3753,7.0054 0.688,15.0116 -8.4439,1.5637 0.7505,-16.1374"
-           inkscape:connector-curvature="0" />
-        <path
-           id="path10179"
-           d="m 2723.6897,-2060.8245 c 1.2093,0 2.4811,-0.3127 3.8155,-0.9382 1.3343,-0.6255 2.3768,-1.251 3.1274,-1.8764 l 1.1258,-0.9383 2.8772,3.5027 c -0.417,0.7089 -1.0633,1.522 -1.939,2.4394 -0.8757,0.9174 -1.7722,1.7096 -2.6895,2.3768 -0.8757,0.6255 -2.0641,1.2093 -3.5653,1.7514 -1.4594,0.5004 -2.9814,0.7505 -4.566,0.7505 -3.3776,0 -6.1297,-1.2718 -8.2563,-3.8154 -2.1267,-2.5853 -3.19,-5.9003 -3.19,-9.9451 0,-5.0873 1.4803,-9.549 4.4409,-13.3853 2.9606,-3.8363 6.4216,-5.7544 10.383,-5.7544 3.044,0 5.4,0.8548 7.068,2.5644 1.7096,1.7097 2.5644,4.1074 2.5644,7.1931 0,1.8347 -0.3127,3.9197 -0.9382,6.2548 l -1.251,1.3135 -15.637,1.4386 c 0.7089,4.7119 2.9189,7.0679 6.6301,7.0679 m 0,-19.3899 c -1.8347,0 -3.3776,0.7506 -4.6285,2.2517 -1.251,1.4595 -1.9807,3.336 -2.1892,5.6294 l 10.6332,-1.3135 c 0.125,-0.9591 0.1876,-1.7514 0.1876,-2.3769 0,-2.7938 -1.3344,-4.1907 -4.0031,-4.1907"
-           inkscape:connector-curvature="0" />
-        <path
-           id="path10181"
-           d="m 2760.1934,-2097.9155 -0.6881,20.3907 15.9498,-20.2656 6.3799,4.2533 -14.3861,16.7003 15.2618,18.0764 -8.0062,5.0038 -15.2617,-21.579 -0.1251,4.6285 0.7506,16.2625 -8.7568,0 0.7506,-15.1992 -0.7506,-26.708 8.8819,-1.5637"
-           inkscape:connector-curvature="0" />
-        <path
-           id="path10183"
-           d="m 2793.6018,-2054.4446 c -2.2517,0 -4.0864,-0.7297 -5.5042,-2.1892 -1.376,-1.5011 -2.0641,-3.461 -2.0641,-5.8795 l 0.3753,-7.193 -0.688,-15.0115 8.3814,-1.5638 -0.8131,19.0147 c 0,1.793 0.3127,3.1065 0.9382,3.9405 0.6672,0.834 1.7097,1.251 3.1274,1.251 1.4178,0 3.3776,-0.7089 5.8796,-2.1267 l 0,-3.7529 -0.6881,-16.4501 8.444,-1.5637 -0.7506,15.637 0.063,5.129 c 0,2.6687 0.688,5.3582 2.0641,8.0687 l -6.505,2.8772 c -1.3761,-2.7104 -2.2101,-4.8788 -2.502,-6.505 -3.7529,4.2115 -7.0053,6.3173 -9.7575,6.3173"
-           inkscape:connector-curvature="0" />
-        <path
-           id="path10185"
-           d="m 2836.1199,-2070.2692 c 0,-2.8772 -0.2502,-4.7954 -0.7506,-5.7545 -0.5004,-0.959 -1.668,-1.4385 -3.5027,-1.4386 -1.8347,1e-4 -3.9197,0.7298 -6.2548,2.1892 l 0,4.566 0.688,14.6988 -8.3814,1.5637 0.7506,-15.1992 c -0.1668,-5.5876 -0.6255,-10.5914 -1.3761,-15.0115 l 7.5683,-1.6263 c 0.2919,2.4186 0.5004,4.7746 0.6255,7.068 0.6255,-0.834 1.3344,-1.6471 2.1266,-2.4394 0.7923,-0.8339 2.0433,-1.7513 3.7529,-2.7521 1.7097,-1.0424 3.5027,-1.5637 5.3792,-1.5637 1.9181,0 3.5235,0.7506 4.8162,2.2517 1.2926,1.4595 1.9389,3.461 1.939,6.0046 l -0.3753,7.0054 0.688,15.0116 -8.444,1.5637 0.7506,-16.1374"
-           inkscape:connector-curvature="0" />
-        <path
-           id="path10187"
-           d="m 2848.5318,-2048.2523 c 0,-3.6695 3.4193,-6.5467 10.2579,-8.6317 -1.6263,-0.8339 -2.4394,-1.9181 -2.4394,-3.2525 0,-0.542 0.2085,-1.0841 0.6255,-1.6262 0.4169,-0.5421 1.0216,-1.105 1.8139,-1.6888 l 0,-0.5004 c -3.0024,0 -5.4,-0.834 -7.1931,-2.5019 -1.7513,-1.6679 -2.627,-3.8988 -2.627,-6.6927 0,-3.6277 1.4803,-6.776 4.4409,-9.4447 2.9606,-2.7104 6.4633,-4.0656 10.5081,-4.0656 1.0425,0 2.5645,0.2085 4.566,0.6254 2.0015,0.4171 3.9614,0.6256 5.8795,0.6255 l 4.8162,0 -0.6254,5.4417 -4.6912,-0.5629 c 0.417,1.2093 0.6255,2.5853 0.6255,4.1281 0,1.5429 -0.3753,3.0858 -1.1258,4.6286 -0.7089,1.5012 -1.5846,2.7104 -2.6271,3.6278 -1.0007,0.9174 -2.0224,1.7305 -3.0648,2.4394 -1.0008,0.6672 -1.8765,1.2718 -2.627,1.8139 -0.7089,0.5004 -1.0634,0.959 -1.0633,1.376 -1e-4,0.7923 0.8339,1.4803 2.5019,2.0641 4.42,1.7931 7.4015,3.3151 8.9443,4.566 1.5846,1.251 2.3768,2.7938 2.3769,4.6286 -10e-5,3.0857 -1.5638,5.6084 -4.6911,7.5683 -3.0858,2.0015 -6.7761,3.0023 -11.071,3.0023 -4.295,0 -7.6309,-0.688 -10.0077,-2.0641 -2.3352,-1.3761 -3.5027,-3.2108 -3.5027,-5.5042 m 18.8895,-26.2702 c 0,-2.0432 -0.4587,-3.5444 -1.3761,-4.5035 -0.9174,-0.959 -2.356,-1.4386 -4.3158,-1.4386 -3.7946,0 -5.6919,1.8765 -5.6919,5.6293 0,3.7112 2.0224,5.5668 6.0672,5.5668 3.5444,0 5.3166,-1.7513 5.3166,-5.254 m 2.2517,24.2061 c 0,-0.7506 -0.3961,-1.4595 -1.1884,-2.1266 -0.7923,-0.6255 -2.21,-1.3761 -4.2533,-2.2518 -3.044,0.834 -5.1498,1.6054 -6.3173,2.3143 -1.1259,0.7089 -1.6888,1.5846 -1.6888,2.627 0,1.0425 0.5629,1.8765 1.6888,2.5019 1.1675,0.6672 2.7729,1.0008 4.8162,1.0008 2.0432,0 3.7112,-0.3753 5.0038,-1.1258 1.2927,-0.7506 1.939,-1.7305 1.939,-2.9398"
-           inkscape:connector-curvature="0" />
-        <path
-           id="path10189"
-           d="m 2915.4651,-2090.9101 -13.3228,0 -0.3753,11.5088 11.9467,0 -0.688,6.6301 -11.4463,0 -0.063,2.0641 0.7506,16.2625 -8.7567,0 0.7506,-15.1992 -0.7506,-27.959 22.6424,0 -0.688,6.6927"
-           inkscape:connector-curvature="0" />
-        <path
-           id="path10191"
-           d="m 2927.2495,-2054.4446 c -2.2517,0 -4.0865,-0.7297 -5.5042,-2.1892 -1.3761,-1.5011 -2.0641,-3.461 -2.0641,-5.8795 l 0.3753,-7.193 -0.6881,-15.0115 8.3815,-1.5638 -0.8131,19.0147 c -10e-5,1.793 0.3127,3.1065 0.9382,3.9405 0.6671,0.834 1.7096,1.251 3.1274,1.251 1.4177,0 3.3776,-0.7089 5.8795,-2.1267 l 0,-3.7529 -0.688,-16.4501 8.444,-1.5637 -0.7506,15.637 0.063,5.129 c 0,2.6687 0.688,5.3582 2.0641,8.0687 l -6.505,2.8772 c -1.3761,-2.7104 -2.21,-4.8788 -2.5019,-6.505 -3.7529,4.2115 -7.0054,6.3173 -9.7575,6.3173"
-           inkscape:connector-curvature="0" />
-      </g>
-      <g
-         id="text3016"
-         style="font-size:55.5345993px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Acme;-inkscape-font-specification:Acme"
-         transform="scale(1.0640965,0.93976441)">
-        <path
-           id="path10194"
-           d="m 2910.1662,-1832.5692 -3.1655,0 0,0.3332 0.6664,14.439 -7.4416,0 0.6664,-13.4949 -0.6664,-24.8239 12.051,0 c 3.5542,0 6.3864,0.9626 8.4968,2.8878 2.1103,1.9252 3.1654,4.4798 3.1655,7.6637 -10e-5,2.4066 -0.6295,4.6094 -1.8882,6.6087 -1.2218,1.9622 -2.8878,3.5172 -4.9981,4.6649 l 9.663,13.6615 -6.7197,3.4431 -8.6634,-15.4386 c -0.2592,0.037 -0.6479,0.056 -1.1662,0.056 m 0.111,-17.9376 -2.7212,0 -0.3887,12.5508 3.7764,0 c 1.8511,0 3.2765,-0.6109 4.2761,-1.8327 1.0366,-1.2217 1.555,-2.9618 1.555,-5.2202 0,-3.6653 -2.1659,-5.4979 -6.4976,-5.4979"
-           inkscape:connector-curvature="0" />
-        <path
-           id="path10196"
-           d="m 2941.1059,-1823.4615 c 1.0736,0 2.2028,-0.2777 3.3876,-0.833 1.1847,-0.5553 2.1103,-1.1107 2.7767,-1.666 l 0.9997,-0.8331 2.5545,3.11 c -0.3702,0.6294 -0.9441,1.3513 -1.7215,2.1658 -0.7775,0.8145 -1.5735,1.518 -2.388,2.1103 -0.7775,0.5554 -1.8327,1.0737 -3.1655,1.555 -1.2958,0.4443 -2.6471,0.6664 -4.054,0.6664 -2.9989,0 -5.4424,-1.1292 -7.3306,-3.3876 -1.8882,-2.2954 -2.8322,-5.2387 -2.8322,-8.83 0,-4.5168 1.3143,-8.4783 3.9429,-11.8844 2.6286,-3.4061 5.7016,-5.1092 9.2188,-5.1092 2.7026,0 4.7944,0.759 6.2754,2.2769 1.5179,1.518 2.2769,3.6468 2.2769,6.3865 0,1.6291 -0.2777,3.4802 -0.833,5.5535 l -1.1107,1.1662 -13.8837,1.2773 c 0.6294,4.1836 2.5916,6.2754 5.8867,6.2754 m 0,-17.2157 c -1.629,0 -2.9989,0.6664 -4.1096,1.9992 -1.1107,1.2959 -1.7586,2.9619 -1.9437,4.9981 l 9.4409,-1.1662 c 0.1111,-0.8515 0.1666,-1.5549 0.1666,-2.1103 0,-2.4805 -1.1847,-3.7208 -3.5542,-3.7208"
-           inkscape:connector-curvature="0" />
-        <path
-           id="path10198"
-           d="m 2965.2808,-1824.7388 0.7219,0 1.8327,-7.6082 2.9989,-13.7171 6.8862,0.833 -4.165,13.7726 -3.4987,12.9951 -9.4964,0.833 -3.2766,-13.4949 -3.9985,-13.4949 7.5527,-1.3883 2.388,12.2176 2.0548,9.0521"
-           inkscape:connector-curvature="0" />
-        <path
-           id="path10200"
-           d="m 2988.8795,-1846.0641 -0.6664,13.8281 0.6109,13.0507 -7.4416,1.3883 0.6664,-13.4949 -0.6109,-13.3838 7.4416,-1.3884 m -7.8303,-8.108 c 0,-1.2588 0.4812,-2.3695 1.4439,-3.3321 0.9625,-0.9996 2.0732,-1.4994 3.332,-1.4994 1.2588,0 2.2214,0.3517 2.8878,1.0551 0.6664,0.6665 0.9996,1.6846 0.9997,3.0544 -10e-5,1.3699 -0.4814,2.5547 -1.4439,3.5542 -0.9256,0.9627 -2.0178,1.444 -3.2766,1.4439 -1.2218,10e-5 -2.1844,-0.4072 -2.8878,-1.2217 -0.7034,-0.8145 -1.0551,-1.8326 -1.0551,-3.0544"
-           inkscape:connector-curvature="0" />
-        <path
-           id="path10202"
-           d="m 3004.9924,-1823.4615 c 1.0736,0 2.2028,-0.2777 3.3876,-0.833 1.1847,-0.5553 2.1103,-1.1107 2.7767,-1.666 l 0.9996,-0.8331 2.5546,3.11 c -0.3702,0.6294 -0.9441,1.3513 -1.7215,2.1658 -0.7775,0.8145 -1.5735,1.518 -2.388,2.1103 -0.7775,0.5554 -1.8327,1.0737 -3.1655,1.555 -1.2958,0.4443 -2.6472,0.6664 -4.054,0.6664 -2.9989,0 -5.4424,-1.1292 -7.3306,-3.3876 -1.8882,-2.2954 -2.8323,-5.2387 -2.8323,-8.83 0,-4.5168 1.3144,-8.4783 3.943,-11.8844 2.6286,-3.4061 5.7015,-5.1092 9.2187,-5.1092 2.7027,0 4.7945,0.759 6.2754,2.2769 1.518,1.518 2.2769,3.6468 2.277,6.3865 -1e-4,1.6291 -0.2777,3.4802 -0.8331,5.5535 l -1.1106,1.1662 -13.8837,1.2773 c 0.6294,4.1836 2.5916,6.2754 5.8867,6.2754 m 0,-17.2157 c -1.6291,0 -2.9989,0.6664 -4.1096,1.9992 -1.1107,1.2959 -1.7586,2.9619 -1.9437,4.9981 l 9.4409,-1.1662 c 0.111,-0.8515 0.1666,-1.5549 0.1666,-2.1103 0,-2.4805 -1.1848,-3.7208 -3.5542,-3.7208"
-           inkscape:connector-curvature="0" />
-        <path
-           id="path10204"
-           d="m 3044.4948,-1824.7388 0.722,0 1.7771,-7.6082 2.8878,-13.7171 6.7752,0.833 -4.054,13.7726 -3.3876,12.9951 -9.1077,0.833 -3.0544,-13.4949 -0.2777,-0.8885 -3.7208,13.5504 -8.83,0.833 -3.221,-13.4949 -3.8319,-13.4949 7.275,-1.3883 2.277,12.2176 1.9992,9.0521 0.7219,0 4.776,-15.8273 -1.1662,-4.0541 7.164,-1.3883 2.3324,12.2176 1.9437,9.0521"
-           inkscape:connector-curvature="0" />
-        <path
-           id="path10206"
-           d="m 3081.9121,-1817.1861 c -4.1095,0 -7.5712,-1.7216 -10.3849,-5.1647 -2.8138,-3.4431 -4.2207,-7.6638 -4.2207,-12.6619 0,-6.1088 1.6846,-11.255 5.0537,-15.4386 3.4061,-4.1836 7.5712,-6.2754 12.4953,-6.2754 1.9251,0 3.7022,0.2037 5.3313,0.6109 1.666,0.3702 2.8507,0.759 3.5542,1.1662 l 0.9996,0.6109 -3.7763,7.7193 c -0.8516,-0.7404 -2.0548,-1.4254 -3.6098,-2.0548 -1.555,-0.6664 -2.9618,-0.9996 -4.2206,-0.9996 -2.5546,0 -4.6094,1.0552 -6.1644,3.1655 -1.5179,2.1103 -2.2769,5.1277 -2.2769,9.0521 0,3.8874 0.796,7.0899 2.388,9.6075 1.592,2.4805 3.7578,3.7208 6.4976,3.7208 1.7771,0 3.3876,-0.5553 4.8315,-1.666 1.4438,-1.1107 2.462,-2.5916 3.0544,-4.4428 l 4.6093,2.6656 c -1.2217,3.2581 -3.0914,5.8127 -5.6089,7.6638 -2.5176,1.8142 -5.3684,2.7212 -8.5524,2.7212"
-           inkscape:connector-curvature="0" />
-        <path
-           id="path10208"
-           d="m 3106.1131,-1817.797 c -1.9993,0 -3.6283,-0.6479 -4.8871,-1.9437 -1.2217,-1.3328 -1.8326,-3.0729 -1.8326,-5.2202 l 0.3332,-6.3865 -0.6109,-13.3283 7.4416,-1.3884 -0.7219,16.8825 c 0,1.592 0.2777,2.7583 0.833,3.4987 0.5924,0.7405 1.5179,1.1107 2.7767,1.1107 1.2588,0 2.9989,-0.6294 5.2203,-1.8882 l 0,-3.332 -0.6109,-14.6056 7.4972,-1.3884 -0.6664,13.8837 0.055,4.5538 c 0,2.3695 0.6109,4.7575 1.8326,7.164 l -5.7756,2.5545 c -1.2217,-2.4065 -1.9622,-4.3317 -2.2213,-5.7756 -3.3321,3.7394 -6.2199,5.609 -8.6634,5.609"
-           inkscape:connector-curvature="0" />
-        <path
-           id="path10210"
-           d="m 3137.3105,-1839.1778 c -1.629,0 -2.8878,0.5924 -3.7764,1.7771 -0.8885,1.1478 -1.3328,2.9249 -1.3328,5.3313 0,2.4066 0.4443,4.3503 1.3328,5.8312 0.9256,1.4439 2.2584,2.1658 3.9985,2.1658 0.8886,0 1.7586,-0.2776 2.6102,-0.833 0.8515,-0.5553 1.4994,-1.1107 1.9437,-1.666 l 0.6109,-0.8886 2.7767,2.8878 c -0.1111,0.1851 -0.2592,0.4443 -0.4443,0.7775 -0.1481,0.3332 -0.5554,0.9256 -1.2218,1.7771 -0.6294,0.8515 -1.3143,1.6105 -2.0547,2.2769 -0.7035,0.6294 -1.6291,1.2218 -2.7768,1.7771 -1.1477,0.5184 -2.3509,0.7775 -3.6097,0.7775 -2.7768,0 -5.0352,-1.1292 -6.7752,-3.3876 -1.7401,-2.2584 -2.6102,-5.2017 -2.6102,-8.83 0,-4.5168 1.3144,-8.4783 3.943,-11.8844 2.6286,-3.4061 5.7015,-5.1092 9.2187,-5.1092 1.0737,0 2.1474,0.1667 3.221,0.4998 1.0737,0.2962 2.1659,0.7775 3.2766,1.4439 l -3.8874,6.8308 c -1.3329,-1.0366 -2.8138,-1.555 -4.4428,-1.555"
-           inkscape:connector-curvature="0" />
-        <path
-           id="path10212"
-           d="m 3166.4532,-1817.2416 -10.4406,-15.3831 0,0.3887 0.6109,13.0507 -7.4971,1.3883 0.6664,-13.4949 -0.6664,-28.1005 7.6082,-1.3883 -0.722,26.8232 11.107,-12.1621 5.2202,3.5542 -9.7185,9.1077 10.1628,11.9955 -6.3309,4.2206"
-           inkscape:connector-curvature="0" />
-        <path
-           id="path10214"
-           d="m 3174.2514,-1829.4037 c 0,-4.7389 1.2773,-8.7559 3.8319,-12.051 2.5546,-3.295 5.5349,-4.9426 8.9411,-4.9426 3.4431,0 6.1088,1.1107 7.997,3.3321 1.9251,2.2214 2.8877,5.1647 2.8877,8.83 0,4.8871 -1.2402,8.9596 -3.7208,12.2176 -2.4435,3.221 -5.5349,4.8315 -9.2742,4.8315 -3.073,0 -5.6276,-1.1477 -7.6638,-3.4431 -1.9993,-2.3325 -2.9989,-5.2573 -2.9989,-8.7745 m 17.4379,-1.9437 c 0,-5.3683 -1.8327,-8.0525 -5.498,-8.0525 -3.8133,0 -5.72,2.5176 -5.72,7.5527 0,2.6286 0.5368,4.6834 1.6105,6.1643 1.0736,1.481 2.4805,2.2214 4.2206,2.2214 1.7771,0 3.1099,-0.6664 3.9985,-1.9992 0.9256,-1.3699 1.3884,-3.3321 1.3884,-5.8867"
-           inkscape:connector-curvature="0" />
-        <path
-           id="path10216"
-           d="m 3201.0425,-1829.4037 c 0,-4.7389 1.2773,-8.7559 3.8319,-12.051 2.5546,-3.295 5.5349,-4.9426 8.9411,-4.9426 3.4431,0 6.1088,1.1107 7.997,3.3321 1.9251,2.2214 2.8877,5.1647 2.8878,8.83 -10e-5,4.8871 -1.2403,8.9596 -3.7209,12.2176 -2.4435,3.221 -5.5349,4.8315 -9.2742,4.8315 -3.073,0 -5.6276,-1.1477 -7.6638,-3.4431 -1.9993,-2.3325 -2.9989,-5.2573 -2.9989,-8.7745 m 17.4379,-1.9437 c 0,-5.3683 -1.8327,-8.0525 -5.4979,-8.0525 -3.8134,0 -5.7201,2.5176 -5.7201,7.5527 0,2.6286 0.5368,4.6834 1.6105,6.1643 1.0737,1.481 2.4805,2.2214 4.2206,2.2214 1.7771,0 3.1099,-0.6664 3.9985,-1.9992 0.9256,-1.3699 1.3884,-3.3321 1.3884,-5.8867"
-           inkscape:connector-curvature="0" />
-      </g>
-    </g>
-  </g>
-</svg>
diff --git a/contrib/themes/diffy/static/diffymute.svg b/contrib/themes/diffy/static/diffymute.svg
deleted file mode 100644
index 6833ba6..0000000
--- a/contrib/themes/diffy/static/diffymute.svg
+++ /dev/null
@@ -1,198 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
-   xmlns:dc="http://purl.org/dc/elements/1.1/"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   width="600.06732"
-   height="558.20709"
-   id="svg2"
-   version="1.1"
-   inkscape:version="0.48.3.1 r9886"
-   sodipodi:docname="diffy.svg"
-   inkscape:export-filename="/home/sarah/art/diffy.png"
-   inkscape:export-xdpi="500"
-   inkscape:export-ydpi="500">
-  <defs
-     id="defs4" />
-  <sodipodi:namedview
-     id="base"
-     pagecolor="#ffffff"
-     bordercolor="#666666"
-     borderopacity="1.0"
-     inkscape:pageopacity="0.0"
-     inkscape:pageshadow="2"
-     inkscape:zoom="0.66618777"
-     inkscape:cx="300.03365"
-     inkscape:cy="258.35521"
-     inkscape:document-units="px"
-     inkscape:current-layer="g10119"
-     showgrid="false"
-     inkscape:window-width="1366"
-     inkscape:window-height="744"
-     inkscape:window-x="0"
-     inkscape:window-y="24"
-     inkscape:window-maximized="1"
-     fit-margin-top="0"
-     fit-margin-left="0"
-     fit-margin-right="0"
-     fit-margin-bottom="0" />
-  <metadata
-     id="metadata7">
-    <rdf:RDF>
-      <cc:Work
-         rdf:about="">
-        <dc:format>image/svg+xml</dc:format>
-        <dc:type
-           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
-        <dc:title></dc:title>
-      </cc:Work>
-    </rdf:RDF>
-  </metadata>
-  <g
-     inkscape:label="Layer 1"
-     inkscape:groupmode="layer"
-     id="layer1"
-     transform="translate(-3069.8852,2251.5084)">
-    <text
-       xml:space="preserve"
-       style="font-size:51.65934372px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:end;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:end;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeSans;-inkscape-font-specification:FreeSans Bold"
-       x="475.83173"
-       y="900.47076"
-       id="text4257"
-       sodipodi:linespacing="125%"
-       transform="scale(1.1411753,0.87628955)"><tspan
-         sodipodi:role="line"
-         x="475.83173"
-         y="900.47076"
-         style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:end;text-anchor:end;font-family:Acme;-inkscape-font-specification:Acme Bold"
-         id="tspan3008" /></text>
-    <flowRoot
-       xml:space="preserve"
-       id="flowRoot4263"
-       style="font-size:12px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><flowRegion
-         id="flowRegion4265"><rect
-           id="rect4267"
-           width="313.14728"
-           height="343.45187"
-           x="-335.37064"
-           y="205.85435" /></flowRegion><flowPara
-         id="flowPara4269" /></flowRoot>    <g
-       id="g10119"
-       transform="matrix(0.99201906,-0.12608805,0.12608805,0.99201906,273.85438,408.58042)">
-      <path
-         sodipodi:nodetypes="csccscac"
-         inkscape:connector-curvature="0"
-         id="path4130"
-         d="m 3378.9965,-2206.2972 c 0,0 13.4039,-8.8398 27.0022,-17.1718 13.5984,-8.3319 22.2322,-21.276 22.2322,-21.276 l 34.2799,28.1467 c 0,0 -16.0641,15.553 -24.7742,21.6893 -3.709,2.6129 -10.9463,9.1577 -10.9463,9.1577 0,0 -14.3617,-10.0769 -22.3623,-13.5225 -8.0773,-3.4786 -25.4315,-7.0234 -25.4315,-7.0234 z"
-         style="fill:#ffffff;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
-      <path
-         sodipodi:nodetypes="cac"
-         inkscape:connector-curvature="0"
-         d="m 3414.7387,-2204.9828 c 0,0 8.2233,-5.8571 11.9326,-9.2622 3.8224,-3.5089 10.5596,-11.437 10.5596,-11.437"
-         style="fill:#ffffff;stroke:#ff0000;stroke-width:8.97858429;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
-         id="path4185" />
-      <path
-         sodipodi:nodetypes="cascacsc"
-         inkscape:connector-curvature="0"
-         id="path3307"
-         d="m 3247.6516,-2171.4037 c 0,0 4.3348,-19.2835 10.7071,-26.2701 7.4284,-8.1446 19.072,-14.1542 29.3495,-15.24 27.8177,-2.9386 63.2371,6.1908 63.2371,6.1908 0,0 -30.8042,0.7262 -45.6022,4.5134 -5.1922,1.3289 -14.7941,6.2977 -14.7941,6.2977 0,0 -16.1166,0.8374 -29.8971,12.0927 -13.7804,11.2552 -3.0344,9.7831 -3.0344,9.7831"
-         style="fill:#ffffff;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
-      <path
-         sodipodi:nodetypes="ccsccscscccccscc"
-         inkscape:connector-curvature="0"
-         id="path3011"
-         d="m 3332.9985,-2012.4352 -20.0742,24.9886 c 0,0 -16.3479,-5.082 -40.3255,4.9725 -23.9776,10.0546 -39.2193,26.4042 -39.2193,26.4042 l 7.8897,-0.7975 c 0,0 20.4818,-14.4371 31.9614,-17.1294 11.4796,-2.6923 23.6197,-2.8388 23.6197,-2.8388 0,0 -25.492,10.0264 -30.7497,12.3759 -6.2584,2.7968 -15.0874,9.199 -15.0874,9.199 l 10.4761,-0.2965 c 17.6867,-7.2528 41.3221,-16.0187 59.9685,-15.047 15.5919,1.1274 33.9405,16.7543 33.9405,16.7543 l -3.8165,-9.9895 c 0,0 -10.8307,-8.1932 -16.3866,-13.7491 -5.5558,-5.5558 17.5848,-25.8193 17.5848,-25.8193 -14.3098,-2.8351 -2.1172,-7.9424 -19.7815,-9.0274 z"
-         style="fill:#ffff00;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
-      <path
-         style="fill:#ffff00;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
-         d="m 3397.9929,-1984.0461 -15.5774,33.0846 c 0,0 -14.4499,-2.2165 -39.5097,4.7139 -25.0597,6.9303 -44.9575,20.0476 -44.9575,20.0476 l 10.5706,1.822 c 0,0 19.7276,-11.1182 30.516,-14.1955 7.747,-2.2097 23.9934,-2.9008 23.9934,-2.9008 0,0 -20.2687,6.6869 -30.057,10.9033 -5.4067,2.329 -15.7817,7.9273 -15.7817,7.9273 l 10.8391,1.5269 c 0,0 31.056,-11.8625 47.4047,-13.3504 11.1103,-1.0112 11.8331,-2.962 33.324,3.1079 10.0509,2.8388 16.3928,6.252 16.3928,6.252 l -3.3122,-5.8696 c 0,0 -5.3909,-4.5711 -8.2712,-6.6111 -3.5982,-2.5484 -11.2202,-7.0059 -11.2202,-7.0059 l 15.4484,-30.858 c -13.3255,-5.9362 -2.8177,-3.6211 -19.8021,-8.5942 z"
-         id="path3013"
-         inkscape:connector-curvature="0"
-         sodipodi:nodetypes="ccsccacaccasccaccc" />
-      <path
-         sodipodi:nodetypes="aaczsccsssca"
-         inkscape:connector-curvature="0"
-         id="path3014"
-         d="m 3138.8926,-2124.2277 c 9.8375,-20.5358 27.6337,-38.9533 48.4705,-48.136 22.7321,-10.018 51.1582,-10.0879 74.4382,-3.5941 7.6944,6.5 10.3115,-17.1804 46.9059,-27.4208 36.5944,-10.2403 91.6429,-6.509 142.1505,22.7854 54.0494,31.3487 -23.7386,41.9213 -8.0812,94.4492 32.3844,113.578 -63.1345,101.5204 -63.1345,101.5204 0,0 -25.2539,-16.1625 -35.3554,-17.1726 -10.1015,-1.0102 -46.6429,-25.1251 -68.8662,-28.6607 -22.2234,-3.5355 -36.9003,18.6222 -107.991,-9.9282 -24.3178,-9.7661 -33.8401,-33.335 -33.8401,-33.335 0,0 -2.0102,-35.2405 5.3033,-50.5076 z"
-         style="fill:#0000ff;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
-      <path
-         sodipodi:nodetypes="cssscscsac"
-         inkscape:connector-curvature="0"
-         id="path3028"
-         d="m 3179.7636,-2035.4822 c 0,0 33.988,-7.9036 57.2215,-14.9746 23.2335,-7.0711 36.1637,-43.7474 71.519,-48.7981 35.3554,-5.0508 68.528,22.1624 80.6498,47.4162 12.1218,25.2539 50.6701,55.6193 50.6701,55.6193 0,0 1.0101,11.1117 -14.1422,18.1828 -15.1522,7.0711 -10.4989,7.6628 -10.4989,7.6628 0,0 -43.1103,-11.4139 -62.396,-21.4139 -19.2857,-10 -61.236,-23.044 -92.9377,-30.7963 -26.2653,-6.4228 -35.0334,2.5255 -80.0856,-12.8982 z"
-         style="fill:#ff6600;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
-      <path
-         sodipodi:nodetypes="cscccccscc"
-         inkscape:connector-curvature="0"
-         id="path3009"
-         d="m 3470.1209,-2002.8128 c 0,0 13.8937,99.0002 33.6829,157.8517 19.7893,58.8514 78.4464,180.4742 78.4464,180.4742 l 28.1031,-31.8521 -52.5625,-109.7203 60.5152,95.9016 26.0124,-35.7271 c 0,0 -70.663,-88.4878 -93.8954,-140.4597 -23.2324,-51.9718 -37.4945,-152.9082 -37.4945,-152.9082 z"
-         style="fill:#0000ff;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
-      <path
-         sodipodi:nodetypes="csscsccccccccccsc"
-         inkscape:connector-curvature="0"
-         id="path3016"
-         d="m 3359.2154,-2191.502 c 0,0 -40.7142,5 -57.1428,42.1428 -16.4286,37.1429 50.7143,115.7143 60.7143,132.1429 10,16.4286 24.2857,38.5714 24.2857,38.5714 0,0 47.6766,32.0335 82.9062,53.8444 32.6567,20.2179 65.2266,33.1109 65.2266,33.1109 0,0 1.1687,-0.4937 7.0265,-19.1963 -12.1218,-21.7183 -38.3216,-53.2543 -54.5036,-69.192 24.2792,14.1167 43.346,28.1254 60.1041,56.5482 0,0 4.9588,-21.3916 6.5659,-30.7894 -3.0357,-9.5763 -47.4771,-56.5685 -47.4771,-56.5685 20.5517,8.7613 33.7508,23.0255 50.0892,44.7732 0,0 3.3263,-21.5598 3.8366,-32.9472 -2.5253,-6.566 -33.7228,-35.0595 -33.7228,-35.0595 13.1654,6.6374 19.714,11.0661 34.4127,24.0476 0,0 -3.0357,-60.7142 -66.6072,-130.7142 -63.5714,-70 -135.7143,-50.7143 -135.7143,-50.7143 z"
-         style="fill:#000080;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
-      <g
-         transform="translate(2935.644,-2513.1499)"
-         id="g3138">
-        <path
-           style="fill:#ff0000;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
-           d="m 337.66259,394.0585 c 0,0 -28.86328,-30.61257 -60.78781,-25.80202 -31.92454,4.81054 -41.10831,33.67382 -39.79634,39.79633 1.31197,6.12252 13.557,11.80771 22.30345,15.74361 8.74644,3.9359 32.79917,-0.43732 45.48152,-6.55984 12.68235,-6.12251 32.79918,-23.17808 32.79918,-23.17808 z"
-           id="path3022"
-           inkscape:connector-curvature="0" />
-        <path
-           style="fill:#ffff00;fill-opacity:1;stroke:#000000;stroke-width:3;stroke-miterlimit:4"
-           d="m 286.65383,372.49887 c 12.50235,2.18615 20.86528,14.09354 18.67912,26.59589 -2.18615,12.50234 -14.09354,20.86527 -26.59589,18.67911 -12.50235,-2.18616 -20.86528,-14.09354 -18.67912,-26.59588 z"
-           id="path3024"
-           inkscape:connector-curvature="0"
-           sodipodi:nodetypes="csscc" />
-        <path
-           sodipodi:type="arc"
-           style="fill:#000000;fill-opacity:1;stroke:none"
-           id="path3026"
-           sodipodi:cx="229.64285"
-           sodipodi:cy="403.79074"
-           sodipodi:rx="8.2142859"
-           sodipodi:ry="8.2142859"
-           d="m 237.85714,403.79074 c 0,4.53663 -3.67766,8.21429 -8.21429,8.21429 -4.53662,0 -8.21428,-3.67766 -8.21428,-8.21429 0,-4.53662 3.67766,-8.21428 8.21428,-8.21428 4.53663,0 8.21429,3.67766 8.21429,8.21428 z"
-           transform="matrix(1.5694327,0,0,1.5694327,-85.566867,-233.30737)" />
-      </g>
-      <path
-         sodipodi:nodetypes="cssssssc"
-         style="fill:#ffff00;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
-         d="m 3110.2692,-2103.8575 c -32.3249,20.7081 -31.4522,56.0845 -27.4116,60.6301 4.0406,4.5457 8.0812,1.0102 15.6574,-1.5152 7.5761,-2.5254 26.2639,-10.1015 42.4264,-11.1117 16.1624,-1.0101 21.2132,3.0305 23.7386,-0.505 2.5254,-3.5356 4.0406,-15.1523 3.0304,-25.7589 -1.0101,-10.6066 -1.0101,-23.2335 -7.071,-25.2538 -6.061,-2.0203 -38.2484,-5.0718 -50.3702,3.5145 z"
-         id="path3296"
-         inkscape:connector-curvature="0" />
-      <path
-         inkscape:connector-curvature="0"
-         id="path3305"
-         d="m 3251.1404,-2178.2729 c 0,0 12.1428,21.0714 67.1428,32.5 55,11.4286 125.7143,-38.2143 125.7143,-38.2143 l 38.7857,38.6429 c 0,0 -113.0714,25.6428 -150.2143,21 -37.1428,-4.6429 -81.4285,-50.3572 -81.4285,-50.3572"
-         style="fill:#ffffff;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
-      <path
-         sodipodi:nodetypes="ccccc"
-         inkscape:connector-curvature="0"
-         id="path4099"
-         d="m 3453.1169,-2166.1191 -30.2186,14.3667 6.8378,5.5184 29.9689,-14.0733 z"
-         style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#008000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans" />
-      <path
-         sodipodi:nodetypes="cacscacsc"
-         inkscape:connector-curvature="0"
-         id="path3303"
-         d="m 3154.8351,-2149.0637 c 0,0 -6.1891,7.2925 -8.7928,11.2888 -2.8625,4.3936 -7.4472,13.8569 -7.4472,13.8569 0,0 49.8667,-24.0831 67.6065,-30.2972 18.2787,-6.4028 54.803,-21.7873 54.803,-21.7873 0,0 -3.8451,-1.8876 -5.9103,-2.3872 -3.1018,-0.7503 -9.5343,-0.868 -9.5343,-0.868 0,0 -25.7837,-0.1235 -42.8032,6.1343 -17.0196,6.2579 -47.9217,24.0597 -47.9217,24.0597 z"
-         style="fill:#ffffff;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
-      <path
-         style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#008000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans"
-         d="m 3433.0379,-2169.2212 24.4739,22.8165 -8.1844,3.1976 -24.3243,-22.4616 z"
-         id="path27241"
-         inkscape:connector-curvature="0"
-         sodipodi:nodetypes="ccccc" />
-    </g>
-  </g>
-</svg>
diff --git a/contrib/themes/diffy/static/logo.png b/contrib/themes/diffy/static/logo.png
deleted file mode 100644
index 989c303..0000000
--- a/contrib/themes/diffy/static/logo.png
+++ /dev/null
Binary files differ
diff --git a/contrib/themes/spotify/etc/GerritSite.css b/contrib/themes/spotify/etc/GerritSite.css
deleted file mode 100644
index 489452a..0000000
--- a/contrib/themes/spotify/etc/GerritSite.css
+++ /dev/null
@@ -1,113 +0,0 @@
-#gerrit_topmenu {
-	font-size: 9pt !important;
-	padding-top: 5px !important;
-	padding-left: 15px !important;
-	padding-right: 15px !important;
-	background: url(static/background-spotigreen.jpg) !important;
-	float: left !important;
-	margin-left: 250px !important;
-	margin-right: 150px !important;
-	border-radius: 0 0 10px 10px !important;
-	padding-bottom: 10px !important;
-	border-bottom: 1px solid #abc506 !important;
-	border-right: 1px solid #abc506 !important;
-	border-left: 1px solid #abc506 !important;
-}
-
-body, .gwt-DialogBox .dialogMiddleCenter {
-	background: #FFF url(static/background-gradient.png) no-repeat !important;
-}
-
-#logo-spotify {
-	background: url(static/logo.png) no-repeat;
-	height: 98px;
-	width: 228px;
-	margin-left: 2px;
-}
-
-#gerrit_header {
-	background: #FCFEEF;
-	padding-bottom: 10px;
-	border-bottom: dashed 1px rgba(0, 0, 0, 0.05);
-}
-
-#gerrit_topmenu .gwt-TextBox {
-	margin-top: 15px;
-	width: 240px;
-}
-
-#gerrit_topmenu > table {
-    float: left;
-}
-
-#gerrit_topmenu > table > tbody > tr > td:nth-child(3) a {
-	margin-left: 5px;
-	background-color: #FFC;
-	border-radius: 3px;
-	padding: 5px 5px;
-	border-right: none !important;
-}
-
-.gwt-TabPanelBottom {
-	background: #FFC !important;
-	border-radius: 0 0 3px 3px !important;
-	padding-bottom: 6px !important;
-	padding-top: 6px !important;
-}
-
-.gwt-TabBar .gwt-TabBarItem, .gwt-TabBar .gwt-TabBarRest, .gwt-TabBar .gwt-TabPanelBottom {
-	background: transparent !important;
-}
-
-.gwt-TabBarItem-wrapper {
-	background: rgba(255, 255, 255, 0.2) !important;
-}
-
-#gerrit_topmenu .gwt-TabBar .gwt-TabBarItem-selected {
-	background: #FFC !important;
-	border-radius: 3px 3px 0 0 !important;
-}
-
-#gerrit_topmenu .gwt-TabBar .gwt-TabBarItem-selected:hover {
-	background: #FFC !important;
-}
-
-#gerrit_topmenu .gwt-TabBar .gwt-TabBarItem-selected:focus {
-	outline: none !important;
-}
-
-#gerrit_topmenu > table > tbody > tr > td > table {
-	border: none !important;
-}
-
-.gwt-TabBar {
-	border-bottom: 1px solid #E2E2AD !important;
-}
-
-.gwt-TabBarItem {
-	border-right: 1px solid rgba(79, 58, 0, 0.3) !important;
-	background: rgba(255, 255, 255, 0.2) !important;
-}
-
-.gwt-TabBarItem:hover {
-	background: rgba(255, 255, 255, 0.1) !important;
-}
-
-a, a:visited {
-	text-decoration: none !important;
-	color: #3F4D00 !important;
-}
-
-a:hover {
-	color: #5d7200 !important;
-	text-decoration: underline !important;
-}
-
-.gwt-Label {
-height: 30px !important;
-line-height: 30px !important;
-}
-
-#gerrit_btmmenu > div {
-	color: rgba(0, 0, 0, 0.3) !important;
-}
\ No newline at end of file
diff --git a/contrib/themes/spotify/etc/GerritSiteHeader.html b/contrib/themes/spotify/etc/GerritSiteHeader.html
deleted file mode 100644
index 8af84be..0000000
--- a/contrib/themes/spotify/etc/GerritSiteHeader.html
+++ /dev/null
@@ -1 +0,0 @@
-<div id="logo-spotify"></div>
\ No newline at end of file
diff --git a/contrib/themes/spotify/static/background-gradient.png b/contrib/themes/spotify/static/background-gradient.png
deleted file mode 100644
index 3b9422e..0000000
--- a/contrib/themes/spotify/static/background-gradient.png
+++ /dev/null
Binary files differ
diff --git a/contrib/themes/spotify/static/background-spotigreen.jpg b/contrib/themes/spotify/static/background-spotigreen.jpg
deleted file mode 100644
index fcc7983..0000000
--- a/contrib/themes/spotify/static/background-spotigreen.jpg
+++ /dev/null
Binary files differ
diff --git a/contrib/themes/spotify/static/logo.png b/contrib/themes/spotify/static/logo.png
deleted file mode 100644
index c231031..0000000
--- a/contrib/themes/spotify/static/logo.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtdebug/BUILD b/gerrit-gwtdebug/BUILD
deleted file mode 100644
index ecf525e..0000000
--- a/gerrit-gwtdebug/BUILD
+++ /dev/null
@@ -1,18 +0,0 @@
-load("@rules_java//java:defs.bzl", "java_library")
-
-java_library(
-    name = "gwtdebug",
-    srcs = glob(["src/main/java/**/*.java"]),
-    visibility = ["//visibility:public"],
-    deps = [
-        "//java/com/google/gerrit/pgm",
-        "//java/com/google/gerrit/pgm/util",
-        "//java/com/google/gerrit/util/cli",
-        "//lib/flogger:api",
-        "//lib/gwt:dev",
-        "//lib/jetty:server",
-        "//lib/jetty:servlet",
-        "//lib/jetty:servlets",
-        "//lib/log:log4j",
-    ],
-)
diff --git a/gerrit-gwtdebug/src/main/java/com/google/gerrit/gwtdebug/GerritGwtDebugLauncher.java b/gerrit-gwtdebug/src/main/java/com/google/gerrit/gwtdebug/GerritGwtDebugLauncher.java
deleted file mode 100644
index cf84919..0000000
--- a/gerrit-gwtdebug/src/main/java/com/google/gerrit/gwtdebug/GerritGwtDebugLauncher.java
+++ /dev/null
@@ -1,77 +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.gwtdebug;
-
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.pgm.Daemon;
-import com.google.gwt.dev.codeserver.CodeServer;
-import com.google.gwt.dev.codeserver.Options;
-import java.util.ArrayList;
-import java.util.List;
-
-class GerritGwtDebugLauncher {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
-  public static void main(String[] argv) throws Exception {
-    GerritGwtDebugLauncher launcher = new GerritGwtDebugLauncher();
-    launcher.mainImpl(argv);
-  }
-
-  private int mainImpl(String[] argv) {
-    List<String> sdmLauncherOptions = new ArrayList<>();
-    List<String> daemonLauncherOptions = new ArrayList<>();
-
-    // Separator between Daemon and Codeserver parameters is "--"
-    boolean daemonArgumentSeparator = false;
-    int i = 0;
-    for (; i < argv.length; i++) {
-      if (!argv[i].equals("--")) {
-        sdmLauncherOptions.add(argv[i]);
-      } else {
-        daemonArgumentSeparator = true;
-        break;
-      }
-    }
-    if (daemonArgumentSeparator) {
-      ++i;
-      for (; i < argv.length; i++) {
-        daemonLauncherOptions.add(argv[i]);
-      }
-    }
-
-    Options options = new Options();
-    if (!options.parseArgs(sdmLauncherOptions.toArray(new String[sdmLauncherOptions.size()]))) {
-      logger.atSevere().log("Failed to parse codeserver arguments");
-      return 1;
-    }
-
-    CodeServer.main(options);
-
-    try {
-      int r =
-          new Daemon()
-              .main(daemonLauncherOptions.toArray(new String[daemonLauncherOptions.size()]));
-      if (r != 0) {
-        logger.atSevere().log("Daemon exited with return code: %d", r);
-        return 1;
-      }
-    } catch (Exception e) {
-      logger.atSevere().withCause(e).log("Cannot start daemon");
-      return 1;
-    }
-
-    return 0;
-  }
-}
diff --git a/gerrit-gwtui-common/BUILD b/gerrit-gwtui-common/BUILD
deleted file mode 100644
index e9ed363..0000000
--- a/gerrit-gwtui-common/BUILD
+++ /dev/null
@@ -1,63 +0,0 @@
-load("@rules_java//java:defs.bzl", "java_library")
-load("//tools/bzl:gwt.bzl", "gwt_module")
-load("//tools/bzl:java.bzl", "java_library2")
-load("//tools/bzl:junit.bzl", "junit_tests")
-
-EXPORTED_DEPS = [
-    "//java/com/google/gerrit/common:client",
-    "//java/com/google/gwtexpui/clippy",
-    "//java/com/google/gwtexpui/globalkey",
-    "//java/com/google/gwtexpui/progress",
-    "//java/com/google/gwtexpui/safehtml",
-    "//java/com/google/gwtexpui/user:agent",
-]
-
-DEPS = ["//lib/gwt:user-neverlink"]
-
-SRC = "src/main/java/com/google/gerrit/"
-
-gwt_module(
-    name = "client",
-    srcs = glob(["src/main/**/*.java"]),
-    exported_deps = EXPORTED_DEPS,
-    gwt_xml = SRC + "GerritGwtUICommon.gwt.xml",
-    resources = glob(
-        ["src/main/**/*"],
-        exclude = [SRC + "client/**/*.java"] + [
-            SRC + "GerritGwtUICommon.gwt.xml",
-        ],
-    ),
-    visibility = ["//visibility:public"],
-    deps = DEPS,
-)
-
-java_library2(
-    name = "client-lib",
-    srcs = glob(["src/main/**/*.java"]),
-    exported_deps = EXPORTED_DEPS,
-    resources = glob(["src/main/**/*"]),
-    visibility = ["//visibility:public"],
-    deps = DEPS,
-)
-
-java_library(
-    name = "diffy_logo",
-    data = [
-        "//lib:LICENSE-CC-BY3.0-unported",
-        "//lib:LICENSE-diffy",
-    ],
-    resources = glob(["src/main/resources/com/google/gerrit/client/diffy*.png"]),
-    visibility = ["//visibility:public"],
-)
-
-junit_tests(
-    name = "client_tests",
-    srcs = glob(["src/test/java/**/*.java"]),
-    visibility = ["//visibility:public"],
-    deps = [
-        ":client",
-        "//lib:junit",
-        "//lib/gwt:dev",
-        "//lib/jgit/org.eclipse.jgit:jgit",
-    ],
-)
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/GerritGwtUICommon.gwt.xml b/gerrit-gwtui-common/src/main/java/com/google/gerrit/GerritGwtUICommon.gwt.xml
deleted file mode 100644
index dc478fc..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/GerritGwtUICommon.gwt.xml
+++ /dev/null
@@ -1,43 +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.
--->
-<module>
-  <inherits name='org.eclipse.jgit.JGit'/>
-  <inherits name='com.google.gerrit.common.Common'/>
-  <inherits name='com.google.gerrit.extensions.Extensions'/>
-  <inherits name='com.google.gerrit.prettify.PrettyFormatter'/>
-  <inherits name='com.google.gwtexpui.clippy.Clippy'/>
-  <inherits name='com.google.gwtexpui.globalkey.GlobalKey'/>
-  <inherits name='com.google.gwtexpui.progress.Progress'/>
-  <inherits name='com.google.gwtexpui.safehtml.SafeHtml'/>
-  <source path='client'>
-    <include name='AccountFormatter.java'/>
-    <include name='CommonConstants.java'/>
-    <include name='CommonMessages.java'/>
-    <include name='DateFormatter.java'/>
-    <include name='GerritUiExtensionPoint.java'/>
-    <include name='RelativeDateFormatter.java'/>
-    <include name='Resources.java'/>
-    <include name='CommonConstants.properties'/>
-    <include name='CommonMessages.properties'/>
-    <include name='info/*.java'/>
-    <include name='rpc/NativeMap.java'/>
-    <include name='rpc/Natives.java'/>
-    <include name='rpc/NativeString.java'/>
-    <include name='rpc/TransformCallback.java'/>
-    <include name='ui/HighlightSuggestion.java'/>
-    <include name='ui/RemoteSuggestOracle.java'/>
-  </source>
-</module>
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/AccountFormatter.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/AccountFormatter.java
deleted file mode 100644
index 3058971..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/AccountFormatter.java
+++ /dev/null
@@ -1,70 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client;
-
-import com.google.gerrit.client.info.AccountInfo;
-
-public class AccountFormatter {
-  private final String anonymousCowardName;
-
-  public AccountFormatter(String anonymousCowardName) {
-    this.anonymousCowardName = anonymousCowardName;
-  }
-
-  /**
-   * Formats an account as a name and an email address.
-   *
-   * <p>Example output:
-   *
-   * <ul>
-   *   <li>{@code A U. Thor &lt;author@example.com&gt;}: full populated
-   *   <li>{@code A U. Thor (12)}: missing email address
-   *   <li>{@code Anonymous Coward &lt;author@example.com&gt;}: missing name
-   *   <li>{@code Anonymous Coward (12)}: missing name and email address
-   * </ul>
-   */
-  public String nameEmail(AccountInfo info) {
-    String name = info.name();
-    if (name == null || name.trim().isEmpty()) {
-      name = anonymousCowardName;
-    }
-
-    StringBuilder b = new StringBuilder().append(name);
-    if (info.email() != null) {
-      b.append(" <").append(info.email()).append(">");
-    } else if (info._accountId() > 0) {
-      b.append(" (").append(info._accountId()).append(")");
-    }
-    return b.toString();
-  }
-
-  /**
-   * Formats an account name.
-   *
-   * <p>If the account has a full name, it returns only the full name. Otherwise it returns a longer
-   * form that includes the email address.
-   */
-  public String name(AccountInfo ai) {
-    if (ai.name() != null && !ai.name().trim().isEmpty()) {
-      return ai.name();
-    }
-    String email = ai.email();
-    if (email != null) {
-      int at = email.indexOf('@');
-      return 0 < at ? email.substring(0, at) : email;
-    }
-    return nameEmail(ai);
-  }
-}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonConstants.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonConstants.java
deleted file mode 100644
index e769730..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonConstants.java
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client;
-
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.i18n.client.Constants;
-
-public interface CommonConstants extends Constants {
-  CommonConstants C = GWT.create(CommonConstants.class);
-
-  String inTheFuture();
-
-  String month();
-
-  String months();
-
-  String year();
-
-  String years();
-
-  String oneSecondAgo();
-
-  String oneMinuteAgo();
-
-  String oneHourAgo();
-
-  String oneDayAgo();
-
-  String oneWeekAgo();
-
-  String oneMonthAgo();
-
-  String oneYearAgo();
-}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonConstants.properties b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonConstants.properties
deleted file mode 100644
index 3202bfc..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonConstants.properties
+++ /dev/null
@@ -1,13 +0,0 @@
-inTheFuture = in the future
-month = month
-months = months
-years = years
-year = year
-
-oneSecondAgo = 1 second ago
-oneMinuteAgo = 1 minute ago
-oneHourAgo = 1 hour ago
-oneDayAgo = 1 day ago
-oneWeekAgo = 1 week ago
-oneMonthAgo = 1 month ago
-oneYearAgo = 1 year ago
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonMessages.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonMessages.java
deleted file mode 100644
index 5314254..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonMessages.java
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client;
-
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.i18n.client.Messages;
-
-public interface CommonMessages extends Messages {
-  CommonMessages M = GWT.create(CommonMessages.class);
-
-  String secondsAgo(long seconds);
-
-  String minutesAgo(long minutes);
-
-  String hoursAgo(long hours);
-
-  String daysAgo(long days);
-
-  String weeksAgo(long weeks);
-
-  String monthsAgo(long months);
-
-  String yearsAgo(long years);
-
-  String years0MonthsAgo(long years, String yearLabel);
-
-  String yearsMonthsAgo(long years, String yearLabel, long months, String monthLabel);
-}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonMessages.properties b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonMessages.properties
deleted file mode 100644
index 738602e..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonMessages.properties
+++ /dev/null
@@ -1,9 +0,0 @@
-secondsAgo = {0} seconds ago
-minutesAgo = {0} minutes ago
-hoursAgo = {0} hours ago
-daysAgo = {0} days ago
-weeksAgo = {0} weeks ago
-monthsAgo = {0} months ago
-years0MonthsAgo = {0} {1} ago
-yearsMonthsAgo = {0} {1}, {2} {3} ago
-yearsAgo = {0} years ago
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/DateFormatter.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/DateFormatter.java
deleted file mode 100644
index 4df2f5f..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/DateFormatter.java
+++ /dev/null
@@ -1,99 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client;
-
-import com.google.gerrit.client.info.GeneralPreferences;
-import com.google.gwt.i18n.client.DateTimeFormat;
-import java.util.Date;
-
-public class DateFormatter {
-  private static final long ONE_YEAR = 182L * 24 * 60 * 60 * 1000;
-
-  private final DateTimeFormat sTime;
-  private final DateTimeFormat sDate;
-  private final DateTimeFormat sdtFmt;
-  private final DateTimeFormat mDate;
-  private final DateTimeFormat dtfmt;
-
-  public DateFormatter(GeneralPreferences prefs) {
-    String fmt_sTime = prefs.timeFormat().getFormat();
-    String fmt_sDate = prefs.dateFormat().getShortFormat();
-    String fmt_mDate = prefs.dateFormat().getLongFormat();
-
-    sTime = DateTimeFormat.getFormat(fmt_sTime);
-    sDate = DateTimeFormat.getFormat(fmt_sDate);
-    sdtFmt = DateTimeFormat.getFormat(fmt_sDate + " " + fmt_sTime);
-    mDate = DateTimeFormat.getFormat(fmt_mDate);
-    dtfmt = DateTimeFormat.getFormat(fmt_mDate + " " + fmt_sTime);
-  }
-
-  /** Format a date using a really short format. */
-  public String shortFormat(Date dt) {
-    if (dt == null) {
-      return "";
-    }
-
-    Date now = new Date();
-    dt = new Date(dt.getTime());
-    if (mDate.format(now).equals(mDate.format(dt))) {
-      // Same day as today, report only the time.
-      //
-      return sTime.format(dt);
-
-    } else if (Math.abs(now.getTime() - dt.getTime()) < ONE_YEAR) {
-      // Within the last year, show a shorter date.
-      //
-      return sDate.format(dt);
-
-    } else {
-      // Report only date and year, its far away from now.
-      //
-      return mDate.format(dt);
-    }
-  }
-
-  /** Format a date using a really short format. */
-  public String shortFormatDayTime(Date dt) {
-    if (dt == null) {
-      return "";
-    }
-
-    Date now = new Date();
-    dt = new Date(dt.getTime());
-    if (mDate.format(now).equals(mDate.format(dt))) {
-      // Same day as today, report only the time.
-      //
-      return sTime.format(dt);
-
-    } else if (Math.abs(now.getTime() - dt.getTime()) < ONE_YEAR) {
-      // Within the last year, show a shorter date.
-      //
-      return sdtFmt.format(dt);
-
-    } else {
-      // Report only date and year, its far away from now.
-      //
-      return mDate.format(dt);
-    }
-  }
-
-  /** Format a date using the locale's medium length format. */
-  public String mediumFormat(Date dt) {
-    if (dt == null) {
-      return "";
-    }
-    return dtfmt.format(new Date(dt.getTime()));
-  }
-}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/GerritUiExtensionPoint.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/GerritUiExtensionPoint.java
deleted file mode 100644
index 66a3b6b..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/GerritUiExtensionPoint.java
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client;
-
-public enum GerritUiExtensionPoint {
-  /* ChangeScreen */
-  CHANGE_SCREEN_HEADER,
-  CHANGE_SCREEN_HEADER_RIGHT_OF_BUTTONS,
-  CHANGE_SCREEN_HEADER_RIGHT_OF_POP_DOWNS,
-  CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK,
-  CHANGE_SCREEN_BELOW_RELATED_INFO_BLOCK,
-  CHANGE_SCREEN_BELOW_COMMIT_INFO_BLOCK,
-  CHANGE_SCREEN_HISTORY_RIGHT_OF_BUTTONS,
-
-  /* MyPasswordScreen */
-  PASSWORD_SCREEN_BOTTOM,
-
-  /* MyPreferencesScreen */
-  PREFERENCES_SCREEN_BOTTOM,
-
-  /* MyProfileScreen */
-  PROFILE_SCREEN_BOTTOM,
-
-  /* ProjectInfoScreen */
-  PROJECT_INFO_SCREEN_TOP,
-  PROJECT_INFO_SCREEN_BOTTOM;
-
-  public enum Key {
-    ACCOUNT_INFO,
-    CHANGE_INFO,
-    PROJECT_NAME,
-    REVISION_INFO
-  }
-}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java
deleted file mode 100644
index e0cc9ca..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java
+++ /dev/null
@@ -1,142 +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.client;
-
-import java.util.Date;
-
-/**
- * Formatter to format timestamps relative to the current time using time units in the format
- * defined by {@code git log --relative-date}.
- */
-public class RelativeDateFormatter {
-  private static CommonConstants constants;
-  private static CommonMessages messages;
-
-  static final long SECOND_IN_MILLIS = 1000;
-  static final long MINUTE_IN_MILLIS = 60 * SECOND_IN_MILLIS;
-  static final long HOUR_IN_MILLIS = 60 * MINUTE_IN_MILLIS;
-  static final long DAY_IN_MILLIS = 24 * HOUR_IN_MILLIS;
-  static final long WEEK_IN_MILLIS = 7 * DAY_IN_MILLIS;
-  static final long MONTH_IN_MILLIS = 30 * DAY_IN_MILLIS;
-  static final long YEAR_IN_MILLIS = 365 * DAY_IN_MILLIS;
-
-  static void setConstants(CommonConstants c, CommonMessages m) {
-    constants = c;
-    messages = m;
-  }
-
-  private static CommonConstants c() {
-    return constants != null ? constants : CommonConstants.C;
-  }
-
-  private static CommonMessages m() {
-    return messages != null ? messages : CommonMessages.M;
-  }
-
-  /**
-   * @param when {@link Date} to format
-   * @return age of given {@link Date} compared to now formatted in the same relative format as
-   *     returned by {@code git log --relative-date}
-   */
-  public static String format(Date when) {
-    long ageMillis = (new Date()).getTime() - when.getTime();
-
-    // shouldn't happen in a perfect world
-    if (ageMillis < 0) {
-      return c().inTheFuture();
-    }
-
-    // seconds
-    if (ageMillis < upperLimit(MINUTE_IN_MILLIS)) {
-      long seconds = round(ageMillis, SECOND_IN_MILLIS);
-      if (seconds == 1) {
-        return c().oneSecondAgo();
-      }
-      return m().secondsAgo(seconds);
-    }
-
-    // minutes
-    if (ageMillis < upperLimit(HOUR_IN_MILLIS)) {
-      long minutes = round(ageMillis, MINUTE_IN_MILLIS);
-      if (minutes == 1) {
-        return c().oneMinuteAgo();
-      }
-      return m().minutesAgo(minutes);
-    }
-
-    // hours
-    if (ageMillis < upperLimit(DAY_IN_MILLIS)) {
-      long hours = round(ageMillis, HOUR_IN_MILLIS);
-      if (hours == 1) {
-        return c().oneHourAgo();
-      }
-      return m().hoursAgo(hours);
-    }
-
-    // up to 14 days use days
-    if (ageMillis < 14 * DAY_IN_MILLIS) {
-      long days = round(ageMillis, DAY_IN_MILLIS);
-      if (days == 1) {
-        return c().oneDayAgo();
-      }
-      return m().daysAgo(days);
-    }
-
-    // up to 10 weeks use weeks
-    if (ageMillis < 10 * WEEK_IN_MILLIS) {
-      long weeks = round(ageMillis, WEEK_IN_MILLIS);
-      if (weeks == 1) {
-        return c().oneWeekAgo();
-      }
-      return m().weeksAgo(weeks);
-    }
-
-    // months
-    if (ageMillis < YEAR_IN_MILLIS) {
-      long months = round(ageMillis, MONTH_IN_MILLIS);
-      if (months == 1) {
-        return c().oneMonthAgo();
-      }
-      return m().monthsAgo(months);
-    }
-
-    // up to 5 years use "year, months" rounded to months
-    if (ageMillis < 5 * YEAR_IN_MILLIS) {
-      long years = round(ageMillis, MONTH_IN_MILLIS) / 12;
-      String yearLabel = (years > 1) ? c().years() : c().year();
-      long months = round(ageMillis - years * YEAR_IN_MILLIS, MONTH_IN_MILLIS);
-      String monthLabel = (months > 1) ? c().months() : (months == 1 ? c().month() : "");
-      if (months == 0) {
-        return m().years0MonthsAgo(years, yearLabel);
-      }
-      return m().yearsMonthsAgo(years, yearLabel, months, monthLabel);
-    }
-
-    // years
-    long years = round(ageMillis, YEAR_IN_MILLIS);
-    if (years == 1) {
-      return c().oneYearAgo();
-    }
-    return m().yearsAgo(years);
-  }
-
-  private static long upperLimit(long unit) {
-    return unit + unit / 2;
-  }
-
-  private static long round(long n, long unit) {
-    return (n + unit / 2) / unit;
-  }
-}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/Resources.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/Resources.java
deleted file mode 100644
index 67627c3..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/Resources.java
+++ /dev/null
@@ -1,126 +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.client;
-
-import com.google.gwt.resources.client.ClientBundle;
-import com.google.gwt.resources.client.ImageResource;
-
-public interface Resources extends ClientBundle {
-  /** silk icons (CC-BY3.0): http://famfamfam.com/lab/icons/silk/ */
-  @Source("note_add.png")
-  ImageResource addFileComment();
-
-  @Source("tag_blue_add.png")
-  ImageResource addHashtag();
-
-  @Source("user_add.png")
-  ImageResource addUser();
-
-  @Source("user_edit.png")
-  ImageResource editUser();
-
-  // derived from resultset_next.png
-  @Source("resultset_down_gray.png")
-  ImageResource arrowDown();
-
-  // derived from resultset_next.png
-  @Source("resultset_next_gray.png")
-  ImageResource arrowRight();
-
-  // derived from resultset_next.png
-  @Source("resultset_up_gray.png")
-  ImageResource arrowUp();
-
-  @Source("lightbulb.png")
-  ImageResource blame();
-
-  @Source("page_white_put.png")
-  ImageResource downloadIcon();
-
-  // derived from comment.png
-  @Source("comment_draft.png")
-  ImageResource draftComments();
-
-  @Source("page_edit.png")
-  ImageResource edit();
-
-  @Source("arrow_undo.png")
-  ImageResource editUndo();
-
-  @Source("cog.png")
-  ImageResource gear();
-
-  @Source("tick.png")
-  ImageResource greenCheck();
-
-  @Source("tag_blue.png")
-  ImageResource hashtag();
-
-  @Source("lightbulb.png")
-  ImageResource info();
-
-  @Source("find.png")
-  ImageResource queryIcon();
-
-  @Source("lock.png")
-  ImageResource readOnly();
-
-  @Source("cross.png")
-  ImageResource redNot();
-
-  @Source("disk.png")
-  ImageResource save();
-
-  @Source("star.png")
-  ImageResource starFilled();
-
-  // derived from star.png
-  @Source("star-open.png")
-  ImageResource starOpen();
-
-  @Source("exclamation.png")
-  ImageResource warning();
-
-  @Source("help.png")
-  ImageResource question();
-
-  /** tango icon library (public domain): http://tango.freedesktop.org/Tango_Icon_Library */
-  @Source("goNext.png")
-  ImageResource goNext();
-
-  @Source("goPrev.png")
-  ImageResource goPrev();
-
-  @Source("goUp.png")
-  ImageResource goUp();
-
-  @Source("listAdd.png")
-  ImageResource listAdd();
-
-  // derived from important.png
-  @Source("merge.png")
-  ImageResource merge();
-
-  /** contributed by the artist under Apache2.0 */
-  @Source("sideBySideDiff.png")
-  ImageResource sideBySideDiff();
-
-  @Source("unifiedDiff.png")
-  ImageResource unifiedDiff();
-
-  /** contributed by the artist under CC-BY3.0 */
-  @Source("diffy26.png")
-  ImageResource gerritAvatar26();
-}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AccountInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AccountInfo.java
deleted file mode 100644
index e4c008c..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AccountInfo.java
+++ /dev/null
@@ -1,93 +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.client.info;
-
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.core.client.JsArrayString;
-import com.google.gwtjsonrpc.client.impl.ser.JavaSqlTimestamp_JsonSerializer;
-import java.sql.Timestamp;
-
-public class AccountInfo extends JavaScriptObject {
-  public final native int _accountId() /*-{ return this._account_id || 0; }-*/;
-
-  public final native String name() /*-{ return this.name; }-*/;
-
-  public final native String email() /*-{ return this.email; }-*/;
-
-  public final native JsArrayString secondaryEmails() /*-{ return this.secondary_emails; }-*/;
-
-  public final native String username() /*-{ return this.username; }-*/;
-
-  public final Timestamp registeredOn() {
-    Timestamp ts = _getRegisteredOn();
-    if (ts == null) {
-      ts = JavaSqlTimestamp_JsonSerializer.parseTimestamp(registeredOnRaw());
-      _setRegisteredOn(ts);
-    }
-    return ts;
-  }
-
-  private native String registeredOnRaw() /*-{ return this.registered_on; }-*/;
-
-  private native Timestamp _getRegisteredOn() /*-{ return this._cts; }-*/;
-
-  private native void _setRegisteredOn(Timestamp ts) /*-{ this._cts = ts; }-*/;
-
-  /**
-   * @return true if the server supplied avatar information about this account. The information may
-   *     be an empty list, indicating no avatars are available, such as when no plugin is installed.
-   *     This method returns false if the server did not check on avatars for the account.
-   */
-  public final native boolean hasAvatarInfo() /*-{ return this.hasOwnProperty('avatars') }-*/;
-
-  public final AvatarInfo avatar(int sz) {
-    JsArray<AvatarInfo> a = avatars();
-    for (int i = 0; a != null && i < a.length(); i++) {
-      AvatarInfo r = a.get(i);
-      if (r.height() == sz) {
-        return r;
-      }
-    }
-    return null;
-  }
-
-  private native JsArray<AvatarInfo> avatars() /*-{ return this.avatars }-*/;
-
-  public final native void name(String n) /*-{ this.name = n }-*/;
-
-  public final native void email(String e) /*-{ this.email = e }-*/;
-
-  public final native void username(String n) /*-{ this.username = n }-*/;
-
-  public static native AccountInfo create(int id, String name, String email, String username) /*-{
-    return {'_account_id': id, 'name': name, 'email': email,
-        'username': username};
-  }-*/;
-
-  protected AccountInfo() {}
-
-  public static class AvatarInfo extends JavaScriptObject {
-    public static final int DEFAULT_SIZE = 26;
-
-    public final native String url() /*-{ return this.url }-*/;
-
-    public final native int height() /*-{ return this.height || 0 }-*/;
-
-    public final native int width() /*-{ return this.width || 0 }-*/;
-
-    protected AvatarInfo() {}
-  }
-}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ActionInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ActionInfo.java
deleted file mode 100644
index d09d5b7..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ActionInfo.java
+++ /dev/null
@@ -1,32 +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.client.info;
-
-import com.google.gwt.core.client.JavaScriptObject;
-
-public class ActionInfo extends JavaScriptObject {
-
-  public final native String id() /*-{ return this.id; }-*/;
-
-  public final native String method() /*-{ return this.method; }-*/;
-
-  public final native String label() /*-{ return this.label; }-*/;
-
-  public final native String title() /*-{ return this.title; }-*/;
-
-  public final native boolean enabled() /*-{ return this.enabled || false; }-*/;
-
-  protected ActionInfo() {}
-}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AgreementInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AgreementInfo.java
deleted file mode 100644
index 04fba4f..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AgreementInfo.java
+++ /dev/null
@@ -1,29 +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.client.info;
-
-import com.google.gwt.core.client.JavaScriptObject;
-
-public class AgreementInfo extends JavaScriptObject {
-  public final native String name() /*-{ return this.name; }-*/;
-
-  public final native String description() /*-{ return this.description; }-*/;
-
-  public final native String url() /*-{ return this.url; }-*/;
-
-  public final native GroupInfo autoVerifyGroup() /*-{ return this.auto_verify_group; }-*/;
-
-  protected AgreementInfo() {}
-}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AuthInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AuthInfo.java
deleted file mode 100644
index 43281bd..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AuthInfo.java
+++ /dev/null
@@ -1,121 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.info;
-
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.extensions.client.AccountFieldName;
-import com.google.gerrit.extensions.client.AuthType;
-import com.google.gerrit.extensions.client.GitBasicAuthPolicy;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.core.client.JsArrayString;
-import java.util.ArrayList;
-import java.util.List;
-
-public class AuthInfo extends JavaScriptObject {
-  public final AuthType authType() {
-    return AuthType.valueOf(authTypeRaw());
-  }
-
-  public final boolean isLdap() {
-    return authType() == AuthType.LDAP || authType() == AuthType.LDAP_BIND;
-  }
-
-  public final boolean isOpenId() {
-    return authType() == AuthType.OPENID;
-  }
-
-  public final boolean isOAuth() {
-    return authType() == AuthType.OAUTH;
-  }
-
-  public final boolean isDev() {
-    return authType() == AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT;
-  }
-
-  public final boolean isClientSslCertLdap() {
-    return authType() == AuthType.CLIENT_SSL_CERT_LDAP;
-  }
-
-  public final boolean isCustomExtension() {
-    return authType() == AuthType.CUSTOM_EXTENSION;
-  }
-
-  public final boolean canEdit(AccountFieldName f) {
-    return editableAccountFields().contains(f);
-  }
-
-  public final List<AccountFieldName> editableAccountFields() {
-    List<AccountFieldName> fields = new ArrayList<>();
-    for (String f : Natives.asList(_editableAccountFields())) {
-      fields.add(AccountFieldName.valueOf(f));
-    }
-    return fields;
-  }
-
-  public final List<AgreementInfo> contributorAgreements() {
-    List<AgreementInfo> agreements = new ArrayList<>();
-    JsArray<AgreementInfo> contributorAgreements = _contributorAgreements();
-    if (contributorAgreements != null) {
-      agreements.addAll(Natives.asList(contributorAgreements));
-    }
-    return agreements;
-  }
-
-  public final boolean siteHasUsernames() {
-    if (isCustomExtension() && httpPasswordUrl() != null && !canEdit(AccountFieldName.USER_NAME)) {
-      return false;
-    }
-    return true;
-  }
-
-  public final boolean isHttpPasswordSettingsEnabled() {
-    return gitBasicAuthPolicy() == GitBasicAuthPolicy.HTTP
-        || gitBasicAuthPolicy() == GitBasicAuthPolicy.HTTP_LDAP;
-  }
-
-  public final GitBasicAuthPolicy gitBasicAuthPolicy() {
-    return GitBasicAuthPolicy.valueOf(gitBasicAuthPolicyRaw());
-  }
-
-  public final native boolean useContributorAgreements()
-      /*-{ return this.use_contributor_agreements || false; }-*/ ;
-
-  public final native String loginUrl() /*-{ return this.login_url; }-*/;
-
-  public final native String loginText() /*-{ return this.login_text; }-*/;
-
-  public final native String switchAccountUrl() /*-{ return this.switch_account_url; }-*/;
-
-  public final native String registerUrl() /*-{ return this.register_url; }-*/;
-
-  public final native String registerText() /*-{ return this.register_text; }-*/;
-
-  public final native String editFullNameUrl() /*-{ return this.edit_full_name_url; }-*/;
-
-  public final native String httpPasswordUrl() /*-{ return this.http_password_url; }-*/;
-
-  private native String gitBasicAuthPolicyRaw() /*-{ return this.git_basic_auth_policy; }-*/;
-
-  private native String authTypeRaw() /*-{ return this.auth_type; }-*/;
-
-  private native JsArrayString _editableAccountFields()
-      /*-{ return this.editable_account_fields; }-*/ ;
-
-  private native JsArray<AgreementInfo> _contributorAgreements()
-      /*-{ return this.contributor_agreements; }-*/ ;
-
-  protected AuthInfo() {}
-}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
deleted file mode 100644
index 0f786a6..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
+++ /dev/null
@@ -1,575 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.info;
-
-import static java.util.Comparator.comparing;
-
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.NativeString;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.common.data.LabelValue;
-import com.google.gerrit.common.data.SubmitRecord;
-import com.google.gerrit.extensions.client.ReviewerState;
-import com.google.gerrit.extensions.client.SubmitType;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.core.client.JsArrayString;
-import com.google.gwtjsonrpc.client.impl.ser.JavaSqlTimestamp_JsonSerializer;
-import java.sql.Timestamp;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.SortedSet;
-import java.util.TreeSet;
-
-public class ChangeInfo extends JavaScriptObject {
-  public final void init() {
-    if (allLabels() != null) {
-      allLabels().copyKeysIntoChildren("_name");
-    }
-  }
-
-  public final Project.NameKey projectNameKey() {
-    return new Project.NameKey(project());
-  }
-
-  public final Change.Id legacyId() {
-    return new Change.Id(_number());
-  }
-
-  public final Timestamp created() {
-    Timestamp ts = _getCts();
-    if (ts == null) {
-      ts = JavaSqlTimestamp_JsonSerializer.parseTimestamp(createdRaw());
-      _setCts(ts);
-    }
-    return ts;
-  }
-
-  public final boolean hasEditBasedOnCurrentPatchSet() {
-    JsArray<RevisionInfo> revList = revisions().values();
-    RevisionInfo.sortRevisionInfoByNumber(revList);
-    return revList.get(revList.length() - 1).isEdit();
-  }
-
-  private native Timestamp _getCts() /*-{ return this._cts; }-*/;
-
-  private native void _setCts(Timestamp ts) /*-{ this._cts = ts; }-*/;
-
-  public final Timestamp updated() {
-    return JavaSqlTimestamp_JsonSerializer.parseTimestamp(updatedRaw());
-  }
-
-  public final Timestamp submitted() {
-    return JavaSqlTimestamp_JsonSerializer.parseTimestamp(submittedRaw());
-  }
-
-  public final String idAbbreviated() {
-    return new Change.Key(changeId()).abbreviate();
-  }
-
-  public final Change.Status status() {
-    return Change.Status.valueOf(statusRaw());
-  }
-
-  public final Set<String> labels() {
-    return allLabels().keySet();
-  }
-
-  public final Set<Integer> removableReviewerIds() {
-    Set<Integer> removable = new HashSet<>();
-    if (removableReviewers() != null) {
-      for (AccountInfo a : Natives.asList(removableReviewers())) {
-        removable.add(a._accountId());
-      }
-    }
-    return removable;
-  }
-
-  public final native String id() /*-{ return this.id; }-*/;
-
-  public final native String project() /*-{ return this.project; }-*/;
-
-  public final native String branch() /*-{ return this.branch; }-*/;
-
-  public final native String topic() /*-{ return this.topic; }-*/;
-
-  public final native String changeId() /*-{ return this.change_id; }-*/;
-
-  public final native boolean mergeable() /*-{ return this.mergeable ? true : false; }-*/;
-
-  public final native int insertions() /*-{ return this.insertions; }-*/;
-
-  public final native int deletions() /*-{ return this.deletions; }-*/;
-
-  private native String statusRaw() /*-{ return this.status; }-*/;
-
-  public final native String subject() /*-{ return this.subject; }-*/;
-
-  public final native AccountInfo owner() /*-{ return this.owner; }-*/;
-
-  public final native AccountInfo assignee() /*-{ return this.assignee; }-*/;
-
-  private native String createdRaw() /*-{ return this.created; }-*/;
-
-  private native String updatedRaw() /*-{ return this.updated; }-*/;
-
-  private native String submittedRaw() /*-{ return this.submitted; }-*/;
-
-  public final native AccountInfo submitter() /*-{ return this.submitter; }-*/;
-
-  public final native boolean starred() /*-{ return this.starred ? true : false; }-*/;
-
-  public final native boolean reviewed() /*-{ return this.reviewed ? true : false; }-*/;
-
-  public final native boolean isPrivate() /*-{ return this.is_private ? true : false; }-*/;
-
-  public final native boolean
-      isWorkInProgress() /*-{ return this.work_in_progress ? true : false; }-*/;
-
-  public final native NativeMap<LabelInfo> allLabels() /*-{ return this.labels; }-*/;
-
-  public final native LabelInfo label(String n) /*-{ return this.labels[n]; }-*/;
-
-  public final native String currentRevision() /*-{ return this.current_revision; }-*/;
-
-  public final native void setCurrentRevision(String r) /*-{ this.current_revision = r; }-*/;
-
-  public final native NativeMap<RevisionInfo> revisions() /*-{ return this.revisions; }-*/;
-
-  public final native RevisionInfo revision(String n) /*-{ return this.revisions[n]; }-*/;
-
-  public final native JsArray<MessageInfo> messages() /*-{ return this.messages; }-*/;
-
-  public final native void setEdit(EditInfo edit) /*-{ this.edit = edit; }-*/;
-
-  public final native EditInfo edit() /*-{ return this.edit; }-*/;
-
-  public final native boolean hasEdit() /*-{ return this.hasOwnProperty('edit') }-*/;
-
-  public final native JsArrayString hashtags() /*-{ return this.hashtags; }-*/;
-
-  public final native boolean hasPermittedLabels()
-      /*-{ return this.hasOwnProperty('permitted_labels') }-*/ ;
-
-  public final native NativeMap<JsArrayString> permittedLabels()
-      /*-{ return this.permitted_labels; }-*/ ;
-
-  public final native JsArrayString permittedValues(String n)
-      /*-{ return this.permitted_labels[n]; }-*/ ;
-
-  public final native JsArray<AccountInfo> removableReviewers()
-      /*-{ return this.removable_reviewers; }-*/ ;
-
-  private native NativeMap<JsArray<AccountInfo>> _reviewers() /*-{ return this.reviewers; }-*/;
-
-  public final Map<ReviewerState, List<AccountInfo>> reviewers() {
-    NativeMap<JsArray<AccountInfo>> reviewers = _reviewers();
-    Map<ReviewerState, List<AccountInfo>> result = new HashMap<>();
-    for (String k : reviewers.keySet()) {
-      ReviewerState state = ReviewerState.valueOf(k.toUpperCase());
-      List<AccountInfo> accounts = result.get(state);
-      if (accounts == null) {
-        accounts = new ArrayList<>();
-        result.put(state, accounts);
-      }
-      accounts.addAll(Natives.asList(reviewers.get(k)));
-    }
-    return result;
-  }
-
-  public final native boolean hasActions() /*-{ return this.hasOwnProperty('actions') }-*/;
-
-  public final native NativeMap<ActionInfo> actions() /*-{ return this.actions; }-*/;
-
-  public final native int _number() /*-{ return this._number; }-*/;
-
-  public final native boolean _more_changes() /*-{ return this._more_changes ? true : false; }-*/;
-
-  public final SubmitType submitType() {
-    String submitType = _submitType();
-    if (submitType == null) {
-      return null;
-    }
-    return SubmitType.valueOf(submitType);
-  }
-
-  private native String _submitType() /*-{ return this.submit_type; }-*/;
-
-  public final boolean submittable() {
-    init();
-    return _submittable();
-  }
-
-  private native boolean _submittable() /*-{ return this.submittable ? true : false; }-*/;
-
-  /**
-   * @return the index of the missing label or -1 if no label is missing, or if more than one label
-   *     is missing.
-   */
-  public final int getMissingLabelIndex() {
-    int i = -1;
-    int ret = -1;
-    List<LabelInfo> labels = Natives.asList(allLabels().values());
-    for (LabelInfo label : labels) {
-      i++;
-      if (!permittedLabels().containsKey(label.name())) {
-        continue;
-      }
-
-      JsArrayString values = permittedValues(label.name());
-      if (values.length() == 0) {
-        continue;
-      }
-
-      switch (label.status()) {
-        case NEED: // Label is required for submit.
-          if (ret != -1) {
-            // more than one label is missing, so it's unclear which to quick
-            // approve, return -1
-            return -1;
-          }
-          ret = i;
-          continue;
-
-        case OK: // Label already applied.
-        case MAY: // Label is not required.
-          continue;
-
-        case REJECT: // Submit cannot happen, do not quick approve.
-        case IMPOSSIBLE:
-          return -1;
-      }
-    }
-    return ret;
-  }
-
-  protected ChangeInfo() {}
-
-  public static class LabelInfo extends JavaScriptObject {
-    public final SubmitRecord.Label.Status status() {
-      if (approved() != null) {
-        return SubmitRecord.Label.Status.OK;
-      } else if (rejected() != null) {
-        return SubmitRecord.Label.Status.REJECT;
-      } else if (optional()) {
-        return SubmitRecord.Label.Status.MAY;
-      } else {
-        return SubmitRecord.Label.Status.NEED;
-      }
-    }
-
-    public final native String name() /*-{ return this._name; }-*/;
-
-    public final native AccountInfo approved() /*-{ return this.approved; }-*/;
-
-    public final native AccountInfo rejected() /*-{ return this.rejected; }-*/;
-
-    public final native AccountInfo recommended() /*-{ return this.recommended; }-*/;
-
-    public final native AccountInfo disliked() /*-{ return this.disliked; }-*/;
-
-    public final native JsArray<ApprovalInfo> all() /*-{ return this.all; }-*/;
-
-    public final ApprovalInfo forUser(int user) {
-      JsArray<ApprovalInfo> all = all();
-      for (int i = 0; all != null && i < all.length(); i++) {
-        if (all.get(i)._accountId() == user) {
-          return all.get(i);
-        }
-      }
-      return null;
-    }
-
-    private native NativeMap<NativeString> _values() /*-{ return this.values; }-*/;
-
-    public final Set<String> values() {
-      return Natives.keys(_values());
-    }
-
-    public final native String valueText(String n) /*-{ return this.values[n]; }-*/;
-
-    public final native boolean optional() /*-{ return this.optional ? true : false; }-*/;
-
-    public final native boolean blocking() /*-{ return this.blocking ? true : false; }-*/;
-
-    public final native short defaultValue() /*-{ return this.default_value; }-*/;
-
-    public final native short _value() /*-{
-      if (this.value) return this.value;
-      if (this.disliked) return -1;
-      if (this.recommended) return 1;
-      return 0;
-    }-*/;
-
-    public final String maxValue() {
-      return LabelValue.formatValue(valueSet().last());
-    }
-
-    public final SortedSet<Short> valueSet() {
-      SortedSet<Short> values = new TreeSet<>();
-      for (String v : values()) {
-        values.add(parseValue(v));
-      }
-      return values;
-    }
-
-    public static final short parseValue(String formatted) {
-      if (formatted.startsWith("+")) {
-        formatted = formatted.substring(1);
-      } else if (formatted.startsWith(" ")) {
-        formatted = formatted.trim();
-      }
-      return Short.parseShort(formatted);
-    }
-
-    protected LabelInfo() {}
-  }
-
-  public static class ApprovalInfo extends AccountInfo {
-    public final native boolean hasValue() /*-{ return this.hasOwnProperty('value'); }-*/;
-
-    public final native short value() /*-{ return this.value || 0; }-*/;
-
-    public final native VotingRangeInfo
-        permittedVotingRange() /*-{ return this.permitted_voting_range; }-*/;
-
-    protected ApprovalInfo() {}
-  }
-
-  public static class VotingRangeInfo extends AccountInfo {
-    public final native short min() /*-{ return this.min || 0; }-*/;
-
-    public final native short max() /*-{ return this.max || 0; }-*/;
-
-    protected VotingRangeInfo() {}
-  }
-
-  public static class EditInfo extends JavaScriptObject {
-    public final native String name() /*-{ return this.name; }-*/;
-
-    public final native String setName(String n) /*-{ this.name = n; }-*/;
-
-    public final native String baseRevision() /*-{ return this.base_revision; }-*/;
-
-    public final native CommitInfo commit() /*-{ return this.commit; }-*/;
-
-    public final native boolean hasActions() /*-{ return this.hasOwnProperty('actions') }-*/;
-
-    public final native NativeMap<ActionInfo> actions() /*-{ return this.actions; }-*/;
-
-    public final native boolean hasFetch() /*-{ return this.hasOwnProperty('fetch') }-*/;
-
-    public final native NativeMap<FetchInfo> fetch() /*-{ return this.fetch; }-*/;
-
-    public final native boolean hasFiles() /*-{ return this.hasOwnProperty('files') }-*/;
-
-    public final native NativeMap<FileInfo> files() /*-{ return this.files; }-*/;
-
-    protected EditInfo() {}
-  }
-
-  public static class RevisionInfo extends JavaScriptObject {
-    public static RevisionInfo fromEdit(EditInfo edit) {
-      RevisionInfo revisionInfo = createObject().cast();
-      revisionInfo.takeFromEdit(edit);
-      return revisionInfo;
-    }
-
-    public static RevisionInfo forParent(int number, CommitInfo commit) {
-      RevisionInfo revisionInfo = createObject().cast();
-      revisionInfo.takeFromParent(number, commit);
-      return revisionInfo;
-    }
-
-    private native void takeFromEdit(EditInfo edit) /*-{
-      this._number = 0;
-      this.name = edit.name;
-      this.commit = edit.commit;
-      this.edit_base = edit.base_revision;
-    }-*/;
-
-    private native void takeFromParent(int number, CommitInfo commit) /*-{
-      this._number = number;
-      this.commit = commit;
-      this.name = this._number;
-    }-*/;
-
-    public final native int _number() /*-{ return this._number; }-*/;
-
-    public final native String name() /*-{ return this.name; }-*/;
-
-    public final native AccountInfo uploader() /*-{ return this.uploader; }-*/;
-
-    public final native boolean isEdit() /*-{ return this._number == 0; }-*/;
-
-    public final native CommitInfo commit() /*-{ return this.commit; }-*/;
-
-    public final native void setCommit(CommitInfo c) /*-{ this.commit = c; }-*/;
-
-    public final native String editBase() /*-{ return this.edit_base; }-*/;
-
-    public final native boolean hasFiles() /*-{ return this.hasOwnProperty('files') }-*/;
-
-    public final native NativeMap<FileInfo> files() /*-{ return this.files; }-*/;
-
-    public final native boolean hasActions() /*-{ return this.hasOwnProperty('actions') }-*/;
-
-    public final native NativeMap<ActionInfo> actions() /*-{ return this.actions; }-*/;
-
-    public final native boolean hasFetch() /*-{ return this.hasOwnProperty('fetch') }-*/;
-
-    public final native NativeMap<FetchInfo> fetch() /*-{ return this.fetch; }-*/;
-
-    public final native boolean
-        hasPushCertificate() /*-{ return this.hasOwnProperty('push_certificate'); }-*/;
-
-    public final native PushCertificateInfo
-        pushCertificate() /*-{ return this.push_certificate; }-*/;
-
-    public static void sortRevisionInfoByNumber(JsArray<RevisionInfo> list) {
-      final int editParent = findEditParent(list);
-      Natives.asList(list)
-          .sort(comparing(r -> !r.isEdit() ? 2 * (r._number() - 1) + 1 : 2 * editParent));
-    }
-
-    public static int findEditParent(JsArray<RevisionInfo> list) {
-      RevisionInfo r = findEditParentRevision(list);
-      return r == null ? -1 : r._number();
-    }
-
-    public static RevisionInfo findEditParentRevision(JsArray<RevisionInfo> list) {
-      for (int i = 0; i < list.length(); i++) {
-        // edit under revisions?
-        RevisionInfo editInfo = list.get(i);
-        if (editInfo.isEdit()) {
-          String parentRevision = editInfo.editBase();
-          // find parent
-          for (int j = 0; j < list.length(); j++) {
-            RevisionInfo parentInfo = list.get(j);
-            String name = parentInfo.name();
-            if (name.equals(parentRevision)) {
-              // found parent pacth set number
-              return parentInfo;
-            }
-          }
-        }
-      }
-      return null;
-    }
-
-    public final String id() {
-      return PatchSet.Id.toId(_number());
-    }
-
-    public final boolean isMerge() {
-      return commit().parents().length() > 1;
-    }
-
-    protected RevisionInfo() {}
-  }
-
-  public static class FetchInfo extends JavaScriptObject {
-    public final native String url() /*-{ return this.url }-*/;
-
-    public final native String ref() /*-{ return this.ref }-*/;
-
-    public final native NativeMap<NativeString> commands() /*-{ return this.commands }-*/;
-
-    public final native String command(String n) /*-{ return this.commands[n]; }-*/;
-
-    protected FetchInfo() {}
-  }
-
-  public static class CommitInfo extends JavaScriptObject {
-    public final native String commit() /*-{ return this.commit; }-*/;
-
-    public final native JsArray<CommitInfo> parents() /*-{ return this.parents; }-*/;
-
-    public final native GitPerson author() /*-{ return this.author; }-*/;
-
-    public final native GitPerson committer() /*-{ return this.committer; }-*/;
-
-    public final native String subject() /*-{ return this.subject; }-*/;
-
-    public final native String message() /*-{ return this.message; }-*/;
-
-    public final native JsArray<WebLinkInfo> webLinks() /*-{ return this.web_links; }-*/;
-
-    protected CommitInfo() {}
-  }
-
-  public static class GitPerson extends JavaScriptObject {
-    public final native String name() /*-{ return this.name; }-*/;
-
-    public final native String email() /*-{ return this.email; }-*/;
-
-    private native String dateRaw() /*-{ return this.date; }-*/;
-
-    public final Timestamp date() {
-      return JavaSqlTimestamp_JsonSerializer.parseTimestamp(dateRaw());
-    }
-
-    protected GitPerson() {}
-  }
-
-  public static class MessageInfo extends JavaScriptObject {
-    public final native AccountInfo author() /*-{ return this.author; }-*/;
-
-    public final native String message() /*-{ return this.message; }-*/;
-
-    public final native int _revisionNumber() /*-{ return this._revision_number || 0; }-*/;
-
-    public final native String tag() /*-{ return this.tag; }-*/;
-
-    private native String dateRaw() /*-{ return this.date; }-*/;
-
-    public final Timestamp date() {
-      return JavaSqlTimestamp_JsonSerializer.parseTimestamp(dateRaw());
-    }
-
-    protected MessageInfo() {}
-  }
-
-  public static class MergeableInfo extends JavaScriptObject {
-    public final native String submitType() /*-{ return this.submit_type }-*/;
-
-    public final native boolean mergeable() /*-{ return this.mergeable }-*/;
-
-    protected MergeableInfo() {}
-  }
-
-  public static class IncludedInInfo extends JavaScriptObject {
-    public final Set<String> externalNames() {
-      return Natives.keys(external());
-    }
-
-    public final native JsArrayString branches() /*-{ return this.branches; }-*/;
-
-    public final native JsArrayString tags() /*-{ return this.tags; }-*/;
-
-    public final native JsArrayString external(String n) /*-{ return this.external[n]; }-*/;
-
-    private native NativeMap<JsArrayString> external() /*-{ return this.external; }-*/;
-
-    protected IncludedInInfo() {}
-  }
-}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/DownloadInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/DownloadInfo.java
deleted file mode 100644
index a22a1e8..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/DownloadInfo.java
+++ /dev/null
@@ -1,126 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.info;
-
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.NativeString;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArrayString;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-public class DownloadInfo extends JavaScriptObject {
-  public final List<String> schemes() {
-    return _schemes().sortedKeys();
-  }
-
-  public final List<String> archives() {
-    List<String> archives = new ArrayList<>();
-    archives.addAll(Natives.asList(_archives()));
-    return archives;
-  }
-
-  public final native DownloadSchemeInfo scheme(String n) /*-{ return this.schemes[n]; }-*/;
-
-  private native NativeMap<DownloadSchemeInfo> _schemes() /*-{ return this.schemes; }-*/;
-
-  private native JsArrayString _archives() /*-{ return this.archives; }-*/;
-
-  protected DownloadInfo() {}
-
-  public static class DownloadSchemeInfo extends JavaScriptObject {
-    public final List<String> commandNames() {
-      return _commands().sortedKeys();
-    }
-
-    public final Set<DownloadCommandInfo> commands(String project) {
-      Set<DownloadCommandInfo> commands = new HashSet<>();
-      for (String commandName : commandNames()) {
-        commands.add(new DownloadCommandInfo(commandName, command(commandName, project)));
-      }
-      return commands;
-    }
-
-    public final String command(String commandName, String project) {
-      return command(commandName).replaceAll("\\$\\{project\\}", project);
-    }
-
-    private static String projectBaseName(String project) {
-      return project.substring(project.lastIndexOf('/') + 1);
-    }
-
-    public final List<String> cloneCommandNames() {
-      return _cloneCommands().sortedKeys();
-    }
-
-    public final List<DownloadCommandInfo> cloneCommands(String project) {
-      List<String> commandNames = cloneCommandNames();
-      List<DownloadCommandInfo> commands = new ArrayList<>(commandNames.size());
-      for (String commandName : commandNames) {
-        commands.add(new DownloadCommandInfo(commandName, cloneCommand(commandName, project)));
-      }
-      return commands;
-    }
-
-    public final String cloneCommand(String commandName, String project) {
-      return cloneCommand(commandName)
-          .replaceAll("\\$\\{project\\}", project)
-          .replaceAll("\\$\\{project-base-name\\}", projectBaseName(project));
-    }
-
-    public final String getUrl(String project) {
-      return url().replaceAll("\\$\\{project\\}", project);
-    }
-
-    public final native String name() /*-{ return this.name; }-*/;
-
-    public final native String url() /*-{ return this.url; }-*/;
-
-    public final native boolean isAuthRequired() /*-{ return this.is_auth_required || false; }-*/;
-
-    public final native boolean isAuthSupported() /*-{ return this.is_auth_supported || false; }-*/;
-
-    public final native String command(String n) /*-{ return this.commands[n]; }-*/;
-
-    public final native String cloneCommand(String n) /*-{ return this.clone_commands[n]; }-*/;
-
-    private native NativeMap<NativeString> _commands() /*-{ return this.commands; }-*/;
-
-    private native NativeMap<NativeString> _cloneCommands() /*-{ return this.clone_commands; }-*/;
-
-    protected DownloadSchemeInfo() {}
-  }
-
-  public static class DownloadCommandInfo {
-    private final String name;
-    private final String command;
-
-    DownloadCommandInfo(String name, String command) {
-      this.name = name;
-      this.command = command;
-    }
-
-    public String name() {
-      return name;
-    }
-
-    public String command() {
-      return command;
-    }
-  }
-}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/FileInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/FileInfo.java
deleted file mode 100644
index fc3dbf1..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/FileInfo.java
+++ /dev/null
@@ -1,75 +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.client.info;
-
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.common.data.FilenameComparator;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
-import java.util.Comparator;
-
-public class FileInfo extends JavaScriptObject {
-  public final native String path() /*-{ return this.path; }-*/;
-
-  public final native String oldPath() /*-{ return this.old_path; }-*/;
-
-  public final native int linesInserted() /*-{ return this.lines_inserted || 0; }-*/;
-
-  public final native int linesDeleted() /*-{ return this.lines_deleted || 0; }-*/;
-
-  public final native boolean binary() /*-{ return this.binary || false; }-*/;
-
-  public final native String status() /*-{ return this.status; }-*/;
-
-  // JSNI methods cannot have 'long' as a parameter type or a return type and
-  // it's suggested to use double in this case:
-  // http://www.gwtproject.org/doc/latest/DevGuideCodingBasicsJSNI.html#important
-  public final long size() {
-    return (long) _size();
-  }
-
-  private native double _size() /*-{ return this.size || 0; }-*/;
-
-  public final long sizeDelta() {
-    return (long) _sizeDelta();
-  }
-
-  private native double _sizeDelta() /*-{ return this.size_delta || 0; }-*/;
-
-  public final native int _row() /*-{ return this._row }-*/;
-
-  public final native void _row(int r) /*-{ this._row = r }-*/;
-
-  public static void sortFileInfoByPath(JsArray<FileInfo> list) {
-    Natives.asList(list).sort(Comparator.comparing(FileInfo::path, FilenameComparator.INSTANCE));
-  }
-
-  public static String getFileName(String path) {
-    String fileName;
-    if (Patch.COMMIT_MSG.equals(path)) {
-      fileName = "Commit Message";
-    } else if (Patch.MERGE_LIST.equals(path)) {
-      fileName = "Merge List";
-    } else {
-      fileName = path;
-    }
-
-    int s = fileName.lastIndexOf('/');
-    return s >= 0 ? fileName.substring(s + 1) : fileName;
-  }
-
-  protected FileInfo() {}
-}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GeneralPreferences.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GeneralPreferences.java
deleted file mode 100644
index fbdf52c..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GeneralPreferences.java
+++ /dev/null
@@ -1,274 +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.client.info;
-
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.NativeString;
-import com.google.gerrit.client.rpc.Natives;
-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.DownloadCommand;
-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.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-public class GeneralPreferences extends JavaScriptObject {
-  public static GeneralPreferences create() {
-    return createObject().cast();
-  }
-
-  public static GeneralPreferences createDefault() {
-    GeneralPreferencesInfo d = GeneralPreferencesInfo.defaults();
-    GeneralPreferences p = createObject().cast();
-    p.changesPerPage(d.changesPerPage);
-    p.showSiteHeader(d.showSiteHeader);
-    p.useFlashClipboard(d.useFlashClipboard);
-    p.downloadScheme(d.downloadScheme);
-    p.downloadCommand(d.downloadCommand);
-    p.dateFormat(d.getDateFormat());
-    p.timeFormat(d.getTimeFormat());
-    p.highlightAssigneeInChangeTable(d.highlightAssigneeInChangeTable);
-    p.relativeDateInChangeTable(d.relativeDateInChangeTable);
-    p.sizeBarInChangeTable(d.sizeBarInChangeTable);
-    p.legacycidInChangeTable(d.legacycidInChangeTable);
-    p.muteCommonPathPrefixes(d.muteCommonPathPrefixes);
-    p.signedOffBy(d.signedOffBy);
-    p.emailFormat(d.emailFormat);
-    p.reviewCategoryStrategy(d.getReviewCategoryStrategy());
-    p.diffView(d.getDiffView());
-    p.emailStrategy(d.emailStrategy);
-    p.defaultBaseForMerges(d.defaultBaseForMerges);
-    return p;
-  }
-
-  public final int changesPerPage() {
-    int changesPerPage = get("changes_per_page", GeneralPreferencesInfo.DEFAULT_PAGESIZE);
-    return 0 < changesPerPage ? changesPerPage : GeneralPreferencesInfo.DEFAULT_PAGESIZE;
-  }
-
-  private native short get(String n, int d) /*-{ return this.hasOwnProperty(n) ? this[n] : d }-*/;
-
-  public final native boolean showSiteHeader() /*-{ return this.show_site_header || false }-*/;
-
-  public final native boolean useFlashClipboard()
-      /*-{ return this.use_flash_clipboard || false }-*/ ;
-
-  public final native String downloadScheme() /*-{ return this.download_scheme }-*/;
-
-  public final DownloadCommand downloadCommand() {
-    String s = downloadCommandRaw();
-    return s != null ? DownloadCommand.valueOf(s) : null;
-  }
-
-  private native String downloadCommandRaw() /*-{ return this.download_command }-*/;
-
-  public final DateFormat dateFormat() {
-    String s = dateFormatRaw();
-    return s != null ? DateFormat.valueOf(s) : null;
-  }
-
-  private native String dateFormatRaw() /*-{ return this.date_format }-*/;
-
-  public final TimeFormat timeFormat() {
-    String s = timeFormatRaw();
-    return s != null ? TimeFormat.valueOf(s) : null;
-  }
-
-  private native String timeFormatRaw() /*-{ return this.time_format }-*/;
-
-  public final native boolean highlightAssigneeInChangeTable()
-      /*-{ return this.highlight_assignee_in_change_table || false }-*/ ;
-
-  public final native boolean relativeDateInChangeTable()
-      /*-{ return this.relative_date_in_change_table || false }-*/ ;
-
-  public final native boolean sizeBarInChangeTable()
-      /*-{ return this.size_bar_in_change_table || false }-*/ ;
-
-  public final native boolean legacycidInChangeTable()
-      /*-{ return this.legacycid_in_change_table || false }-*/ ;
-
-  public final native boolean muteCommonPathPrefixes()
-      /*-{ return this.mute_common_path_prefixes || false }-*/ ;
-
-  public final native boolean signedOffBy() /*-{ return this.signed_off_by || false }-*/;
-
-  public final ReviewCategoryStrategy reviewCategoryStrategy() {
-    String s = reviewCategeoryStrategyRaw();
-    return s != null ? ReviewCategoryStrategy.valueOf(s) : ReviewCategoryStrategy.NONE;
-  }
-
-  private native String reviewCategeoryStrategyRaw() /*-{ return this.review_category_strategy }-*/;
-
-  public final DiffView diffView() {
-    String s = diffViewRaw();
-    return s != null ? DiffView.valueOf(s) : null;
-  }
-
-  private native String diffViewRaw() /*-{ return this.diff_view }-*/;
-
-  public final EmailStrategy emailStrategy() {
-    String s = emailStrategyRaw();
-    return s != null ? EmailStrategy.valueOf(s) : null;
-  }
-
-  private native String emailStrategyRaw() /*-{ return this.email_strategy }-*/;
-
-  public final EmailFormat emailFormat() {
-    String s = emailFormatRaw();
-    return s != null ? EmailFormat.valueOf(s) : null;
-  }
-
-  private native String emailFormatRaw() /*-{ return this.email_format }-*/;
-
-  public final DefaultBase defaultBaseForMerges() {
-    String s = defaultBaseForMergesRaw();
-    return s != null ? DefaultBase.valueOf(s) : null;
-  }
-
-  private native String defaultBaseForMergesRaw() /*-{ return this.default_base_for_merges }-*/;
-
-  public final native boolean
-      publishCommentsOnPush() /*-{ return this.publish_comments_on_push || false }-*/;
-
-  public final native boolean
-      workInProgressByDefault() /*-{ return this.work_in_progress_by_default || false }-*/;
-
-  public final native JsArray<TopMenuItem> my() /*-{ return this.my; }-*/;
-
-  public final native void changesPerPage(int n) /*-{ this.changes_per_page = n }-*/;
-
-  public final native void showSiteHeader(boolean s) /*-{ this.show_site_header = s }-*/;
-
-  public final native void useFlashClipboard(boolean u) /*-{ this.use_flash_clipboard = u }-*/;
-
-  public final native void downloadScheme(String d) /*-{ this.download_scheme = d }-*/;
-
-  public final void downloadCommand(DownloadCommand d) {
-    downloadCommandRaw(d != null ? d.toString() : null);
-  }
-
-  public final native void downloadCommandRaw(String d) /*-{ this.download_command = d }-*/;
-
-  public final void dateFormat(DateFormat f) {
-    dateFormatRaw(f != null ? f.toString() : null);
-  }
-
-  private native void dateFormatRaw(String f) /*-{ this.date_format = f }-*/;
-
-  public final void timeFormat(TimeFormat f) {
-    timeFormatRaw(f != null ? f.toString() : null);
-  }
-
-  private native void timeFormatRaw(String f) /*-{ this.time_format = f }-*/;
-
-  public final native void highlightAssigneeInChangeTable(boolean d)
-      /*-{ this.highlight_assignee_in_change_table = d }-*/ ;
-
-  public final native void relativeDateInChangeTable(boolean d)
-      /*-{ this.relative_date_in_change_table = d }-*/ ;
-
-  public final native void sizeBarInChangeTable(boolean s)
-      /*-{ this.size_bar_in_change_table = s }-*/ ;
-
-  public final native void legacycidInChangeTable(boolean s)
-      /*-{ this.legacycid_in_change_table = s }-*/ ;
-
-  public final native void muteCommonPathPrefixes(boolean s)
-      /*-{ this.mute_common_path_prefixes = s }-*/ ;
-
-  public final native void signedOffBy(boolean s) /*-{ this.signed_off_by = s }-*/;
-
-  public final void reviewCategoryStrategy(ReviewCategoryStrategy s) {
-    reviewCategoryStrategyRaw(s != null ? s.toString() : null);
-  }
-
-  private native void reviewCategoryStrategyRaw(String s)
-      /*-{ this.review_category_strategy = s }-*/ ;
-
-  public final void diffView(DiffView d) {
-    diffViewRaw(d != null ? d.toString() : null);
-  }
-
-  private native void diffViewRaw(String d) /*-{ this.diff_view = d }-*/;
-
-  public final void emailStrategy(EmailStrategy s) {
-    emailStrategyRaw(s != null ? s.toString() : null);
-  }
-
-  private native void emailStrategyRaw(String s) /*-{ this.email_strategy = s }-*/;
-
-  public final void emailFormat(EmailFormat f) {
-    emailFormatRaw(f != null ? f.toString() : null);
-  }
-
-  private native void emailFormatRaw(String s) /*-{ this.email_format = s }-*/;
-
-  public final void defaultBaseForMerges(DefaultBase b) {
-    defaultBaseForMergesRaw(b != null ? b.toString() : null);
-  }
-
-  private native void defaultBaseForMergesRaw(String b) /*-{ this.default_base_for_merges = b }-*/;
-
-  public final native void publishCommentsOnPush(
-      boolean p) /*-{ this.publish_comments_on_push = p }-*/;
-
-  public final native void workInProgressByDefault(
-      boolean p) /*-{ this.work_in_progress_by_default = p }-*/;
-
-  public final void setMyMenus(List<TopMenuItem> myMenus) {
-    initMy();
-    for (TopMenuItem n : myMenus) {
-      addMy(n);
-    }
-  }
-
-  final native void initMy() /*-{ this.my = []; }-*/;
-
-  final native void addMy(TopMenuItem m) /*-{ this.my.push(m); }-*/;
-
-  public final Map<String, String> urlAliases() {
-    Map<String, String> urlAliases = new HashMap<>();
-    for (String k : Natives.keys(_urlAliases())) {
-      urlAliases.put(k, urlAliasToken(k));
-    }
-    return urlAliases;
-  }
-
-  private native String urlAliasToken(String m) /*-{ return this.url_aliases[m]; }-*/;
-
-  private native NativeMap<NativeString> _urlAliases() /*-{ return this.url_aliases; }-*/;
-
-  public final void setUrlAliases(Map<String, String> urlAliases) {
-    initUrlAliases();
-    for (Map.Entry<String, String> e : urlAliases.entrySet()) {
-      putUrlAlias(e.getKey(), e.getValue());
-    }
-  }
-
-  private native void putUrlAlias(String m, String t) /*-{ this.url_aliases[m] = t; }-*/;
-
-  private native void initUrlAliases() /*-{ this.url_aliases = {}; }-*/;
-
-  protected GeneralPreferences() {}
-}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GerritInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GerritInfo.java
deleted file mode 100644
index 78ca417..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GerritInfo.java
+++ /dev/null
@@ -1,70 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.info;
-
-import com.google.gerrit.extensions.client.UiType;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArrayString;
-import java.util.ArrayList;
-import java.util.List;
-
-public class GerritInfo extends JavaScriptObject {
-  public final Project.NameKey allProjectsNameKey() {
-    return new Project.NameKey(allProjects());
-  }
-
-  public final boolean isAllProjects(Project.NameKey p) {
-    return allProjectsNameKey().equals(p);
-  }
-
-  public final Project.NameKey allUsersNameKey() {
-    return new Project.NameKey(allUsers());
-  }
-
-  public final boolean isAllUsers(Project.NameKey p) {
-    return allUsersNameKey().equals(p);
-  }
-
-  public final native String allProjects() /*-{ return this.all_projects; }-*/;
-
-  public final native String allUsers() /*-{ return this.all_users; }-*/;
-
-  public final native boolean docSearch() /*-{ return this.doc_search; }-*/;
-
-  public final native String docUrl() /*-{ return this.doc_url; }-*/;
-
-  public final native boolean editGpgKeys() /*-{ return this.edit_gpg_keys || false; }-*/;
-
-  public final native String reportBugUrl() /*-{ return this.report_bug_url; }-*/;
-
-  public final native String reportBugText() /*-{ return this.report_bug_text; }-*/;
-
-  private native JsArrayString _webUis() /*-{ return this.web_uis; }-*/;
-
-  public final List<UiType> webUis() {
-    JsArrayString webUis = _webUis();
-    List<UiType> result = new ArrayList<>(webUis.length());
-    for (int i = 0; i < webUis.length(); i++) {
-      UiType t = UiType.parse(webUis.get(i));
-      if (t != null) {
-        result.add(t);
-      }
-    }
-    return result;
-  }
-
-  protected GerritInfo() {}
-}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GpgKeyInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GpgKeyInfo.java
deleted file mode 100644
index fd4fde7..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GpgKeyInfo.java
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.info;
-
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArrayString;
-
-public class GpgKeyInfo extends JavaScriptObject {
-  public enum Status {
-    BAD,
-    OK,
-    TRUSTED;
-  }
-
-  public final native String id() /*-{ return this.id; }-*/;
-
-  public final native String fingerprint() /*-{ return this.fingerprint; }-*/;
-
-  public final native JsArrayString userIds() /*-{ return this.user_ids; }-*/;
-
-  public final native String key() /*-{ return this.key; }-*/;
-
-  private native String statusRaw() /*-{ return this.status; }-*/;
-
-  public final Status status() {
-    String s = statusRaw();
-    if (s == null) {
-      return null;
-    }
-    return Status.valueOf(s);
-  }
-
-  public final native boolean hasProblems() /*-{ return this.hasOwnProperty('problems'); }-*/;
-
-  public final native JsArrayString problems() /*-{ return this.problems; }-*/;
-
-  protected GpgKeyInfo() {}
-}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GroupBaseInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GroupBaseInfo.java
deleted file mode 100644
index 94905c0..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GroupBaseInfo.java
+++ /dev/null
@@ -1,31 +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.client.info;
-
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.http.client.URL;
-
-public class GroupBaseInfo extends JavaScriptObject {
-  public final AccountGroup.UUID getGroupUUID() {
-    return new AccountGroup.UUID(URL.decodeQueryString(id()));
-  }
-
-  public final native String id() /*-{ return this.id; }-*/;
-
-  public final native String name() /*-{ return this.name; }-*/;
-
-  protected GroupBaseInfo() {}
-}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GroupInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GroupInfo.java
deleted file mode 100644
index 9bf3411a..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GroupInfo.java
+++ /dev/null
@@ -1,67 +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.client.info;
-
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.http.client.URL;
-
-public class GroupInfo extends GroupBaseInfo {
-  public final AccountGroup.Id getGroupId() {
-    return new AccountGroup.Id(group_id());
-  }
-
-  public final native GroupOptionsInfo options() /*-{ return this.options; }-*/;
-
-  public final native String description() /*-{ return this.description; }-*/;
-
-  public final native String url() /*-{ return this.url; }-*/;
-
-  public final native String owner() /*-{ return this.owner; }-*/;
-
-  public final native void owner(String o) /*-{ if(o)this.owner=o; }-*/;
-
-  public final native JsArray<AccountInfo> members() /*-{ return this.members; }-*/;
-
-  public final native JsArray<GroupInfo> includes() /*-{ return this.includes; }-*/;
-
-  private native int group_id() /*-{ return this.group_id; }-*/;
-
-  private native String owner_id() /*-{ return this.owner_id; }-*/;
-
-  private native void owner_id(String o) /*-{ if(o)this.owner_id=o; }-*/;
-
-  public final AccountGroup.UUID getOwnerUUID() {
-    String owner = owner_id();
-    if (owner != null) {
-      return new AccountGroup.UUID(URL.decodeQueryString(owner));
-    }
-    return null;
-  }
-
-  public final void setOwnerUUID(AccountGroup.UUID uuid) {
-    owner_id(URL.encodeQueryString(uuid.get()));
-  }
-
-  protected GroupInfo() {}
-
-  public static class GroupOptionsInfo extends JavaScriptObject {
-    public final native boolean
-        isVisibleToAll() /*-{ return this['visible_to_all'] ? true : false; }-*/;
-
-    protected GroupOptionsInfo() {}
-  }
-}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/OAuthTokenInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/OAuthTokenInfo.java
deleted file mode 100644
index d96adaa..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/OAuthTokenInfo.java
+++ /dev/null
@@ -1,34 +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.client.info;
-
-import com.google.gwt.core.client.JavaScriptObject;
-
-public class OAuthTokenInfo extends JavaScriptObject {
-
-  protected OAuthTokenInfo() {}
-
-  public final native String username() /*-{ return this.username; }-*/;
-
-  public final native String resourceHost() /*-{ return this.resource_host; }-*/;
-
-  public final native String accessToken() /*-{ return this.access_token; }-*/;
-
-  public final native String providerId() /*-{ return this.provider_id; }-*/;
-
-  public final native String expiresAt() /*-{ return this.expires_at; }-*/;
-
-  public final native String type() /*-{ return this.type; }-*/;
-}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/PushCertificateInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/PushCertificateInfo.java
deleted file mode 100644
index fb5d932..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/PushCertificateInfo.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.info;
-
-import com.google.gwt.core.client.JavaScriptObject;
-
-public class PushCertificateInfo extends JavaScriptObject {
-  public final native String certificate() /*-{ return this.certificate; }-*/;
-
-  public final native GpgKeyInfo key() /*-{ return this.key; }-*/;
-
-  protected PushCertificateInfo() {}
-}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ServerInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ServerInfo.java
deleted file mode 100644
index d3274e6..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ServerInfo.java
+++ /dev/null
@@ -1,113 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.info;
-
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.NativeString;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArrayString;
-import java.util.HashMap;
-import java.util.Map;
-
-public class ServerInfo extends JavaScriptObject {
-  public final native AuthInfo auth() /*-{ return this.auth; }-*/;
-
-  public final native ChangeConfigInfo change() /*-{ return this.change; }-*/;
-
-  public final native DownloadInfo download() /*-{ return this.download; }-*/;
-
-  public final native GerritInfo gerrit() /*-{ return this.gerrit; }-*/;
-
-  public final native PluginConfigInfo plugin() /*-{ return this.plugin; }-*/;
-
-  public final native SshdInfo sshd() /*-{ return this.sshd; }-*/;
-
-  public final native SuggestInfo suggest() /*-{ return this.suggest; }-*/;
-
-  public final native UserConfigInfo user() /*-{ return this.user; }-*/;
-
-  public final native ReceiveInfo receive() /*-{ return this.receive; }-*/;
-
-  public final Map<String, String> urlAliases() {
-    Map<String, String> urlAliases = new HashMap<>();
-    for (String k : Natives.keys(_urlAliases())) {
-      urlAliases.put(k, urlAliasToken(k));
-    }
-    return urlAliases;
-  }
-
-  public final native String urlAliasToken(String n) /*-{ return this.url_aliases[n]; }-*/;
-
-  private native NativeMap<NativeString> _urlAliases() /*-{ return this.url_aliases; }-*/;
-
-  public final boolean hasSshd() {
-    return sshd() != null;
-  }
-
-  protected ServerInfo() {}
-
-  public static class ChangeConfigInfo extends JavaScriptObject {
-    public final native boolean allowBlame() /*-{ return this.allow_blame || false; }-*/;
-
-    public final native int largeChange() /*-{ return this.large_change || 0; }-*/;
-
-    public final native String replyLabel() /*-{ return this.reply_label; }-*/;
-
-    public final native String replyTooltip() /*-{ return this.reply_tooltip; }-*/;
-
-    public final native boolean
-        showAssigneeInChangesTable() /*-{ return this.show_assignee_in_changes_table || false; }-*/;
-
-    public final native int updateDelay() /*-{ return this.update_delay || 0; }-*/;
-
-    public final native boolean isSubmitWholeTopicEnabled() /*-{
-        return this.submit_whole_topic; }-*/;
-
-    protected ChangeConfigInfo() {}
-  }
-
-  public static class PluginConfigInfo extends JavaScriptObject {
-    public final native boolean hasAvatars() /*-{ return this.has_avatars || false; }-*/;
-
-    public final native JsArrayString jsResourcePaths() /*-{
-        return this.js_resource_paths || []; }-*/;
-
-    protected PluginConfigInfo() {}
-  }
-
-  public static class SshdInfo extends JavaScriptObject {
-    protected SshdInfo() {}
-  }
-
-  public static class SuggestInfo extends JavaScriptObject {
-    public final native int from() /*-{ return this.from || 0; }-*/;
-
-    protected SuggestInfo() {}
-  }
-
-  public static class UserConfigInfo extends JavaScriptObject {
-    public final native String anonymousCowardName() /*-{ return this.anonymous_coward_name; }-*/;
-
-    protected UserConfigInfo() {}
-  }
-
-  public static class ReceiveInfo extends JavaScriptObject {
-    public final native boolean enableSignedPush()
-        /*-{ return this.enable_signed_push || false; }-*/ ;
-
-    protected ReceiveInfo() {}
-  }
-}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/TopMenu.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/TopMenu.java
deleted file mode 100644
index 7e25af2..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/TopMenu.java
+++ /dev/null
@@ -1,27 +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.client.info;
-
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
-
-public class TopMenu extends JavaScriptObject {
-
-  protected TopMenu() {}
-
-  public final native String getName() /*-{ return this.name; }-*/;
-
-  public final native JsArray<TopMenuItem> getItems() /*-{ return this.items; }-*/;
-}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/TopMenuItem.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/TopMenuItem.java
deleted file mode 100644
index 3a286a2..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/TopMenuItem.java
+++ /dev/null
@@ -1,40 +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.client.info;
-
-import com.google.gwt.core.client.JavaScriptObject;
-
-public class TopMenuItem extends JavaScriptObject {
-  public static TopMenuItem create(String name, String url) {
-    TopMenuItem i = createObject().cast();
-    i.name(name);
-    i.url(url);
-    return i;
-  }
-
-  public final native String getName() /*-{ return this.name; }-*/;
-
-  public final native String getUrl() /*-{ return this.url; }-*/;
-
-  public final native String getTarget() /*-{ return this.target; }-*/;
-
-  public final native String getId() /*-{ return this.id; }-*/;
-
-  public final native void name(String n) /*-{ this.name = n }-*/;
-
-  public final native void url(String u) /*-{ this.url = u }-*/;
-
-  protected TopMenuItem() {}
-}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/TopMenuList.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/TopMenuList.java
deleted file mode 100644
index d7df1f7..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/TopMenuList.java
+++ /dev/null
@@ -1,22 +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.client.info;
-
-import com.google.gwt.core.client.JsArray;
-
-public class TopMenuList extends JsArray<TopMenu> {
-
-  protected TopMenuList() {}
-}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/WebLinkInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/WebLinkInfo.java
deleted file mode 100644
index bcf3dde..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/WebLinkInfo.java
+++ /dev/null
@@ -1,50 +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.client.info;
-
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.Image;
-
-public class WebLinkInfo extends JavaScriptObject {
-
-  public final native String name() /*-{ return this.name; }-*/;
-
-  public final native String imageUrl() /*-{ return this.image_url; }-*/;
-
-  public final native String url() /*-{ return this.url; }-*/;
-
-  public final native String target() /*-{ return this.target; }-*/;
-
-  protected WebLinkInfo() {}
-
-  public final Anchor toAnchor() {
-    Anchor a = new Anchor();
-    a.setHref(url());
-    if (target() != null && !target().isEmpty()) {
-      a.setTarget(target());
-    }
-    if (imageUrl() != null && !imageUrl().isEmpty()) {
-      Image img = new Image();
-      img.setAltText(name());
-      img.setUrl(imageUrl());
-      img.setTitle(name());
-      a.getElement().appendChild(img.getElement());
-    } else {
-      a.setText("(" + name() + ")");
-    }
-    return a;
-  }
-}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/NativeMap.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/NativeMap.java
deleted file mode 100644
index 41306ff..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/NativeMap.java
+++ /dev/null
@@ -1,101 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.rpc;
-
-import static java.util.stream.Collectors.toCollection;
-
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
-
-/** A map of native JSON objects, keyed by a string. */
-public class NativeMap<T extends JavaScriptObject> extends JavaScriptObject {
-  public static <T extends JavaScriptObject> NativeMap<T> create() {
-    return createObject().cast();
-  }
-
-  /**
-   * Loop through the result map's entries and copy the key strings into the "name" property of the
-   * corresponding child object. This only runs on the top level map of the result, and requires the
-   * children to be JSON objects and not a JSON primitive (e.g. boolean or string).
-   */
-  public static <T extends JavaScriptObject, M extends NativeMap<T>>
-      AsyncCallback<M> copyKeysIntoChildren(AsyncCallback<M> callback) {
-    return copyKeysIntoChildren("name", callback);
-  }
-
-  /** Loop through the result map and set asProperty on the children. */
-  public static <T extends JavaScriptObject, M extends NativeMap<T>>
-      AsyncCallback<M> copyKeysIntoChildren(String asProperty, AsyncCallback<M> callback) {
-    return new TransformCallback<M, M>(callback) {
-      @Override
-      protected M transform(M result) {
-        result.copyKeysIntoChildren(asProperty);
-        return result;
-      }
-    };
-  }
-
-  protected NativeMap() {}
-
-  public final Set<String> keySet() {
-    return Natives.keys(this);
-  }
-
-  public final List<String> sortedKeys() {
-    return keySet().stream().sorted().collect(toCollection(ArrayList::new));
-  }
-
-  public final native JsArray<T> values() /*-{
-    var s = this;
-    var v = [];
-    var i = 0;
-    for (var k in s) {
-      if (s.hasOwnProperty(k)) {
-        v[i++] = s[k];
-      }
-    }
-    return v;
-  }-*/;
-
-  public final int size() {
-    return keySet().size();
-  }
-
-  public final boolean isEmpty() {
-    return size() == 0;
-  }
-
-  public final boolean containsKey(String n) {
-    return get(n) != null;
-  }
-
-  public final native T get(String n) /*-{ return this[n]; }-*/;
-
-  public final native void put(String n, T v) /*-{ this[n] = v; }-*/;
-
-  public final native void copyKeysIntoChildren(String p) /*-{
-    var s = this;
-    for (var k in s) {
-      if (s.hasOwnProperty(k)) {
-        var c = s[k];
-        c[p] = k;
-      }
-    }
-  }-*/;
-}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/NativeString.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/NativeString.java
deleted file mode 100644
index e0bca0e..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/NativeString.java
+++ /dev/null
@@ -1,63 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.rpc;
-
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-
-/** Wraps a String that was returned from a JSON API. */
-public final class NativeString extends JavaScriptObject {
-  public static final JavaScriptObject TYPE = init();
-
-  // Used from core and plugins
-  private static native JavaScriptObject init() /*-{
-    if ($wnd.Gerrit === undefined || $wnd.Gerrit.JsonString === undefined) {
-      return function(s){this.s=s};
-    } else {
-      return $wnd.Gerrit.JsonString;
-    }
-  }-*/;
-
-  static NativeString wrap(String s) {
-    return wrap0(TYPE, s);
-  }
-
-  private static native NativeString wrap0(JavaScriptObject T, String s) /*-{ return new T(s) }-*/;
-
-  public native String asString() /*-{ return this.s; }-*/;
-
-  public static AsyncCallback<NativeString> unwrap(AsyncCallback<String> cb) {
-    return new AsyncCallback<NativeString>() {
-      @Override
-      public void onSuccess(NativeString result) {
-        cb.onSuccess(result != null ? result.asString() : null);
-      }
-
-      @Override
-      public void onFailure(Throwable caught) {
-        cb.onFailure(caught);
-      }
-    };
-  }
-
-  public static boolean is(JavaScriptObject o) {
-    return is(TYPE, o);
-  }
-
-  private static native boolean is(JavaScriptObject T, JavaScriptObject o)
-      /*-{ return o instanceof T }-*/ ;
-
-  protected NativeString() {}
-}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/Natives.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/Natives.java
deleted file mode 100644
index 1421386..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/Natives.java
+++ /dev/null
@@ -1,107 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.rpc;
-
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.core.client.JsArrayString;
-import com.google.gwt.json.client.JSONObject;
-import java.util.AbstractList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Set;
-
-public class Natives {
-  /**
-   * Get the names of defined properties on the object. The returned set iterates in the native
-   * iteration order, which may match the source order.
-   */
-  public static Set<String> keys(JavaScriptObject obj) {
-    if (obj != null) {
-      return new JSONObject(obj).keySet();
-    }
-    return Collections.emptySet();
-  }
-
-  public static List<String> asList(JsArrayString arr) {
-    if (arr == null) {
-      return null;
-    }
-    return new AbstractList<String>() {
-      @Override
-      public String set(int index, String element) {
-        String old = arr.get(index);
-        arr.set(index, element);
-        return old;
-      }
-
-      @Override
-      public String get(int index) {
-        return arr.get(index);
-      }
-
-      @Override
-      public int size() {
-        return arr.length();
-      }
-    };
-  }
-
-  public static <T extends JavaScriptObject> List<T> asList(JsArray<T> arr) {
-    if (arr == null) {
-      return null;
-    }
-    return new AbstractList<T>() {
-      @Override
-      public T set(int index, T element) {
-        T old = arr.get(index);
-        arr.set(index, element);
-        return old;
-      }
-
-      @Override
-      public T get(int index) {
-        return arr.get(index);
-      }
-
-      @Override
-      public int size() {
-        return arr.length();
-      }
-    };
-  }
-
-  public static <T extends JavaScriptObject> JsArray<T> arrayOf(T element) {
-    JsArray<T> arr = JavaScriptObject.createArray().cast();
-    arr.push(element);
-    return arr;
-  }
-
-  public static JsArrayString arrayOf(Iterable<String> elements) {
-    JsArrayString arr = JavaScriptObject.createArray().cast();
-    for (String elem : elements) {
-      arr.push(elem);
-    }
-    return arr;
-  }
-
-  public static JsArrayString arrayOf(String element) {
-    JsArrayString arr = JavaScriptObject.createArray().cast();
-    arr.push(element);
-    return arr;
-  }
-
-  private Natives() {}
-}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/TransformCallback.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/TransformCallback.java
deleted file mode 100644
index 00e6bb1..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/TransformCallback.java
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.rpc;
-
-import com.google.gwt.user.client.rpc.AsyncCallback;
-
-/** Transforms a value and passes it on to another callback. */
-public abstract class TransformCallback<I, O> implements AsyncCallback<I> {
-  private final AsyncCallback<O> callback;
-
-  protected TransformCallback(AsyncCallback<O> callback) {
-    this.callback = callback;
-  }
-
-  @Override
-  public void onSuccess(I result) {
-    callback.onSuccess(transform(result));
-  }
-
-  @Override
-  public void onFailure(Throwable caught) {
-    callback.onFailure(caught);
-  }
-
-  protected abstract O transform(I result);
-}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/ui/HighlightSuggestion.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/ui/HighlightSuggestion.java
deleted file mode 100644
index 513f570..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/ui/HighlightSuggestion.java
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.ui;
-
-import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
-import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
-
-/** A {@code Suggestion} with highlights. */
-public class HighlightSuggestion implements Suggestion {
-
-  private final String keyword;
-  private final String value;
-
-  public HighlightSuggestion(String keyword, String value) {
-    this.keyword = keyword;
-    this.value = value;
-  }
-
-  @Override
-  public String getDisplayString() {
-    int start = 0;
-    int keyLen = keyword.length();
-    SafeHtmlBuilder builder = new SafeHtmlBuilder();
-    for (; ; ) {
-      int index = value.indexOf(keyword, start);
-      if (index == -1) {
-        builder.appendEscaped(value.substring(start));
-        break;
-      }
-      builder.appendEscaped(value.substring(start, index));
-      builder.appendHtmlConstant("<strong>");
-      start = index + keyLen;
-      builder.appendEscaped(value.substring(index, start));
-      builder.appendHtmlConstant("</strong>");
-    }
-    return builder.toSafeHtml().asString();
-  }
-
-  @Override
-  public String getReplacementString() {
-    return value;
-  }
-}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/ui/RemoteSuggestOracle.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/ui/RemoteSuggestOracle.java
deleted file mode 100644
index d66d6a6..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/ui/RemoteSuggestOracle.java
+++ /dev/null
@@ -1,129 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT 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.client.ui;
-
-import com.google.gwt.user.client.Timer;
-import com.google.gwt.user.client.ui.SuggestOracle;
-
-/**
- * Delegates to a slow SuggestOracle, such as a remote server API.
- *
- * <p>A response is only supplied to the UI if no requests were made after the oracle begin that
- * request.
- *
- * <p>When a request is made while the delegate is still processing a prior request all intermediate
- * requests are discarded and the most recent request is queued. The pending request's response is
- * discarded and the most recent request is started.
- */
-public class RemoteSuggestOracle extends SuggestOracle {
-  private final SuggestOracle oracle;
-  private Query query;
-  private String last;
-  private Timer requestRetentionTimer;
-  private boolean cancelOutstandingRequest;
-
-  private boolean serveSuggestions;
-
-  public RemoteSuggestOracle(SuggestOracle src) {
-    oracle = src;
-  }
-
-  public String getLast() {
-    return last;
-  }
-
-  @Override
-  public void requestSuggestions(Request req, Callback cb) {
-    if (!serveSuggestions) {
-      return;
-    }
-
-    // Use a timer for key stroke retention, such that we don't query the
-    // backend for each and every keystroke we receive.
-    if (requestRetentionTimer != null) {
-      requestRetentionTimer.cancel();
-    }
-    requestRetentionTimer =
-        new Timer() {
-          @Override
-          public void run() {
-            Query q = new Query(req, cb);
-            if (query == null) {
-              query = q;
-              q.start();
-            } else {
-              query = q;
-            }
-          }
-        };
-    requestRetentionTimer.schedule(200);
-  }
-
-  @Override
-  public void requestDefaultSuggestions(Request req, Callback cb) {
-    requestSuggestions(req, cb);
-  }
-
-  @Override
-  public boolean isDisplayStringHTML() {
-    return oracle.isDisplayStringHTML();
-  }
-
-  public void cancelOutstandingRequest() {
-    if (requestRetentionTimer != null) {
-      requestRetentionTimer.cancel();
-    }
-    if (query != null) {
-      cancelOutstandingRequest = true;
-    }
-  }
-
-  public void setServeSuggestions(boolean serveSuggestions) {
-    this.serveSuggestions = serveSuggestions;
-  }
-
-  private class Query implements Callback {
-    final Request request;
-    final Callback callback;
-
-    Query(Request req, Callback cb) {
-      request = req;
-      callback = cb;
-    }
-
-    void start() {
-      oracle.requestSuggestions(request, this);
-    }
-
-    @Override
-    public void onSuggestionsReady(Request req, Response res) {
-      if (cancelOutstandingRequest || !serveSuggestions) {
-        // If cancelOutstandingRequest() was called, we ignore this response
-        cancelOutstandingRequest = false;
-        query = null;
-      } else if (query == this) {
-        // No new request was started while this query was running.
-        // Propose this request's response as the suggestions.
-        query = null;
-        last = request.getQuery();
-        callback.onSuggestionsReady(req, res);
-      } else {
-        // Another query came in while this one was running. Skip
-        // this response and start the most recent query.
-        query.start();
-      }
-    }
-  }
-}
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/arrow_undo.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/arrow_undo.png
deleted file mode 100644
index 6972c5e..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/arrow_undo.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/cog.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/cog.png
deleted file mode 100644
index 67de2c6..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/cog.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/comment_draft.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/comment_draft.png
deleted file mode 100644
index 3408ddf..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/comment_draft.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/cross.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/cross.png
deleted file mode 100644
index 1514d51..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/cross.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/diffy26.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/diffy26.png
deleted file mode 100644
index 88b59d8..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/diffy26.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/disk.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/disk.png
deleted file mode 100644
index 99d532e..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/disk.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/exclamation.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/exclamation.png
deleted file mode 100644
index c37bd06..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/exclamation.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/find.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/find.png
deleted file mode 100644
index 1547479..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/find.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/goDown.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/goDown.png
deleted file mode 100644
index 5d87e45..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/goDown.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/goNext.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/goNext.png
deleted file mode 100644
index 872c197..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/goNext.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/goPrev.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/goPrev.png
deleted file mode 100644
index d68f29b..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/goPrev.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/goUp.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/goUp.png
deleted file mode 100644
index f75bed4..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/goUp.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/help.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/help.png
deleted file mode 100644
index 5c87017..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/help.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/lightbulb.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/lightbulb.png
deleted file mode 100644
index d22fde8..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/lightbulb.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/listAdd.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/listAdd.png
deleted file mode 100644
index 1aa7f09..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/listAdd.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/lock.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/lock.png
deleted file mode 100644
index 2ebc4f6..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/lock.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/merge.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/merge.png
deleted file mode 100644
index 9c892db..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/merge.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/note_add.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/note_add.png
deleted file mode 100644
index abdad91..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/note_add.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/page_edit.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/page_edit.png
deleted file mode 100644
index 046811e..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/page_edit.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/page_white_put.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/page_white_put.png
deleted file mode 100644
index 884ffd6..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/page_white_put.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/resultset_down_gray.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/resultset_down_gray.png
deleted file mode 100644
index 7bdd8ea..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/resultset_down_gray.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/resultset_next_gray.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/resultset_next_gray.png
deleted file mode 100644
index 3049ef4..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/resultset_next_gray.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/resultset_up_gray.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/resultset_up_gray.png
deleted file mode 100644
index 966a25e..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/resultset_up_gray.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/sideBySideDiff.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/sideBySideDiff.png
deleted file mode 100755
index ee70080..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/sideBySideDiff.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/star-open.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/star-open.png
deleted file mode 100644
index edd577c..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/star-open.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/star.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/star.png
deleted file mode 100644
index b88c857..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/star.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/tag_blue.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/tag_blue.png
deleted file mode 100644
index 9757fc6..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/tag_blue.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/tag_blue_add.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/tag_blue_add.png
deleted file mode 100644
index f135248..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/tag_blue_add.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/tick.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/tick.png
deleted file mode 100644
index a9925a0..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/tick.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/unifiedDiff.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/unifiedDiff.png
deleted file mode 100755
index ec5f97a..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/unifiedDiff.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/user_add.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/user_add.png
deleted file mode 100644
index deae99b..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/user_add.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/user_edit.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/user_edit.png
deleted file mode 100644
index c1974cd..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/user_edit.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/test/java/com/google/gerrit/client/RelativeDateFormatterTest.java b/gerrit-gwtui-common/src/test/java/com/google/gerrit/client/RelativeDateFormatterTest.java
deleted file mode 100644
index 6915ba7..0000000
--- a/gerrit-gwtui-common/src/test/java/com/google/gerrit/client/RelativeDateFormatterTest.java
+++ /dev/null
@@ -1,216 +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.client;
-
-import static com.google.gerrit.client.RelativeDateFormatter.DAY_IN_MILLIS;
-import static com.google.gerrit.client.RelativeDateFormatter.HOUR_IN_MILLIS;
-import static com.google.gerrit.client.RelativeDateFormatter.MINUTE_IN_MILLIS;
-import static com.google.gerrit.client.RelativeDateFormatter.SECOND_IN_MILLIS;
-import static com.google.gerrit.client.RelativeDateFormatter.YEAR_IN_MILLIS;
-import static org.junit.Assert.assertEquals;
-
-import java.util.Date;
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
-import org.junit.Test;
-
-public class RelativeDateFormatterTest {
-
-  @BeforeClass
-  public static void setConstants() {
-    Constants c = new Constants();
-    RelativeDateFormatter.setConstants(c, c);
-  }
-
-  @AfterClass
-  public static void unsetConstants() {
-    RelativeDateFormatter.setConstants(null, null);
-  }
-
-  private static void assertFormat(long ageFromNow, long timeUnit, String expectedFormat) {
-    Date d = new Date(System.currentTimeMillis() - ageFromNow * timeUnit);
-    String s = RelativeDateFormatter.format(d);
-    assertEquals(expectedFormat, s);
-  }
-
-  @Test
-  public void future() {
-    assertFormat(-100, YEAR_IN_MILLIS, "in the future");
-    assertFormat(-1, SECOND_IN_MILLIS, "in the future");
-  }
-
-  @Test
-  public void formatSeconds() {
-    assertFormat(1, SECOND_IN_MILLIS, "1 second ago");
-    assertFormat(89, SECOND_IN_MILLIS, "89 seconds ago");
-  }
-
-  @Test
-  public void formatMinutes() {
-    assertFormat(90, SECOND_IN_MILLIS, "2 minutes ago");
-    assertFormat(3, MINUTE_IN_MILLIS, "3 minutes ago");
-    assertFormat(60, MINUTE_IN_MILLIS, "60 minutes ago");
-    assertFormat(89, MINUTE_IN_MILLIS, "89 minutes ago");
-  }
-
-  @Test
-  public void formatHours() {
-    assertFormat(90, MINUTE_IN_MILLIS, "2 hours ago");
-    assertFormat(149, MINUTE_IN_MILLIS, "2 hours ago");
-    assertFormat(35, HOUR_IN_MILLIS, "35 hours ago");
-  }
-
-  @Test
-  public void formatDays() {
-    assertFormat(36, HOUR_IN_MILLIS, "2 days ago");
-    assertFormat(13, DAY_IN_MILLIS, "13 days ago");
-  }
-
-  @Test
-  public void formatWeeks() {
-    assertFormat(14, DAY_IN_MILLIS, "2 weeks ago");
-    assertFormat(69, DAY_IN_MILLIS, "10 weeks ago");
-  }
-
-  @Test
-  public void formatMonths() {
-    assertFormat(70, DAY_IN_MILLIS, "2 months ago");
-    assertFormat(75, DAY_IN_MILLIS, "3 months ago");
-    assertFormat(364, DAY_IN_MILLIS, "12 months ago");
-  }
-
-  @Test
-  public void formatYearsMonths() {
-    assertFormat(366, DAY_IN_MILLIS, "1 year ago");
-    assertFormat(380, DAY_IN_MILLIS, "1 year, 1 month ago");
-    assertFormat(410, DAY_IN_MILLIS, "1 year, 2 months ago");
-    assertFormat(2, YEAR_IN_MILLIS, "2 years ago");
-    assertFormat(1824, DAY_IN_MILLIS, "5 years ago");
-    assertFormat(2 * 365 - 10, DAY_IN_MILLIS, "2 years ago");
-  }
-
-  @Test
-  public void formatYears() {
-    assertFormat(5, YEAR_IN_MILLIS, "5 years ago");
-    assertFormat(60, YEAR_IN_MILLIS, "60 years ago");
-  }
-
-  private static class Constants implements CommonConstants, CommonMessages {
-    @Override
-    public String inTheFuture() {
-      return "in the future";
-    }
-
-    @Override
-    public String month() {
-      return "month";
-    }
-
-    @Override
-    public String months() {
-      return "months";
-    }
-
-    @Override
-    public String year() {
-      return "year";
-    }
-
-    @Override
-    public String years() {
-      return "years";
-    }
-
-    @Override
-    public String oneSecondAgo() {
-      return "1 second ago";
-    }
-
-    @Override
-    public String oneMinuteAgo() {
-      return "1 minute ago";
-    }
-
-    @Override
-    public String oneHourAgo() {
-      return "1 hour ago";
-    }
-
-    @Override
-    public String oneDayAgo() {
-      return "1 day ago";
-    }
-
-    @Override
-    public String oneWeekAgo() {
-      return "1 week ago";
-    }
-
-    @Override
-    public String oneMonthAgo() {
-      return "1 month ago";
-    }
-
-    @Override
-    public String oneYearAgo() {
-      return "1 year ago";
-    }
-
-    @Override
-    public String secondsAgo(long seconds) {
-      return seconds + " seconds ago";
-    }
-
-    @Override
-    public String minutesAgo(long minutes) {
-      return minutes + " minutes ago";
-    }
-
-    @Override
-    public String hoursAgo(long hours) {
-      return hours + " hours ago";
-    }
-
-    @Override
-    public String daysAgo(long days) {
-      return days + " days ago";
-    }
-
-    @Override
-    public String weeksAgo(long weeks) {
-      return weeks + " weeks ago";
-    }
-
-    @Override
-    public String monthsAgo(long months) {
-      return months + " months ago";
-    }
-
-    @Override
-    public String yearsAgo(long years) {
-      return years + " years ago";
-    }
-
-    @Override
-    public String years0MonthsAgo(long years, String yearLabel) {
-      return years + " " + yearLabel + " ago";
-    }
-
-    @Override
-    public String yearsMonthsAgo(long years, String yearLabel, long months, String monthLabel) {
-      return years + " " + yearLabel + ", " + months + " " + monthLabel + " ago";
-    }
-  }
-}
diff --git a/gerrit-gwtui-common/src/test/java/com/google/gerrit/client/ui/HighlightSuggestionTest.java b/gerrit-gwtui-common/src/test/java/com/google/gerrit/client/ui/HighlightSuggestionTest.java
deleted file mode 100644
index 9bf2d95..0000000
--- a/gerrit-gwtui-common/src/test/java/com/google/gerrit/client/ui/HighlightSuggestionTest.java
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.ui;
-
-import static org.junit.Assert.assertEquals;
-
-import org.junit.Test;
-
-public class HighlightSuggestionTest {
-
-  @Test
-  public void singleHighlight() throws Exception {
-    String keyword = "key";
-    String value = "somethingkeysomething";
-    HighlightSuggestion suggestion = new HighlightSuggestion(keyword, value);
-    assertEquals("something<strong>key</strong>something", suggestion.getDisplayString());
-    assertEquals(value, suggestion.getReplacementString());
-  }
-
-  @Test
-  public void noHighlight() throws Exception {
-    String keyword = "key";
-    String value = "something";
-    HighlightSuggestion suggestion = new HighlightSuggestion(keyword, value);
-    assertEquals(value, suggestion.getDisplayString());
-    assertEquals(value, suggestion.getReplacementString());
-  }
-
-  @Test
-  public void doubleHighlight() throws Exception {
-    String keyword = "key";
-    String value = "somethingkeysomethingkeysomething";
-    HighlightSuggestion suggestion = new HighlightSuggestion(keyword, value);
-    assertEquals(
-        "something<strong>key</strong>something<strong>key</strong>something",
-        suggestion.getDisplayString());
-    assertEquals(value, suggestion.getReplacementString());
-  }
-}
diff --git a/gerrit-gwtui/BUILD b/gerrit-gwtui/BUILD
deleted file mode 100644
index 24718e5..0000000
--- a/gerrit-gwtui/BUILD
+++ /dev/null
@@ -1,55 +0,0 @@
-load(
-    "//tools/bzl:gwt.bzl",
-    "gen_ui_module",
-    "gwt_genrule",
-    "gwt_user_agent_permutations",
-)
-load("//tools/bzl:java.bzl", "java_library2")
-load("//tools/bzl:junit.bzl", "junit_tests")
-load("//tools/bzl:license.bzl", "license_test")
-
-gwt_genrule()
-
-gwt_genrule(suffix = "_r")
-
-gen_ui_module(name = "ui_module")
-
-gen_ui_module(
-    name = "ui_module",
-    suffix = "_r",
-)
-
-gwt_user_agent_permutations()
-
-java_library2(
-    name = "client-lib",
-    srcs = glob(["src/main/**/*.java"]),
-    exported_deps = [":ui_module"],
-    resources = glob(["src/main/**/*"]),
-    visibility = ["//visibility:public"],
-    deps = [
-        "//gerrit-gwtui-common:client-lib",
-        "//lib/gwt:dev",
-        "//lib/gwt:user",
-    ],
-)
-
-license_test(
-    name = "ui_module_license_test",
-    target = ":ui_module",
-)
-
-junit_tests(
-    name = "ui_tests",
-    srcs = glob(["src/test/java/**/*.java"]),
-    visibility = ["//visibility:public"],
-    deps = [
-        ":ui_module",
-        "//java/com/google/gerrit/common:client",
-        "//java/com/google/gerrit/extensions:client",
-        "//lib:junit",
-        "//lib/gwt:dev",
-        "//lib/gwt:user",
-        "//lib/truth",
-    ],
-)
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUI.gwt.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUI.gwt.xml
deleted file mode 100644
index 9ac919b..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUI.gwt.xml
+++ /dev/null
@@ -1,39 +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.
--->
-<module rename-to="gerrit_ui">
-  <inherits name='com.google.gwt.editor.Editor'/>
-  <inherits name='com.google.gwt.user.User'/>
-  <inherits name='com.google.gwt.resources.Resources'/>
-  <inherits name='com.google.gwt.user.theme.chrome.Chrome'/>
-  <inherits name='com.google.gwtexpui.css.CSS'/>
-  <inherits name='com.google.gerrit.GerritGwtUICommon'/>
-  <inherits name='com.google.gerrit.UserAgent'/>
-  <inherits name='net.codemirror.CodeMirror'/>
-
-  <extend-property name='locale' values='en'/>
-  <set-property-fallback name='locale' value='en'/>
-  <set-property name='locale' value='en'/>
-  <set-configuration-property name='UiBinder.useSafeHtmlTemplates' value='true'/>
-  <set-configuration-property name='CssResource.style' value='stable'/>
-  <add-linker name='xsiframe'/>
-
-  <set-property name='gwt.logging.logLevel' value='SEVERE'/>
-
-  <!-- Disable GSS -->
-  <set-configuration-property name='CssResource.enableGss' value='false'/>
-
-  <entry-point class='com.google.gerrit.client.Gerrit'/>
-</module>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/UserAgent.gwt.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/UserAgent.gwt.xml
deleted file mode 100644
index 9644093..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/UserAgent.gwt.xml
+++ /dev/null
@@ -1,41 +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.
--->
-<module>
-  <replace-with class="com.google.gerrit.client.api.PluginName.PluginNameMoz">
-    <when-type-is class="com.google.gerrit.client.api.PluginName" />
-    <when-property-is name="compiler.stackMode" value="native" />
-    <when-property-is name="user.agent" value="safari" />
-    <when-property-is name="user.agent" value="gecko1_8" />
-  </replace-with>
-
-  <replace-with class="com.google.gerrit.client.ui.FancyFlexTableImplIE8">
-    <when-type-is class="com.google.gerrit.client.ui.FancyFlexTableImpl" />
-    <any>
-      <when-property-is name="user.agent" value="ie8"/>
-    </any>
-  </replace-with>
-
-  <replace-with class="com.google.gerrit.client.Themer.ThemerIE">
-    <when-type-is class="com.google.gerrit.client.Themer" />
-    <any>
-      <when-property-is name="user.agent" value="ie8"/>
-      <when-property-is name="user.agent" value="ie9"/>
-      <when-property-is name="user.agent" value="ie10"/>
-      <when-property-is name="user.agent" value="ie11"/>
-      <when-property-is name="user.agent" value="edge"/>
-    </any>
-  </replace-with>
-</module>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
deleted file mode 100644
index 107d663..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
+++ /dev/null
@@ -1,202 +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.client;
-
-import com.google.gerrit.client.changes.Util;
-import com.google.gerrit.client.info.AccountInfo;
-import com.google.gerrit.client.info.AccountInfo.AvatarInfo;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gwt.event.dom.client.LoadEvent;
-import com.google.gwt.event.dom.client.LoadHandler;
-import com.google.gwt.event.dom.client.MouseOutEvent;
-import com.google.gwt.event.dom.client.MouseOutHandler;
-import com.google.gwt.event.dom.client.MouseOverEvent;
-import com.google.gwt.event.dom.client.MouseOverHandler;
-import com.google.gwt.user.client.Timer;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwt.user.client.ui.UIObject;
-
-public class AvatarImage extends Image implements LoadHandler {
-  public AvatarImage() {
-    setVisible(false);
-    addLoadHandler(this);
-  }
-
-  /** A default sized avatar image. */
-  public AvatarImage(AccountInfo account) {
-    this(account, AccountInfo.AvatarInfo.DEFAULT_SIZE, true);
-  }
-
-  /**
-   * An avatar image for the given account using the requested size.
-   *
-   * @param account The account in which we are interested
-   * @param size A requested size. Note that the size can be ignored depending on the avatar
-   *     provider. A size <= 0 indicates to let the provider decide a default size.
-   * @param addPopup show avatar popup with user info on hovering over the avatar image
-   */
-  public AvatarImage(AccountInfo account, int size, boolean addPopup) {
-    addLoadHandler(this);
-    setAccount(account, size, addPopup);
-  }
-
-  public void setAccount(AccountInfo account, int size, boolean addPopup) {
-    if (account == null) {
-      setVisible(false);
-    } else if (isGerritServer(account)) {
-      setVisible(true);
-      setResource(Gerrit.RESOURCES.gerritAvatar26());
-    } else if (account.hasAvatarInfo()) {
-      setVisible(false);
-      AvatarInfo info = account.avatar(size);
-      if (info != null) {
-        setWidth(info.width() > 0 ? info.width() + "px" : "");
-        setHeight(info.height() > 0 ? info.height() + "px" : "");
-        setUrl(info.url());
-        popup(account, addPopup);
-      } else if (account.email() != null) {
-        loadAvatar(account, size, addPopup);
-      }
-    } else if (account.email() != null) {
-      loadAvatar(account, size, addPopup);
-    } else {
-      setVisible(false);
-    }
-  }
-
-  private void loadAvatar(AccountInfo account, int size, boolean addPopup) {
-    if (!Gerrit.info().plugin().hasAvatars()) {
-      setVisible(false);
-      return;
-    }
-
-    // TODO Kill /accounts/*/avatar URL.
-    String u = account.email();
-    if (Gerrit.isSignedIn() && u.equals(Gerrit.getUserAccount().email())) {
-      u = "self";
-    }
-    RestApi api = new RestApi("/accounts/").id(u).view("avatar");
-    if (size > 0) {
-      api.addParameter("s", size);
-      setSize("", size + "px");
-    }
-    setVisible(false);
-    setUrl(api.url());
-    popup(account, addPopup);
-  }
-
-  private void popup(AccountInfo account, boolean addPopup) {
-    if (addPopup) {
-      PopupHandler popupHandler = new PopupHandler(account, this);
-      addMouseOverHandler(popupHandler);
-      addMouseOutHandler(popupHandler);
-    }
-  }
-
-  @Override
-  public void onLoad(LoadEvent event) {
-    setVisible(true);
-  }
-
-  private static boolean isGerritServer(AccountInfo account) {
-    return account._accountId() == 0 && Util.C.messageNoAuthor().equals(account.name());
-  }
-
-  private static class PopupHandler implements MouseOverHandler, MouseOutHandler {
-    private final AccountInfo account;
-    private final UIObject target;
-
-    private UserPopupPanel popup;
-    private Timer showTimer;
-    private Timer hideTimer;
-
-    PopupHandler(AccountInfo account, UIObject target) {
-      this.account = account;
-      this.target = target;
-    }
-
-    private UserPopupPanel createPopupPanel(AccountInfo account) {
-      UserPopupPanel popup = new UserPopupPanel(account, false, false);
-      popup.addDomHandler(
-          new MouseOverHandler() {
-            @Override
-            public void onMouseOver(MouseOverEvent event) {
-              scheduleShow();
-            }
-          },
-          MouseOverEvent.getType());
-      popup.addDomHandler(
-          new MouseOutHandler() {
-            @Override
-            public void onMouseOut(MouseOutEvent event) {
-              scheduleHide();
-            }
-          },
-          MouseOutEvent.getType());
-      return popup;
-    }
-
-    @Override
-    public void onMouseOver(MouseOverEvent event) {
-      scheduleShow();
-    }
-
-    @Override
-    public void onMouseOut(MouseOutEvent event) {
-      scheduleHide();
-    }
-
-    private void scheduleShow() {
-      if (hideTimer != null) {
-        hideTimer.cancel();
-        hideTimer = null;
-      }
-      if ((popup != null && popup.isShowing() && popup.isVisible()) || showTimer != null) {
-        return;
-      }
-      showTimer =
-          new Timer() {
-            @Override
-            public void run() {
-              if (popup == null) {
-                popup = createPopupPanel(account);
-              }
-              if (!popup.isShowing() || !popup.isVisible()) {
-                popup.showRelativeTo(target);
-              }
-            }
-          };
-      showTimer.schedule(600);
-    }
-
-    private void scheduleHide() {
-      if (showTimer != null) {
-        showTimer.cancel();
-        showTimer = null;
-      }
-      if (popup == null || !popup.isShowing() || !popup.isVisible() || hideTimer != null) {
-        return;
-      }
-      hideTimer =
-          new Timer() {
-            @Override
-            public void run() {
-              popup.hide();
-            }
-          };
-      hideTimer.schedule(50);
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationCallback.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationCallback.java
deleted file mode 100644
index cc30873..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationCallback.java
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT 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.client;
-
-/**
- * Interface that a caller must implement to react on the result of a {@link ConfirmationDialog}.
- */
-public abstract class ConfirmationCallback {
-
-  /**
-   * Called when the {@link ConfirmationDialog} is finished with OK. To be overwritten by
-   * subclasses.
-   */
-  public abstract void onOk();
-
-  /**
-   * Called when the {@link ConfirmationDialog} is finished with Cancel. To be overwritten by
-   * subclasses.
-   */
-  public void onCancel() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationDialog.java
deleted file mode 100644
index 438df34..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationDialog.java
+++ /dev/null
@@ -1,88 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT 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.client;
-
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-import com.google.gwtexpui.safehtml.client.SafeHtml;
-import com.google.gwtexpui.user.client.AutoCenterDialogBox;
-
-public class ConfirmationDialog extends AutoCenterDialogBox {
-
-  private Button cancelButton;
-  private Button okButton;
-
-  public ConfirmationDialog(
-      final String dialogTitle, SafeHtml message, ConfirmationCallback callback) {
-    super(/* auto hide */ false, /* modal */ true);
-    setGlassEnabled(true);
-    setText(dialogTitle);
-
-    final FlowPanel buttons = new FlowPanel();
-
-    okButton = new Button();
-    okButton.setText(Gerrit.C.confirmationDialogOk());
-    okButton.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            hide();
-            callback.onOk();
-          }
-        });
-    buttons.add(okButton);
-
-    cancelButton = new Button();
-    cancelButton.getElement().getStyle().setProperty("marginLeft", "300px");
-    cancelButton.setText(Gerrit.C.confirmationDialogCancel());
-    cancelButton.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            hide();
-            callback.onCancel();
-          }
-        });
-    buttons.add(cancelButton);
-
-    final FlowPanel center = new FlowPanel();
-    final Widget msgWidget = message.toBlockWidget();
-    center.add(msgWidget);
-    center.add(buttons);
-    add(center);
-
-    msgWidget.setWidth("400px");
-
-    setWidget(center);
-  }
-
-  @Override
-  public void center() {
-    super.center();
-    GlobalKey.dialog(this);
-    cancelButton.setFocus(true);
-  }
-
-  public void setCancelVisible(boolean visible) {
-    cancelButton.setVisible(visible);
-    if (!visible) {
-      okButton.setFocus(true);
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/DiffObject.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/DiffObject.java
deleted file mode 100644
index 21ced4c..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/DiffObject.java
+++ /dev/null
@@ -1,183 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client;
-
-import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DefaultBase;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-
-/**
- * Represent an object that can be diffed. This can be either a regular patch set, the base of a
- * patch set, the parent of a merge, the auto-merge of a merge or an edit patch set.
- */
-public class DiffObject {
-  public static final String AUTO_MERGE = "AutoMerge";
-
-  /**
-   * Parses a string that represents a diff object.
-   *
-   * <p>The following string representations are supported:
-   *
-   * <ul>
-   *   <li>a positive integer: represents a patch set
-   *   <li>a negative integer: represents a parent of a merge patch set
-   *   <li>'0': represents the edit patch set
-   *   <li>empty string or null: represents the parent of a 1-parent patch set, also called base
-   *   <li>'AutoMerge': represents the auto-merge of a merge patch set
-   * </ul>
-   *
-   * @param changeId the ID of the change to which the diff object belongs
-   * @param str the string representation of the diff object
-   * @return the parsed diff object, {@code null} if str cannot be parsed as diff object
-   */
-  public static DiffObject parse(Change.Id changeId, String str) {
-    if (str == null || str.isEmpty()) {
-      return new DiffObject(false);
-    }
-
-    if (AUTO_MERGE.equals(str)) {
-      return new DiffObject(true);
-    }
-
-    return new DiffObject(Dispatcher.toPsId(changeId, str));
-  }
-
-  /** Create a DiffObject that represents the parent of a 1-parent patch set. */
-  public static DiffObject base() {
-    return new DiffObject(false);
-  }
-
-  /** Create a DiffObject that represents the auto-merge for a merge patch set. */
-  public static DiffObject autoMerge() {
-    return new DiffObject(true);
-  }
-
-  /** Create a DiffObject that represents a patch set. */
-  public static DiffObject patchSet(PatchSet.Id psId) {
-    return new DiffObject(psId);
-  }
-
-  private final PatchSet.Id psId;
-  private final boolean autoMerge;
-
-  private DiffObject(PatchSet.Id psId) {
-    this.psId = psId;
-    this.autoMerge = false;
-  }
-
-  private DiffObject(boolean autoMerge) {
-    this.psId = null;
-    this.autoMerge = autoMerge;
-  }
-
-  public boolean isBase() {
-    return psId == null && !autoMerge;
-  }
-
-  public boolean isAutoMerge() {
-    return psId == null && autoMerge;
-  }
-
-  public boolean isBaseOrAutoMerge() {
-    return psId == null;
-  }
-
-  public boolean isPatchSet() {
-    return psId != null && psId.get() > 0;
-  }
-
-  public boolean isParent() {
-    return psId != null && psId.get() < 0;
-  }
-
-  public boolean isEdit() {
-    return psId != null && psId.get() == 0;
-  }
-
-  /**
-   * Returns the DiffObject as PatchSet.Id.
-   *
-   * @return PatchSet.Id with an id > 0 for a regular patch set; PatchSet.Id with an id < 0 for a
-   *     parent of a merge; PatchSet.Id with id == 0 for an edit patch set; {@code null} for the
-   *     base of a 1-parent patch set and for the auto-merge of a merge patch set
-   */
-  public PatchSet.Id asPatchSetId() {
-    return psId;
-  }
-
-  /**
-   * Returns the parent number for a parent of a merge.
-   *
-   * @return 1-based parent number, 0 if this DiffObject is not a parent of a merge
-   */
-  public int getParentNum() {
-    if (!isParent()) {
-      return 0;
-    }
-
-    return -psId.get();
-  }
-
-  /**
-   * Returns a string representation of this DiffObject that can be used in URLs.
-   *
-   * <p>The following string representations are returned:
-   *
-   * <ul>
-   *   <li>a positive integer for a patch set
-   *   <li>a negative integer for a parent of a merge patch set
-   *   <li>'0' for the edit patch set
-   *   <li>{@code null} for the parent of a 1-parent patch set, also called base
-   *   <li>'AutoMerge' for the auto-merge of a merge patch set
-   * </ul>
-   *
-   * @return string representation of this DiffObject
-   */
-  public String asString() {
-    if (autoMerge) {
-      if (Gerrit.getUserPreferences().defaultBaseForMerges() != DefaultBase.AUTO_MERGE) {
-        return AUTO_MERGE;
-      }
-      return null;
-    }
-
-    if (psId != null) {
-      return psId.getId();
-    }
-
-    return null;
-  }
-
-  @Override
-  public String toString() {
-    if (isPatchSet()) {
-      return "Patch Set " + psId.getId();
-    }
-
-    if (isParent()) {
-      return "Parent " + psId.getId();
-    }
-
-    if (isEdit()) {
-      return "Edit Patch Set";
-    }
-
-    if (isAutoMerge()) {
-      return "Auto Merge";
-    }
-
-    return "Base";
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/DiffWebLinkInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/DiffWebLinkInfo.java
deleted file mode 100644
index fe7016f..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/DiffWebLinkInfo.java
+++ /dev/null
@@ -1,27 +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.client;
-
-import com.google.gerrit.client.info.WebLinkInfo;
-
-public class DiffWebLinkInfo extends WebLinkInfo {
-  public final native boolean showOnSideBySideDiffView()
-      /*-{ return this.show_on_side_by_side_diff_view || false; }-*/ ;
-
-  public final native boolean showOnUnifiedDiffView()
-      /*-{ return this.show_on_unified_diff_view || false; }-*/ ;
-
-  protected DiffWebLinkInfo() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
deleted file mode 100644
index 4b84864..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
+++ /dev/null
@@ -1,879 +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.client;
-
-import static com.google.gerrit.common.PageLinks.ADMIN_CREATE_GROUP;
-import static com.google.gerrit.common.PageLinks.ADMIN_CREATE_PROJECT;
-import static com.google.gerrit.common.PageLinks.ADMIN_GROUPS;
-import static com.google.gerrit.common.PageLinks.ADMIN_PLUGINS;
-import static com.google.gerrit.common.PageLinks.ADMIN_PROJECTS;
-import static com.google.gerrit.common.PageLinks.DASHBOARDS;
-import static com.google.gerrit.common.PageLinks.MINE;
-import static com.google.gerrit.common.PageLinks.MY_GROUPS;
-import static com.google.gerrit.common.PageLinks.PROJECTS;
-import static com.google.gerrit.common.PageLinks.QUERY;
-import static com.google.gerrit.common.PageLinks.REGISTER;
-import static com.google.gerrit.common.PageLinks.SETTINGS;
-import static com.google.gerrit.common.PageLinks.SETTINGS_AGREEMENTS;
-import static com.google.gerrit.common.PageLinks.SETTINGS_CONTACT;
-import static com.google.gerrit.common.PageLinks.SETTINGS_DIFF_PREFERENCES;
-import static com.google.gerrit.common.PageLinks.SETTINGS_EDIT_PREFERENCES;
-import static com.google.gerrit.common.PageLinks.SETTINGS_EXTENSION;
-import static com.google.gerrit.common.PageLinks.SETTINGS_GPGKEYS;
-import static com.google.gerrit.common.PageLinks.SETTINGS_HTTP_PASSWORD;
-import static com.google.gerrit.common.PageLinks.SETTINGS_MYGROUPS;
-import static com.google.gerrit.common.PageLinks.SETTINGS_NEW_AGREEMENT;
-import static com.google.gerrit.common.PageLinks.SETTINGS_OAUTH_TOKEN;
-import static com.google.gerrit.common.PageLinks.SETTINGS_PREFERENCES;
-import static com.google.gerrit.common.PageLinks.SETTINGS_PROJECTS;
-import static com.google.gerrit.common.PageLinks.SETTINGS_SSHKEYS;
-import static com.google.gerrit.common.PageLinks.SETTINGS_WEBIDENT;
-
-import com.google.gerrit.client.account.MyAgreementsScreen;
-import com.google.gerrit.client.account.MyContactInformationScreen;
-import com.google.gerrit.client.account.MyDiffPreferencesScreen;
-import com.google.gerrit.client.account.MyEditPreferencesScreen;
-import com.google.gerrit.client.account.MyGpgKeysScreen;
-import com.google.gerrit.client.account.MyGroupsScreen;
-import com.google.gerrit.client.account.MyIdentitiesScreen;
-import com.google.gerrit.client.account.MyOAuthTokenScreen;
-import com.google.gerrit.client.account.MyPasswordScreen;
-import com.google.gerrit.client.account.MyPreferencesScreen;
-import com.google.gerrit.client.account.MyProfileScreen;
-import com.google.gerrit.client.account.MySshKeysScreen;
-import com.google.gerrit.client.account.MyWatchedProjectsScreen;
-import com.google.gerrit.client.account.NewAgreementScreen;
-import com.google.gerrit.client.account.RegisterScreen;
-import com.google.gerrit.client.account.ValidateEmailScreen;
-import com.google.gerrit.client.admin.AccountGroupAuditLogScreen;
-import com.google.gerrit.client.admin.AccountGroupInfoScreen;
-import com.google.gerrit.client.admin.AccountGroupMembersScreen;
-import com.google.gerrit.client.admin.AccountGroupScreen;
-import com.google.gerrit.client.admin.CreateGroupScreen;
-import com.google.gerrit.client.admin.CreateProjectScreen;
-import com.google.gerrit.client.admin.GroupListScreen;
-import com.google.gerrit.client.admin.PluginListScreen;
-import com.google.gerrit.client.admin.ProjectAccessScreen;
-import com.google.gerrit.client.admin.ProjectBranchesScreen;
-import com.google.gerrit.client.admin.ProjectDashboardsScreen;
-import com.google.gerrit.client.admin.ProjectInfoScreen;
-import com.google.gerrit.client.admin.ProjectListScreen;
-import com.google.gerrit.client.admin.ProjectScreen;
-import com.google.gerrit.client.admin.ProjectTagsScreen;
-import com.google.gerrit.client.api.ExtensionScreen;
-import com.google.gerrit.client.api.ExtensionSettingsScreen;
-import com.google.gerrit.client.change.ChangeScreen;
-import com.google.gerrit.client.change.FileTable;
-import com.google.gerrit.client.change.ProjectChangeId;
-import com.google.gerrit.client.changes.AccountDashboardScreen;
-import com.google.gerrit.client.changes.CustomDashboardScreen;
-import com.google.gerrit.client.changes.ProjectDashboardScreen;
-import com.google.gerrit.client.changes.QueryScreen;
-import com.google.gerrit.client.dashboards.DashboardInfo;
-import com.google.gerrit.client.dashboards.DashboardList;
-import com.google.gerrit.client.diff.DisplaySide;
-import com.google.gerrit.client.diff.SideBySide;
-import com.google.gerrit.client.diff.Unified;
-import com.google.gerrit.client.documentation.DocScreen;
-import com.google.gerrit.client.editor.EditScreen;
-import com.google.gerrit.client.groups.GroupApi;
-import com.google.gerrit.client.info.GroupInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.client.ui.Screen;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DiffView;
-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.gwt.core.client.GWT;
-import com.google.gwt.core.client.RunAsyncCallback;
-import com.google.gwt.http.client.URL;
-import com.google.gwtexpui.user.client.UserAgent;
-import com.google.gwtorm.client.KeyUtil;
-
-public class Dispatcher {
-  public static String toPatch(
-      @Nullable Project.NameKey project,
-      DiffObject diffBase,
-      PatchSet.Id revision,
-      String fileName) {
-    return toPatch("", project, diffBase, revision, fileName, null, 0);
-  }
-
-  public static String toPatch(
-      @Nullable Project.NameKey project,
-      DiffObject diffBase,
-      PatchSet.Id revision,
-      String fileName,
-      DisplaySide side,
-      int line) {
-    return toPatch("", project, diffBase, revision, fileName, side, line);
-  }
-
-  public static String toSideBySide(
-      @Nullable Project.NameKey project,
-      DiffObject diffBase,
-      PatchSet.Id revision,
-      String fileName) {
-    return toPatch("sidebyside", project, diffBase, revision, fileName, null, 0);
-  }
-
-  public static String toUnified(
-      @Nullable Project.NameKey project,
-      DiffObject diffBase,
-      PatchSet.Id revision,
-      String fileName) {
-    return toPatch("unified", project, diffBase, revision, fileName, null, 0);
-  }
-
-  public static String toPatch(
-      @Nullable Project.NameKey project, String type, DiffObject diffBase, Patch.Key id) {
-    return toPatch(type, project, diffBase, id.getParentKey(), id.get(), null, 0);
-  }
-
-  public static String toEditScreen(
-      @Nullable Project.NameKey project, PatchSet.Id revision, String fileName) {
-    return toEditScreen(project, revision, fileName, 0);
-  }
-
-  public static String toEditScreen(
-      @Nullable Project.NameKey project, PatchSet.Id revision, String fileName, int line) {
-    return toPatch("edit", project, DiffObject.base(), revision, fileName, null, line);
-  }
-
-  private static String toPatch(
-      String type,
-      @Nullable Project.NameKey project,
-      DiffObject diffBase,
-      PatchSet.Id revision,
-      String fileName,
-      DisplaySide side,
-      int line) {
-    Change.Id c = revision.getParentKey();
-    StringBuilder p = new StringBuilder(PageLinks.toChange(project, c));
-    if (diffBase != null && diffBase.asString() != null) {
-      p.append(diffBase.asString()).append("..");
-    }
-    p.append(revision.getId()).append("/").append(KeyUtil.encode(fileName));
-    if (type != null && !type.isEmpty() && (!"sidebyside".equals(type) || preferUnified())) {
-      p.append(",").append(type);
-    }
-    if (side == DisplaySide.A && line > 0) {
-      p.append("@a").append(line);
-    } else if (line > 0) {
-      p.append("@").append(line);
-    }
-    return p.toString();
-  }
-
-  public static String toGroup(AccountGroup.Id id) {
-    return ADMIN_GROUPS + id.toString();
-  }
-
-  public static String toGroup(AccountGroup.Id id, String panel) {
-    return ADMIN_GROUPS + id.toString() + "," + panel;
-  }
-
-  public static String toGroup(AccountGroup.UUID uuid) {
-    return PageLinks.toGroup(uuid);
-  }
-
-  public static String toGroup(AccountGroup.UUID uuid, String panel) {
-    return toGroup(uuid) + "," + panel;
-  }
-
-  public static String toProject(Project.NameKey n) {
-    return toProjectAdmin(n, ProjectScreen.getSavedPanel());
-  }
-
-  public static String toProjectAdmin(Project.NameKey n, String panel) {
-    if (panel == null || ProjectScreen.INFO.equals(panel)) {
-      return ADMIN_PROJECTS + n.toString();
-    }
-    return ADMIN_PROJECTS + n.toString() + "," + panel;
-  }
-
-  static final String RELOAD_UI = "/reload-ui/";
-  private static boolean wasStartedByReloadUI;
-
-  void display(String token) {
-    assert token != null;
-    try {
-      try {
-        if (matchPrefix(RELOAD_UI, token)) {
-          wasStartedByReloadUI = true;
-          token = skip(token);
-        }
-        select(token);
-      } finally {
-        wasStartedByReloadUI = false;
-      }
-    } catch (RuntimeException err) {
-      GWT.log("Error parsing history token: " + token, err);
-      Gerrit.display(token, new NotFoundScreen());
-    }
-  }
-
-  private static void select(String token) {
-    token = Gerrit.getUrlAliasMatcher().replace(token);
-
-    if (matchPrefix(QUERY, token)) {
-      query(token);
-
-    } else if (matchPrefix("/Documentation/q/", token)) {
-      docSearch(token);
-
-    } else if (matchPrefix("/c/", token)) {
-      change(token);
-
-    } else if (matchPrefix("/x/", token)) {
-      extension(token);
-
-    } else if (matchExact(MINE, token)) {
-      String defaultScreenToken = Gerrit.getDefaultScreenToken();
-      if (defaultScreenToken != null && !MINE.equals(defaultScreenToken)) {
-        select(defaultScreenToken);
-      } else {
-        Gerrit.display(token, mine());
-      }
-
-    } else if (matchPrefix("/dashboard/", token)) {
-      dashboard(token);
-
-    } else if (matchPrefix(PROJECTS, token)) {
-      projects(token);
-
-    } else if (matchExact(SETTINGS, token)
-        || matchPrefix("/settings/", token)
-        || matchExact(MY_GROUPS, token)
-        || matchExact("register", token)
-        || matchExact(REGISTER, token)
-        || matchPrefix("/register/", token)
-        || matchPrefix("/VE/", token)
-        || matchPrefix("VE,", token)
-        || matchPrefix("/SignInFailure,", token)) {
-      settings(token);
-
-    } else if (matchPrefix("/admin/", token)) {
-      admin(token);
-
-    } else {
-      Gerrit.display(token, new NotFoundScreen());
-    }
-  }
-
-  private static void query(String token) {
-    String s = skip(token);
-    int c = s.indexOf(',');
-    Screen screen;
-    if (c >= 0) {
-      String prefix = s.substring(0, c);
-      if (s.substring(c).equals(",n,z")) {
-        // Respect legacy token with max sortkey.
-        screen = new QueryScreen(prefix, 0);
-      } else {
-        screen = new QueryScreen(prefix, Integer.parseInt(s.substring(c + 1)));
-      }
-    } else {
-      screen = new QueryScreen(s, 0);
-    }
-    Gerrit.display(token, screen);
-  }
-
-  private static Screen mine() {
-    if (Gerrit.isSignedIn()) {
-      return new AccountDashboardScreen(Gerrit.getUserAccount()._accountId());
-    }
-    Screen r = new AccountDashboardScreen(null);
-    r.setRequiresSignIn(true);
-    return r;
-  }
-
-  private static void dashboard(String token) {
-    String rest = skip(token);
-    if (rest.matches("[0-9]+")) {
-      int accountId = Integer.parseInt(rest);
-      Gerrit.display(token, new AccountDashboardScreen(accountId));
-      return;
-    }
-
-    if (rest.equals("self")) {
-      if (Gerrit.isSignedIn()) {
-        Gerrit.display(token, new AccountDashboardScreen(Gerrit.getUserAccount()._accountId()));
-      } else {
-        Screen s = new AccountDashboardScreen(null);
-        s.setRequiresSignIn(true);
-        Gerrit.display(token, s);
-      }
-      return;
-    }
-
-    if (rest.startsWith("?")) {
-      Gerrit.display(token, new CustomDashboardScreen(rest.substring(1)));
-      return;
-    }
-
-    Gerrit.display(token, new NotFoundScreen());
-  }
-
-  private static void projects(String token) {
-    String rest = skip(token);
-    int c = rest.indexOf(DASHBOARDS);
-    if (0 <= c) {
-      final String project = URL.decodePathSegment(rest.substring(0, c));
-      rest = rest.substring(c);
-      if (matchPrefix(DASHBOARDS, rest)) {
-        final String dashboardId = skip(rest);
-        GerritCallback<DashboardInfo> cb =
-            new GerritCallback<DashboardInfo>() {
-              @Override
-              public void onSuccess(DashboardInfo result) {
-                if (matchPrefix("/dashboard/", result.url())) {
-                  String params = skip(result.url()).substring(1);
-                  ProjectDashboardScreen dash =
-                      new ProjectDashboardScreen(new Project.NameKey(project), params);
-                  Gerrit.display(token, dash);
-                }
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {
-                if ("default".equals(dashboardId) && RestApi.isNotFound(caught)) {
-                  Gerrit.display(
-                      PageLinks.toChangeQuery(
-                          PageLinks.projectQuery(new Project.NameKey(project))));
-                } else {
-                  super.onFailure(caught);
-                }
-              }
-            };
-        if ("default".equals(dashboardId)) {
-          DashboardList.getDefault(new Project.NameKey(project), cb);
-          return;
-        }
-        c = dashboardId.indexOf(":");
-        if (0 <= c) {
-          final String ref = URL.decodeQueryString(dashboardId.substring(0, c));
-          final String path = URL.decodeQueryString(dashboardId.substring(c + 1));
-          DashboardList.get(new Project.NameKey(project), ref + ":" + path, cb);
-          return;
-        }
-      }
-    }
-
-    Gerrit.display(token, new NotFoundScreen());
-  }
-
-  private static void change(String token) {
-    String rest = skip(token);
-    int c = rest.lastIndexOf(',');
-    String panel = null;
-    if (0 <= c) {
-      panel = rest.substring(c + 1);
-      rest = rest.substring(0, c);
-      int at = panel.lastIndexOf('@');
-      if (at > 0) {
-        rest += panel.substring(at);
-        panel = panel.substring(0, at);
-      }
-    }
-
-    ProjectChangeId id = ProjectChangeId.create(rest);
-    rest = rest.length() > id.identifierLength() ? rest.substring(id.identifierLength() + 1) : "";
-
-    if (rest.isEmpty()) {
-      FileTable.Mode mode = FileTable.Mode.REVIEW;
-      if (panel != null && (panel.equals("edit") || panel.startsWith("edit/"))) {
-        mode = FileTable.Mode.EDIT;
-        panel = null;
-      }
-      Gerrit.display(
-          token,
-          panel == null
-              ? new ChangeScreen(
-                  id.getProject(), id.getChangeId(), DiffObject.base(), null, false, mode)
-              : new NotFoundScreen());
-      return;
-    }
-
-    String psIdStr;
-    int s = rest.indexOf('/');
-    if (0 <= s) {
-      psIdStr = rest.substring(0, s);
-      rest = rest.substring(s + 1);
-    } else {
-      psIdStr = rest;
-      rest = "";
-    }
-
-    DiffObject base = DiffObject.base();
-    PatchSet.Id ps;
-    int dotdot = psIdStr.indexOf("..");
-    if (1 <= dotdot) {
-      base = DiffObject.parse(id.getChangeId(), psIdStr.substring(0, dotdot));
-      if (base == null) {
-        Gerrit.display(token, new NotFoundScreen());
-      }
-      psIdStr = psIdStr.substring(dotdot + 2);
-    }
-    ps = toPsId(id.getChangeId(), psIdStr);
-
-    if (!rest.isEmpty()) {
-      DisplaySide side = DisplaySide.B;
-      int line = 0;
-      int at = rest.lastIndexOf('@');
-      if (at > 0) {
-        String l = rest.substring(at + 1);
-        if (l.startsWith("a")) {
-          side = DisplaySide.A;
-          l = l.substring(1);
-        }
-        line = Integer.parseInt(l);
-        rest = rest.substring(0, at);
-      }
-      Patch.Key p = new Patch.Key(ps, KeyUtil.decode(rest));
-      patch(token, id.getProject(), base, p, side, line, panel);
-    } else {
-      if (panel == null) {
-        Gerrit.display(
-            token,
-            new ChangeScreen(
-                id.getProject(),
-                id.getChangeId(),
-                base,
-                String.valueOf(ps.get()),
-                false,
-                FileTable.Mode.REVIEW));
-      } else {
-        Gerrit.display(token, new NotFoundScreen());
-      }
-    }
-  }
-
-  public static PatchSet.Id toPsId(Change.Id id, String psIdStr) {
-    return new PatchSet.Id(id, psIdStr.equals("edit") ? 0 : Integer.parseInt(psIdStr));
-  }
-
-  private static void extension(String token) {
-    ExtensionScreen view = new ExtensionScreen(skip(token));
-    if (view.isFound()) {
-      Gerrit.display(token, view);
-    } else {
-      Gerrit.display(token, new NotFoundScreen());
-    }
-  }
-
-  private static void patch(
-      String token,
-      @Nullable Project.NameKey project,
-      DiffObject base,
-      Patch.Key id,
-      DisplaySide side,
-      int line,
-      String panelType) {
-    String panel = panelType;
-    if (panel == null) {
-      int c = token.lastIndexOf(',');
-      panel = 0 <= c ? token.substring(c + 1) : "";
-    }
-
-    if ("".equals(panel) || /* DEPRECATED URL */ "cm".equals(panel)) {
-      if (preferUnified()) {
-        unified(token, project, base, id, side, line);
-      } else {
-        codemirror(token, base, project, id, side, line);
-      }
-    } else if ("sidebyside".equals(panel)) {
-      codemirror(token, base, project, id, side, line);
-    } else if ("unified".equals(panel)) {
-      unified(token, project, base, id, side, line);
-    } else if ("edit".equals(panel)) {
-      if (!Patch.isMagic(id.get()) || Patch.COMMIT_MSG.equals(id.get())) {
-        codemirrorForEdit(token, project, id, line);
-      } else {
-        Gerrit.display(token, new NotFoundScreen());
-      }
-    } else {
-      Gerrit.display(token, new NotFoundScreen());
-    }
-  }
-
-  private static boolean preferUnified() {
-    return DiffView.UNIFIED_DIFF.equals(Gerrit.getUserPreferences().diffView())
-        || (UserAgent.isPortrait() && UserAgent.isMobile());
-  }
-
-  private static void unified(
-      final String token,
-      final Project.NameKey project,
-      final DiffObject base,
-      final Patch.Key id,
-      final DisplaySide side,
-      final int line) {
-    GWT.runAsync(
-        new AsyncSplit(token) {
-          @Override
-          public void onSuccess() {
-            Gerrit.display(
-                token,
-                new Unified(
-                    project, base, DiffObject.patchSet(id.getParentKey()), id.get(), side, line));
-          }
-        });
-  }
-
-  private static void codemirror(
-      final String token,
-      final DiffObject base,
-      @Nullable final Project.NameKey project,
-      final Patch.Key id,
-      final DisplaySide side,
-      final int line) {
-    GWT.runAsync(
-        new AsyncSplit(token) {
-          @Override
-          public void onSuccess() {
-            Gerrit.display(
-                token,
-                new SideBySide(
-                    project, base, DiffObject.patchSet(id.getParentKey()), id.get(), side, line));
-          }
-        });
-  }
-
-  private static void codemirrorForEdit(
-      final String token,
-      @Nullable final Project.NameKey project,
-      final Patch.Key id,
-      final int line) {
-    GWT.runAsync(
-        new AsyncSplit(token) {
-          @Override
-          public void onSuccess() {
-            Gerrit.display(token, new EditScreen(project, id, line));
-          }
-        });
-  }
-
-  private static void settings(String token) {
-    GWT.runAsync(
-        new AsyncSplit(token) {
-          @Override
-          public void onSuccess() {
-            Gerrit.display(token, select());
-          }
-
-          private Screen select() {
-            if (matchExact(SETTINGS, token)) {
-              return new MyProfileScreen();
-            }
-
-            if (matchExact(SETTINGS_PREFERENCES, token)) {
-              return new MyPreferencesScreen();
-            }
-
-            if (matchExact(SETTINGS_DIFF_PREFERENCES, token)) {
-              return new MyDiffPreferencesScreen();
-            }
-
-            if (matchExact(SETTINGS_EDIT_PREFERENCES, token)) {
-              return new MyEditPreferencesScreen();
-            }
-
-            if (matchExact(SETTINGS_PROJECTS, token)) {
-              return new MyWatchedProjectsScreen();
-            }
-
-            if (matchExact(SETTINGS_CONTACT, token)) {
-              return new MyContactInformationScreen();
-            }
-
-            if (matchExact(SETTINGS_SSHKEYS, token)) {
-              return new MySshKeysScreen();
-            }
-
-            if (matchExact(SETTINGS_GPGKEYS, token) && Gerrit.info().gerrit().editGpgKeys()) {
-              return new MyGpgKeysScreen();
-            }
-
-            if (matchExact(SETTINGS_WEBIDENT, token)) {
-              return new MyIdentitiesScreen();
-            }
-
-            if (matchExact(SETTINGS_HTTP_PASSWORD, token)) {
-              return new MyPasswordScreen();
-            }
-
-            if (matchExact(SETTINGS_OAUTH_TOKEN, token) && Gerrit.info().auth().isOAuth()) {
-              return new MyOAuthTokenScreen();
-            }
-
-            if (matchExact(MY_GROUPS, token) || matchExact(SETTINGS_MYGROUPS, token)) {
-              return new MyGroupsScreen();
-            }
-
-            if (matchExact(SETTINGS_AGREEMENTS, token)
-                && Gerrit.info().auth().useContributorAgreements()) {
-              return new MyAgreementsScreen();
-            }
-
-            if (matchExact(REGISTER, token)
-                || matchExact("/register/", token)
-                || matchExact("register", token)) {
-              return new RegisterScreen(MINE);
-            } else if (matchPrefix("/register/", token)) {
-              return new RegisterScreen("/" + skip(token));
-            }
-
-            if (matchPrefix("/VE/", token) || matchPrefix("VE,", token)) {
-              return new ValidateEmailScreen(skip(token));
-            }
-
-            if (matchExact(SETTINGS_NEW_AGREEMENT, token)) {
-              return new NewAgreementScreen();
-            }
-
-            if (matchPrefix(SETTINGS_NEW_AGREEMENT + "/", token)) {
-              return new NewAgreementScreen(skip(token));
-            }
-
-            if (matchPrefix(SETTINGS_EXTENSION, token)) {
-              ExtensionSettingsScreen view = new ExtensionSettingsScreen(skip(token));
-              if (view.isFound()) {
-                return view;
-              }
-              return new NotFoundScreen();
-            }
-
-            return new NotFoundScreen();
-          }
-        });
-  }
-
-  private static void admin(String token) {
-    GWT.runAsync(
-        new AsyncSplit(token) {
-          @Override
-          public void onSuccess() {
-            if (matchExact(ADMIN_GROUPS, token) || matchExact("/admin/groups", token)) {
-              Gerrit.display(token, new GroupListScreen());
-
-            } else if (matchPrefix(ADMIN_GROUPS, token)) {
-              String rest = skip(token);
-              if (rest.startsWith("?")) {
-                Gerrit.display(token, new GroupListScreen(rest.substring(1)));
-              } else {
-                group();
-              }
-
-            } else if (matchPrefix("/admin/groups", token)) {
-              String rest = skip(token);
-              if (rest.startsWith("?")) {
-                Gerrit.display(token, new GroupListScreen(rest.substring(1)));
-              }
-
-            } else if (matchExact(ADMIN_PROJECTS, token) || matchExact("/admin/projects", token)) {
-              Gerrit.display(token, new ProjectListScreen());
-
-            } else if (matchPrefix(ADMIN_PROJECTS, token)) {
-              String rest = skip(token);
-              if (rest.startsWith("?")) {
-                Gerrit.display(token, new ProjectListScreen(rest.substring(1)));
-              } else {
-                Gerrit.display(token, selectProject());
-              }
-
-            } else if (matchPrefix("/admin/projects", token)) {
-              String rest = skip(token);
-              if (rest.startsWith("?")) {
-                Gerrit.display(token, new ProjectListScreen(rest.substring(1)));
-              }
-
-            } else if (matchPrefix(ADMIN_PLUGINS, token) || matchExact("/admin/plugins", token)) {
-              Gerrit.display(token, new PluginListScreen());
-
-            } else if (matchExact(ADMIN_CREATE_PROJECT, token)
-                || matchExact("/admin/create-project", token)) {
-              Gerrit.display(token, new CreateProjectScreen());
-
-            } else if (matchExact(ADMIN_CREATE_GROUP, token)
-                || matchExact("/admin/create-group", token)) {
-              Gerrit.display(token, new CreateGroupScreen());
-
-            } else {
-              Gerrit.display(token, new NotFoundScreen());
-            }
-          }
-
-          private void group() {
-            final String panel;
-            final String group;
-
-            if (matchPrefix("/admin/groups/uuid-", token)) {
-              String p = skip(token);
-              int c = p.indexOf(',');
-              if (c < 0) {
-                group = p;
-                panel = null;
-              } else {
-                group = p.substring(0, c);
-                panel = p.substring(c + 1);
-              }
-            } else if (matchPrefix(ADMIN_GROUPS, token)) {
-              String p = skip(token);
-              int c = p.indexOf(',');
-              if (c < 0) {
-                group = p;
-                panel = null;
-              } else {
-                group = p.substring(0, c);
-                panel = p.substring(c + 1);
-              }
-            } else {
-              Gerrit.display(token, new NotFoundScreen());
-              return;
-            }
-
-            GroupApi.getGroupDetail(
-                group,
-                new GerritCallback<GroupInfo>() {
-                  @Override
-                  public void onSuccess(GroupInfo group) {
-                    if (panel == null || panel.isEmpty()) {
-                      // The token does not say which group screen should be shown,
-                      // as default for internal groups show the members, as default
-                      // for external and system groups show the info screen (since
-                      // for external and system groups the members cannot be
-                      // shown in the web UI).
-                      //
-                      if (AccountGroup.isInternalGroup(group.getGroupUUID())) {
-                        String newToken = toGroup(group.getGroupId(), AccountGroupScreen.MEMBERS);
-                        Gerrit.display(newToken, new AccountGroupMembersScreen(group, newToken));
-                      } else {
-                        String newToken = toGroup(group.getGroupId(), AccountGroupScreen.INFO);
-                        Gerrit.display(newToken, new AccountGroupInfoScreen(group, newToken));
-                      }
-                    } else if (AccountGroupScreen.INFO.equals(panel)) {
-                      Gerrit.display(token, new AccountGroupInfoScreen(group, token));
-                    } else if (AccountGroupScreen.MEMBERS.equals(panel)) {
-                      Gerrit.display(token, new AccountGroupMembersScreen(group, token));
-                    } else if (AccountGroupScreen.AUDIT_LOG.equals(panel)) {
-                      Gerrit.display(token, new AccountGroupAuditLogScreen(group, token));
-                    } else {
-                      Gerrit.display(token, new NotFoundScreen());
-                    }
-                  }
-                });
-          }
-
-          private Screen selectProject() {
-            if (matchPrefix(ADMIN_PROJECTS, token)) {
-              String rest = skip(token);
-              int c = rest.lastIndexOf(',');
-              if (c < 0) {
-                return new ProjectInfoScreen(Project.NameKey.parse(rest));
-              } else if (c == 0) {
-                return new NotFoundScreen();
-              }
-
-              int q = rest.lastIndexOf('?');
-              if (q > 0 && rest.lastIndexOf(',', q) > 0) {
-                c = rest.substring(0, q - 1).lastIndexOf(',');
-              }
-
-              Project.NameKey k = Project.NameKey.parse(rest.substring(0, c));
-              String panel = rest.substring(c + 1);
-
-              if (ProjectScreen.INFO.equals(panel)) {
-                return new ProjectInfoScreen(k);
-              }
-
-              if (ProjectScreen.BRANCHES.equals(panel)
-                  || matchPrefix(ProjectScreen.BRANCHES, panel)) {
-                return new ProjectBranchesScreen(k);
-              }
-
-              if (ProjectScreen.TAGS.equals(panel) || matchPrefix(ProjectScreen.TAGS, panel)) {
-                return new ProjectTagsScreen(k);
-              }
-
-              if (ProjectScreen.ACCESS.equals(panel)) {
-                return new ProjectAccessScreen(k);
-              }
-
-              if (ProjectScreen.DASHBOARDS.equals(panel)) {
-                return new ProjectDashboardsScreen(k);
-              }
-            }
-            return new NotFoundScreen();
-          }
-        });
-  }
-
-  private static boolean matchExact(String want, String token) {
-    return token.equals(want);
-  }
-
-  private static int prefixlen;
-
-  private static boolean matchPrefix(String want, String token) {
-    if (token.startsWith(want)) {
-      prefixlen = want.length();
-      return true;
-    }
-    return false;
-  }
-
-  private static String skip(String token) {
-    return token.substring(prefixlen);
-  }
-
-  private abstract static class AsyncSplit implements RunAsyncCallback {
-    private final boolean isReloadUi;
-    protected final String token;
-
-    protected AsyncSplit(String token) {
-      this.isReloadUi = wasStartedByReloadUI;
-      this.token = token;
-    }
-
-    @Override
-    public final void onFailure(Throwable reason) {
-      if (!isReloadUi && "HTTP download failed with status 404".equals(reason.getMessage())) {
-        // The server was upgraded since we last download the main script,
-        // so the pointers to the splits aren't valid anymore.  Force the
-        // page to reload itself and pick up the new code.
-        //
-        Gerrit.upgradeUI(token);
-      } else {
-        new ErrorDialog(reason).center();
-      }
-    }
-  }
-
-  private static void docSearch(String token) {
-    GWT.runAsync(
-        new AsyncSplit(token) {
-          @Override
-          public void onSuccess() {
-            Gerrit.display(token, new DocScreen(skip(token)));
-          }
-        });
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java
deleted file mode 100644
index c116d76..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java
+++ /dev/null
@@ -1,168 +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.client;
-
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.client.rpc.RpcConstants;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwt.http.client.Response;
-import com.google.gwt.user.client.Window;
-import com.google.gwt.user.client.rpc.StatusCodeException;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.Label;
-import com.google.gwt.user.client.ui.PopupPanel;
-import com.google.gwtexpui.safehtml.client.SafeHtml;
-import com.google.gwtjsonrpc.client.RemoteJsonException;
-
-/** A dialog box showing an error message, when bad things happen. */
-public class ErrorDialog extends PopupPanel {
-  private final Label text;
-  private final FlowPanel body;
-  private final Button closey;
-
-  protected ErrorDialog() {
-    super(/* auto hide */ false, /* modal */ true);
-    setGlassEnabled(true);
-    getGlassElement().addClassName(Gerrit.RESOURCES.css().errorDialogGlass());
-
-    text = new Label();
-    text.setStyleName(Gerrit.RESOURCES.css().errorDialogTitle());
-
-    body = new FlowPanel();
-
-    final FlowPanel buttons = new FlowPanel();
-    buttons.setStyleName(Gerrit.RESOURCES.css().errorDialogButtons());
-
-    closey = new Button();
-    closey.setText(Gerrit.C.errorDialogContinue());
-    closey.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            hide();
-          }
-        });
-    closey.addKeyPressHandler(
-        new KeyPressHandler() {
-          @Override
-          public void onKeyPress(KeyPressEvent event) {
-            // if the close button is triggered by a key we need to consume the key
-            // event, otherwise the key event would be propagated to the parent
-            // screen and eventually trigger some unwanted action there after the
-            // error dialog was closed
-            event.stopPropagation();
-          }
-        });
-    buttons.add(closey);
-
-    final FlowPanel center = new FlowPanel();
-    center.add(text);
-    center.add(body);
-    center.add(buttons);
-
-    setText(Gerrit.C.errorTitle());
-    addStyleName(Gerrit.RESOURCES.css().errorDialog());
-    add(center);
-
-    int l = Window.getScrollLeft() + 20;
-    int t = Window.getScrollTop() + 20;
-    setPopupPosition(l, t);
-  }
-
-  /** Create a dialog box to show a single message string. */
-  public ErrorDialog(String message) {
-    this();
-    body.add(createErrorMsgLabel(message));
-  }
-
-  /** Create a dialog box to show a single message string. */
-  public ErrorDialog(SafeHtml message) {
-    this();
-    body.add(message.toBlockWidget());
-  }
-
-  /** Create a dialog box to nicely format an exception. */
-  public ErrorDialog(Throwable what) {
-    this();
-
-    String hdr;
-    String msg;
-
-    if (what instanceof StatusCodeException) {
-      StatusCodeException sc = (StatusCodeException) what;
-      if (RestApi.isExpected(sc.getStatusCode())) {
-        hdr = null;
-        msg = sc.getEncodedResponse();
-      } else if (sc.getStatusCode() == Response.SC_INTERNAL_SERVER_ERROR) {
-        hdr = null;
-        msg = what.getMessage();
-      } else {
-        hdr = RpcConstants.C.errorServerUnavailable();
-        msg = what.getMessage();
-      }
-
-    } else if (what instanceof RemoteJsonException) {
-      // TODO Remove RemoteJsonException from Gerrit sources.
-      hdr = RpcConstants.C.errorRemoteJsonException();
-      msg = what.getMessage();
-
-    } else {
-      // TODO Fix callers of ErrorDialog to stop passing random types.
-      hdr = what.getClass().getName();
-      if (hdr.startsWith("java.lang.")) {
-        hdr = hdr.substring("java.lang.".length());
-      } else if (hdr.startsWith("com.google.gerrit.")) {
-        hdr = hdr.substring(hdr.lastIndexOf('.') + 1);
-      }
-      if (hdr.endsWith("Exception")) {
-        hdr = hdr.substring(0, hdr.length() - "Exception".length());
-      } else if (hdr.endsWith("Error")) {
-        hdr = hdr.substring(0, hdr.length() - "Error".length());
-      }
-      msg = what.getMessage();
-    }
-
-    if (hdr != null) {
-      final Label r = new Label(hdr);
-      r.setStyleName(Gerrit.RESOURCES.css().errorDialogErrorType());
-      body.add(r);
-    }
-
-    if (msg != null) {
-      body.add(createErrorMsgLabel(msg));
-    }
-  }
-
-  private Label createErrorMsgLabel(String message) {
-    Label m = new Label(message);
-    m.getElement().getStyle().setProperty("white-space", "pre");
-    return m;
-  }
-
-  public ErrorDialog setText(String t) {
-    text.setText(t);
-    return this;
-  }
-
-  @Override
-  public void center() {
-    show();
-    closey.setFocus(true);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
deleted file mode 100644
index c8d052e..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
+++ /dev/null
@@ -1,132 +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.client;
-
-import com.google.gerrit.client.change.Resources;
-import com.google.gerrit.client.info.AccountInfo;
-import com.google.gerrit.client.info.GeneralPreferences;
-import com.google.gwt.i18n.client.NumberFormat;
-import java.util.Date;
-
-/** Misc. formatting functions. */
-public class FormatUtil {
-  private static DateFormatter dateFormatter;
-
-  public static void setPreferences(GeneralPreferences prefs) {
-    dateFormatter = new DateFormatter(prefs);
-  }
-
-  /** Format a date using a really short format. */
-  public static String shortFormat(Date dt) {
-    ensureInited();
-    return dateFormatter.shortFormat(dt);
-  }
-
-  /** Format a date using a really short format. */
-  public static String shortFormatDayTime(Date dt) {
-    ensureInited();
-    return dateFormatter.shortFormatDayTime(dt);
-  }
-
-  /** Format a date using the locale's medium length format. */
-  public static String mediumFormat(Date dt) {
-    ensureInited();
-    return dateFormatter.mediumFormat(dt);
-  }
-
-  private static void ensureInited() {
-    if (dateFormatter == null) {
-      setPreferences(Gerrit.getUserPreferences());
-    }
-  }
-
-  /** Format a date using git log's relative date format. */
-  public static String relativeFormat(Date dt) {
-    return RelativeDateFormatter.format(dt);
-  }
-
-  /**
-   * Formats an account as a name and an email address.
-   *
-   * <p>Example output:
-   *
-   * <ul>
-   *   <li>{@code A U. Thor &lt;author@example.com&gt;}: full populated
-   *   <li>{@code A U. Thor (12)}: missing email address
-   *   <li>{@code Anonymous Coward &lt;author@example.com&gt;}: missing name
-   *   <li>{@code Anonymous Coward (12)}: missing name and email address
-   * </ul>
-   */
-  public static String nameEmail(AccountInfo info) {
-    return createAccountFormatter().nameEmail(info);
-  }
-
-  /**
-   * Formats an account name.
-   *
-   * <p>If the account has a full name, it returns only the full name. Otherwise it returns a longer
-   * form that includes the email address.
-   */
-  public static String name(AccountInfo info) {
-    return createAccountFormatter().name(info);
-  }
-
-  private static AccountFormatter createAccountFormatter() {
-    return new AccountFormatter(Gerrit.info().user().anonymousCowardName());
-  }
-
-  /** The returned format string doesn't contain any +/- sign. */
-  public static String formatAbsBytes(long bytes) {
-    return formatBytes(bytes, true);
-  }
-
-  public static String formatBytes(long bytes) {
-    return formatBytes(bytes, false);
-  }
-
-  private static String formatBytes(long bytes, boolean abs) {
-    bytes = abs ? Math.abs(bytes) : bytes;
-
-    if (bytes == 0) {
-      return abs ? "0 B" : "+/- 0 B";
-    }
-
-    if (Math.abs(bytes) < 1024) {
-      return (bytes > 0 && !abs ? "+" : "") + bytes + " B";
-    }
-
-    int exp = (int) (Math.log(Math.abs(bytes)) / Math.log(1024));
-    return (bytes > 0 && !abs ? "+" : "")
-        + NumberFormat.getFormat("#.0").format(bytes / Math.pow(1024, exp))
-        + " "
-        + "KMGTPE".charAt(exp - 1)
-        + "iB";
-  }
-
-  public static String formatPercentage(long size, long delta) {
-    if (size == 0) {
-      return Resources.C.notAvailable();
-    }
-    return (delta > 0 ? "+" : "-") + formatAbsPercentage(size, delta);
-  }
-
-  public static String formatAbsPercentage(long size, long delta) {
-    if (size == 0) {
-      return Resources.C.notAvailable();
-    }
-    long percentage = Math.abs(Math.round(delta * 100.0 / size));
-    return percentage + "%";
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
deleted file mode 100644
index afc65c9..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
+++ /dev/null
@@ -1,1134 +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.client;
-
-import static com.google.gerrit.common.data.GlobalCapability.CREATE_GROUP;
-import static com.google.gerrit.common.data.GlobalCapability.CREATE_PROJECT;
-import static com.google.gerrit.common.data.GlobalCapability.VIEW_PLUGINS;
-import static com.google.gerrit.common.data.HostPageData.XSRF_COOKIE_NAME;
-
-import com.google.gerrit.client.account.AccountApi;
-import com.google.gerrit.client.account.AccountCapabilities;
-import com.google.gerrit.client.account.EditPreferences;
-import com.google.gerrit.client.admin.ProjectScreen;
-import com.google.gerrit.client.api.ApiGlue;
-import com.google.gerrit.client.api.PluginLoader;
-import com.google.gerrit.client.change.LocalComments;
-import com.google.gerrit.client.changes.ChangeListScreen;
-import com.google.gerrit.client.config.ConfigServerApi;
-import com.google.gerrit.client.documentation.DocInfo;
-import com.google.gerrit.client.info.AccountInfo;
-import com.google.gerrit.client.info.AuthInfo;
-import com.google.gerrit.client.info.GeneralPreferences;
-import com.google.gerrit.client.info.ServerInfo;
-import com.google.gerrit.client.info.TopMenu;
-import com.google.gerrit.client.info.TopMenuItem;
-import com.google.gerrit.client.info.TopMenuList;
-import com.google.gerrit.client.rpc.CallbackGroup;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.ui.LinkMenuBar;
-import com.google.gerrit.client.ui.LinkMenuItem;
-import com.google.gerrit.client.ui.MorphingTabPanel;
-import com.google.gerrit.client.ui.ProjectLinkMenuItem;
-import com.google.gerrit.client.ui.Screen;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.HostPageData;
-import com.google.gerrit.common.data.SystemInfoService;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo;
-import com.google.gerrit.extensions.client.EditPreferencesInfo;
-import com.google.gerrit.extensions.client.GerritTopMenu;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.aria.client.Roles;
-import com.google.gwt.core.client.EntryPoint;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.dom.client.Document;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyDownEvent;
-import com.google.gwt.event.dom.client.KeyDownHandler;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.logical.shared.ValueChangeHandler;
-import com.google.gwt.event.shared.EventBus;
-import com.google.gwt.event.shared.SimpleEventBus;
-import com.google.gwt.http.client.Request;
-import com.google.gwt.http.client.RequestBuilder;
-import com.google.gwt.http.client.RequestCallback;
-import com.google.gwt.http.client.RequestException;
-import com.google.gwt.http.client.Response;
-import com.google.gwt.http.client.URL;
-import com.google.gwt.http.client.UrlBuilder;
-import com.google.gwt.user.client.Command;
-import com.google.gwt.user.client.Cookies;
-import com.google.gwt.user.client.History;
-import com.google.gwt.user.client.Window;
-import com.google.gwt.user.client.Window.Location;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.FocusPanel;
-import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
-import com.google.gwt.user.client.ui.InlineHTML;
-import com.google.gwt.user.client.ui.InlineLabel;
-import com.google.gwt.user.client.ui.RootPanel;
-import com.google.gwtexpui.clippy.client.CopyableLabel;
-import com.google.gwtexpui.user.client.UserAgent;
-import com.google.gwtexpui.user.client.ViewSite;
-import com.google.gwtjsonrpc.client.JsonDefTarget;
-import com.google.gwtjsonrpc.client.JsonUtil;
-import com.google.gwtjsonrpc.client.XsrfManager;
-import com.google.gwtorm.client.KeyUtil;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-public class Gerrit implements EntryPoint {
-  public static final GerritConstants C = GWT.create(GerritConstants.class);
-  public static final GerritMessages M = GWT.create(GerritMessages.class);
-  public static final GerritResources RESOURCES = GWT.create(GerritResources.class);
-  public static final SystemInfoService SYSTEM_SVC;
-  public static final EventBus EVENT_BUS = GWT.create(SimpleEventBus.class);
-  public static final Themer THEMER = GWT.create(Themer.class);
-  public static final String PROJECT_NAME_MENU_VAR = "${projectName}";
-  public static final String INDEX = "Documentation/index.html";
-
-  private static String myHost;
-  private static ServerInfo myServerInfo;
-  private static AccountInfo myAccount;
-  private static GeneralPreferences myPrefs;
-  private static UrlAliasMatcher urlAliasMatcher;
-  private static boolean hasDocumentation;
-  private static boolean docSearch;
-  private static String docUrl;
-  private static HostPageData.Theme myTheme;
-  private static String defaultScreenToken;
-  private static DiffPreferencesInfo myAccountDiffPref;
-  private static EditPreferencesInfo editPrefs;
-  private static String xGerritAuth;
-  private static boolean isNoteDbEnabled;
-
-  private static Map<String, LinkMenuBar> menuBars;
-
-  private static MorphingTabPanel menuLeft;
-  private static LinkMenuBar menuRight;
-  private static RootPanel topMenu;
-  private static RootPanel siteHeader;
-  private static RootPanel siteFooter;
-  private static RootPanel bottomMenu;
-  private static SearchPanel searchPanel;
-  private static final Dispatcher dispatcher = new Dispatcher();
-  private static ViewSite<Screen> body;
-  private static String lastChangeListToken;
-  private static String lastViewToken;
-  private static Anchor uiSwitcherLink;
-
-  static {
-    SYSTEM_SVC = GWT.create(SystemInfoService.class);
-    JsonUtil.bind(SYSTEM_SVC, "rpc/SystemInfoService");
-  }
-
-  static void upgradeUI(String token) {
-    History.newItem(Dispatcher.RELOAD_UI + token, false);
-    Window.Location.reload();
-  }
-
-  public static void displayLastChangeList() {
-    if (lastChangeListToken != null) {
-      display(lastChangeListToken);
-    } else if (isSignedIn()) {
-      display(PageLinks.MINE);
-    } else {
-      display(PageLinks.toChangeQuery("status:open"));
-    }
-  }
-
-  public static String getPriorView() {
-    return lastViewToken;
-  }
-
-  /**
-   * Load the screen at the given location, displaying when ready.
-   *
-   * <p>If the URL is not already pointing at this location, a new item will be added to the
-   * browser's history when the screen is fully loaded and displayed on the UI.
-   *
-   * @param token location to parse, load, and render.
-   */
-  public static void display(String token) {
-    if (body.getView() == null || !body.getView().displayToken(token)) {
-      dispatcher.display(token);
-      updateUiLink(token);
-    }
-  }
-
-  /**
-   * Load the screen passed, assuming token can be used to locate it.
-   *
-   * <p>The screen is loaded in the background. When it is ready to be visible a new item will be
-   * added to the browser's history, the screen will be made visible, and the window title may be
-   * updated.
-   *
-   * <p>If {@link Screen#isRequiresSignIn()} is true and the user is not signed in yet the screen
-   * instance will be discarded, sign-in will take place, and will redirect to this location upon
-   * success.
-   *
-   * @param token location that refers to {@code view}.
-   * @param view the view to load.
-   */
-  public static void display(String token, Screen view) {
-    if (view.isRequiresSignIn() && !isSignedIn()) {
-      doSignIn(token);
-    } else {
-      view.setToken(token);
-      if (isSignedIn()) {
-        LocalComments.saveInlineComments();
-      }
-      body.setView(view);
-      updateUiLink(token);
-    }
-  }
-
-  public static void selectMenu(LinkMenuBar bar) {
-    menuLeft.selectTab(menuLeft.getWidgetIndex(bar));
-  }
-
-  /**
-   * Update the current history token after a screen change.
-   *
-   * <p>The caller has already updated the UI, but wants to publish a different history token for
-   * the current browser state. This really only makes sense if the caller is a {@code TabPanel} and
-   * is firing an event when the tab changed to a different part.
-   *
-   * @param token new location that is already visible.
-   */
-  public static void updateImpl(String token) {
-    History.newItem(token, false);
-    dispatchHistoryHooks(token);
-  }
-
-  public static void setQueryString(String query) {
-    searchPanel.setText(query);
-  }
-
-  public static void setWindowTitle(Screen screen, String text) {
-    if (screen == body.getView()) {
-      if (text == null || text.length() == 0) {
-        Window.setTitle(M.windowTitle1(myHost));
-      } else {
-        Window.setTitle(M.windowTitle2(text, myHost));
-      }
-    }
-  }
-
-  public static int getHeaderFooterHeight() {
-    int h = bottomMenu.getOffsetHeight();
-    if (topMenu.isVisible()) {
-      h += topMenu.getOffsetHeight();
-    }
-    if (siteHeader.isVisible()) {
-      h += siteHeader.getOffsetHeight();
-    }
-    if (siteFooter.isVisible()) {
-      h += siteFooter.getOffsetHeight();
-    }
-    return h;
-  }
-
-  public static void setHeaderVisible(boolean visible) {
-    topMenu.setVisible(visible);
-    siteHeader.setVisible(visible && getUserPreferences().showSiteHeader());
-  }
-
-  public static boolean isHeaderVisible() {
-    return topMenu.isVisible();
-  }
-
-  public static String getDefaultScreenToken() {
-    return defaultScreenToken;
-  }
-
-  public static RootPanel getBottomMenu() {
-    return bottomMenu;
-  }
-
-  /** Get the public configuration data used by this Gerrit instance. */
-  public static ServerInfo info() {
-    return myServerInfo;
-  }
-
-  public static UrlAliasMatcher getUrlAliasMatcher() {
-    return urlAliasMatcher;
-  }
-
-  /** Site theme information (site specific colors)/ */
-  public static HostPageData.Theme getTheme() {
-    return myTheme;
-  }
-
-  /** @return the currently signed in user's account data; empty account data if no account */
-  public static AccountInfo getUserAccount() {
-    return myAccount;
-  }
-
-  /** @return access token to prove user identity during REST API calls. */
-  @Nullable
-  public static String getXGerritAuth() {
-    return xGerritAuth;
-  }
-
-  /**
-   * @return the preferences of the currently signed in user, the default preferences if not signed
-   *     in
-   */
-  public static GeneralPreferences getUserPreferences() {
-    return myPrefs;
-  }
-
-  /** @return the currently signed in users's diff preferences, or default values */
-  public static DiffPreferencesInfo getDiffPreferences() {
-    return myAccountDiffPref;
-  }
-
-  public static void setDiffPreferences(DiffPreferencesInfo accountDiffPref) {
-    myAccountDiffPref = accountDiffPref;
-  }
-
-  /** @return the edit preferences of the current user, null if not signed-in */
-  public static EditPreferencesInfo getEditPreferences() {
-    return editPrefs;
-  }
-
-  public static void setEditPreferences(EditPreferencesInfo p) {
-    editPrefs = p;
-  }
-
-  /** @return true if the user is currently authenticated */
-  public static boolean isSignedIn() {
-    return xGerritAuth != null;
-  }
-
-  /** Sign the user into the application. */
-  public static void doSignIn(String token) {
-    Location.assign(loginRedirect(token));
-  }
-
-  public static boolean isNoteDbEnabled() {
-    return isNoteDbEnabled;
-  }
-
-  public static String loginRedirect(String token) {
-    if (token == null) {
-      token = "";
-    } else if (token.startsWith("/")) {
-      token = token.substring(1);
-    }
-
-    return selfRedirect("login/") + URL.encodePathSegment("#/" + token);
-  }
-
-  public static String selfRedirect(String suffix) {
-    // Clean up the path. Users seem to like putting extra slashes into the URL
-    // which can break redirections by misinterpreting at either client or server.
-    String path = Location.getPath();
-    if (path == null || path.isEmpty()) {
-      path = "/";
-    } else {
-      while (path.startsWith("//")) {
-        path = path.substring(1);
-      }
-      while (path.endsWith("//")) {
-        path = path.substring(0, path.length() - 1);
-      }
-      if (!path.endsWith("/")) {
-        path = path + "/";
-      }
-    }
-
-    if (suffix != null) {
-      while (suffix.startsWith("/")) {
-        suffix = suffix.substring(1);
-      }
-      path += suffix;
-    }
-
-    UrlBuilder builder = new UrlBuilder();
-    builder.setProtocol(Location.getProtocol());
-    builder.setHost(Location.getHost());
-    String port = Location.getPort();
-    if (port != null && !port.isEmpty()) {
-      builder.setPort(Integer.parseInt(port));
-    }
-    builder.setPath(path);
-    return builder.buildString();
-  }
-
-  static void deleteSessionCookie() {
-    myAccount = AccountInfo.create(0, null, null, null);
-    myAccountDiffPref = null;
-    editPrefs = null;
-    myPrefs = GeneralPreferences.createDefault();
-    urlAliasMatcher.clearUserAliases();
-    xGerritAuth = null;
-    refreshMenuBar();
-
-    // If the cookie was HttpOnly, this request to delete it will
-    // most likely not be successful.  We can try anyway though.
-    //
-    Cookies.removeCookie("GerritAccount");
-  }
-
-  private void setXsrfToken() {
-    xGerritAuth = Cookies.getCookie(XSRF_COOKIE_NAME);
-    JsonUtil.setDefaultXsrfManager(
-        new XsrfManager() {
-          @Override
-          public String getToken(JsonDefTarget proxy) {
-            return xGerritAuth;
-          }
-
-          @Override
-          public void setToken(JsonDefTarget proxy, String token) {
-            // Ignore the request, we always rely upon the cookie.
-          }
-        });
-  }
-
-  @Override
-  public void onModuleLoad() {
-    if (!canLoadInIFrame()) {
-      UserAgent.assertNotInIFrame();
-    }
-    setXsrfToken();
-
-    KeyUtil.setEncoderImpl(
-        new KeyUtil.Encoder() {
-          @Override
-          public String encode(String e) {
-            e = URL.encodeQueryString(e);
-            e = fixPathImpl(e);
-            e = fixColonImpl(e);
-            e = fixDoubleQuote(e);
-            return e;
-          }
-
-          @Override
-          public String decode(String e) {
-            return URL.decodeQueryString(e);
-          }
-
-          private native String fixPathImpl(String path)
-              /*-{ return path.replace(/%2F/g, "/"); }-*/ ;
-
-          private native String fixColonImpl(String path)
-              /*-{ return path.replace(/%3A/g, ":"); }-*/ ;
-
-          private native String fixDoubleQuote(String path)
-              /*-{ return path.replace(/%22/g, '"'); }-*/ ;
-        });
-
-    initHostname();
-    Window.setTitle(M.windowTitle1(myHost));
-
-    RpcStatus.INSTANCE = new RpcStatus();
-    CallbackGroup cbg = new CallbackGroup();
-    getDocIndex(
-        cbg.add(
-            new GerritCallback<DocInfo>() {
-              @Override
-              public void onSuccess(DocInfo indexInfo) {
-                hasDocumentation = indexInfo != null;
-                docUrl = selfRedirect("/Documentation/");
-              }
-            }));
-    ConfigServerApi.serverInfo(
-        cbg.add(
-            new GerritCallback<ServerInfo>() {
-              @Override
-              public void onSuccess(ServerInfo info) {
-                myServerInfo = info;
-                urlAliasMatcher = new UrlAliasMatcher(info.urlAliases());
-                String du = info.gerrit().docUrl();
-                if (du != null && !du.isEmpty()) {
-                  hasDocumentation = true;
-                  docUrl = du;
-                }
-                docSearch = info.gerrit().docSearch();
-              }
-            }));
-    HostPageDataService hpd = GWT.create(HostPageDataService.class);
-    hpd.load(
-        cbg.addFinal(
-            new GerritCallback<HostPageData>() {
-              @Override
-              public void onSuccess(HostPageData result) {
-                Document.get().getElementById("gerrit_hostpagedata").removeFromParent();
-                myTheme = result.theme;
-                isNoteDbEnabled = result.isNoteDbEnabled;
-                if (result.accountDiffPref != null) {
-                  myAccountDiffPref = result.accountDiffPref;
-                }
-                if (result.accountDiffPref != null) {
-                  // TODO: Support options on the GetDetail REST endpoint so that it can
-                  // also return the preferences. Then we can fetch everything with a
-                  // single request and we don't need the callback group anymore.
-                  CallbackGroup cbg = new CallbackGroup();
-                  AccountApi.self()
-                      .view("detail")
-                      .get(
-                          cbg.add(
-                              new GerritCallback<AccountInfo>() {
-                                @Override
-                                public void onSuccess(AccountInfo result) {
-                                  myAccount = result;
-                                }
-                              }));
-                  AccountApi.self()
-                      .view("preferences")
-                      .get(
-                          cbg.add(
-                              new GerritCallback<GeneralPreferences>() {
-                                @Override
-                                public void onSuccess(GeneralPreferences prefs) {
-                                  myPrefs = prefs;
-                                  onModuleLoad2(result);
-                                }
-                              }));
-                  AccountApi.getEditPreferences(
-                      cbg.addFinal(
-                          new GerritCallback<EditPreferences>() {
-                            @Override
-                            public void onSuccess(EditPreferences prefs) {
-                              EditPreferencesInfo prefsInfo = new EditPreferencesInfo();
-                              prefs.copyTo(prefsInfo);
-                              editPrefs = prefsInfo;
-                            }
-                          }));
-                } else {
-                  myAccount = AccountInfo.create(0, null, null, null);
-                  myPrefs = GeneralPreferences.createDefault();
-                  editPrefs = null;
-                  onModuleLoad2(result);
-                }
-              }
-            }));
-  }
-
-  private native boolean canLoadInIFrame() /*-{
-    return $wnd.gerrit_hostpagedata.canLoadInIFrame || false;
-  }-*/;
-
-  private static void initHostname() {
-    myHost = Location.getHostName();
-    final int d1 = myHost.indexOf('.');
-    if (d1 < 0) {
-      return;
-    }
-    final int d2 = myHost.indexOf('.', d1 + 1);
-    if (d2 >= 0) {
-      myHost = myHost.substring(0, d2);
-    }
-  }
-
-  private static void dispatchHistoryHooks(String token) {
-    ApiGlue.fireEvent("history", token);
-  }
-
-  private static String getUiSwitcherUrl(String token) {
-    UrlBuilder builder = new UrlBuilder();
-    builder.setProtocol(Location.getProtocol());
-    builder.setHost(Location.getHost());
-    String port = Location.getPort();
-    if (port != null && !port.isEmpty()) {
-      builder.setPort(Integer.parseInt(port));
-    }
-    String[] tokens = token.split("@", 2);
-    if (Location.getPath().endsWith("/") && tokens[0].startsWith("/")) {
-      tokens[0] = tokens[0].substring(1);
-    }
-    if (tokens[0].startsWith("projects/") && tokens[0].contains(",dashboards/")) {
-      // Rewrite project dashboard URIs to a new format, because otherwise
-      // "/projects/..." would be served as an API request.
-      tokens[0] = "p/" + tokens[0].substring("projects/".length());
-      tokens[0] = tokens[0].replace(",dashboards/", "/+/dashboard/");
-    }
-    builder.setPath(Location.getPath() + tokens[0]);
-    if (tokens.length == 2) {
-      builder.setHash(tokens[1]);
-    }
-    builder.setParameter("polygerrit", "1");
-    return builder.buildString();
-  }
-
-  private static void populateBottomMenu(RootPanel btmmenu, HostPageData hpd) {
-    String vs = hpd.version;
-    if (vs == null || vs.isEmpty()) {
-      vs = "dev";
-    }
-
-    btmmenu.add(new InlineHTML(M.poweredBy(vs)));
-
-    btmmenu.add(new InlineLabel(" | "));
-    uiSwitcherLink = new Anchor(C.newUi(), getUiSwitcherUrl(History.getToken()));
-    uiSwitcherLink.setStyleName("");
-    btmmenu.add(uiSwitcherLink);
-
-    String reportBugUrl = info().gerrit().reportBugUrl();
-    if (reportBugUrl != null) {
-      String reportBugText = info().gerrit().reportBugText();
-      Anchor a = new Anchor(reportBugText == null ? C.reportBug() : reportBugText, reportBugUrl);
-      a.setTarget("_blank");
-      a.setStyleName("");
-      btmmenu.add(new InlineLabel(" | "));
-      btmmenu.add(a);
-    }
-    btmmenu.add(new InlineLabel(" | "));
-    btmmenu.add(new InlineLabel(C.keyHelp()));
-  }
-
-  private static void updateUiLink(String token) {
-    if (uiSwitcherLink != null) {
-      uiSwitcherLink.setHref(getUiSwitcherUrl(token));
-    }
-  }
-
-  private void onModuleLoad2(HostPageData hpd) {
-    RESOURCES.gwt_override().ensureInjected();
-    RESOURCES.css().ensureInjected();
-
-    topMenu = RootPanel.get("gerrit_topmenu");
-    final RootPanel gStarting = RootPanel.get("gerrit_startinggerrit");
-    final RootPanel gBody = RootPanel.get("gerrit_body");
-    bottomMenu = RootPanel.get("gerrit_btmmenu");
-
-    topMenu.setStyleName(RESOURCES.css().gerritTopMenu());
-    gBody.setStyleName(RESOURCES.css().gerritBody());
-
-    final Grid menuLine = new Grid(1, 3);
-    menuLeft = new MorphingTabPanel();
-    menuRight = new LinkMenuBar();
-    searchPanel = new SearchPanel();
-    menuLeft.setStyleName(RESOURCES.css().topmenuMenuLeft());
-    menuLine.setStyleName(RESOURCES.css().topmenu());
-    topMenu.add(menuLine);
-    final FlowPanel menuRightPanel = new FlowPanel();
-    menuRightPanel.setStyleName(RESOURCES.css().topmenuMenuRight());
-    menuRightPanel.add(searchPanel);
-    menuRightPanel.add(menuRight);
-    menuLine.setWidget(0, 0, menuLeft);
-    menuLine.setWidget(0, 1, new FlowPanel());
-    menuLine.setWidget(0, 2, menuRightPanel);
-    final CellFormatter fmt = menuLine.getCellFormatter();
-    fmt.setStyleName(0, 0, RESOURCES.css().topmenuTDmenu());
-    fmt.setStyleName(0, 1, RESOURCES.css().topmenuTDglue());
-    fmt.setStyleName(0, 2, RESOURCES.css().topmenuTDmenu());
-
-    siteHeader = RootPanel.get("gerrit_header");
-    siteFooter = RootPanel.get("gerrit_footer");
-
-    body =
-        new ViewSite<Screen>() {
-          @Override
-          protected void onShowView(Screen view) {
-            String token = view.getToken();
-            History.newItem(token, false);
-            dispatchHistoryHooks(token);
-
-            if (view instanceof ChangeListScreen) {
-              lastChangeListToken = token;
-            }
-
-            super.onShowView(view);
-            view.onShowView();
-            lastViewToken = token;
-          }
-        };
-    gBody.add(body);
-
-    JsonUtil.addRpcStartHandler(RpcStatus.INSTANCE);
-    JsonUtil.addRpcCompleteHandler(RpcStatus.INSTANCE);
-
-    gStarting.getElement().getParentElement().removeChild(gStarting.getElement());
-    RootPanel.detachNow(gStarting);
-    ApiGlue.init();
-
-    applyUserPreferences();
-    populateBottomMenu(bottomMenu, hpd);
-    refreshMenuBar();
-
-    History.addValueChangeHandler(
-        new ValueChangeHandler<String>() {
-          @Override
-          public void onValueChange(ValueChangeEvent<String> event) {
-            display(event.getValue());
-          }
-        });
-    JumpKeys.register(body);
-
-    saveDefaultTheme();
-    if (hpd.messages != null) {
-      new MessageOfTheDayBar(hpd.messages).show();
-    }
-    PluginLoader.load(
-        hpd.plugins,
-        hpd.pluginsLoadTimeout,
-        new GerritCallback<VoidResult>() {
-          @Override
-          public void onSuccess(VoidResult result) {
-            String token = History.getToken();
-            if (token.isEmpty()) {
-              token = isSignedIn() ? PageLinks.MINE : PageLinks.toChangeQuery("status:open");
-            }
-            display(token);
-          }
-        });
-  }
-
-  private void saveDefaultTheme() {
-    THEMER.init(
-        Document.get().getElementById("gerrit_sitecss"),
-        Document.get().getElementById("gerrit_header"),
-        Document.get().getElementById("gerrit_footer"));
-  }
-
-  public static void refreshMenuBar() {
-    menuLeft.clear();
-    menuRight.clear();
-
-    menuBars = new HashMap<>();
-
-    boolean signedIn = isSignedIn();
-    AuthInfo authInfo = info().auth();
-    LinkMenuBar m;
-
-    m = new LinkMenuBar();
-    menuBars.put(GerritTopMenu.ALL.menuName, m);
-    addLink(m, C.menuAllOpen(), PageLinks.toChangeQuery("status:open"));
-    addLink(m, C.menuAllMerged(), PageLinks.toChangeQuery("status:merged"));
-    addLink(m, C.menuAllAbandoned(), PageLinks.toChangeQuery("status:abandoned"));
-    menuLeft.add(m, C.menuAll());
-
-    if (signedIn) {
-      LinkMenuBar myBar = new LinkMenuBar();
-      menuBars.put(GerritTopMenu.MY.menuName, myBar);
-
-      if (myPrefs.my() != null) {
-        myBar.clear();
-        String url = null;
-        List<TopMenuItem> myMenuItems = Natives.asList(myPrefs.my());
-        if (!myMenuItems.isEmpty()) {
-          if (myMenuItems.get(0).getUrl().startsWith("#")) {
-            url = myMenuItems.get(0).getUrl().substring(1);
-          }
-          for (TopMenuItem item : myMenuItems) {
-            addExtensionLink(myBar, item);
-          }
-        }
-        defaultScreenToken = url;
-      }
-
-      menuLeft.add(myBar, C.menuMine());
-      menuLeft.selectTab(1);
-    } else {
-      menuLeft.selectTab(0);
-    }
-
-    final LinkMenuBar projectsBar = new LinkMenuBar();
-    menuBars.put(GerritTopMenu.PROJECTS.menuName, projectsBar);
-    addLink(projectsBar, C.menuProjectsList(), PageLinks.ADMIN_PROJECTS);
-    projectsBar.addItem(new ProjectLinkMenuItem(C.menuProjectsInfo(), ProjectScreen.INFO));
-    projectsBar.addItem(new ProjectLinkMenuItem(C.menuProjectsBranches(), ProjectScreen.BRANCHES));
-    projectsBar.addItem(new ProjectLinkMenuItem(C.menuProjectsTags(), ProjectScreen.TAGS));
-    projectsBar.addItem(new ProjectLinkMenuItem(C.menuProjectsAccess(), ProjectScreen.ACCESS));
-    final LinkMenuItem dashboardsMenuItem =
-        new ProjectLinkMenuItem(C.menuProjectsDashboards(), ProjectScreen.DASHBOARDS) {
-          @Override
-          protected boolean match(String token) {
-            return super.match(token)
-                || (!getTargetHistoryToken().isEmpty()
-                    && ("/admin" + token).startsWith(getTargetHistoryToken()));
-          }
-        };
-    projectsBar.addItem(dashboardsMenuItem);
-    menuLeft.add(projectsBar, C.menuProjects());
-
-    if (signedIn) {
-      final LinkMenuBar peopleBar = new LinkMenuBar();
-      menuBars.put(GerritTopMenu.PEOPLE.menuName, peopleBar);
-      final LinkMenuItem groupsListMenuItem =
-          addLink(peopleBar, C.menuPeopleGroupsList(), PageLinks.ADMIN_GROUPS);
-      menuLeft.add(peopleBar, C.menuPeople());
-
-      final LinkMenuBar pluginsBar = new LinkMenuBar();
-      menuBars.put(GerritTopMenu.PLUGINS.menuName, pluginsBar);
-      AccountCapabilities.all(
-          new GerritCallback<AccountCapabilities>() {
-            @Override
-            public void onSuccess(AccountCapabilities result) {
-              if (result.canPerform(CREATE_PROJECT)) {
-                insertLink(
-                    projectsBar,
-                    C.menuProjectsCreate(),
-                    PageLinks.ADMIN_CREATE_PROJECT,
-                    projectsBar.getWidgetIndex(dashboardsMenuItem) + 1);
-              }
-              if (result.canPerform(CREATE_GROUP)) {
-                insertLink(
-                    peopleBar,
-                    C.menuPeopleGroupsCreate(),
-                    PageLinks.ADMIN_CREATE_GROUP,
-                    peopleBar.getWidgetIndex(groupsListMenuItem) + 1);
-              }
-              if (result.canPerform(VIEW_PLUGINS)) {
-                insertLink(pluginsBar, C.menuPluginsInstalled(), PageLinks.ADMIN_PLUGINS, 0);
-                menuLeft.insert(
-                    pluginsBar, C.menuPlugins(), menuLeft.getWidgetIndex(peopleBar) + 1);
-              }
-            }
-          },
-          CREATE_PROJECT,
-          CREATE_GROUP,
-          VIEW_PLUGINS);
-    }
-
-    if (hasDocumentation) {
-      m = new LinkMenuBar();
-      menuBars.put(GerritTopMenu.DOCUMENTATION.menuName, m);
-      addDocLink(m, C.menuDocumentationTOC(), "index.html");
-      addDocLink(m, C.menuDocumentationSearch(), "user-search.html");
-      addDocLink(m, C.menuDocumentationUpload(), "user-upload.html");
-      addDocLink(m, C.menuDocumentationAccess(), "access-control.html");
-      addDocLink(m, C.menuDocumentationAPI(), "rest-api.html");
-      addDocLink(m, C.menuDocumentationProjectOwnerGuide(), "intro-project-owner.html");
-      menuLeft.add(m, C.menuDocumentation());
-    }
-
-    if (signedIn) {
-      whoAmI(!authInfo.isClientSslCertLdap());
-    } else {
-      switch (authInfo.authType()) {
-        case CLIENT_SSL_CERT_LDAP:
-          break;
-
-        case OPENID:
-          menuRight.addItem(
-              C.menuRegister(),
-              new Command() {
-                @Override
-                public void execute() {
-                  String t = History.getToken();
-                  if (t == null) {
-                    t = "";
-                  }
-                  doSignIn(PageLinks.REGISTER + t);
-                }
-              });
-          menuRight.addItem(
-              C.menuSignIn(),
-              new Command() {
-                @Override
-                public void execute() {
-                  doSignIn(History.getToken());
-                }
-              });
-          break;
-
-        case OAUTH:
-          menuRight.addItem(
-              C.menuSignIn(),
-              new Command() {
-                @Override
-                public void execute() {
-                  doSignIn(History.getToken());
-                }
-              });
-          break;
-
-        case OPENID_SSO:
-          menuRight.addItem(
-              C.menuSignIn(),
-              new Command() {
-                @Override
-                public void execute() {
-                  doSignIn(History.getToken());
-                }
-              });
-          break;
-
-        case HTTP:
-        case HTTP_LDAP:
-          if (authInfo.loginUrl() != null) {
-            String signinText =
-                authInfo.loginText() == null ? C.menuSignIn() : authInfo.loginText();
-            menuRight.add(anchor(signinText, authInfo.loginUrl()));
-          }
-          break;
-
-        case LDAP:
-        case LDAP_BIND:
-        case CUSTOM_EXTENSION:
-          if (authInfo.registerUrl() != null) {
-            String registerText =
-                authInfo.registerText() == null ? C.menuRegister() : authInfo.registerText();
-            menuRight.add(anchor(registerText, authInfo.registerUrl()));
-          }
-          menuRight.addItem(
-              C.menuSignIn(),
-              new Command() {
-                @Override
-                public void execute() {
-                  doSignIn(History.getToken());
-                }
-              });
-          break;
-
-        case DEVELOPMENT_BECOME_ANY_ACCOUNT:
-          menuRight.add(anchor("Become", loginRedirect("")));
-          break;
-      }
-    }
-    ConfigServerApi.topMenus(
-        new GerritCallback<TopMenuList>() {
-          @Override
-          public void onSuccess(TopMenuList result) {
-            List<TopMenu> topMenuExtensions = Natives.asList(result);
-            for (TopMenu menu : topMenuExtensions) {
-              String name = menu.getName();
-              LinkMenuBar existingBar = menuBars.get(name);
-              LinkMenuBar bar = existingBar != null ? existingBar : new LinkMenuBar();
-              for (TopMenuItem item : Natives.asList(menu.getItems())) {
-                addMenuLink(bar, item);
-              }
-              if (existingBar == null) {
-                menuBars.put(name, bar);
-                menuLeft.add(bar, name);
-              }
-            }
-          }
-        });
-  }
-
-  public static void refreshUserPreferences() {
-    if (isSignedIn()) {
-      AccountApi.self()
-          .view("preferences")
-          .get(
-              new GerritCallback<GeneralPreferences>() {
-                @Override
-                public void onSuccess(GeneralPreferences prefs) {
-                  setUserPreferences(prefs);
-                }
-              });
-    } else {
-      setUserPreferences(GeneralPreferences.createDefault());
-    }
-  }
-
-  public static void setUserPreferences(GeneralPreferences prefs) {
-    myPrefs = prefs;
-    applyUserPreferences();
-    refreshMenuBar();
-  }
-
-  private static void applyUserPreferences() {
-    CopyableLabel.setFlashEnabled(myPrefs.useFlashClipboard());
-    if (siteHeader != null) {
-      siteHeader.setVisible(myPrefs.showSiteHeader());
-    }
-    if (siteFooter != null) {
-      siteFooter.setVisible(myPrefs.showSiteHeader());
-    }
-    FormatUtil.setPreferences(myPrefs);
-    urlAliasMatcher.updateUserAliases(myPrefs.urlAliases());
-  }
-
-  public static boolean hasDocSearch() {
-    return docSearch;
-  }
-
-  private static void getDocIndex(AsyncCallback<DocInfo> cb) {
-    RequestBuilder req = new RequestBuilder(RequestBuilder.HEAD, GWT.getHostPageBaseURL() + INDEX);
-    req.setCallback(
-        new RequestCallback() {
-          @Override
-          public void onResponseReceived(Request req, Response resp) {
-            switch (resp.getStatusCode()) {
-              case Response.SC_OK:
-              case Response.SC_MOVED_PERMANENTLY:
-              case Response.SC_MOVED_TEMPORARILY:
-                cb.onSuccess(DocInfo.create());
-                break;
-              default:
-                cb.onSuccess(null);
-                break;
-            }
-          }
-
-          @Override
-          public void onError(Request request, Throwable e) {
-            cb.onFailure(e);
-          }
-        });
-    try {
-      req.send();
-    } catch (RequestException e) {
-      cb.onFailure(e);
-    }
-  }
-
-  private static void whoAmI(boolean canLogOut) {
-    AccountInfo account = getUserAccount();
-    final UserPopupPanel userPopup = new UserPopupPanel(account, canLogOut, true);
-    final FlowPanel userSummaryPanel = new FlowPanel();
-    class PopupHandler implements KeyDownHandler, ClickHandler {
-      private void showHidePopup() {
-        if (userPopup.isShowing() && userPopup.isVisible()) {
-          userPopup.hide();
-        } else {
-          userPopup.showRelativeTo(userSummaryPanel);
-        }
-      }
-
-      @Override
-      public void onClick(ClickEvent event) {
-        showHidePopup();
-      }
-
-      @Override
-      public void onKeyDown(KeyDownEvent event) {
-        if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
-          showHidePopup();
-          event.preventDefault();
-        }
-      }
-    }
-    final PopupHandler popupHandler = new PopupHandler();
-    final InlineLabel l = new InlineLabel(FormatUtil.name(account));
-    l.setStyleName(RESOURCES.css().menuBarUserName());
-    final AvatarImage avatar = new AvatarImage(account, 26, false);
-    avatar.setStyleName(RESOURCES.css().menuBarUserNameAvatar());
-    userSummaryPanel.setStyleName(RESOURCES.css().menuBarUserNamePanel());
-    userSummaryPanel.add(l);
-    userSummaryPanel.add(avatar);
-    // "BLACK DOWN-POINTING SMALL TRIANGLE"
-    userSummaryPanel.add(new InlineLabel(" \u25be"));
-    userPopup.addAutoHidePartner(userSummaryPanel.getElement());
-    FocusPanel fp = new FocusPanel(userSummaryPanel);
-    fp.setStyleName(RESOURCES.css().menuBarUserNameFocusPanel());
-    fp.addKeyDownHandler(popupHandler);
-    fp.addClickHandler(popupHandler);
-    menuRight.add(fp);
-  }
-
-  private static Anchor anchor(String text, String to) {
-    final Anchor a = new Anchor(text, to);
-    a.setStyleName(RESOURCES.css().menuItem());
-    Roles.getMenuitemRole().set(a.getElement());
-    return a;
-  }
-
-  private static LinkMenuItem addLink(final LinkMenuBar m, String text, String historyToken) {
-    LinkMenuItem i = new LinkMenuItem(text, historyToken);
-    m.addItem(i);
-    return i;
-  }
-
-  private static void insertLink(
-      final LinkMenuBar m, String text, String historyToken, int beforeIndex) {
-    m.insertItem(new LinkMenuItem(text, historyToken), beforeIndex);
-  }
-
-  private static LinkMenuItem addProjectLink(LinkMenuBar m, TopMenuItem item) {
-    LinkMenuItem i =
-        new ProjectLinkMenuItem(item.getName(), item.getUrl()) {
-          @Override
-          protected void onScreenLoad(Project.NameKey project) {
-            String p = panel.replace(PROJECT_NAME_MENU_VAR, URL.encodeQueryString(project.get()));
-            if (!panel.startsWith("/x/") && !isAbsolute(panel)) {
-              UrlBuilder builder = new UrlBuilder();
-              builder.setProtocol(Location.getProtocol());
-              builder.setHost(Location.getHost());
-              String port = Location.getPort();
-              if (port != null && !port.isEmpty()) {
-                builder.setPort(Integer.parseInt(port));
-              }
-              builder.setPath(Location.getPath());
-              p = builder.buildString() + p;
-            }
-            getElement().setPropertyString("href", p);
-          }
-
-          @Override
-          public void go() {
-            String href = getElement().getPropertyString("href");
-            if (href.startsWith("#")) {
-              super.go();
-            } else {
-              Window.open(href, getElement().getPropertyString("target"), "");
-            }
-          }
-        };
-    if (item.getTarget() != null && !item.getTarget().isEmpty()) {
-      i.getElement().setAttribute("target", item.getTarget());
-    }
-    if (item.getId() != null) {
-      i.getElement().setAttribute("id", item.getId());
-    }
-    m.addItem(i);
-    return i;
-  }
-
-  private static void addDocLink(LinkMenuBar m, String text, String href) {
-    final Anchor atag = anchor(text, docUrl + href);
-    atag.setTarget("_blank");
-    m.add(atag);
-  }
-
-  private static void addMenuLink(LinkMenuBar m, TopMenuItem item) {
-    if (item.getUrl().contains(PROJECT_NAME_MENU_VAR)) {
-      addProjectLink(m, item);
-    } else {
-      addExtensionLink(m, item);
-    }
-  }
-
-  private static void addExtensionLink(LinkMenuBar m, TopMenuItem item) {
-    if (item.getUrl().startsWith("#") && (item.getTarget() == null || item.getTarget().isEmpty())) {
-      LinkMenuItem a = new LinkMenuItem(item.getName(), item.getUrl().substring(1));
-      if (item.getId() != null) {
-        a.getElement().setAttribute("id", item.getId());
-      }
-      m.addItem(a);
-    } else {
-      Anchor atag =
-          anchor(
-              item.getName(),
-              isAbsolute(item.getUrl()) ? item.getUrl() : selfRedirect(item.getUrl()));
-      if (item.getTarget() != null && !item.getTarget().isEmpty()) {
-        atag.setTarget(item.getTarget());
-      }
-      if (item.getId() != null) {
-        atag.getElement().setAttribute("id", item.getId());
-      }
-      m.add(atag);
-    }
-  }
-
-  private static boolean isAbsolute(String url) {
-    return url.matches("^https?://.*");
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
deleted file mode 100644
index eae3431..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
+++ /dev/null
@@ -1,199 +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.client;
-
-import com.google.gwt.i18n.client.Constants;
-
-public interface GerritConstants extends Constants {
-  String menuSignIn();
-
-  String menuRegister();
-
-  String reportBug();
-
-  String loadingPlugins();
-
-  String signInDialogTitle();
-
-  String signInDialogGoAnonymous();
-
-  String linkIdentityDialogTitle();
-
-  String registerDialogTitle();
-
-  String loginTypeUnsupported();
-
-  String errorTitle();
-
-  String errorDialogContinue();
-
-  String warnTitle();
-
-  String confirmationDialogOk();
-
-  String confirmationDialogCancel();
-
-  String branchCreationDialogTitle();
-
-  String branchCreationConfirmationMessage();
-
-  String tagCreationDialogTitle();
-
-  String tagCreationConfirmationMessage();
-
-  String branchDeletionDialogTitle();
-
-  String branchDeletionConfirmationMessage();
-
-  String tagDeletionDialogTitle();
-
-  String tagDeletionConfirmationMessage();
-
-  String newUi();
-
-  String notSignedInTitle();
-
-  String notSignedInBody();
-
-  String notFoundTitle();
-
-  String notFoundBody();
-
-  String noSuchAccountTitle();
-
-  String noSuchGroupTitle();
-
-  String inactiveAccountBody();
-
-  String labelNotApplicable();
-
-  String menuAll();
-
-  String menuAllOpen();
-
-  String menuAllMerged();
-
-  String menuAllAbandoned();
-
-  String menuMine();
-
-  String menuMyChanges();
-
-  String menuMyWatchedChanges();
-
-  String menuMyStarredChanges();
-
-  String menuMyDraftComments();
-
-  String menuDiff();
-
-  String menuDiffCommit();
-
-  String menuDiffPreferences();
-
-  String menuDiffPatchSets();
-
-  String menuDiffFiles();
-
-  String menuProjects();
-
-  String menuProjectsList();
-
-  String menuProjectsInfo();
-
-  String menuProjectsBranches();
-
-  String menuProjectsTags();
-
-  String menuProjectsAccess();
-
-  String menuProjectsDashboards();
-
-  String menuProjectsCreate();
-
-  String menuPeople();
-
-  String menuPeopleGroupsList();
-
-  String menuPeopleGroupsCreate();
-
-  String menuPlugins();
-
-  String menuPluginsInstalled();
-
-  String menuDocumentation();
-
-  String menuDocumentationTOC();
-
-  String menuDocumentationSearch();
-
-  String menuDocumentationUpload();
-
-  String menuDocumentationAccess();
-
-  String menuDocumentationAPI();
-
-  String menuDocumentationProjectOwnerGuide();
-
-  String searchHint();
-
-  String searchButton();
-
-  String rpcStatusWorking();
-
-  String sectionNavigation();
-
-  String sectionActions();
-
-  String keySearch();
-
-  String keyEditor();
-
-  String keyHelp();
-
-  String sectionJumping();
-
-  String jumpAllOpen();
-
-  String jumpAllMerged();
-
-  String jumpAllAbandoned();
-
-  String jumpMine();
-
-  String jumpMineWatched();
-
-  String jumpMineStarred();
-
-  String jumpMineDraftComments();
-
-  String projectAccessError();
-
-  String projectAccessProposeForReviewHint();
-
-  String userCannotVoteToolTip();
-
-  String stringListPanelAdd();
-
-  String stringListPanelDelete();
-
-  String stringListPanelUp();
-
-  String stringListPanelDown();
-
-  String searchDropdownChanges();
-
-  String searchDropdownDoc();
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
deleted file mode 100644
index 2819d22..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
+++ /dev/null
@@ -1,121 +0,0 @@
-menuSignIn = Sign In
-menuRegister = Register
-reportBug = Report Bug
-loadingPlugins = Loading plugins ...
-
-signInDialogTitle = Code Review - Sign In
-signInDialogGoAnonymous = Go Anonymous
-
-linkIdentityDialogTitle = Code Review - Link Identity
-registerDialogTitle = Code Review - Register New Account
-loginTypeUnsupported = Sign in is not available.
-
-errorTitle = Code Review - Error
-errorDialogContinue = Continue
-warnTitle = Code Review - Warning
-
-confirmationDialogOk = OK
-confirmationDialogCancel = Cancel
-
-branchCreationDialogTitle = Branch Creation
-branchCreationConfirmationMessage = The following branch was successfully created:
-
-tagCreationDialogTitle = Tag Creation
-tagCreationConfirmationMessage = The following tag was successfully created:
-
-branchDeletionDialogTitle = Branch Deletion
-branchDeletionConfirmationMessage = Do you really want to delete the following branches?
-
-tagDeletionDialogTitle = Tag Deletion
-tagDeletionConfirmationMessage = Do you really want to delete the following tags?
-
-newUi = Switch to New UI
-
-notSignedInTitle = Code Review - Session Expired
-notSignedInBody = <b>Session Expired</b>\
-<p>You are no longer signed in to Gerrit Code Review.</p>\
-<p>To continue, please sign-in again.</p>
-
-notFoundTitle = Not Found
-notFoundBody = The page you requested was not found, or you do not have permission to view this page.
-noSuchAccountTitle = Code Review - Unknown User
-
-noSuchGroupTitle = Code Review - Unknown Group
-
-inactiveAccountBody = This user is currently inactive.
-
-labelNotApplicable = Label not applicable
-
-menuAll = All
-menuAllOpen = Open
-menuAllMerged = Merged
-menuAllAbandoned = Abandoned
-
-menuMine = My
-menuMyChanges = Changes
-menuMyStarredChanges = Starred Changes
-menuMyWatchedChanges = Watched Changes
-menuMyDraftComments = Draft Comments
-
-menuDiff = Differences
-menuDiffCommit = Commit Message
-menuDiffPreferences = Preferences
-menuDiffPatchSets = Patch Sets
-menuDiffFiles = Files
-
-menuProjects = Projects
-menuProjectsList = List
-menuProjectsInfo = General
-menuProjectsBranches = Branches
-menuProjectsTags = Tags
-menuProjectsAccess = Access
-menuProjectsDashboards = Dashboards
-menuProjectsCreate = Create New Project
-
-menuPeople = People
-menuPeopleGroupsList = List Groups
-menuPeopleGroupsCreate = Create New Group
-
-menuPlugins = Plugins
-menuPluginsInstalled = Installed
-
-menuDocumentation = Documentation
-menuDocumentationTOC = Table of Contents
-menuDocumentationSearch = Searching
-menuDocumentationUpload = Uploading
-menuDocumentationAccess = Access Controls
-menuDocumentationAPI = REST API
-menuDocumentationProjectOwnerGuide = Project Owner Guide
-
-searchHint = Search term
-searchButton = Search
-
-rpcStatusWorking = Working ...
-
-sectionNavigation = Navigation
-sectionActions = Actions
-keySearch = Search
-keyEditor = Open Inline Editor
-keyHelp = Press '?' to view keyboard shortcuts
-
-sectionJumping = Jumping
-jumpAllOpen = Go to all open changes
-jumpAllMerged = Go to all merged changes
-jumpAllAbandoned = Go to all abandoned changes
-jumpMine = Go to my dashboard
-jumpMineWatched = Go to watched changes
-jumpMineStarred = Go to starred changes
-jumpMineDraftComments = Go to draft comments
-
-projectAccessError = You don't have permissions to modify the access rights for the following refs:
-projectAccessProposeForReviewHint = You may propose these modifications to the project owners by clicking on 'Save for Review'.
-
-userCannotVoteToolTip = User cannot vote in this category
-
-stringListPanelAdd = Add
-stringListPanelDelete = Delete
-stringListPanelUp = Up
-stringListPanelDown = Down
-
-searchDropdownChanges = Changes
-searchDropdownDoc = Docs
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
deleted file mode 100644
index 968104c..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
+++ /dev/null
@@ -1,299 +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.client;
-
-import com.google.gwt.resources.client.CssResource;
-
-public interface GerritCss extends CssResource {
-  String accountDashboard();
-
-  String accountInfoBlock();
-
-  String accountLinkPanel();
-
-  String accountPassword();
-
-  String accountUsername();
-
-  String activeRow();
-
-  String addBranch();
-
-  String addMemberTextBox();
-
-  String addSshKeyPanel();
-
-  String addWatchPanel();
-
-  String avatarInfoPanel();
-
-  String bottomheader();
-
-  String branchTableDeleteButton();
-
-  String branchTablePrevNextLinks();
-
-  String cAPPROVAL();
-
-  String cASSIGNEE();
-
-  String cASSIGNEDTOME();
-
-  String cLastUpdate();
-
-  String cOWNER();
-
-  String cSIZE();
-
-  String cSUBJECT();
-
-  String cSTATUS();
-
-  String changeSize();
-
-  String changeTable();
-
-  String changeTablePrevNextLinks();
-
-  String commentedActionDialog();
-
-  String commentedActionMessage();
-
-  String contributorAgreementAlreadySubmitted();
-
-  String contributorAgreementButton();
-
-  String contributorAgreementLegal();
-
-  String contributorAgreementShortDescription();
-
-  String createProjectPanel();
-
-  String dataCell();
-
-  String dataCellHidden();
-
-  String dataHeader();
-
-  String dataHeaderHidden();
-
-  String downloadBox();
-
-  String downloadBoxTable();
-
-  String downloadBoxTableCommandColumn();
-
-  String downloadBoxSpacer();
-
-  String downloadBoxScheme();
-
-  String downloadBoxCopyLabel();
-
-  String downloadLink();
-
-  String downloadLinkCopyLabel();
-
-  String downloadLinkHeader();
-
-  String downloadLinkHeaderGap();
-
-  String downloadLinkList();
-
-  String downloadLink_Active();
-
-  String editHeadButton();
-
-  String emptySection();
-
-  String errorDialog();
-
-  String errorDialogButtons();
-
-  String errorDialogErrorType();
-
-  String errorDialogGlass();
-
-  String errorDialogTitle();
-
-  String extensionPanel();
-
-  String loadingPluginsDialog();
-
-  String gerritBody();
-
-  String gerritTopMenu();
-
-  String greenCheckClass();
-
-  String groupDescriptionPanel();
-
-  String groupIncludesTable();
-
-  String groupMembersTable();
-
-  String groupName();
-
-  String groupNamePanel();
-
-  String groupNameTextBox();
-
-  String groupOptionsPanel();
-
-  String groupOwnerPanel();
-
-  String groupOwnerTextBox();
-
-  String groupUUIDPanel();
-
-  String header();
-
-  String iconCell();
-
-  String iconHeader();
-
-  String identityUntrustedExternalId();
-
-  String infoBlock();
-
-  String inputFieldTypeHint();
-
-  String labelNotApplicable();
-
-  String leftMostCell();
-
-  String link();
-
-  String linkMenuBar();
-
-  String linkMenuItemNotLast();
-
-  String maxObjectSizeLimitEffectiveLabel();
-
-  String menuBarUserName();
-
-  String menuBarUserNameAvatar();
-
-  String menuBarUserNameFocusPanel();
-
-  String menuBarUserNamePanel();
-
-  String menuItem();
-
-  String menuScreenMenuBar();
-
-  String needsReview();
-
-  String negscore();
-
-  String oauthExpires();
-
-  String oauthInfoBlock();
-
-  String oauthPanel();
-
-  String oauthPanelCookieEntry();
-
-  String oauthPanelCookieHeading();
-
-  String oauthPanelNetRCEntry();
-
-  String oauthPanelNetRCHeading();
-
-  String oauthToken();
-
-  String pagingLink();
-
-  String patchSetActions();
-
-  String pluginProjectConfigInheritedValue();
-
-  String pluginsTable();
-
-  String posscore();
-
-  String projectActions();
-
-  String projectFilterLabel();
-
-  String projectFilterPanel();
-
-  String projectNameColumn();
-
-  String queryIcon();
-
-  String rebaseContentPanel();
-
-  String rebaseSuggestBox();
-
-  String registerScreenExplain();
-
-  String registerScreenNextLinks();
-
-  String registerScreenSection();
-
-  String rpcStatus();
-
-  String screen();
-
-  String screenHeader();
-
-  String searchPanel();
-
-  String suggestBoxPopup();
-
-  String sectionHeader();
-
-  String singleLine();
-
-  String smallHeading();
-
-  String specialBranchDataCell();
-
-  String specialBranchIconCell();
-
-  String sshHostKeyPanel();
-
-  String sshHostKeyPanelFingerprintData();
-
-  String sshHostKeyPanelHeading();
-
-  String sshHostKeyPanelKnownHostEntry();
-
-  String sshKeyPanelEncodedKey();
-
-  String sshKeyPanelInvalid();
-
-  String sshKeyTable();
-
-  String stringListPanelButtons();
-
-  String topmenu();
-
-  String topmenuMenuLeft();
-
-  String topmenuMenuRight();
-
-  String topmenuTDglue();
-
-  String topmenuTDmenu();
-
-  String topmost();
-
-  String userInfoPopup();
-
-  String usernameField();
-
-  String watchedProjectFilter();
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.java
deleted file mode 100644
index 980529a..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.java
+++ /dev/null
@@ -1,51 +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.client;
-
-import com.google.gwt.i18n.client.Messages;
-
-public interface GerritMessages extends Messages {
-  String windowTitle1(String hostname);
-
-  String windowTitle2(String section, String hostname);
-
-  String poweredBy(String version);
-
-  String noSuchAccountMessage(String who);
-
-  String noSuchGroupMessage(String who);
-
-  String nameAlreadyUsedBody(String alreadyUsedName);
-
-  String branchCreationFailed(String branchName, String error);
-
-  String invalidBranchName(String branchName);
-
-  String invalidRevision(String revision);
-
-  String branchCreationNotAllowedUnderRefnamePrefix(String refnamePrefix);
-
-  String branchAlreadyExists(String branchName);
-
-  String branchCreationConflict(String branchName, String existingBranchName);
-
-  String pluginFailed(String scriptPath);
-
-  String cannotDownloadPlugin(String scriptPath);
-
-  String parentUpdateFailed(String message);
-
-  String fileCount(int fileNumber, int fileCount);
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties
deleted file mode 100644
index b2d67b8..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties
+++ /dev/null
@@ -1,21 +0,0 @@
-windowTitle1 = {0} Code Review
-windowTitle2 = {0} | {1} Code Review
-poweredBy = Powered by <a href="https://www.gerritcodereview.com/" target="_blank">Gerrit Code Review</a> ({0})
-
-noSuchAccountMessage = {0} is not a registered user.
-noSuchGroupMessage = Group {0} does not exist or is not visible to you.
-nameAlreadyUsedBody = The name {0} is already in use.
-
-branchCreationFailed = Creating branch {0} failed. Error: {1}
-invalidBranchName = The branch name {0} is not valid.
-invalidRevision = The revision {0} is not valid.
-branchCreationNotAllowedUnderRefnamePrefix = Branch creation is not allowed under {0}.
-branchAlreadyExists = A branch with the name {0} already exists.
-branchCreationConflict = Cannot create branch {0} since it conflicts with branch {1}.
-
-pluginFailed = Plugin "{0}" failed to load
-cannotDownloadPlugin = Cannot load plugin from {0}
-
-parentUpdateFailed = Setting parent project failed: {0}
-
-fileCount = File {0} of {1}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java
deleted file mode 100644
index 01be2f2..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java
+++ /dev/null
@@ -1,25 +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.client;
-
-import com.google.gwt.resources.client.CssResource;
-
-public interface GerritResources extends Resources {
-  @Source("gerrit.css")
-  GerritCss css();
-
-  @Source("gwt_override.css")
-  CssResource gwt_override();
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/HostPageDataService.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/HostPageDataService.java
deleted file mode 100644
index bd3d483..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/HostPageDataService.java
+++ /dev/null
@@ -1,28 +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.client;
-
-import com.google.gerrit.common.data.HostPageData;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.gwtjsonrpc.common.HostPageCache;
-import com.google.gwtjsonrpc.common.RemoteJsonService;
-import com.google.gwtjsonrpc.common.RpcImpl;
-import com.google.gwtjsonrpc.common.RpcImpl.Version;
-
-@RpcImpl(version = Version.V2_0)
-interface HostPageDataService extends RemoteJsonService {
-  @HostPageCache(name = "gerrit_hostpagedata", once = true)
-  void load(AsyncCallback<HostPageData> callback);
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/JumpKeys.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/JumpKeys.java
deleted file mode 100644
index a4879ca..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/JumpKeys.java
+++ /dev/null
@@ -1,103 +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.client;
-
-import com.google.gerrit.common.PageLinks;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtexpui.globalkey.client.CompoundKeyCommand;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-import com.google.gwtexpui.globalkey.client.KeyCommand;
-import com.google.gwtexpui.globalkey.client.KeyCommandSet;
-
-public class JumpKeys {
-  private static HandlerRegistration activeHandler;
-  private static KeyCommandSet keys;
-  private static Widget bodyWidget;
-
-  public static void enable(boolean enable) {
-    if (enable && activeHandler == null) {
-      activeHandler = GlobalKey.add(bodyWidget, keys);
-    } else if (!enable && activeHandler != null) {
-      activeHandler.removeHandler();
-      activeHandler = null;
-    }
-  }
-
-  static void register(Widget body) {
-    final KeyCommandSet jumps = new KeyCommandSet();
-
-    jumps.add(
-        new KeyCommand(0, 'o', Gerrit.C.jumpAllOpen()) {
-          @Override
-          public void onKeyPress(KeyPressEvent event) {
-            Gerrit.display(PageLinks.toChangeQuery("status:open"));
-          }
-        });
-    jumps.add(
-        new KeyCommand(0, 'm', Gerrit.C.jumpAllMerged()) {
-          @Override
-          public void onKeyPress(KeyPressEvent event) {
-            Gerrit.display(PageLinks.toChangeQuery("status:merged"));
-          }
-        });
-    jumps.add(
-        new KeyCommand(0, 'a', Gerrit.C.jumpAllAbandoned()) {
-          @Override
-          public void onKeyPress(KeyPressEvent event) {
-            Gerrit.display(PageLinks.toChangeQuery("status:abandoned"));
-          }
-        });
-
-    if (Gerrit.isSignedIn()) {
-      jumps.add(
-          new KeyCommand(0, 'i', Gerrit.C.jumpMine()) {
-            @Override
-            public void onKeyPress(KeyPressEvent event) {
-              Gerrit.display(PageLinks.MINE);
-            }
-          });
-      jumps.add(
-          new KeyCommand(0, 'c', Gerrit.C.jumpMineDraftComments()) {
-            @Override
-            public void onKeyPress(KeyPressEvent event) {
-              Gerrit.display(PageLinks.toChangeQuery("has:draft"));
-            }
-          });
-      jumps.add(
-          new KeyCommand(0, 'w', Gerrit.C.jumpMineWatched()) {
-            @Override
-            public void onKeyPress(KeyPressEvent event) {
-              Gerrit.display(PageLinks.toChangeQuery("is:watched status:open"));
-            }
-          });
-      jumps.add(
-          new KeyCommand(0, 's', Gerrit.C.jumpMineStarred()) {
-            @Override
-            public void onKeyPress(KeyPressEvent event) {
-              Gerrit.display(PageLinks.toChangeQuery("is:starred"));
-            }
-          });
-    }
-
-    keys = new KeyCommandSet(Gerrit.C.sectionJumping());
-    keys.add(new CompoundKeyCommand(0, 'g', "", jumps));
-    bodyWidget = body;
-    activeHandler = GlobalKey.add(body, keys);
-  }
-
-  private JumpKeys() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/MessageOfTheDayBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/MessageOfTheDayBar.java
deleted file mode 100644
index 36fa6e8..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/MessageOfTheDayBar.java
+++ /dev/null
@@ -1,91 +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.client;
-
-import com.google.gerrit.common.data.HostPageData;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.uibinder.client.UiHandler;
-import com.google.gwt.user.client.Cookies;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.HTML;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwt.user.client.ui.RootPanel;
-import com.google.gwtexpui.safehtml.client.SafeHtml;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-import java.util.ArrayList;
-import java.util.List;
-
-/** Displays pending messages from the server. */
-class MessageOfTheDayBar extends Composite {
-  interface Binder extends UiBinder<HTMLPanel, MessageOfTheDayBar> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  private final List<HostPageData.Message> motd;
-  @UiField HTML message;
-  @UiField Anchor dismiss;
-
-  MessageOfTheDayBar(List<HostPageData.Message> motd) {
-    this.motd = filter(motd);
-    initWidget(uiBinder.createAndBindUi(this));
-
-    SafeHtmlBuilder b = new SafeHtmlBuilder();
-    if (this.motd.size() == 1) {
-      b.append(SafeHtml.asis(this.motd.get(0).html));
-    } else {
-      for (HostPageData.Message m : this.motd) {
-        b.openDiv();
-        b.append(SafeHtml.asis(m.html));
-        b.openElement("hr");
-        b.closeSelf();
-        b.closeDiv();
-      }
-    }
-    message.setHTML(b);
-  }
-
-  void show() {
-    if (!motd.isEmpty()) {
-      RootPanel.get().add(this);
-    }
-  }
-
-  @UiHandler("dismiss")
-  void onDismiss(@SuppressWarnings("unused") ClickEvent e) {
-    removeFromParent();
-
-    for (HostPageData.Message m : motd) {
-      Cookies.setCookie(cookieName(m), "1", m.redisplay);
-    }
-  }
-
-  private static List<HostPageData.Message> filter(List<HostPageData.Message> in) {
-    List<HostPageData.Message> show = new ArrayList<>();
-    for (HostPageData.Message m : in) {
-      if (Cookies.getCookie(cookieName(m)) == null) {
-        show.add(m);
-      }
-    }
-    return show;
-  }
-
-  private static String cookieName(HostPageData.Message m) {
-    return "msg-" + m.id;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/MessageOfTheDayBar.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/MessageOfTheDayBar.ui.xml
deleted file mode 100644
index 36d08b1..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/MessageOfTheDayBar.ui.xml
+++ /dev/null
@@ -1,71 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder
-    xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:style gss='false'>
-    .popup {
-      position: fixed;
-      top: 5px;
-      left: 50%;
-      margin-left: -200px;
-      z-index: 201;
-      padding-top: 5px;
-      padding-bottom: 5px;
-      padding-left: 12px;
-      padding-right: 12px;
-      background: #FFF1A8;
-      border-radius: 10px;
-    }
-
-    @if user.agent safari {
-      .popup {
-        \-webkit-border-radius: 10px;
-      }
-    }
-    @if user.agent gecko1_8 {
-      .popup {
-        \-moz-border-radius: 10px;
-      }
-    }
-
-    .message {
-      display: inline;
-    }
-    .message a {
-      color: #222;
-      text-decoration: underline;
-    }
-    a.action {
-      color: #222;
-      text-decoration: underline;
-      display: inline-block;
-      margin-left: 0.5em;
-    }
-  </ui:style>
-  <g:HTMLPanel styleName='{style.popup}'>
-    <g:HTML ui:field='message' styleName='{style.message}'/>
-    <g:Anchor ui:field='dismiss'
-        styleName='{style.action}'
-        href='javascript:;'
-        title='Hide this message'>
-      <ui:attribute name='title'/>
-      Dismiss
-    </g:Anchor>
-  </g:HTMLPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/NotFoundScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/NotFoundScreen.java
deleted file mode 100644
index 29bbf85..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/NotFoundScreen.java
+++ /dev/null
@@ -1,34 +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.client;
-
-import com.google.gerrit.client.ui.Screen;
-import com.google.gwt.user.client.ui.Label;
-
-/** Displays an error message letting the user know the page doesn't exist. */
-public class NotFoundScreen extends Screen {
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    setPageTitle(Gerrit.C.notFoundTitle());
-    add(new Label(Gerrit.C.notFoundBody()));
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    display();
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/NotSignedInDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/NotSignedInDialog.java
deleted file mode 100644
index cd5197a..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/NotSignedInDialog.java
+++ /dev/null
@@ -1,99 +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.client;
-
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.logical.shared.CloseEvent;
-import com.google.gwt.event.logical.shared.CloseHandler;
-import com.google.gwt.user.client.History;
-import com.google.gwt.user.client.Window;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.HTML;
-import com.google.gwt.user.client.ui.Label;
-import com.google.gwt.user.client.ui.PopupPanel;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-
-/** A dialog box telling the user they are not signed in. */
-public class NotSignedInDialog extends PopupPanel implements CloseHandler<PopupPanel> {
-  private Button signin;
-  private boolean buttonClicked;
-
-  public NotSignedInDialog() {
-    super(/* auto hide */ false, /* modal */ true);
-    setGlassEnabled(true);
-    getGlassElement().addClassName(Gerrit.RESOURCES.css().errorDialogGlass());
-    addStyleName(Gerrit.RESOURCES.css().errorDialog());
-
-    final FlowPanel buttons = new FlowPanel();
-    signin = new Button();
-    signin.setText(Gerrit.C.menuSignIn());
-    signin.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            buttonClicked = true;
-            hide();
-            Gerrit.doSignIn(History.getToken());
-          }
-        });
-    buttons.add(signin);
-
-    final Button close = new Button();
-    close.getElement().getStyle().setProperty("marginLeft", "200px");
-    close.setText(Gerrit.C.signInDialogGoAnonymous());
-    close.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            buttonClicked = true;
-            Gerrit.deleteSessionCookie();
-            hide();
-          }
-        });
-    buttons.add(close);
-
-    Label title = new Label(Gerrit.C.notSignedInTitle());
-    title.setStyleName(Gerrit.RESOURCES.css().errorDialogTitle());
-
-    FlowPanel center = new FlowPanel();
-    center.add(title);
-    center.add(new HTML(Gerrit.C.notSignedInBody()));
-    center.add(buttons);
-    add(center);
-
-    int l = Window.getScrollLeft() + 20;
-    int t = Window.getScrollTop() + 20;
-    setPopupPosition(l, t);
-    addCloseHandler(this);
-  }
-
-  @Override
-  public void onClose(CloseEvent<PopupPanel> event) {
-    if (!buttonClicked) {
-      // the dialog was closed without one of the buttons being pressed
-      // e.g. the user pressed ESC to close the dialog
-      Gerrit.deleteSessionCookie();
-    }
-  }
-
-  @Override
-  public void center() {
-    show();
-    GlobalKey.dialog(this);
-    signin.setFocus(true);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/RangeInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RangeInfo.java
deleted file mode 100644
index 42454a3..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/RangeInfo.java
+++ /dev/null
@@ -1,25 +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.client;
-
-import com.google.gwt.core.client.JavaScriptObject;
-
-public class RangeInfo extends JavaScriptObject {
-  public final native int start() /*-{ return this.start; }-*/;
-
-  public final native int end() /*-{ return this.end; }-*/;
-
-  protected RangeInfo() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/RpcStatus.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RpcStatus.java
deleted file mode 100644
index 4153439..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/RpcStatus.java
+++ /dev/null
@@ -1,74 +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.client;
-
-import com.google.gwt.user.client.ui.InlineLabel;
-import com.google.gwt.user.client.ui.Label;
-import com.google.gwt.user.client.ui.RootPanel;
-import com.google.gwtjsonrpc.client.event.RpcCompleteEvent;
-import com.google.gwtjsonrpc.client.event.RpcCompleteHandler;
-import com.google.gwtjsonrpc.client.event.RpcStartEvent;
-import com.google.gwtjsonrpc.client.event.RpcStartHandler;
-
-public class RpcStatus implements RpcStartHandler, RpcCompleteHandler {
-  public static RpcStatus INSTANCE;
-
-  private static int hideDepth;
-
-  /** Execute code, hiding the RPCs they execute from being shown visually. */
-  public static void hide(Runnable run) {
-    try {
-      hideDepth++;
-      run.run();
-    } finally {
-      hideDepth--;
-    }
-  }
-
-  private final Label loading;
-  private int activeCalls;
-
-  RpcStatus() {
-    loading = new InlineLabel();
-    loading.setText(Gerrit.C.rpcStatusWorking());
-    loading.setStyleName(Gerrit.RESOURCES.css().rpcStatus());
-    loading.setVisible(false);
-    RootPanel.get().add(loading);
-  }
-
-  @Override
-  public void onRpcStart(RpcStartEvent event) {
-    onRpcStart();
-  }
-
-  public void onRpcStart() {
-    if (++activeCalls == 1) {
-      if (hideDepth == 0) {
-        loading.setVisible(true);
-      }
-    }
-  }
-
-  @Override
-  public void onRpcComplete(RpcCompleteEvent event) {
-    onRpcComplete();
-  }
-
-  public void onRpcComplete() {
-    if (--activeCalls == 0) {
-      loading.setVisible(false);
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java
deleted file mode 100644
index 406ab4e..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java
+++ /dev/null
@@ -1,163 +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.client;
-
-import com.google.gerrit.client.changes.QueryScreen;
-import com.google.gerrit.client.ui.HintTextBox;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.ListBox;
-import com.google.gwt.user.client.ui.SuggestBox;
-import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-import com.google.gwtexpui.globalkey.client.KeyCommand;
-
-class SearchPanel extends Composite {
-  private final HintTextBox searchBox;
-  private final ListBox dropdown;
-  private HandlerRegistration regFocus;
-
-  SearchPanel() {
-    final FlowPanel body = new FlowPanel();
-    initWidget(body);
-    setStyleName(Gerrit.RESOURCES.css().searchPanel());
-
-    searchBox = new HintTextBox();
-    final MySuggestionDisplay suggestionDisplay = new MySuggestionDisplay();
-    searchBox.addKeyPressHandler(
-        new KeyPressHandler() {
-          @Override
-          public void onKeyPress(KeyPressEvent event) {
-            if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
-              if (!suggestionDisplay.isSuggestionSelected) {
-                doSearch();
-              }
-            }
-          }
-        });
-
-    if (Gerrit.hasDocSearch()) {
-      dropdown = new ListBox();
-      dropdown.setStyleName("searchDropdown");
-      dropdown.addItem(Gerrit.C.searchDropdownChanges());
-      dropdown.addItem(Gerrit.C.searchDropdownDoc());
-      dropdown.setVisibleItemCount(1);
-      dropdown.setSelectedIndex(0);
-    } else {
-      // Doc search is NOT available.
-      dropdown = null;
-    }
-
-    final SuggestBox suggestBox =
-        new SuggestBox(new SearchSuggestOracle(), searchBox, suggestionDisplay);
-    searchBox.setStyleName("searchTextBox");
-    searchBox.setVisibleLength(70);
-    searchBox.setHintText(Gerrit.C.searchHint());
-
-    final Button searchButton = new Button(Gerrit.C.searchButton());
-    searchButton.setStyleName("searchButton");
-    searchButton.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            doSearch();
-          }
-        });
-
-    body.add(suggestBox);
-    if (dropdown != null) {
-      body.add(dropdown);
-    }
-    body.add(searchButton);
-  }
-
-  void setText(String query) {
-    searchBox.setText(query);
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    if (regFocus == null) {
-      regFocus =
-          GlobalKey.addApplication(
-              this,
-              new KeyCommand(0, '/', Gerrit.C.keySearch()) {
-                @Override
-                public void onKeyPress(KeyPressEvent event) {
-                  event.preventDefault();
-                  searchBox.setFocus(true);
-                  searchBox.selectAll();
-                }
-              });
-    }
-  }
-
-  @Override
-  protected void onUnload() {
-    if (regFocus != null) {
-      regFocus.removeHandler();
-      regFocus = null;
-    }
-  }
-
-  private void doSearch() {
-    final String query = searchBox.getText().trim();
-    if ("".equals(query)) {
-      return;
-    }
-
-    searchBox.setFocus(false);
-
-    if (dropdown != null && dropdown.getSelectedValue().equals(Gerrit.C.searchDropdownDoc())) {
-      // doc
-      Gerrit.display(PageLinks.toDocumentationQuery(query));
-    } else {
-      // changes
-      if (query.matches("^[1-9][0-9]*$")) {
-        // Query is a change number. Project can't be supplied.
-        Gerrit.display(PageLinks.toChange(null, Change.Id.parse(query)));
-      } else {
-        Gerrit.display(PageLinks.toChangeQuery(query), QueryScreen.forQuery(query));
-      }
-    }
-  }
-
-  private static class MySuggestionDisplay extends SuggestBox.DefaultSuggestionDisplay {
-    private boolean isSuggestionSelected;
-
-    private MySuggestionDisplay() {
-      super();
-
-      getPopupPanel().addStyleName(Gerrit.RESOURCES.css().suggestBoxPopup());
-    }
-
-    @Override
-    protected Suggestion getCurrentSelection() {
-      Suggestion currentSelection = super.getCurrentSelection();
-      isSuggestionSelected = currentSelection != null;
-      return currentSelection;
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
deleted file mode 100644
index e74ed71..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
+++ /dev/null
@@ -1,312 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client;
-
-import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
-import com.google.gerrit.client.ui.AccountSuggestOracle;
-import com.google.gerrit.client.ui.ProjectNameSuggestOracle;
-import com.google.gwt.user.client.ui.SuggestOracle;
-import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.TreeSet;
-
-public class SearchSuggestOracle extends HighlightSuggestOracle {
-  private static final List<ParamSuggester> paramSuggester =
-      Arrays.asList(
-          new ParamSuggester(
-              Arrays.asList("project:", "p:", "parentproject:"), new ProjectNameSuggestOracle()),
-          new ParamSuggester(
-              Arrays.asList(
-                  "owner:",
-                  "o:",
-                  "reviewer:",
-                  "r:",
-                  "commentby:",
-                  "reviewedby:",
-                  "author:",
-                  "committer:",
-                  "from:",
-                  "assignee:",
-                  "cc:"),
-              new AccountSuggestOracle() {
-                @Override
-                public void onRequestSuggestions(Request request, Callback done) {
-                  super.onRequestSuggestions(
-                      request,
-                      new Callback() {
-                        @Override
-                        public void onSuggestionsReady(final Request request, Response response) {
-                          if ("self".startsWith(request.getQuery())) {
-                            final ArrayList<SuggestOracle.Suggestion> r =
-                                new ArrayList<>(response.getSuggestions().size() + 1);
-                            r.add(
-                                new SuggestOracle.Suggestion() {
-                                  @Override
-                                  public String getDisplayString() {
-                                    return getReplacementString();
-                                  }
-
-                                  @Override
-                                  public String getReplacementString() {
-                                    return "self";
-                                  }
-                                });
-                            r.addAll(response.getSuggestions());
-                            response.setSuggestions(r);
-                          }
-                          done.onSuggestionsReady(request, response);
-                        }
-                      });
-                }
-              }),
-          new ParamSuggester(
-              Arrays.asList("ownerin:", "reviewerin:"), new AccountGroupSuggestOracle()));
-
-  private static final TreeSet<String> suggestions = new TreeSet<>();
-
-  static {
-    suggestions.add("age:");
-    suggestions.add("age:1week"); // Give an example age
-
-    suggestions.add("change:");
-
-    suggestions.add("owner:");
-    suggestions.add("owner:self");
-    suggestions.add("ownerin:");
-    suggestions.add("author:");
-    suggestions.add("committer:");
-    suggestions.add("assignee:");
-
-    suggestions.add("reviewer:");
-    suggestions.add("reviewer:self");
-    suggestions.add("reviewerin:");
-    suggestions.add("reviewedby:");
-
-    suggestions.add("commit:");
-    suggestions.add("comment:");
-    suggestions.add("message:");
-    suggestions.add("commentby:");
-    suggestions.add("from:");
-    suggestions.add("file:");
-    suggestions.add("conflicts:");
-    suggestions.add("project:");
-    suggestions.add("projects:");
-    suggestions.add("parentproject:");
-    suggestions.add("branch:");
-    suggestions.add("topic:");
-    suggestions.add("intopic:");
-    suggestions.add("ref:");
-    suggestions.add("tr:");
-    suggestions.add("bug:");
-    suggestions.add("label:");
-    suggestions.add("query:");
-    suggestions.add("has:");
-    suggestions.add("has:draft");
-    suggestions.add("has:edit");
-    suggestions.add("has:star");
-    suggestions.add("has:stars");
-    suggestions.add("has:unresolved");
-    suggestions.add("star:");
-
-    suggestions.add("is:");
-    suggestions.add("is:starred");
-    suggestions.add("is:watched");
-    suggestions.add("is:reviewed");
-    suggestions.add("is:owner");
-    suggestions.add("is:reviewer");
-    suggestions.add("is:open");
-    suggestions.add("is:pending");
-    suggestions.add("is:private");
-    suggestions.add("is:closed");
-    suggestions.add("is:merged");
-    suggestions.add("is:abandoned");
-    suggestions.add("is:mergeable");
-    suggestions.add("is:ignored");
-    suggestions.add("is:wip");
-    suggestions.add("is:assigned");
-    suggestions.add("is:submittable");
-
-    suggestions.add("status:");
-    suggestions.add("status:open");
-    suggestions.add("status:pending");
-    suggestions.add("status:reviewed");
-    suggestions.add("status:closed");
-    suggestions.add("status:merged");
-    suggestions.add("status:abandoned");
-
-    suggestions.add("added:");
-    suggestions.add("deleted:");
-    suggestions.add("delta:");
-    suggestions.add("size:");
-
-    suggestions.add("unresolved:");
-
-    suggestions.add("revertof:");
-
-    if (Gerrit.isNoteDbEnabled()) {
-      suggestions.add("cc:");
-      suggestions.add("hashtag:");
-    }
-
-    suggestions.add("is:assigned");
-    suggestions.add("is:unassigned");
-    suggestions.add("assignee:");
-
-    suggestions.add("AND");
-    suggestions.add("OR");
-    suggestions.add("NOT");
-  }
-
-  @Override
-  public void requestDefaultSuggestions(Request request, Callback done) {
-    final ArrayList<SearchSuggestion> r = new ArrayList<>();
-    // No text - show some default suggestions.
-    r.add(new SearchSuggestion("status:open", "status:open"));
-    r.add(new SearchSuggestion("age:1week", "age:1week"));
-    if (Gerrit.isSignedIn()) {
-      r.add(new SearchSuggestion("owner:self", "owner:self"));
-    }
-    done.onSuggestionsReady(request, new Response(r));
-  }
-
-  @Override
-  protected void onRequestSuggestions(Request request, Callback done) {
-    final String query = request.getQuery();
-
-    final String lastWord = getLastWord(query);
-    if (lastWord == null) {
-      // Starting a new word - don't show suggestions yet.
-      done.onSuggestionsReady(request, null);
-      return;
-    }
-
-    for (ParamSuggester ps : paramSuggester) {
-      if (ps.applicable(lastWord)) {
-        ps.suggest(lastWord, request, done);
-        return;
-      }
-    }
-
-    final ArrayList<SearchSuggestion> r = new ArrayList<>();
-    for (String suggestion : suggestions.tailSet(lastWord)) {
-      if ((lastWord.length() < suggestion.length()) && suggestion.startsWith(lastWord)) {
-        if (suggestion.contains("self") && !Gerrit.isSignedIn()) {
-          continue;
-        }
-        r.add(new SearchSuggestion(suggestion, query + suggestion.substring(lastWord.length())));
-      }
-    }
-    done.onSuggestionsReady(request, new Response(r));
-  }
-
-  private String getLastWord(String query) {
-    final int lastSpace = query.lastIndexOf(' ');
-    if (lastSpace == query.length() - 1) {
-      return null;
-    }
-    if (lastSpace == -1) {
-      return query;
-    }
-    return query.substring(lastSpace + 1);
-  }
-
-  @Override
-  protected String getQueryPattern(String query) {
-    return super.getQueryPattern(getLastWord(query));
-  }
-
-  @Override
-  protected boolean isHTML() {
-    return true;
-  }
-
-  private static class SearchSuggestion implements SuggestOracle.Suggestion {
-    private final String suggestion;
-    private final String fullQuery;
-
-    SearchSuggestion(String suggestion, String fullQuery) {
-      this.suggestion = suggestion;
-      // Add a space to the query if it is a complete operation (e.g.
-      // "status:open") so the user can keep on typing.
-      this.fullQuery = fullQuery.endsWith(":") ? fullQuery : fullQuery + " ";
-    }
-
-    @Override
-    public String getDisplayString() {
-      return suggestion;
-    }
-
-    @Override
-    public String getReplacementString() {
-      return fullQuery;
-    }
-  }
-
-  private static class ParamSuggester {
-    private final List<String> operators;
-    private final SuggestOracle parameterSuggestionOracle;
-
-    ParamSuggester(List<String> operators, SuggestOracle parameterSuggestionOracle) {
-      this.operators = operators;
-      this.parameterSuggestionOracle = parameterSuggestionOracle;
-    }
-
-    boolean applicable(String query) {
-      final String operator = getApplicableOperator(query, operators);
-      return operator != null && query.length() > operator.length();
-    }
-
-    private String getApplicableOperator(String lastWord, List<String> operators) {
-      for (String operator : operators) {
-        if (lastWord.startsWith(operator)) {
-          return operator;
-        }
-      }
-      return null;
-    }
-
-    void suggest(String lastWord, Request request, Callback done) {
-      final String operator = getApplicableOperator(lastWord, operators);
-      parameterSuggestionOracle.requestSuggestions(
-          new Request(lastWord.substring(operator.length()), request.getLimit()),
-          new Callback() {
-            @Override
-            public void onSuggestionsReady(Request req, Response response) {
-              final String query = request.getQuery();
-              final List<SearchSuggestOracle.Suggestion> r =
-                  new ArrayList<>(response.getSuggestions().size());
-              for (SearchSuggestOracle.Suggestion s : response.getSuggestions()) {
-                r.add(
-                    new SearchSuggestion(
-                        s.getDisplayString(),
-                        query.substring(0, query.length() - lastWord.length())
-                            + operator
-                            + quoteIfNeeded(s.getReplacementString())));
-              }
-              done.onSuggestionsReady(request, new Response(r));
-            }
-
-            private String quoteIfNeeded(String s) {
-              if (!s.matches("^\\S*$")) {
-                return "\"" + s + "\"";
-              }
-              return s;
-            }
-          });
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/StringListPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/StringListPanel.java
deleted file mode 100644
index f771fee..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/StringListPanel.java
+++ /dev/null
@@ -1,359 +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.client;
-
-import com.google.gerrit.client.ui.NavigationTable;
-import com.google.gerrit.client.ui.OnEditEnabler;
-import com.google.gerrit.client.ui.SmallHeading;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.logical.shared.ValueChangeHandler;
-import com.google.gwt.user.client.Event;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.FocusWidget;
-import com.google.gwt.user.client.ui.HasEnabled;
-import com.google.gwt.user.client.ui.HorizontalPanel;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwt.user.client.ui.ImageResourceRenderer;
-import com.google.gwtexpui.globalkey.client.NpTextBox;
-import java.util.ArrayList;
-import java.util.List;
-
-public class StringListPanel extends FlowPanel implements HasEnabled {
-  private final StringListTable t;
-  private HorizontalPanel titlePanel;
-  protected final HorizontalPanel buttonPanel;
-  private final Button deleteButton;
-  private Image info;
-  protected FocusWidget widget;
-
-  public StringListPanel(String title, List<String> fieldNames, FocusWidget w, boolean autoSort) {
-    widget = w;
-    if (title != null) {
-      titlePanel = new HorizontalPanel();
-      SmallHeading titleLabel = new SmallHeading(title);
-      titlePanel.add(titleLabel);
-      add(titlePanel);
-    }
-
-    t = new StringListTable(fieldNames, autoSort);
-    add(t);
-
-    buttonPanel = new HorizontalPanel();
-    buttonPanel.setStyleName(Gerrit.RESOURCES.css().stringListPanelButtons());
-    deleteButton = new Button(Gerrit.C.stringListPanelDelete());
-    deleteButton.setEnabled(false);
-    deleteButton.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            widget.setEnabled(true);
-            t.deleteChecked();
-          }
-        });
-    buttonPanel.add(deleteButton);
-    add(buttonPanel);
-  }
-
-  public void display(List<List<String>> values) {
-    t.display(values);
-  }
-
-  public void setInfo(String msg) {
-    if (info == null && titlePanel != null) {
-      info = new Image(Gerrit.RESOURCES.info());
-      titlePanel.add(info);
-    }
-    if (info != null) {
-      info.setTitle(msg);
-    }
-  }
-
-  public List<List<String>> getValues() {
-    return t.getValues();
-  }
-
-  public List<String> getValues(int i) {
-    List<List<String>> allValuesList = getValues();
-    List<String> singleValueList = new ArrayList<>(allValuesList.size());
-    for (List<String> values : allValuesList) {
-      singleValueList.add(values.get(i));
-    }
-    return singleValueList;
-  }
-
-  private class StringListTable extends NavigationTable<List<String>> {
-    private final Button addButton;
-    private final List<NpTextBox> inputs;
-    private final boolean autoSort;
-
-    StringListTable(List<String> names, boolean autoSort) {
-      this.autoSort = autoSort;
-
-      addButton = new Button(new ImageResourceRenderer().render(Gerrit.RESOURCES.listAdd()));
-      addButton.setTitle(Gerrit.C.stringListPanelAdd());
-      OnEditEnabler e = new OnEditEnabler(addButton);
-      inputs = new ArrayList<>();
-
-      FlexCellFormatter fmt = table.getFlexCellFormatter();
-      fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().iconHeader());
-      fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().leftMostCell());
-      for (int i = 0; i < names.size(); i++) {
-        fmt.addStyleName(0, i + 1, Gerrit.RESOURCES.css().dataHeader());
-        table.setText(0, i + 1, names.get(i));
-
-        NpTextBox input = new NpTextBox();
-        input.setVisibleLength(35);
-        input.addKeyPressHandler(
-            new KeyPressHandler() {
-              @Override
-              public void onKeyPress(KeyPressEvent event) {
-                if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
-                  widget.setEnabled(true);
-                  add();
-                }
-              }
-            });
-        inputs.add(input);
-        fmt.addStyleName(1, i + 1, Gerrit.RESOURCES.css().dataHeader());
-        table.setWidget(1, i + 1, input);
-        e.listenTo(input);
-      }
-      addButton.setEnabled(false);
-
-      addButton.addClickHandler(
-          new ClickHandler() {
-            @Override
-            public void onClick(ClickEvent event) {
-              widget.setEnabled(true);
-              add();
-            }
-          });
-      fmt.addStyleName(1, 0, Gerrit.RESOURCES.css().iconHeader());
-      fmt.addStyleName(1, 0, Gerrit.RESOURCES.css().leftMostCell());
-      table.setWidget(1, 0, addButton);
-
-      if (!autoSort) {
-        fmt.addStyleName(0, names.size() + 1, Gerrit.RESOURCES.css().iconHeader());
-        fmt.addStyleName(0, names.size() + 2, Gerrit.RESOURCES.css().iconHeader());
-        fmt.addStyleName(1, names.size() + 1, Gerrit.RESOURCES.css().iconHeader());
-        fmt.addStyleName(1, names.size() + 2, Gerrit.RESOURCES.css().iconHeader());
-      }
-    }
-
-    void display(List<List<String>> values) {
-      for (int row = 2; row < table.getRowCount(); row++) {
-        table.removeRow(row--);
-      }
-      int row = 2;
-      for (List<String> v : values) {
-        populate(row, v);
-        row++;
-      }
-      updateNavigationLinks();
-    }
-
-    List<List<String>> getValues() {
-      List<List<String>> values = new ArrayList<>();
-      for (int row = 2; row < table.getRowCount(); row++) {
-        values.add(getRowItem(row));
-      }
-      return values;
-    }
-
-    @Override
-    protected List<String> getRowItem(int row) {
-      List<String> v = new ArrayList<>();
-      for (int i = 0; i < inputs.size(); i++) {
-        v.add(table.getText(row, i + 1));
-      }
-      return v;
-    }
-
-    private void populate(int row, List<String> values) {
-      FlexCellFormatter fmt = table.getFlexCellFormatter();
-      fmt.addStyleName(row, 0, Gerrit.RESOURCES.css().iconCell());
-      fmt.addStyleName(row, 0, Gerrit.RESOURCES.css().leftMostCell());
-      CheckBox checkBox = new CheckBox();
-      checkBox.addValueChangeHandler(
-          new ValueChangeHandler<Boolean>() {
-            @Override
-            public void onValueChange(ValueChangeEvent<Boolean> event) {
-              enableDelete();
-            }
-          });
-      table.setWidget(row, 0, checkBox);
-      for (int i = 0; i < values.size(); i++) {
-        fmt.addStyleName(row, i + 1, Gerrit.RESOURCES.css().dataCell());
-        table.setText(row, i + 1, values.get(i));
-      }
-      if (!autoSort) {
-        fmt.addStyleName(row, values.size() + 1, Gerrit.RESOURCES.css().iconCell());
-        fmt.addStyleName(row, values.size() + 2, Gerrit.RESOURCES.css().dataCell());
-
-        Image down = new Image(Gerrit.RESOURCES.arrowDown());
-        down.setTitle(Gerrit.C.stringListPanelDown());
-        down.addClickHandler(
-            new ClickHandler() {
-              @Override
-              public void onClick(ClickEvent event) {
-                moveDown(row);
-              }
-            });
-        table.setWidget(row, values.size() + 1, down);
-
-        Image up = new Image(Gerrit.RESOURCES.arrowUp());
-        up.setTitle(Gerrit.C.stringListPanelUp());
-        up.addClickHandler(
-            new ClickHandler() {
-              @Override
-              public void onClick(ClickEvent event) {
-                moveUp(row);
-              }
-            });
-        table.setWidget(row, values.size() + 2, up);
-      }
-    }
-
-    @Override
-    protected void onCellSingleClick(Event event, int row, int column) {
-      if (column == inputs.size() + 1 && row >= 2 && row < table.getRowCount() - 2) {
-        moveDown(row);
-      } else if (column == inputs.size() + 2 && row > 2) {
-        moveUp(row);
-      }
-    }
-
-    void moveDown(int row) {
-      if (row < table.getRowCount() - 1) {
-        swap(row, row + 1);
-      }
-    }
-
-    void moveUp(int row) {
-      if (row > 2) {
-        swap(row - 1, row);
-      }
-    }
-
-    void swap(int row1, int row2) {
-      List<String> value = getRowItem(row1);
-      List<String> nextValue = getRowItem(row2);
-      populate(row1, nextValue);
-      populate(row2, value);
-      updateNavigationLinks();
-      widget.setEnabled(true);
-    }
-
-    private void updateNavigationLinks() {
-      if (!autoSort) {
-        for (int row = 2; row < table.getRowCount(); row++) {
-          table.getWidget(row, inputs.size() + 1).setVisible(row < table.getRowCount() - 1);
-          table.getWidget(row, inputs.size() + 2).setVisible(row > 2);
-        }
-      }
-    }
-
-    void add() {
-      List<String> values = new ArrayList<>();
-      for (NpTextBox input : inputs) {
-        values.add(input.getValue().trim());
-        input.setValue("");
-      }
-      insert(values);
-    }
-
-    void insert(List<String> v) {
-      int insertPos = table.getRowCount();
-      if (autoSort) {
-        for (int row = 1; row < table.getRowCount(); row++) {
-          int compareResult = v.get(0).compareTo(table.getText(row, 1));
-          if (compareResult < 0) {
-            insertPos = row;
-            break;
-          } else if (compareResult == 0) {
-            return;
-          }
-        }
-      }
-      table.insertRow(insertPos);
-      populate(insertPos, v);
-      updateNavigationLinks();
-    }
-
-    void enableDelete() {
-      for (int row = 2; row < table.getRowCount(); row++) {
-        if (((CheckBox) table.getWidget(row, 0)).getValue()) {
-          deleteButton.setEnabled(true);
-          return;
-        }
-      }
-      deleteButton.setEnabled(false);
-    }
-
-    void deleteChecked() {
-      deleteButton.setEnabled(false);
-      for (int row = 2; row < table.getRowCount(); row++) {
-        if (((CheckBox) table.getWidget(row, 0)).getValue()) {
-          table.removeRow(row--);
-        }
-      }
-      updateNavigationLinks();
-    }
-
-    @Override
-    protected void onOpenRow(int row) {}
-
-    @Override
-    protected Object getRowItemKey(List<String> item) {
-      return item.get(0);
-    }
-
-    void setEnabled(boolean enabled) {
-      addButton.setVisible(enabled);
-      for (NpTextBox input : inputs) {
-        input.setEnabled(enabled);
-      }
-      for (int row = 2; row < table.getRowCount(); row++) {
-        table.getWidget(row, 0).setVisible(enabled);
-        if (!autoSort) {
-          table.getWidget(row, inputs.size() + 1).setVisible(enabled);
-          table.getWidget(row, inputs.size() + 2).setVisible(enabled);
-        }
-      }
-      if (enabled) {
-        updateNavigationLinks();
-      }
-    }
-  }
-
-  @Override
-  public boolean isEnabled() {
-    return deleteButton.isVisible();
-  }
-
-  @Override
-  public void setEnabled(boolean enabled) {
-    t.setEnabled(enabled);
-    deleteButton.setVisible(enabled);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Themer.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Themer.java
deleted file mode 100644
index d87c0b8..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Themer.java
+++ /dev/null
@@ -1,83 +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.client;
-
-import com.google.gerrit.client.projects.ThemeInfo;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.StyleElement;
-
-public class Themer {
-  public static class ThemerIE extends Themer {
-    protected ThemerIE() {}
-
-    @Override
-    protected String getCssText(StyleElement el) {
-      return el.getCssText();
-    }
-
-    @Override
-    protected void setCssText(StyleElement el, String css) {
-      el.setCssText(css);
-    }
-  }
-
-  protected StyleElement cssElement;
-  protected Element headerElement;
-  protected Element footerElement;
-  protected String cssText;
-  protected String headerHtml;
-  protected String footerHtml;
-
-  protected Themer() {}
-
-  public void set(ThemeInfo theme) {
-    if (theme != null) {
-      set(
-          theme.css() != null ? theme.css() : cssText,
-          theme.header() != null ? theme.header() : headerHtml,
-          theme.footer() != null ? theme.footer() : footerHtml);
-    } else {
-      set(cssText, headerHtml, footerHtml);
-    }
-  }
-
-  public void clear() {
-    set(null);
-  }
-
-  void init(Element css, Element header, Element footer) {
-    cssElement = StyleElement.as(css);
-    headerElement = header;
-    footerElement = footer;
-
-    cssText = getCssText(this.cssElement);
-    headerHtml = header.getInnerHTML();
-    footerHtml = footer.getInnerHTML();
-  }
-
-  protected String getCssText(StyleElement el) {
-    return el.getInnerHTML();
-  }
-
-  protected void setCssText(StyleElement el, String css) {
-    el.setInnerHTML(css);
-  }
-
-  private void set(String css, String header, String footer) {
-    setCssText(cssElement, css);
-    headerElement.setInnerHTML(header);
-    footerElement.setInnerHTML(footer);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/UrlAliasMatcher.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UrlAliasMatcher.java
deleted file mode 100644
index acaaf46..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/UrlAliasMatcher.java
+++ /dev/null
@@ -1,65 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client;
-
-import com.google.gwt.regexp.shared.RegExp;
-import java.util.HashMap;
-import java.util.Map;
-
-public class UrlAliasMatcher {
-  private final Map<RegExp, String> userUrlAliases;
-  private final Map<RegExp, String> globalUrlAliases;
-
-  UrlAliasMatcher(Map<String, String> globalUrlAliases) {
-    this.globalUrlAliases = compile(globalUrlAliases);
-    this.userUrlAliases = new HashMap<>();
-  }
-
-  private static Map<RegExp, String> compile(Map<String, String> urlAliases) {
-    Map<RegExp, String> compiledUrlAliases = new HashMap<>();
-    if (urlAliases != null) {
-      for (Map.Entry<String, String> e : urlAliases.entrySet()) {
-        compiledUrlAliases.put(RegExp.compile(e.getKey()), e.getValue());
-      }
-    }
-    return compiledUrlAliases;
-  }
-
-  void clearUserAliases() {
-    this.userUrlAliases.clear();
-  }
-
-  void updateUserAliases(Map<String, String> userUrlAliases) {
-    clearUserAliases();
-    this.userUrlAliases.putAll(compile(userUrlAliases));
-  }
-
-  public String replace(String token) {
-    for (Map.Entry<RegExp, String> e : userUrlAliases.entrySet()) {
-      RegExp pat = e.getKey();
-      if (pat.exec(token) != null) {
-        return pat.replace(token, e.getValue());
-      }
-    }
-
-    for (Map.Entry<RegExp, String> e : globalUrlAliases.entrySet()) {
-      RegExp pat = e.getKey();
-      if (pat.exec(token) != null) {
-        return pat.replace(token, e.getValue());
-      }
-    }
-    return token;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java
deleted file mode 100644
index cb529f4..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java
+++ /dev/null
@@ -1,85 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client;
-
-import com.google.gerrit.client.info.AccountInfo;
-import com.google.gerrit.client.ui.InlineHyperlink;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.dom.client.AnchorElement;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.user.client.ui.Label;
-import com.google.gwt.user.client.ui.PopupPanel;
-import com.google.gwt.user.client.ui.Widget;
-
-public class UserPopupPanel extends PopupPanel {
-  interface Binder extends UiBinder<Widget, UserPopupPanel> {}
-
-  private static final Binder binder = GWT.create(Binder.class);
-
-  @UiField(provided = true)
-  AvatarImage avatar;
-
-  @UiField Label userName;
-  @UiField Label userEmail;
-  @UiField Element userLinks;
-  @UiField AnchorElement switchAccount;
-  @UiField AnchorElement logout;
-  @UiField InlineHyperlink settings;
-
-  public UserPopupPanel(AccountInfo account, boolean canLogOut, boolean showSettingsLink) {
-    super(/* auto hide */ true, /* modal */ false);
-    avatar = new AvatarImage(account, 100, false);
-    setWidget(binder.createAndBindUi(this));
-    setStyleName(Gerrit.RESOURCES.css().userInfoPopup());
-    if (account.name() != null) {
-      userName.setText(account.name());
-    }
-    if (account.email() != null) {
-      userEmail.setText(account.email());
-    }
-    if (showSettingsLink) {
-      String switchAccountUrl = Gerrit.info().auth().switchAccountUrl();
-      if (switchAccountUrl != null) {
-        switchAccount.setHref(switchAccountUrl.replace("${path}", "/"));
-      } else if (Gerrit.info().auth().isDev() || Gerrit.info().auth().isOpenId()) {
-        switchAccount.setHref(Gerrit.selfRedirect("/login"));
-      } else {
-        switchAccount.removeFromParent();
-        switchAccount = null;
-      }
-      if (canLogOut) {
-        logout.setHref(Gerrit.selfRedirect("/logout"));
-      } else {
-        logout.removeFromParent();
-        logout = null;
-      }
-
-    } else {
-      settings.removeFromParent();
-      settings = null;
-      userLinks.removeFromParent();
-      userLinks = null;
-      logout = null;
-    }
-
-    // We must show and then hide this popup so that it is part of the DOM.
-    // Otherwise the image does not get any events.  Calling hide() would
-    // remove it from the DOM so we use setVisible(false) instead.
-    show();
-    setVisible(false);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.ui.xml
deleted file mode 100644
index 8f5073d..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.ui.xml
+++ /dev/null
@@ -1,70 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2012 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
-<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
-  xmlns:g='urn:import:com.google.gwt.user.client.ui'
-  xmlns:gerrit='urn:import:com.google.gerrit.client'
-  xmlns:u='urn:import:com.google.gerrit.client.ui'>
-  <ui:style gss='false'>
-    .panel {
-      padding: 8px;
-    }
-    .avatar {
-      padding-right: 4px;
-      width: 100px;
-      height: 100px;
-    }
-    .infoCell {
-      vertical-align: top;
-    }
-    .userName {
-      font-weight: bold;
-    }
-    .email {
-      padding-bottom: 6px;
-    }
-    .userLinks {
-      min-width: 250px;
-    }
-    .userLinksRight {
-      float: right;
-    }
-    .switchAccount {
-      border-right: 1px solid black;
-      padding-right: 0.5em;
-      margin-right: 0.5em;
-    }
-  </ui:style>
-
-  <g:HTMLPanel styleName='{style.panel}'>
-    <table><tr><td>
-      <gerrit:AvatarImage ui:field='avatar' styleName='{style.avatar}' />
-    </td><td class='{style.infoCell}'>
-      <g:Label ui:field='userName' styleName="{style.userName}" />
-      <g:Label ui:field='userEmail' styleName="{style.email}" />
-    </td></tr></table>
-    <div ui:field='userLinks' class='{style.userLinks}'>
-      <u:InlineHyperlink ui:field='settings' targetHistoryToken='/settings/'>
-        <ui:msg>Settings</ui:msg>
-      </u:InlineHyperlink>
-      <span class='{style.userLinksRight}'>
-        <a ui:field='switchAccount' class='{style.switchAccount}'><ui:msg>Switch Account</ui:msg></a
-        ><a ui:field='logout'><ui:msg>Sign Out</ui:msg></a>
-      </span>
-    </div>
-  </g:HTMLPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/VoidResult.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/VoidResult.java
deleted file mode 100644
index 810ebe7..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/VoidResult.java
+++ /dev/null
@@ -1,25 +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.client;
-
-import com.google.gwt.core.client.JavaScriptObject;
-
-public final class VoidResult extends JavaScriptObject {
-  protected VoidResult() {}
-
-  public static VoidResult create() {
-    return createObject().cast();
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/AccessMap.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/AccessMap.java
deleted file mode 100644
index a0060d5..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/AccessMap.java
+++ /dev/null
@@ -1,51 +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.client.access;
-
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import java.util.Collections;
-import java.util.Set;
-
-/** Access rights available from {@code /access/}. */
-public class AccessMap extends NativeMap<ProjectAccessInfo> {
-  public static void get(Set<Project.NameKey> projects, AsyncCallback<AccessMap> callback) {
-    RestApi api = new RestApi("/access/");
-    for (Project.NameKey p : projects) {
-      api.addParameter("project", p.get());
-    }
-    api.get(NativeMap.copyKeysIntoChildren(callback));
-  }
-
-  public static void get(Project.NameKey project, AsyncCallback<ProjectAccessInfo> cb) {
-    get(
-        Collections.singleton(project),
-        new AsyncCallback<AccessMap>() {
-          @Override
-          public void onSuccess(AccessMap result) {
-            cb.onSuccess(result.get(project.get()));
-          }
-
-          @Override
-          public void onFailure(Throwable caught) {
-            cb.onFailure(caught);
-          }
-        });
-  }
-
-  protected AccessMap() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/ProjectAccessInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/ProjectAccessInfo.java
deleted file mode 100644
index 88635df..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/ProjectAccessInfo.java
+++ /dev/null
@@ -1,29 +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.client.access;
-
-import com.google.gwt.core.client.JavaScriptObject;
-
-public class ProjectAccessInfo extends JavaScriptObject {
-  public final native boolean canAddRefs() /*-{ return this.can_add ? true : false; }-*/;
-
-  public final native boolean canAddTagRefs() /*-{ return this.can_add_tags ? true : false; }-*/;
-
-  public final native boolean isOwner() /*-{ return this.is_owner ? true : false; }-*/;
-
-  public final native boolean configVisible() /*-{ return this.config_visible ? true : false; }-*/;
-
-  protected ProjectAccessInfo() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
deleted file mode 100644
index 7f4522f..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
+++ /dev/null
@@ -1,287 +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.client.account;
-
-import com.google.gerrit.client.VoidResult;
-import com.google.gerrit.client.info.AccountInfo;
-import com.google.gerrit.client.info.AgreementInfo;
-import com.google.gerrit.client.info.GpgKeyInfo;
-import com.google.gerrit.client.rpc.CallbackGroup;
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.NativeString;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.core.client.JsArrayString;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwtorm.client.KeyUtil;
-import java.util.HashSet;
-import java.util.Set;
-
-/** A collection of static methods which work on the Gerrit REST API for specific accounts. */
-public class AccountApi {
-  public static RestApi self() {
-    return new RestApi("/accounts/").view("self");
-  }
-
-  /** Retrieve the account edit preferences */
-  public static void getEditPreferences(AsyncCallback<EditPreferences> cb) {
-    self().view("preferences.edit").get(cb);
-  }
-
-  /** Put the account edit preferences */
-  public static void putEditPreferences(EditPreferences in, AsyncCallback<EditPreferences> cb) {
-    self().view("preferences.edit").put(in, cb);
-  }
-
-  public static void suggest(String query, int limit, AsyncCallback<JsArray<AccountInfo>> cb) {
-    new RestApi("/accounts/")
-        .addParameterTrue("suggest")
-        .addParameterRaw("q", KeyUtil.encode(query))
-        .addParameter("n", limit)
-        .background()
-        .get(cb);
-  }
-
-  public static void putDiffPreferences(DiffPreferences in, AsyncCallback<DiffPreferences> cb) {
-    self().view("preferences.diff").put(in, cb);
-  }
-
-  /** Retrieve the username */
-  public static void getUsername(String account, AsyncCallback<NativeString> cb) {
-    new RestApi("/accounts/").id(account).view("username").get(cb);
-  }
-
-  /** Set the username */
-  public static void setUsername(String account, String username, AsyncCallback<NativeString> cb) {
-    UsernameInput input = UsernameInput.create();
-    input.username(username);
-    new RestApi("/accounts/").id(account).view("username").put(input, cb);
-  }
-
-  /** Retrieve the account name */
-  public static void getName(String account, AsyncCallback<NativeString> cb) {
-    new RestApi("/accounts/").id(account).view("name").get(cb);
-  }
-
-  /** Set the account name */
-  public static void setName(String account, String name, AsyncCallback<NativeString> cb) {
-    AccountNameInput input = AccountNameInput.create();
-    input.name(name);
-    new RestApi("/accounts/").id(account).view("name").put(input, cb);
-  }
-
-  /** Retrieve email addresses */
-  public static void getEmails(String account, AsyncCallback<JsArray<EmailInfo>> cb) {
-    new RestApi("/accounts/").id(account).view("emails").get(cb);
-  }
-
-  /** Register a new email address */
-  public static void registerEmail(String account, String email, AsyncCallback<EmailInfo> cb) {
-    JavaScriptObject in = JavaScriptObject.createObject();
-    new RestApi("/accounts/").id(account).view("emails").id(email).ifNoneMatch().put(in, cb);
-  }
-
-  /** Set preferred email address */
-  public static void setPreferredEmail(
-      String account, String email, AsyncCallback<NativeString> cb) {
-    new RestApi("/accounts/").id(account).view("emails").id(email).view("preferred").put(cb);
-  }
-
-  /** Retrieve SSH keys */
-  public static void getSshKeys(String account, AsyncCallback<JsArray<SshKeyInfo>> cb) {
-    new RestApi("/accounts/").id(account).view("sshkeys").get(cb);
-  }
-
-  /** Add a new SSH keys */
-  public static void addSshKey(String account, String sshPublicKey, AsyncCallback<SshKeyInfo> cb) {
-    new RestApi("/accounts/").id(account).view("sshkeys").post(sshPublicKey, cb);
-  }
-
-  /** Retrieve Watched Projects */
-  public static void getWatchedProjects(
-      String account, AsyncCallback<JsArray<ProjectWatchInfo>> cb) {
-    new RestApi("/accounts/").id(account).view("watched.projects").get(cb);
-  }
-
-  /** Create/Update Watched Project */
-  public static void updateWatchedProject(
-      String account,
-      ProjectWatchInfo watchedProjectInfo,
-      AsyncCallback<JsArray<ProjectWatchInfo>> cb) {
-    Set<ProjectWatchInfo> watchedProjectInfos = new HashSet<>();
-    watchedProjectInfos.add(watchedProjectInfo);
-    updateWatchedProjects(account, watchedProjectInfos, cb);
-  }
-
-  /** Create/Update Watched Projects */
-  public static void updateWatchedProjects(
-      String account,
-      Set<ProjectWatchInfo> watchedProjectInfos,
-      AsyncCallback<JsArray<ProjectWatchInfo>> cb) {
-    new RestApi("/accounts/")
-        .id(account)
-        .view("watched.projects")
-        .post(projectWatchArrayFromSet(watchedProjectInfos), cb);
-  }
-
-  /** Delete Watched Project */
-  public static void deleteWatchedProject(
-      String account,
-      ProjectWatchInfo watchedProjectInfo,
-      AsyncCallback<JsArray<ProjectWatchInfo>> cb) {
-    Set<ProjectWatchInfo> watchedProjectInfos = new HashSet<>();
-    watchedProjectInfos.add(watchedProjectInfo);
-    deleteWatchedProjects(account, watchedProjectInfos, cb);
-  }
-
-  /** Delete Watched Projects */
-  public static void deleteWatchedProjects(
-      String account,
-      Set<ProjectWatchInfo> watchedProjectInfos,
-      AsyncCallback<JsArray<ProjectWatchInfo>> cb) {
-    new RestApi("/accounts/")
-        .id(account)
-        .view("watched.projects:delete")
-        .post(projectWatchArrayFromSet(watchedProjectInfos), cb);
-  }
-
-  /**
-   * Delete SSH keys. For each key to be deleted a separate DELETE request is fired to the server.
-   * The {@code onSuccess} method of the provided callback is invoked once after all requests
-   * succeeded. If any request fails the callbacks' {@code onFailure} method is invoked. In a
-   * failure case it can be that still some of the keys were successfully deleted.
-   */
-  public static void deleteSshKeys(
-      String account, Set<Integer> sequenceNumbers, AsyncCallback<VoidResult> cb) {
-    CallbackGroup group = new CallbackGroup();
-    for (int seq : sequenceNumbers) {
-      new RestApi("/accounts/").id(account).view("sshkeys").id(seq).delete(group.add(cb));
-      cb = CallbackGroup.emptyCallback();
-    }
-    group.done();
-  }
-
-  /** Generate a new HTTP password */
-  public static void generateHttpPassword(String account, AsyncCallback<NativeString> cb) {
-    HttpPasswordInput in = HttpPasswordInput.create();
-    in.generate(true);
-    new RestApi("/accounts/").id(account).view("password.http").put(in, cb);
-  }
-
-  /** Retrieve account external ids */
-  public static void getExternalIds(AsyncCallback<JsArray<ExternalIdInfo>> cb) {
-    self().view("external.ids").get(cb);
-  }
-
-  /** Delete account external ids */
-  public static void deleteExternalIds(Set<String> ids, AsyncCallback<VoidResult> cb) {
-    self().view("external.ids:delete").post(Natives.arrayOf(ids), cb);
-  }
-
-  /** Enter a contributor agreement */
-  public static void enterAgreement(String account, String name, AsyncCallback<NativeString> cb) {
-    AgreementInput in = AgreementInput.create();
-    in.name(name);
-    new RestApi("/accounts/").id(account).view("agreements").put(in, cb);
-  }
-
-  private static JsArray<ProjectWatchInfo> projectWatchArrayFromSet(Set<ProjectWatchInfo> set) {
-    JsArray<ProjectWatchInfo> jsArray = JsArray.createArray().cast();
-    for (ProjectWatchInfo p : set) {
-      jsArray.push(p);
-    }
-    return jsArray;
-  }
-
-  private static class AgreementInput extends JavaScriptObject {
-    final native void name(String n) /*-{ if(n)this.name=n; }-*/;
-
-    static AgreementInput create() {
-      return createObject().cast();
-    }
-
-    protected AgreementInput() {}
-  }
-
-  private static class HttpPasswordInput extends JavaScriptObject {
-    final native void generate(boolean g) /*-{ if(g)this.generate=g; }-*/;
-
-    static HttpPasswordInput create() {
-      return createObject().cast();
-    }
-
-    protected HttpPasswordInput() {}
-  }
-
-  private static class UsernameInput extends JavaScriptObject {
-    final native void username(String u) /*-{ if(u)this.username=u; }-*/;
-
-    static UsernameInput create() {
-      return createObject().cast();
-    }
-
-    protected UsernameInput() {}
-  }
-
-  private static class AccountNameInput extends JavaScriptObject {
-    final native void name(String n) /*-{ if(n)this.name=n; }-*/;
-
-    static AccountNameInput create() {
-      return createObject().cast();
-    }
-
-    protected AccountNameInput() {}
-  }
-
-  public static void addGpgKey(
-      String account, String armored, AsyncCallback<NativeMap<GpgKeyInfo>> cb) {
-    new RestApi("/accounts/").id(account).view("gpgkeys").post(GpgKeysInput.add(armored), cb);
-  }
-
-  public static void deleteGpgKeys(
-      String account, Iterable<String> fingerprints, AsyncCallback<NativeMap<GpgKeyInfo>> cb) {
-    new RestApi("/accounts/")
-        .id(account)
-        .view("gpgkeys")
-        .post(GpgKeysInput.delete(fingerprints), cb);
-  }
-
-  /** List contributor agreements */
-  public static void getAgreements(String account, AsyncCallback<JsArray<AgreementInfo>> cb) {
-    new RestApi("/accounts/").id(account).view("agreements").get(cb);
-  }
-
-  private static class GpgKeysInput extends JavaScriptObject {
-    static GpgKeysInput add(String key) {
-      return createWithAdd(Natives.arrayOf(key));
-    }
-
-    static GpgKeysInput delete(Iterable<String> fingerprints) {
-      return createWithDelete(Natives.arrayOf(fingerprints));
-    }
-
-    private static native GpgKeysInput createWithAdd(JsArrayString keys) /*-{
-      return {'add': keys};
-    }-*/;
-
-    private static native GpgKeysInput createWithDelete(JsArrayString fingerprints) /*-{
-      return {'delete': fingerprints};
-    }-*/;
-
-    protected GpgKeysInput() {}
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountCapabilities.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountCapabilities.java
deleted file mode 100644
index d317881..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountCapabilities.java
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.account;
-
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-
-/** Capabilities the caller has from {@code /accounts/self/capabilities}. */
-public class AccountCapabilities extends JavaScriptObject {
-  public static void all(AsyncCallback<AccountCapabilities> cb, String... filter) {
-    new RestApi("/accounts/self/capabilities").addParameter("q", filter).get(cb);
-  }
-
-  protected AccountCapabilities() {}
-
-  public final native boolean canPerform(String name) /*-{ return this[name] ? true : false; }-*/;
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
deleted file mode 100644
index 3e21619..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
+++ /dev/null
@@ -1,311 +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.client.account;
-
-import com.google.gwt.i18n.client.Constants;
-
-public interface AccountConstants extends Constants {
-  String settingsHeading();
-
-  String changeAvatar();
-
-  String fullName();
-
-  String preferredEmail();
-
-  String registeredOn();
-
-  String accountId();
-
-  String diffViewLabel();
-
-  String maximumPageSizeFieldLabel();
-
-  String dateFormatLabel();
-
-  String contextWholeFile();
-
-  String showSiteHeader();
-
-  String useFlashClipboard();
-
-  String reviewCategoryLabel();
-
-  String messageShowInReviewCategoryNone();
-
-  String messageShowInReviewCategoryName();
-
-  String messageShowInReviewCategoryEmail();
-
-  String messageShowInReviewCategoryUsername();
-
-  String messageShowInReviewCategoryAbbrev();
-
-  String buttonSaveChanges();
-
-  String highlightAssigneeInChangeTable();
-
-  String showRelativeDateInChangeTable();
-
-  String showSizeBarInChangeTable();
-
-  String showLegacycidInChangeTable();
-
-  String muteCommonPathPrefixes();
-
-  String signedOffBy();
-
-  String publishCommentsOnPush();
-
-  String workInProgressByDefault();
-
-  String myMenu();
-
-  String myMenuInfo();
-
-  String myMenuName();
-
-  String myMenuUrl();
-
-  String myMenuReset();
-
-  String tabAccountSummary();
-
-  String tabAgreements();
-
-  String tabContactInformation();
-
-  String tabDiffPreferences();
-
-  String tabEditPreferences();
-
-  String tabGpgKeys();
-
-  String tabHttpAccess();
-
-  String tabMyGroups();
-
-  String tabOAuthToken();
-
-  String tabPreferences();
-
-  String tabSshKeys();
-
-  String tabWatchedProjects();
-
-  String tabWebIdentities();
-
-  String buttonShowAddSshKey();
-
-  String buttonCloseAddSshKey();
-
-  String buttonDeleteSshKey();
-
-  String buttonClearSshKeyInput();
-
-  String buttonAddSshKey();
-
-  String userName();
-
-  String password();
-
-  String buttonSetUserName();
-
-  String confirmSetUserNameTitle();
-
-  String confirmSetUserName();
-
-  String buttonClearPassword();
-
-  String buttonGeneratePassword();
-
-  String revokePassword();
-
-  String linkObtainPassword();
-
-  String linkEditFullName();
-
-  String linkReloadContact();
-
-  String invalidUserName();
-
-  String invalidUserEmail();
-
-  String labelOAuthToken();
-
-  String labelOAuthExpires();
-
-  String labelOAuthNetRCEntry();
-
-  String labelOAuthGitCookie();
-
-  String labelOAuthExpired();
-
-  String sshKeyInvalid();
-
-  String sshKeyAlgorithm();
-
-  String sshKeyKey();
-
-  String sshKeyComment();
-
-  String sshKeyStatus();
-
-  String addSshKeyPanelHeader();
-
-  String addSshKeyHelpTitle();
-
-  String addSshKeyHelp();
-
-  String sshJavaAppletNotAvailable();
-
-  String invalidSshKeyError();
-
-  String sshHostKeyTitle();
-
-  String sshHostKeyFingerprint();
-
-  String sshHostKeyKnownHostEntry();
-
-  String gpgKeyId();
-
-  String gpgKeyFingerprint();
-
-  String gpgKeyUserIds();
-
-  String webIdStatus();
-
-  String webIdEmail();
-
-  String webIdIdentity();
-
-  String untrustedProvider();
-
-  String buttonDeleteIdentity();
-
-  String buttonLinkIdentity();
-
-  String buttonWatchProject();
-
-  String defaultProjectName();
-
-  String defaultFilter();
-
-  String buttonBrowseProjects();
-
-  String projects();
-
-  String projectsClose();
-
-  String projectListOpen();
-
-  String watchedProjectName();
-
-  String watchedProjectFilter();
-
-  String watchedProjectColumnEmailNotifications();
-
-  String watchedProjectColumnNewChanges();
-
-  String watchedProjectColumnNewPatchSets();
-
-  String watchedProjectColumnAllComments();
-
-  String watchedProjectColumnSubmittedChanges();
-
-  String watchedProjectColumnAbandonedChanges();
-
-  String contactFieldFullName();
-
-  String contactFieldEmail();
-
-  String buttonOpenRegisterNewEmail();
-
-  String buttonSendRegisterNewEmail();
-
-  String buttonCancel();
-
-  String titleRegisterNewEmail();
-
-  String descRegisterNewEmail();
-
-  String errorDialogTitleRegisterNewEmail();
-
-  String emailFilterHelpTitle();
-
-  String emailFilterHelp();
-
-  String newAgreement();
-
-  String agreementName();
-
-  String agreementDescription();
-
-  String newAgreementSelectTypeHeading();
-
-  String newAgreementNoneAvailable();
-
-  String newAgreementReviewLegalHeading();
-
-  String newAgreementReviewContactHeading();
-
-  String newAgreementCompleteHeading();
-
-  String newAgreementIAGREE();
-
-  String newAgreementAlreadySubmitted();
-
-  String buttonSubmitNewAgreement();
-
-  String welcomeToGerritCodeReview();
-
-  String welcomeReviewContact();
-
-  String welcomeContactFrom();
-
-  String welcomeUsernameHeading();
-
-  String welcomeSshKeyHeading();
-
-  String welcomeSshKeyText();
-
-  String welcomeAgreementHeading();
-
-  String welcomeAgreementText();
-
-  String welcomeAgreementLater();
-
-  String welcomeContinue();
-
-  String messageEnabled();
-
-  String messageCCMeOnMyComments();
-
-  String messageDisabled();
-
-  String emailFieldLabel();
-
-  String emailFormatFieldLabel();
-
-  String messagePlaintextOnly();
-
-  String messageHtmlPlaintext();
-
-  String defaultBaseForMerges();
-
-  String autoMerge();
-
-  String firstParent();
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
deleted file mode 100644
index c32efed..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
+++ /dev/null
@@ -1,274 +0,0 @@
-settingsHeading = Settings
-
-changeAvatar = Change Avatar
-fullName = Full Name
-preferredEmail = Email Address
-registeredOn = Registered
-accountId = Account ID
-showSiteHeader = Show Site Header / Footer
-useFlashClipboard = Use Flash Clipboard Widget
-reviewCategoryLabel = Display In Review Category
-messageShowInReviewCategoryNone = None (default)
-messageShowInReviewCategoryName = Show Name
-messageShowInReviewCategoryEmail = Show Email
-messageShowInReviewCategoryUsername = Show Username
-messageShowInReviewCategoryAbbrev = Show Abbreviated Name
-
-emailFieldLabel = Email Notifications:
-messageCCMeOnMyComments = Every Comment
-messageEnabled = Only Comments Left By Others
-messageDisabled = None
-
-emailFormatFieldLabel = Email Format:
-messagePlaintextOnly = Plaintext Only
-messageHtmlPlaintext = HTML and Plaintext
-
-defaultBaseForMerges = Default Base For Merges:
-autoMerge = Auto Merge
-firstParent = First Parent
-
-maximumPageSizeFieldLabel = Maximum Page Size:
-diffViewLabel = Diff View:
-dateFormatLabel = Date/Time Format:
-contextWholeFile = Whole File
-buttonSaveChanges = Save Changes
-highlightAssigneeInChangeTable = Highlight Changes Assigned To Me In Changes Table
-showRelativeDateInChangeTable = Show Relative Dates In Changes Table
-showSizeBarInChangeTable = Show Change Sizes As Colored Bars
-showLegacycidInChangeTable = Show Change Number In Changes Table
-muteCommonPathPrefixes = Mute Common Path Prefixes In File List
-signedOffBy = Insert Signed-off-by Footer For Inline Edit Changes
-publishCommentsOnPush = Publish Comments On Push
-workInProgressByDefault = Set all new changes work-in-progress by default
-myMenu = My Menu
-myMenuInfo = \
-  Menu items for the 'My' top level menu. \
-  The first menu item defines the default screen.
-myMenuName = Name
-myMenuUrl = URL
-myMenuReset = Reset
-
-tabAccountSummary = Profile
-tabAgreements = Agreements
-tabContactInformation = Contact Information
-tabDiffPreferences = Diff Preferences
-tabEditPreferences = Edit Preferences
-tabGpgKeys = GPG Public Keys
-tabHttpAccess = HTTP Password
-tabOAuthToken = OAuth Token
-tabMyGroups = Groups
-tabPreferences = Preferences
-tabSshKeys = SSH Public Keys
-tabWatchedProjects = Watched Projects
-tabWebIdentities = Identities
-
-buttonShowAddSshKey = Add Key ...
-buttonCloseAddSshKey = Close
-buttonDeleteSshKey = Delete
-buttonClearSshKeyInput = Clear
-buttonAddSshKey = Add
-
-userName = Username
-password = Password
-buttonSetUserName = Select Username
-confirmSetUserNameTitle = Confirm Setting the Username
-confirmSetUserName = Setting the Username is permanent.  Are you sure?
-buttonClearPassword = Clear Password
-buttonGeneratePassword = Generate Password
-revokePassword = (click 'Generate Password' to revoke an old password)
-linkObtainPassword = Obtain Password
-linkEditFullName = Edit
-linkReloadContact = Reload
-invalidUserName = Username must contain only letters, numbers, _, - or .
-invalidUserEmail = Email format is wrong.
-
-labelOAuthToken = Access Token
-labelOAuthExpires = Expires
-labelOAuthNetRCEntry = Entry for ~/.netrc
-labelOAuthGitCookie = Entry for ~/.gitcookies
-labelOAuthExpired = To obtain an access token please sign out and sign in again.
-
-sshKeyInvalid = Invalid Key
-sshKeyAlgorithm = Algorithm
-sshKeyKey = Key
-sshKeyComment = Comment
-sshKeyStatus = Status
-
-sshHostKeyTitle = Server Host Key
-sshHostKeyFingerprint = Fingerprint:
-sshHostKeyKnownHostEntry = Entry for <code>~/.ssh/known_hosts</code>:
-
-gpgKeyId = ID
-gpgKeyFingerprint = Fingerprint
-gpgKeyUserIds = User IDs
-
-webIdStatus = Status
-webIdEmail = Email Address
-webIdIdentity = Identity
-untrustedProvider = Untrusted
-buttonDeleteIdentity = Delete
-buttonLinkIdentity = Link Another Identity
-
-addSshKeyPanelHeader = Add SSH Public Key
-addSshKeyHelpTitle = How to Generate an SSH Key
-addSshKeyHelp = \
-  <ol>\
-    <li>\
-      From the Terminal or Git Bash, run <em>ssh-keygen</em>\
-    </li>\
-    <li>\
-      Confirm the default path <em>.ssh/id_rsa</em>\
-    </li>\
-    <li>\
-      Enter a passphrase (recommended) or leave it blank.<br />\
-      Remember this passphrase, as you will need it to unlock the<br />\
-      key whenever you use it.\
-    </li>\
-    <li>\
-      Open <em>~/.ssh/id_rsa.pub</em> and copy & paste the contents into<br />\
-      the box below, then click on "Add".<br />\
-      Note that <em>id_rsa.pub</em> is your public key and can be shared,<br />\
-      while <em>id_rsa</em> is your private key and should be kept secret.\
-    </li>\
-  <\ol>
-invalidSshKeyError = Invalid SSH Key
-sshJavaAppletNotAvailable = Open Key Unavailable: Java not enabled
-
-buttonWatchProject = Watch
-defaultProjectName = Project Name
-defaultFilter = branch:name, or other search expression
-projects = All Watchable Projects
-projectsClose = Close
-buttonBrowseProjects = Browse
-projectListOpen = Watch Selected project
-watchedProjectName = Project Name
-watchedProjectFilter = Only If
-watchedProjectColumnEmailNotifications = Email Notifications
-watchedProjectColumnNewChanges = New Changes
-watchedProjectColumnNewPatchSets = New Patch Sets
-watchedProjectColumnAllComments = All Comments
-watchedProjectColumnSubmittedChanges = Submitted Changes
-watchedProjectColumnAbandonedChanges = Abandoned Changes
-
-contactFieldFullName = Full Name
-contactFieldEmail = Preferred Email
-buttonOpenRegisterNewEmail = Register New Email ...
-buttonSendRegisterNewEmail = Register
-buttonCancel = Cancel
-titleRegisterNewEmail = Register Email Address
-descRegisterNewEmail = \
-  <p>A confirmation link will be sent by email to this address.</p>\
-  <p>You must click on the link to complete the registration and make the address available for selection.</p>
-errorDialogTitleRegisterNewEmail = Email Registration Failed
-emailFilterHelpTitle = Mail Filters
-emailFilterHelp = \
-  <p>\
-    Gerrit emails include metadata about the change to support \
-    writing mail filters.\
-  </p>\
-  <p>\
-    Here are some example Gmail queries that can be used for filters or \
-    for searching through archived messages. View the \
-    <a href="https://gerrit-review.googlesource.com/Documentation/user-notify.html"\
-        target="_blank" rel="nofollow">Gerrit documentation</a> for \
-    the complete set of footers.\
-  </p>\
-  <table>\
-    <tbody>\
-      <tr><th>Name</th><th>Query</th></tr>\
-      <tr>\
-        <td>Changes requesting my review</td>\
-        <td>\
-          <code>\
-            "Gerrit-Reviewer: <em>Your Name</em>\
-            &lt;<em>your.email@example.com</em>&gt;"\
-          </code>\
-        </td>\
-      </tr>\
-      <tr>\
-        <td>Changes from a specific owner</td>\
-        <td>\
-          <code>\
-            "Gerrit-Owner: <em>Owner name</em>\
-            &lt;<em>owner.email@example.com</em>&gt;"\
-          </code>\
-        </td>\
-      </tr>\
-      <tr>\
-        <td>Changes targeting a specific branch</td>\
-        <td>\
-          <code>\
-            "Gerrit-Branch: <em>branch-name</em>"\
-          </code>\
-        </td>\
-      </tr>\
-      <tr>\
-        <td>Changes in a specific project</td>\
-        <td>\
-          <code>\
-            "Gerrit-Project: <em>project-name</em>"\
-          </code>\
-        </td>\
-      </tr>\
-      <tr>\
-        <td>Messages related to a specific Change ID</td>\
-        <td>\
-          <code>\
-            "Gerrit-Change-Id: <em>Change ID</em>"\
-          </code>\
-        </td>\
-      </tr>\
-      <tr>\
-        <td>Messages related to a specific change number</td>\
-        <td>\
-          <code>\
-            "Gerrit-Change-Number: <em>change number</em>"\
-          </code>\
-        </td>\
-      </tr>\
-    </tbody>\
-  </table>
-
-newAgreement = New Contributor Agreement
-agreementName = Name
-agreementDescription = Description
-
-newAgreementSelectTypeHeading = Select an agreement type:
-newAgreementNoneAvailable = No contributor agreements are configured.
-newAgreementReviewLegalHeading = Review the agreement:
-newAgreementReviewContactHeading = Review your contact information:
-newAgreementCompleteHeading = Complete the agreement:
-newAgreementIAGREE = I AGREE
-newAgreementAlreadySubmitted = Agreement already submitted.
-buttonSubmitNewAgreement = Submit Agreement
-
-welcomeToGerritCodeReview = Welcome to Gerrit Code Review
-welcomeReviewContact = Please review your contact information:
-welcomeContactFrom = \
-  <p>The following contact information was automatically obtained when \
-  you signed-in to the site.  This information is used to display who \
-  you are to others, and to send updates to code reviews you have either \
-  started or subscribed to.</p>
-
-welcomeUsernameHeading = Select a unique username:
-
-welcomeSshKeyHeading = Register an SSH public key:
-welcomeSshKeyText = \
-  <p>Gerrit Code Review uses \
-  <a href="http://en.wikipedia.org/wiki/Public-key_cryptography" target="_blank">public-key cryptography</a> \
-  and \
-  <a href="http://en.wikipedia.org/wiki/Secure_Shell" target="_blank">SSH</a> \
-  to authenticate \
-  you during git's push and pull commands to hosted projects.  Registering \
-  your public key allows Gerrit to identify you whenever you connect through \
-  SSH.</p>\
-  <p>This step can also be completed at a later time.</p>
-
-welcomeAgreementHeading = Complete a contributor agreement:
-welcomeAgreementText = \
-  <p>If you will be contributing code or documentation changes to projects \
-  hosted here, please consider taking a minute to review and complete \
-  a contributor agreement.</p>\
-  <p>This step can also be completed at a later time.</p>
-welcomeAgreementLater = Continue Without Agreement
-welcomeContinue = Continue
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountMessages.java
deleted file mode 100644
index 4cff1e2..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountMessages.java
+++ /dev/null
@@ -1,27 +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.client.account;
-
-import com.google.gwt.i18n.client.Messages;
-
-public interface AccountMessages extends Messages {
-  String lines(short cnt);
-
-  String rowsPerPage(int cnt);
-
-  String changeScreenServerDefault(String d);
-
-  String enterIAGREE(String iagree);
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountMessages.properties
deleted file mode 100644
index a8d61cf..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountMessages.properties
+++ /dev/null
@@ -1,4 +0,0 @@
-lines = {0} lines
-rowsPerPage = {0} rows per page
-changeScreenServerDefault = Server Default ({0})
-enterIAGREE = (enter {0} in the box to the left)
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
deleted file mode 100644
index a537063..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
+++ /dev/null
@@ -1,484 +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.client.account;
-
-import com.google.gerrit.client.ErrorDialog;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.info.AccountInfo;
-import com.google.gerrit.client.rpc.CallbackGroup;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.NativeString;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.ui.ComplexDisclosurePanel;
-import com.google.gerrit.client.ui.OnEditEnabler;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.errors.EmailException;
-import com.google.gerrit.extensions.client.AccountFieldName;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.event.dom.client.ChangeEvent;
-import com.google.gwt.event.dom.client.ChangeHandler;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.i18n.client.LocaleInfo;
-import com.google.gwt.user.client.Window;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.FormPanel;
-import com.google.gwt.user.client.ui.FormPanel.SubmitEvent;
-import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.HTML;
-import com.google.gwt.user.client.ui.ListBox;
-import com.google.gwt.user.client.ui.VerticalPanel;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtexpui.globalkey.client.NpTextBox;
-import com.google.gwtexpui.user.client.AutoCenterDialogBox;
-
-class ContactPanelShort extends Composite {
-  protected final FlowPanel body;
-  protected int labelIdx;
-  protected int fieldIdx;
-  protected Button save;
-
-  private String currentEmail;
-  protected boolean haveAccount;
-  private boolean haveEmails;
-
-  NpTextBox nameTxt;
-  private ListBox emailPick;
-  private Button registerNewEmail;
-  private OnEditEnabler onEditEnabler;
-
-  ContactPanelShort() {
-    body = new FlowPanel();
-    initWidget(body);
-  }
-
-  protected void onInitUI() {
-    if (LocaleInfo.getCurrentLocale().isRTL()) {
-      labelIdx = 1;
-      fieldIdx = 0;
-    } else {
-      labelIdx = 0;
-      fieldIdx = 1;
-    }
-
-    nameTxt = new NpTextBox();
-    nameTxt.setVisibleLength(60);
-    nameTxt.setReadOnly(!canEditFullName());
-
-    emailPick = new ListBox();
-
-    final Grid infoPlainText = new Grid(2, 2);
-    infoPlainText.setStyleName(Gerrit.RESOURCES.css().infoBlock());
-    infoPlainText.addStyleName(Gerrit.RESOURCES.css().accountInfoBlock());
-
-    body.add(infoPlainText);
-
-    registerNewEmail = new Button(Util.C.buttonOpenRegisterNewEmail());
-    registerNewEmail.setEnabled(false);
-    registerNewEmail.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            doRegisterNewEmail();
-          }
-        });
-    final FlowPanel emailLine = new FlowPanel();
-    emailLine.add(emailPick);
-    if (canRegisterNewEmail()) {
-      emailLine.add(registerNewEmail);
-    }
-
-    int row = 0;
-    if (!Gerrit.info().auth().canEdit(AccountFieldName.USER_NAME)
-        && Gerrit.info().auth().siteHasUsernames()) {
-      infoPlainText.resizeRows(infoPlainText.getRowCount() + 1);
-      row(infoPlainText, row++, Util.C.userName(), new UsernameField());
-    }
-
-    if (!canEditFullName()) {
-      FlowPanel nameLine = new FlowPanel();
-      nameLine.add(nameTxt);
-      if (Gerrit.info().auth().editFullNameUrl() != null) {
-        Button edit = new Button(Util.C.linkEditFullName());
-        edit.addClickHandler(
-            new ClickHandler() {
-              @Override
-              public void onClick(ClickEvent event) {
-                Window.open(Gerrit.info().auth().editFullNameUrl(), "_blank", null);
-              }
-            });
-        nameLine.add(edit);
-      }
-      Button reload = new Button(Util.C.linkReloadContact());
-      reload.addClickHandler(
-          new ClickHandler() {
-            @Override
-            public void onClick(ClickEvent event) {
-              Window.Location.replace(Gerrit.loginRedirect(PageLinks.SETTINGS_CONTACT));
-            }
-          });
-      nameLine.add(reload);
-      row(infoPlainText, row++, Util.C.contactFieldFullName(), nameLine);
-    } else {
-      row(infoPlainText, row++, Util.C.contactFieldFullName(), nameTxt);
-    }
-    row(infoPlainText, row++, Util.C.contactFieldEmail(), emailLine);
-
-    infoPlainText.getCellFormatter().addStyleName(0, 0, Gerrit.RESOURCES.css().topmost());
-    infoPlainText.getCellFormatter().addStyleName(0, 1, Gerrit.RESOURCES.css().topmost());
-    infoPlainText
-        .getCellFormatter()
-        .addStyleName(row - 1, 0, Gerrit.RESOURCES.css().bottomheader());
-
-    save = new Button(Util.C.buttonSaveChanges());
-    save.setEnabled(false);
-    save.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            doSave();
-          }
-        });
-
-    final ComplexDisclosurePanel mailFilterHelp =
-        new ComplexDisclosurePanel(Util.C.emailFilterHelpTitle(), false);
-    mailFilterHelp.setContent(new HTML(Util.C.emailFilterHelp()));
-    body.add(mailFilterHelp);
-
-    emailPick.addChangeHandler(
-        new ChangeHandler() {
-          @Override
-          public void onChange(ChangeEvent event) {
-            final int idx = emailPick.getSelectedIndex();
-            final String v = 0 <= idx ? emailPick.getValue(idx) : null;
-            if (Util.C.buttonOpenRegisterNewEmail().equals(v)) {
-              for (int i = 0; i < emailPick.getItemCount(); i++) {
-                if (currentEmail.equals(emailPick.getValue(i))) {
-                  emailPick.setSelectedIndex(i);
-                  break;
-                }
-              }
-              doRegisterNewEmail();
-            } else {
-              save.setEnabled(true);
-            }
-          }
-        });
-
-    onEditEnabler = new OnEditEnabler(save, nameTxt);
-  }
-
-  private boolean canEditFullName() {
-    return Gerrit.info().auth().canEdit(AccountFieldName.FULL_NAME);
-  }
-
-  private boolean canRegisterNewEmail() {
-    return Gerrit.info().auth().canEdit(AccountFieldName.REGISTER_NEW_EMAIL);
-  }
-
-  void hideSaveButton() {
-    save.setVisible(false);
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-
-    onInitUI();
-    body.add(save);
-    display(Gerrit.getUserAccount());
-
-    emailPick.clear();
-    emailPick.setEnabled(false);
-    registerNewEmail.setEnabled(false);
-
-    haveAccount = false;
-    haveEmails = false;
-
-    CallbackGroup group = new CallbackGroup();
-    AccountApi.getName(
-        "self",
-        group.add(
-            new GerritCallback<NativeString>() {
-
-              @Override
-              public void onSuccess(NativeString result) {
-                nameTxt.setText(result.asString());
-                haveAccount = true;
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {}
-            }));
-
-    AccountApi.getEmails(
-        "self",
-        group.addFinal(
-            new GerritCallback<JsArray<EmailInfo>>() {
-              @Override
-              public void onSuccess(JsArray<EmailInfo> result) {
-                for (EmailInfo i : Natives.asList(result)) {
-                  emailPick.addItem(i.email());
-                  if (i.isPreferred()) {
-                    currentEmail = i.email();
-                  }
-                }
-                haveEmails = true;
-                postLoad();
-              }
-            }));
-  }
-
-  private void postLoad() {
-    if (haveAccount && haveEmails) {
-      updateEmailList();
-      registerNewEmail.setEnabled(true);
-      save.setEnabled(false);
-      onEditEnabler.updateOriginalValue(nameTxt);
-    }
-    display();
-  }
-
-  void display() {}
-
-  protected void row(Grid info, int row, String name, Widget field) {
-    info.setText(row, labelIdx, name);
-    info.setWidget(row, fieldIdx, field);
-    info.getCellFormatter().addStyleName(row, 0, Gerrit.RESOURCES.css().header());
-  }
-
-  protected void display(AccountInfo account) {
-    currentEmail = account.email();
-    nameTxt.setText(account.name());
-    save.setEnabled(false);
-    onEditEnabler.updateOriginalValue(nameTxt);
-  }
-
-  private void doRegisterNewEmail() {
-    if (!canRegisterNewEmail()) {
-      return;
-    }
-
-    final AutoCenterDialogBox box = new AutoCenterDialogBox(true, true);
-    final VerticalPanel body = new VerticalPanel();
-
-    final NpTextBox inEmail = new NpTextBox();
-    inEmail.setVisibleLength(60);
-
-    final Button register = new Button(Util.C.buttonSendRegisterNewEmail());
-    final Button cancel = new Button(Util.C.buttonCancel());
-    final FormPanel form = new FormPanel();
-    form.addSubmitHandler(
-        new FormPanel.SubmitHandler() {
-          @Override
-          public void onSubmit(SubmitEvent event) {
-            event.cancel();
-            final String addr = inEmail.getText().trim();
-            if (!addr.contains("@")) {
-              new ErrorDialog(Util.C.invalidUserEmail()).center();
-              return;
-            }
-
-            inEmail.setEnabled(false);
-            register.setEnabled(false);
-            AccountApi.registerEmail(
-                "self",
-                addr,
-                new GerritCallback<EmailInfo>() {
-                  @Override
-                  public void onSuccess(EmailInfo result) {
-                    box.hide();
-                    if (Gerrit.info().auth().isDev()) {
-                      currentEmail = addr;
-                      if (emailPick.getItemCount() == 0) {
-                        AccountInfo me = Gerrit.getUserAccount();
-                        me.email(addr);
-                        onSaveSuccess(me);
-                      } else {
-                        save.setEnabled(true);
-                      }
-                      updateEmailList();
-                    }
-                  }
-
-                  @Override
-                  public void onFailure(Throwable caught) {
-                    inEmail.setEnabled(true);
-                    register.setEnabled(true);
-                    if (caught.getMessage().startsWith(EmailException.MESSAGE)) {
-                      final ErrorDialog d =
-                          new ErrorDialog(
-                              caught.getMessage().substring(EmailException.MESSAGE.length()));
-                      d.setText(Util.C.errorDialogTitleRegisterNewEmail());
-                      d.center();
-                    } else {
-                      super.onFailure(caught);
-                    }
-                  }
-                });
-          }
-        });
-    form.setWidget(body);
-
-    register.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            form.submit();
-          }
-        });
-    cancel.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            box.hide();
-          }
-        });
-
-    final FlowPanel buttons = new FlowPanel();
-    buttons.setStyleName(Gerrit.RESOURCES.css().patchSetActions());
-    buttons.add(register);
-    buttons.add(cancel);
-
-    if (!Gerrit.info().auth().isDev()) {
-      body.add(new HTML(Util.C.descRegisterNewEmail()));
-    }
-    body.add(inEmail);
-    body.add(buttons);
-
-    box.setText(Util.C.titleRegisterNewEmail());
-    box.setWidget(form);
-    box.center();
-    inEmail.setFocus(true);
-  }
-
-  void doSave() {
-    final String newName;
-    String name = canEditFullName() ? nameTxt.getText() : null;
-    if (name != null && name.trim().isEmpty()) {
-      newName = null;
-    } else {
-      newName = name;
-    }
-
-    final String newEmail;
-    if (emailPick.isEnabled() && emailPick.getSelectedIndex() >= 0) {
-      final String v = emailPick.getValue(emailPick.getSelectedIndex());
-      if (Util.C.buttonOpenRegisterNewEmail().equals(v)) {
-        newEmail = currentEmail;
-      } else {
-        newEmail = v;
-      }
-    } else {
-      newEmail = currentEmail;
-    }
-
-    save.setEnabled(false);
-    registerNewEmail.setEnabled(false);
-
-    CallbackGroup group = new CallbackGroup();
-    if (currentEmail != null && !newEmail.equals(currentEmail)) {
-      AccountApi.setPreferredEmail(
-          "self",
-          newEmail,
-          group.add(
-              new GerritCallback<NativeString>() {
-                @Override
-                public void onSuccess(NativeString result) {}
-              }));
-    }
-    AccountApi.setName(
-        "self",
-        newName,
-        group.add(
-            new GerritCallback<NativeString>() {
-              @Override
-              public void onSuccess(NativeString result) {}
-
-              @Override
-              public void onFailure(Throwable caught) {
-                save.setEnabled(true);
-                registerNewEmail.setEnabled(true);
-                super.onFailure(caught);
-              }
-            }));
-    group.done();
-    group.addListener(
-        new GerritCallback<Void>() {
-          @Override
-          public void onSuccess(Void result) {
-            currentEmail = newEmail;
-            AccountInfo me = Gerrit.getUserAccount();
-            me.email(currentEmail);
-            me.name(newName);
-            onSaveSuccess(me);
-            registerNewEmail.setEnabled(true);
-          }
-        });
-  }
-
-  void onSaveSuccess(AccountInfo result) {
-    AccountInfo me = Gerrit.getUserAccount();
-    me.name(result.name());
-    me.email(result.email());
-    Gerrit.refreshMenuBar();
-    display(me);
-  }
-
-  private int emailListIndexOf(String value) {
-    for (int i = 0; i < emailPick.getItemCount(); i++) {
-      if (value.equalsIgnoreCase(emailPick.getValue(i))) {
-        return i;
-      }
-    }
-    return -1;
-  }
-
-  private void updateEmailList() {
-    if (currentEmail != null) {
-      int index = emailListIndexOf(currentEmail);
-      if (index == -1) {
-        emailPick.addItem(currentEmail);
-        emailPick.setSelectedIndex(emailPick.getItemCount() - 1);
-      } else {
-        emailPick.setSelectedIndex(index);
-      }
-    }
-    if (emailPick.getItemCount() > 0) {
-      if (currentEmail == null) {
-        int index = emailListIndexOf("");
-        if (index != -1) {
-          emailPick.removeItem(index);
-        }
-        emailPick.insertItem("", 0);
-        emailPick.setSelectedIndex(0);
-      }
-      emailPick.setVisible(true);
-      emailPick.setEnabled(true);
-      if (canRegisterNewEmail()) {
-        final String t = Util.C.buttonOpenRegisterNewEmail();
-        int index = emailListIndexOf(t);
-        if (index != -1) {
-          emailPick.removeItem(index);
-        }
-        emailPick.addItem("... " + t + "  ", t);
-      }
-    } else {
-      emailPick.setVisible(false);
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/DiffPreferences.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/DiffPreferences.java
deleted file mode 100644
index 286d29a..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/DiffPreferences.java
+++ /dev/null
@@ -1,226 +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.client.account;
-
-import com.google.gerrit.extensions.client.DiffPreferencesInfo;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
-import com.google.gerrit.extensions.client.Theme;
-import com.google.gwt.core.client.JavaScriptObject;
-
-public class DiffPreferences extends JavaScriptObject {
-  public static DiffPreferences create(DiffPreferencesInfo in) {
-    if (in == null) {
-      in = DiffPreferencesInfo.defaults();
-    }
-    DiffPreferences p = createObject().cast();
-    p.ignoreWhitespace(in.ignoreWhitespace);
-    p.tabSize(in.tabSize);
-    p.lineLength(in.lineLength);
-    p.cursorBlinkRate(in.cursorBlinkRate);
-    p.context(in.context);
-    p.intralineDifference(in.intralineDifference);
-    p.showLineEndings(in.showLineEndings);
-    p.showTabs(in.showTabs);
-    p.showWhitespaceErrors(in.showWhitespaceErrors);
-    p.syntaxHighlighting(in.syntaxHighlighting);
-    p.hideTopMenu(in.hideTopMenu);
-    p.autoHideDiffTableHeader(in.autoHideDiffTableHeader);
-    p.hideLineNumbers(in.hideLineNumbers);
-    p.expandAllComments(in.expandAllComments);
-    p.manualReview(in.manualReview);
-    p.renderEntireFile(in.renderEntireFile);
-    p.theme(in.theme);
-    p.hideEmptyPane(in.hideEmptyPane);
-    p.retainHeader(in.retainHeader);
-    p.skipUnchanged(in.skipUnchanged);
-    p.skipUncommented(in.skipUncommented);
-    p.skipDeleted(in.skipDeleted);
-    p.matchBrackets(in.matchBrackets);
-    p.lineWrapping(in.lineWrapping);
-    return p;
-  }
-
-  public final void copyTo(DiffPreferencesInfo p) {
-    p.context = context();
-    p.tabSize = tabSize();
-    p.lineLength = lineLength();
-    p.cursorBlinkRate = cursorBlinkRate();
-    p.expandAllComments = expandAllComments();
-    p.intralineDifference = intralineDifference();
-    p.manualReview = manualReview();
-    p.retainHeader = retainHeader();
-    p.showLineEndings = showLineEndings();
-    p.showTabs = showTabs();
-    p.showWhitespaceErrors = showWhitespaceErrors();
-    p.skipDeleted = skipDeleted();
-    p.skipUnchanged = skipUnchanged();
-    p.skipUncommented = skipUncommented();
-    p.syntaxHighlighting = syntaxHighlighting();
-    p.hideTopMenu = hideTopMenu();
-    p.autoHideDiffTableHeader = autoHideDiffTableHeader();
-    p.hideLineNumbers = hideLineNumbers();
-    p.renderEntireFile = renderEntireFile();
-    p.hideEmptyPane = hideEmptyPane();
-    p.matchBrackets = matchBrackets();
-    p.lineWrapping = lineWrapping();
-    p.theme = theme();
-    p.ignoreWhitespace = ignoreWhitespace();
-  }
-
-  public final void ignoreWhitespace(Whitespace i) {
-    setIgnoreWhitespaceRaw(i.toString());
-  }
-
-  public final void theme(Theme i) {
-    setThemeRaw(i != null ? i.toString() : Theme.DEFAULT.toString());
-  }
-
-  public final void showLineNumbers(boolean s) {
-    hideLineNumbers(!s);
-  }
-
-  public final Whitespace ignoreWhitespace() {
-    String s = ignoreWhitespaceRaw();
-    return s != null ? Whitespace.valueOf(s) : Whitespace.IGNORE_NONE;
-  }
-
-  public final Theme theme() {
-    String s = themeRaw();
-    return s != null ? Theme.valueOf(s) : Theme.DEFAULT;
-  }
-
-  public final int tabSize() {
-    return get("tab_size", 8);
-  }
-
-  public final int context() {
-    return get("context", 10);
-  }
-
-  public final int lineLength() {
-    return get("line_length", 100);
-  }
-
-  public final int cursorBlinkRate() {
-    return get("cursor_blink_rate", 0);
-  }
-
-  public final boolean showLineNumbers() {
-    return !hideLineNumbers();
-  }
-
-  public final boolean autoReview() {
-    return !manualReview();
-  }
-
-  public final native void tabSize(int t) /*-{ this.tab_size = t }-*/;
-
-  public final native void lineLength(int c) /*-{ this.line_length = c }-*/;
-
-  public final native void context(int c) /*-{ this.context = c }-*/;
-
-  public final native void cursorBlinkRate(int r) /*-{ this.cursor_blink_rate = r }-*/;
-
-  public final native void intralineDifference(Boolean i) /*-{ this.intraline_difference = i }-*/;
-
-  public final native void showLineEndings(Boolean s) /*-{ this.show_line_endings = s }-*/;
-
-  public final native void showTabs(Boolean s) /*-{ this.show_tabs = s }-*/;
-
-  public final native void showWhitespaceErrors(
-      Boolean s) /*-{ this.show_whitespace_errors = s }-*/;
-
-  public final native void syntaxHighlighting(Boolean s) /*-{ this.syntax_highlighting = s }-*/;
-
-  public final native void hideTopMenu(Boolean s) /*-{ this.hide_top_menu = s }-*/;
-
-  public final native void autoHideDiffTableHeader(
-      Boolean s) /*-{ this.auto_hide_diff_table_header = s }-*/;
-
-  public final native void hideLineNumbers(Boolean s) /*-{ this.hide_line_numbers = s }-*/;
-
-  public final native void expandAllComments(Boolean e) /*-{ this.expand_all_comments = e }-*/;
-
-  public final native void manualReview(Boolean r) /*-{ this.manual_review = r }-*/;
-
-  public final native void renderEntireFile(Boolean r) /*-{ this.render_entire_file = r }-*/;
-
-  public final native void retainHeader(Boolean r) /*-{ this.retain_header = r }-*/;
-
-  public final native void hideEmptyPane(Boolean s) /*-{ this.hide_empty_pane = s }-*/;
-
-  public final native void skipUnchanged(Boolean s) /*-{ this.skip_unchanged = s }-*/;
-
-  public final native void skipUncommented(Boolean s) /*-{ this.skip_uncommented = s }-*/;
-
-  public final native void skipDeleted(Boolean s) /*-{ this.skip_deleted = s }-*/;
-
-  public final native void matchBrackets(Boolean m) /*-{ this.match_brackets = m }-*/;
-
-  public final native void lineWrapping(Boolean w) /*-{ this.line_wrapping = w }-*/;
-
-  public final native boolean
-      intralineDifference() /*-{ return this.intraline_difference || false }-*/;
-
-  public final native boolean showLineEndings() /*-{ return this.show_line_endings || false }-*/;
-
-  public final native boolean showTabs() /*-{ return this.show_tabs || false }-*/;
-
-  public final native boolean
-      showWhitespaceErrors() /*-{ return this.show_whitespace_errors || false }-*/;
-
-  public final native boolean
-      syntaxHighlighting() /*-{ return this.syntax_highlighting || false }-*/;
-
-  public final native boolean hideTopMenu() /*-{ return this.hide_top_menu || false }-*/;
-
-  public final native boolean
-      autoHideDiffTableHeader() /*-{ return this.auto_hide_diff_table_header || false }-*/;
-
-  public final native boolean hideLineNumbers() /*-{ return this.hide_line_numbers || false }-*/;
-
-  public final native boolean
-      expandAllComments() /*-{ return this.expand_all_comments || false }-*/;
-
-  public final native boolean manualReview() /*-{ return this.manual_review || false }-*/;
-
-  public final native boolean renderEntireFile() /*-{ return this.render_entire_file || false }-*/;
-
-  public final native boolean hideEmptyPane() /*-{ return this.hide_empty_pane || false }-*/;
-
-  public final native boolean retainHeader() /*-{ return this.retain_header || false }-*/;
-
-  public final native boolean skipUnchanged() /*-{ return this.skip_unchanged || false }-*/;
-
-  public final native boolean skipUncommented() /*-{ return this.skip_uncommented || false }-*/;
-
-  public final native boolean skipDeleted() /*-{ return this.skip_deleted || false }-*/;
-
-  public final native boolean matchBrackets() /*-{ return this.match_brackets || false }-*/;
-
-  public final native boolean lineWrapping() /*-{ return this.line_wrapping || false }-*/;
-
-  private native void setThemeRaw(String i) /*-{ this.theme = i }-*/;
-
-  private native void setIgnoreWhitespaceRaw(String i) /*-{ this.ignore_whitespace = i }-*/;
-
-  private native String ignoreWhitespaceRaw() /*-{ return this.ignore_whitespace }-*/;
-
-  private native String themeRaw() /*-{ return this.theme }-*/;
-
-  private native int get(String n, int d) /*-{ return this.hasOwnProperty(n) ? this[n] : d }-*/;
-
-  protected DiffPreferences() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/EditPreferences.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/EditPreferences.java
deleted file mode 100644
index 9cd2f17..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/EditPreferences.java
+++ /dev/null
@@ -1,161 +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.client.account;
-
-import com.google.gerrit.extensions.client.EditPreferencesInfo;
-import com.google.gerrit.extensions.client.KeyMapType;
-import com.google.gerrit.extensions.client.Theme;
-import com.google.gwt.core.client.JavaScriptObject;
-
-public class EditPreferences extends JavaScriptObject {
-  public static EditPreferences create(EditPreferencesInfo in) {
-    EditPreferences p = createObject().cast();
-    p.tabSize(in.tabSize);
-    p.lineLength(in.lineLength);
-    p.indentUnit(in.indentUnit);
-    p.cursorBlinkRate(in.cursorBlinkRate);
-    p.hideTopMenu(in.hideTopMenu);
-    p.showTabs(in.showTabs);
-    p.showWhitespaceErrors(in.showWhitespaceErrors);
-    p.syntaxHighlighting(in.syntaxHighlighting);
-    p.hideLineNumbers(in.hideLineNumbers);
-    p.matchBrackets(in.matchBrackets);
-    p.lineWrapping(in.lineWrapping);
-    p.indentWithTabs(in.indentWithTabs);
-    p.autoCloseBrackets(in.autoCloseBrackets);
-    p.showBase(in.showBase);
-    p.theme(in.theme);
-    p.keyMapType(in.keyMapType);
-    return p;
-  }
-
-  public final EditPreferencesInfo copyTo(EditPreferencesInfo p) {
-    p.tabSize = tabSize();
-    p.lineLength = lineLength();
-    p.indentUnit = indentUnit();
-    p.cursorBlinkRate = cursorBlinkRate();
-    p.hideTopMenu = hideTopMenu();
-    p.showTabs = showTabs();
-    p.showWhitespaceErrors = showWhitespaceErrors();
-    p.syntaxHighlighting = syntaxHighlighting();
-    p.hideLineNumbers = hideLineNumbers();
-    p.matchBrackets = matchBrackets();
-    p.lineWrapping = lineWrapping();
-    p.indentWithTabs = indentWithTabs();
-    p.autoCloseBrackets = autoCloseBrackets();
-    p.showBase = showBase();
-    p.theme = theme();
-    p.keyMapType = keyMapType();
-    return p;
-  }
-
-  public final void theme(Theme i) {
-    setThemeRaw(i != null ? i.toString() : Theme.DEFAULT.toString());
-  }
-
-  private native void setThemeRaw(String i) /*-{ this.theme = i }-*/;
-
-  public final void keyMapType(KeyMapType i) {
-    setkeyMapTypeRaw(i != null ? i.toString() : KeyMapType.DEFAULT.toString());
-  }
-
-  private native void setkeyMapTypeRaw(String i) /*-{ this.key_map_type = i }-*/;
-
-  public final native void tabSize(int t) /*-{ this.tab_size = t }-*/;
-
-  public final native void lineLength(int c) /*-{ this.line_length = c }-*/;
-
-  public final native void indentUnit(int c) /*-{ this.indent_unit = c }-*/;
-
-  public final native void cursorBlinkRate(int r) /*-{ this.cursor_blink_rate = r }-*/;
-
-  public final native void hideTopMenu(boolean s) /*-{ this.hide_top_menu = s }-*/;
-
-  public final native void showTabs(boolean s) /*-{ this.show_tabs = s }-*/;
-
-  public final native void showWhitespaceErrors(
-      boolean s) /*-{ this.show_whitespace_errors = s }-*/;
-
-  public final native void syntaxHighlighting(boolean s) /*-{ this.syntax_highlighting = s }-*/;
-
-  public final native void hideLineNumbers(boolean s) /*-{ this.hide_line_numbers = s }-*/;
-
-  public final native void matchBrackets(boolean m) /*-{ this.match_brackets = m }-*/;
-
-  public final native void lineWrapping(boolean w) /*-{ this.line_wrapping = w }-*/;
-
-  public final native void indentWithTabs(boolean w) /*-{ this.indent_with_tabs = w }-*/;
-
-  public final native void autoCloseBrackets(boolean c) /*-{ this.auto_close_brackets = c }-*/;
-
-  public final native void showBase(boolean s) /*-{ this.show_base = s }-*/;
-
-  public final Theme theme() {
-    String s = themeRaw();
-    return s != null ? Theme.valueOf(s) : Theme.DEFAULT;
-  }
-
-  private native String themeRaw() /*-{ return this.theme }-*/;
-
-  public final KeyMapType keyMapType() {
-    String s = keyMapTypeRaw();
-    return s != null ? KeyMapType.valueOf(s) : KeyMapType.DEFAULT;
-  }
-
-  private native String keyMapTypeRaw() /*-{ return this.key_map_type }-*/;
-
-  public final int tabSize() {
-    return get("tab_size", 8);
-  }
-
-  public final int lineLength() {
-    return get("line_length", 100);
-  }
-
-  public final int indentUnit() {
-    return get("indent_unit", 2);
-  }
-
-  public final int cursorBlinkRate() {
-    return get("cursor_blink_rate", 0);
-  }
-
-  public final native boolean hideTopMenu() /*-{ return this.hide_top_menu || false }-*/;
-
-  public final native boolean showTabs() /*-{ return this.show_tabs || false }-*/;
-
-  public final native boolean
-      showWhitespaceErrors() /*-{ return this.show_whitespace_errors || false }-*/;
-
-  public final native boolean
-      syntaxHighlighting() /*-{ return this.syntax_highlighting || false }-*/;
-
-  public final native boolean hideLineNumbers() /*-{ return this.hide_line_numbers || false }-*/;
-
-  public final native boolean matchBrackets() /*-{ return this.match_brackets || false }-*/;
-
-  public final native boolean lineWrapping() /*-{ return this.line_wrapping || false }-*/;
-
-  public final native boolean indentWithTabs() /*-{ return this.indent_with_tabs || false }-*/;
-
-  public final native boolean
-      autoCloseBrackets() /*-{ return this.auto_close_brackets || false }-*/;
-
-  public final native boolean showBase() /*-{ return this.show_base || false }-*/;
-
-  private native int get(String n, int d) /*-{ return this.hasOwnProperty(n) ? this[n] : d }-*/;
-
-  protected EditPreferences() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/EmailInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/EmailInfo.java
deleted file mode 100644
index 9c324be..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/EmailInfo.java
+++ /dev/null
@@ -1,28 +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.client.account;
-
-import com.google.gwt.core.client.JavaScriptObject;
-
-public class EmailInfo extends JavaScriptObject {
-  public final native String email() /*-{ return this.email; }-*/;
-
-  public final native boolean isPreferred() /*-{ return this['preferred'] ? true : false; }-*/;
-
-  public final native boolean
-      isConfirmationPending() /*-{ return this['pending_confirmation'] ? true : false; }-*/;
-
-  protected EmailInfo() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ExternalIdInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ExternalIdInfo.java
deleted file mode 100644
index 4ac0716..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ExternalIdInfo.java
+++ /dev/null
@@ -1,89 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.account;
-
-import com.google.gerrit.client.auth.openid.OpenIdUtil;
-import com.google.gerrit.common.auth.openid.OpenIdUrls;
-import com.google.gerrit.extensions.client.AuthType;
-import com.google.gwt.core.client.JavaScriptObject;
-
-public class ExternalIdInfo extends JavaScriptObject implements Comparable<ExternalIdInfo> {
-  /**
-   * Scheme used for {@link AuthType#LDAP}, {@link AuthType#CLIENT_SSL_CERT_LDAP}, {@link
-   * AuthType#HTTP_LDAP}, and {@link AuthType#LDAP_BIND} usernames.
-   *
-   * <p>The name {@code gerrit:} was a very poor choice.
-   */
-  private static final String SCHEME_GERRIT = "gerrit:";
-
-  /** Scheme used to represent only an email address. */
-  private static final String SCHEME_MAILTO = "mailto:";
-
-  /** Scheme for the username used to authenticate an account, e.g. over SSH. */
-  private static final String SCHEME_USERNAME = "username:";
-
-  public final native String identity() /*-{ return this.identity; }-*/;
-
-  public final native String emailAddress() /*-{ return this.email_address; }-*/;
-
-  public final native boolean isTrusted() /*-{ return this['trusted'] ? true : false; }-*/;
-
-  public final native boolean canDelete() /*-{ return this['can_delete'] ? true : false; }-*/;
-
-  public final boolean isUsername() {
-    return isScheme(SCHEME_USERNAME);
-  }
-
-  public final String describe() {
-    if (isScheme(SCHEME_GERRIT)) {
-      // A local user identity should just be itself.
-      return getSchemeRest();
-    } else if (isScheme(SCHEME_USERNAME)) {
-      // A local user identity should just be itself.
-      return getSchemeRest();
-    } else if (isScheme(SCHEME_MAILTO)) {
-      // Describe a mailto address as just its email address,
-      // which is already shown in the email address field.
-      return "";
-    } else if (isScheme(OpenIdUrls.URL_LAUNCHPAD)) {
-      return OpenIdUtil.C.nameLaunchpad();
-    } else if (isScheme(OpenIdUrls.URL_YAHOO)) {
-      return OpenIdUtil.C.nameYahoo();
-    }
-
-    return identity();
-  }
-
-  @Override
-  public final int compareTo(ExternalIdInfo a) {
-    return emailOf(this).compareTo(emailOf(a));
-  }
-
-  private boolean isScheme(String scheme) {
-    return identity() != null && identity().startsWith(scheme);
-  }
-
-  private String getSchemeRest() {
-    int colonIdx = identity().indexOf(':');
-    String scheme = (colonIdx > 0) ? identity().substring(0, colonIdx) : null;
-    return scheme != null ? identity().substring(scheme.length() + 1) : null;
-  }
-
-  private String emailOf(ExternalIdInfo a) {
-    return a.emailAddress() != null ? a.emailAddress() : "";
-  }
-
-  protected ExternalIdInfo() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java
deleted file mode 100644
index 4592b62..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java
+++ /dev/null
@@ -1,97 +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.client.account;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.info.AgreementInfo;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.client.ui.FancyFlexTable;
-import com.google.gerrit.client.ui.Hyperlink;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.ContributorAgreement;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
-import java.util.List;
-
-public class MyAgreementsScreen extends SettingsScreen {
-  private AgreementTable agreements;
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-
-    agreements = new AgreementTable();
-    add(agreements);
-    add(new Hyperlink(Util.C.newAgreement(), PageLinks.SETTINGS_NEW_AGREEMENT));
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    AccountApi.getAgreements(
-        "self",
-        new ScreenLoadCallback<JsArray<AgreementInfo>>(this) {
-          @Override
-          public void preDisplay(JsArray<AgreementInfo> result) {
-            agreements.display(Natives.asList(result));
-          }
-        });
-  }
-
-  private static class AgreementTable extends FancyFlexTable<ContributorAgreement> {
-    AgreementTable() {
-      table.setWidth("");
-      table.setText(0, 1, Util.C.agreementName());
-      table.setText(0, 2, Util.C.agreementDescription());
-
-      FlexCellFormatter fmt = table.getFlexCellFormatter();
-      for (int c = 1; c < 3; c++) {
-        fmt.addStyleName(0, c, Gerrit.RESOURCES.css().dataHeader());
-      }
-    }
-
-    void display(List<AgreementInfo> result) {
-      while (1 < table.getRowCount()) {
-        table.removeRow(table.getRowCount() - 1);
-      }
-
-      for (AgreementInfo info : result) {
-        addOne(info);
-      }
-    }
-
-    void addOne(AgreementInfo info) {
-      int row = table.getRowCount();
-      table.insertRow(row);
-      applyDataRowStyle(row);
-
-      String url = info.url();
-      if (url != null && url.length() > 0) {
-        Anchor a = new Anchor(info.name(), url);
-        a.setTarget("_blank");
-        table.setWidget(row, 1, a);
-      } else {
-        table.setText(row, 1, info.name());
-      }
-      table.setText(row, 2, info.description());
-      FlexCellFormatter fmt = table.getFlexCellFormatter();
-      for (int c = 1; c < 3; c++) {
-        fmt.addStyleName(row, c, Gerrit.RESOURCES.css().dataCell());
-      }
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyContactInformationScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyContactInformationScreen.java
deleted file mode 100644
index d5884f4..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyContactInformationScreen.java
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT 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.client.account;
-
-public class MyContactInformationScreen extends SettingsScreen {
-  private ContactPanelShort panel;
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    panel =
-        new ContactPanelShort() {
-          @Override
-          void display() {
-            MyContactInformationScreen.this.display();
-          }
-        };
-    add(panel);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyDiffPreferencesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyDiffPreferencesScreen.java
deleted file mode 100644
index a721441..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyDiffPreferencesScreen.java
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.account;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.diff.PreferencesBox;
-import com.google.gwt.user.client.ui.FlowPanel;
-
-public class MyDiffPreferencesScreen extends SettingsScreen {
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-
-    PreferencesBox pb = new PreferencesBox(null);
-    pb.set(DiffPreferences.create(Gerrit.getDiffPreferences()));
-    FlowPanel p = new FlowPanel();
-    p.setStyleName(pb.getStyle().dialog());
-    p.add(pb);
-    add(p);
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    display();
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyEditPreferencesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyEditPreferencesScreen.java
deleted file mode 100644
index 424b5d5..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyEditPreferencesScreen.java
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.account;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.editor.EditPreferencesBox;
-import com.google.gwt.user.client.ui.FlowPanel;
-
-public class MyEditPreferencesScreen extends SettingsScreen {
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-
-    EditPreferencesBox pb = new EditPreferencesBox(null);
-    pb.set(EditPreferences.create(Gerrit.getEditPreferences()));
-    FlowPanel p = new FlowPanel();
-    p.setStyleName(pb.getStyle().dialog());
-    p.add(pb);
-    add(p);
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    display();
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.java
deleted file mode 100644
index 1a0090a..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.java
+++ /dev/null
@@ -1,289 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.account;
-
-import static java.util.Comparator.comparing;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.info.GpgKeyInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.ui.FancyFlexTable;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.logical.shared.ValueChangeHandler;
-import com.google.gwt.http.client.Response;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.uibinder.client.UiHandler;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.rpc.StatusCodeException;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwt.user.client.ui.InlineLabel;
-import com.google.gwt.user.client.ui.Label;
-import com.google.gwt.user.client.ui.VerticalPanel;
-import com.google.gwtexpui.clippy.client.CopyableLabel;
-import com.google.gwtexpui.globalkey.client.NpTextArea;
-import java.util.ArrayList;
-import java.util.List;
-
-public class MyGpgKeysScreen extends SettingsScreen {
-  interface Binder extends UiBinder<HTMLPanel, MyGpgKeysScreen> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  @UiField(provided = true)
-  GpgKeyTable keys;
-
-  @UiField Button deleteKey;
-  @UiField Button addKey;
-
-  @UiField VerticalPanel addKeyBlock;
-  @UiField NpTextArea keyText;
-
-  @UiField VerticalPanel errorPanel;
-  @UiField Label errorText;
-
-  @UiField Button clearButton;
-  @UiField Button addButton;
-  @UiField Button closeButton;
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    keys = new GpgKeyTable();
-    add(uiBinder.createAndBindUi(this));
-    keys.updateDeleteButton();
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    refreshKeys();
-  }
-
-  @UiHandler("deleteKey")
-  void onDeleteKey(@SuppressWarnings("unused") ClickEvent e) {
-    keys.deleteChecked();
-  }
-
-  @UiHandler("addKey")
-  void onAddKey(@SuppressWarnings("unused") ClickEvent e) {
-    showAddKeyBlock(true);
-  }
-
-  @UiHandler("clearButton")
-  void onClearButton(@SuppressWarnings("unused") ClickEvent e) {
-    keyText.setText("");
-    keyText.setFocus(true);
-    errorPanel.setVisible(false);
-  }
-
-  @UiHandler("closeButton")
-  void onCloseButton(@SuppressWarnings("unused") ClickEvent e) {
-    showAddKeyBlock(false);
-  }
-
-  @UiHandler("addButton")
-  void onAddButton(@SuppressWarnings("unused") ClickEvent e) {
-    doAddKey();
-  }
-
-  private void refreshKeys() {
-    AccountApi.self()
-        .view("gpgkeys")
-        .get(
-            NativeMap.copyKeysIntoChildren(
-                "id",
-                new GerritCallback<NativeMap<GpgKeyInfo>>() {
-                  @Override
-                  public void onSuccess(NativeMap<GpgKeyInfo> result) {
-                    List<GpgKeyInfo> list = Natives.asList(result.values());
-                    // TODO(dborowitz): Sort on something more meaningful, like
-                    // created date?
-                    list.sort(comparing(GpgKeyInfo::id));
-                    keys.clear();
-                    keyText.setText("");
-                    errorPanel.setVisible(false);
-                    addButton.setEnabled(true);
-                    if (!list.isEmpty()) {
-                      keys.setVisible(true);
-                      for (GpgKeyInfo k : list) {
-                        keys.addOneKey(k);
-                      }
-                      showKeyTable(true);
-                      showAddKeyBlock(false);
-                    } else {
-                      keys.setVisible(false);
-                      showAddKeyBlock(true);
-                      showKeyTable(false);
-                    }
-
-                    display();
-                  }
-                }));
-  }
-
-  private void showAddKeyBlock(boolean show) {
-    addKey.setVisible(!show);
-    addKeyBlock.setVisible(show);
-  }
-
-  private void showKeyTable(boolean show) {
-    keys.setVisible(show);
-    deleteKey.setVisible(show);
-    addKey.setVisible(show);
-  }
-
-  private void doAddKey() {
-    if (keyText.getText().isEmpty()) {
-      return;
-    }
-    addButton.setEnabled(false);
-    keyText.setEnabled(false);
-    AccountApi.addGpgKey(
-        "self",
-        keyText.getText(),
-        new AsyncCallback<NativeMap<GpgKeyInfo>>() {
-          @Override
-          public void onSuccess(NativeMap<GpgKeyInfo> result) {
-            keyText.setEnabled(true);
-            refreshKeys();
-          }
-
-          @Override
-          public void onFailure(Throwable caught) {
-            keyText.setEnabled(true);
-            addButton.setEnabled(true);
-            if (caught instanceof StatusCodeException) {
-              StatusCodeException sce = (StatusCodeException) caught;
-              if (sce.getStatusCode() == Response.SC_CONFLICT
-                  || sce.getStatusCode() == Response.SC_BAD_REQUEST) {
-                errorText.setText(sce.getEncodedResponse());
-              } else {
-                errorText.setText(sce.getMessage());
-              }
-            } else {
-              errorText.setText("Unexpected error saving key: " + caught.getMessage());
-            }
-            errorPanel.setVisible(true);
-          }
-        });
-  }
-
-  private class GpgKeyTable extends FancyFlexTable<GpgKeyInfo> {
-    private final ValueChangeHandler<Boolean> updateDeleteHandler;
-
-    GpgKeyTable() {
-      table.setWidth("");
-      table.setText(0, 1, Util.C.gpgKeyId());
-      table.setText(0, 2, Util.C.gpgKeyFingerprint());
-      table.setText(0, 3, Util.C.gpgKeyUserIds());
-
-      FlexCellFormatter fmt = table.getFlexCellFormatter();
-      fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().iconHeader());
-      fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().dataHeader());
-      fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
-      fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader());
-
-      updateDeleteHandler =
-          new ValueChangeHandler<Boolean>() {
-            @Override
-            public void onValueChange(ValueChangeEvent<Boolean> event) {
-              updateDeleteButton();
-            }
-          };
-    }
-
-    private void addOneKey(GpgKeyInfo k) {
-      int row = table.getRowCount();
-      table.insertRow(row);
-      applyDataRowStyle(row);
-
-      CheckBox sel = new CheckBox();
-      sel.addValueChangeHandler(updateDeleteHandler);
-      table.setWidget(row, 0, sel);
-      table.setWidget(row, 1, new CopyableLabel(k.id()));
-      table.setText(row, 2, k.fingerprint());
-
-      VerticalPanel userIds = new VerticalPanel();
-      for (int i = 0; i < k.userIds().length(); i++) {
-        userIds.add(new InlineLabel(k.userIds().get(i)));
-      }
-      table.setWidget(row, 3, userIds);
-
-      FlexCellFormatter fmt = table.getFlexCellFormatter();
-      fmt.addStyleName(row, 0, Gerrit.RESOURCES.css().iconCell());
-      fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().dataCell());
-      fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell());
-      fmt.addStyleName(row, 3, Gerrit.RESOURCES.css().dataCell());
-
-      setRowItem(row, k);
-    }
-
-    private void updateDeleteButton() {
-      for (int row = 1; row < table.getRowCount(); row++) {
-        if (isChecked(row)) {
-          deleteKey.setEnabled(true);
-          return;
-        }
-      }
-      deleteKey.setEnabled(false);
-    }
-
-    private void deleteChecked() {
-      deleteKey.setEnabled(false);
-      List<String> toDelete = new ArrayList<>(table.getRowCount());
-      for (int row = 1; row < table.getRowCount(); row++) {
-        if (isChecked(row)) {
-          toDelete.add(getRowItem(row).fingerprint());
-        }
-      }
-      AccountApi.deleteGpgKeys(
-          "self",
-          toDelete,
-          new GerritCallback<NativeMap<GpgKeyInfo>>() {
-            @Override
-            public void onSuccess(NativeMap<GpgKeyInfo> result) {
-              refreshKeys();
-            }
-
-            @Override
-            public void onFailure(Throwable caught) {
-              deleteKey.setEnabled(true);
-              super.onFailure(caught);
-            }
-          });
-    }
-
-    private boolean isChecked(int row) {
-      return ((CheckBox) table.getWidget(row, 0)).getValue();
-    }
-
-    private void clear() {
-      while (table.getRowCount() > 1) {
-        table.removeRow(1);
-      }
-      for (int i = table.getRowCount() - 1; i >= 1; i++) {
-        table.removeRow(i);
-      }
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.ui.xml
deleted file mode 100644
index dc73736..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.ui.xml
+++ /dev/null
@@ -1,94 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'
-    xmlns:expui='urn:import:com.google.gwtexpui.globalkey.client'>
-  <ui:with field='res' type='com.google.gerrit.client.GerritResources'/>
-
-  <ui:style gss='false'>
-    .errorHeader {
-      font-weight: bold;
-    }
-    .errorText {
-      white-space: pre-wrap;
-      padding-bottom: 6px;
-    }
-  </ui:style>
-
-  <g:HTMLPanel>
-    <g:Widget ui:field='keys' addStyleNames='{res.css.sshKeyTable}'/>
-    <g:FlowPanel>
-      <g:Button ui:field='deleteKey'>
-        <div><ui:msg>Delete</ui:msg></div>
-      </g:Button>
-      <g:Button ui:field='addKey'>
-        <div><ui:msg>Add Key ...</ui:msg></div>
-      </g:Button>
-    </g:FlowPanel>
-    <g:VerticalPanel ui:field='addKeyBlock'
-        styleName='{res.css.addSshKeyPanel}'
-        visible='false'>
-      <g:Label>Add GPG Public Key</g:Label>
-      <g:DisclosurePanel>
-        <g:header>How to generate a GPG key</g:header>
-        <g:HTMLPanel>
-          <ol>
-            <li>
-              From the Terminal or Git Bash, run <em>gpg --gen-key</em> and
-              follow the prompts to create the key.
-            </li>
-            <li>
-              Use the default kind. Use the default (or higher) keysize. Choose
-              any value for your expiration.
-            </li>
-            <li>
-              The user ID should contain one of your registered email addresses.
-            </li>
-            <li>Setting a passphrase is strongly recommended.</li>
-            <li>Note the ID of your new key.</li>
-            <li>
-              To export your key, run the following and paste the full output
-              into the text box:
-              <br/>
-              <code>gpg --export -a &lt;key ID&gt;</code>
-            </li>
-          </ol>
-        </g:HTMLPanel>
-      </g:DisclosurePanel>
-      <expui:NpTextArea
-          visibleLines='12'
-          characterWidth='80'
-          spellCheck='false'
-          ui:field='keyText'/>
-      <g:VerticalPanel ui:field='errorPanel' visible='false'>
-        <g:Label styleName='{style.errorHeader}'>Error adding GPG key:</g:Label>
-        <g:Label styleName='{style.errorText}' ui:field='errorText'/>
-      </g:VerticalPanel>
-      <g:FlowPanel>
-        <g:Button ui:field='clearButton'>
-          <div><ui:msg>Clear</ui:msg></div>
-        </g:Button>
-        <g:Button ui:field='addButton'>
-          <div><ui:msg>Add</ui:msg></div>
-        </g:Button>
-        <g:Button ui:field='closeButton'>
-          <div><ui:msg>Close</ui:msg></div>
-        </g:Button>
-      </g:FlowPanel>
-    </g:VerticalPanel>
-  </g:HTMLPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsScreen.java
deleted file mode 100644
index e9112de..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsScreen.java
+++ /dev/null
@@ -1,43 +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.client.account;
-
-import com.google.gerrit.client.admin.GroupTable;
-import com.google.gerrit.client.groups.GroupList;
-import com.google.gerrit.client.rpc.ScreenLoadCallback;
-
-public class MyGroupsScreen extends SettingsScreen {
-  private GroupTable groups;
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    groups = new GroupTable();
-    add(groups);
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    GroupList.my(
-        new ScreenLoadCallback<GroupList>(this) {
-          @Override
-          protected void preDisplay(GroupList result) {
-            groups.display(result);
-            groups.finishDisplay();
-          }
-        });
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java
deleted file mode 100644
index 730d98e..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java
+++ /dev/null
@@ -1,224 +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.client.account;
-
-import static java.util.Comparator.naturalOrder;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.VoidResult;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.ui.FancyFlexTable;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.logical.shared.ValueChangeHandler;
-import com.google.gwt.user.client.History;
-import com.google.gwt.user.client.Window.Location;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
-import java.util.HashSet;
-import java.util.List;
-
-public class MyIdentitiesScreen extends SettingsScreen {
-  private IdTable identites;
-  private Button deleteIdentity;
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-
-    identites = new IdTable();
-    add(identites);
-
-    deleteIdentity = new Button(Util.C.buttonDeleteIdentity());
-    deleteIdentity.setEnabled(false);
-    deleteIdentity.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            identites.deleteChecked();
-          }
-        });
-    add(deleteIdentity);
-
-    if (Gerrit.info().auth().isOpenId() || Gerrit.info().auth().isOAuth()) {
-      Button linkIdentity = new Button(Util.C.buttonLinkIdentity());
-      linkIdentity.addClickHandler(
-          new ClickHandler() {
-            @Override
-            public void onClick(ClickEvent event) {
-              Location.assign(Gerrit.loginRedirect(History.getToken()) + "?link");
-            }
-          });
-      add(linkIdentity);
-    }
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    AccountApi.getExternalIds(
-        new GerritCallback<JsArray<ExternalIdInfo>>() {
-          @Override
-          public void onSuccess(JsArray<ExternalIdInfo> results) {
-            identites.display(results);
-            display();
-          }
-        });
-  }
-
-  private class IdTable extends FancyFlexTable<ExternalIdInfo> {
-    private ValueChangeHandler<Boolean> updateDeleteHandler;
-
-    IdTable() {
-      table.setWidth("");
-      table.setText(0, 2, Util.C.webIdStatus());
-      table.setText(0, 3, Util.C.webIdEmail());
-      table.setText(0, 4, Util.C.webIdIdentity());
-
-      final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().iconHeader());
-      fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
-      fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader());
-      fmt.addStyleName(0, 4, Gerrit.RESOURCES.css().dataHeader());
-
-      updateDeleteHandler =
-          new ValueChangeHandler<Boolean>() {
-            @Override
-            public void onValueChange(ValueChangeEvent<Boolean> event) {
-              updateDeleteButton();
-            }
-          };
-    }
-
-    void deleteChecked() {
-      final HashSet<String> keys = new HashSet<>();
-      for (int row = 1; row < table.getRowCount(); row++) {
-        final ExternalIdInfo k = getRowItem(row);
-        if (k == null) {
-          continue;
-        }
-        final CheckBox cb = (CheckBox) table.getWidget(row, 1);
-        if (cb == null) {
-          continue;
-        }
-        if (cb.getValue()) {
-          keys.add(k.identity());
-        }
-      }
-      if (keys.isEmpty()) {
-        updateDeleteButton();
-      } else {
-        deleteIdentity.setEnabled(false);
-        AccountApi.deleteExternalIds(
-            keys,
-            new GerritCallback<VoidResult>() {
-              @Override
-              public void onSuccess(VoidResult result) {
-                for (int row = 1; row < table.getRowCount(); ) {
-                  final ExternalIdInfo k = getRowItem(row);
-                  if (k != null && keys.contains(k.identity())) {
-                    table.removeRow(row);
-                  } else {
-                    row++;
-                  }
-                }
-                updateDeleteButton();
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {
-                updateDeleteButton();
-                super.onFailure(caught);
-              }
-            });
-      }
-    }
-
-    void updateDeleteButton() {
-      int off = 0;
-      boolean on = false;
-      for (int row = 1; row < table.getRowCount(); row++) {
-        if (table.getWidget(row, 1) == null) {
-          off++;
-        } else {
-          CheckBox sel = (CheckBox) table.getWidget(row, 1);
-          if (sel.getValue()) {
-            on = true;
-            break;
-          }
-        }
-      }
-      deleteIdentity.setVisible(off < table.getRowCount() - 1);
-      deleteIdentity.setEnabled(on);
-    }
-
-    void display(JsArray<ExternalIdInfo> results) {
-      List<ExternalIdInfo> idList = Natives.asList(results);
-      idList.sort(naturalOrder());
-
-      while (1 < table.getRowCount()) {
-        table.removeRow(table.getRowCount() - 1);
-      }
-
-      for (ExternalIdInfo k : idList) {
-        addOneId(k);
-      }
-      updateDeleteButton();
-    }
-
-    void addOneId(ExternalIdInfo k) {
-      if (k.isUsername()) {
-        // Don't display the username as an identity here.
-        return;
-      }
-
-      final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      final int row = table.getRowCount();
-      table.insertRow(row);
-      applyDataRowStyle(row);
-
-      if (k.canDelete()) {
-        final CheckBox sel = new CheckBox();
-        sel.addValueChangeHandler(updateDeleteHandler);
-        table.setWidget(row, 1, sel);
-      } else {
-        table.setText(row, 1, "");
-      }
-      if (k.isTrusted()) {
-        table.setText(row, 2, "");
-      } else {
-        table.setText(row, 2, Util.C.untrustedProvider());
-        fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().identityUntrustedExternalId());
-      }
-      if (k.emailAddress() != null && k.emailAddress().length() > 0) {
-        table.setText(row, 3, k.emailAddress());
-      } else {
-        table.setText(row, 3, "");
-      }
-      table.setText(row, 4, k.describe());
-
-      fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().iconCell());
-      fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell());
-      fmt.addStyleName(row, 3, Gerrit.RESOURCES.css().dataCell());
-      fmt.addStyleName(row, 4, Gerrit.RESOURCES.css().dataCell());
-
-      setRowItem(row, k);
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyOAuthTokenScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyOAuthTokenScreen.java
deleted file mode 100644
index 173dba6..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyOAuthTokenScreen.java
+++ /dev/null
@@ -1,198 +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.client.account;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.info.GeneralPreferences;
-import com.google.gerrit.client.info.OAuthTokenInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gwt.i18n.client.DateTimeFormat;
-import com.google.gwt.i18n.client.LocaleInfo;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
-import com.google.gwt.user.client.ui.Label;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtexpui.clippy.client.CopyableLabel;
-import java.util.Date;
-
-public class MyOAuthTokenScreen extends SettingsScreen {
-  private CopyableLabel tokenLabel;
-  private Label expiresLabel;
-  private Label expiredNote;
-  private CopyableLabel netrcValue;
-  private CopyableLabel cookieValue;
-  private FlowPanel flow;
-  private Grid grid;
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-
-    tokenLabel = new CopyableLabel("");
-    tokenLabel.addStyleName(Gerrit.RESOURCES.css().oauthToken());
-
-    expiresLabel = new Label("");
-    expiresLabel.addStyleName(Gerrit.RESOURCES.css().oauthExpires());
-
-    grid = new Grid(2, 2);
-    grid.setStyleName(Gerrit.RESOURCES.css().infoBlock());
-    grid.addStyleName(Gerrit.RESOURCES.css().oauthInfoBlock());
-    add(grid);
-
-    expiredNote = new Label(Util.C.labelOAuthExpired());
-    expiredNote.setVisible(false);
-    add(expiredNote);
-
-    row(grid, 0, Util.C.labelOAuthToken(), tokenLabel);
-    row(grid, 1, Util.C.labelOAuthExpires(), expiresLabel);
-
-    CellFormatter fmt = grid.getCellFormatter();
-    fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().topmost());
-    fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().topmost());
-    fmt.addStyleName(1, 0, Gerrit.RESOURCES.css().bottomheader());
-
-    flow = new FlowPanel();
-    flow.setStyleName(Gerrit.RESOURCES.css().oauthPanel());
-    add(flow);
-
-    Label netrcLabel = new Label(Util.C.labelOAuthNetRCEntry());
-    netrcLabel.setStyleName(Gerrit.RESOURCES.css().oauthPanelNetRCHeading());
-    flow.add(netrcLabel);
-    netrcValue = new CopyableLabel("");
-    netrcValue.setStyleName(Gerrit.RESOURCES.css().oauthPanelNetRCEntry());
-    flow.add(netrcValue);
-
-    Label cookieLabel = new Label(Util.C.labelOAuthGitCookie());
-    cookieLabel.setStyleName(Gerrit.RESOURCES.css().oauthPanelCookieHeading());
-    flow.add(cookieLabel);
-    cookieValue = new CopyableLabel("");
-    cookieValue.setStyleName(Gerrit.RESOURCES.css().oauthPanelCookieEntry());
-    flow.add(cookieValue);
-  }
-
-  private void row(Grid grid, int row, String name, Widget field) {
-    final CellFormatter fmt = grid.getCellFormatter();
-    if (LocaleInfo.getCurrentLocale().isRTL()) {
-      grid.setText(row, 1, name);
-      grid.setWidget(row, 0, field);
-      fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().header());
-    } else {
-      grid.setText(row, 0, name);
-      grid.setWidget(row, 1, field);
-      fmt.addStyleName(row, 0, Gerrit.RESOURCES.css().header());
-    }
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    AccountApi.self()
-        .view("preferences")
-        .get(
-            new ScreenLoadCallback<GeneralPreferences>(this) {
-              @Override
-              protected void preDisplay(GeneralPreferences prefs) {
-                display(prefs);
-              }
-            });
-  }
-
-  private void display(GeneralPreferences prefs) {
-    AccountApi.self()
-        .view("oauthtoken")
-        .get(
-            new GerritCallback<OAuthTokenInfo>() {
-              @Override
-              public void onSuccess(OAuthTokenInfo tokenInfo) {
-                tokenLabel.setText(tokenInfo.accessToken());
-                expiresLabel.setText(getExpiresAt(tokenInfo, prefs));
-                netrcValue.setText(getNetRC(tokenInfo));
-                cookieValue.setText(getCookie(tokenInfo));
-                flow.setVisible(true);
-                expiredNote.setVisible(false);
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {
-                if (isNoSuchEntity(caught) || isSigninFailure(caught)) {
-                  tokenLabel.setText("");
-                  expiresLabel.setText("");
-                  netrcValue.setText("");
-                  cookieValue.setText("");
-                  flow.setVisible(false);
-                  expiredNote.setVisible(true);
-                } else {
-                  showFailure(caught);
-                }
-              }
-            });
-  }
-
-  private static long getExpiresAt(OAuthTokenInfo tokenInfo) {
-    if (tokenInfo.expiresAt() == null) {
-      return Long.MAX_VALUE;
-    }
-    long expiresAt;
-    try {
-      expiresAt = Long.parseLong(tokenInfo.expiresAt());
-    } catch (NumberFormatException e) {
-      return Long.MAX_VALUE;
-    }
-    return expiresAt;
-  }
-
-  private static long getExpiresAtSeconds(OAuthTokenInfo tokenInfo) {
-    return getExpiresAt(tokenInfo) / 1000L;
-  }
-
-  private static String getExpiresAt(OAuthTokenInfo tokenInfo, GeneralPreferences prefs) {
-    long expiresAt = getExpiresAt(tokenInfo);
-    if (expiresAt == Long.MAX_VALUE) {
-      return "";
-    }
-    String dateFormat = prefs.dateFormat().getLongFormat();
-    String timeFormat = prefs.timeFormat().getFormat();
-    DateTimeFormat formatter = DateTimeFormat.getFormat(dateFormat + " " + timeFormat);
-    return formatter.format(new Date(expiresAt));
-  }
-
-  private static String getNetRC(OAuthTokenInfo accessTokenInfo) {
-    StringBuilder sb = new StringBuilder();
-    sb.append("machine ");
-    sb.append(accessTokenInfo.resourceHost());
-    sb.append(" login ");
-    sb.append(accessTokenInfo.username());
-    sb.append(" password ");
-    sb.append(accessTokenInfo.accessToken());
-    return sb.toString();
-  }
-
-  private static String getCookie(OAuthTokenInfo accessTokenInfo) {
-    StringBuilder sb = new StringBuilder();
-    sb.append(accessTokenInfo.resourceHost());
-    sb.append("\tFALSE\t/\tTRUE\t");
-    sb.append(getExpiresAtSeconds(accessTokenInfo));
-    sb.append("\tgit-");
-    sb.append(accessTokenInfo.username());
-    sb.append('\t');
-    sb.append(accessTokenInfo.accessToken());
-    if (accessTokenInfo.providerId() != null) {
-      sb.append('@').append(accessTokenInfo.providerId());
-    }
-    return sb.toString();
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPasswordScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPasswordScreen.java
deleted file mode 100644
index 5dd7530..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPasswordScreen.java
+++ /dev/null
@@ -1,161 +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.client.account;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.GerritUiExtensionPoint;
-import com.google.gerrit.client.api.ExtensionPanel;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.NativeString;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.i18n.client.LocaleInfo;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtexpui.clippy.client.CopyableLabel;
-
-public class MyPasswordScreen extends SettingsScreen {
-  private CopyableLabel password;
-  private Button generatePassword;
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-
-    String url = Gerrit.info().auth().httpPasswordUrl();
-    if (url != null) {
-      Anchor link = new Anchor();
-      link.setText(Util.C.linkObtainPassword());
-      link.setHref(url);
-      link.setTarget("_blank");
-      add(link);
-      return;
-    }
-
-    password = new CopyableLabel(Util.C.revokePassword());
-    password.addStyleName(Gerrit.RESOURCES.css().accountPassword());
-
-    generatePassword = new Button(Util.C.buttonGeneratePassword());
-    generatePassword.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            doGeneratePassword();
-          }
-        });
-
-    final Grid userInfo = new Grid(2, 2);
-    final CellFormatter fmt = userInfo.getCellFormatter();
-    userInfo.setStyleName(Gerrit.RESOURCES.css().infoBlock());
-    userInfo.addStyleName(Gerrit.RESOURCES.css().accountInfoBlock());
-    add(userInfo);
-
-    row(userInfo, 0, Util.C.userName(), new UsernameField());
-    row(userInfo, 1, Util.C.password(), password);
-
-    fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().topmost());
-    fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().topmost());
-    fmt.addStyleName(1, 0, Gerrit.RESOURCES.css().bottomheader());
-
-    final FlowPanel buttons = new FlowPanel();
-    buttons.add(generatePassword);
-    add(buttons);
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    ExtensionPanel extensionPanel =
-        createExtensionPoint(GerritUiExtensionPoint.PASSWORD_SCREEN_BOTTOM);
-    extensionPanel.addStyleName(Gerrit.RESOURCES.css().extensionPanel());
-    add(extensionPanel);
-
-    if (password == null) {
-      display();
-      return;
-    }
-
-    enableUI(false);
-    AccountApi.getUsername(
-        "self",
-        new GerritCallback<NativeString>() {
-          @Override
-          public void onSuccess(NativeString user) {
-            Gerrit.getUserAccount().username(user.asString());
-            enableUI(true);
-            display();
-          }
-
-          @Override
-          public void onFailure(Throwable caught) {
-            if (RestApi.isNotFound(caught)) {
-              Gerrit.getUserAccount().username(null);
-              display();
-            } else {
-              super.onFailure(caught);
-            }
-          }
-        });
-  }
-
-  private void display(String pass) {
-    password.setText(pass != null ? pass : "");
-    password.setVisible(pass != null);
-    enableUI(true);
-  }
-
-  private void row(Grid info, int row, String name, Widget field) {
-    final CellFormatter fmt = info.getCellFormatter();
-    if (LocaleInfo.getCurrentLocale().isRTL()) {
-      info.setText(row, 1, name);
-      info.setWidget(row, 0, field);
-      fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().header());
-    } else {
-      info.setText(row, 0, name);
-      info.setWidget(row, 1, field);
-      fmt.addStyleName(row, 0, Gerrit.RESOURCES.css().header());
-    }
-  }
-
-  private void doGeneratePassword() {
-    if (Gerrit.getUserAccount().username() != null) {
-      enableUI(false);
-      AccountApi.generateHttpPassword(
-          "self",
-          new GerritCallback<NativeString>() {
-            @Override
-            public void onSuccess(NativeString newPassword) {
-              display(newPassword.asString());
-            }
-
-            @Override
-            public void onFailure(Throwable caught) {
-              enableUI(true);
-            }
-          });
-    }
-  }
-
-  private void enableUI(boolean on) {
-    on &= Gerrit.getUserAccount().username() != null;
-
-    generatePassword.setEnabled(on);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
deleted file mode 100644
index afb8718..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
+++ /dev/null
@@ -1,515 +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.client.account;
-
-import static com.google.gerrit.extensions.client.GeneralPreferencesInfo.DEFAULT_PAGESIZE;
-import static com.google.gerrit.extensions.client.GeneralPreferencesInfo.PAGESIZE_CHOICES;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.GerritUiExtensionPoint;
-import com.google.gerrit.client.StringListPanel;
-import com.google.gerrit.client.api.ExtensionPanel;
-import com.google.gerrit.client.config.ConfigServerApi;
-import com.google.gerrit.client.info.GeneralPreferences;
-import com.google.gerrit.client.info.TopMenuItem;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.client.ui.OnEditEnabler;
-import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
-import com.google.gerrit.extensions.client.GeneralPreferencesInfo.ReviewCategoryStrategy;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.i18n.client.DateTimeFormat;
-import com.google.gwt.i18n.client.LocaleInfo;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.ListBox;
-import com.google.gwtexpui.user.client.UserAgent;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Date;
-import java.util.List;
-
-public class MyPreferencesScreen extends SettingsScreen {
-  private CheckBox showSiteHeader;
-  private CheckBox useFlashClipboard;
-  private CheckBox highlightAssigneeInChangeTable;
-  private CheckBox relativeDateInChangeTable;
-  private CheckBox sizeBarInChangeTable;
-  private CheckBox legacycidInChangeTable;
-  private CheckBox muteCommonPathPrefixes;
-  private CheckBox signedOffBy;
-  private CheckBox publishCommentsOnPush;
-  private CheckBox workInProgressByDefault;
-  private ListBox maximumPageSize;
-  private ListBox dateFormat;
-  private ListBox timeFormat;
-  private ListBox reviewCategoryStrategy;
-  private ListBox diffView;
-  private ListBox emailStrategy;
-  private ListBox emailFormat;
-  private ListBox defaultBaseForMerges;
-  private StringListPanel myMenus;
-  private Button save;
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-
-    showSiteHeader = new CheckBox(Util.C.showSiteHeader());
-    useFlashClipboard = new CheckBox(Util.C.useFlashClipboard());
-    maximumPageSize = new ListBox();
-    for (int v : PAGESIZE_CHOICES) {
-      maximumPageSize.addItem(Util.M.rowsPerPage(v), String.valueOf(v));
-    }
-
-    reviewCategoryStrategy = new ListBox();
-    reviewCategoryStrategy.addItem(
-        Util.C.messageShowInReviewCategoryNone(),
-        GeneralPreferencesInfo.ReviewCategoryStrategy.NONE.name());
-    reviewCategoryStrategy.addItem(
-        Util.C.messageShowInReviewCategoryName(),
-        GeneralPreferencesInfo.ReviewCategoryStrategy.NAME.name());
-    reviewCategoryStrategy.addItem(
-        Util.C.messageShowInReviewCategoryEmail(),
-        GeneralPreferencesInfo.ReviewCategoryStrategy.EMAIL.name());
-    reviewCategoryStrategy.addItem(
-        Util.C.messageShowInReviewCategoryUsername(),
-        GeneralPreferencesInfo.ReviewCategoryStrategy.USERNAME.name());
-    reviewCategoryStrategy.addItem(
-        Util.C.messageShowInReviewCategoryAbbrev(),
-        GeneralPreferencesInfo.ReviewCategoryStrategy.ABBREV.name());
-
-    emailStrategy = new ListBox();
-    emailStrategy.addItem(
-        Util.C.messageCCMeOnMyComments(),
-        GeneralPreferencesInfo.EmailStrategy.CC_ON_OWN_COMMENTS.name());
-    emailStrategy.addItem(
-        Util.C.messageEnabled(), GeneralPreferencesInfo.EmailStrategy.ENABLED.name());
-    emailStrategy.addItem(
-        Util.C.messageDisabled(), GeneralPreferencesInfo.EmailStrategy.DISABLED.name());
-
-    emailFormat = new ListBox();
-    emailFormat.addItem(
-        Util.C.messagePlaintextOnly(), GeneralPreferencesInfo.EmailFormat.PLAINTEXT.name());
-    emailFormat.addItem(
-        Util.C.messageHtmlPlaintext(), GeneralPreferencesInfo.EmailFormat.HTML_PLAINTEXT.name());
-
-    defaultBaseForMerges = new ListBox();
-    defaultBaseForMerges.addItem(
-        Util.C.autoMerge(), GeneralPreferencesInfo.DefaultBase.AUTO_MERGE.name());
-    defaultBaseForMerges.addItem(
-        Util.C.firstParent(), GeneralPreferencesInfo.DefaultBase.FIRST_PARENT.name());
-
-    diffView = new ListBox();
-    diffView.addItem(
-        com.google.gerrit.client.changes.Util.C.sideBySide(),
-        GeneralPreferencesInfo.DiffView.SIDE_BY_SIDE.name());
-    diffView.addItem(
-        com.google.gerrit.client.changes.Util.C.unifiedDiff(),
-        GeneralPreferencesInfo.DiffView.UNIFIED_DIFF.name());
-
-    Date now = new Date();
-    dateFormat = new ListBox();
-    for (GeneralPreferencesInfo.DateFormat fmt : GeneralPreferencesInfo.DateFormat.values()) {
-      StringBuilder r = new StringBuilder();
-      r.append(DateTimeFormat.getFormat(fmt.getShortFormat()).format(now));
-      r.append(" ; ");
-      r.append(DateTimeFormat.getFormat(fmt.getLongFormat()).format(now));
-      dateFormat.addItem(r.toString(), fmt.name());
-    }
-
-    timeFormat = new ListBox();
-    for (GeneralPreferencesInfo.TimeFormat fmt : GeneralPreferencesInfo.TimeFormat.values()) {
-      StringBuilder r = new StringBuilder();
-      r.append(DateTimeFormat.getFormat(fmt.getFormat()).format(now));
-      timeFormat.addItem(r.toString(), fmt.name());
-    }
-
-    FlowPanel dateTimePanel = new FlowPanel();
-
-    final int labelIdx;
-    final int fieldIdx;
-    if (LocaleInfo.getCurrentLocale().isRTL()) {
-      labelIdx = 1;
-      fieldIdx = 0;
-      dateTimePanel.add(timeFormat);
-      dateTimePanel.add(dateFormat);
-    } else {
-      labelIdx = 0;
-      fieldIdx = 1;
-      dateTimePanel.add(dateFormat);
-      dateTimePanel.add(timeFormat);
-    }
-    highlightAssigneeInChangeTable = new CheckBox(Util.C.highlightAssigneeInChangeTable());
-    relativeDateInChangeTable = new CheckBox(Util.C.showRelativeDateInChangeTable());
-    sizeBarInChangeTable = new CheckBox(Util.C.showSizeBarInChangeTable());
-    legacycidInChangeTable = new CheckBox(Util.C.showLegacycidInChangeTable());
-    muteCommonPathPrefixes = new CheckBox(Util.C.muteCommonPathPrefixes());
-    signedOffBy = new CheckBox(Util.C.signedOffBy());
-    publishCommentsOnPush = new CheckBox(Util.C.publishCommentsOnPush());
-    workInProgressByDefault = new CheckBox(Util.C.workInProgressByDefault());
-
-    boolean flashClippy = !UserAgent.hasJavaScriptClipboard() && UserAgent.Flash.isInstalled();
-    final Grid formGrid = new Grid(16 + (flashClippy ? 1 : 0), 2);
-
-    int row = 0;
-
-    formGrid.setText(row, labelIdx, Util.C.reviewCategoryLabel());
-    formGrid.setWidget(row, fieldIdx, reviewCategoryStrategy);
-    row++;
-
-    formGrid.setText(row, labelIdx, Util.C.maximumPageSizeFieldLabel());
-    formGrid.setWidget(row, fieldIdx, maximumPageSize);
-    row++;
-
-    formGrid.setText(row, labelIdx, Util.C.dateFormatLabel());
-    formGrid.setWidget(row, fieldIdx, dateTimePanel);
-    row++;
-
-    formGrid.setText(row, labelIdx, Util.C.emailFieldLabel());
-    formGrid.setWidget(row, fieldIdx, emailStrategy);
-    row++;
-
-    formGrid.setText(row, labelIdx, Util.C.emailFormatFieldLabel());
-    formGrid.setWidget(row, fieldIdx, emailFormat);
-    row++;
-
-    formGrid.setText(row, labelIdx, Util.C.defaultBaseForMerges());
-    formGrid.setWidget(row, fieldIdx, defaultBaseForMerges);
-    row++;
-
-    formGrid.setText(row, labelIdx, Util.C.diffViewLabel());
-    formGrid.setWidget(row, fieldIdx, diffView);
-    row++;
-
-    formGrid.setText(row, labelIdx, "");
-    formGrid.setWidget(row, fieldIdx, showSiteHeader);
-    row++;
-
-    formGrid.setText(row, labelIdx, "");
-    formGrid.setWidget(row, fieldIdx, highlightAssigneeInChangeTable);
-    row++;
-
-    formGrid.setText(row, labelIdx, "");
-    formGrid.setWidget(row, fieldIdx, relativeDateInChangeTable);
-    row++;
-
-    formGrid.setText(row, labelIdx, "");
-    formGrid.setWidget(row, fieldIdx, sizeBarInChangeTable);
-    row++;
-
-    formGrid.setText(row, labelIdx, "");
-    formGrid.setWidget(row, fieldIdx, legacycidInChangeTable);
-    row++;
-
-    formGrid.setText(row, labelIdx, "");
-    formGrid.setWidget(row, fieldIdx, muteCommonPathPrefixes);
-    row++;
-
-    formGrid.setText(row, labelIdx, "");
-    formGrid.setWidget(row, fieldIdx, signedOffBy);
-    row++;
-
-    formGrid.setText(row, labelIdx, "");
-    formGrid.setWidget(row, fieldIdx, publishCommentsOnPush);
-    row++;
-
-    formGrid.setText(row, labelIdx, "");
-    formGrid.setWidget(row, fieldIdx, workInProgressByDefault);
-    row++;
-
-    if (flashClippy) {
-      formGrid.setText(row, labelIdx, "");
-      formGrid.setWidget(row, fieldIdx, useFlashClipboard);
-    }
-
-    add(formGrid);
-
-    save = new Button(Util.C.buttonSaveChanges());
-    save.setEnabled(false);
-    save.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            doSave();
-          }
-        });
-
-    myMenus = new MyMenuPanel(save);
-    add(myMenus);
-
-    add(save);
-
-    final OnEditEnabler e = new OnEditEnabler(save);
-    e.listenTo(showSiteHeader);
-    e.listenTo(useFlashClipboard);
-    e.listenTo(maximumPageSize);
-    e.listenTo(dateFormat);
-    e.listenTo(timeFormat);
-    e.listenTo(highlightAssigneeInChangeTable);
-    e.listenTo(relativeDateInChangeTable);
-    e.listenTo(sizeBarInChangeTable);
-    e.listenTo(legacycidInChangeTable);
-    e.listenTo(muteCommonPathPrefixes);
-    e.listenTo(signedOffBy);
-    e.listenTo(publishCommentsOnPush);
-    e.listenTo(workInProgressByDefault);
-    e.listenTo(diffView);
-    e.listenTo(reviewCategoryStrategy);
-    e.listenTo(emailStrategy);
-    e.listenTo(emailFormat);
-    e.listenTo(defaultBaseForMerges);
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    ExtensionPanel extensionPanel =
-        createExtensionPoint(GerritUiExtensionPoint.PREFERENCES_SCREEN_BOTTOM);
-    extensionPanel.addStyleName(Gerrit.RESOURCES.css().extensionPanel());
-    add(extensionPanel);
-
-    AccountApi.self()
-        .view("preferences")
-        .get(
-            new ScreenLoadCallback<GeneralPreferences>(this) {
-              @Override
-              public void preDisplay(GeneralPreferences prefs) {
-                display(prefs);
-              }
-            });
-  }
-
-  private void enable(boolean on) {
-    showSiteHeader.setEnabled(on);
-    useFlashClipboard.setEnabled(on);
-    maximumPageSize.setEnabled(on);
-    dateFormat.setEnabled(on);
-    timeFormat.setEnabled(on);
-    highlightAssigneeInChangeTable.setEnabled(on);
-    relativeDateInChangeTable.setEnabled(on);
-    sizeBarInChangeTable.setEnabled(on);
-    legacycidInChangeTable.setEnabled(on);
-    muteCommonPathPrefixes.setEnabled(on);
-    signedOffBy.setEnabled(on);
-    publishCommentsOnPush.setEnabled(on);
-    workInProgressByDefault.setEnabled(on);
-    reviewCategoryStrategy.setEnabled(on);
-    diffView.setEnabled(on);
-    emailStrategy.setEnabled(on);
-    emailFormat.setEnabled(on);
-    defaultBaseForMerges.setEnabled(on);
-  }
-
-  private void display(GeneralPreferences p) {
-    showSiteHeader.setValue(p.showSiteHeader());
-    useFlashClipboard.setValue(p.useFlashClipboard());
-    setListBox(maximumPageSize, DEFAULT_PAGESIZE, p.changesPerPage());
-    setListBox(
-        dateFormat,
-        GeneralPreferencesInfo.DateFormat.STD, //
-        p.dateFormat());
-    setListBox(
-        timeFormat,
-        GeneralPreferencesInfo.TimeFormat.HHMM_12, //
-        p.timeFormat());
-    highlightAssigneeInChangeTable.setValue(p.highlightAssigneeInChangeTable());
-    relativeDateInChangeTable.setValue(p.relativeDateInChangeTable());
-    sizeBarInChangeTable.setValue(p.sizeBarInChangeTable());
-    legacycidInChangeTable.setValue(p.legacycidInChangeTable());
-    muteCommonPathPrefixes.setValue(p.muteCommonPathPrefixes());
-    signedOffBy.setValue(p.signedOffBy());
-    publishCommentsOnPush.setValue(p.publishCommentsOnPush());
-    workInProgressByDefault.setValue(p.workInProgressByDefault());
-    setListBox(
-        reviewCategoryStrategy,
-        GeneralPreferencesInfo.ReviewCategoryStrategy.NONE,
-        p.reviewCategoryStrategy());
-    setListBox(diffView, GeneralPreferencesInfo.DiffView.SIDE_BY_SIDE, p.diffView());
-    setListBox(emailStrategy, GeneralPreferencesInfo.EmailStrategy.ENABLED, p.emailStrategy());
-    setListBox(emailFormat, GeneralPreferencesInfo.EmailFormat.HTML_PLAINTEXT, p.emailFormat());
-    setListBox(
-        defaultBaseForMerges,
-        GeneralPreferencesInfo.DefaultBase.FIRST_PARENT,
-        p.defaultBaseForMerges());
-    display(p.my());
-  }
-
-  private void display(JsArray<TopMenuItem> items) {
-    List<List<String>> values = new ArrayList<>();
-    for (TopMenuItem item : Natives.asList(items)) {
-      values.add(Arrays.asList(item.getName(), item.getUrl()));
-    }
-    myMenus.display(values);
-  }
-
-  private void setListBox(ListBox f, int defaultValue, int currentValue) {
-    setListBox(f, String.valueOf(defaultValue), String.valueOf(currentValue));
-  }
-
-  private <T extends Enum<?>> void setListBox(final ListBox f, T defaultValue, T currentValue) {
-    setListBox(
-        f,
-        defaultValue != null ? defaultValue.name() : "",
-        currentValue != null ? currentValue.name() : "");
-  }
-
-  private void setListBox(ListBox f, String defaultValue, String currentValue) {
-    final int n = f.getItemCount();
-    for (int i = 0; i < n; i++) {
-      if (f.getValue(i).equals(currentValue)) {
-        f.setSelectedIndex(i);
-        return;
-      }
-    }
-    if (!currentValue.equals(defaultValue)) {
-      setListBox(f, defaultValue, defaultValue);
-    }
-  }
-
-  private int getListBox(ListBox f, int defaultValue) {
-    final int idx = f.getSelectedIndex();
-    if (0 <= idx) {
-      return Short.parseShort(f.getValue(idx));
-    }
-    return defaultValue;
-  }
-
-  private <T extends Enum<?>> T getListBox(ListBox f, T defaultValue, T[] all) {
-    final int idx = f.getSelectedIndex();
-    if (0 <= idx) {
-      String v = f.getValue(idx);
-      if ("".equals(v)) {
-        return defaultValue;
-      }
-      for (T t : all) {
-        if (t.name().equals(v)) {
-          return t;
-        }
-      }
-    }
-    return defaultValue;
-  }
-
-  private void doSave() {
-    GeneralPreferences p = GeneralPreferences.create();
-    p.showSiteHeader(showSiteHeader.getValue());
-    p.useFlashClipboard(useFlashClipboard.getValue());
-    p.changesPerPage(getListBox(maximumPageSize, DEFAULT_PAGESIZE));
-    p.dateFormat(
-        getListBox(
-            dateFormat,
-            GeneralPreferencesInfo.DateFormat.STD,
-            GeneralPreferencesInfo.DateFormat.values()));
-    p.timeFormat(
-        getListBox(
-            timeFormat,
-            GeneralPreferencesInfo.TimeFormat.HHMM_12,
-            GeneralPreferencesInfo.TimeFormat.values()));
-    p.highlightAssigneeInChangeTable(highlightAssigneeInChangeTable.getValue());
-    p.relativeDateInChangeTable(relativeDateInChangeTable.getValue());
-    p.sizeBarInChangeTable(sizeBarInChangeTable.getValue());
-    p.legacycidInChangeTable(legacycidInChangeTable.getValue());
-    p.muteCommonPathPrefixes(muteCommonPathPrefixes.getValue());
-    p.signedOffBy(signedOffBy.getValue());
-    p.publishCommentsOnPush(publishCommentsOnPush.getValue());
-    p.workInProgressByDefault(workInProgressByDefault.getValue());
-    p.reviewCategoryStrategy(
-        getListBox(
-            reviewCategoryStrategy, ReviewCategoryStrategy.NONE, ReviewCategoryStrategy.values()));
-    p.diffView(
-        getListBox(
-            diffView,
-            GeneralPreferencesInfo.DiffView.SIDE_BY_SIDE,
-            GeneralPreferencesInfo.DiffView.values()));
-
-    p.emailStrategy(
-        getListBox(
-            emailStrategy,
-            GeneralPreferencesInfo.EmailStrategy.ENABLED,
-            GeneralPreferencesInfo.EmailStrategy.values()));
-
-    p.emailFormat(
-        getListBox(
-            emailFormat,
-            GeneralPreferencesInfo.EmailFormat.HTML_PLAINTEXT,
-            GeneralPreferencesInfo.EmailFormat.values()));
-
-    p.defaultBaseForMerges(
-        getListBox(
-            defaultBaseForMerges,
-            GeneralPreferencesInfo.DefaultBase.FIRST_PARENT,
-            GeneralPreferencesInfo.DefaultBase.values()));
-
-    List<TopMenuItem> items = new ArrayList<>();
-    for (List<String> v : myMenus.getValues()) {
-      items.add(TopMenuItem.create(v.get(0), v.get(1)));
-    }
-    p.setMyMenus(items);
-
-    enable(false);
-    save.setEnabled(false);
-
-    AccountApi.self()
-        .view("preferences")
-        .put(
-            p,
-            new GerritCallback<GeneralPreferences>() {
-              @Override
-              public void onSuccess(GeneralPreferences prefs) {
-                Gerrit.setUserPreferences(prefs);
-                enable(true);
-                display(prefs);
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {
-                enable(true);
-                save.setEnabled(true);
-                super.onFailure(caught);
-              }
-            });
-  }
-
-  private class MyMenuPanel extends StringListPanel {
-    MyMenuPanel(Button save) {
-      super(Util.C.myMenu(), Arrays.asList(Util.C.myMenuName(), Util.C.myMenuUrl()), save, false);
-
-      setInfo(Util.C.myMenuInfo());
-
-      Button resetButton = new Button(Util.C.myMenuReset());
-      resetButton.addClickHandler(
-          new ClickHandler() {
-            @Override
-            public void onClick(ClickEvent event) {
-              ConfigServerApi.defaultPreferences(
-                  new GerritCallback<GeneralPreferences>() {
-                    @Override
-                    public void onSuccess(GeneralPreferences p) {
-                      MyPreferencesScreen.this.display(p.my());
-                      widget.setEnabled(true);
-                    }
-                  });
-            }
-          });
-      buttonPanel.add(resetButton);
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java
deleted file mode 100644
index 0275948..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java
+++ /dev/null
@@ -1,127 +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.client.account;
-
-import static com.google.gerrit.client.FormatUtil.mediumFormat;
-
-import com.google.gerrit.client.AvatarImage;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.GerritUiExtensionPoint;
-import com.google.gerrit.client.info.AccountInfo;
-import com.google.gerrit.client.rpc.NativeString;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gwt.i18n.client.LocaleInfo;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
-import com.google.gwt.user.client.ui.HorizontalPanel;
-import com.google.gwt.user.client.ui.VerticalPanel;
-
-public class MyProfileScreen extends SettingsScreen {
-  private AvatarImage avatar;
-  private Anchor changeAvatar;
-  private int labelIdx;
-  private int fieldIdx;
-  private Grid info;
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-
-    HorizontalPanel h = new HorizontalPanel();
-    add(h);
-
-    if (Gerrit.info().plugin().hasAvatars()) {
-      VerticalPanel v = new VerticalPanel();
-      v.addStyleName(Gerrit.RESOURCES.css().avatarInfoPanel());
-      h.add(v);
-      avatar = new AvatarImage();
-      v.add(avatar);
-      changeAvatar = new Anchor(Util.C.changeAvatar(), "", "_blank");
-      changeAvatar.setVisible(false);
-      v.add(changeAvatar);
-    }
-
-    if (LocaleInfo.getCurrentLocale().isRTL()) {
-      labelIdx = 1;
-      fieldIdx = 0;
-    } else {
-      labelIdx = 0;
-      fieldIdx = 1;
-    }
-
-    info = new Grid((Gerrit.info().auth().siteHasUsernames() ? 1 : 0) + 4, 2);
-    info.setStyleName(Gerrit.RESOURCES.css().infoBlock());
-    info.addStyleName(Gerrit.RESOURCES.css().accountInfoBlock());
-    h.add(info);
-
-    int row = 0;
-    if (Gerrit.info().auth().siteHasUsernames()) {
-      infoRow(row++, Util.C.userName());
-    }
-    infoRow(row++, Util.C.fullName());
-    infoRow(row++, Util.C.preferredEmail());
-    infoRow(row++, Util.C.registeredOn());
-    infoRow(row++, Util.C.accountId());
-
-    final CellFormatter fmt = info.getCellFormatter();
-    fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().topmost());
-    fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().topmost());
-    fmt.addStyleName(row - 1, 0, Gerrit.RESOURCES.css().bottomheader());
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    add(createExtensionPoint(GerritUiExtensionPoint.PROFILE_SCREEN_BOTTOM));
-    display(Gerrit.getUserAccount());
-    display();
-  }
-
-  private void infoRow(int row, String name) {
-    info.setText(row, labelIdx, name);
-    info.getCellFormatter().addStyleName(row, 0, Gerrit.RESOURCES.css().header());
-  }
-
-  void display(AccountInfo account) {
-    if (Gerrit.info().plugin().hasAvatars()) {
-      avatar.setAccount(account, 93, false);
-      new RestApi("/accounts/")
-          .id("self")
-          .view("avatar.change.url")
-          .get(
-              new AsyncCallback<NativeString>() {
-                @Override
-                public void onSuccess(NativeString changeUrl) {
-                  changeAvatar.setHref(changeUrl.asString());
-                  changeAvatar.setVisible(true);
-                }
-
-                @Override
-                public void onFailure(Throwable caught) {}
-              });
-    }
-
-    int row = 0;
-    if (Gerrit.info().auth().siteHasUsernames()) {
-      info.setWidget(row++, fieldIdx, new UsernameField());
-    }
-    info.setText(row++, fieldIdx, account.name());
-    info.setText(row++, fieldIdx, account.email());
-    info.setText(row++, fieldIdx, mediumFormat(account.registeredOn()));
-    info.setText(row, fieldIdx, Integer.toString(account._accountId()));
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MySshKeysScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MySshKeysScreen.java
deleted file mode 100644
index 6ba63aa..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MySshKeysScreen.java
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT 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.client.account;
-
-public class MySshKeysScreen extends SettingsScreen {
-  private SshPanel panel;
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    panel =
-        new SshPanel() {
-          @Override
-          void display() {
-            MySshKeysScreen.this.display();
-          }
-        };
-    add(panel);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
deleted file mode 100644
index f29b573..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
+++ /dev/null
@@ -1,228 +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.client.account;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.HintTextBox;
-import com.google.gerrit.client.ui.ProjectListPopup;
-import com.google.gerrit.client.ui.ProjectNameSuggestOracle;
-import com.google.gerrit.client.ui.RemoteSuggestBox;
-import com.google.gerrit.common.PageLinks;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwt.event.logical.shared.SelectionEvent;
-import com.google.gwt.event.logical.shared.SelectionHandler;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
-import com.google.gwt.user.client.ui.HorizontalPanel;
-
-public class MyWatchedProjectsScreen extends SettingsScreen {
-  private Button addNew;
-  private RemoteSuggestBox nameBox;
-  private HintTextBox filterTxt;
-  private MyWatchesTable watchesTab;
-  private Button browse;
-  private Button delSel;
-  private Grid grid;
-  private ProjectListPopup projectsPopup;
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    createWidgets();
-
-    /* top table */
-    grid = new Grid(2, 2);
-    grid.setStyleName(Gerrit.RESOURCES.css().infoBlock());
-    grid.setText(0, 0, Util.C.watchedProjectName());
-    final HorizontalPanel hp = new HorizontalPanel();
-    hp.add(nameBox);
-    hp.add(browse);
-    grid.setWidget(0, 1, hp);
-
-    grid.setText(1, 0, Util.C.watchedProjectFilter());
-    grid.setWidget(1, 1, filterTxt);
-
-    final CellFormatter fmt = grid.getCellFormatter();
-    fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().topmost());
-    fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().topmost());
-    fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().header());
-    fmt.addStyleName(1, 0, Gerrit.RESOURCES.css().header());
-    fmt.addStyleName(1, 0, Gerrit.RESOURCES.css().bottomheader());
-
-    final FlowPanel fp = new FlowPanel();
-    fp.setStyleName(Gerrit.RESOURCES.css().addWatchPanel());
-    fp.add(grid);
-    fp.add(addNew);
-    add(fp);
-
-    /* bottom table */
-    add(watchesTab);
-    add(delSel);
-
-    /* popup */
-    projectsPopup =
-        new ProjectListPopup() {
-          @Override
-          protected void onMovePointerTo(String projectName) {
-            // prevent user input from being overwritten by simply poping up
-            if (!projectsPopup.isPoppingUp() || "".equals(nameBox.getText())) {
-              nameBox.setText(projectName);
-            }
-          }
-
-          @Override
-          protected void openRow(String projectName) {
-            nameBox.setText(projectName);
-            doAddNew();
-          }
-        };
-    projectsPopup.initPopup(Util.C.projects(), PageLinks.SETTINGS_PROJECTS);
-  }
-
-  protected void createWidgets() {
-    nameBox = new RemoteSuggestBox(new ProjectNameSuggestOracle());
-    nameBox.setVisibleLength(50);
-    nameBox.setHintText(Util.C.defaultProjectName());
-    nameBox.addSelectionHandler(
-        new SelectionHandler<String>() {
-          @Override
-          public void onSelection(SelectionEvent<String> event) {
-            doAddNew();
-          }
-        });
-
-    filterTxt = new HintTextBox();
-    filterTxt.setVisibleLength(50);
-    filterTxt.setHintText(Util.C.defaultFilter());
-    filterTxt.addKeyPressHandler(
-        new KeyPressHandler() {
-          @Override
-          public void onKeyPress(KeyPressEvent event) {
-            if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
-              doAddNew();
-            }
-          }
-        });
-
-    addNew = new Button(Util.C.buttonWatchProject());
-    addNew.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            doAddNew();
-          }
-        });
-
-    browse = new Button(Util.C.buttonBrowseProjects());
-    browse.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            int top = grid.getAbsoluteTop() - 50; // under page header
-            // Try to place it to the right of everything else, but not
-            // right justified
-            int left =
-                5
-                    + Math.max(
-                        grid.getAbsoluteLeft() + grid.getOffsetWidth(),
-                        watchesTab.getAbsoluteLeft() + watchesTab.getOffsetWidth());
-            projectsPopup.setPreferredCoordinates(top, left);
-            projectsPopup.displayPopup();
-          }
-        });
-
-    watchesTab = new MyWatchesTable();
-
-    delSel = new Button(Util.C.buttonDeleteSshKey());
-    delSel.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            watchesTab.deleteChecked();
-          }
-        });
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    populateWatches();
-  }
-
-  @Override
-  protected void onUnload() {
-    super.onUnload();
-    projectsPopup.closePopup();
-  }
-
-  protected void doAddNew() {
-    final String projectName = nameBox.getText().trim();
-    if ("".equals(projectName)) {
-      return;
-    }
-
-    addNew.setEnabled(false);
-    nameBox.setEnabled(false);
-    filterTxt.setEnabled(false);
-
-    final ProjectWatchInfo projectWatchInfo = JavaScriptObject.createObject().cast();
-    projectWatchInfo.project(projectName);
-    projectWatchInfo.filter(filterTxt.getText());
-
-    AccountApi.updateWatchedProject(
-        "self",
-        projectWatchInfo,
-        new GerritCallback<JsArray<ProjectWatchInfo>>() {
-          @Override
-          public void onSuccess(JsArray<ProjectWatchInfo> watchedProjects) {
-            addNew.setEnabled(true);
-            nameBox.setEnabled(true);
-            filterTxt.setEnabled(true);
-
-            nameBox.setText("");
-            watchesTab.insertWatch(projectWatchInfo);
-          }
-
-          @Override
-          public void onFailure(Throwable caught) {
-            addNew.setEnabled(true);
-            nameBox.setEnabled(true);
-            filterTxt.setEnabled(true);
-            super.onFailure(caught);
-          }
-        });
-  }
-
-  protected void populateWatches() {
-    AccountApi.getWatchedProjects(
-        "self",
-        new GerritCallback<JsArray<ProjectWatchInfo>>() {
-          @Override
-          public void onSuccess(JsArray<ProjectWatchInfo> watchedProjects) {
-            watchesTab.display(watchedProjects);
-            display();
-          }
-        });
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java
deleted file mode 100644
index 0a61b2d..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java
+++ /dev/null
@@ -1,193 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT 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.client.account;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.ui.FancyFlexTable;
-import com.google.gerrit.client.ui.ProjectLink;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.Label;
-import java.util.HashSet;
-import java.util.Set;
-
-public class MyWatchesTable extends FancyFlexTable<ProjectWatchInfo> {
-
-  public MyWatchesTable() {
-    table.setWidth("");
-    table.insertRow(1);
-    table.setText(0, 2, Util.C.watchedProjectName());
-    table.setText(0, 3, Util.C.watchedProjectColumnEmailNotifications());
-
-    final FlexCellFormatter fmt = table.getFlexCellFormatter();
-    fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().iconHeader());
-    fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
-    fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader());
-    fmt.setRowSpan(0, 0, 2);
-    fmt.setRowSpan(0, 1, 2);
-    fmt.setRowSpan(0, 2, 2);
-    fmt.getElement(0, 3).setPropertyString("align", "center");
-
-    fmt.setColSpan(0, 3, 5);
-    table.setText(1, 0, Util.C.watchedProjectColumnNewChanges());
-    table.setText(1, 1, Util.C.watchedProjectColumnNewPatchSets());
-    table.setText(1, 2, Util.C.watchedProjectColumnAllComments());
-    table.setText(1, 3, Util.C.watchedProjectColumnSubmittedChanges());
-    table.setText(1, 4, Util.C.watchedProjectColumnAbandonedChanges());
-    fmt.addStyleName(1, 0, Gerrit.RESOURCES.css().dataHeader());
-    fmt.addStyleName(1, 1, Gerrit.RESOURCES.css().dataHeader());
-    fmt.addStyleName(1, 2, Gerrit.RESOURCES.css().dataHeader());
-    fmt.addStyleName(1, 3, Gerrit.RESOURCES.css().dataHeader());
-    fmt.addStyleName(1, 4, Gerrit.RESOURCES.css().dataHeader());
-  }
-
-  public void deleteChecked() {
-    final Set<ProjectWatchInfo> infos = getCheckedProjectWatchInfos();
-    if (!infos.isEmpty()) {
-      AccountApi.deleteWatchedProjects(
-          "self",
-          infos,
-          new GerritCallback<JsArray<ProjectWatchInfo>>() {
-            @Override
-            public void onSuccess(JsArray<ProjectWatchInfo> watchedProjects) {
-              remove(infos);
-            }
-          });
-    }
-  }
-
-  protected void remove(Set<ProjectWatchInfo> infos) {
-    for (int row = 1; row < table.getRowCount(); ) {
-      final ProjectWatchInfo k = getRowItem(row);
-      if (k != null && infos.contains(k)) {
-        table.removeRow(row);
-      } else {
-        row++;
-      }
-    }
-  }
-
-  protected Set<ProjectWatchInfo> getCheckedProjectWatchInfos() {
-    final Set<ProjectWatchInfo> infos = new HashSet<>();
-    for (int row = 1; row < table.getRowCount(); row++) {
-      final ProjectWatchInfo k = getRowItem(row);
-      if (k != null && ((CheckBox) table.getWidget(row, 1)).getValue()) {
-        infos.add(k);
-      }
-    }
-    return infos;
-  }
-
-  public void insertWatch(ProjectWatchInfo k) {
-    final String newName = k.project();
-    int row = 1;
-    for (; row < table.getRowCount(); row++) {
-      final ProjectWatchInfo i = getRowItem(row);
-      if (i != null && i.project().compareTo(newName) >= 0) {
-        break;
-      }
-    }
-
-    table.insertRow(row);
-    applyDataRowStyle(row);
-    populate(row, k);
-  }
-
-  public void display(JsArray<ProjectWatchInfo> result) {
-    while (2 < table.getRowCount()) {
-      table.removeRow(table.getRowCount() - 1);
-    }
-
-    for (ProjectWatchInfo info : Natives.asList(result)) {
-      final int row = table.getRowCount();
-      table.insertRow(row);
-      applyDataRowStyle(row);
-      populate(row, info);
-    }
-  }
-
-  protected void populate(int row, ProjectWatchInfo info) {
-    final FlowPanel fp = new FlowPanel();
-    fp.add(new ProjectLink(info.project(), new Project.NameKey(info.project())));
-    if (info.filter() != null) {
-      Label filter = new Label(info.filter());
-      filter.setStyleName(Gerrit.RESOURCES.css().watchedProjectFilter());
-      fp.add(filter);
-    }
-
-    table.setWidget(row, 1, new CheckBox());
-    table.setWidget(row, 2, fp);
-
-    addNotifyButton(ProjectWatchInfo.Type.NEW_CHANGES, info, row, 3);
-    addNotifyButton(ProjectWatchInfo.Type.NEW_PATCHSETS, info, row, 4);
-    addNotifyButton(ProjectWatchInfo.Type.ALL_COMMENTS, info, row, 5);
-    addNotifyButton(ProjectWatchInfo.Type.SUBMITTED_CHANGES, info, row, 6);
-    addNotifyButton(ProjectWatchInfo.Type.ABANDONED_CHANGES, info, row, 7);
-
-    final FlexCellFormatter fmt = table.getFlexCellFormatter();
-    fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().iconCell());
-    fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell());
-    fmt.addStyleName(row, 3, Gerrit.RESOURCES.css().dataCell());
-    fmt.addStyleName(row, 4, Gerrit.RESOURCES.css().dataCell());
-    fmt.addStyleName(row, 5, Gerrit.RESOURCES.css().dataCell());
-    fmt.addStyleName(row, 6, Gerrit.RESOURCES.css().dataCell());
-    fmt.addStyleName(row, 7, Gerrit.RESOURCES.css().dataCell());
-
-    setRowItem(row, info);
-  }
-
-  protected void addNotifyButton(
-      final ProjectWatchInfo.Type type, ProjectWatchInfo info, int row, int col) {
-    final CheckBox cbox = new CheckBox();
-
-    cbox.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            final Boolean oldVal = info.notify(type);
-            info.notify(type, cbox.getValue());
-            cbox.setEnabled(false);
-
-            AccountApi.updateWatchedProject(
-                "self",
-                info,
-                new GerritCallback<JsArray<ProjectWatchInfo>>() {
-                  @Override
-                  public void onSuccess(JsArray<ProjectWatchInfo> watchedProjects) {
-                    cbox.setEnabled(true);
-                  }
-
-                  @Override
-                  public void onFailure(Throwable caught) {
-                    cbox.setEnabled(true);
-                    info.notify(type, oldVal);
-                    cbox.setValue(oldVal);
-                    super.onFailure(caught);
-                  }
-                });
-          }
-        });
-
-    cbox.setValue(info.notify(type));
-    table.setWidget(row, col, cbox);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java
deleted file mode 100644
index 7c90884..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java
+++ /dev/null
@@ -1,258 +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.client.account;
-
-import com.google.gerrit.client.ErrorDialog;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.info.AgreementInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.NativeString;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.ui.AccountScreen;
-import com.google.gerrit.client.ui.OnEditEnabler;
-import com.google.gerrit.client.ui.SmallHeading;
-import com.google.gerrit.common.PageLinks;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.http.client.Request;
-import com.google.gwt.http.client.RequestBuilder;
-import com.google.gwt.http.client.RequestCallback;
-import com.google.gwt.http.client.RequestException;
-import com.google.gwt.http.client.Response;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.FormPanel;
-import com.google.gwt.user.client.ui.HTML;
-import com.google.gwt.user.client.ui.InlineLabel;
-import com.google.gwt.user.client.ui.Label;
-import com.google.gwt.user.client.ui.Panel;
-import com.google.gwt.user.client.ui.RadioButton;
-import com.google.gwt.user.client.ui.VerticalPanel;
-import com.google.gwtexpui.globalkey.client.NpTextBox;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-public class NewAgreementScreen extends AccountScreen {
-  private final String nextToken;
-  private Set<String> mySigned;
-  private List<AgreementInfo> available;
-  private AgreementInfo current;
-
-  private VerticalPanel radios;
-
-  private Panel agreementGroup;
-  private HTML agreementHtml;
-
-  private Panel finalGroup;
-  private NpTextBox yesIAgreeBox;
-  private Button submit;
-
-  public NewAgreementScreen() {
-    this(null);
-  }
-
-  public NewAgreementScreen(String token) {
-    nextToken = token != null ? token : PageLinks.SETTINGS_AGREEMENTS;
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    AccountApi.getAgreements(
-        "self",
-        new GerritCallback<JsArray<AgreementInfo>>() {
-          @Override
-          public void onSuccess(JsArray<AgreementInfo> result) {
-            if (isAttached()) {
-              mySigned = new HashSet<>();
-              for (AgreementInfo info : Natives.asList(result)) {
-                mySigned.add(info.name());
-              }
-              postRPC();
-            }
-          }
-        });
-
-    available = Gerrit.info().auth().contributorAgreements();
-    postRPC();
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    setPageTitle(Util.C.newAgreement());
-
-    final FlowPanel formBody = new FlowPanel();
-    radios = new VerticalPanel();
-    formBody.add(radios);
-
-    agreementGroup = new FlowPanel();
-    agreementGroup.add(new SmallHeading(Util.C.newAgreementReviewLegalHeading()));
-
-    agreementHtml = new HTML();
-    agreementHtml.setStyleName(Gerrit.RESOURCES.css().contributorAgreementLegal());
-    agreementGroup.add(agreementHtml);
-    formBody.add(agreementGroup);
-
-    finalGroup = new VerticalPanel();
-    finalGroup.add(new SmallHeading(Util.C.newAgreementCompleteHeading()));
-    final FlowPanel fp = new FlowPanel();
-    yesIAgreeBox = new NpTextBox();
-    yesIAgreeBox.setVisibleLength(Util.C.newAgreementIAGREE().length() + 8);
-    yesIAgreeBox.setMaxLength(Util.C.newAgreementIAGREE().length());
-    fp.add(yesIAgreeBox);
-    fp.add(new InlineLabel(Util.M.enterIAGREE(Util.C.newAgreementIAGREE())));
-    finalGroup.add(fp);
-    submit = new Button(Util.C.buttonSubmitNewAgreement());
-    submit.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            doSign();
-          }
-        });
-    finalGroup.add(submit);
-    formBody.add(finalGroup);
-    new OnEditEnabler(submit, yesIAgreeBox);
-
-    final FormPanel form = new FormPanel();
-    form.add(formBody);
-    add(form);
-  }
-
-  private void postRPC() {
-    if (mySigned != null && available != null) {
-      renderSelf();
-      display();
-    }
-  }
-
-  private void renderSelf() {
-    current = null;
-    agreementGroup.setVisible(false);
-    finalGroup.setVisible(false);
-    radios.clear();
-
-    final SmallHeading hdr = new SmallHeading();
-    if (available.isEmpty()) {
-      hdr.setText(Util.C.newAgreementNoneAvailable());
-    } else {
-      hdr.setText(Util.C.newAgreementSelectTypeHeading());
-    }
-    radios.add(hdr);
-
-    for (AgreementInfo cla : available) {
-      final RadioButton r = new RadioButton("cla_id", cla.name());
-      r.addStyleName(Gerrit.RESOURCES.css().contributorAgreementButton());
-      radios.add(r);
-
-      if (mySigned.contains(cla.name())) {
-        r.setEnabled(false);
-        final Label l = new Label(Util.C.newAgreementAlreadySubmitted());
-        l.setStyleName(Gerrit.RESOURCES.css().contributorAgreementAlreadySubmitted());
-        radios.add(l);
-      } else {
-        r.addClickHandler(
-            new ClickHandler() {
-              @Override
-              public void onClick(ClickEvent event) {
-                showCLA(cla);
-              }
-            });
-      }
-
-      if (cla.description() != null && !cla.description().equals("")) {
-        final Label l = new Label(cla.description());
-        l.setStyleName(Gerrit.RESOURCES.css().contributorAgreementShortDescription());
-        radios.add(l);
-      }
-    }
-  }
-
-  private void doSign() {
-    submit.setEnabled(false);
-
-    if (current == null || !Util.C.newAgreementIAGREE().equalsIgnoreCase(yesIAgreeBox.getText())) {
-      yesIAgreeBox.setText("");
-      yesIAgreeBox.setFocus(true);
-      return;
-    }
-    doEnterAgreement();
-  }
-
-  private void doEnterAgreement() {
-    AccountApi.enterAgreement(
-        "self",
-        current.name(),
-        new GerritCallback<NativeString>() {
-          @Override
-          public void onSuccess(NativeString result) {
-            Gerrit.display(nextToken);
-          }
-
-          @Override
-          public void onFailure(Throwable caught) {
-            yesIAgreeBox.setText("");
-            super.onFailure(caught);
-          }
-        });
-  }
-
-  private void showCLA(AgreementInfo cla) {
-    current = cla;
-    String url = cla.url();
-    if (url != null && url.length() > 0) {
-      agreementGroup.setVisible(true);
-      agreementHtml.setText(Gerrit.C.rpcStatusWorking());
-      if (!url.startsWith("http:") && !url.startsWith("https:")) {
-        url = GWT.getHostPageBaseURL() + url;
-      }
-      final RequestBuilder rb = new RequestBuilder(RequestBuilder.GET, url);
-      rb.setCallback(
-          new RequestCallback() {
-            @Override
-            public void onError(Request request, Throwable exception) {
-              new ErrorDialog(exception).center();
-            }
-
-            @Override
-            public void onResponseReceived(Request request, Response response) {
-              final String ct = response.getHeader("Content-Type");
-              if (response.getStatusCode() == 200
-                  && ct != null
-                  && (ct.equals("text/html") || ct.startsWith("text/html;"))) {
-                agreementHtml.setHTML(response.getText());
-              } else {
-                new ErrorDialog(response.getStatusText()).center();
-              }
-            }
-          });
-      try {
-        rb.send();
-      } catch (RequestException e) {
-        new ErrorDialog(e).show();
-      }
-    } else {
-      agreementGroup.setVisible(false);
-    }
-
-    finalGroup.setVisible(cla.autoVerifyGroup() != null);
-    yesIAgreeBox.setText("");
-    submit.setEnabled(false);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ProjectWatchInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ProjectWatchInfo.java
deleted file mode 100644
index fab25ae..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ProjectWatchInfo.java
+++ /dev/null
@@ -1,97 +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.client.account;
-
-import com.google.gwt.core.client.JavaScriptObject;
-
-public class ProjectWatchInfo extends JavaScriptObject {
-
-  public enum Type {
-    NEW_CHANGES,
-    NEW_PATCHSETS,
-    ALL_COMMENTS,
-    SUBMITTED_CHANGES,
-    ABANDONED_CHANGES
-  }
-
-  public final native String project() /*-{ return this.project; }-*/;
-
-  public final native String filter() /*-{ return this.filter; }-*/;
-
-  public final native void project(String s) /*-{ this.project = s; }-*/;
-
-  public final native void filter(String s) /*-{ this.filter = s; }-*/;
-
-  public final void notify(ProjectWatchInfo.Type t, Boolean b) {
-    if (t == ProjectWatchInfo.Type.NEW_CHANGES) {
-      notifyNewChanges(b.booleanValue());
-    } else if (t == Type.NEW_PATCHSETS) {
-      notifyNewPatchSets(b.booleanValue());
-    } else if (t == Type.ALL_COMMENTS) {
-      notifyAllComments(b.booleanValue());
-    } else if (t == Type.SUBMITTED_CHANGES) {
-      notifySubmittedChanges(b.booleanValue());
-    } else if (t == Type.ABANDONED_CHANGES) {
-      notifyAbandonedChanges(b.booleanValue());
-    }
-  }
-
-  public final Boolean notify(ProjectWatchInfo.Type t) {
-    boolean b = false;
-    if (t == ProjectWatchInfo.Type.NEW_CHANGES) {
-      b = notifyNewChanges();
-    } else if (t == Type.NEW_PATCHSETS) {
-      b = notifyNewPatchSets();
-    } else if (t == Type.ALL_COMMENTS) {
-      b = notifyAllComments();
-    } else if (t == Type.SUBMITTED_CHANGES) {
-      b = notifySubmittedChanges();
-    } else if (t == Type.ABANDONED_CHANGES) {
-      b = notifyAbandonedChanges();
-    }
-    return Boolean.valueOf(b);
-  }
-
-  private native boolean
-      notifyNewChanges() /*-{ return this['notify_new_changes'] ? true : false; }-*/;
-
-  private native boolean
-      notifyNewPatchSets() /*-{ return this['notify_new_patch_sets'] ? true : false; }-*/;
-
-  private native boolean
-      notifyAllComments() /*-{ return this['notify_all_comments'] ? true : false; }-*/;
-
-  private native boolean
-      notifySubmittedChanges() /*-{ return this['notify_submitted_changes'] ? true : false; }-*/;
-
-  private native boolean
-      notifyAbandonedChanges() /*-{ return this['notify_abandoned_changes'] ? true : false; }-*/;
-
-  private native void notifyNewChanges(
-      boolean b) /*-{ this['notify_new_changes'] = b ? true : null; }-*/;
-
-  private native void notifyNewPatchSets(
-      boolean b) /*-{ this['notify_new_patch_sets'] = b ? true : null; }-*/;
-
-  private native void notifyAllComments(
-      boolean b) /*-{ this['notify_all_comments'] = b ? true : null; }-*/;
-
-  private native void notifySubmittedChanges(
-      boolean b) /*-{ this['notify_submitted_changes'] = b ? true : null; }-*/;
-
-  private native void notifyAbandonedChanges(
-      boolean b) /*-{ this['notify_abandoned_changes'] = b ? true : null; }-*/;
-
-  protected ProjectWatchInfo() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java
deleted file mode 100644
index 29de14a..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java
+++ /dev/null
@@ -1,141 +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.client.account;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.info.AccountInfo;
-import com.google.gerrit.client.ui.AccountScreen;
-import com.google.gerrit.client.ui.InlineHyperlink;
-import com.google.gerrit.client.ui.SmallHeading;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.extensions.client.AccountFieldName;
-import com.google.gwt.i18n.client.LocaleInfo;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.FormPanel;
-import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.HTML;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
-
-public class RegisterScreen extends AccountScreen {
-  private final String nextToken;
-
-  public RegisterScreen(String next) {
-    nextToken = next;
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    display();
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    setPageTitle(Util.C.welcomeToGerritCodeReview());
-
-    final FlowPanel formBody = new FlowPanel();
-
-    final FlowPanel contactGroup = new FlowPanel();
-    contactGroup.setStyleName(Gerrit.RESOURCES.css().registerScreenSection());
-    contactGroup.add(new SmallHeading(Util.C.welcomeReviewContact()));
-    final HTML whereFrom = new HTML(Util.C.welcomeContactFrom());
-    whereFrom.setStyleName(Gerrit.RESOURCES.css().registerScreenExplain());
-    contactGroup.add(whereFrom);
-    contactGroup.add(
-        new ContactPanelShort() {
-          @Override
-          protected void display(AccountInfo account) {
-            super.display(account);
-
-            if ("".equals(nameTxt.getText())) {
-              // No name? Encourage the user to provide us something.
-              //
-              nameTxt.setFocus(true);
-              save.setEnabled(true);
-            }
-          }
-        });
-    formBody.add(contactGroup);
-
-    if (Gerrit.getUserAccount().username() == null
-        && Gerrit.info().auth().canEdit(AccountFieldName.USER_NAME)) {
-      final FlowPanel fp = new FlowPanel();
-      fp.setStyleName(Gerrit.RESOURCES.css().registerScreenSection());
-      fp.add(new SmallHeading(Util.C.welcomeUsernameHeading()));
-
-      final Grid userInfo = new Grid(1, 2);
-      final CellFormatter fmt = userInfo.getCellFormatter();
-      userInfo.setStyleName(Gerrit.RESOURCES.css().infoBlock());
-      userInfo.addStyleName(Gerrit.RESOURCES.css().accountInfoBlock());
-      fp.add(userInfo);
-
-      fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().topmost());
-      fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().topmost());
-      fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().bottomheader());
-
-      UsernameField field = new UsernameField();
-      if (LocaleInfo.getCurrentLocale().isRTL()) {
-        userInfo.setText(0, 1, Util.C.userName());
-        userInfo.setWidget(0, 0, field);
-        fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().header());
-      } else {
-        userInfo.setText(0, 0, Util.C.userName());
-        userInfo.setWidget(0, 1, field);
-        fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().header());
-      }
-
-      formBody.add(fp);
-    }
-
-    if (Gerrit.info().hasSshd()) {
-      final FlowPanel sshKeyGroup = new FlowPanel();
-      sshKeyGroup.setStyleName(Gerrit.RESOURCES.css().registerScreenSection());
-      sshKeyGroup.add(new SmallHeading(Util.C.welcomeSshKeyHeading()));
-      final HTML whySshKey = new HTML(Util.C.welcomeSshKeyText());
-      whySshKey.setStyleName(Gerrit.RESOURCES.css().registerScreenExplain());
-      sshKeyGroup.add(whySshKey);
-      sshKeyGroup.add(
-          new SshPanel() {
-            {
-              setKeyTableVisible(false);
-            }
-          });
-      formBody.add(sshKeyGroup);
-    }
-
-    final FlowPanel choices = new FlowPanel();
-    choices.setStyleName(Gerrit.RESOURCES.css().registerScreenNextLinks());
-    if (Gerrit.info().auth().useContributorAgreements()) {
-      final FlowPanel agreementGroup = new FlowPanel();
-      agreementGroup.setStyleName(Gerrit.RESOURCES.css().registerScreenSection());
-      agreementGroup.add(new SmallHeading(Util.C.welcomeAgreementHeading()));
-      final HTML whyAgreement = new HTML(Util.C.welcomeAgreementText());
-      whyAgreement.setStyleName(Gerrit.RESOURCES.css().registerScreenExplain());
-      agreementGroup.add(whyAgreement);
-
-      choices.add(new InlineHyperlink(Util.C.newAgreement(), PageLinks.SETTINGS_NEW_AGREEMENT));
-      choices.add(new InlineHyperlink(Util.C.welcomeAgreementLater(), nextToken));
-      formBody.add(agreementGroup);
-    } else {
-      choices.add(new InlineHyperlink(Util.C.welcomeContinue(), nextToken));
-    }
-    formBody.add(choices);
-
-    final FormPanel form = new FormPanel();
-    form.add(formBody);
-    add(form);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java
deleted file mode 100644
index a948595..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java
+++ /dev/null
@@ -1,103 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT 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.client.account;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.GerritUiExtensionPoint;
-import com.google.gerrit.client.api.ExtensionPanel;
-import com.google.gerrit.client.api.ExtensionSettingsScreen;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.ui.MenuScreen;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.extensions.client.GitBasicAuthPolicy;
-import java.util.HashSet;
-import java.util.Set;
-
-public abstract class SettingsScreen extends MenuScreen {
-  private final Set<String> allMenuNames;
-  private final Set<String> ambiguousMenuNames;
-
-  public SettingsScreen() {
-    setRequiresSignIn(true);
-
-    allMenuNames = new HashSet<>();
-    ambiguousMenuNames = new HashSet<>();
-
-    linkByGerrit(Util.C.tabAccountSummary(), PageLinks.SETTINGS);
-    linkByGerrit(Util.C.tabPreferences(), PageLinks.SETTINGS_PREFERENCES);
-    linkByGerrit(Util.C.tabDiffPreferences(), PageLinks.SETTINGS_DIFF_PREFERENCES);
-    linkByGerrit(Util.C.tabEditPreferences(), PageLinks.SETTINGS_EDIT_PREFERENCES);
-    linkByGerrit(Util.C.tabWatchedProjects(), PageLinks.SETTINGS_PROJECTS);
-    linkByGerrit(Util.C.tabContactInformation(), PageLinks.SETTINGS_CONTACT);
-    if (Gerrit.info().hasSshd()) {
-      linkByGerrit(Util.C.tabSshKeys(), PageLinks.SETTINGS_SSHKEYS);
-    }
-    if (Gerrit.info().auth().isHttpPasswordSettingsEnabled()) {
-      linkByGerrit(Util.C.tabHttpAccess(), PageLinks.SETTINGS_HTTP_PASSWORD);
-    }
-    if (Gerrit.info().auth().isOAuth()
-        && Gerrit.info().auth().gitBasicAuthPolicy() == GitBasicAuthPolicy.OAUTH) {
-      linkByGerrit(Util.C.tabOAuthToken(), PageLinks.SETTINGS_OAUTH_TOKEN);
-    }
-    if (Gerrit.info().gerrit().editGpgKeys()) {
-      linkByGerrit(Util.C.tabGpgKeys(), PageLinks.SETTINGS_GPGKEYS);
-    }
-    linkByGerrit(Util.C.tabWebIdentities(), PageLinks.SETTINGS_WEBIDENT);
-    linkByGerrit(Util.C.tabMyGroups(), PageLinks.SETTINGS_MYGROUPS);
-    if (Gerrit.info().auth().useContributorAgreements()) {
-      linkByGerrit(Util.C.tabAgreements(), PageLinks.SETTINGS_AGREEMENTS);
-    }
-
-    for (String pluginName : ExtensionSettingsScreen.Definition.plugins()) {
-      for (ExtensionSettingsScreen.Definition def :
-          Natives.asList(ExtensionSettingsScreen.Definition.get(pluginName))) {
-        if (!allMenuNames.add(def.getMenu())) {
-          ambiguousMenuNames.add(def.getMenu());
-        }
-      }
-    }
-
-    for (String pluginName : ExtensionSettingsScreen.Definition.plugins()) {
-      for (ExtensionSettingsScreen.Definition def :
-          Natives.asList(ExtensionSettingsScreen.Definition.get(pluginName))) {
-        linkByPlugin(pluginName, def.getMenu(), PageLinks.toSettings(pluginName, def.getPath()));
-      }
-    }
-  }
-
-  private void linkByGerrit(String text, String target) {
-    allMenuNames.add(text);
-    link(text, target);
-  }
-
-  private void linkByPlugin(String pluginName, String text, String target) {
-    if (ambiguousMenuNames.contains(text)) {
-      text += " (" + pluginName + ")";
-    }
-    link(text, target);
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    setPageTitle(Util.C.settingsHeading());
-  }
-
-  protected ExtensionPanel createExtensionPoint(GerritUiExtensionPoint extensionPoint) {
-    ExtensionPanel extensionPanel = new ExtensionPanel(extensionPoint);
-    extensionPanel.putObject(GerritUiExtensionPoint.Key.ACCOUNT_INFO, Gerrit.getUserAccount());
-    return extensionPanel;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshHostKeyPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshHostKeyPanel.java
deleted file mode 100644
index 2dfc2ed..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshHostKeyPanel.java
+++ /dev/null
@@ -1,52 +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.client.account;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.ui.SmallHeading;
-import com.google.gerrit.common.data.SshHostKey;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.HTML;
-import com.google.gwt.user.client.ui.Label;
-import com.google.gwtexpui.clippy.client.CopyableLabel;
-
-class SshHostKeyPanel extends Composite {
-  SshHostKeyPanel(SshHostKey info) {
-    final FlowPanel body = new FlowPanel();
-    body.setStyleName(Gerrit.RESOURCES.css().sshHostKeyPanel());
-    body.add(new SmallHeading(Util.C.sshHostKeyTitle()));
-    {
-      final Label fpLbl = new Label(Util.C.sshHostKeyFingerprint());
-      fpLbl.setStyleName(Gerrit.RESOURCES.css().sshHostKeyPanelHeading());
-      body.add(fpLbl);
-      final Label fpVal = new Label(info.getFingerprint());
-      fpVal.setStyleName(Gerrit.RESOURCES.css().sshHostKeyPanelFingerprintData());
-      body.add(fpVal);
-    }
-    {
-      final HTML hdr = new HTML(Util.C.sshHostKeyKnownHostEntry());
-      hdr.setStyleName(Gerrit.RESOURCES.css().sshHostKeyPanelHeading());
-      body.add(hdr);
-
-      final CopyableLabel lbl;
-      lbl = new CopyableLabel(info.getHostIdent() + " " + info.getHostKey());
-      lbl.setPreviewText(SshPanel.elide(lbl.getText(), 80));
-      lbl.addStyleName(Gerrit.RESOURCES.css().sshHostKeyPanelKnownHostEntry());
-      body.add(lbl);
-    }
-    initWidget(body);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshKeyInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshKeyInfo.java
deleted file mode 100644
index 23b3d2d..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshKeyInfo.java
+++ /dev/null
@@ -1,33 +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.client.account;
-
-import com.google.gwt.core.client.JavaScriptObject;
-
-public class SshKeyInfo extends JavaScriptObject {
-  public final native int seq() /*-{ return this.seq || 0; }-*/;
-
-  public final native String sshPublicKey() /*-{ return this.ssh_public_key; }-*/;
-
-  public final native String encodedKey() /*-{ return this.encoded_key; }-*/;
-
-  public final native String algorithm() /*-{ return this.algorithm; }-*/;
-
-  public final native String comment() /*-{ return this.comment; }-*/;
-
-  public final native boolean isValid() /*-{ return this['valid'] ? true : false; }-*/;
-
-  protected SshKeyInfo() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshPanel.java
deleted file mode 100644
index 6a8b44d..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshPanel.java
+++ /dev/null
@@ -1,387 +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.client.account;
-
-import com.google.gerrit.client.ErrorDialog;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.VoidResult;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.ui.ComplexDisclosurePanel;
-import com.google.gerrit.client.ui.FancyFlexTable;
-import com.google.gerrit.client.ui.SmallHeading;
-import com.google.gerrit.common.data.SshHostKey;
-import com.google.gerrit.common.errors.InvalidSshKeyException;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.logical.shared.ValueChangeHandler;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.HTML;
-import com.google.gwt.user.client.ui.HasHorizontalAlignment;
-import com.google.gwt.user.client.ui.HorizontalPanel;
-import com.google.gwt.user.client.ui.Panel;
-import com.google.gwt.user.client.ui.VerticalPanel;
-import com.google.gwtexpui.clippy.client.CopyableLabel;
-import com.google.gwtexpui.globalkey.client.NpTextArea;
-import com.google.gwtjsonrpc.client.RemoteJsonException;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-
-class SshPanel extends Composite {
-  private SshKeyTable keys;
-
-  private Button showAddKeyBlock;
-  private Panel addKeyBlock;
-  private Button closeAddKeyBlock;
-  private Button clearNew;
-  private Button addNew;
-  private NpTextArea addTxt;
-  private Button deleteKey;
-
-  private Panel serverKeys;
-
-  private int loadCount;
-
-  SshPanel() {
-    final FlowPanel body = new FlowPanel();
-
-    showAddKeyBlock = new Button(Util.C.buttonShowAddSshKey());
-    showAddKeyBlock.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            showAddKeyBlock(true);
-          }
-        });
-
-    keys = new SshKeyTable();
-    body.add(keys);
-    {
-      final FlowPanel fp = new FlowPanel();
-      deleteKey = new Button(Util.C.buttonDeleteSshKey());
-      deleteKey.setEnabled(false);
-      deleteKey.addClickHandler(
-          new ClickHandler() {
-            @Override
-            public void onClick(ClickEvent event) {
-              keys.deleteChecked();
-            }
-          });
-      fp.add(deleteKey);
-      fp.add(showAddKeyBlock);
-      body.add(fp);
-    }
-
-    addKeyBlock = new VerticalPanel();
-    addKeyBlock.setVisible(false);
-    addKeyBlock.setStyleName(Gerrit.RESOURCES.css().addSshKeyPanel());
-    addKeyBlock.add(new SmallHeading(Util.C.addSshKeyPanelHeader()));
-
-    final ComplexDisclosurePanel addSshKeyHelp =
-        new ComplexDisclosurePanel(Util.C.addSshKeyHelpTitle(), false);
-    addSshKeyHelp.setContent(new HTML(Util.C.addSshKeyHelp()));
-    addKeyBlock.add(addSshKeyHelp);
-
-    addTxt = new NpTextArea();
-    addTxt.setVisibleLines(12);
-    addTxt.setCharacterWidth(80);
-    addTxt.setSpellCheck(false);
-    addKeyBlock.add(addTxt);
-
-    final HorizontalPanel buttons = new HorizontalPanel();
-    addKeyBlock.add(buttons);
-
-    clearNew = new Button(Util.C.buttonClearSshKeyInput());
-    clearNew.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            addTxt.setText("");
-            addTxt.setFocus(true);
-          }
-        });
-    buttons.add(clearNew);
-
-    addNew = new Button(Util.C.buttonAddSshKey());
-    addNew.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            doAddNew();
-          }
-        });
-    buttons.add(addNew);
-
-    closeAddKeyBlock = new Button(Util.C.buttonCloseAddSshKey());
-    closeAddKeyBlock.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            showAddKeyBlock(false);
-          }
-        });
-    buttons.add(closeAddKeyBlock);
-    buttons.setCellWidth(closeAddKeyBlock, "100%");
-    buttons.setCellHorizontalAlignment(closeAddKeyBlock, HasHorizontalAlignment.ALIGN_RIGHT);
-
-    body.add(addKeyBlock);
-
-    serverKeys = new FlowPanel();
-    body.add(serverKeys);
-
-    initWidget(body);
-  }
-
-  void setKeyTableVisible(boolean on) {
-    keys.setVisible(on);
-    deleteKey.setVisible(on);
-    closeAddKeyBlock.setVisible(on);
-  }
-
-  void doAddNew() {
-    final String txt = addTxt.getText();
-    if (txt != null && txt.length() > 0) {
-      addNew.setEnabled(false);
-      AccountApi.addSshKey(
-          "self",
-          txt,
-          new GerritCallback<SshKeyInfo>() {
-            @Override
-            public void onSuccess(SshKeyInfo k) {
-              addNew.setEnabled(true);
-              addTxt.setText("");
-              keys.addOneKey(k);
-              if (!keys.isVisible()) {
-                showAddKeyBlock(false);
-                setKeyTableVisible(true);
-                keys.updateDeleteButton();
-              }
-            }
-
-            @Override
-            public void onFailure(Throwable caught) {
-              addNew.setEnabled(true);
-
-              if (isInvalidSshKey(caught)) {
-                new ErrorDialog(Util.C.invalidSshKeyError()).center();
-
-              } else {
-                super.onFailure(caught);
-              }
-            }
-
-            private boolean isInvalidSshKey(Throwable caught) {
-              if (caught instanceof InvalidSshKeyException) {
-                return true;
-              }
-              return caught instanceof RemoteJsonException
-                  && InvalidSshKeyException.MESSAGE.equals(caught.getMessage());
-            }
-          });
-    }
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    refreshSshKeys();
-    Gerrit.SYSTEM_SVC.daemonHostKeys(
-        new GerritCallback<List<SshHostKey>>() {
-          @Override
-          public void onSuccess(List<SshHostKey> result) {
-            serverKeys.clear();
-            for (SshHostKey keyInfo : result) {
-              serverKeys.add(new SshHostKeyPanel(keyInfo));
-            }
-            if (++loadCount == 2) {
-              display();
-            }
-          }
-        });
-  }
-
-  private void refreshSshKeys() {
-    AccountApi.getSshKeys(
-        "self",
-        new GerritCallback<JsArray<SshKeyInfo>>() {
-          @Override
-          public void onSuccess(JsArray<SshKeyInfo> result) {
-            keys.display(Natives.asList(result));
-            if (result.length() == 0 && keys.isVisible()) {
-              showAddKeyBlock(true);
-            }
-            if (++loadCount == 2) {
-              display();
-            }
-          }
-        });
-  }
-
-  void display() {}
-
-  private void showAddKeyBlock(boolean show) {
-    showAddKeyBlock.setVisible(!show);
-    addKeyBlock.setVisible(show);
-  }
-
-  private class SshKeyTable extends FancyFlexTable<SshKeyInfo> {
-    private ValueChangeHandler<Boolean> updateDeleteHandler;
-
-    SshKeyTable() {
-      table.setWidth("");
-      table.setText(0, 2, Util.C.sshKeyStatus());
-      table.setText(0, 3, Util.C.sshKeyAlgorithm());
-      table.setText(0, 4, Util.C.sshKeyKey());
-      table.setText(0, 5, Util.C.sshKeyComment());
-
-      final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().iconHeader());
-      fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
-      fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader());
-      fmt.addStyleName(0, 4, Gerrit.RESOURCES.css().dataHeader());
-      fmt.addStyleName(0, 5, Gerrit.RESOURCES.css().dataHeader());
-
-      updateDeleteHandler =
-          new ValueChangeHandler<Boolean>() {
-            @Override
-            public void onValueChange(ValueChangeEvent<Boolean> event) {
-              updateDeleteButton();
-            }
-          };
-    }
-
-    void deleteChecked() {
-      final HashSet<Integer> sequenceNumbers = new HashSet<>();
-      for (int row = 1; row < table.getRowCount(); row++) {
-        final SshKeyInfo k = getRowItem(row);
-        if (k != null && ((CheckBox) table.getWidget(row, 1)).getValue()) {
-          sequenceNumbers.add(k.seq());
-        }
-      }
-      if (sequenceNumbers.isEmpty()) {
-        updateDeleteButton();
-      } else {
-        deleteKey.setEnabled(false);
-        AccountApi.deleteSshKeys(
-            "self",
-            sequenceNumbers,
-            new GerritCallback<VoidResult>() {
-              @Override
-              public void onSuccess(VoidResult result) {
-                for (int row = 1; row < table.getRowCount(); ) {
-                  final SshKeyInfo k = getRowItem(row);
-                  if (k != null && sequenceNumbers.contains(k.seq())) {
-                    table.removeRow(row);
-                  } else {
-                    row++;
-                  }
-                }
-                if (table.getRowCount() == 1) {
-                  display(Collections.<SshKeyInfo>emptyList());
-                } else {
-                  updateDeleteButton();
-                }
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {
-                refreshSshKeys();
-                updateDeleteButton();
-                super.onFailure(caught);
-              }
-            });
-      }
-    }
-
-    void display(List<SshKeyInfo> result) {
-      if (result.isEmpty()) {
-        setKeyTableVisible(false);
-        showAddKeyBlock(true);
-      } else {
-        while (1 < table.getRowCount()) {
-          table.removeRow(table.getRowCount() - 1);
-        }
-        for (SshKeyInfo k : result) {
-          addOneKey(k);
-        }
-        setKeyTableVisible(true);
-        deleteKey.setEnabled(false);
-      }
-    }
-
-    void addOneKey(SshKeyInfo k) {
-      final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      final int row = table.getRowCount();
-      table.insertRow(row);
-      applyDataRowStyle(row);
-
-      final CheckBox sel = new CheckBox();
-      sel.addValueChangeHandler(updateDeleteHandler);
-
-      table.setWidget(row, 1, sel);
-      if (k.isValid()) {
-        table.setText(row, 2, "");
-        fmt.removeStyleName(
-            row,
-            2, //
-            Gerrit.RESOURCES.css().sshKeyPanelInvalid());
-      } else {
-        table.setText(row, 2, Util.C.sshKeyInvalid());
-        fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().sshKeyPanelInvalid());
-      }
-      table.setText(row, 3, k.algorithm());
-
-      CopyableLabel keyLabel = new CopyableLabel(k.sshPublicKey());
-      keyLabel.setPreviewText(elide(k.encodedKey(), 40));
-      table.setWidget(row, 4, keyLabel);
-
-      table.setText(row, 5, k.comment());
-
-      fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().iconCell());
-      fmt.addStyleName(row, 4, Gerrit.RESOURCES.css().sshKeyPanelEncodedKey());
-      for (int c = 2; c <= 5; c++) {
-        fmt.addStyleName(row, c, Gerrit.RESOURCES.css().dataCell());
-      }
-
-      setRowItem(row, k);
-    }
-
-    void updateDeleteButton() {
-      boolean on = false;
-      for (int row = 1; row < table.getRowCount(); row++) {
-        CheckBox sel = (CheckBox) table.getWidget(row, 1);
-        if (sel.getValue()) {
-          on = true;
-          break;
-        }
-      }
-      deleteKey.setEnabled(on);
-    }
-  }
-
-  static String elide(String s, int len) {
-    if (s == null || s.length() < len || len <= 10) {
-      return s;
-    }
-    return s.substring(0, len - 10) + "..." + s.substring(s.length() - 10);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java
deleted file mode 100644
index 4fdd067..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java
+++ /dev/null
@@ -1,204 +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.client.account;
-
-import com.google.gerrit.client.ConfirmationCallback;
-import com.google.gerrit.client.ConfirmationDialog;
-import com.google.gerrit.client.ErrorDialog;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.NativeString;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.client.ui.OnEditEnabler;
-import com.google.gerrit.extensions.client.AccountFieldName;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.TextBox;
-import com.google.gwtexpui.clippy.client.CopyableLabel;
-import com.google.gwtexpui.globalkey.client.NpTextBox;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-
-class UsernameField extends Composite {
-  // If these regular expressions are modified the same modifications should be done to the
-  // corresponding regular expressions in the
-  // com.google.gerrit.server.account.externalids.ExternalId class.
-  private static final String USER_NAME_PATTERN_FIRST_REGEX = "[a-zA-Z0-9]";
-  private static final String USER_NAME_PATTERN_REST_REGEX = "[a-zA-Z0-9.!#$%&’*+=?^_`\\{|\\}~@-]";
-
-  private CopyableLabel userNameLbl;
-  private NpTextBox userNameTxt;
-  private Button setUserName;
-
-  UsernameField() {
-    String user = Gerrit.getUserAccount().username();
-    userNameLbl = new CopyableLabel(user != null ? user : "");
-    userNameLbl.setStyleName(Gerrit.RESOURCES.css().accountUsername());
-
-    if (user != null || !canEditUserName()) {
-      initWidget(userNameLbl);
-
-    } else {
-      final FlowPanel body = new FlowPanel();
-      initWidget(body);
-      setStyleName(Gerrit.RESOURCES.css().usernameField());
-
-      userNameTxt = new NpTextBox();
-      userNameTxt.addKeyPressHandler(new UserNameValidator());
-      userNameTxt.addStyleName(Gerrit.RESOURCES.css().accountUsername());
-      userNameTxt.setVisibleLength(16);
-      userNameTxt.addKeyPressHandler(
-          new KeyPressHandler() {
-            @Override
-            public void onKeyPress(KeyPressEvent event) {
-              if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
-                confirmSetUserName();
-              }
-            }
-          });
-
-      setUserName = new Button(Util.C.buttonSetUserName());
-      setUserName.setVisible(canEditUserName());
-      setUserName.setEnabled(false);
-      setUserName.addClickHandler(
-          new ClickHandler() {
-            @Override
-            public void onClick(ClickEvent event) {
-              confirmSetUserName();
-            }
-          });
-      new OnEditEnabler(setUserName, userNameTxt);
-
-      userNameLbl.setVisible(false);
-      body.add(userNameLbl);
-      body.add(userNameTxt);
-      body.add(setUserName);
-    }
-  }
-
-  private boolean canEditUserName() {
-    return Gerrit.info().auth().canEdit(AccountFieldName.USER_NAME);
-  }
-
-  private void confirmSetUserName() {
-    new ConfirmationDialog(
-            Util.C.confirmSetUserNameTitle(),
-            new SafeHtmlBuilder().append(Util.C.confirmSetUserName()),
-            new ConfirmationCallback() {
-              @Override
-              public void onOk() {
-                doSetUserName();
-              }
-            })
-        .center();
-  }
-
-  private void doSetUserName() {
-    if (!canEditUserName()) {
-      return;
-    }
-
-    enableUI(false);
-
-    String newName = userNameTxt.getText();
-    if ("".equals(newName)) {
-      newName = null;
-    }
-    final String newUserName = newName;
-
-    AccountApi.setUsername(
-        "self",
-        newUserName,
-        new GerritCallback<NativeString>() {
-          @Override
-          public void onSuccess(NativeString result) {
-            Gerrit.getUserAccount().username(newUserName);
-            userNameLbl.setText(newUserName);
-            userNameLbl.setVisible(true);
-            userNameTxt.setVisible(false);
-            setUserName.setVisible(false);
-          }
-
-          @Override
-          public void onFailure(Throwable caught) {
-            enableUI(true);
-            if (RestApi.isExpected(422 /* Unprocessable Entity */)) {
-              new ErrorDialog(Util.C.invalidUserName()).center();
-            } else {
-              super.onFailure(caught);
-            }
-          }
-        });
-  }
-
-  private void enableUI(boolean on) {
-    userNameTxt.setEnabled(on);
-    setUserName.setEnabled(on);
-  }
-
-  private static final class UserNameValidator implements KeyPressHandler {
-    @Override
-    public void onKeyPress(KeyPressEvent event) {
-      final char code = event.getCharCode();
-      final int nativeCode = event.getNativeEvent().getKeyCode();
-      switch (nativeCode) {
-        case KeyCodes.KEY_ALT:
-        case KeyCodes.KEY_BACKSPACE:
-        case KeyCodes.KEY_CTRL:
-        case KeyCodes.KEY_DELETE:
-        case KeyCodes.KEY_DOWN:
-        case KeyCodes.KEY_END:
-        case KeyCodes.KEY_ENTER:
-        case KeyCodes.KEY_ESCAPE:
-        case KeyCodes.KEY_HOME:
-        case KeyCodes.KEY_LEFT:
-        case KeyCodes.KEY_PAGEDOWN:
-        case KeyCodes.KEY_PAGEUP:
-        case KeyCodes.KEY_RIGHT:
-        case KeyCodes.KEY_SHIFT:
-        case KeyCodes.KEY_TAB:
-        case KeyCodes.KEY_UP:
-          // Allow these, even if one of their assigned codes is
-          // identical to an ASCII character we do not want to
-          // allow in the box.
-          //
-          // We still want to let the user move around the input box
-          // with their arrow keys, or to move between fields using tab.
-          // Invalid characters introduced will be caught through the
-          // server's own validation of the input data.
-          //
-          break;
-
-        default:
-          final TextBox box = (TextBox) event.getSource();
-          final String re;
-          if (box.getCursorPos() == 0) {
-            re = USER_NAME_PATTERN_FIRST_REGEX;
-          } else {
-            re = USER_NAME_PATTERN_REST_REGEX;
-          }
-          if (!String.valueOf(code).matches("^" + re + "$")) {
-            event.preventDefault();
-            event.stopPropagation();
-          }
-      }
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Util.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Util.java
deleted file mode 100644
index 1c4870c..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Util.java
+++ /dev/null
@@ -1,30 +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.client.account;
-
-import com.google.gerrit.common.data.ProjectAdminService;
-import com.google.gwt.core.client.GWT;
-import com.google.gwtjsonrpc.client.JsonUtil;
-
-public class Util {
-  public static final AccountConstants C = GWT.create(AccountConstants.class);
-  public static final AccountMessages M = GWT.create(AccountMessages.class);
-  public static final ProjectAdminService PROJECT_SVC;
-
-  static {
-    PROJECT_SVC = GWT.create(ProjectAdminService.class);
-    JsonUtil.bind(PROJECT_SVC, "rpc/ProjectAdminService");
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ValidateEmailScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ValidateEmailScreen.java
deleted file mode 100644
index b66f108..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ValidateEmailScreen.java
+++ /dev/null
@@ -1,52 +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.client.account;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.VoidResult;
-import com.google.gerrit.client.config.ConfigServerApi;
-import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.client.ui.AccountScreen;
-import com.google.gerrit.common.PageLinks;
-
-public class ValidateEmailScreen extends AccountScreen {
-  private final String magicToken;
-
-  public ValidateEmailScreen(String magicToken) {
-    this.magicToken = magicToken;
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    setPageTitle(Util.C.settingsHeading());
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    ConfigServerApi.confirmEmail(
-        magicToken,
-        new ScreenLoadCallback<VoidResult>(this) {
-          @Override
-          protected void preDisplay(VoidResult result) {}
-
-          @Override
-          protected void postDisplay() {
-            Gerrit.display(PageLinks.SETTINGS_CONTACT);
-          }
-        });
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/actions/ActionButton.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/actions/ActionButton.java
deleted file mode 100644
index 85937db..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/actions/ActionButton.java
+++ /dev/null
@@ -1,118 +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.client.actions;
-
-import com.google.gerrit.client.api.ActionContext;
-import com.google.gerrit.client.api.ChangeGlue;
-import com.google.gerrit.client.api.EditGlue;
-import com.google.gerrit.client.api.ProjectGlue;
-import com.google.gerrit.client.api.RevisionGlue;
-import com.google.gerrit.client.info.ActionInfo;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.info.ChangeInfo.EditInfo;
-import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
-import com.google.gerrit.client.projects.BranchInfo;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-
-public class ActionButton extends Button implements ClickHandler {
-  private final Project.NameKey project;
-  private final BranchInfo branch;
-  private final ChangeInfo change;
-  private final EditInfo edit;
-  private final RevisionInfo revision;
-  private final ActionInfo action;
-  private ActionContext ctx;
-
-  public ActionButton(Project.NameKey project, ActionInfo action) {
-    this(project, null, null, null, null, action);
-  }
-
-  public ActionButton(Project.NameKey project, BranchInfo branch, ActionInfo action) {
-    this(project, branch, null, null, null, action);
-  }
-
-  public ActionButton(ChangeInfo change, ActionInfo action) {
-    this(null, null, change, null, null, action);
-  }
-
-  public ActionButton(ChangeInfo change, RevisionInfo revision, ActionInfo action) {
-    this(null, null, change, null, revision, action);
-  }
-
-  private ActionButton(
-      Project.NameKey project,
-      BranchInfo branch,
-      ChangeInfo change,
-      EditInfo edit,
-      RevisionInfo revision,
-      ActionInfo action) {
-    super(new SafeHtmlBuilder().openDiv().append(action.label()).closeDiv());
-    setStyleName("");
-    setTitle(action.title());
-    setEnabled(action.enabled());
-    addClickHandler(this);
-
-    this.project = project;
-    this.branch = branch;
-    this.change = change;
-    this.edit = edit;
-    this.revision = revision;
-    this.action = action;
-  }
-
-  @Override
-  public void onClick(ClickEvent event) {
-    if (ctx != null && ctx.has_popup()) {
-      ctx.hide();
-      ctx = null;
-      return;
-    }
-
-    if (revision != null) {
-      RevisionGlue.onAction(change, revision, action, this);
-    } else if (edit != null) {
-      EditGlue.onAction(change, edit, action, this);
-    } else if (change != null) {
-      ChangeGlue.onAction(change, action, this);
-    } else if (branch != null) {
-      ProjectGlue.onAction(project, branch, action, this);
-    } else if (project != null) {
-      ProjectGlue.onAction(project, action, this);
-    }
-  }
-
-  @Override
-  public void onUnload() {
-    if (ctx != null) {
-      if (ctx.has_popup()) {
-        ctx.hide();
-      }
-      ctx = null;
-    }
-    super.onUnload();
-  }
-
-  public void link(ActionContext ctx) {
-    this.ctx = ctx;
-  }
-
-  public void unlink() {
-    ctx = null;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
deleted file mode 100644
index 7bd8b82..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
+++ /dev/null
@@ -1,295 +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.client.admin;
-
-import static java.util.stream.Collectors.toCollection;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.common.data.ProjectAccess;
-import com.google.gerrit.common.data.RefConfigSection;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.core.client.Scheduler;
-import com.google.gwt.core.client.Scheduler.ScheduledCommand;
-import com.google.gwt.dom.client.DivElement;
-import com.google.gwt.dom.client.SpanElement;
-import com.google.gwt.dom.client.Style.Display;
-import com.google.gwt.editor.client.Editor;
-import com.google.gwt.editor.client.EditorDelegate;
-import com.google.gwt.editor.client.ValueAwareEditor;
-import com.google.gwt.editor.client.adapters.EditorSource;
-import com.google.gwt.editor.client.adapters.ListEditor;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.MouseOutEvent;
-import com.google.gwt.event.dom.client.MouseOverEvent;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.logical.shared.ValueChangeHandler;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.uibinder.client.UiHandler;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwt.user.client.ui.ValueListBox;
-import java.util.ArrayList;
-import java.util.List;
-
-public class AccessSectionEditor extends Composite
-    implements Editor<AccessSection>, ValueAwareEditor<AccessSection> {
-  interface Binder extends UiBinder<HTMLPanel, AccessSectionEditor> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  @UiField ValueEditor<String> name;
-
-  @UiField FlowPanel permissionContainer;
-  ListEditor<Permission, PermissionEditor> permissions;
-
-  @UiField DivElement addContainer;
-
-  @UiField(provided = true)
-  @Editor.Ignore
-  ValueListBox<String> permissionSelector;
-
-  @UiField SpanElement deletedName;
-
-  @UiField Anchor deleteSection;
-
-  @UiField DivElement normal;
-  @UiField DivElement deleted;
-
-  @UiField SpanElement sectionType;
-  @UiField SpanElement sectionName;
-
-  private final ProjectAccess projectAccess;
-  private AccessSection value;
-  private boolean editing;
-  private boolean readOnly;
-  private boolean isDeleted;
-
-  public AccessSectionEditor(ProjectAccess access) {
-    projectAccess = access;
-    permissionSelector = new ValueListBox<>(new PermissionNameRenderer(access.getCapabilities()));
-    permissionSelector.addValueChangeHandler(
-        new ValueChangeHandler<String>() {
-          @Override
-          public void onValueChange(ValueChangeEvent<String> event) {
-            if (!AdminConstants.I.addPermission().equals(event.getValue())) {
-              onAddPermission(event.getValue());
-            }
-          }
-        });
-
-    initWidget(uiBinder.createAndBindUi(this));
-    permissions = ListEditor.of(new PermissionEditorSource());
-  }
-
-  @UiHandler("deleteSection")
-  void onDeleteHover(@SuppressWarnings("unused") MouseOverEvent event) {
-    normal.addClassName(AdminResources.I.css().deleteSectionHover());
-  }
-
-  @UiHandler("deleteSection")
-  void onDeleteNonHover(@SuppressWarnings("unused") MouseOutEvent event) {
-    normal.removeClassName(AdminResources.I.css().deleteSectionHover());
-  }
-
-  @UiHandler("deleteSection")
-  void onDeleteSection(@SuppressWarnings("unused") ClickEvent event) {
-    isDeleted = true;
-
-    if (name.isVisible() && RefConfigSection.isValid(name.getValue())) {
-      deletedName.setInnerText(AdminMessages.I.deletedReference(name.getValue()));
-    } else {
-      String name = AdminConstants.I.sectionNames().get(value.getName());
-      if (name == null) {
-        name = value.getName();
-      }
-      deletedName.setInnerText(AdminMessages.I.deletedSection(name));
-    }
-
-    normal.getStyle().setDisplay(Display.NONE);
-    deleted.getStyle().setDisplay(Display.BLOCK);
-  }
-
-  @UiHandler("undoDelete")
-  void onUndoDelete(@SuppressWarnings("unused") ClickEvent event) {
-    isDeleted = false;
-    deleted.getStyle().setDisplay(Display.NONE);
-    normal.getStyle().setDisplay(Display.BLOCK);
-  }
-
-  void onAddPermission(String varName) {
-    int idx = permissions.getList().size();
-
-    Permission p = value.getPermission(varName, true);
-    permissions.getList().add(p);
-
-    PermissionEditor e = permissions.getEditors().get(idx);
-    e.beginAddRule();
-
-    rebuildPermissionSelector();
-  }
-
-  void editRefPattern() {
-    name.edit();
-    Scheduler.get()
-        .scheduleDeferred(
-            new ScheduledCommand() {
-              @Override
-              public void execute() {
-                name.setFocus(true);
-              }
-            });
-  }
-
-  void enableEditing() {
-    readOnly = false;
-    addContainer.getStyle().setDisplay(Display.BLOCK);
-    rebuildPermissionSelector();
-  }
-
-  boolean isDeleted() {
-    return isDeleted;
-  }
-
-  @Override
-  public void setValue(AccessSection value) {
-    sortPermissions(value);
-
-    this.value = value;
-    this.readOnly = !editing || !(projectAccess.isOwnerOf(value) || projectAccess.canUpload());
-
-    name.setEnabled(!readOnly);
-    deleteSection.setVisible(!readOnly);
-
-    if (RefConfigSection.isValid(value.getName())) {
-      name.setVisible(true);
-      name.setIgnoreEditorValue(false);
-      sectionType.setInnerText(AdminConstants.I.sectionTypeReference());
-
-    } else {
-      name.setVisible(false);
-      name.setIgnoreEditorValue(true);
-
-      String name = AdminConstants.I.sectionNames().get(value.getName());
-      if (name != null) {
-        sectionType.setInnerText(name);
-        sectionName.getStyle().setDisplay(Display.NONE);
-      } else {
-        sectionType.setInnerText(AdminConstants.I.sectionTypeSection());
-        sectionName.setInnerText(value.getName());
-        sectionName.getStyle().clearDisplay();
-      }
-    }
-
-    if (readOnly) {
-      addContainer.getStyle().setDisplay(Display.NONE);
-    } else {
-      enableEditing();
-    }
-  }
-
-  private void sortPermissions(AccessSection accessSection) {
-    accessSection.setPermissions(
-        accessSection.getPermissions().stream().sorted().collect(toCollection(ArrayList::new)));
-  }
-
-  void setEditing(boolean editing) {
-    this.editing = editing;
-  }
-
-  private void rebuildPermissionSelector() {
-    List<String> perms = new ArrayList<>();
-
-    if (AccessSection.GLOBAL_CAPABILITIES.equals(value.getName())) {
-      for (String varName : projectAccess.getCapabilities().keySet()) {
-        addPermission(varName, perms);
-      }
-    } else if (RefConfigSection.isValid(value.getName())) {
-      for (LabelType t : projectAccess.getLabelTypes().getLabelTypes()) {
-        addPermission(Permission.forLabel(t.getName()), perms);
-      }
-      for (LabelType t : projectAccess.getLabelTypes().getLabelTypes()) {
-        addPermission(Permission.forLabelAs(t.getName()), perms);
-      }
-      for (String varName : AdminConstants.I.permissionNames().keySet()) {
-        addPermission(varName, perms);
-      }
-    }
-    if (perms.isEmpty()) {
-      addContainer.getStyle().setDisplay(Display.NONE);
-    } else {
-      addContainer.getStyle().setDisplay(Display.BLOCK);
-      perms.add(0, AdminConstants.I.addPermission());
-      permissionSelector.setValue(AdminConstants.I.addPermission());
-      permissionSelector.setAcceptableValues(perms);
-    }
-  }
-
-  private void addPermission(String permissionName, List<String> permissionList) {
-    if (value.getPermission(permissionName) != null) {
-      return;
-    }
-    if (Gerrit.info().gerrit().isAllProjects(projectAccess.getProjectName())
-        && !Permission.canBeOnAllProjects(value.getName(), permissionName)) {
-      return;
-    }
-    permissionList.add(permissionName);
-  }
-
-  @Override
-  public void flush() {
-    List<Permission> src = permissions.getList();
-    List<Permission> keep = new ArrayList<>(src.size());
-
-    for (int i = 0; i < src.size(); i++) {
-      PermissionEditor e = (PermissionEditor) permissionContainer.getWidget(i);
-      if (!e.isDeleted()) {
-        keep.add(src.get(i));
-      }
-    }
-    value.setPermissions(keep);
-  }
-
-  @Override
-  public void onPropertyChange(String... paths) {}
-
-  @Override
-  public void setDelegate(EditorDelegate<AccessSection> delegate) {}
-
-  private class PermissionEditorSource extends EditorSource<PermissionEditor> {
-    @Override
-    public PermissionEditor create(int index) {
-      PermissionEditor subEditor =
-          new PermissionEditor(projectAccess, readOnly, value, projectAccess.getLabelTypes());
-      permissionContainer.insert(subEditor, index);
-      return subEditor;
-    }
-
-    @Override
-    public void dispose(PermissionEditor subEditor) {
-      subEditor.removeFromParent();
-    }
-
-    @Override
-    public void setIndex(PermissionEditor subEditor, int index) {
-      permissionContainer.insert(subEditor, index);
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.ui.xml
deleted file mode 100644
index 3710265..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.ui.xml
+++ /dev/null
@@ -1,157 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder
-  xmlns:ui='urn:ui:com.google.gwt.uibinder'
-  xmlns:g='urn:import:com.google.gwt.user.client.ui'
-  xmlns:e='urn:import:com.google.gwt.editor.ui.client'
-  xmlns:my='urn:import:com.google.gerrit.client.admin'
-  ui:generateFormat='com.google.gwt.i18n.rebind.format.PropertiesFormat'
-  ui:generateKeys='com.google.gwt.i18n.rebind.keygen.MD5KeyGenerator'
-  ui:generateLocales='default,en'
-  >
-<ui:with field='res' type='com.google.gerrit.client.admin.AdminResources'/>
-<ui:style gss='false'>
-  @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
-  @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
-
-  .panel {
-    width: 50em;
-    position: relative;
-  }
-
-  .content {
-    margin-top: 4px;
-    margin-bottom: 4px;
-    padding-bottom: 2px;
-  }
-
-  .normal {
-    background-color: trimColor;
-  }
-
-  .deleted {
-    padding-left: 7px;
-    padding-bottom: 2px;
-  }
-
-  .header {
-    padding-left: 5px;
-    padding-right: 5px;
-  }
-  .headerText {
-    vertical-align: top;
-    white-space: nowrap;
-    font-weight: bold;
-  }
-  .headerTable {
-    border: 0;
-    width: 100%;
-    padding-right: 40px;
-  }
-
-  .header:hover {
-    background-color: selectionColor;
-  }
-
-  .name {
-    width: 100%;
-  }
-  .nameEdit {
-    width: 100%;
-  }
-
-  .permissionList {
-    margin-left: 5px;
-    margin-right: 5px;
-    margin-bottom: 5px;
-  }
-
-  .addContainer {
-    padding-left: 16px;
-    padding-right: 16px;
-    font-size: 80%;
-  }
-  .addContainer:hover {
-    background-color: selectionColor;
-  }
-
-  .deleteIcon {
-    position: absolute;
-    top: 5px;
-    right: 17px;
-  }
-
-  .undoIcon {
-    position: absolute;
-    top: 2px;
-    right: 17px;
-  }
-</ui:style>
-
-<g:HTMLPanel styleName='{style.panel}'>
-<div ui:field='normal' class='{style.normal} {style.content}'>
-  <div class='{style.header}'>
-    <table class='{style.headerTable}'><tr>
-      <td class='{style.headerText}'>
-        <span ui:field='sectionType'/>
-      </td>
-      <td width='100%'>
-        <my:ValueEditor
-            ui:field='name'
-            addStyleNames='{style.name}'
-            editTitle='Edit reference pattern'>
-          <ui:attribute name='editTitle'/>
-          <my:editor>
-            <my:RefPatternBox styleName='{style.nameEdit}'/>
-          </my:editor>
-        </my:ValueEditor>
-        <span ui:field='sectionName' class='{style.name}'/>
-      </td>
-    </tr></table>
-
-    <g:Anchor
-        ui:field='deleteSection'
-        href='javascript:void'
-        styleName='{style.deleteIcon} {res.css.deleteIcon}'
-        title='Delete this section (and nested rules)'>
-      <ui:attribute name='title'/>
-    </g:Anchor>
-  </div>
-
-  <g:FlowPanel
-      ui:field='permissionContainer'
-      styleName='{style.permissionList}'/>
-  <div ui:field='addContainer' class='{style.addContainer}'>
-    <g:ValueListBox ui:field='permissionSelector'/>
-  </div>
-</div>
-
-<div
-    ui:field='deleted'
-    class='{style.deleted} {res.css.deleted}'
-    style='display: none'>
-  <span ui:field='deletedName'/>
-  <g:Anchor
-      ui:field='undoDelete'
-      href='javascript:void'
-      styleName='{style.undoIcon} {res.css.undoIcon}'
-      title='Undo deletion'>
-    <ui:attribute name='title'/>
-  </g:Anchor>
-</div>
-</g:HTMLPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupAuditLogScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupAuditLogScreen.java
deleted file mode 100644
index 5e38a14..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupAuditLogScreen.java
+++ /dev/null
@@ -1,159 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.admin;
-
-import static com.google.gerrit.client.FormatUtil.mediumFormat;
-import static com.google.gerrit.client.FormatUtil.name;
-
-import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.groups.GroupApi;
-import com.google.gerrit.client.groups.GroupAuditEventInfo;
-import com.google.gerrit.client.info.AccountInfo;
-import com.google.gerrit.client.info.GroupInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.ui.FancyFlexTable;
-import com.google.gerrit.client.ui.Hyperlink;
-import com.google.gerrit.client.ui.SmallHeading;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
-import java.util.List;
-
-public class AccountGroupAuditLogScreen extends AccountGroupScreen {
-  private AuditEventTable auditEventTable;
-
-  public AccountGroupAuditLogScreen(GroupInfo toShow, String token) {
-    super(toShow, token);
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    add(new SmallHeading(AdminConstants.I.headingAuditLog()));
-    auditEventTable = new AuditEventTable();
-    add(auditEventTable);
-  }
-
-  @Override
-  protected void display(GroupInfo group, boolean canModify) {
-    GroupApi.getAuditLog(
-        group.getGroupUUID(),
-        new GerritCallback<JsArray<GroupAuditEventInfo>>() {
-          @Override
-          public void onSuccess(JsArray<GroupAuditEventInfo> result) {
-            auditEventTable.display(Natives.asList(result));
-          }
-        });
-  }
-
-  private static class AuditEventTable extends FancyFlexTable<GroupAuditEventInfo> {
-    AuditEventTable() {
-      table.setText(0, 1, AdminConstants.I.columnDate());
-      table.setText(0, 2, AdminConstants.I.columnType());
-      table.setText(0, 3, AdminConstants.I.columnMember());
-      table.setText(0, 4, AdminConstants.I.columnByUser());
-
-      FlexCellFormatter fmt = table.getFlexCellFormatter();
-      fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().dataHeader());
-      fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
-      fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader());
-      fmt.addStyleName(0, 4, Gerrit.RESOURCES.css().dataHeader());
-    }
-
-    void display(List<GroupAuditEventInfo> auditEvents) {
-      while (1 < table.getRowCount()) {
-        table.removeRow(table.getRowCount() - 1);
-      }
-
-      for (GroupAuditEventInfo auditEvent : auditEvents) {
-        int row = table.getRowCount();
-        table.insertRow(row);
-        applyDataRowStyle(row);
-        populate(row, auditEvent);
-      }
-    }
-
-    void populate(int row, GroupAuditEventInfo auditEvent) {
-      FlexCellFormatter fmt = table.getFlexCellFormatter();
-      table.setText(row, 1, mediumFormat(auditEvent.date()));
-
-      switch (auditEvent.type()) {
-        case ADD_USER:
-        case ADD_GROUP:
-          table.setText(row, 2, AdminConstants.I.typeAdded());
-          break;
-        case REMOVE_USER:
-        case REMOVE_GROUP:
-          table.setText(row, 2, AdminConstants.I.typeRemoved());
-          break;
-      }
-
-      switch (auditEvent.type()) {
-        case ADD_USER:
-        case REMOVE_USER:
-          table.setText(row, 3, formatAccount(auditEvent.memberAsUser()));
-          break;
-        case ADD_GROUP:
-        case REMOVE_GROUP:
-          GroupInfo member = auditEvent.memberAsGroup();
-          if (AccountGroup.isInternalGroup(member.getGroupUUID())) {
-            table.setWidget(
-                row,
-                3,
-                new Hyperlink(formatGroup(member), Dispatcher.toGroup(member.getGroupUUID())));
-            fmt.getElement(row, 3).setTitle(null);
-          } else if (member.url() != null) {
-            Anchor a = new Anchor();
-            a.setText(formatGroup(member));
-            a.setHref(member.url());
-            a.setTitle("UUID " + member.getGroupUUID().get());
-            table.setWidget(row, 3, a);
-            fmt.getElement(row, 3).setTitle(null);
-          } else {
-            table.setText(row, 3, formatGroup(member));
-            fmt.getElement(row, 3).setTitle("UUID " + member.getGroupUUID().get());
-          }
-          break;
-      }
-
-      table.setText(row, 4, formatAccount(auditEvent.user()));
-
-      fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().dataCell());
-      fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell());
-      fmt.addStyleName(row, 3, Gerrit.RESOURCES.css().dataCell());
-      fmt.addStyleName(row, 4, Gerrit.RESOURCES.css().dataCell());
-
-      setRowItem(row, auditEvent);
-    }
-  }
-
-  private static String formatAccount(AccountInfo account) {
-    StringBuilder b = new StringBuilder();
-    b.append(name(account));
-    b.append(" (");
-    b.append(account._accountId());
-    b.append(")");
-    return b.toString();
-  }
-
-  private static String formatGroup(GroupInfo group) {
-    return group.name() != null && !group.name().isEmpty()
-        ? group.name()
-        : group.getGroupUUID().get();
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
deleted file mode 100644
index 34a1ac9..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
+++ /dev/null
@@ -1,243 +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.client.admin;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.VoidResult;
-import com.google.gerrit.client.groups.GroupApi;
-import com.google.gerrit.client.info.GroupInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
-import com.google.gerrit.client.ui.OnEditEnabler;
-import com.google.gerrit.client.ui.RemoteSuggestBox;
-import com.google.gerrit.client.ui.SmallHeading;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.VerticalPanel;
-import com.google.gwtexpui.clippy.client.CopyableLabel;
-import com.google.gwtexpui.globalkey.client.NpTextArea;
-import com.google.gwtexpui.globalkey.client.NpTextBox;
-
-public class AccountGroupInfoScreen extends AccountGroupScreen {
-  private CopyableLabel groupUUIDLabel;
-
-  private NpTextBox groupNameTxt;
-  private Button saveName;
-
-  private RemoteSuggestBox ownerTxt;
-  private Button saveOwner;
-
-  private NpTextArea descTxt;
-  private Button saveDesc;
-
-  private CheckBox visibleToAllCheckBox;
-  private Button saveGroupOptions;
-
-  public AccountGroupInfoScreen(GroupInfo toShow, String token) {
-    super(toShow, token);
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    initUUID();
-    initName();
-    initOwner();
-    initDescription();
-    initGroupOptions();
-  }
-
-  private void enableForm(boolean canModify) {
-    groupNameTxt.setEnabled(canModify);
-    ownerTxt.setEnabled(canModify);
-    descTxt.setEnabled(canModify);
-    visibleToAllCheckBox.setEnabled(canModify);
-  }
-
-  private void initUUID() {
-    final VerticalPanel groupUUIDPanel = new VerticalPanel();
-    groupUUIDPanel.setStyleName(Gerrit.RESOURCES.css().groupUUIDPanel());
-    groupUUIDPanel.add(new SmallHeading(AdminConstants.I.headingGroupUUID()));
-    groupUUIDLabel = new CopyableLabel("");
-    groupUUIDPanel.add(groupUUIDLabel);
-    add(groupUUIDPanel);
-  }
-
-  private void initName() {
-    final VerticalPanel groupNamePanel = new VerticalPanel();
-    groupNamePanel.setStyleName(Gerrit.RESOURCES.css().groupNamePanel());
-    groupNameTxt = new NpTextBox();
-    groupNameTxt.setStyleName(Gerrit.RESOURCES.css().groupNameTextBox());
-    groupNameTxt.setVisibleLength(60);
-    groupNamePanel.add(groupNameTxt);
-
-    saveName = new Button(AdminConstants.I.buttonRenameGroup());
-    saveName.setEnabled(false);
-    saveName.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            final String newName = groupNameTxt.getText().trim();
-            GroupApi.renameGroup(
-                getGroupUUID(),
-                newName,
-                new GerritCallback<com.google.gerrit.client.VoidResult>() {
-                  @Override
-                  public void onSuccess(com.google.gerrit.client.VoidResult result) {
-                    saveName.setEnabled(false);
-                    setPageTitle(AdminMessages.I.group(newName));
-                    groupNameTxt.setText(newName);
-                    if (getGroupUUID().equals(getOwnerGroupUUID())) {
-                      ownerTxt.setText(newName);
-                    }
-                  }
-                });
-          }
-        });
-    groupNamePanel.add(saveName);
-    add(groupNamePanel);
-  }
-
-  private void initOwner() {
-    final VerticalPanel ownerPanel = new VerticalPanel();
-    ownerPanel.setStyleName(Gerrit.RESOURCES.css().groupOwnerPanel());
-    ownerPanel.add(new SmallHeading(AdminConstants.I.headingOwner()));
-
-    final AccountGroupSuggestOracle accountGroupOracle = new AccountGroupSuggestOracle();
-    ownerTxt = new RemoteSuggestBox(accountGroupOracle);
-    ownerTxt.setStyleName(Gerrit.RESOURCES.css().groupOwnerTextBox());
-    ownerTxt.setVisibleLength(60);
-    ownerPanel.add(ownerTxt);
-
-    saveOwner = new Button(AdminConstants.I.buttonChangeGroupOwner());
-    saveOwner.setEnabled(false);
-    saveOwner.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            final String newOwner = ownerTxt.getText().trim();
-            if (newOwner.length() > 0) {
-              AccountGroup.UUID ownerUuid = accountGroupOracle.getUUID(newOwner);
-              String ownerId = ownerUuid != null ? ownerUuid.get() : newOwner;
-              GroupApi.setGroupOwner(
-                  getGroupUUID(),
-                  ownerId,
-                  new GerritCallback<GroupInfo>() {
-                    @Override
-                    public void onSuccess(GroupInfo result) {
-                      updateOwnerGroup(result);
-                      saveOwner.setEnabled(false);
-                    }
-                  });
-            }
-          }
-        });
-    ownerPanel.add(saveOwner);
-    add(ownerPanel);
-  }
-
-  private void initDescription() {
-    final VerticalPanel vp = new VerticalPanel();
-    vp.setStyleName(Gerrit.RESOURCES.css().groupDescriptionPanel());
-    vp.add(new SmallHeading(AdminConstants.I.headingDescription()));
-
-    descTxt = new NpTextArea();
-    descTxt.setVisibleLines(6);
-    descTxt.setCharacterWidth(60);
-    vp.add(descTxt);
-
-    saveDesc = new Button(AdminConstants.I.buttonSaveDescription());
-    saveDesc.setEnabled(false);
-    saveDesc.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            final String txt = descTxt.getText().trim();
-            GroupApi.setGroupDescription(
-                getGroupUUID(),
-                txt,
-                new GerritCallback<VoidResult>() {
-                  @Override
-                  public void onSuccess(VoidResult result) {
-                    saveDesc.setEnabled(false);
-                  }
-                });
-          }
-        });
-    vp.add(saveDesc);
-    add(vp);
-  }
-
-  private void initGroupOptions() {
-    final VerticalPanel groupOptionsPanel = new VerticalPanel();
-
-    final VerticalPanel vp = new VerticalPanel();
-    vp.setStyleName(Gerrit.RESOURCES.css().groupOptionsPanel());
-    vp.add(new SmallHeading(AdminConstants.I.headingGroupOptions()));
-
-    visibleToAllCheckBox = new CheckBox(AdminConstants.I.isVisibleToAll());
-    vp.add(visibleToAllCheckBox);
-    groupOptionsPanel.add(vp);
-
-    saveGroupOptions = new Button(AdminConstants.I.buttonSaveGroupOptions());
-    saveGroupOptions.setEnabled(false);
-    saveGroupOptions.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            GroupApi.setGroupOptions(
-                getGroupUUID(),
-                visibleToAllCheckBox.getValue(),
-                new GerritCallback<VoidResult>() {
-                  @Override
-                  public void onSuccess(VoidResult result) {
-                    saveGroupOptions.setEnabled(false);
-                  }
-                });
-          }
-        });
-    groupOptionsPanel.add(saveGroupOptions);
-
-    add(groupOptionsPanel);
-
-    final OnEditEnabler enabler = new OnEditEnabler(saveGroupOptions);
-    enabler.listenTo(visibleToAllCheckBox);
-  }
-
-  @Override
-  protected void display(GroupInfo group, boolean canModify) {
-    groupUUIDLabel.setText(group.getGroupUUID().get());
-    groupNameTxt.setText(group.name());
-    ownerTxt.setText(
-        group.owner() != null
-            ? group.owner()
-            : AdminMessages.I.deletedReference(group.getOwnerUUID().get()));
-    descTxt.setText(group.description());
-    visibleToAllCheckBox.setValue(group.options().isVisibleToAll());
-    setMembersTabVisible(AccountGroup.isInternalGroup(group.getGroupUUID()));
-
-    enableForm(canModify);
-    saveName.setVisible(canModify);
-    saveOwner.setVisible(canModify);
-    saveDesc.setVisible(canModify);
-    saveGroupOptions.setVisible(canModify);
-    new OnEditEnabler(saveDesc, descTxt);
-    new OnEditEnabler(saveName, groupNameTxt);
-    new OnEditEnabler(saveOwner, ownerTxt.getTextBox());
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
deleted file mode 100644
index 6eaab5d..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
+++ /dev/null
@@ -1,436 +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.client.admin;
-
-import static java.util.Comparator.comparing;
-
-import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.VoidResult;
-import com.google.gerrit.client.groups.GroupApi;
-import com.google.gerrit.client.info.AccountInfo;
-import com.google.gerrit.client.info.GroupInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
-import com.google.gerrit.client.ui.AccountLinkPanel;
-import com.google.gerrit.client.ui.AccountSuggestOracle;
-import com.google.gerrit.client.ui.AddMemberBox;
-import com.google.gerrit.client.ui.FancyFlexTable;
-import com.google.gerrit.client.ui.Hyperlink;
-import com.google.gerrit.client.ui.SmallHeading;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.Panel;
-import java.util.Comparator;
-import java.util.HashSet;
-import java.util.List;
-
-public class AccountGroupMembersScreen extends AccountGroupScreen {
-
-  private MemberTable members;
-  private IncludeTable includes;
-
-  private Panel memberPanel;
-  private AddMemberBox addMemberBox;
-  private Button delMember;
-
-  private Panel includePanel;
-  private AddMemberBox addIncludeBox;
-  private Button delInclude;
-
-  private FlowPanel noMembersInfo;
-  private AccountGroupSuggestOracle accountGroupSuggestOracle;
-
-  public AccountGroupMembersScreen(GroupInfo toShow, String token) {
-    super(toShow, token);
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    initMemberList();
-    initIncludeList();
-    initNoMembersInfo();
-  }
-
-  private void enableForm(boolean canModify) {
-    addMemberBox.setEnabled(canModify);
-    members.setEnabled(canModify);
-    addIncludeBox.setEnabled(canModify);
-    includes.setEnabled(canModify);
-  }
-
-  private void initMemberList() {
-    addMemberBox =
-        new AddMemberBox(
-            AdminConstants.I.buttonAddGroupMember(),
-            AdminConstants.I.defaultAccountName(),
-            new AccountSuggestOracle());
-
-    addMemberBox.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            doAddNewMember();
-          }
-        });
-
-    members = new MemberTable();
-    members.addStyleName(Gerrit.RESOURCES.css().groupMembersTable());
-
-    delMember = new Button(AdminConstants.I.buttonDeleteGroupMembers());
-    delMember.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            members.deleteChecked();
-          }
-        });
-
-    memberPanel = new FlowPanel();
-    memberPanel.add(new SmallHeading(AdminConstants.I.headingMembers()));
-    memberPanel.add(addMemberBox);
-    memberPanel.add(members);
-    memberPanel.add(delMember);
-    add(memberPanel);
-  }
-
-  private void initIncludeList() {
-    accountGroupSuggestOracle = new AccountGroupSuggestOracle();
-    addIncludeBox =
-        new AddMemberBox(
-            AdminConstants.I.buttonAddIncludedGroup(),
-            AdminConstants.I.defaultAccountGroupName(),
-            accountGroupSuggestOracle);
-
-    addIncludeBox.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            doAddNewInclude();
-          }
-        });
-
-    includes = new IncludeTable();
-    includes.addStyleName(Gerrit.RESOURCES.css().groupIncludesTable());
-
-    delInclude = new Button(AdminConstants.I.buttonDeleteIncludedGroup());
-    delInclude.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            includes.deleteChecked();
-          }
-        });
-
-    includePanel = new FlowPanel();
-    includePanel.add(new SmallHeading(AdminConstants.I.headingIncludedGroups()));
-    includePanel.add(addIncludeBox);
-    includePanel.add(includes);
-    includePanel.add(delInclude);
-    add(includePanel);
-  }
-
-  private void initNoMembersInfo() {
-    noMembersInfo = new FlowPanel();
-    noMembersInfo.setVisible(false);
-    noMembersInfo.add(new SmallHeading(AdminConstants.I.noMembersInfo()));
-    add(noMembersInfo);
-  }
-
-  @Override
-  protected void display(GroupInfo group, boolean canModify) {
-    if (AccountGroup.isInternalGroup(group.getGroupUUID())) {
-      members.display(Natives.asList(group.members()));
-      includes.display(Natives.asList(group.includes()));
-    } else {
-      memberPanel.setVisible(false);
-      includePanel.setVisible(false);
-      noMembersInfo.setVisible(true);
-    }
-
-    enableForm(canModify);
-    delMember.setVisible(canModify);
-    delInclude.setVisible(canModify);
-  }
-
-  void doAddNewMember() {
-    final String nameEmail = addMemberBox.getText();
-    if (nameEmail.length() == 0) {
-      return;
-    }
-
-    addMemberBox.setEnabled(false);
-    GroupApi.addMember(
-        getGroupUUID(),
-        nameEmail,
-        new GerritCallback<AccountInfo>() {
-          @Override
-          public void onSuccess(AccountInfo memberInfo) {
-            addMemberBox.setEnabled(true);
-            addMemberBox.setText("");
-            members.insert(memberInfo);
-          }
-
-          @Override
-          public void onFailure(Throwable caught) {
-            addMemberBox.setEnabled(true);
-            super.onFailure(caught);
-          }
-        });
-  }
-
-  void doAddNewInclude() {
-    String groupName = addIncludeBox.getText();
-    if (groupName.length() == 0) {
-      return;
-    }
-
-    AccountGroup.UUID uuid = accountGroupSuggestOracle.getUUID(groupName);
-    if (uuid == null) {
-      return;
-    }
-
-    addIncludeBox.setEnabled(false);
-    GroupApi.addIncludedGroup(
-        getGroupUUID(),
-        uuid.get(),
-        new GerritCallback<GroupInfo>() {
-          @Override
-          public void onSuccess(GroupInfo result) {
-            addIncludeBox.setEnabled(true);
-            addIncludeBox.setText("");
-            includes.insert(result);
-          }
-
-          @Override
-          public void onFailure(Throwable caught) {
-            addIncludeBox.setEnabled(true);
-            super.onFailure(caught);
-          }
-        });
-  }
-
-  private class MemberTable extends FancyFlexTable<AccountInfo> {
-    private boolean enabled = true;
-
-    MemberTable() {
-      table.setText(0, 2, AdminConstants.I.columnMember());
-      table.setText(0, 3, AdminConstants.I.columnEmailAddress());
-
-      final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().iconHeader());
-      fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
-      fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader());
-    }
-
-    void setEnabled(boolean enabled) {
-      this.enabled = enabled;
-      for (int row = 1; row < table.getRowCount(); row++) {
-        final AccountInfo i = getRowItem(row);
-        if (i != null) {
-          ((CheckBox) table.getWidget(row, 1)).setEnabled(enabled);
-        }
-      }
-    }
-
-    void deleteChecked() {
-      final HashSet<Integer> ids = new HashSet<>();
-      for (int row = 1; row < table.getRowCount(); row++) {
-        final AccountInfo i = getRowItem(row);
-        if (i != null && ((CheckBox) table.getWidget(row, 1)).getValue()) {
-          ids.add(i._accountId());
-        }
-      }
-      if (!ids.isEmpty()) {
-        GroupApi.removeMembers(
-            getGroupUUID(),
-            ids,
-            new GerritCallback<VoidResult>() {
-              @Override
-              public void onSuccess(VoidResult result) {
-                for (int row = 1; row < table.getRowCount(); ) {
-                  final AccountInfo i = getRowItem(row);
-                  if (i != null && ids.contains(i._accountId())) {
-                    table.removeRow(row);
-                  } else {
-                    row++;
-                  }
-                }
-              }
-            });
-      }
-    }
-
-    void display(List<AccountInfo> result) {
-      while (1 < table.getRowCount()) {
-        table.removeRow(table.getRowCount() - 1);
-      }
-
-      for (AccountInfo i : result) {
-        final int row = table.getRowCount();
-        table.insertRow(row);
-        applyDataRowStyle(row);
-        populate(row, i);
-      }
-    }
-
-    void insert(AccountInfo info) {
-      Comparator<AccountInfo> c =
-          comparing((AccountInfo a) -> nullToEmpty(a.name()))
-              .thenComparing(a -> nullToEmpty(a.email()))
-              .thenComparing(AccountInfo::_accountId);
-      int insertPos = getInsertRow(c, info);
-      if (insertPos >= 0) {
-        table.insertRow(insertPos);
-        applyDataRowStyle(insertPos);
-        populate(insertPos, info);
-      }
-    }
-
-    void populate(int row, AccountInfo i) {
-      CheckBox checkBox = new CheckBox();
-      table.setWidget(row, 1, checkBox);
-      checkBox.setEnabled(enabled);
-      table.setWidget(row, 2, AccountLinkPanel.create(i));
-      table.setText(row, 3, i.email());
-
-      final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().iconCell());
-      fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell());
-      fmt.addStyleName(row, 3, Gerrit.RESOURCES.css().dataCell());
-
-      setRowItem(row, i);
-    }
-  }
-
-  private class IncludeTable extends FancyFlexTable<GroupInfo> {
-    private boolean enabled = true;
-
-    IncludeTable() {
-      table.setText(0, 2, AdminConstants.I.columnGroupName());
-      table.setText(0, 3, AdminConstants.I.columnGroupDescription());
-
-      final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().iconHeader());
-      fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
-      fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader());
-    }
-
-    void setEnabled(boolean enabled) {
-      this.enabled = enabled;
-      for (int row = 1; row < table.getRowCount(); row++) {
-        final GroupInfo i = getRowItem(row);
-        if (i != null) {
-          ((CheckBox) table.getWidget(row, 1)).setEnabled(enabled);
-        }
-      }
-    }
-
-    void deleteChecked() {
-      final HashSet<AccountGroup.UUID> ids = new HashSet<>();
-      for (int row = 1; row < table.getRowCount(); row++) {
-        final GroupInfo i = getRowItem(row);
-        if (i != null && ((CheckBox) table.getWidget(row, 1)).getValue()) {
-          ids.add(i.getGroupUUID());
-        }
-      }
-      if (!ids.isEmpty()) {
-        GroupApi.removeIncludedGroups(
-            getGroupUUID(),
-            ids,
-            new GerritCallback<VoidResult>() {
-              @Override
-              public void onSuccess(VoidResult result) {
-                for (int row = 1; row < table.getRowCount(); ) {
-                  final GroupInfo i = getRowItem(row);
-                  if (i != null && ids.contains(i.getGroupUUID())) {
-                    table.removeRow(row);
-                  } else {
-                    row++;
-                  }
-                }
-              }
-            });
-      }
-    }
-
-    void display(List<GroupInfo> list) {
-      while (1 < table.getRowCount()) {
-        table.removeRow(table.getRowCount() - 1);
-      }
-
-      for (GroupInfo i : list) {
-        final int row = table.getRowCount();
-        table.insertRow(row);
-        applyDataRowStyle(row);
-        populate(row, i);
-      }
-    }
-
-    void insert(GroupInfo info) {
-      Comparator<GroupInfo> c =
-          comparing((GroupInfo g) -> nullToEmpty(g.name())).thenComparing(GroupInfo::getGroupUUID);
-      int insertPos = getInsertRow(c, info);
-      if (insertPos >= 0) {
-        table.insertRow(insertPos);
-        applyDataRowStyle(insertPos);
-        populate(insertPos, info);
-      }
-    }
-
-    void populate(int row, GroupInfo i) {
-      final FlexCellFormatter fmt = table.getFlexCellFormatter();
-
-      AccountGroup.UUID uuid = i.getGroupUUID();
-      CheckBox checkBox = new CheckBox();
-      table.setWidget(row, 1, checkBox);
-      checkBox.setEnabled(enabled);
-      if (AccountGroup.isInternalGroup(uuid)) {
-        table.setWidget(row, 2, new Hyperlink(i.name(), Dispatcher.toGroup(uuid)));
-        fmt.getElement(row, 2).setTitle(null);
-        table.setText(row, 3, i.description());
-      } else if (i.url() != null) {
-        Anchor a = new Anchor();
-        a.setText(i.name());
-        a.setHref(i.url());
-        a.setTitle("UUID " + uuid.get());
-        table.setWidget(row, 2, a);
-        fmt.getElement(row, 2).setTitle(null);
-      } else {
-        table.setText(row, 2, i.name());
-        fmt.getElement(row, 2).setTitle("UUID " + uuid.get());
-      }
-
-      fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().iconCell());
-      fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell());
-      fmt.addStyleName(row, 3, Gerrit.RESOURCES.css().dataCell());
-
-      setRowItem(row, i);
-    }
-  }
-
-  // Like Guava's Strings#nullToEmpty, which can't be used in GWT UI code.
-  private static String nullToEmpty(String str) {
-    return str == null ? "" : str;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java
deleted file mode 100644
index b67213b..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java
+++ /dev/null
@@ -1,97 +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.client.admin;
-
-import static com.google.gerrit.client.Dispatcher.toGroup;
-
-import com.google.gerrit.client.groups.GroupApi;
-import com.google.gerrit.client.info.GroupInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.MenuScreen;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-
-public abstract class AccountGroupScreen extends MenuScreen {
-  public static final String INFO = "info";
-  public static final String MEMBERS = "members";
-  public static final String AUDIT_LOG = "audit-log";
-
-  private final GroupInfo group;
-  private final String token;
-  private final String membersTabToken;
-  private final String auditLogTabToken;
-
-  public AccountGroupScreen(GroupInfo toShow, String token) {
-    setRequiresSignIn(true);
-
-    this.group = toShow;
-    this.token = token;
-    this.membersTabToken = getTabToken(token, MEMBERS);
-    this.auditLogTabToken = getTabToken(token, AUDIT_LOG);
-
-    link(AdminConstants.I.groupTabGeneral(), getTabToken(token, INFO));
-    link(
-        AdminConstants.I.groupTabMembers(),
-        membersTabToken,
-        AccountGroup.isInternalGroup(group.getGroupUUID()));
-  }
-
-  private String getTabToken(String token, String tab) {
-    if (token.startsWith("/admin/groups/uuid-")) {
-      return toGroup(group.getGroupUUID(), tab);
-    }
-    return toGroup(group.getGroupId(), tab);
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    setPageTitle(AdminMessages.I.group(group.name()));
-    display();
-    GroupApi.isGroupOwner(
-        group.name(),
-        new GerritCallback<Boolean>() {
-          @Override
-          public void onSuccess(Boolean result) {
-            if (result) {
-              link(
-                  AdminConstants.I.groupTabAuditLog(),
-                  auditLogTabToken,
-                  AccountGroup.isInternalGroup(group.getGroupUUID()));
-              setToken(token);
-            }
-            display(group, result);
-          }
-        });
-  }
-
-  protected abstract void display(GroupInfo group, boolean canModify);
-
-  protected AccountGroup.UUID getGroupUUID() {
-    return group.getGroupUUID();
-  }
-
-  protected void updateOwnerGroup(GroupInfo ownerGroup) {
-    group.setOwnerUUID(ownerGroup.getGroupUUID());
-    group.owner(ownerGroup.name());
-  }
-
-  protected AccountGroup.UUID getOwnerGroupUUID() {
-    return group.getOwnerUUID();
-  }
-
-  protected void setMembersTabVisible(boolean visible) {
-    setLinkVisible(membersTabToken, visible);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
deleted file mode 100644
index 9def3b3..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
+++ /dev/null
@@ -1,293 +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.client.admin;
-
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.i18n.client.Constants;
-import java.util.Map;
-
-public interface AdminConstants extends Constants {
-  AdminConstants I = GWT.create(AdminConstants.class);
-
-  String defaultAccountName();
-
-  String defaultAccountGroupName();
-
-  String defaultBranchName();
-
-  String defaultTagName();
-
-  String defaultRevisionSpec();
-
-  String annotation();
-
-  String buttonDeleteIncludedGroup();
-
-  String buttonAddIncludedGroup();
-
-  String buttonDeleteGroupMembers();
-
-  String buttonAddGroupMember();
-
-  String buttonSaveDescription();
-
-  String buttonRenameGroup();
-
-  String buttonCreateGroup();
-
-  String buttonCreateProject();
-
-  String buttonChangeGroupOwner();
-
-  String buttonChangeGroupType();
-
-  String buttonSelectGroup();
-
-  String buttonSaveChanges();
-
-  String checkBoxEmptyCommit();
-
-  String checkBoxPermissionsOnly();
-
-  String useContentMerge();
-
-  String useContributorAgreements();
-
-  String useSignedOffBy();
-
-  String createNewChangeForAllNotInTarget();
-
-  String enableSignedPush();
-
-  String requireSignedPush();
-
-  String requireChangeID();
-
-  String rejectImplicitMerges();
-
-  String privateByDefault();
-
-  String workInProgressByDefault();
-
-  String enableReviewerByEmail();
-
-  String matchAuthorToCommitterDate();
-
-  String headingMaxObjectSizeLimit();
-
-  String headingGroupOptions();
-
-  String isVisibleToAll();
-
-  String buttonSaveGroupOptions();
-
-  String suggestedGroupLabel();
-
-  String parentSuggestions();
-
-  String buttonBrowseProjects();
-
-  String projects();
-
-  String projectRepoBrowser();
-
-  String headingGroupUUID();
-
-  String headingOwner();
-
-  String headingDescription();
-
-  String headingProjectOptions();
-
-  String headingProjectCommands();
-
-  String headingCommands();
-
-  String headingMembers();
-
-  String headingIncludedGroups();
-
-  String noMembersInfo();
-
-  String headingExternalGroup();
-
-  String headingCreateGroup();
-
-  String headingParentProjectName();
-
-  String columnProjectName();
-
-  String headingAgreements();
-
-  String headingAuditLog();
-
-  String headingProjectSubmitType();
-
-  String projectSubmitType_INHERIT();
-
-  String projectSubmitType_FAST_FORWARD_ONLY();
-
-  String projectSubmitType_MERGE_ALWAYS();
-
-  String projectSubmitType_MERGE_IF_NECESSARY();
-
-  String projectSubmitType_REBASE_IF_NECESSARY();
-
-  String projectSubmitType_REBASE_ALWAYS();
-
-  String projectSubmitType_CHERRY_PICK();
-
-  String headingProjectState();
-
-  String projectState_ACTIVE();
-
-  String projectState_READ_ONLY();
-
-  String projectState_HIDDEN();
-
-  String columnMember();
-
-  String columnEmailAddress();
-
-  String columnGroupName();
-
-  String columnGroupDescription();
-
-  String columnGroupType();
-
-  String columnGroupNotifications();
-
-  String columnGroupVisibleToAll();
-
-  String columnDate();
-
-  String columnType();
-
-  String columnByUser();
-
-  String typeAdded();
-
-  String typeRemoved();
-
-  String columnBranchName();
-
-  String columnBranchRevision();
-
-  String columnTagName();
-
-  String columnTagRevision();
-
-  String columnTagAnnotation();
-
-  String initialRevision();
-
-  String revision();
-
-  String buttonAddBranch();
-
-  String buttonDeleteBranch();
-
-  String buttonAddTag();
-
-  String buttonDeleteTag();
-
-  String saveHeadButton();
-
-  String cancelHeadButton();
-
-  String groupItemHelp();
-
-  String groupListTitle();
-
-  String groupFilter();
-
-  String createGroupTitle();
-
-  String groupTabGeneral();
-
-  String groupTabMembers();
-
-  String groupTabAuditLog();
-
-  String projectListTitle();
-
-  String projectFilter();
-
-  String createProjectTitle();
-
-  String projectListQueryLink();
-
-  String plugins();
-
-  String pluginEnabled();
-
-  String pluginDisabled();
-
-  String pluginSettingsToolTip();
-
-  String columnPluginName();
-
-  String columnPluginSettings();
-
-  String columnPluginVersion();
-
-  String columnPluginStatus();
-
-  String noGroupSelected();
-
-  String errorNoMatchingGroups();
-
-  String errorNoGitRepository();
-
-  String addPermission();
-
-  Map<String, String> permissionNames();
-
-  String refErrorEmpty();
-
-  String refErrorBeginSlash();
-
-  String refErrorDoubleSlash();
-
-  String refErrorNoSpace();
-
-  String refErrorPrintable();
-
-  String errorsMustBeFixed();
-
-  String sectionTypeReference();
-
-  String sectionTypeSection();
-
-  Map<String, String> sectionNames();
-
-  String pagedListPrev();
-
-  String pagedListNext();
-
-  String buttonCreate();
-
-  String buttonCreateDescription();
-
-  String buttonCreateChange();
-
-  String buttonCreateChangeDescription();
-
-  String buttonEditConfig();
-
-  String buttonEditConfigDescription();
-
-  String editConfigMessage();
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
deleted file mode 100644
index 7901f33..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
+++ /dev/null
@@ -1,204 +0,0 @@
-defaultAccountName = Name or Email
-defaultAccountGroupName = Group Name
-defaultBranchName = Branch Name
-defaultTagName = Tag Name
-defaultRevisionSpec = Revision (Branch or SHA-1)
-annotation = Annotation (optional)
-
-buttonDeleteIncludedGroup = Delete
-buttonAddIncludedGroup = Add
-buttonDeleteGroupMembers = Delete
-buttonAddGroupMember = Add
-buttonRenameGroup = Rename Group
-buttonSaveDescription = Save Description
-buttonCreateGroup = Create Group
-buttonCreateProject = Create Project
-buttonChangeGroupOwner = Change Owner
-buttonChangeGroupType = Change Type
-buttonSelectGroup = Select
-buttonSaveChanges = Save Changes
-checkBoxEmptyCommit = Create initial empty commit
-checkBoxPermissionsOnly = Only serve as parent for other projects
-buttonBrowseProjects = Browse
-projects = All projects
-projectRepoBrowser = Repository Browser
-useContentMerge = Allow content merges
-useContributorAgreements = Require a valid contributor agreement to upload
-useSignedOffBy = Require <code>Signed-off-by</code> in commit message
-createNewChangeForAllNotInTarget = Create a new change for every commit not in the target branch
-enableSignedPush = Enable signed push
-requireSignedPush = Require signed push
-requireChangeID = Require <code>Change-Id</code> in commit message
-rejectImplicitMerges = Reject implicit merges when changes are pushed for review
-privateByDefault = Set all new changes private by default
-workInProgressByDefault = Set all new changes work-in-progress by default
-headingMaxObjectSizeLimit = Maximum Git object size limit
-headingGroupOptions = Group Options
-isVisibleToAll = Make group visible to all registered users.
-buttonSaveGroupOptions = Save Group Options
-suggestedGroupLabel = group
-headingParentProjectName = Rights Inherit From
-parentSuggestions = Parent Suggestion
-columnProjectName = Project Name
-enableReviewerByEmail = Enable adding unregistered users as reviewers and CCs on changes
-matchAuthorToCommitterDate = Match authored date with committer date upon submit
-
-headingGroupUUID = Group UUID
-headingOwner = Owners
-headingDescription = Description
-headingProjectOptions = Project Options
-headingProjectCommands = Project Commands
-headingCommands = Commands
-headingMembers = Members
-headingIncludedGroups = Included Groups
-noMembersInfo = Group Members can only be viewed for Gerrit internal groups. For external groups and Gerrit system groups the members cannot be displayed.
-headingExternalGroup = Selected External Group
-headingCreateGroup = Create New Group
-headingAgreements = Contributor Agreements
-headingAuditLog = Audit Log
-
-headingProjectSubmitType = Submit Type
-projectSubmitType_INHERIT = Inherit
-projectSubmitType_FAST_FORWARD_ONLY = Fast Forward Only
-projectSubmitType_MERGE_IF_NECESSARY = Merge if Necessary
-projectSubmitType_REBASE_ALWAYS = Rebase Always
-projectSubmitType_REBASE_IF_NECESSARY = Rebase if Necessary
-projectSubmitType_MERGE_ALWAYS = Always Merge
-projectSubmitType_CHERRY_PICK = Cherry Pick
-
-headingProjectState = State
-projectState_ACTIVE = Active
-projectState_READ_ONLY = Read Only
-projectState_HIDDEN = Hidden
-
-columnMember = Member
-columnEmailAddress = Email Address
-columnGroupName = Group Name
-columnGroupDescription = Description
-columnGroupType = Group Type
-columnGroupNotifications = Email Only Authors
-columnGroupVisibleToAll = Visible To All
-
-columnDate = Date
-columnType = Type
-columnByUser = By User
-
-typeAdded = Added
-typeRemoved = Removed
-
-columnBranchName = Branch Name
-columnBranchRevision = Revision
-columnTagName = Tag Name
-columnTagRevision = Revision
-columnTagAnnotation = Annotation
-initialRevision = Initial Revision
-revision = Revision
-buttonAddBranch = Create Branch
-buttonAddTag = Create Tag
-buttonDeleteBranch = Delete
-buttonDeleteTag = Delete
-saveHeadButton = Save
-cancelHeadButton = Cancel
-
-groupItemHelp = group
-
-groupListTitle = Groups
-groupFilter = Filter
-createGroupTitle = Create Group
-groupTabGeneral = General
-groupTabMembers = Members
-groupTabAuditLog = Audit Log
-projectListTitle = Projects
-projectFilter = Filter
-createProjectTitle = Create Project
-projectListQueryLink = Search for changes on this project
-
-plugins = Plugins
-pluginEnabled = Enabled
-pluginDisabled = Disabled
-pluginSettingsToolTip = Plugin Settings
-columnPluginName = Plugin Name
-columnPluginSettings = Settings
-columnPluginVersion = Version
-columnPluginStatus = Status
-
-noGroupSelected = (No group selected)
-errorNoMatchingGroups = No Matching Groups
-errorNoGitRepository = No Git Repository
-
-pagedListPrev = &#x21e6;Prev
-pagedListNext = Next&#x21e8;
-
-addPermission = Add Permission ...
-
-# Permission Names
-permissionNames = \
-	abandon, \
-	addPatchSet, \
-	create, \
-	createTag, \
-	createSignedTag, \
-	delete, \
-	deleteChanges, \
-	deleteOwnChanges, \
-	editAssignee, \
-	editHashtags, \
-	editTopicName, \
-	forgeAuthor, \
-	forgeCommitter, \
-	forgeServerAsCommitter, \
-	owner, \
-	push, \
-	pushMerge, \
-	read, \
-	rebase, \
-	removeReviewer, \
-	submit, \
-	submitAs, \
-	viewPrivateChanges
-
-abandon = Abandon
-addPatchSet = Add Patch Set
-create = Create Reference
-createTag = Create Annotated Tag
-createSignedTag = Create Signed Tag
-delete = Delete Reference
-deleteChanges = Delete Changes
-deleteOwnChanges = Delete Own Changes
-editAssignee = Edit Assignee
-editHashtags = Edit Hashtags
-editTopicName = Edit Topic Name
-forgeAuthor = Forge Author Identity
-forgeCommitter = Forge Committer Identity
-forgeServerAsCommitter = Forge Server Identity
-owner = Owner
-push = Push
-pushMerge = Push Merge Commit
-read = Read
-rebase = Rebase
-removeReviewer = Remove Reviewer
-submit = Submit
-submitAs = Submit (On Behalf Of)
-viewPrivateChanges = View Private Changes
-
-refErrorEmpty = Reference must be supplied
-refErrorBeginSlash = Reference must not start with '/'
-refErrorDoubleSlash = References cannot contain '//'
-refErrorNoSpace = References cannot contain spaces
-refErrorPrintable = References may contain only printable characters
-errorsMustBeFixed = Errors must be fixed before committing changes.
-
-# Section Names
-sectionTypeReference = Reference:
-sectionTypeSection = Section:
-sectionNames = \
-  GLOBAL_CAPABILITIES
-GLOBAL_CAPABILITIES = Global Capabilities
-
-buttonCreate = Create
-buttonCreateDescription = Insert the description of the change.
-buttonCreateChange = Create Change
-buttonCreateChangeDescription = Create change directly in the browser.
-buttonEditConfig = Edit Config
-buttonEditConfigDescription = Creates a change to edit the project configuration in the browser.
-editConfigMessage = Edit Project Config
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminCss.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminCss.java
deleted file mode 100644
index 53b2e72..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminCss.java
+++ /dev/null
@@ -1,29 +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.client.admin;
-
-import com.google.gwt.resources.client.CssResource;
-
-public interface AdminCss extends CssResource {
-  String deleteIcon();
-
-  String undoIcon();
-
-  String deleted();
-
-  String deletedBorder();
-
-  String deleteSectionHover();
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.java
deleted file mode 100644
index 7b18a39..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.java
+++ /dev/null
@@ -1,46 +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.client.admin;
-
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.i18n.client.Messages;
-
-public interface AdminMessages extends Messages {
-  AdminMessages I = GWT.create(AdminMessages.class);
-
-  String group(String name);
-
-  String label(String name);
-
-  String labelAs(String name);
-
-  String project(String name);
-
-  String deletedGroup(int id);
-
-  String deletedReference(String name);
-
-  String deletedSection(String name);
-
-  String effectiveMaxObjectSizeLimit(String effectiveMaxObjectSizeLimit);
-
-  String noMaxObjectSizeLimit();
-
-  String pluginProjectOptionsTitle(String pluginName);
-
-  String pluginProjectInheritedValue(String value);
-
-  String pluginProjectInheritedListValue(String value);
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.properties
deleted file mode 100644
index c9aa987..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.properties
+++ /dev/null
@@ -1,13 +0,0 @@
-group = Group {0}
-label = Label {0}
-labelAs = Label {0} (On Behalf Of)
-project = Project {0}
-deletedGroup = Deleted Group {0}
-deletedReference = Reference {0} was deleted
-deletedSection = Section {0} was deleted
-effectiveMaxObjectSizeLimit = effective: {0} bytes
-noMaxObjectSizeLimit = No max object size limit is set.
-pluginProjectOptionsTitle = {0} Plugin Options
-pluginProjectOptionsTitle = {0} Plugin
-pluginProjectInheritedValue = inherited: {0}
-pluginProjectInheritedListValue = INHERIT ({0})
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminResources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminResources.java
deleted file mode 100644
index dfbfbb6..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminResources.java
+++ /dev/null
@@ -1,37 +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.client.admin;
-
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.resources.client.ClientBundle;
-import com.google.gwt.resources.client.ImageResource;
-
-public interface AdminResources extends ClientBundle {
-  AdminResources I = GWT.create(AdminResources.class);
-
-  @Source("admin.css")
-  AdminCss css();
-
-  /** unknown origin TODO replace icons */
-  @Source("deleteNormal.png")
-  ImageResource deleteNormal();
-
-  @Source("deleteHover.png")
-  ImageResource deleteHover();
-
-  /** silk icons (CC-BY3.0): http://famfamfam.com/lab/icons/silk/ */
-  @Source("arrow_undo.png")
-  ImageResource undoNormal();
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateChangeAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateChangeAction.java
deleted file mode 100644
index 611db85..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateChangeAction.java
+++ /dev/null
@@ -1,69 +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.client.admin;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.CreateChangeDialog;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.event.logical.shared.CloseEvent;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.PopupPanel;
-
-class CreateChangeAction {
-  static void call(Button b, String project) {
-    // TODO Replace CreateChangeDialog with a nicer looking display.
-    b.setEnabled(false);
-    new CreateChangeDialog(new Project.NameKey(project)) {
-      {
-        sendButton.setText(AdminConstants.I.buttonCreate());
-        message.setText(AdminConstants.I.buttonCreateDescription());
-      }
-
-      @Override
-      public void onSend() {
-        ChangeApi.createChange(
-            project,
-            getDestinationBranch(),
-            getDestinationTopic(),
-            message.getText(),
-            null,
-            new GerritCallback<ChangeInfo>() {
-              @Override
-              public void onSuccess(ChangeInfo result) {
-                sent = true;
-                hide();
-                Gerrit.display(PageLinks.toChange(result.projectNameKey(), result.legacyId()));
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {
-                enableButtons(true);
-                super.onFailure(caught);
-              }
-            });
-      }
-
-      @Override
-      public void onClose(CloseEvent<PopupPanel> event) {
-        super.onClose(event);
-        b.setEnabled(true);
-      }
-    }.center();
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateGroupScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateGroupScreen.java
deleted file mode 100644
index 6914ee9..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateGroupScreen.java
+++ /dev/null
@@ -1,160 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.admin;
-
-import static com.google.gerrit.common.data.GlobalCapability.CREATE_GROUP;
-
-import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.NotFoundScreen;
-import com.google.gerrit.client.account.AccountCapabilities;
-import com.google.gerrit.client.groups.GroupApi;
-import com.google.gerrit.client.info.GroupInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.OnEditEnabler;
-import com.google.gerrit.client.ui.Screen;
-import com.google.gerrit.client.ui.SmallHeading;
-import com.google.gerrit.common.PageLinks;
-import com.google.gwt.core.client.Scheduler;
-import com.google.gwt.core.client.Scheduler.ScheduledCommand;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwt.user.client.Event;
-import com.google.gwt.user.client.History;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.VerticalPanel;
-import com.google.gwtexpui.globalkey.client.NpTextBox;
-
-public class CreateGroupScreen extends Screen {
-
-  private NpTextBox addTxt;
-  private Button addNew;
-
-  public CreateGroupScreen() {
-    super();
-    setRequiresSignIn(true);
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    AccountCapabilities.all(
-        new GerritCallback<AccountCapabilities>() {
-          @Override
-          public void onSuccess(AccountCapabilities ac) {
-            if (ac.canPerform(CREATE_GROUP)) {
-              display();
-            } else {
-              Gerrit.display(PageLinks.ADMIN_CREATE_GROUP, new NotFoundScreen());
-            }
-          }
-        },
-        CREATE_GROUP);
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    setPageTitle(AdminConstants.I.createGroupTitle());
-    addCreateGroupPanel();
-  }
-
-  @Override
-  public void onShowView() {
-    super.onShowView();
-    if (addTxt != null) {
-      addTxt.setFocus(true);
-    }
-  }
-
-  private void addCreateGroupPanel() {
-    VerticalPanel addPanel = new VerticalPanel();
-    addPanel.setStyleName(Gerrit.RESOURCES.css().addSshKeyPanel());
-    addPanel.add(new SmallHeading(AdminConstants.I.headingCreateGroup()));
-
-    addTxt =
-        new NpTextBox() {
-          @Override
-          public void onBrowserEvent(Event event) {
-            super.onBrowserEvent(event);
-            if (event.getTypeInt() == Event.ONPASTE) {
-              Scheduler.get()
-                  .scheduleDeferred(
-                      new ScheduledCommand() {
-                        @Override
-                        public void execute() {
-                          if (addTxt.getValue().trim().length() != 0) {
-                            addNew.setEnabled(true);
-                          }
-                        }
-                      });
-            }
-          }
-        };
-    addTxt.sinkEvents(Event.ONPASTE);
-
-    addTxt.setVisibleLength(60);
-    addTxt.addKeyPressHandler(
-        new KeyPressHandler() {
-          @Override
-          public void onKeyPress(KeyPressEvent event) {
-            if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
-              doCreateGroup();
-            }
-          }
-        });
-    addPanel.add(addTxt);
-
-    addNew = new Button(AdminConstants.I.buttonCreateGroup());
-    addNew.setEnabled(false);
-    addNew.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            doCreateGroup();
-          }
-        });
-    addPanel.add(addNew);
-    add(addPanel);
-
-    new OnEditEnabler(addNew, addTxt);
-  }
-
-  private void doCreateGroup() {
-    final String newName = addTxt.getText();
-    if (newName == null || newName.length() == 0) {
-      return;
-    }
-
-    addNew.setEnabled(false);
-    GroupApi.createGroup(
-        newName,
-        new GerritCallback<GroupInfo>() {
-          @Override
-          public void onSuccess(GroupInfo result) {
-            History.newItem(Dispatcher.toGroup(result.getGroupId(), AccountGroupScreen.MEMBERS));
-          }
-
-          @Override
-          public void onFailure(Throwable caught) {
-            super.onFailure(caught);
-            addNew.setEnabled(true);
-          }
-        });
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java
deleted file mode 100644
index 02b9169..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java
+++ /dev/null
@@ -1,306 +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.client.admin;
-
-import static com.google.gerrit.common.data.GlobalCapability.CREATE_PROJECT;
-
-import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.client.ErrorDialog;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.NotFoundScreen;
-import com.google.gerrit.client.VoidResult;
-import com.google.gerrit.client.account.AccountCapabilities;
-import com.google.gerrit.client.projects.ProjectApi;
-import com.google.gerrit.client.projects.ProjectInfo;
-import com.google.gerrit.client.projects.ProjectMap;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.OnEditEnabler;
-import com.google.gerrit.client.ui.ProjectListPopup;
-import com.google.gerrit.client.ui.ProjectNameSuggestOracle;
-import com.google.gerrit.client.ui.ProjectsTable;
-import com.google.gerrit.client.ui.RemoteSuggestBox;
-import com.google.gerrit.client.ui.Screen;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.ProjectUtil;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.Scheduler;
-import com.google.gwt.core.client.Scheduler.ScheduledCommand;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwt.user.client.Event;
-import com.google.gwt.user.client.History;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.VerticalPanel;
-import com.google.gwtexpui.globalkey.client.NpTextBox;
-
-public class CreateProjectScreen extends Screen {
-  private Grid grid;
-  private NpTextBox project;
-  private Button create;
-  private Button browse;
-  private RemoteSuggestBox parent;
-  private CheckBox emptyCommit;
-  private CheckBox permissionsOnly;
-  private ProjectsTable suggestedParentsTab;
-  private ProjectListPopup projectsPopup;
-
-  public CreateProjectScreen() {
-    super();
-    setRequiresSignIn(true);
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    AccountCapabilities.all(
-        new GerritCallback<AccountCapabilities>() {
-          @Override
-          public void onSuccess(AccountCapabilities ac) {
-            if (ac.canPerform(CREATE_PROJECT)) {
-              display();
-            } else {
-              Gerrit.display(PageLinks.ADMIN_CREATE_PROJECT, new NotFoundScreen());
-            }
-          }
-        },
-        CREATE_PROJECT);
-  }
-
-  @Override
-  protected void onUnload() {
-    super.onUnload();
-    projectsPopup.closePopup();
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    setPageTitle(AdminConstants.I.createProjectTitle());
-    addCreateProjectPanel();
-
-    /* popup */
-    projectsPopup =
-        new ProjectListPopup() {
-          @Override
-          protected void onMovePointerTo(String projectName) {
-            // prevent user input from being overwritten by simply poping up
-            if (!projectsPopup.isPoppingUp() || "".equals(parent.getText())) {
-              parent.setText(projectName);
-            }
-          }
-        };
-    projectsPopup.initPopup(AdminConstants.I.projects(), PageLinks.ADMIN_PROJECTS);
-  }
-
-  @Override
-  public void onShowView() {
-    super.onShowView();
-    if (project != null) {
-      project.setFocus(true);
-    }
-  }
-
-  private void addCreateProjectPanel() {
-    final VerticalPanel fp = new VerticalPanel();
-    fp.setStyleName(Gerrit.RESOURCES.css().createProjectPanel());
-
-    initCreateButton();
-    initCreateTxt();
-    initParentBox();
-
-    addGrid(fp);
-
-    emptyCommit = new CheckBox(AdminConstants.I.checkBoxEmptyCommit());
-    emptyCommit.setValue(true);
-    permissionsOnly = new CheckBox(AdminConstants.I.checkBoxPermissionsOnly());
-    fp.add(emptyCommit);
-    fp.add(permissionsOnly);
-    fp.add(create);
-    VerticalPanel vp = new VerticalPanel();
-    vp.add(fp);
-    initSuggestedParents();
-    vp.add(suggestedParentsTab);
-    add(vp);
-  }
-
-  private void initCreateTxt() {
-    project =
-        new NpTextBox() {
-          @Override
-          public void onBrowserEvent(Event event) {
-            super.onBrowserEvent(event);
-            if (event.getTypeInt() == Event.ONPASTE) {
-              Scheduler.get()
-                  .scheduleDeferred(
-                      new ScheduledCommand() {
-                        @Override
-                        public void execute() {
-                          if (project.getValue().trim().length() != 0) {
-                            create.setEnabled(true);
-                          }
-                        }
-                      });
-            }
-          }
-        };
-    project.sinkEvents(Event.ONPASTE);
-    project.setVisibleLength(50);
-    project.addKeyPressHandler(
-        new KeyPressHandler() {
-          @Override
-          public void onKeyPress(KeyPressEvent event) {
-            if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
-              doCreateProject();
-            }
-          }
-        });
-    new OnEditEnabler(create, project);
-  }
-
-  private void initCreateButton() {
-    create = new Button(AdminConstants.I.buttonCreateProject());
-    create.setEnabled(false);
-    create.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            doCreateProject();
-          }
-        });
-
-    browse = new Button(AdminConstants.I.buttonBrowseProjects());
-    browse.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            int top = grid.getAbsoluteTop() - 50; // under page header
-            // Try to place it to the right of everything else, but not
-            // right justified
-            int left =
-                5
-                    + Math.max(
-                        grid.getAbsoluteLeft() + grid.getOffsetWidth(),
-                        suggestedParentsTab.getAbsoluteLeft()
-                            + suggestedParentsTab.getOffsetWidth());
-            projectsPopup.setPreferredCoordinates(top, left);
-            projectsPopup.displayPopup();
-          }
-        });
-  }
-
-  private void initParentBox() {
-    parent = new RemoteSuggestBox(new ProjectNameSuggestOracle());
-    parent.setVisibleLength(50);
-  }
-
-  private void initSuggestedParents() {
-    suggestedParentsTab =
-        new ProjectsTable() {
-          {
-            table.setText(0, 1, AdminConstants.I.parentSuggestions());
-          }
-
-          @Override
-          protected void populate(int row, ProjectInfo k) {
-            populateState(row, k);
-            final Anchor projectLink = new Anchor(k.name());
-            projectLink.addClickHandler(
-                new ClickHandler() {
-
-                  @Override
-                  public void onClick(ClickEvent event) {
-                    parent.setText(getRowItem(row).name());
-                  }
-                });
-
-            table.setWidget(row, ProjectsTable.C_NAME, projectLink);
-            table.setText(row, ProjectsTable.C_DESCRIPTION, k.description());
-
-            setRowItem(row, k);
-          }
-        };
-    suggestedParentsTab.setVisible(false);
-
-    ProjectMap.parentCandidates(
-        new GerritCallback<ProjectMap>() {
-          @Override
-          public void onSuccess(ProjectMap list) {
-            if (!list.isEmpty()) {
-              suggestedParentsTab.setVisible(true);
-              suggestedParentsTab.display(list);
-              suggestedParentsTab.finishDisplay();
-            }
-          }
-        });
-  }
-
-  private void addGrid(VerticalPanel fp) {
-    grid = new Grid(2, 3);
-    grid.setStyleName(Gerrit.RESOURCES.css().infoBlock());
-    grid.setText(0, 0, AdminConstants.I.columnProjectName() + ":");
-    grid.setWidget(0, 1, project);
-    grid.setText(1, 0, AdminConstants.I.headingParentProjectName() + ":");
-    grid.setWidget(1, 1, parent);
-    grid.setWidget(1, 2, browse);
-    fp.add(grid);
-  }
-
-  private void doCreateProject() {
-    final String projectName = project.getText().trim();
-    final String parentName = parent.getText().trim();
-
-    if ("".equals(projectName)) {
-      project.setFocus(true);
-      return;
-    }
-
-    enableForm(false);
-    ProjectApi.createProject(
-        projectName,
-        parentName,
-        emptyCommit.getValue(),
-        permissionsOnly.getValue(),
-        new AsyncCallback<VoidResult>() {
-          @Override
-          public void onSuccess(VoidResult result) {
-            String nameWithoutSuffix = ProjectUtil.stripGitSuffix(projectName);
-            History.newItem(
-                Dispatcher.toProjectAdmin(
-                    new Project.NameKey(nameWithoutSuffix), ProjectScreen.INFO));
-          }
-
-          @Override
-          public void onFailure(Throwable caught) {
-            new ErrorDialog(caught.getMessage()).center();
-            enableForm(true);
-          }
-        });
-  }
-
-  private void enableForm(boolean enabled) {
-    project.setEnabled(enabled);
-    create.setEnabled(enabled);
-    parent.setEnabled(enabled);
-    emptyCommit.setEnabled(enabled);
-    permissionsOnly.setEnabled(enabled);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/EditConfigAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/EditConfigAction.java
deleted file mode 100644
index cb2ca0f..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/EditConfigAction.java
+++ /dev/null
@@ -1,53 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.admin;
-
-import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gwt.user.client.ui.Button;
-
-public class EditConfigAction {
-
-  static void call(Button b, Project.NameKey project) {
-    b.setEnabled(false);
-
-    ChangeApi.createChange(
-        project.get(),
-        RefNames.REFS_CONFIG,
-        null,
-        AdminConstants.I.editConfigMessage(),
-        null,
-        new GerritCallback<ChangeInfo>() {
-          @Override
-          public void onSuccess(ChangeInfo result) {
-            Gerrit.display(
-                Dispatcher.toEditScreen(
-                    project, new PatchSet.Id(result.legacyId(), 1), "project.config"));
-          }
-
-          @Override
-          public void onFailure(Throwable caught) {
-            b.setEnabled(true);
-            super.onFailure(caught);
-          }
-        });
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
deleted file mode 100644
index b37a680..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
+++ /dev/null
@@ -1,232 +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.client.admin;
-
-import static com.google.gerrit.common.PageLinks.ADMIN_GROUPS;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.groups.GroupMap;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.Hyperlink;
-import com.google.gerrit.client.ui.PagingHyperlink;
-import com.google.gerrit.client.ui.Screen;
-import com.google.gerrit.common.PageLinks;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyUpEvent;
-import com.google.gwt.event.dom.client.KeyUpHandler;
-import com.google.gwt.http.client.URL;
-import com.google.gwt.user.client.ui.HorizontalPanel;
-import com.google.gwt.user.client.ui.Label;
-import com.google.gwtexpui.globalkey.client.NpTextBox;
-
-public class GroupListScreen extends Screen {
-  private Hyperlink prev;
-  private Hyperlink next;
-  private GroupTable groups;
-  private NpTextBox filterTxt;
-  private int pageSize;
-
-  private String match = "";
-  private int start;
-  private Query query;
-
-  public GroupListScreen() {
-    setRequiresSignIn(true);
-    pageSize = Gerrit.getUserPreferences().changesPerPage();
-  }
-
-  public GroupListScreen(String params) {
-    this();
-    for (String kvPair : params.split("[,;&]")) {
-      String[] kv = kvPair.split("=", 2);
-      if (kv.length != 2 || kv[0].isEmpty()) {
-        continue;
-      }
-
-      if ("filter".equals(kv[0])) {
-        match = URL.decodeQueryString(kv[1]);
-      }
-
-      if ("skip".equals(kv[0]) && URL.decodeQueryString(kv[1]).matches("^[\\d]+")) {
-        start = Integer.parseInt(URL.decodeQueryString(kv[1]));
-      }
-    }
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    query = new Query(match).start(start).run();
-  }
-
-  private void setupNavigationLink(Hyperlink link, String filter, int skip) {
-    link.setTargetHistoryToken(getTokenForScreen(filter, skip));
-    link.setVisible(true);
-  }
-
-  private String getTokenForScreen(String filter, int skip) {
-    String token = ADMIN_GROUPS;
-    if (filter != null && !filter.isEmpty()) {
-      token += "?filter=" + URL.encodeQueryString(filter);
-    }
-    if (skip > 0) {
-      if (token.contains("?filter=")) {
-        token += ",";
-      } else {
-        token += "?";
-      }
-      token += "skip=" + skip;
-    }
-    return token;
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    setPageTitle(AdminConstants.I.groupListTitle());
-    initPageHeader();
-
-    prev = PagingHyperlink.createPrev();
-    prev.setVisible(false);
-
-    next = PagingHyperlink.createNext();
-    next.setVisible(false);
-
-    groups = new GroupTable(PageLinks.ADMIN_GROUPS);
-    add(groups);
-
-    final HorizontalPanel buttons = new HorizontalPanel();
-    buttons.setStyleName(Gerrit.RESOURCES.css().changeTablePrevNextLinks());
-    buttons.add(prev);
-    buttons.add(next);
-    add(buttons);
-  }
-
-  private void initPageHeader() {
-    final HorizontalPanel hp = new HorizontalPanel();
-    hp.setStyleName(Gerrit.RESOURCES.css().projectFilterPanel());
-    final Label filterLabel = new Label(AdminConstants.I.projectFilter());
-    filterLabel.setStyleName(Gerrit.RESOURCES.css().projectFilterLabel());
-    hp.add(filterLabel);
-    filterTxt = new NpTextBox();
-    filterTxt.setValue(match);
-    filterTxt.addKeyUpHandler(
-        new KeyUpHandler() {
-          @Override
-          public void onKeyUp(KeyUpEvent event) {
-            Query q =
-                new Query(filterTxt.getValue())
-                    .open(event.getNativeKeyCode() == KeyCodes.KEY_ENTER);
-            if (match.equals(q.qMatch)) {
-              q.start(start);
-            }
-            if (q.open || !match.equals(q.qMatch)) {
-              if (query == null) {
-                q.run();
-              }
-              query = q;
-            }
-          }
-        });
-    hp.add(filterTxt);
-    add(hp);
-  }
-
-  @Override
-  public void onShowView() {
-    super.onShowView();
-    if (match != null) {
-      filterTxt.setCursorPos(match.length());
-    }
-    filterTxt.setFocus(true);
-  }
-
-  @Override
-  public void registerKeys() {
-    super.registerKeys();
-    groups.setRegisterKeys(true);
-  }
-
-  private class Query {
-    private final String qMatch;
-    private int qStart;
-    private boolean open;
-
-    Query(String match) {
-      this.qMatch = match;
-    }
-
-    Query start(int start) {
-      this.qStart = start;
-      return this;
-    }
-
-    Query open(boolean open) {
-      this.open = open;
-      return this;
-    }
-
-    Query run() {
-      int limit = open ? 1 : pageSize + 1;
-      GroupMap.match(
-          qMatch,
-          limit,
-          qStart,
-          new GerritCallback<GroupMap>() {
-            @Override
-            public void onSuccess(GroupMap result) {
-              if (!isAttached()) {
-                // View has been disposed.
-              } else if (query == Query.this) {
-                query = null;
-                showMap(result);
-              } else {
-                query.run();
-              }
-            }
-          });
-      return this;
-    }
-
-    private void showMap(GroupMap result) {
-      if (open && !result.isEmpty()) {
-        Gerrit.display(PageLinks.toGroup(result.values().get(0).getGroupUUID()));
-        return;
-      }
-
-      setToken(getTokenForScreen(qMatch, qStart));
-      GroupListScreen.this.match = qMatch;
-      GroupListScreen.this.start = qStart;
-
-      if (result.size() <= pageSize) {
-        groups.display(result, qMatch);
-        next.setVisible(false);
-      } else {
-        groups.displaySubset(result, 0, result.size() - 1, qMatch);
-        setupNavigationLink(next, qMatch, qStart + pageSize);
-      }
-
-      if (qStart > 0) {
-        setupNavigationLink(prev, qMatch, qStart - pageSize);
-      } else {
-        prev.setVisible(false);
-      }
-
-      if (!isCurrentView()) {
-        display();
-      }
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupReferenceBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupReferenceBox.java
deleted file mode 100644
index db138a9..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupReferenceBox.java
+++ /dev/null
@@ -1,116 +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.client.admin;
-
-import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
-import com.google.gerrit.client.ui.RemoteSuggestBox;
-import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.editor.client.LeafValueEditor;
-import com.google.gwt.event.logical.shared.CloseEvent;
-import com.google.gwt.event.logical.shared.CloseHandler;
-import com.google.gwt.event.logical.shared.HasCloseHandlers;
-import com.google.gwt.event.logical.shared.HasSelectionHandlers;
-import com.google.gwt.event.logical.shared.SelectionEvent;
-import com.google.gwt.event.logical.shared.SelectionHandler;
-import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.Focusable;
-
-public class GroupReferenceBox extends Composite
-    implements LeafValueEditor<GroupReference>,
-        HasSelectionHandlers<GroupReference>,
-        HasCloseHandlers<GroupReferenceBox>,
-        Focusable {
-  private final AccountGroupSuggestOracle oracle;
-  private final RemoteSuggestBox suggestBox;
-
-  public GroupReferenceBox() {
-    oracle = new AccountGroupSuggestOracle();
-    suggestBox = new RemoteSuggestBox(oracle);
-    initWidget(suggestBox);
-
-    suggestBox.addSelectionHandler(
-        new SelectionHandler<String>() {
-          @Override
-          public void onSelection(SelectionEvent<String> event) {
-            SelectionEvent.fire(GroupReferenceBox.this, toValue(event.getSelectedItem()));
-          }
-        });
-    suggestBox.addCloseHandler(
-        new CloseHandler<RemoteSuggestBox>() {
-          @Override
-          public void onClose(CloseEvent<RemoteSuggestBox> event) {
-            suggestBox.setText("");
-            CloseEvent.fire(GroupReferenceBox.this, GroupReferenceBox.this);
-          }
-        });
-  }
-
-  public void setVisibleLength(int len) {
-    suggestBox.setVisibleLength(len);
-  }
-
-  @Override
-  public HandlerRegistration addSelectionHandler(SelectionHandler<GroupReference> handler) {
-    return addHandler(handler, SelectionEvent.getType());
-  }
-
-  @Override
-  public HandlerRegistration addCloseHandler(CloseHandler<GroupReferenceBox> handler) {
-    return addHandler(handler, CloseEvent.getType());
-  }
-
-  @Override
-  public GroupReference getValue() {
-    return toValue(suggestBox.getText());
-  }
-
-  private GroupReference toValue(String name) {
-    if (name != null && !name.isEmpty()) {
-      return new GroupReference(oracle.getUUID(name), name);
-    }
-    return null;
-  }
-
-  @Override
-  public void setValue(GroupReference value) {
-    suggestBox.setText(value != null ? value.getName() : "");
-  }
-
-  @Override
-  public int getTabIndex() {
-    return suggestBox.getTabIndex();
-  }
-
-  @Override
-  public void setTabIndex(int index) {
-    suggestBox.setTabIndex(index);
-  }
-
-  @Override
-  public void setFocus(boolean focused) {
-    suggestBox.setFocus(focused);
-  }
-
-  @Override
-  public void setAccessKey(char key) {
-    suggestBox.setAccessKey(key);
-  }
-
-  public void setProject(Project.NameKey projectName) {
-    oracle.setProject(projectName);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java
deleted file mode 100644
index be0db41..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java
+++ /dev/null
@@ -1,151 +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.client.admin;
-
-import static java.util.Comparator.comparing;
-
-import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.groups.GroupList;
-import com.google.gerrit.client.groups.GroupMap;
-import com.google.gerrit.client.info.GroupInfo;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.ui.HighlightingInlineHyperlink;
-import com.google.gerrit.client.ui.NavigationTable;
-import com.google.gerrit.client.ui.Util;
-import com.google.gerrit.common.PageLinks;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.user.client.History;
-import com.google.gwt.user.client.Window;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
-import com.google.gwt.user.client.ui.HTMLTable.Cell;
-import com.google.gwt.user.client.ui.Image;
-import java.util.List;
-
-public class GroupTable extends NavigationTable<GroupInfo> {
-  private static final int NUM_COLS = 3;
-
-  public GroupTable() {
-    this(null);
-  }
-
-  public GroupTable(String pointerId) {
-    super(AdminConstants.I.groupItemHelp());
-    setSavePointerId(pointerId);
-
-    table.setText(0, 1, AdminConstants.I.columnGroupName());
-    table.setText(0, 2, AdminConstants.I.columnGroupDescription());
-    table.setText(0, 3, AdminConstants.I.columnGroupVisibleToAll());
-    table.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            final Cell cell = table.getCellForEvent(event);
-            if (cell != null
-                && cell.getCellIndex() != 1
-                && getRowItem(cell.getRowIndex()) != null) {
-              movePointerTo(cell.getRowIndex());
-            }
-          }
-        });
-
-    final FlexCellFormatter fmt = table.getFlexCellFormatter();
-    for (int i = 1; i <= NUM_COLS; i++) {
-      fmt.addStyleName(0, i, Gerrit.RESOURCES.css().dataHeader());
-    }
-  }
-
-  @Override
-  protected Object getRowItemKey(GroupInfo item) {
-    return item.getGroupId();
-  }
-
-  @Override
-  protected void onOpenRow(int row) {
-    GroupInfo groupInfo = getRowItem(row);
-    if (isInteralGroup(groupInfo)) {
-      History.newItem(Dispatcher.toGroup(groupInfo.getGroupId()));
-    } else if (groupInfo.url() != null) {
-      Window.open(groupInfo.url(), "_self", null);
-    }
-  }
-
-  public void display(GroupMap groups, String toHighlight) {
-    display(Natives.asList(groups.values()), toHighlight);
-  }
-
-  public void display(GroupList groups) {
-    display(Natives.asList(groups), null);
-  }
-
-  public void display(List<GroupInfo> list, String toHighlight) {
-    displaySubset(list, toHighlight, 0, list.size());
-  }
-
-  public void displaySubset(GroupMap groups, int fromIndex, int toIndex, String toHighlight) {
-    displaySubset(Natives.asList(groups.values()), toHighlight, fromIndex, toIndex);
-  }
-
-  public void displaySubset(List<GroupInfo> list, String toHighlight, int fromIndex, int toIndex) {
-    while (1 < table.getRowCount()) {
-      table.removeRow(table.getRowCount() - 1);
-    }
-
-    list.sort(comparing(GroupInfo::name));
-    for (GroupInfo group : list.subList(fromIndex, toIndex)) {
-      final int row = table.getRowCount();
-      table.insertRow(row);
-      applyDataRowStyle(row);
-      populate(row, group, toHighlight);
-    }
-  }
-
-  void populate(int row, GroupInfo k, String toHighlight) {
-    if (k.url() != null) {
-      if (isInteralGroup(k)) {
-        table.setWidget(
-            row,
-            1,
-            new HighlightingInlineHyperlink(
-                k.name(), Dispatcher.toGroup(k.getGroupId()), toHighlight));
-      } else {
-        Anchor link = new Anchor();
-        link.setHTML(Util.highlight(k.name(), toHighlight));
-        link.setHref(k.url());
-        table.setWidget(row, 1, link);
-      }
-    } else {
-      table.setHTML(row, 1, Util.highlight(k.name(), toHighlight));
-    }
-    table.setText(row, 2, k.description());
-    if (k.options().isVisibleToAll()) {
-      table.setWidget(row, 3, new Image(Gerrit.RESOURCES.greenCheck()));
-    }
-
-    final FlexCellFormatter fmt = table.getFlexCellFormatter();
-    fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().groupName());
-    for (int i = 1; i <= NUM_COLS; i++) {
-      fmt.addStyleName(row, i, Gerrit.RESOURCES.css().dataCell());
-    }
-
-    setRowItem(row, k);
-  }
-
-  private boolean isInteralGroup(GroupInfo groupInfo) {
-    return groupInfo != null && groupInfo.url().startsWith("#" + PageLinks.ADMIN_GROUPS);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PaginatedProjectScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PaginatedProjectScreen.java
deleted file mode 100644
index 75c3cb6..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PaginatedProjectScreen.java
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.admin;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.ui.Hyperlink;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.http.client.URL;
-
-abstract class PaginatedProjectScreen extends ProjectScreen {
-  protected int pageSize;
-  protected String match = "";
-  protected int start;
-
-  PaginatedProjectScreen(Project.NameKey toShow) {
-    super(toShow);
-    pageSize = Gerrit.getUserPreferences().changesPerPage();
-  }
-
-  protected void parseToken(String token) {
-    for (String kvPair : token.split("[,;&/?]")) {
-      String[] kv = kvPair.split("=", 2);
-      if (kv.length != 2 || kv[0].isEmpty()) {
-        continue;
-      }
-
-      if ("filter".equals(kv[0])) {
-        match = URL.decodeQueryString(kv[1]);
-      }
-
-      if ("skip".equals(kv[0]) && URL.decodeQueryString(kv[1]).matches("^[\\d]+")) {
-        start = Integer.parseInt(URL.decodeQueryString(kv[1]));
-      }
-    }
-  }
-
-  protected void parseToken() {
-    parseToken(getToken());
-  }
-
-  protected String getTokenForScreen(String filter, int skip) {
-    String token = getScreenToken();
-    if (filter != null && !filter.isEmpty()) {
-      token += "?filter=" + URL.encodeQueryString(filter);
-    }
-    if (skip > 0) {
-      if (token.contains("?filter=")) {
-        token += ",";
-      } else {
-        token += "?";
-      }
-      token += "skip=" + skip;
-    }
-    return token;
-  }
-
-  protected abstract String getScreenToken();
-
-  protected void setupNavigationLink(Hyperlink link, String filter, int skip) {
-    link.setTargetHistoryToken(getTokenForScreen(filter, skip));
-    link.setVisible(true);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
deleted file mode 100644
index 39dadcc..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
+++ /dev/null
@@ -1,329 +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.client.admin;
-
-import com.google.gerrit.client.ErrorDialog;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.groups.GroupMap;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.data.GroupInfo;
-import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.LabelTypes;
-import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.common.data.PermissionRange;
-import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.common.data.ProjectAccess;
-import com.google.gerrit.common.data.RefConfigSection;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.core.client.Scheduler;
-import com.google.gwt.core.client.Scheduler.ScheduledCommand;
-import com.google.gwt.dom.client.DivElement;
-import com.google.gwt.dom.client.Style.Display;
-import com.google.gwt.editor.client.Editor;
-import com.google.gwt.editor.client.EditorDelegate;
-import com.google.gwt.editor.client.ValueAwareEditor;
-import com.google.gwt.editor.client.adapters.EditorSource;
-import com.google.gwt.editor.client.adapters.ListEditor;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.MouseOutEvent;
-import com.google.gwt.event.dom.client.MouseOverEvent;
-import com.google.gwt.event.logical.shared.CloseEvent;
-import com.google.gwt.event.logical.shared.SelectionEvent;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.uibinder.client.UiHandler;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwt.user.client.ui.ValueLabel;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-public class PermissionEditor extends Composite
-    implements Editor<Permission>, ValueAwareEditor<Permission> {
-  interface Binder extends UiBinder<HTMLPanel, PermissionEditor> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  @UiField(provided = true)
-  @Path("name")
-  ValueLabel<String> normalName;
-
-  @UiField(provided = true)
-  @Path("name")
-  ValueLabel<String> deletedName;
-
-  @UiField CheckBox exclusiveGroup;
-
-  @UiField FlowPanel ruleContainer;
-  ListEditor<PermissionRule, PermissionRuleEditor> rules;
-
-  @UiField DivElement addContainer;
-  @UiField DivElement addStage1;
-  @UiField DivElement addStage2;
-  @UiField Anchor beginAddRule;
-  @UiField @Editor.Ignore GroupReferenceBox groupToAdd;
-  @UiField Button addRule;
-
-  @UiField Anchor deletePermission;
-
-  @UiField DivElement normal;
-  @UiField DivElement deleted;
-
-  private final Project.NameKey projectName;
-  private final Map<AccountGroup.UUID, GroupInfo> groupInfo;
-  private final boolean readOnly;
-  private final AccessSection section;
-  private final LabelTypes labelTypes;
-  private Permission value;
-  private PermissionRange.WithDefaults validRange;
-  private boolean isDeleted;
-
-  public PermissionEditor(
-      ProjectAccess projectAccess, boolean readOnly, AccessSection section, LabelTypes labelTypes) {
-    this.readOnly = readOnly;
-    this.section = section;
-    this.projectName = projectAccess.getProjectName();
-    this.groupInfo = projectAccess.getGroupInfo();
-    this.labelTypes = labelTypes;
-
-    PermissionNameRenderer nameRenderer =
-        new PermissionNameRenderer(projectAccess.getCapabilities());
-    normalName = new ValueLabel<>(nameRenderer);
-    deletedName = new ValueLabel<>(nameRenderer);
-
-    initWidget(uiBinder.createAndBindUi(this));
-    groupToAdd.setProject(projectName);
-    rules = ListEditor.of(new RuleEditorSource());
-
-    exclusiveGroup.setEnabled(!readOnly);
-    exclusiveGroup.setVisible(RefConfigSection.isValid(section.getName()));
-
-    if (readOnly) {
-      addContainer.removeFromParent();
-      addContainer = null;
-
-      deletePermission.removeFromParent();
-      deletePermission = null;
-    }
-  }
-
-  @UiHandler("deletePermission")
-  void onDeleteHover(@SuppressWarnings("unused") MouseOverEvent event) {
-    addStyleName(AdminResources.I.css().deleteSectionHover());
-  }
-
-  @UiHandler("deletePermission")
-  void onDeleteNonHover(@SuppressWarnings("unused") MouseOutEvent event) {
-    removeStyleName(AdminResources.I.css().deleteSectionHover());
-  }
-
-  @UiHandler("deletePermission")
-  void onDeletePermission(@SuppressWarnings("unused") ClickEvent event) {
-    isDeleted = true;
-    normal.getStyle().setDisplay(Display.NONE);
-    deleted.getStyle().setDisplay(Display.BLOCK);
-  }
-
-  @UiHandler("undoDelete")
-  void onUndoDelete(@SuppressWarnings("unused") ClickEvent event) {
-    isDeleted = false;
-    deleted.getStyle().setDisplay(Display.NONE);
-    normal.getStyle().setDisplay(Display.BLOCK);
-  }
-
-  @UiHandler("beginAddRule")
-  void onBeginAddRule(@SuppressWarnings("unused") ClickEvent event) {
-    beginAddRule();
-  }
-
-  void beginAddRule() {
-    addStage1.getStyle().setDisplay(Display.NONE);
-    addStage2.getStyle().setDisplay(Display.BLOCK);
-
-    Scheduler.get()
-        .scheduleDeferred(
-            new ScheduledCommand() {
-              @Override
-              public void execute() {
-                groupToAdd.setFocus(true);
-              }
-            });
-  }
-
-  @UiHandler("addRule")
-  void onAddGroupByClick(@SuppressWarnings("unused") ClickEvent event) {
-    GroupReference ref = groupToAdd.getValue();
-    if (ref != null) {
-      addGroup(ref);
-    } else {
-      groupToAdd.setFocus(true);
-    }
-  }
-
-  @UiHandler("groupToAdd")
-  void onAddGroupByEnter(SelectionEvent<GroupReference> event) {
-    GroupReference ref = event.getSelectedItem();
-    if (ref != null) {
-      addGroup(ref);
-    }
-  }
-
-  @UiHandler("groupToAdd")
-  void onAbortAddGroup(@SuppressWarnings("unused") CloseEvent<GroupReferenceBox> event) {
-    hideAddGroup();
-  }
-
-  @UiHandler("hideAddGroup")
-  void hideAddGroup(@SuppressWarnings("unused") ClickEvent event) {
-    hideAddGroup();
-  }
-
-  private void hideAddGroup() {
-    addStage1.getStyle().setDisplay(Display.BLOCK);
-    addStage2.getStyle().setDisplay(Display.NONE);
-  }
-
-  private void addGroup(GroupReference ref) {
-    if (ref.getUUID() != null) {
-      if (value.getRule(ref) == null) {
-        PermissionRule newRule = value.getRule(ref, true);
-        if (validRange != null) {
-          int min = validRange.getDefaultMin();
-          int max = validRange.getDefaultMax();
-          newRule.setRange(min, max);
-
-        } else if (GlobalCapability.PRIORITY.equals(value.getName())) {
-          newRule.setAction(PermissionRule.Action.BATCH);
-        }
-
-        rules.getList().add(newRule);
-      }
-      groupToAdd.setValue(null);
-      groupToAdd.setFocus(true);
-
-    } else {
-      // If the oracle didn't get to complete a UUID, resolve it now.
-      //
-      addRule.setEnabled(false);
-      GroupMap.suggestAccountGroupForProject(
-          projectName.get(),
-          ref.getName(),
-          1,
-          new GerritCallback<GroupMap>() {
-            @Override
-            public void onSuccess(GroupMap result) {
-              addRule.setEnabled(true);
-              if (result.values().length() == 1) {
-                addGroup(
-                    new GroupReference(
-                        result.values().get(0).getGroupUUID(), result.values().get(0).name()));
-              } else {
-                groupToAdd.setFocus(true);
-                new ErrorDialog(Gerrit.M.noSuchGroupMessage(ref.getName())).center();
-              }
-            }
-
-            @Override
-            public void onFailure(Throwable caught) {
-              addRule.setEnabled(true);
-              super.onFailure(caught);
-            }
-          });
-    }
-  }
-
-  boolean isDeleted() {
-    return isDeleted;
-  }
-
-  @Override
-  public void setValue(Permission value) {
-    this.value = value;
-
-    if (Permission.hasRange(value.getName())) {
-      LabelType lt = labelTypes.byLabel(value.getLabel());
-      if (lt != null) {
-        validRange =
-            new PermissionRange.WithDefaults(
-                value.getName(),
-                lt.getMin().getValue(),
-                lt.getMax().getValue(),
-                lt.getMin().getValue(),
-                lt.getMax().getValue());
-      }
-    } else if (GlobalCapability.isGlobalCapability(value.getName())) {
-      validRange = GlobalCapability.getRange(value.getName());
-
-    } else {
-      validRange = null;
-    }
-
-    if (Permission.OWNER.equals(value.getName())) {
-      exclusiveGroup.setEnabled(false);
-    } else {
-      exclusiveGroup.setEnabled(!readOnly);
-    }
-  }
-
-  @Override
-  public void flush() {
-    List<PermissionRule> src = rules.getList();
-    List<PermissionRule> keep = new ArrayList<>(src.size());
-
-    for (int i = 0; i < src.size(); i++) {
-      PermissionRuleEditor e = (PermissionRuleEditor) ruleContainer.getWidget(i);
-      if (!e.isDeleted()) {
-        keep.add(src.get(i));
-      }
-    }
-    value.setRules(keep);
-  }
-
-  @Override
-  public void onPropertyChange(String... paths) {}
-
-  @Override
-  public void setDelegate(EditorDelegate<Permission> delegate) {}
-
-  private class RuleEditorSource extends EditorSource<PermissionRuleEditor> {
-    @Override
-    public PermissionRuleEditor create(int index) {
-      PermissionRuleEditor subEditor =
-          new PermissionRuleEditor(readOnly, groupInfo, section, value, validRange);
-      ruleContainer.insert(subEditor, index);
-      return subEditor;
-    }
-
-    @Override
-    public void dispose(PermissionRuleEditor subEditor) {
-      subEditor.removeFromParent();
-    }
-
-    @Override
-    public void setIndex(PermissionRuleEditor subEditor, int index) {
-      ruleContainer.insert(subEditor, index);
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.ui.xml
deleted file mode 100644
index 00c41dc..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.ui.xml
+++ /dev/null
@@ -1,146 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder
-  xmlns:ui='urn:ui:com.google.gwt.uibinder'
-  xmlns:g='urn:import:com.google.gwt.user.client.ui'
-  xmlns:e='urn:import:com.google.gwt.editor.ui.client'
-  xmlns:my='urn:import:com.google.gerrit.client.admin'
-  ui:generateFormat='com.google.gwt.i18n.rebind.format.PropertiesFormat'
-  ui:generateKeys='com.google.gwt.i18n.rebind.keygen.MD5KeyGenerator'
-  ui:generateLocales='default,en'
-  >
-<ui:with field='res' type='com.google.gerrit.client.admin.AdminResources'/>
-<ui:style gss='false'>
-  @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
-  @eval backgroundColor com.google.gerrit.client.Gerrit.getTheme().backgroundColor;
-
-  .panel {
-    position: relative;
-  }
-
-  .normal {
-    border: 1px solid backgroundColor;
-    margin-top: -1px;
-    margin-bottom: -1px;
-  }
-
-  .header {
-    position: relative;
-    padding-left: 5px;
-    padding-right: 5px;
-    padding-bottom: 1px;
-    white-space: nowrap;
-  }
-
-  .header:hover {
-    background-color: selectionColor;
-  }
-
-  .name {
-    font-style: italic;
-  }
-
-  .exclusiveGroup {
-    position: absolute;
-    top: 0;
-    right: 36px;
-    width: 7em;
-    font-size: 80%;
-  }
-
-  .addContainer {
-    padding-left: 10px;
-    position: relative;
-  }
-  .addContainer:hover {
-    background-color: selectionColor;
-  }
-  .addLink {
-    font-size: 80%;
-  }
-
-  .deleteIcon {
-    position: absolute;
-    top: 1px;
-    right: 12px;
-  }
-</ui:style>
-
-<g:HTMLPanel stylePrimaryName='{style.panel}'>
-<div ui:field='normal' class='{style.normal}'>
-  <div class='{style.header}'>
-    <g:ValueLabel styleName='{style.name}' ui:field='normalName'/>
-    <g:CheckBox
-        ui:field='exclusiveGroup'
-        addStyleNames='{style.exclusiveGroup}'
-        text='Exclusive'>
-      <ui:attribute name='text'/>
-    </g:CheckBox>
-  <g:Anchor
-      ui:field='deletePermission'
-      href='javascript:void'
-      styleName='{style.deleteIcon} {res.css.deleteIcon}'
-      title='Delete this permission (and nested rules)'>
-    <ui:attribute name='title'/>
-  </g:Anchor>
-  </div>
-  <g:FlowPanel ui:field='ruleContainer'/>
-  <div ui:field='addContainer' class='{style.addContainer}'>
-    <div ui:field='addStage1'>
-      <g:Anchor
-          ui:field='beginAddRule'
-          styleName='{style.addLink}'
-          href='javascript:void'
-          text='Add Group'>
-        <ui:attribute name='text'/>
-      </g:Anchor>
-    </div>
-    <div ui:field='addStage2' style='display: none'>
-      <ui:msg>Group Name: <my:GroupReferenceBox
-                                            ui:field='groupToAdd'
-                                            visibleLength='45'/></ui:msg>
-      <g:Button
-          ui:field='addRule'
-          text='Add'>
-        <ui:attribute name='text'/>
-      </g:Button>
-      <g:Anchor
-          ui:field='hideAddGroup'
-          href='javascript:void'
-          styleName='{style.deleteIcon} {res.css.deleteIcon}'
-          title='Cancel additional group'>
-        <ui:attribute name='title'/>
-      </g:Anchor>
-    </div>
-  </div>
-</div>
-
-<div
-    ui:field='deleted'
-    class='{res.css.deleted} {res.css.deletedBorder}'
-    style='display: none'>
-  <ui:msg>Permission <g:ValueLabel styleName='{style.name}' ui:field='deletedName'/> was deleted</ui:msg>
-  <g:Anchor
-      ui:field='undoDelete'
-      href='javascript:void'
-      styleName='{style.deleteIcon} {res.css.undoIcon}'
-      title='Undo deletion'>
-    <ui:attribute name='title'/>
-  </g:Anchor>
-</div>
-</g:HTMLPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionNameRenderer.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionNameRenderer.java
deleted file mode 100644
index ef02bd0..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionNameRenderer.java
+++ /dev/null
@@ -1,74 +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.client.admin;
-
-import com.google.gerrit.common.data.Permission;
-import com.google.gwt.text.shared.Renderer;
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
-
-class PermissionNameRenderer implements Renderer<String> {
-  private static final Map<String, String> permissions;
-
-  static {
-    permissions = new HashMap<>();
-    for (Map.Entry<String, String> e : AdminConstants.I.permissionNames().entrySet()) {
-      permissions.put(e.getKey(), e.getValue());
-      permissions.put(e.getKey().toLowerCase(), e.getValue());
-    }
-  }
-
-  private final Map<String, String> fromServer;
-
-  PermissionNameRenderer(Map<String, String> allFromOutside) {
-    fromServer = allFromOutside;
-  }
-
-  @Override
-  public String render(String varName) {
-    if (Permission.isLabelAs(varName)) {
-      return AdminMessages.I.labelAs(Permission.extractLabel(varName));
-    } else if (Permission.isLabel(varName)) {
-      return AdminMessages.I.label(Permission.extractLabel(varName));
-    }
-
-    String desc = permissions.get(varName);
-    if (desc != null) {
-      return desc;
-    }
-
-    desc = fromServer.get(varName);
-    if (desc != null) {
-      return desc;
-    }
-
-    desc = permissions.get(varName.toLowerCase());
-    if (desc != null) {
-      return desc;
-    }
-
-    desc = fromServer.get(varName.toLowerCase());
-    if (desc != null) {
-      return desc;
-    }
-    return varName;
-  }
-
-  @Override
-  public void render(String object, Appendable appendable) throws IOException {
-    appendable.append(render(object));
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java
deleted file mode 100644
index 16dd167..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java
+++ /dev/null
@@ -1,258 +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.client.admin;
-
-import static com.google.gerrit.common.data.Permission.EDIT_TOPIC_NAME;
-import static com.google.gerrit.common.data.Permission.PUSH;
-
-import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.data.GroupInfo;
-import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.common.data.PermissionRange;
-import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.dom.client.DivElement;
-import com.google.gwt.dom.client.SpanElement;
-import com.google.gwt.dom.client.Style.Display;
-import com.google.gwt.editor.client.Editor;
-import com.google.gwt.editor.client.EditorDelegate;
-import com.google.gwt.editor.client.ValueAwareEditor;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.text.shared.Renderer;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.uibinder.client.UiHandler;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwt.user.client.ui.UIObject;
-import com.google.gwt.user.client.ui.ValueListBox;
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-
-public class PermissionRuleEditor extends Composite
-    implements Editor<PermissionRule>, ValueAwareEditor<PermissionRule> {
-  interface Binder extends UiBinder<HTMLPanel, PermissionRuleEditor> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  @UiField(provided = true)
-  ValueListBox<PermissionRule.Action> action;
-
-  @UiField(provided = true)
-  RangeBox min;
-
-  @UiField(provided = true)
-  RangeBox max;
-
-  @UiField CheckBox force;
-
-  @UiField Anchor groupNameLink;
-  @UiField SpanElement groupNameSpan;
-  @UiField SpanElement deletedGroupName;
-
-  @UiField Anchor deleteRule;
-
-  @UiField DivElement normal;
-  @UiField DivElement deleted;
-
-  @UiField SpanElement rangeEditor;
-
-  private Map<AccountGroup.UUID, GroupInfo> groupInfo;
-  private boolean isDeleted;
-  private HandlerRegistration clickHandler;
-
-  public PermissionRuleEditor(
-      boolean readOnly,
-      Map<AccountGroup.UUID, GroupInfo> groupInfo,
-      AccessSection section,
-      Permission permission,
-      PermissionRange.WithDefaults validRange) {
-    this.groupInfo = groupInfo;
-    action = new ValueListBox<>(actionRenderer);
-
-    if (validRange != null && 10 < validRange.getRangeSize()) {
-      min = new RangeBox.Box();
-      max = new RangeBox.Box();
-
-    } else if (validRange != null) {
-      RangeBox.List minList = new RangeBox.List();
-      RangeBox.List maxList = new RangeBox.List();
-      List<Integer> valueList = validRange.getValuesAsList();
-
-      minList.list.setValue(validRange.getMin());
-      maxList.list.setValue(validRange.getMax());
-
-      minList.list.setAcceptableValues(valueList);
-      maxList.list.setAcceptableValues(valueList);
-
-      min = minList;
-      max = maxList;
-
-      action.setAcceptableValues(
-          Arrays.asList(
-              PermissionRule.Action.ALLOW,
-              PermissionRule.Action.DENY,
-              PermissionRule.Action.BLOCK));
-
-    } else {
-      min = new RangeBox.Box();
-      max = new RangeBox.Box();
-
-      if (GlobalCapability.PRIORITY.equals(permission.getName())) {
-        action.setValue(PermissionRule.Action.INTERACTIVE);
-        action.setAcceptableValues(
-            Arrays.asList(PermissionRule.Action.INTERACTIVE, PermissionRule.Action.BATCH));
-
-      } else {
-        action.setValue(PermissionRule.Action.ALLOW);
-        action.setAcceptableValues(
-            Arrays.asList(
-                PermissionRule.Action.ALLOW,
-                PermissionRule.Action.DENY,
-                PermissionRule.Action.BLOCK));
-      }
-    }
-
-    initWidget(uiBinder.createAndBindUi(this));
-
-    String name = permission.getName();
-    boolean canForce = PUSH.equals(name);
-    if (canForce) {
-      String ref = section.getName();
-      canForce = !ref.startsWith("refs/for/") && !ref.startsWith("^refs/for/");
-      force.setText(PermissionRule.FORCE_PUSH);
-    } else {
-      canForce = EDIT_TOPIC_NAME.equals(name);
-      force.setText(PermissionRule.FORCE_EDIT);
-    }
-    force.setVisible(canForce);
-    force.setEnabled(!readOnly);
-    action.getElement().setPropertyBoolean("disabled", readOnly);
-
-    if (validRange != null) {
-      min.setEnabled(!readOnly);
-      max.setEnabled(!readOnly);
-    } else {
-      rangeEditor.getStyle().setDisplay(Display.NONE);
-    }
-
-    if (readOnly) {
-      deleteRule.removeFromParent();
-      deleteRule = null;
-    }
-
-    if (name.equals(GlobalCapability.BATCH_CHANGES_LIMIT)) {
-      min.setEnabled(false);
-    }
-  }
-
-  boolean isDeleted() {
-    return isDeleted;
-  }
-
-  @UiHandler("deleteRule")
-  void onDeleteRule(@SuppressWarnings("unused") ClickEvent event) {
-    isDeleted = true;
-    normal.getStyle().setDisplay(Display.NONE);
-    deleted.getStyle().setDisplay(Display.BLOCK);
-  }
-
-  @UiHandler("undoDelete")
-  void onUndoDelete(@SuppressWarnings("unused") ClickEvent event) {
-    isDeleted = false;
-    deleted.getStyle().setDisplay(Display.NONE);
-    normal.getStyle().setDisplay(Display.BLOCK);
-  }
-
-  @Override
-  public void setValue(PermissionRule value) {
-    if (clickHandler != null) {
-      clickHandler.removeHandler();
-      clickHandler = null;
-    }
-
-    GroupReference ref = value.getGroup();
-    GroupInfo info =
-        groupInfo != null && ref.getUUID() != null ? groupInfo.get(ref.getUUID()) : null;
-
-    boolean link;
-    if (ref.getUUID() != null && AccountGroup.isInternalGroup(ref.getUUID())) {
-      final String token = Dispatcher.toGroup(ref.getUUID());
-      groupNameLink.setText(ref.getName());
-      groupNameLink.setHref("#" + token);
-      groupNameLink.setTitle(info != null ? info.getDescription() : null);
-      groupNameLink.setTarget(null);
-      clickHandler =
-          groupNameLink.addClickHandler(
-              new ClickHandler() {
-                @Override
-                public void onClick(ClickEvent event) {
-                  event.preventDefault();
-                  event.stopPropagation();
-                  Gerrit.display(token);
-                }
-              });
-      link = true;
-    } else if (info != null && info.getUrl() != null) {
-      groupNameLink.setText(ref.getName());
-      groupNameLink.setHref(info.getUrl());
-      groupNameLink.setTitle(info.getDescription());
-      groupNameLink.setTarget("_blank");
-      link = true;
-    } else {
-      groupNameSpan.setInnerText(ref.getName());
-      groupNameSpan.setTitle(ref.getUUID() != null ? ref.getUUID().get() : "");
-      link = false;
-    }
-
-    deletedGroupName.setInnerText(ref.getName());
-    groupNameLink.setVisible(link);
-    UIObject.setVisible(groupNameSpan, !link);
-  }
-
-  @Override
-  public void setDelegate(EditorDelegate<PermissionRule> delegate) {}
-
-  @Override
-  public void flush() {}
-
-  @Override
-  public void onPropertyChange(String... paths) {}
-
-  private static class ActionRenderer implements Renderer<PermissionRule.Action> {
-    @Override
-    public String render(PermissionRule.Action object) {
-      return object != null ? object.toString() : "";
-    }
-
-    @Override
-    public void render(PermissionRule.Action object, Appendable appendable) throws IOException {
-      appendable.append(render(object));
-    }
-  }
-
-  private static final ActionRenderer actionRenderer = new ActionRenderer();
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.ui.xml
deleted file mode 100644
index 644fef4..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.ui.xml
+++ /dev/null
@@ -1,107 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder
-  xmlns:ui='urn:ui:com.google.gwt.uibinder'
-  xmlns:g='urn:import:com.google.gwt.user.client.ui'
-  xmlns:e='urn:import:com.google.gwt.editor.ui.client'
-  xmlns:my='urn:import:com.google.gerrit.client.admin'
-  xmlns:q='urn:import:com.google.gerrit.client.ui'
-  ui:generateFormat='com.google.gwt.i18n.rebind.format.PropertiesFormat'
-  ui:generateKeys='com.google.gwt.i18n.rebind.keygen.MD5KeyGenerator'
-  ui:generateLocales='default,en'
-  >
-<ui:with field='res' type='com.google.gerrit.client.admin.AdminResources'/>
-<ui:style gss='false'>
-  @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
-
-  .panel {
-    position: relative;
-    height: 1.5em;
-  }
-
-  .panel:hover {
-    background-color: selectionColor;
-  }
-
-  .normal {
-    padding-left: 10px;
-    white-space: nowrap;
-    height: 100%;
-  }
-
-  .deleted {
-    height: 100%;
-  }
-
-  .actionList, .minmax {
-    font-size: 80%;
-  }
-
-  .forcePush {
-    position: absolute;
-    top: 0;
-    right: 36px;
-    width: 7em;
-    font-size: 80%;
-  }
-
-  .deleteIcon {
-    position: absolute;
-    top: 2px;
-    right: 11px;
-  }
-
-  .groupName {
-    display: inline;
-  }
-</ui:style>
-
-<g:HTMLPanel styleName='{style.panel}'>
-<div ui:field='normal' class='{style.normal}'>
-  <g:ValueListBox ui:field='action' styleName='{style.actionList}'/>
-  <span ui:field='rangeEditor'>
-    <g:Widget ui:field='min' styleName='{style.minmax}'/>
-    <g:Widget ui:field='max' styleName='{style.minmax}'/>
-  </span>
-
-  <g:Anchor ui:field='groupNameLink' styleName='{style.groupName}'/>
-  <span ui:field='groupNameSpan' styleName='{style.groupName}'/>
-  <g:CheckBox ui:field='force' addStyleNames='{style.forcePush}'/>
-  <g:Anchor
-      ui:field='deleteRule'
-      href='javascript:void'
-      styleName='{style.deleteIcon} {res.css.deleteIcon}'
-      title='Delete this rule'>
-    <ui:attribute name='title'/>
-  </g:Anchor>
-</div>
-
-<div
-    ui:field='deleted'
-    class='{res.css.deleted} {style.deleted}'
-    style='display: none'>
-  <ui:msg>Group <span ui:field='deletedGroupName'/> was deleted</ui:msg>
-  <g:Anchor
-      ui:field='undoDelete'
-      href='javascript:void'
-      styleName='{style.deleteIcon} {res.css.undoIcon}'
-      title='Undo deletion'>
-    <ui:attribute name='title'/>
-  </g:Anchor>
-</div>
-</g:HTMLPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginListScreen.java
deleted file mode 100644
index 381c644..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginListScreen.java
+++ /dev/null
@@ -1,122 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.admin;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.api.ExtensionScreen;
-import com.google.gerrit.client.plugins.PluginInfo;
-import com.google.gerrit.client.plugins.PluginMap;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.client.ui.FancyFlexTable;
-import com.google.gerrit.client.ui.InlineHyperlink;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.ImageResourceRenderer;
-import com.google.gwt.user.client.ui.Panel;
-
-public class PluginListScreen extends PluginScreen {
-
-  private Panel pluginPanel;
-  private PluginTable pluginTable;
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    initPluginList();
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    PluginMap.all(
-        new ScreenLoadCallback<PluginMap>(this) {
-          @Override
-          protected void preDisplay(PluginMap result) {
-            pluginTable.display(result);
-          }
-        });
-  }
-
-  private void initPluginList() {
-    pluginTable = new PluginTable();
-    pluginTable.addStyleName(Gerrit.RESOURCES.css().pluginsTable());
-
-    pluginPanel = new FlowPanel();
-    pluginPanel.setWidth("500px");
-    pluginPanel.add(pluginTable);
-    add(pluginPanel);
-  }
-
-  private static class PluginTable extends FancyFlexTable<PluginInfo> {
-    PluginTable() {
-      table.setText(0, 1, AdminConstants.I.columnPluginName());
-      table.setText(0, 2, AdminConstants.I.columnPluginSettings());
-      table.setText(0, 3, AdminConstants.I.columnPluginVersion());
-      table.setText(0, 4, AdminConstants.I.columnPluginStatus());
-
-      final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().dataHeader());
-      fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
-      fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader());
-      fmt.addStyleName(0, 4, Gerrit.RESOURCES.css().dataHeader());
-    }
-
-    void display(PluginMap plugins) {
-      while (1 < table.getRowCount()) {
-        table.removeRow(table.getRowCount() - 1);
-      }
-
-      for (PluginInfo p : Natives.asList(plugins.values())) {
-        final int row = table.getRowCount();
-        table.insertRow(row);
-        applyDataRowStyle(row);
-        populate(row, p);
-      }
-    }
-
-    void populate(int row, PluginInfo plugin) {
-      if (plugin.disabled() || plugin.indexUrl() == null) {
-        table.setText(row, 1, plugin.name());
-      } else {
-        table.setWidget(
-            row, 1, new Anchor(plugin.name(), Gerrit.selfRedirect(plugin.indexUrl()), "_blank"));
-
-        if (new ExtensionScreen(plugin.name() + "/settings").isFound()) {
-          InlineHyperlink adminScreenLink = new InlineHyperlink();
-          adminScreenLink.setHTML(new ImageResourceRenderer().render(Gerrit.RESOURCES.gear()));
-          adminScreenLink.setTargetHistoryToken("/x/" + plugin.name() + "/settings");
-          adminScreenLink.setTitle(AdminConstants.I.pluginSettingsToolTip());
-          table.setWidget(row, 2, adminScreenLink);
-        }
-      }
-
-      table.setText(row, 3, plugin.version());
-      table.setText(
-          row,
-          4,
-          plugin.disabled() ? AdminConstants.I.pluginDisabled() : AdminConstants.I.pluginEnabled());
-
-      final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().dataCell());
-      fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell());
-      fmt.addStyleName(row, 3, Gerrit.RESOURCES.css().dataCell());
-      fmt.addStyleName(row, 4, Gerrit.RESOURCES.css().dataCell());
-
-      setRowItem(row, plugin);
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginScreen.java
deleted file mode 100644
index 0909fe1..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginScreen.java
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.admin;
-
-import com.google.gerrit.client.ui.Screen;
-
-public abstract class PluginScreen extends Screen {
-
-  public PluginScreen() {
-    setRequiresSignIn(true);
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    setPageTitle(AdminConstants.I.plugins());
-    display();
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java
deleted file mode 100644
index a52ea60..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java
+++ /dev/null
@@ -1,196 +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.client.admin;
-
-import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.client.ui.Hyperlink;
-import com.google.gerrit.client.ui.ParentProjectBox;
-import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.ProjectAccess;
-import com.google.gerrit.common.data.WebLinkInfoCommon;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.dom.client.DivElement;
-import com.google.gwt.dom.client.Style.Display;
-import com.google.gwt.editor.client.Editor;
-import com.google.gwt.editor.client.EditorDelegate;
-import com.google.gwt.editor.client.ValueAwareEditor;
-import com.google.gwt.editor.client.adapters.EditorSource;
-import com.google.gwt.editor.client.adapters.ListEditor;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.uibinder.client.UiHandler;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwt.user.client.ui.Image;
-import java.util.ArrayList;
-import java.util.List;
-
-public class ProjectAccessEditor extends Composite
-    implements Editor<ProjectAccess>, ValueAwareEditor<ProjectAccess> {
-  interface Binder extends UiBinder<HTMLPanel, ProjectAccessEditor> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  @UiField DivElement inheritsFrom;
-
-  @UiField Hyperlink parentProject;
-
-  @UiField @Editor.Ignore ParentProjectBox parentProjectBox;
-
-  @UiField DivElement history;
-
-  @UiField FlowPanel webLinkPanel;
-
-  @UiField FlowPanel localContainer;
-  ListEditor<AccessSection, AccessSectionEditor> local;
-
-  @UiField Anchor addSection;
-
-  private ProjectAccess value;
-
-  private boolean editing;
-
-  public ProjectAccessEditor() {
-    initWidget(uiBinder.createAndBindUi(this));
-    local = ListEditor.of(new Source(localContainer));
-  }
-
-  @UiHandler("addSection")
-  void onAddSection(@SuppressWarnings("unused") ClickEvent event) {
-    int index = local.getList().size();
-    local.getList().add(new AccessSection("refs/heads/*"));
-
-    AccessSectionEditor editor = local.getEditors().get(index);
-    editor.enableEditing();
-    editor.editRefPattern();
-  }
-
-  @Override
-  public void setValue(ProjectAccess value) {
-    // If the owner can edit the Global Capabilities but they don't exist in this
-    // project, create an empty one at the beginning of the list making it
-    // possible to add permissions to it.
-    if (editing
-        && value.isOwnerOf(AccessSection.GLOBAL_CAPABILITIES)
-        && value.getLocal(AccessSection.GLOBAL_CAPABILITIES) == null) {
-      value.getLocal().add(0, new AccessSection(AccessSection.GLOBAL_CAPABILITIES));
-    }
-
-    this.value = value;
-
-    Project.NameKey parent = value.getInheritsFrom();
-    if (parent != null) {
-      inheritsFrom.getStyle().setDisplay(Display.BLOCK);
-      parentProject.setText(parent.get());
-      parentProject.setTargetHistoryToken( //
-          Dispatcher.toProjectAdmin(parent, ProjectScreen.ACCESS));
-
-      parentProjectBox.setVisible(editing);
-      parentProjectBox.setProject(value.getProjectName());
-      parentProjectBox.setParentProject(value.getInheritsFrom());
-      parentProject.setVisible(!parentProjectBox.isVisible());
-    } else {
-      inheritsFrom.getStyle().setDisplay(Display.NONE);
-    }
-    setUpWebLinks();
-
-    addSection.setVisible(editing && (!value.getOwnerOf().isEmpty() || value.canUpload()));
-  }
-
-  @Override
-  public void flush() {
-    List<AccessSection> src = local.getList();
-    List<AccessSection> keep = new ArrayList<>(src.size());
-
-    for (int i = 0; i < src.size(); i++) {
-      AccessSectionEditor e = (AccessSectionEditor) localContainer.getWidget(i);
-      if (!e.isDeleted() && !src.get(i).getPermissions().isEmpty()) {
-        keep.add(src.get(i));
-      }
-    }
-    value.setLocal(keep);
-    value.setInheritsFrom(parentProjectBox.getParentProjectName());
-  }
-
-  @Override
-  public void onPropertyChange(String... paths) {}
-
-  @Override
-  public void setDelegate(EditorDelegate<ProjectAccess> delegate) {}
-
-  void setEditing(boolean editing) {
-    this.editing = editing;
-    addSection.setVisible(editing);
-  }
-
-  private void setUpWebLinks() {
-    List<WebLinkInfoCommon> links = value.getFileHistoryLinks();
-    if (!value.isConfigVisible() || links == null || links.isEmpty()) {
-      history.getStyle().setDisplay(Display.NONE);
-      return;
-    }
-    for (WebLinkInfoCommon link : links) {
-      webLinkPanel.add(toAnchor(link));
-    }
-  }
-
-  private static Anchor toAnchor(WebLinkInfoCommon info) {
-    Anchor a = new Anchor();
-    a.setHref(info.url);
-    if (info.target != null && !info.target.isEmpty()) {
-      a.setTarget(info.target);
-    }
-    if (info.imageUrl != null && !info.imageUrl.isEmpty()) {
-      Image img = new Image();
-      img.setAltText(info.name);
-      img.setUrl(info.imageUrl);
-      img.setTitle(info.name);
-      a.getElement().appendChild(img.getElement());
-    } else {
-      a.setText("(" + info.name + ")");
-    }
-    return a;
-  }
-
-  private class Source extends EditorSource<AccessSectionEditor> {
-    private final FlowPanel container;
-
-    Source(FlowPanel container) {
-      this.container = container;
-    }
-
-    @Override
-    public AccessSectionEditor create(int index) {
-      AccessSectionEditor subEditor = new AccessSectionEditor(value);
-      subEditor.setEditing(editing);
-      container.insert(subEditor, index);
-      return subEditor;
-    }
-
-    @Override
-    public void dispose(AccessSectionEditor subEditor) {
-      subEditor.removeFromParent();
-    }
-
-    @Override
-    public void setIndex(AccessSectionEditor subEditor, int index) {
-      container.insert(subEditor, index);
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.ui.xml
deleted file mode 100644
index 7fbf70d..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.ui.xml
+++ /dev/null
@@ -1,82 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder
-  xmlns:ui='urn:ui:com.google.gwt.uibinder'
-  xmlns:g='urn:import:com.google.gwt.user.client.ui'
-  xmlns:q='urn:import:com.google.gerrit.client.ui'
-  ui:generateFormat='com.google.gwt.i18n.rebind.format.PropertiesFormat'
-  ui:generateKeys='com.google.gwt.i18n.rebind.keygen.MD5KeyGenerator'
-  ui:generateLocales='default,en'
-  >
-<ui:style gss='false'>
-  .inheritsFrom {
-    margin-bottom: 0.5em;
-  }
-  .parentTitle {
-    font-weight: bold;
-  }
-  .parentLink {
-    display: inline;
-  }
-
-  .history {
-    margin-bottom: 0.5em;
-  }
-  .historyTitle {
-    font-weight: bold;
-  }
-  .webLinkPanel a {
-    display: inline;
-  }
-  .webLinkPanel>a {
-    margin-left:2px;
-  }
-
-  .addContainer {
-    margin-top: 5px;
-  }
-  .addContainer:hover {
-    background-color: selectionColor;
-  }
-</ui:style>
-
-<g:HTMLPanel>
-  <div ui:field='inheritsFrom' class='{style.inheritsFrom}'>
-    <span class='{style.parentTitle}'><ui:msg>Rights Inherit From:</ui:msg></span>
-    <q:Hyperlink ui:field='parentProject' styleName='{style.parentLink}'/>
-    <q:ParentProjectBox
-      ui:field='parentProjectBox'
-      visible='false'/>
-  </div>
-  <div ui:field='history' class='{style.history}'>
-    <span class='{style.historyTitle}'><ui:msg>History:</ui:msg></span>
-    <td>
-      <g:FlowPanel ui:field="webLinkPanel" styleName='{style.webLinkPanel}'/>
-    </td>
-  </div>
-
-  <g:FlowPanel ui:field='localContainer'/>
-  <div class='{style.addContainer}'>
-    <g:Anchor
-        ui:field='addSection'
-        href='javascript:void'
-        text='Add Reference'>
-      <ui:attribute name='text'/>
-    </g:Anchor>
-  </div>
-</g:HTMLPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
deleted file mode 100644
index eb44bda..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
+++ /dev/null
@@ -1,312 +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.client.admin;
-
-import static com.google.gerrit.common.ProjectAccessUtil.mergeSections;
-import static com.google.gerrit.common.ProjectAccessUtil.removeEmptyPermissionsAndSections;
-
-import com.google.gerrit.client.ErrorDialog;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.config.CapabilityInfo;
-import com.google.gerrit.client.config.ConfigServerApi;
-import com.google.gerrit.client.rpc.CallbackGroup;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.ProjectAccess;
-import com.google.gerrit.common.errors.UpdateParentFailedException;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.dom.client.DivElement;
-import com.google.gwt.editor.client.SimpleBeanEditorDriver;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.uibinder.client.UiHandler;
-import com.google.gwt.user.client.Window;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwt.user.client.ui.Label;
-import com.google.gwt.user.client.ui.UIObject;
-import com.google.gwt.user.client.ui.VerticalPanel;
-import com.google.gwtexpui.globalkey.client.NpTextArea;
-import com.google.gwtjsonrpc.client.RemoteJsonException;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-public class ProjectAccessScreen extends ProjectScreen {
-  interface Binder extends UiBinder<HTMLPanel, ProjectAccessScreen> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  interface Driver
-      extends SimpleBeanEditorDriver< //
-          ProjectAccess, //
-          ProjectAccessEditor> {}
-
-  @UiField DivElement editTools;
-
-  @UiField Button edit;
-
-  @UiField Button cancel1;
-
-  @UiField Button cancel2;
-
-  @UiField VerticalPanel error;
-
-  @UiField ProjectAccessEditor accessEditor;
-
-  @UiField DivElement commitTools;
-
-  @UiField NpTextArea commitMessage;
-
-  @UiField Button commit;
-
-  @UiField Button review;
-
-  private Driver driver;
-
-  private ProjectAccess access;
-
-  private NativeMap<CapabilityInfo> capabilityMap;
-
-  public ProjectAccessScreen(Project.NameKey toShow) {
-    super(toShow);
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    add(uiBinder.createAndBindUi(this));
-
-    driver = GWT.create(Driver.class);
-    accessEditor.setEditing(false);
-    driver.initialize(accessEditor);
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    CallbackGroup cbs = new CallbackGroup();
-    ConfigServerApi.capabilities(
-        cbs.add(
-            new AsyncCallback<NativeMap<CapabilityInfo>>() {
-              @Override
-              public void onSuccess(NativeMap<CapabilityInfo> result) {
-                capabilityMap = result;
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {
-                // Handled by ScreenLoadCallback.onFailure().
-              }
-            }));
-    Util.PROJECT_SVC.projectAccess(
-        getProjectKey(),
-        cbs.addFinal(
-            new ScreenLoadCallback<ProjectAccess>(this) {
-              @Override
-              public void preDisplay(ProjectAccess access) {
-                displayReadOnly(access);
-              }
-            }));
-    savedPanel = ACCESS;
-  }
-
-  private void displayReadOnly(ProjectAccess access) {
-    this.access = access;
-    Map<String, String> allCapabilities = new HashMap<>();
-    for (CapabilityInfo c : Natives.asList(capabilityMap.values())) {
-      allCapabilities.put(c.id(), c.name());
-    }
-    this.access.setCapabilities(allCapabilities);
-    accessEditor.setEditing(false);
-    UIObject.setVisible(editTools, !access.getOwnerOf().isEmpty() || access.canUpload());
-    edit.setEnabled(!access.getOwnerOf().isEmpty() || access.canUpload());
-    cancel1.setVisible(false);
-    UIObject.setVisible(commitTools, false);
-    driver.edit(access);
-  }
-
-  @UiHandler("edit")
-  void onEdit(@SuppressWarnings("unused") ClickEvent event) {
-    resetEditors();
-
-    edit.setEnabled(false);
-    cancel1.setVisible(true);
-    UIObject.setVisible(commitTools, true);
-    commit.setVisible(!access.getOwnerOf().isEmpty());
-    review.setVisible(access.canUpload());
-    accessEditor.setEditing(true);
-    driver.edit(access);
-  }
-
-  private void resetEditors() {
-    // Push an empty instance through the driver before pushing the real
-    // data. This will force GWT to delete and recreate the editors, which
-    // is required to build initialize them as editable vs. read-only.
-    ProjectAccess mock = new ProjectAccess();
-    mock.setProjectName(access.getProjectName());
-    mock.setRevision(access.getRevision());
-    mock.setLocal(Collections.<AccessSection>emptyList());
-    mock.setOwnerOf(Collections.<String>emptySet());
-    driver.edit(mock);
-  }
-
-  @UiHandler(value = {"cancel1", "cancel2"})
-  void onCancel(@SuppressWarnings("unused") ClickEvent event) {
-    Gerrit.display(PageLinks.toProjectAcceess(getProjectKey()));
-  }
-
-  @UiHandler("commit")
-  void onCommit(@SuppressWarnings("unused") ClickEvent event) {
-    final ProjectAccess access = driver.flush();
-
-    if (driver.hasErrors()) {
-      Window.alert(AdminConstants.I.errorsMustBeFixed());
-      return;
-    }
-
-    String message = commitMessage.getText().trim();
-    if ("".equals(message)) {
-      message = null;
-    }
-
-    enable(false);
-    Util.PROJECT_SVC.changeProjectAccess( //
-        getProjectKey(), //
-        access.getRevision(), //
-        message, //
-        access.getLocal(), //
-        access.getInheritsFrom(), //
-        new GerritCallback<ProjectAccess>() {
-          @Override
-          public void onSuccess(ProjectAccess newAccess) {
-            enable(true);
-            commitMessage.setText("");
-            error.clear();
-            final Set<String> diffs = getDiffs(access, newAccess);
-            if (diffs.isEmpty()) {
-              displayReadOnly(newAccess);
-            } else {
-              error.add(new Label(Gerrit.C.projectAccessError()));
-              for (String diff : diffs) {
-                error.add(new Label(diff));
-              }
-              if (access.canUpload()) {
-                error.add(new Label(Gerrit.C.projectAccessProposeForReviewHint()));
-              }
-            }
-          }
-
-          private Set<String> getDiffs(ProjectAccess wantedAccess, ProjectAccess newAccess) {
-            List<AccessSection> wantedSections =
-                mergeSections(removeEmptyPermissionsAndSections(wantedAccess.getLocal()));
-            List<AccessSection> newSections =
-                removeEmptyPermissionsAndSections(newAccess.getLocal());
-            HashSet<AccessSection> same = new HashSet<>(wantedSections);
-            HashSet<AccessSection> different =
-                new HashSet<>(wantedSections.size() + newSections.size());
-            different.addAll(wantedSections);
-            different.addAll(newSections);
-            same.retainAll(newSections);
-            different.removeAll(same);
-
-            Set<String> differentNames = new HashSet<>();
-            for (AccessSection s : different) {
-              differentNames.add(s.getName());
-            }
-            return differentNames;
-          }
-
-          @Override
-          public void onFailure(Throwable caught) {
-            error.clear();
-            enable(true);
-            if (caught instanceof RemoteJsonException
-                && caught.getMessage().startsWith(UpdateParentFailedException.MESSAGE)) {
-              new ErrorDialog(
-                      Gerrit.M.parentUpdateFailed(
-                          caught
-                              .getMessage()
-                              .substring(UpdateParentFailedException.MESSAGE.length() + 1)))
-                  .center();
-            } else {
-              super.onFailure(caught);
-            }
-          }
-        });
-  }
-
-  @UiHandler("review")
-  void onReview(@SuppressWarnings("unused") ClickEvent event) {
-    final ProjectAccess access = driver.flush();
-
-    if (driver.hasErrors()) {
-      Window.alert(AdminConstants.I.errorsMustBeFixed());
-      return;
-    }
-
-    String message = commitMessage.getText().trim();
-    if ("".equals(message)) {
-      message = null;
-    }
-
-    enable(false);
-    Util.PROJECT_SVC.reviewProjectAccess( //
-        getProjectKey(), //
-        access.getRevision(), //
-        message, //
-        access.getLocal(), //
-        access.getInheritsFrom(), //
-        new GerritCallback<Change.Id>() {
-          @Override
-          public void onSuccess(Change.Id changeId) {
-            enable(true);
-            commitMessage.setText("");
-            error.clear();
-            if (changeId != null) {
-              Gerrit.display(PageLinks.toChange(getProjectKey(), changeId));
-            } else {
-              displayReadOnly(access);
-            }
-          }
-
-          @Override
-          public void onFailure(Throwable caught) {
-            error.clear();
-            enable(true);
-            super.onFailure(caught);
-          }
-        });
-  }
-
-  private void enable(boolean enabled) {
-    commitMessage.setEnabled(enabled);
-    commit.setEnabled(enabled && !access.getOwnerOf().isEmpty());
-    review.setEnabled(enabled && access.canUpload());
-    cancel1.setEnabled(enabled);
-    cancel2.setEnabled(enabled);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.ui.xml
deleted file mode 100644
index 724c7a1..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.ui.xml
+++ /dev/null
@@ -1,87 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder
-  xmlns:ui='urn:ui:com.google.gwt.uibinder'
-  xmlns:g='urn:import:com.google.gwt.user.client.ui'
-  xmlns:my='urn:import:com.google.gerrit.client.admin'
-  xmlns:expui='urn:import:com.google.gwtexpui.globalkey.client'
-  ui:generateFormat='com.google.gwt.i18n.rebind.format.PropertiesFormat'
-  ui:generateKeys='com.google.gwt.i18n.rebind.keygen.MD5KeyGenerator'
-  ui:generateLocales='default,en'
-  >
-<ui:style gss='false'>
-  @external .gwt-TextArea;
-
-  .commitMessage {
-    margin-top: 2em;
-  }
-  .commitMessage .gwt-TextArea {
-    margin: 5px 5px 5px 5px;
-  }
-  .errorMessage {
-    margin-top: 5px;
-    margin-bottom: 5px;
-    color: red;
-  }
-</ui:style>
-
-<g:HTMLPanel>
-  <div ui:field='editTools'>
-    <g:Button
-        ui:field='edit'
-        text='Edit'>
-      <ui:attribute name='text'/>
-    </g:Button>
-    <g:Button
-        ui:field='cancel1'
-        text='Cancel'>
-      <ui:attribute name='text'/>
-    </g:Button>
-  </div>
-  <my:ProjectAccessEditor ui:field='accessEditor'/>
-  <div ui:field='commitTools'>
-    <div class='{style.commitMessage}'>
-      <ui:msg>Commit Message (optional):</ui:msg><br/>
-      <expui:NpTextArea
-          ui:field='commitMessage'
-          visibleLines='4'
-          characterWidth='60'
-          spellCheck='true'
-          />
-    </div>
-    <g:VerticalPanel
-      styleName='{style.errorMessage}'
-      ui:field='error'>
-    </g:VerticalPanel>
-    <g:Button
-        ui:field='commit'
-        text='Save Changes'>
-      <ui:attribute name='text'/>
-    </g:Button>
-    <g:Button
-        ui:field='review'
-        text='Save for Review'>
-      <ui:attribute name='text'/>
-    </g:Button>
-    <g:Button
-        ui:field='cancel2'
-        text='Cancel'>
-      <ui:attribute name='text'/>
-    </g:Button>
-  </div>
-</g:HTMLPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
deleted file mode 100644
index c6a391b..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
+++ /dev/null
@@ -1,673 +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.client.admin;
-
-import static com.google.gerrit.client.ui.Util.highlight;
-
-import com.google.gerrit.client.ConfirmationCallback;
-import com.google.gerrit.client.ConfirmationDialog;
-import com.google.gerrit.client.ErrorDialog;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.VoidResult;
-import com.google.gerrit.client.access.AccessMap;
-import com.google.gerrit.client.access.ProjectAccessInfo;
-import com.google.gerrit.client.actions.ActionButton;
-import com.google.gerrit.client.info.ActionInfo;
-import com.google.gerrit.client.info.WebLinkInfo;
-import com.google.gerrit.client.projects.BranchInfo;
-import com.google.gerrit.client.projects.ProjectApi;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.NativeString;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.client.ui.HintTextBox;
-import com.google.gerrit.client.ui.Hyperlink;
-import com.google.gerrit.client.ui.NavigationTable;
-import com.google.gerrit.client.ui.OnEditEnabler;
-import com.google.gerrit.client.ui.PagingHyperlink;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.core.client.Scheduler;
-import com.google.gwt.core.client.Scheduler.ScheduledCommand;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwt.event.dom.client.KeyUpEvent;
-import com.google.gwt.event.dom.client.KeyUpHandler;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.logical.shared.ValueChangeHandler;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.HorizontalPanel;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwt.user.client.ui.InlineHTML;
-import com.google.gwt.user.client.ui.InlineLabel;
-import com.google.gwt.user.client.ui.Label;
-import com.google.gwt.user.client.ui.TextBox;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtexpui.globalkey.client.NpTextBox;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-public class ProjectBranchesScreen extends PaginatedProjectScreen {
-  private Hyperlink prev;
-  private Hyperlink next;
-  private BranchesTable branchTable;
-  private Button delBranch;
-  private Button addBranch;
-  private HintTextBox nameTxtBox;
-  private HintTextBox irevTxtBox;
-  private FlowPanel addPanel;
-  private NpTextBox filterTxt;
-  private Query query;
-
-  public ProjectBranchesScreen(Project.NameKey toShow) {
-    super(toShow);
-  }
-
-  @Override
-  public String getScreenToken() {
-    return PageLinks.toProjectBranches(getProjectKey());
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    addPanel.setVisible(false);
-    AccessMap.get(
-        getProjectKey(),
-        new GerritCallback<ProjectAccessInfo>() {
-          @Override
-          public void onSuccess(ProjectAccessInfo result) {
-            addPanel.setVisible(result.canAddRefs());
-          }
-        });
-    query = new Query(match).start(start).run();
-    savedPanel = BRANCHES;
-  }
-
-  private void updateForm() {
-    branchTable.updateDeleteButton();
-    addBranch.setEnabled(true);
-    nameTxtBox.setEnabled(true);
-    irevTxtBox.setEnabled(true);
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    initPageHeader();
-
-    prev = PagingHyperlink.createPrev();
-    prev.setVisible(false);
-
-    next = PagingHyperlink.createNext();
-    next.setVisible(false);
-
-    addPanel = new FlowPanel();
-
-    final Grid addGrid = new Grid(2, 2);
-    addGrid.setStyleName(Gerrit.RESOURCES.css().addBranch());
-    final int texBoxLength = 50;
-
-    nameTxtBox = new HintTextBox();
-    nameTxtBox.setVisibleLength(texBoxLength);
-    nameTxtBox.setHintText(AdminConstants.I.defaultBranchName());
-    nameTxtBox.addKeyPressHandler(
-        new KeyPressHandler() {
-          @Override
-          public void onKeyPress(KeyPressEvent event) {
-            if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
-              doAddNewBranch();
-            }
-          }
-        });
-    addGrid.setText(0, 0, AdminConstants.I.columnBranchName() + ":");
-    addGrid.setWidget(0, 1, nameTxtBox);
-
-    irevTxtBox = new HintTextBox();
-    irevTxtBox.setVisibleLength(texBoxLength);
-    irevTxtBox.setHintText(AdminConstants.I.defaultRevisionSpec());
-    irevTxtBox.addKeyPressHandler(
-        new KeyPressHandler() {
-          @Override
-          public void onKeyPress(KeyPressEvent event) {
-            if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
-              doAddNewBranch();
-            }
-          }
-        });
-    addGrid.setText(1, 0, AdminConstants.I.initialRevision() + ":");
-    addGrid.setWidget(1, 1, irevTxtBox);
-
-    addBranch = new Button(AdminConstants.I.buttonAddBranch());
-    addBranch.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            doAddNewBranch();
-          }
-        });
-    addPanel.add(addGrid);
-    addPanel.add(addBranch);
-
-    branchTable = new BranchesTable();
-
-    delBranch = new Button(AdminConstants.I.buttonDeleteBranch());
-    delBranch.setStyleName(Gerrit.RESOURCES.css().branchTableDeleteButton());
-    delBranch.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            branchTable.deleteChecked();
-          }
-        });
-    HorizontalPanel buttons = new HorizontalPanel();
-    buttons.setStyleName(Gerrit.RESOURCES.css().branchTablePrevNextLinks());
-    buttons.add(delBranch);
-    buttons.add(prev);
-    buttons.add(next);
-    add(branchTable);
-    add(buttons);
-    add(addPanel);
-  }
-
-  private void initPageHeader() {
-    parseToken();
-    HorizontalPanel hp = new HorizontalPanel();
-    hp.setStyleName(Gerrit.RESOURCES.css().projectFilterPanel());
-    Label filterLabel = new Label(AdminConstants.I.projectFilter());
-    filterLabel.setStyleName(Gerrit.RESOURCES.css().projectFilterLabel());
-    hp.add(filterLabel);
-    filterTxt = new NpTextBox();
-    filterTxt.setValue(match);
-    filterTxt.addKeyUpHandler(
-        new KeyUpHandler() {
-          @Override
-          public void onKeyUp(KeyUpEvent event) {
-            Query q = new Query(filterTxt.getValue());
-            if (match.equals(q.qMatch)) {
-              q.start(start);
-            } else {
-              if (query == null) {
-                q.run();
-              }
-              query = q;
-            }
-          }
-        });
-    hp.add(filterTxt);
-    add(hp);
-  }
-
-  private void doAddNewBranch() {
-    final String branchName = nameTxtBox.getText().trim();
-    if ("".equals(branchName)) {
-      nameTxtBox.setFocus(true);
-      return;
-    }
-
-    final String rev = irevTxtBox.getText().trim();
-    if ("".equals(rev)) {
-      irevTxtBox.setText("HEAD");
-      Scheduler.get()
-          .scheduleDeferred(
-              new ScheduledCommand() {
-                @Override
-                public void execute() {
-                  irevTxtBox.selectAll();
-                  irevTxtBox.setFocus(true);
-                }
-              });
-      return;
-    }
-
-    addBranch.setEnabled(false);
-    ProjectApi.createBranch(
-        getProjectKey(),
-        branchName,
-        rev,
-        new GerritCallback<BranchInfo>() {
-          @Override
-          public void onSuccess(BranchInfo branch) {
-            showAddedBranch(branch);
-            nameTxtBox.setText("");
-            irevTxtBox.setText("");
-            query = new Query(match).start(start).run();
-          }
-
-          @Override
-          public void onFailure(Throwable caught) {
-            addBranch.setEnabled(true);
-            selectAllAndFocus(nameTxtBox);
-            new ErrorDialog(caught.getMessage()).center();
-          }
-        });
-  }
-
-  void showAddedBranch(BranchInfo branch) {
-    SafeHtmlBuilder b = new SafeHtmlBuilder();
-    b.openElement("b");
-    b.append(Gerrit.C.branchCreationConfirmationMessage());
-    b.closeElement("b");
-
-    b.openElement("p");
-    b.append(branch.ref());
-    b.closeElement("p");
-
-    ConfirmationDialog confirmationDialog =
-        new ConfirmationDialog(
-            Gerrit.C.branchCreationDialogTitle(),
-            b.toSafeHtml(),
-            new ConfirmationCallback() {
-              @Override
-              public void onOk() {
-                // do nothing
-              }
-            });
-    confirmationDialog.center();
-    confirmationDialog.setCancelVisible(false);
-  }
-
-  private static void selectAllAndFocus(TextBox textBox) {
-    textBox.selectAll();
-    textBox.setFocus(true);
-  }
-
-  private class BranchesTable extends NavigationTable<BranchInfo> {
-    private ValueChangeHandler<Boolean> updateDeleteHandler;
-    boolean canDelete;
-
-    BranchesTable() {
-      table.setWidth("");
-      table.setText(0, 2, AdminConstants.I.columnBranchName());
-      table.setText(0, 3, AdminConstants.I.columnBranchRevision());
-
-      final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().iconHeader());
-      fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
-      fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader());
-      fmt.addStyleName(0, 4, Gerrit.RESOURCES.css().dataHeader());
-
-      updateDeleteHandler =
-          new ValueChangeHandler<Boolean>() {
-            @Override
-            public void onValueChange(ValueChangeEvent<Boolean> event) {
-              updateDeleteButton();
-            }
-          };
-    }
-
-    Set<String> getCheckedRefs() {
-      Set<String> refs = new HashSet<>();
-      for (int row = 1; row < table.getRowCount(); row++) {
-        final BranchInfo k = getRowItem(row);
-        if (k != null
-            && table.getWidget(row, 1) instanceof CheckBox
-            && ((CheckBox) table.getWidget(row, 1)).getValue()) {
-          refs.add(k.ref());
-        }
-      }
-      return refs;
-    }
-
-    void setChecked(Set<String> refs) {
-      for (int row = 1; row < table.getRowCount(); row++) {
-        final BranchInfo k = getRowItem(row);
-        if (k != null && refs.contains(k.ref()) && table.getWidget(row, 1) instanceof CheckBox) {
-          ((CheckBox) table.getWidget(row, 1)).setValue(true);
-        }
-      }
-    }
-
-    void deleteChecked() {
-      final Set<String> refs = getCheckedRefs();
-
-      SafeHtmlBuilder b = new SafeHtmlBuilder();
-      b.openElement("b");
-      b.append(Gerrit.C.branchDeletionConfirmationMessage());
-      b.closeElement("b");
-
-      b.openElement("p");
-      boolean first = true;
-      for (String ref : refs) {
-        if (!first) {
-          b.append(",").br();
-        }
-        b.append(ref);
-        first = false;
-      }
-      b.closeElement("p");
-
-      if (refs.isEmpty()) {
-        updateDeleteButton();
-        return;
-      }
-
-      delBranch.setEnabled(false);
-      ConfirmationDialog confirmationDialog =
-          new ConfirmationDialog(
-              Gerrit.C.branchDeletionDialogTitle(),
-              b.toSafeHtml(),
-              new ConfirmationCallback() {
-                @Override
-                public void onOk() {
-                  deleteBranches(refs);
-                }
-
-                @Override
-                public void onCancel() {
-                  branchTable.updateDeleteButton();
-                }
-              });
-      confirmationDialog.center();
-    }
-
-    private void deleteBranches(Set<String> branches) {
-      ProjectApi.deleteBranches(
-          getProjectKey(),
-          branches,
-          new GerritCallback<VoidResult>() {
-            @Override
-            public void onSuccess(VoidResult result) {
-              query = new Query(match).start(start).run();
-            }
-
-            @Override
-            public void onFailure(Throwable caught) {
-              query = new Query(match).start(start).run();
-              super.onFailure(caught);
-            }
-          });
-    }
-
-    void display(List<BranchInfo> branches) {
-      displaySubset(branches, 0, branches.size());
-    }
-
-    void displaySubset(List<BranchInfo> branches, int fromIndex, int toIndex) {
-      canDelete = false;
-
-      while (1 < table.getRowCount()) {
-        table.removeRow(table.getRowCount() - 1);
-      }
-
-      for (BranchInfo k : branches.subList(fromIndex, toIndex)) {
-        final int row = table.getRowCount();
-        table.insertRow(row);
-        applyDataRowStyle(row);
-        populate(row, k);
-      }
-    }
-
-    void populate(int row, BranchInfo k) {
-      if (k.canDelete()) {
-        CheckBox sel = new CheckBox();
-        sel.addValueChangeHandler(updateDeleteHandler);
-        table.setWidget(row, 1, sel);
-        canDelete = true;
-      } else {
-        table.setText(row, 1, "");
-      }
-
-      table.setWidget(row, 2, new InlineHTML(highlight(k.getShortName(), match)));
-
-      if (k.revision() != null) {
-        if ("HEAD".equals(k.getShortName())) {
-          setHeadRevision(row, 3, k.revision());
-        } else {
-          table.setText(row, 3, k.revision());
-        }
-      } else {
-        table.setText(row, 3, "");
-      }
-
-      FlowPanel actionsPanel = new FlowPanel();
-      if (k.webLinks() != null) {
-        for (WebLinkInfo webLink : Natives.asList(k.webLinks())) {
-          actionsPanel.add(webLink.toAnchor());
-        }
-      }
-      if (k.actions() != null) {
-        k.actions().copyKeysIntoChildren("id");
-        for (ActionInfo a : Natives.asList(k.actions().values())) {
-          actionsPanel.add(new ActionButton(getProjectKey(), k, a));
-        }
-      }
-      table.setWidget(row, 4, actionsPanel);
-
-      final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      String iconCellStyle = Gerrit.RESOURCES.css().iconCell();
-      String dataCellStyle = Gerrit.RESOURCES.css().dataCell();
-      if (RefNames.REFS_CONFIG.equals(k.getShortName()) || "HEAD".equals(k.getShortName())) {
-        iconCellStyle = Gerrit.RESOURCES.css().specialBranchIconCell();
-        dataCellStyle = Gerrit.RESOURCES.css().specialBranchDataCell();
-        fmt.setStyleName(row, 0, iconCellStyle);
-      }
-      fmt.addStyleName(row, 1, iconCellStyle);
-      fmt.addStyleName(row, 2, dataCellStyle);
-      fmt.addStyleName(row, 3, dataCellStyle);
-      fmt.addStyleName(row, 4, dataCellStyle);
-
-      setRowItem(row, k);
-    }
-
-    private void setHeadRevision(int row, int column, String rev) {
-      AccessMap.get(
-          getProjectKey(),
-          new GerritCallback<ProjectAccessInfo>() {
-            @Override
-            public void onSuccess(ProjectAccessInfo result) {
-              if (result.isOwner()) {
-                table.setWidget(row, column, getHeadRevisionWidget(rev));
-              } else {
-                table.setText(row, 3, rev);
-              }
-            }
-          });
-    }
-
-    private Widget getHeadRevisionWidget(String headRevision) {
-      FlowPanel p = new FlowPanel();
-      final InlineLabel l = new InlineLabel(headRevision);
-      final Image edit = new Image(Gerrit.RESOURCES.edit());
-      edit.addStyleName(Gerrit.RESOURCES.css().editHeadButton());
-
-      final NpTextBox input = new NpTextBox();
-      input.setVisibleLength(35);
-      input.setValue(headRevision);
-      input.setVisible(false);
-      final Button save = new Button();
-      save.setText(AdminConstants.I.saveHeadButton());
-      save.setVisible(false);
-      save.setEnabled(false);
-      final Button cancel = new Button();
-      cancel.setText(AdminConstants.I.cancelHeadButton());
-      cancel.setVisible(false);
-
-      OnEditEnabler e = new OnEditEnabler(save);
-      e.listenTo(input);
-
-      edit.addClickHandler(
-          new ClickHandler() {
-            @Override
-            public void onClick(ClickEvent event) {
-              l.setVisible(false);
-              edit.setVisible(false);
-              input.setVisible(true);
-              save.setVisible(true);
-              cancel.setVisible(true);
-            }
-          });
-      save.addClickHandler(
-          new ClickHandler() {
-            @Override
-            public void onClick(ClickEvent event) {
-              save.setEnabled(false);
-              ProjectApi.setHead(
-                  getProjectKey(),
-                  input.getValue().trim(),
-                  new GerritCallback<NativeString>() {
-                    @Override
-                    public void onSuccess(NativeString result) {
-                      Gerrit.display(PageLinks.toProjectBranches(getProjectKey()));
-                    }
-
-                    @Override
-                    public void onFailure(Throwable caught) {
-                      super.onFailure(caught);
-                      save.setEnabled(true);
-                    }
-                  });
-            }
-          });
-      cancel.addClickHandler(
-          new ClickHandler() {
-            @Override
-            public void onClick(ClickEvent event) {
-              l.setVisible(true);
-              edit.setVisible(true);
-              input.setVisible(false);
-              input.setValue(headRevision);
-              save.setVisible(false);
-              save.setEnabled(false);
-              cancel.setVisible(false);
-            }
-          });
-
-      p.add(l);
-      p.add(edit);
-      p.add(input);
-      p.add(save);
-      p.add(cancel);
-      return p;
-    }
-
-    boolean hasBranchCanDelete() {
-      return canDelete;
-    }
-
-    void updateDeleteButton() {
-      boolean on = false;
-      for (int row = 1; row < table.getRowCount(); row++) {
-        Widget w = table.getWidget(row, 1);
-        if (w != null && w instanceof CheckBox) {
-          CheckBox sel = (CheckBox) w;
-          if (sel.getValue()) {
-            on = true;
-            break;
-          }
-        }
-      }
-      delBranch.setEnabled(on);
-    }
-
-    @Override
-    protected void onOpenRow(int row) {
-      if (row > 0) {
-        movePointerTo(row);
-      }
-    }
-
-    @Override
-    protected Object getRowItemKey(BranchInfo item) {
-      return item.ref();
-    }
-  }
-
-  @Override
-  public void onShowView() {
-    super.onShowView();
-    if (match != null) {
-      filterTxt.setCursorPos(match.length());
-    }
-    filterTxt.setFocus(true);
-  }
-
-  private class Query {
-    private String qMatch;
-    private int qStart;
-
-    Query(String match) {
-      this.qMatch = match;
-    }
-
-    Query start(int start) {
-      this.qStart = start;
-      return this;
-    }
-
-    Query run() {
-      // Retrieve one more branch than page size to determine if there are more
-      // branches to display
-      ProjectApi.getBranches(
-          getProjectKey(),
-          pageSize + 1,
-          qStart,
-          qMatch,
-          new ScreenLoadCallback<JsArray<BranchInfo>>(ProjectBranchesScreen.this) {
-            @Override
-            public void preDisplay(JsArray<BranchInfo> result) {
-              if (!isAttached()) {
-                // View has been disposed.
-              } else if (query == Query.this) {
-                query = null;
-                showList(result);
-              } else {
-                query.run();
-              }
-            }
-          });
-      return this;
-    }
-
-    void showList(JsArray<BranchInfo> result) {
-      setToken(getTokenForScreen(qMatch, qStart));
-      ProjectBranchesScreen.this.match = qMatch;
-      ProjectBranchesScreen.this.start = qStart;
-
-      if (result.length() <= pageSize) {
-        branchTable.display(Natives.asList(result));
-        next.setVisible(false);
-      } else {
-        branchTable.displaySubset(Natives.asList(result), 0, result.length() - 1);
-        setupNavigationLink(next, qMatch, qStart + pageSize);
-      }
-      if (qStart > 0) {
-        setupNavigationLink(prev, qMatch, qStart - pageSize);
-      } else {
-        prev.setVisible(false);
-      }
-
-      delBranch.setVisible(branchTable.hasBranchCanDelete());
-      Set<String> checkedRefs = branchTable.getCheckedRefs();
-      branchTable.setChecked(checkedRefs);
-      updateForm();
-
-      if (!isCurrentView()) {
-        display();
-      }
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectDashboardsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectDashboardsScreen.java
deleted file mode 100644
index 7b5d04d..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectDashboardsScreen.java
+++ /dev/null
@@ -1,63 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.admin;
-
-import com.google.gerrit.client.dashboards.DashboardList;
-import com.google.gerrit.client.dashboards.DashboardsTable;
-import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.user.client.ui.FlowPanel;
-
-public class ProjectDashboardsScreen extends ProjectScreen {
-  private DashboardsTable dashes;
-  Project.NameKey project;
-
-  public ProjectDashboardsScreen(Project.NameKey project) {
-    super(project);
-    this.project = project;
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    DashboardList.all(
-        getProjectKey(),
-        new ScreenLoadCallback<JsArray<DashboardList>>(this) {
-          @Override
-          protected void preDisplay(JsArray<DashboardList> result) {
-            dashes.display(result);
-          }
-        });
-    savedPanel = DASHBOARDS;
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    dashes = new DashboardsTable(project);
-    FlowPanel fp = new FlowPanel();
-    fp.add(dashes);
-    add(fp);
-    dashes.setSavePointerId("dashboards/project/" + getProjectKey().get());
-    display();
-  }
-
-  @Override
-  public void registerKeys() {
-    super.registerKeys();
-    dashes.setRegisterKeys(true);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
deleted file mode 100644
index d10a031..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
+++ /dev/null
@@ -1,826 +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.client.admin;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.GerritUiExtensionPoint;
-import com.google.gerrit.client.StringListPanel;
-import com.google.gerrit.client.access.AccessMap;
-import com.google.gerrit.client.access.ProjectAccessInfo;
-import com.google.gerrit.client.actions.ActionButton;
-import com.google.gerrit.client.api.ExtensionPanel;
-import com.google.gerrit.client.change.Resources;
-import com.google.gerrit.client.download.DownloadPanel;
-import com.google.gerrit.client.info.ActionInfo;
-import com.google.gerrit.client.info.DownloadInfo.DownloadCommandInfo;
-import com.google.gerrit.client.info.DownloadInfo.DownloadSchemeInfo;
-import com.google.gerrit.client.projects.ConfigInfo;
-import com.google.gerrit.client.projects.ConfigInfo.ConfigParameterInfo;
-import com.google.gerrit.client.projects.ConfigInfo.ConfigParameterValue;
-import com.google.gerrit.client.projects.ConfigInfo.InheritedBooleanInfo;
-import com.google.gerrit.client.projects.ConfigInfo.SubmitTypeInfo;
-import com.google.gerrit.client.projects.ProjectApi;
-import com.google.gerrit.client.rpc.CallbackGroup;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.client.ui.NpIntTextBox;
-import com.google.gerrit.client.ui.OnEditEnabler;
-import com.google.gerrit.client.ui.SmallHeading;
-import com.google.gerrit.extensions.client.InheritableBoolean;
-import com.google.gerrit.extensions.client.ProjectState;
-import com.google.gerrit.extensions.client.SubmitType;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.event.dom.client.ChangeEvent;
-import com.google.gwt.event.dom.client.ChangeHandler;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.FlexTable;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.HasEnabled;
-import com.google.gwt.user.client.ui.HorizontalPanel;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwt.user.client.ui.Label;
-import com.google.gwt.user.client.ui.ListBox;
-import com.google.gwt.user.client.ui.Panel;
-import com.google.gwt.user.client.ui.TextBox;
-import com.google.gwt.user.client.ui.VerticalPanel;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtexpui.globalkey.client.NpTextArea;
-import com.google.gwtexpui.globalkey.client.NpTextBox;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-
-public class ProjectInfoScreen extends ProjectScreen {
-  private boolean isOwner;
-  private boolean configVisible;
-
-  private LabeledWidgetsGrid grid;
-  private Panel pluginOptionsPanel;
-  private LabeledWidgetsGrid actionsGrid;
-
-  // Section: Project Options
-  private ListBox requireChangeID;
-  private ListBox submitType;
-  private ListBox state;
-  private ListBox contentMerge;
-  private ListBox newChangeForAllNotInTarget;
-  private ListBox enableSignedPush;
-  private ListBox requireSignedPush;
-  private ListBox rejectImplicitMerges;
-  private ListBox privateByDefault;
-  private ListBox workInProgressByDefault;
-  private ListBox enableReviewerByEmail;
-  private ListBox matchAuthorToCommitterDate;
-  private NpTextBox maxObjectSizeLimit;
-  private Label effectiveMaxObjectSizeLimit;
-  private Map<String, Map<String, HasEnabled>> pluginConfigWidgets;
-
-  // Section: Contributor Agreements
-  private ListBox contributorAgreements;
-  private ListBox signedOffBy;
-
-  private NpTextArea descTxt;
-  private Button saveProject;
-
-  private OnEditEnabler saveEnabler;
-
-  public ProjectInfoScreen(Project.NameKey toShow) {
-    super(toShow);
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-
-    Resources.I.style().ensureInjected();
-    saveProject = new Button(AdminConstants.I.buttonSaveChanges());
-    saveProject.setStyleName("");
-    saveProject.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            doSave();
-          }
-        });
-
-    ExtensionPanel extensionPanelTop =
-        new ExtensionPanel(GerritUiExtensionPoint.PROJECT_INFO_SCREEN_TOP);
-    extensionPanelTop.put(GerritUiExtensionPoint.Key.PROJECT_NAME, getProjectKey().get());
-    add(extensionPanelTop);
-
-    add(new ProjectDownloadPanel(getProjectKey().get(), true));
-
-    initDescription();
-    grid = new LabeledWidgetsGrid();
-    pluginOptionsPanel = new FlowPanel();
-    actionsGrid = new LabeledWidgetsGrid();
-    initProjectOptions();
-    initAgreements();
-    add(grid);
-    add(pluginOptionsPanel);
-    add(saveProject);
-    add(actionsGrid);
-
-    ExtensionPanel extensionPanelBottom =
-        new ExtensionPanel(GerritUiExtensionPoint.PROJECT_INFO_SCREEN_BOTTOM);
-    extensionPanelBottom.put(GerritUiExtensionPoint.Key.PROJECT_NAME, getProjectKey().get());
-    add(extensionPanelBottom);
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-
-    Project.NameKey project = getProjectKey();
-    CallbackGroup cbg = new CallbackGroup();
-    AccessMap.get(
-        project,
-        cbg.add(
-            new GerritCallback<ProjectAccessInfo>() {
-              @Override
-              public void onSuccess(ProjectAccessInfo result) {
-                isOwner = result.isOwner();
-                configVisible = result.configVisible();
-                enableForm();
-                saveProject.setVisible(isOwner);
-              }
-            }));
-    ProjectApi.getConfig(
-        project,
-        cbg.addFinal(
-            new ScreenLoadCallback<ConfigInfo>(this) {
-              @Override
-              public void preDisplay(ConfigInfo result) {
-                display(result);
-              }
-            }));
-
-    savedPanel = INFO;
-  }
-
-  private void enableForm() {
-    enableForm(isOwner);
-  }
-
-  private void enableForm(boolean isOwner) {
-    state.setEnabled(isOwner);
-    submitType.setEnabled(isOwner);
-    setEnabledForUseContentMerge();
-    newChangeForAllNotInTarget.setEnabled(isOwner);
-    if (enableSignedPush != null) {
-      enableSignedPush.setEnabled(isOwner);
-    }
-    if (requireSignedPush != null) {
-      requireSignedPush.setEnabled(isOwner);
-    }
-    descTxt.setEnabled(isOwner);
-    contributorAgreements.setEnabled(isOwner);
-    signedOffBy.setEnabled(isOwner);
-    requireChangeID.setEnabled(isOwner);
-    rejectImplicitMerges.setEnabled(isOwner);
-    privateByDefault.setEnabled(isOwner);
-    workInProgressByDefault.setEnabled(isOwner);
-    maxObjectSizeLimit.setEnabled(isOwner);
-    enableReviewerByEmail.setEnabled(isOwner);
-    matchAuthorToCommitterDate.setEnabled(isOwner);
-
-    if (pluginConfigWidgets != null) {
-      for (Map<String, HasEnabled> widgetMap : pluginConfigWidgets.values()) {
-        for (HasEnabled widget : widgetMap.values()) {
-          widget.setEnabled(isOwner);
-        }
-      }
-    }
-  }
-
-  private void initDescription() {
-    final VerticalPanel vp = new VerticalPanel();
-    vp.add(new SmallHeading(AdminConstants.I.headingDescription()));
-
-    descTxt = new NpTextArea();
-    descTxt.setVisibleLines(6);
-    descTxt.setCharacterWidth(60);
-    vp.add(descTxt);
-
-    add(vp);
-    saveEnabler = new OnEditEnabler(saveProject);
-    saveEnabler.listenTo(descTxt);
-  }
-
-  private void initProjectOptions() {
-    grid.addHeader(new SmallHeading(AdminConstants.I.headingProjectOptions()));
-
-    state = new ListBox();
-    for (ProjectState stateValue : ProjectState.values()) {
-      state.addItem(Util.toLongString(stateValue), stateValue.name());
-    }
-    saveEnabler.listenTo(state);
-    grid.add(AdminConstants.I.headingProjectState(), state);
-
-    submitType = new ListBox();
-    for (SubmitType type : SubmitType.values()) {
-      submitType.addItem(Util.toLongString(type), type.name());
-    }
-    submitType.addChangeHandler(
-        new ChangeHandler() {
-          @Override
-          public void onChange(ChangeEvent event) {
-            setEnabledForUseContentMerge();
-          }
-        });
-    saveEnabler.listenTo(submitType);
-    grid.add(AdminConstants.I.headingProjectSubmitType(), submitType);
-
-    contentMerge = newInheritedBooleanBox();
-    saveEnabler.listenTo(contentMerge);
-    grid.add(AdminConstants.I.useContentMerge(), contentMerge);
-
-    newChangeForAllNotInTarget = newInheritedBooleanBox();
-    saveEnabler.listenTo(newChangeForAllNotInTarget);
-    grid.add(AdminConstants.I.createNewChangeForAllNotInTarget(), newChangeForAllNotInTarget);
-
-    requireChangeID = newInheritedBooleanBox();
-    saveEnabler.listenTo(requireChangeID);
-    grid.addHtml(AdminConstants.I.requireChangeID(), requireChangeID);
-
-    if (Gerrit.info().receive().enableSignedPush()) {
-      enableSignedPush = newInheritedBooleanBox();
-      saveEnabler.listenTo(enableSignedPush);
-      grid.add(AdminConstants.I.enableSignedPush(), enableSignedPush);
-      requireSignedPush = newInheritedBooleanBox();
-      saveEnabler.listenTo(requireSignedPush);
-      grid.add(AdminConstants.I.requireSignedPush(), requireSignedPush);
-    }
-
-    rejectImplicitMerges = newInheritedBooleanBox();
-    saveEnabler.listenTo(rejectImplicitMerges);
-    grid.addHtml(AdminConstants.I.rejectImplicitMerges(), rejectImplicitMerges);
-
-    privateByDefault = newInheritedBooleanBox();
-    saveEnabler.listenTo(privateByDefault);
-    grid.addHtml(AdminConstants.I.privateByDefault(), privateByDefault);
-
-    workInProgressByDefault = newInheritedBooleanBox();
-    saveEnabler.listenTo(workInProgressByDefault);
-    grid.addHtml(AdminConstants.I.workInProgressByDefault(), workInProgressByDefault);
-
-    enableReviewerByEmail = newInheritedBooleanBox();
-    saveEnabler.listenTo(enableReviewerByEmail);
-    grid.addHtml(AdminConstants.I.enableReviewerByEmail(), enableReviewerByEmail);
-
-    matchAuthorToCommitterDate = newInheritedBooleanBox();
-    saveEnabler.listenTo(matchAuthorToCommitterDate);
-    grid.addHtml(AdminConstants.I.matchAuthorToCommitterDate(), matchAuthorToCommitterDate);
-
-    maxObjectSizeLimit = new NpTextBox();
-    saveEnabler.listenTo(maxObjectSizeLimit);
-    effectiveMaxObjectSizeLimit = new Label();
-    effectiveMaxObjectSizeLimit.setStyleName(
-        Gerrit.RESOURCES.css().maxObjectSizeLimitEffectiveLabel());
-    HorizontalPanel p = new HorizontalPanel();
-    p.add(maxObjectSizeLimit);
-    p.add(effectiveMaxObjectSizeLimit);
-    grid.addHtml(AdminConstants.I.headingMaxObjectSizeLimit(), p);
-  }
-
-  private static ListBox newInheritedBooleanBox() {
-    ListBox box = new ListBox();
-    for (InheritableBoolean b : InheritableBoolean.values()) {
-      box.addItem(b.name(), b.name());
-    }
-    return box;
-  }
-
-  /**
-   * Enables the {@link #contentMerge} checkbox if the selected submit type allows the usage of
-   * content merge. If the submit type (currently only 'Fast Forward Only') does not allow content
-   * merge the useContentMerge checkbox gets disabled.
-   */
-  private void setEnabledForUseContentMerge() {
-    if (SubmitType.FAST_FORWARD_ONLY.equals(
-        SubmitType.valueOf(submitType.getValue(submitType.getSelectedIndex())))) {
-      contentMerge.setEnabled(false);
-      InheritedBooleanInfo b = InheritedBooleanInfo.create();
-      b.setConfiguredValue(InheritableBoolean.FALSE);
-      setBool(contentMerge, b);
-    } else {
-      contentMerge.setEnabled(submitType.isEnabled());
-    }
-  }
-
-  private void initAgreements() {
-    grid.addHeader(new SmallHeading(AdminConstants.I.headingAgreements()));
-
-    contributorAgreements = newInheritedBooleanBox();
-    if (Gerrit.info().auth().useContributorAgreements()) {
-      saveEnabler.listenTo(contributorAgreements);
-      grid.add(AdminConstants.I.useContributorAgreements(), contributorAgreements);
-    }
-
-    signedOffBy = newInheritedBooleanBox();
-    saveEnabler.listenTo(signedOffBy);
-    grid.addHtml(AdminConstants.I.useSignedOffBy(), signedOffBy);
-  }
-
-  private void setSubmitType(SubmitTypeInfo newSubmitType) {
-    int index = -1;
-    if (newSubmitType != null) {
-      for (int i = 0; i < submitType.getItemCount(); i++) {
-        if (submitType.getValue(i).equals(SubmitType.INHERIT.name())) {
-          submitType.setItemText(i, getInheritString(newSubmitType));
-        }
-        if (newSubmitType.configuredValue().name().equals(submitType.getValue(i))) {
-          index = i;
-        }
-      }
-      submitType.setSelectedIndex(index);
-      setEnabledForUseContentMerge();
-    }
-  }
-
-  private static String getInheritString(SubmitTypeInfo submitType) {
-    return Util.toLongString(SubmitType.INHERIT)
-        + " ("
-        + Util.toLongString(submitType.inheritedValue())
-        + ")";
-  }
-
-  private void setState(ProjectState newState) {
-    if (state != null) {
-      for (int i = 0; i < state.getItemCount(); i++) {
-        if (newState.name().equals(state.getValue(i))) {
-          state.setSelectedIndex(i);
-          break;
-        }
-      }
-    }
-  }
-
-  private void setBool(ListBox box, InheritedBooleanInfo inheritedBoolean) {
-    if (box == null) {
-      return;
-    }
-    int inheritedIndex = -1;
-    for (int i = 0; i < box.getItemCount(); i++) {
-      if (box.getValue(i).startsWith(InheritableBoolean.INHERIT.name())) {
-        inheritedIndex = i;
-      }
-      if (box.getValue(i).startsWith(inheritedBoolean.configuredValue().name())) {
-        box.setSelectedIndex(i);
-      }
-    }
-    if (inheritedIndex >= 0) {
-      if (Gerrit.info().gerrit().isAllProjects(getProjectKey())) {
-        if (box.getSelectedIndex() == inheritedIndex) {
-          for (int i = 0; i < box.getItemCount(); i++) {
-            if (box.getValue(i).equals(InheritableBoolean.FALSE.name())) {
-              box.setSelectedIndex(i);
-              break;
-            }
-          }
-        }
-        box.removeItem(inheritedIndex);
-      } else {
-        box.setItemText(
-            inheritedIndex,
-            InheritableBoolean.INHERIT.name() + " (" + inheritedBoolean.inheritedValue() + ")");
-      }
-    }
-  }
-
-  private static InheritableBoolean getBool(ListBox box) {
-    int i = box.getSelectedIndex();
-    if (i >= 0) {
-      final String selectedValue = box.getValue(i);
-      if (selectedValue.startsWith(InheritableBoolean.INHERIT.name())) {
-        return InheritableBoolean.INHERIT;
-      }
-      return InheritableBoolean.valueOf(selectedValue);
-    }
-    return InheritableBoolean.INHERIT;
-  }
-
-  void display(ConfigInfo result) {
-    descTxt.setText(result.description());
-    setBool(contributorAgreements, result.useContributorAgreements());
-    setBool(signedOffBy, result.useSignedOffBy());
-    setBool(contentMerge, result.useContentMerge());
-    setBool(newChangeForAllNotInTarget, result.createNewChangeForAllNotInTarget());
-    setBool(requireChangeID, result.requireChangeId());
-    if (Gerrit.info().receive().enableSignedPush()) {
-      setBool(enableSignedPush, result.enableSignedPush());
-      setBool(requireSignedPush, result.requireSignedPush());
-    }
-    setBool(rejectImplicitMerges, result.rejectImplicitMerges());
-    setBool(privateByDefault, result.privateByDefault());
-    setBool(workInProgressByDefault, result.workInProgressByDefault());
-    setBool(enableReviewerByEmail, result.enableReviewerByEmail());
-    setBool(matchAuthorToCommitterDate, result.matchAuthorToCommitterDate());
-    setSubmitType(result.defaultSubmitType());
-    setState(result.state());
-    maxObjectSizeLimit.setText(result.maxObjectSizeLimit().configuredValue());
-    if (result.maxObjectSizeLimit().value() != null) {
-      effectiveMaxObjectSizeLimit.setText(
-          AdminMessages.I.effectiveMaxObjectSizeLimit(result.maxObjectSizeLimit().value()));
-      if (result.maxObjectSizeLimit().summary() != null) {
-        effectiveMaxObjectSizeLimit.setTitle(result.maxObjectSizeLimit().summary());
-      }
-    } else {
-      effectiveMaxObjectSizeLimit.setText(AdminMessages.I.noMaxObjectSizeLimit());
-    }
-
-    saveProject.setEnabled(false);
-    initPluginOptions(result);
-    initProjectActions(result);
-  }
-
-  private void initPluginOptions(ConfigInfo info) {
-    pluginOptionsPanel.clear();
-    pluginConfigWidgets = new HashMap<>();
-
-    for (String pluginName : info.pluginConfig().keySet()) {
-      Map<String, HasEnabled> widgetMap = new HashMap<>();
-      pluginConfigWidgets.put(pluginName, widgetMap);
-      LabeledWidgetsGrid g = new LabeledWidgetsGrid();
-      g.addHeader(new SmallHeading(AdminMessages.I.pluginProjectOptionsTitle(pluginName)));
-      pluginOptionsPanel.add(g);
-      NativeMap<ConfigParameterInfo> pluginConfig = info.pluginConfig(pluginName);
-      pluginConfig.copyKeysIntoChildren("name");
-      for (ConfigParameterInfo param : Natives.asList(pluginConfig.values())) {
-        HasEnabled w;
-        switch (param.type()) {
-          case "STRING":
-          case "INT":
-          case "LONG":
-            w = renderTextBox(g, param);
-            break;
-          case "BOOLEAN":
-            w = renderCheckBox(g, param);
-            break;
-          case "LIST":
-            w = renderListBox(g, param);
-            break;
-          case "ARRAY":
-            w = renderStringListPanel(g, param);
-            break;
-          default:
-            throw new UnsupportedOperationException("unsupported widget type");
-        }
-        if (param.editable()) {
-          widgetMap.put(param.name(), w);
-        } else {
-          w.setEnabled(false);
-        }
-      }
-    }
-
-    enableForm();
-  }
-
-  private TextBox renderTextBox(LabeledWidgetsGrid g, ConfigParameterInfo param) {
-    NpTextBox textBox = param.type().equals("STRING") ? new NpTextBox() : new NpIntTextBox();
-    if (param.inheritable()) {
-      textBox.setValue(param.configuredValue());
-      Label inheritedLabel =
-          new Label(AdminMessages.I.pluginProjectInheritedValue(param.inheritedValue()));
-      inheritedLabel.setStyleName(Gerrit.RESOURCES.css().pluginProjectConfigInheritedValue());
-      HorizontalPanel p = new HorizontalPanel();
-      p.add(textBox);
-      p.add(inheritedLabel);
-      addWidget(g, p, param);
-    } else {
-      textBox.setValue(param.value());
-      addWidget(g, textBox, param);
-    }
-    if (textBox.getValue().length() > textBox.getVisibleLength()) {
-      textBox.setVisibleLength(textBox.getValue().length());
-    }
-    saveEnabler.listenTo(textBox);
-    return textBox;
-  }
-
-  private CheckBox renderCheckBox(LabeledWidgetsGrid g, ConfigParameterInfo param) {
-    CheckBox checkBox = new CheckBox(getDisplayName(param));
-    checkBox.setValue(Boolean.parseBoolean(param.value()));
-    HorizontalPanel p = new HorizontalPanel();
-    p.add(checkBox);
-    if (param.description() != null) {
-      Image infoImg = new Image(Gerrit.RESOURCES.info());
-      infoImg.setTitle(param.description());
-      p.add(infoImg);
-    }
-    if (param.warning() != null) {
-      Image warningImg = new Image(Gerrit.RESOURCES.warning());
-      warningImg.setTitle(param.warning());
-      p.add(warningImg);
-    }
-    g.add((String) null, p);
-    saveEnabler.listenTo(checkBox);
-    return checkBox;
-  }
-
-  private ListBox renderListBox(LabeledWidgetsGrid g, ConfigParameterInfo param) {
-    if (param.permittedValues() == null) {
-      return null;
-    }
-    ListBox listBox = new ListBox();
-    if (param.inheritable()) {
-      listBox.addItem(AdminMessages.I.pluginProjectInheritedListValue(param.inheritedValue()));
-      if (param.configuredValue() == null) {
-        listBox.setSelectedIndex(0);
-      }
-      for (int i = 0; i < param.permittedValues().length(); i++) {
-        String pv = param.permittedValues().get(i);
-        listBox.addItem(pv);
-        if (pv.equals(param.configuredValue())) {
-          listBox.setSelectedIndex(i + 1);
-        }
-      }
-    } else {
-      for (int i = 0; i < param.permittedValues().length(); i++) {
-        String pv = param.permittedValues().get(i);
-        listBox.addItem(pv);
-        if (pv.equals(param.value())) {
-          listBox.setSelectedIndex(i);
-        }
-      }
-    }
-
-    if (param.editable()) {
-      saveEnabler.listenTo(listBox);
-      addWidget(g, listBox, param);
-    } else {
-      listBox.setEnabled(false);
-
-      if (param.inheritable() && listBox.getSelectedIndex() != 0) {
-        // the inherited value is not selected,
-        // since the listBox is disabled the inherited value cannot be
-        // seen and we have to display it explicitly
-        Label inheritedLabel =
-            new Label(AdminMessages.I.pluginProjectInheritedValue(param.inheritedValue()));
-        inheritedLabel.setStyleName(Gerrit.RESOURCES.css().pluginProjectConfigInheritedValue());
-        HorizontalPanel p = new HorizontalPanel();
-        p.add(listBox);
-        p.add(inheritedLabel);
-        addWidget(g, p, param);
-      } else {
-        addWidget(g, listBox, param);
-      }
-    }
-
-    return listBox;
-  }
-
-  private StringListPanel renderStringListPanel(LabeledWidgetsGrid g, ConfigParameterInfo param) {
-    StringListPanel p =
-        new StringListPanel(null, Arrays.asList(getDisplayName(param)), saveProject, false);
-    List<List<String>> values = new ArrayList<>();
-    for (String v : Natives.asList(param.values())) {
-      values.add(Arrays.asList(v));
-    }
-    p.display(values);
-    if (!param.editable()) {
-      p.setEnabled(false);
-    }
-    addWidget(g, p, param);
-    return p;
-  }
-
-  private void addWidget(LabeledWidgetsGrid g, Widget w, ConfigParameterInfo param) {
-    if (param.description() != null || param.warning() != null) {
-      HorizontalPanel p = new HorizontalPanel();
-      p.add(new Label(getDisplayName(param)));
-      if (param.description() != null) {
-        Image infoImg = new Image(Gerrit.RESOURCES.info());
-        infoImg.setTitle(param.description());
-        p.add(infoImg);
-      }
-      if (param.warning() != null) {
-        Image warningImg = new Image(Gerrit.RESOURCES.warning());
-        warningImg.setTitle(param.warning());
-        p.add(warningImg);
-      }
-      p.add(new Label(":"));
-      g.add(p, w);
-    } else {
-      g.add(getDisplayName(param), w);
-    }
-  }
-
-  private String getDisplayName(ConfigParameterInfo param) {
-    return param.displayName() != null ? param.displayName() : param.name();
-  }
-
-  private void initProjectActions(ConfigInfo info) {
-    actionsGrid.clear(true);
-    actionsGrid.removeAllRows();
-    boolean showCreateChange = Gerrit.isSignedIn();
-
-    NativeMap<ActionInfo> actions = info.actions();
-    if (actions == null) {
-      actions = NativeMap.create().cast();
-    }
-    if (actions.isEmpty() && !showCreateChange) {
-      return;
-    }
-    actions.copyKeysIntoChildren("id");
-    actionsGrid.addHeader(new SmallHeading(AdminConstants.I.headingProjectCommands()));
-    FlowPanel actionsPanel = new FlowPanel();
-    actionsPanel.setStyleName(Gerrit.RESOURCES.css().projectActions());
-    actionsPanel.setVisible(true);
-    actionsGrid.add(AdminConstants.I.headingCommands(), actionsPanel);
-
-    for (String id : actions.keySet()) {
-      actionsPanel.add(new ActionButton(getProjectKey(), actions.get(id)));
-    }
-
-    // TODO: The user should have create permission on the branch referred to by
-    // HEAD. This would have to happen on the server side.
-    if (showCreateChange) {
-      actionsPanel.add(createChangeAction());
-    }
-
-    if (isOwner && configVisible) {
-      actionsPanel.add(createEditConfigAction());
-    }
-  }
-
-  private Button createChangeAction() {
-    final Button createChange = new Button(AdminConstants.I.buttonCreateChange());
-    createChange.setStyleName("");
-    createChange.setTitle(AdminConstants.I.buttonCreateChangeDescription());
-    createChange.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            CreateChangeAction.call(createChange, getProjectKey().get());
-          }
-        });
-    return createChange;
-  }
-
-  private Button createEditConfigAction() {
-    final Button editConfig = new Button(AdminConstants.I.buttonEditConfig());
-    editConfig.setStyleName("");
-    editConfig.setTitle(AdminConstants.I.buttonEditConfigDescription());
-    editConfig.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            EditConfigAction.call(editConfig, getProjectKey());
-          }
-        });
-    return editConfig;
-  }
-
-  private void doSave() {
-    enableForm(false);
-    saveProject.setEnabled(false);
-    InheritableBoolean esp = enableSignedPush != null ? getBool(enableSignedPush) : null;
-    InheritableBoolean rsp = requireSignedPush != null ? getBool(requireSignedPush) : null;
-    ProjectApi.setConfig(
-        getProjectKey(),
-        descTxt.getText().trim(),
-        getBool(contributorAgreements),
-        getBool(contentMerge),
-        getBool(signedOffBy),
-        getBool(newChangeForAllNotInTarget),
-        getBool(requireChangeID),
-        esp,
-        rsp,
-        getBool(rejectImplicitMerges),
-        getBool(privateByDefault),
-        getBool(workInProgressByDefault),
-        getBool(enableReviewerByEmail),
-        getBool(matchAuthorToCommitterDate),
-        maxObjectSizeLimit.getText().trim(),
-        SubmitType.valueOf(submitType.getValue(submitType.getSelectedIndex())),
-        ProjectState.valueOf(state.getValue(state.getSelectedIndex())),
-        getPluginConfigValues(),
-        new GerritCallback<ConfigInfo>() {
-          @Override
-          public void onSuccess(ConfigInfo result) {
-            enableForm();
-            display(result);
-          }
-
-          @Override
-          public void onFailure(Throwable caught) {
-            enableForm();
-            super.onFailure(caught);
-          }
-        });
-  }
-
-  private Map<String, Map<String, ConfigParameterValue>> getPluginConfigValues() {
-    Map<String, Map<String, ConfigParameterValue>> pluginConfigValues =
-        new HashMap<>(pluginConfigWidgets.size());
-    for (Entry<String, Map<String, HasEnabled>> e : pluginConfigWidgets.entrySet()) {
-      Map<String, ConfigParameterValue> values = new HashMap<>(e.getValue().size());
-      pluginConfigValues.put(e.getKey(), values);
-      for (Entry<String, HasEnabled> e2 : e.getValue().entrySet()) {
-        HasEnabled widget = e2.getValue();
-        if (widget instanceof TextBox) {
-          values.put(
-              e2.getKey(),
-              ConfigParameterValue.create().value(((TextBox) widget).getValue().trim()));
-        } else if (widget instanceof CheckBox) {
-          values.put(
-              e2.getKey(),
-              ConfigParameterValue.create()
-                  .value(Boolean.toString(((CheckBox) widget).getValue())));
-        } else if (widget instanceof ListBox) {
-          ListBox listBox = (ListBox) widget;
-          // the inherited value is at index 0,
-          // if it is selected no value should be set on this project
-          String value =
-              listBox.getSelectedIndex() > 0 ? listBox.getValue(listBox.getSelectedIndex()) : null;
-          values.put(e2.getKey(), ConfigParameterValue.create().value(value));
-        } else if (widget instanceof StringListPanel) {
-          values.put(
-              e2.getKey(),
-              ConfigParameterValue.create()
-                  .values(((StringListPanel) widget).getValues(0).toArray(new String[] {})));
-        } else {
-          throw new UnsupportedOperationException("unsupported widget type");
-        }
-      }
-    }
-    return pluginConfigValues;
-  }
-
-  public static class ProjectDownloadPanel extends DownloadPanel {
-    public ProjectDownloadPanel(String project, boolean isAllowsAnonymous) {
-      super(project, isAllowsAnonymous);
-    }
-
-    @Override
-    protected List<DownloadCommandInfo> getCommands(DownloadSchemeInfo schemeInfo) {
-      return schemeInfo.cloneCommands(project);
-    }
-  }
-
-  private static class LabeledWidgetsGrid extends FlexTable {
-    private String labelSuffix;
-
-    LabeledWidgetsGrid() {
-      super();
-      labelSuffix = ":";
-    }
-
-    private void addHeader(Widget widget) {
-      int row = getRowCount();
-      insertRow(row);
-      setWidget(row, 0, widget);
-      getCellFormatter().getElement(row, 0).setAttribute("colSpan", "2");
-    }
-
-    private void add(String label, boolean labelIsHtml, Widget widget) {
-      int row = getRowCount();
-      insertRow(row);
-      if (label != null) {
-        if (labelIsHtml) {
-          setHTML(row, 0, label + labelSuffix);
-        } else {
-          setText(row, 0, label + labelSuffix);
-        }
-      }
-      setWidget(row, 1, widget);
-    }
-
-    public void add(String label, Widget widget) {
-      add(label, false, widget);
-    }
-
-    public void addHtml(String label, Widget widget) {
-      add(label, true, widget);
-    }
-
-    public void add(Widget label, Widget widget) {
-      int row = getRowCount();
-      insertRow(row);
-      setWidget(row, 0, label);
-      setWidget(row, 1, widget);
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
deleted file mode 100644
index 2a03136..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
+++ /dev/null
@@ -1,259 +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.client.admin;
-
-import static com.google.gerrit.common.PageLinks.ADMIN_PROJECTS;
-
-import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.info.WebLinkInfo;
-import com.google.gerrit.client.projects.ProjectInfo;
-import com.google.gerrit.client.projects.ProjectMap;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.ui.HighlightingInlineHyperlink;
-import com.google.gerrit.client.ui.Hyperlink;
-import com.google.gerrit.client.ui.PagingHyperlink;
-import com.google.gerrit.client.ui.ProjectSearchLink;
-import com.google.gerrit.client.ui.ProjectsTable;
-import com.google.gerrit.common.PageLinks;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyUpEvent;
-import com.google.gwt.event.dom.client.KeyUpHandler;
-import com.google.gwt.user.client.History;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.HorizontalPanel;
-import com.google.gwt.user.client.ui.Label;
-import com.google.gwtexpui.globalkey.client.NpTextBox;
-import java.util.List;
-
-public class ProjectListScreen extends PaginatedProjectScreen {
-  private Hyperlink prev;
-  private Hyperlink next;
-  private ProjectsTable projects;
-  private NpTextBox filterTxt;
-
-  private Query query;
-
-  public ProjectListScreen() {
-    super(null);
-  }
-
-  public ProjectListScreen(String params) {
-    this();
-    parseToken(params);
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    query = new Query(match).start(start).run();
-  }
-
-  @Override
-  public String getScreenToken() {
-    return ADMIN_PROJECTS;
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    setPageTitle(AdminConstants.I.projectListTitle());
-    initPageHeader();
-
-    prev = PagingHyperlink.createPrev();
-    prev.setVisible(false);
-
-    next = PagingHyperlink.createNext();
-    next.setVisible(false);
-
-    projects =
-        new ProjectsTable() {
-          @Override
-          protected void initColumnHeaders() {
-            super.initColumnHeaders();
-            table.setText(0, ProjectsTable.C_REPO_BROWSER, AdminConstants.I.projectRepoBrowser());
-            table
-                .getFlexCellFormatter()
-                .addStyleName(0, ProjectsTable.C_REPO_BROWSER, Gerrit.RESOURCES.css().dataHeader());
-          }
-
-          @Override
-          protected void onOpenRow(int row) {
-            History.newItem(link(getRowItem(row)));
-          }
-
-          private String link(ProjectInfo item) {
-            return Dispatcher.toProject(item.name_key());
-          }
-
-          @Override
-          protected void insert(int row, ProjectInfo k) {
-            super.insert(row, k);
-            table
-                .getFlexCellFormatter()
-                .addStyleName(row, ProjectsTable.C_REPO_BROWSER, Gerrit.RESOURCES.css().dataCell());
-          }
-
-          @Override
-          protected void populate(int row, ProjectInfo k) {
-            populateState(row, k);
-            FlowPanel fp = new FlowPanel();
-            fp.add(new ProjectSearchLink(k.name_key()));
-            fp.add(new HighlightingInlineHyperlink(k.name(), link(k), match));
-            table.setWidget(row, ProjectsTable.C_NAME, fp);
-            table.setText(row, ProjectsTable.C_DESCRIPTION, k.description());
-            addWebLinks(row, k);
-
-            setRowItem(row, k);
-          }
-
-          private void addWebLinks(int row, ProjectInfo k) {
-            List<WebLinkInfo> webLinks = Natives.asList(k.webLinks());
-            if (webLinks != null && !webLinks.isEmpty()) {
-              FlowPanel p = new FlowPanel();
-              table.setWidget(row, ProjectsTable.C_REPO_BROWSER, p);
-              for (WebLinkInfo weblink : webLinks) {
-                p.add(weblink.toAnchor());
-              }
-            }
-          }
-        };
-    projects.setSavePointerId(PageLinks.ADMIN_PROJECTS);
-
-    add(projects);
-    final HorizontalPanel buttons = new HorizontalPanel();
-    buttons.setStyleName(Gerrit.RESOURCES.css().changeTablePrevNextLinks());
-    buttons.add(prev);
-    buttons.add(next);
-    add(buttons);
-  }
-
-  private void initPageHeader() {
-    final HorizontalPanel hp = new HorizontalPanel();
-    hp.setStyleName(Gerrit.RESOURCES.css().projectFilterPanel());
-    final Label filterLabel = new Label(AdminConstants.I.projectFilter());
-    filterLabel.setStyleName(Gerrit.RESOURCES.css().projectFilterLabel());
-    hp.add(filterLabel);
-    filterTxt = new NpTextBox();
-    filterTxt.setValue(match);
-    filterTxt.addKeyUpHandler(
-        new KeyUpHandler() {
-          @Override
-          public void onKeyUp(KeyUpEvent event) {
-            Query q =
-                new Query(filterTxt.getValue())
-                    .open(event.getNativeKeyCode() == KeyCodes.KEY_ENTER);
-            if (match.equals(q.qMatch)) {
-              q.start(start);
-            }
-            if (q.open || !match.equals(q.qMatch)) {
-              if (query == null) {
-                q.run();
-              }
-              query = q;
-            }
-          }
-        });
-    hp.add(filterTxt);
-    add(hp);
-  }
-
-  @Override
-  public void onShowView() {
-    super.onShowView();
-    if (match != null) {
-      filterTxt.setCursorPos(match.length());
-    }
-    filterTxt.setFocus(true);
-  }
-
-  @Override
-  public void registerKeys() {
-    super.registerKeys();
-    projects.setRegisterKeys(true);
-  }
-
-  private class Query {
-    private final String qMatch;
-    private int qStart;
-    private boolean open;
-
-    Query(String match) {
-      this.qMatch = match;
-    }
-
-    Query start(int start) {
-      this.qStart = start;
-      return this;
-    }
-
-    Query open(boolean open) {
-      this.open = open;
-      return this;
-    }
-
-    Query run() {
-      int limit = open ? 1 : pageSize + 1;
-      ProjectMap.match(
-          qMatch,
-          limit,
-          qStart,
-          new GerritCallback<ProjectMap>() {
-            @Override
-            public void onSuccess(ProjectMap result) {
-              if (!isAttached()) {
-                // View has been disposed.
-              } else if (query == Query.this) {
-                query = null;
-                showMap(result);
-              } else {
-                query.run();
-              }
-            }
-          });
-      return this;
-    }
-
-    private void showMap(ProjectMap result) {
-      if (open && !result.isEmpty()) {
-        Gerrit.display(PageLinks.toProject(result.values().get(0).name_key()));
-        return;
-      }
-
-      setToken(getTokenForScreen(qMatch, qStart));
-      ProjectListScreen.this.match = qMatch;
-      ProjectListScreen.this.start = qStart;
-
-      if (result.size() <= pageSize) {
-        projects.display(result);
-        next.setVisible(false);
-      } else {
-        projects.displaySubset(result, 0, result.size() - 1);
-        setupNavigationLink(next, qMatch, qStart + pageSize);
-      }
-
-      if (qStart > 0) {
-        setupNavigationLink(prev, qMatch, qStart - pageSize);
-      } else {
-        prev.setVisible(false);
-      }
-
-      if (!isCurrentView()) {
-        display();
-      }
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectScreen.java
deleted file mode 100644
index dc964b8..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectScreen.java
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT 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.client.admin;
-
-import com.google.gerrit.client.ui.Screen;
-import com.google.gerrit.reviewdb.client.Project;
-
-public abstract class ProjectScreen extends Screen {
-  public static final String INFO = "info";
-  public static final String BRANCHES = "branches";
-  public static final String ACCESS = "access";
-  public static final String DASHBOARDS = "dashboards";
-  public static final String TAGS = "tags";
-
-  protected static String savedPanel;
-  protected static Project.NameKey savedKey;
-
-  public static String getSavedPanel() {
-    return savedPanel;
-  }
-
-  public static Project.NameKey getSavedKey() {
-    return savedKey;
-  }
-
-  private final Project.NameKey name;
-
-  public ProjectScreen(Project.NameKey toShow) {
-    name = toShow;
-  }
-
-  public Project.NameKey getProjectKey() {
-    return name;
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    if (name != null) {
-      setPageTitle(AdminMessages.I.project(name.get()));
-    }
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    savedKey = name;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectTagsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectTagsScreen.java
deleted file mode 100644
index 22c331d..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectTagsScreen.java
+++ /dev/null
@@ -1,571 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.admin;
-
-import static com.google.gerrit.client.ui.Util.highlight;
-
-import com.google.gerrit.client.ConfirmationCallback;
-import com.google.gerrit.client.ConfirmationDialog;
-import com.google.gerrit.client.ErrorDialog;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.VoidResult;
-import com.google.gerrit.client.access.AccessMap;
-import com.google.gerrit.client.access.ProjectAccessInfo;
-import com.google.gerrit.client.info.WebLinkInfo;
-import com.google.gerrit.client.projects.ProjectApi;
-import com.google.gerrit.client.projects.TagInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.client.ui.HintTextBox;
-import com.google.gerrit.client.ui.Hyperlink;
-import com.google.gerrit.client.ui.NavigationTable;
-import com.google.gerrit.client.ui.PagingHyperlink;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.core.client.Scheduler;
-import com.google.gwt.core.client.Scheduler.ScheduledCommand;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwt.event.dom.client.KeyUpEvent;
-import com.google.gwt.event.dom.client.KeyUpHandler;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.logical.shared.ValueChangeHandler;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.HorizontalPanel;
-import com.google.gwt.user.client.ui.InlineHTML;
-import com.google.gwt.user.client.ui.Label;
-import com.google.gwt.user.client.ui.TextBox;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtexpui.globalkey.client.NpTextBox;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-public class ProjectTagsScreen extends PaginatedProjectScreen {
-  private Hyperlink prev;
-  private Hyperlink next;
-  private TagsTable tagTable;
-  private Button delTag;
-  private Button addTag;
-  private HintTextBox nameTxtBox;
-  private HintTextBox irevTxtBox;
-  private HintTextBox annotationTxtBox;
-  private FlowPanel addPanel;
-  private NpTextBox filterTxt;
-  private Query query;
-
-  public ProjectTagsScreen(Project.NameKey toShow) {
-    super(toShow);
-  }
-
-  @Override
-  public String getScreenToken() {
-    return PageLinks.toProjectTags(getProjectKey());
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    addPanel.setVisible(false);
-    AccessMap.get(
-        getProjectKey(),
-        new GerritCallback<ProjectAccessInfo>() {
-          @Override
-          public void onSuccess(ProjectAccessInfo result) {
-            addPanel.setVisible(result.canAddTagRefs());
-          }
-        });
-    query = new Query(match).start(start).run();
-    savedPanel = TAGS;
-  }
-
-  private void updateForm() {
-    tagTable.updateDeleteButton();
-    addTag.setEnabled(true);
-    nameTxtBox.setEnabled(true);
-    irevTxtBox.setEnabled(true);
-    annotationTxtBox.setEnabled(true);
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    initPageHeader();
-
-    prev = PagingHyperlink.createPrev();
-    prev.setVisible(false);
-
-    next = PagingHyperlink.createNext();
-    next.setVisible(false);
-
-    addPanel = new FlowPanel();
-
-    Grid addGrid = new Grid(3, 2);
-    addGrid.setStyleName(Gerrit.RESOURCES.css().addBranch());
-    int texBoxLength = 50;
-
-    KeyPressHandler onKeyPress =
-        new KeyPressHandler() {
-          @Override
-          public void onKeyPress(KeyPressEvent event) {
-            if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
-              doAddNewTag();
-            }
-          }
-        };
-
-    nameTxtBox = new HintTextBox();
-    nameTxtBox.setVisibleLength(texBoxLength);
-    nameTxtBox.setHintText(AdminConstants.I.defaultTagName());
-    nameTxtBox.addKeyPressHandler(onKeyPress);
-    addGrid.setText(0, 0, AdminConstants.I.columnTagName() + ":");
-    addGrid.setWidget(0, 1, nameTxtBox);
-
-    irevTxtBox = new HintTextBox();
-    irevTxtBox.setVisibleLength(texBoxLength);
-    irevTxtBox.setHintText(AdminConstants.I.defaultRevisionSpec());
-    irevTxtBox.addKeyPressHandler(onKeyPress);
-    addGrid.setText(1, 0, AdminConstants.I.revision() + ":");
-    addGrid.setWidget(1, 1, irevTxtBox);
-
-    annotationTxtBox = new HintTextBox();
-    annotationTxtBox.setVisibleLength(texBoxLength);
-    annotationTxtBox.setHintText(AdminConstants.I.annotation());
-    annotationTxtBox.addKeyPressHandler(onKeyPress);
-    addGrid.setText(2, 0, AdminConstants.I.columnTagAnnotation() + ":");
-    addGrid.setWidget(2, 1, annotationTxtBox);
-
-    addTag = new Button(AdminConstants.I.buttonAddTag());
-    addTag.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            doAddNewTag();
-          }
-        });
-    addPanel.add(addGrid);
-    addPanel.add(addTag);
-
-    tagTable = new TagsTable();
-
-    delTag = new Button(AdminConstants.I.buttonDeleteTag());
-    delTag.setStyleName(Gerrit.RESOURCES.css().branchTableDeleteButton());
-    delTag.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            tagTable.deleteChecked();
-          }
-        });
-
-    HorizontalPanel buttons = new HorizontalPanel();
-    buttons.setStyleName(Gerrit.RESOURCES.css().branchTablePrevNextLinks());
-    buttons.add(delTag);
-    buttons.add(prev);
-    buttons.add(next);
-    add(tagTable);
-    add(buttons);
-    add(addPanel);
-  }
-
-  private void initPageHeader() {
-    parseToken();
-    HorizontalPanel hp = new HorizontalPanel();
-    hp.setStyleName(Gerrit.RESOURCES.css().projectFilterPanel());
-    Label filterLabel = new Label(AdminConstants.I.projectFilter());
-    filterLabel.setStyleName(Gerrit.RESOURCES.css().projectFilterLabel());
-    hp.add(filterLabel);
-    filterTxt = new NpTextBox();
-    filterTxt.setValue(match);
-    filterTxt.addKeyUpHandler(
-        new KeyUpHandler() {
-          @Override
-          public void onKeyUp(KeyUpEvent event) {
-            Query q = new Query(filterTxt.getValue());
-            if (match.equals(q.qMatch)) {
-              q.start(start);
-            } else {
-              if (query == null) {
-                q.run();
-              }
-              query = q;
-            }
-          }
-        });
-    hp.add(filterTxt);
-    add(hp);
-  }
-
-  private void doAddNewTag() {
-    String tagName = nameTxtBox.getText().trim();
-    if (tagName.isEmpty()) {
-      nameTxtBox.setFocus(true);
-      return;
-    }
-
-    String rev = irevTxtBox.getText().trim();
-    if (rev.isEmpty()) {
-      irevTxtBox.setText("HEAD");
-      Scheduler.get()
-          .scheduleDeferred(
-              new ScheduledCommand() {
-                @Override
-                public void execute() {
-                  irevTxtBox.selectAll();
-                  irevTxtBox.setFocus(true);
-                }
-              });
-      return;
-    }
-
-    String annotation = annotationTxtBox.getText().trim();
-    if (annotation.isEmpty()) {
-      annotation = null;
-    }
-
-    addTag.setEnabled(false);
-    ProjectApi.createTag(
-        getProjectKey(),
-        tagName,
-        rev,
-        annotation,
-        new GerritCallback<TagInfo>() {
-          @Override
-          public void onSuccess(TagInfo tag) {
-            showAddedTag(tag);
-            nameTxtBox.setText("");
-            irevTxtBox.setText("");
-            annotationTxtBox.setText("");
-            query = new Query(match).start(start).run();
-          }
-
-          @Override
-          public void onFailure(Throwable caught) {
-            addTag.setEnabled(true);
-            selectAllAndFocus(nameTxtBox);
-            new ErrorDialog(caught.getMessage()).center();
-          }
-        });
-  }
-
-  void showAddedTag(TagInfo tag) {
-    SafeHtmlBuilder b = new SafeHtmlBuilder();
-    b.openElement("b");
-    b.append(Gerrit.C.tagCreationConfirmationMessage());
-    b.closeElement("b");
-
-    b.openElement("p");
-    b.append(tag.ref());
-    b.closeElement("p");
-
-    ConfirmationDialog confirmationDialog =
-        new ConfirmationDialog(
-            Gerrit.C.tagCreationDialogTitle(),
-            b.toSafeHtml(),
-            new ConfirmationCallback() {
-              @Override
-              public void onOk() {
-                // do nothing
-              }
-            });
-    confirmationDialog.center();
-    confirmationDialog.setCancelVisible(false);
-  }
-
-  private static void selectAllAndFocus(TextBox textBox) {
-    textBox.selectAll();
-    textBox.setFocus(true);
-  }
-
-  private class TagsTable extends NavigationTable<TagInfo> {
-    private ValueChangeHandler<Boolean> updateDeleteHandler;
-    boolean canDelete;
-
-    TagsTable() {
-      table.setWidth("");
-      table.setText(0, 2, AdminConstants.I.columnTagName());
-      table.setText(0, 3, AdminConstants.I.columnTagRevision());
-
-      FlexCellFormatter fmt = table.getFlexCellFormatter();
-      fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().iconHeader());
-      fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
-      fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader());
-      fmt.addStyleName(0, 4, Gerrit.RESOURCES.css().dataHeader());
-
-      updateDeleteHandler =
-          new ValueChangeHandler<Boolean>() {
-            @Override
-            public void onValueChange(ValueChangeEvent<Boolean> event) {
-              updateDeleteButton();
-            }
-          };
-    }
-
-    Set<String> getCheckedRefs() {
-      Set<String> refs = new HashSet<>();
-      for (int row = 1; row < table.getRowCount(); row++) {
-        TagInfo k = getRowItem(row);
-        if (k != null
-            && table.getWidget(row, 1) instanceof CheckBox
-            && ((CheckBox) table.getWidget(row, 1)).getValue()) {
-          refs.add(k.ref());
-        }
-      }
-      return refs;
-    }
-
-    void setChecked(Set<String> refs) {
-      for (int row = 1; row < table.getRowCount(); row++) {
-        TagInfo k = getRowItem(row);
-        if (k != null && refs.contains(k.ref()) && table.getWidget(row, 1) instanceof CheckBox) {
-          ((CheckBox) table.getWidget(row, 1)).setValue(true);
-        }
-      }
-    }
-
-    void deleteChecked() {
-      final Set<String> refs = getCheckedRefs();
-
-      SafeHtmlBuilder b = new SafeHtmlBuilder();
-      b.openElement("b");
-      b.append(Gerrit.C.tagDeletionConfirmationMessage());
-      b.closeElement("b");
-
-      b.openElement("p");
-      boolean first = true;
-      for (String ref : refs) {
-        if (!first) {
-          b.append(",").br();
-        }
-        b.append(ref);
-        first = false;
-      }
-      b.closeElement("p");
-
-      if (refs.isEmpty()) {
-        updateDeleteButton();
-        return;
-      }
-
-      delTag.setEnabled(false);
-      ConfirmationDialog confirmationDialog =
-          new ConfirmationDialog(
-              Gerrit.C.tagDeletionDialogTitle(),
-              b.toSafeHtml(),
-              new ConfirmationCallback() {
-                @Override
-                public void onOk() {
-                  deleteTags(refs);
-                }
-
-                @Override
-                public void onCancel() {
-                  tagTable.updateDeleteButton();
-                }
-              });
-      confirmationDialog.center();
-    }
-
-    private void deleteTags(Set<String> tags) {
-      ProjectApi.deleteTags(
-          getProjectKey(),
-          tags,
-          new GerritCallback<VoidResult>() {
-            @Override
-            public void onSuccess(VoidResult result) {
-              query = new Query(match).start(start).run();
-            }
-
-            @Override
-            public void onFailure(Throwable caught) {
-              query = new Query(match).start(start).run();
-              super.onFailure(caught);
-            }
-          });
-    }
-
-    void display(List<TagInfo> tags) {
-      displaySubset(tags, 0, tags.size());
-    }
-
-    void displaySubset(List<TagInfo> tags, int fromIndex, int toIndex) {
-      canDelete = false;
-
-      while (1 < table.getRowCount()) {
-        table.removeRow(table.getRowCount() - 1);
-      }
-
-      for (TagInfo k : tags.subList(fromIndex, toIndex)) {
-        int row = table.getRowCount();
-        table.insertRow(row);
-        applyDataRowStyle(row);
-        populate(row, k);
-      }
-    }
-
-    void populate(int row, TagInfo k) {
-      if (k.canDelete()) {
-        CheckBox sel = new CheckBox();
-        sel.addValueChangeHandler(updateDeleteHandler);
-        table.setWidget(row, 1, sel);
-        canDelete = true;
-      } else {
-        table.setText(row, 1, "");
-      }
-
-      table.setWidget(row, 2, new InlineHTML(highlight(k.getShortName(), match)));
-
-      if (k.revision() != null) {
-        table.setText(row, 3, k.revision());
-      } else {
-        table.setText(row, 3, "");
-      }
-
-      FlowPanel actionsPanel = new FlowPanel();
-      if (k.webLinks() != null) {
-        for (WebLinkInfo webLink : Natives.asList(k.webLinks())) {
-          actionsPanel.add(webLink.toAnchor());
-        }
-      }
-      table.setWidget(row, 4, actionsPanel);
-
-      FlexCellFormatter fmt = table.getFlexCellFormatter();
-      String iconCellStyle = Gerrit.RESOURCES.css().iconCell();
-      String dataCellStyle = Gerrit.RESOURCES.css().dataCell();
-      fmt.addStyleName(row, 1, iconCellStyle);
-      fmt.addStyleName(row, 2, dataCellStyle);
-      fmt.addStyleName(row, 3, dataCellStyle);
-      fmt.addStyleName(row, 4, dataCellStyle);
-
-      setRowItem(row, k);
-    }
-
-    boolean hasTagCanDelete() {
-      return canDelete;
-    }
-
-    void updateDeleteButton() {
-      boolean on = false;
-      for (int row = 1; row < table.getRowCount(); row++) {
-        Widget w = table.getWidget(row, 1);
-        if (w != null && w instanceof CheckBox) {
-          CheckBox sel = (CheckBox) w;
-          if (sel.getValue()) {
-            on = true;
-            break;
-          }
-        }
-      }
-      delTag.setEnabled(on);
-    }
-
-    @Override
-    protected void onOpenRow(int row) {
-      if (row > 0) {
-        movePointerTo(row);
-      }
-    }
-
-    @Override
-    protected Object getRowItemKey(TagInfo item) {
-      return item.ref();
-    }
-  }
-
-  @Override
-  public void onShowView() {
-    super.onShowView();
-    if (match != null) {
-      filterTxt.setCursorPos(match.length());
-    }
-    filterTxt.setFocus(true);
-  }
-
-  private class Query {
-    private String qMatch;
-    private int qStart;
-
-    Query(String match) {
-      this.qMatch = match;
-    }
-
-    Query start(int start) {
-      this.qStart = start;
-      return this;
-    }
-
-    Query run() {
-      // Retrieve one more tag than page size to determine if there are more
-      // tags to display
-      ProjectApi.getTags(
-          getProjectKey(),
-          pageSize + 1,
-          qStart,
-          qMatch,
-          new ScreenLoadCallback<JsArray<TagInfo>>(ProjectTagsScreen.this) {
-            @Override
-            public void preDisplay(JsArray<TagInfo> result) {
-              if (!isAttached()) {
-                // View has been disposed.
-              } else if (query == Query.this) {
-                query = null;
-                showList(result);
-              } else {
-                query.run();
-              }
-            }
-          });
-      return this;
-    }
-
-    void showList(JsArray<TagInfo> result) {
-      setToken(getTokenForScreen(qMatch, qStart));
-      ProjectTagsScreen.this.match = qMatch;
-      ProjectTagsScreen.this.start = qStart;
-
-      if (result.length() <= pageSize) {
-        tagTable.display(Natives.asList(result));
-        next.setVisible(false);
-      } else {
-        tagTable.displaySubset(Natives.asList(result), 0, result.length() - 1);
-        setupNavigationLink(next, qMatch, qStart + pageSize);
-      }
-      if (qStart > 0) {
-        setupNavigationLink(prev, qMatch, qStart - pageSize);
-      } else {
-        prev.setVisible(false);
-      }
-
-      delTag.setVisible(tagTable.hasTagCanDelete());
-      Set<String> checkedRefs = tagTable.getCheckedRefs();
-      tagTable.setChecked(checkedRefs);
-      updateForm();
-
-      if (!isCurrentView()) {
-        display();
-      }
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/RangeBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/RangeBox.java
deleted file mode 100644
index 063a60c..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/RangeBox.java
+++ /dev/null
@@ -1,85 +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.client.admin;
-
-import com.google.gwt.editor.client.IsEditor;
-import com.google.gwt.editor.client.adapters.TakesValueEditor;
-import com.google.gwt.text.shared.Renderer;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.IntegerBox;
-import com.google.gwt.user.client.ui.ValueBoxBase.TextAlignment;
-import com.google.gwt.user.client.ui.ValueListBox;
-import java.io.IOException;
-
-abstract class RangeBox extends Composite implements IsEditor<TakesValueEditor<Integer>> {
-  static final RangeRenderer rangeRenderer = new RangeRenderer();
-
-  private static class RangeRenderer implements Renderer<Integer> {
-    @Override
-    public String render(Integer object) {
-      if (0 <= object) {
-        return "+" + object;
-      }
-      return String.valueOf(object);
-    }
-
-    @Override
-    public void render(Integer object, Appendable appendable) throws IOException {
-      appendable.append(render(object));
-    }
-  }
-
-  static class List extends RangeBox {
-    final ValueListBox<Integer> list;
-
-    List() {
-      list = new ValueListBox<>(rangeRenderer);
-      initWidget(list);
-    }
-
-    @Override
-    void setEnabled(boolean on) {
-      list.getElement().setPropertyBoolean("disabled", !on);
-    }
-
-    @Override
-    public TakesValueEditor<Integer> asEditor() {
-      return list.asEditor();
-    }
-  }
-
-  static class Box extends RangeBox {
-    private final IntegerBox box;
-
-    Box() {
-      box = new IntegerBox();
-      box.setVisibleLength(10);
-      box.setAlignment(TextAlignment.RIGHT);
-      initWidget(box);
-    }
-
-    @Override
-    void setEnabled(boolean on) {
-      box.getElement().setPropertyBoolean("disabled", !on);
-    }
-
-    @Override
-    public TakesValueEditor<Integer> asEditor() {
-      return box.asEditor();
-    }
-  }
-
-  abstract void setEnabled(boolean on);
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/RefPatternBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/RefPatternBox.java
deleted file mode 100644
index f1180cc..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/RefPatternBox.java
+++ /dev/null
@@ -1,95 +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.client.admin;
-
-import com.google.gwt.dom.client.Document;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwt.text.shared.Parser;
-import com.google.gwt.text.shared.Renderer;
-import com.google.gwt.user.client.ui.ValueBox;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-import java.io.IOException;
-import java.text.ParseException;
-
-public class RefPatternBox extends ValueBox<String> {
-  private static final Renderer<String> RENDERER =
-      new Renderer<String>() {
-        @Override
-        public String render(String ref) {
-          return ref;
-        }
-
-        @Override
-        public void render(String ref, Appendable dst) throws IOException {
-          dst.append(render(ref));
-        }
-      };
-
-  private static final Parser<String> PARSER =
-      new Parser<String>() {
-        @Override
-        public String parse(CharSequence text) throws ParseException {
-          String ref = text.toString();
-
-          if (ref.isEmpty()) {
-            throw new ParseException(AdminConstants.I.refErrorEmpty(), 0);
-          }
-
-          if (ref.charAt(0) == '/') {
-            throw new ParseException(AdminConstants.I.refErrorBeginSlash(), 0);
-          }
-
-          if (ref.charAt(0) == '^') {
-            if (!ref.startsWith("^refs/")) {
-              ref = "^refs/heads/" + ref.substring(1);
-            }
-          } else if (!ref.startsWith("refs/")) {
-            ref = "refs/heads/" + ref;
-          }
-
-          for (int i = 0; i < ref.length(); i++) {
-            final char c = ref.charAt(i);
-
-            if (c == '/' && 0 < i && ref.charAt(i - 1) == '/') {
-              throw new ParseException(AdminConstants.I.refErrorDoubleSlash(), i);
-            }
-
-            if (c == ' ') {
-              throw new ParseException(AdminConstants.I.refErrorNoSpace(), i);
-            }
-
-            if (c < ' ') {
-              throw new ParseException(AdminConstants.I.refErrorPrintable(), i);
-            }
-          }
-          return ref;
-        }
-      };
-
-  public RefPatternBox() {
-    super(Document.get().createTextInputElement(), RENDERER, PARSER);
-    addKeyPressHandler(GlobalKey.STOP_PROPAGATION);
-    addKeyPressHandler(
-        new KeyPressHandler() {
-          @Override
-          public void onKeyPress(KeyPressEvent event) {
-            if (event.getCharCode() == ' ') {
-              event.preventDefault();
-            }
-          }
-        });
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/Util.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/Util.java
deleted file mode 100644
index bbc8a1d..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/Util.java
+++ /dev/null
@@ -1,72 +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.client.admin;
-
-import com.google.gerrit.common.data.ProjectAdminService;
-import com.google.gerrit.extensions.client.ProjectState;
-import com.google.gerrit.extensions.client.SubmitType;
-import com.google.gwt.core.client.GWT;
-import com.google.gwtjsonrpc.client.JsonUtil;
-
-public class Util {
-  public static final ProjectAdminService PROJECT_SVC;
-
-  static {
-    PROJECT_SVC = GWT.create(ProjectAdminService.class);
-    JsonUtil.bind(PROJECT_SVC, "rpc/ProjectAdminService");
-
-    AdminResources.I.css().ensureInjected();
-  }
-
-  public static String toLongString(SubmitType type) {
-    if (type == null) {
-      return "";
-    }
-    switch (type) {
-      case INHERIT:
-        return AdminConstants.I.projectSubmitType_INHERIT();
-      case FAST_FORWARD_ONLY:
-        return AdminConstants.I.projectSubmitType_FAST_FORWARD_ONLY();
-      case MERGE_IF_NECESSARY:
-        return AdminConstants.I.projectSubmitType_MERGE_IF_NECESSARY();
-      case REBASE_IF_NECESSARY:
-        return AdminConstants.I.projectSubmitType_REBASE_IF_NECESSARY();
-      case REBASE_ALWAYS:
-        return AdminConstants.I.projectSubmitType_REBASE_ALWAYS();
-      case MERGE_ALWAYS:
-        return AdminConstants.I.projectSubmitType_MERGE_ALWAYS();
-      case CHERRY_PICK:
-        return AdminConstants.I.projectSubmitType_CHERRY_PICK();
-      default:
-        return type.name();
-    }
-  }
-
-  public static String toLongString(ProjectState type) {
-    if (type == null) {
-      return "";
-    }
-    switch (type) {
-      case ACTIVE:
-        return AdminConstants.I.projectState_ACTIVE();
-      case READ_ONLY:
-        return AdminConstants.I.projectState_READ_ONLY();
-      case HIDDEN:
-        return AdminConstants.I.projectState_HIDDEN();
-      default:
-        return type.name();
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ValueEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ValueEditor.java
deleted file mode 100644
index ad614e5..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ValueEditor.java
+++ /dev/null
@@ -1,209 +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.client.admin;
-
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.dom.client.DivElement;
-import com.google.gwt.dom.client.NativeEvent;
-import com.google.gwt.dom.client.Style.Display;
-import com.google.gwt.editor.client.EditorError;
-import com.google.gwt.editor.client.HasEditorErrors;
-import com.google.gwt.editor.client.IsEditor;
-import com.google.gwt.editor.client.LeafValueEditor;
-import com.google.gwt.editor.ui.client.adapters.ValueBoxEditor;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.DoubleClickEvent;
-import com.google.gwt.event.dom.client.DoubleClickHandler;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiChild;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.Focusable;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwt.user.client.ui.Label;
-import com.google.gwt.user.client.ui.SimplePanel;
-import com.google.gwt.user.client.ui.ValueBoxBase;
-import com.google.gwt.user.client.ui.Widget;
-import java.text.ParseException;
-import java.util.List;
-
-public class ValueEditor<T> extends Composite
-    implements HasEditorErrors<T>, IsEditor<ValueBoxEditor<T>>, LeafValueEditor<T>, Focusable {
-  interface Binder extends UiBinder<Widget, ValueEditor<?>> {}
-
-  static final Binder uiBinder = GWT.create(Binder.class);
-
-  @UiField SimplePanel textPanel;
-  private Label textLabel;
-  private StartEditHandlers startHandlers;
-
-  @UiField Image editIcon;
-
-  @UiField SimplePanel editPanel;
-
-  @UiField DivElement errorLabel;
-
-  private ValueBoxBase<T> editChild;
-  private ValueBoxEditor<T> editProxy;
-  private boolean ignoreEditorValue;
-  private T value;
-
-  public ValueEditor() {
-    startHandlers = new StartEditHandlers();
-    initWidget(uiBinder.createAndBindUi(this));
-    editPanel.setVisible(false);
-    editIcon.addClickHandler(startHandlers);
-  }
-
-  public void edit() {
-    textPanel.removeFromParent();
-    textPanel = null;
-    textLabel = null;
-
-    editIcon.removeFromParent();
-    editIcon = null;
-    startHandlers = null;
-
-    editPanel.setVisible(true);
-  }
-
-  @Override
-  public ValueBoxEditor<T> asEditor() {
-    if (editProxy == null) {
-      editProxy = new EditorProxy();
-    }
-    return editProxy;
-  }
-
-  @Override
-  public T getValue() {
-    return ignoreEditorValue ? value : asEditor().getValue();
-  }
-
-  @Override
-  public void setValue(T value) {
-    this.value = value;
-    asEditor().setValue(value);
-  }
-
-  void setIgnoreEditorValue(boolean off) {
-    ignoreEditorValue = off;
-  }
-
-  public void setEditTitle(String title) {
-    editIcon.setTitle(title);
-  }
-
-  @UiChild(limit = 1, tagname = "display")
-  public void setDisplay(Label widget) {
-    textLabel = widget;
-    textPanel.add(textLabel);
-
-    textLabel.addClickHandler(startHandlers);
-    textLabel.addDoubleClickHandler(startHandlers);
-  }
-
-  @UiChild(limit = 1, tagname = "editor")
-  public void setEditor(ValueBoxBase<T> widget) {
-    editChild = widget;
-    editPanel.add(editChild);
-    editProxy = null;
-  }
-
-  public void setEnabled(boolean enabled) {
-    editIcon.setVisible(enabled);
-    startHandlers.enabled = enabled;
-  }
-
-  @Override
-  public void showErrors(List<EditorError> errors) {
-    StringBuilder buf = new StringBuilder();
-    for (EditorError error : errors) {
-      if (error.getEditor().equals(editProxy)) {
-        buf.append("\n");
-        if (error.getUserData() instanceof ParseException) {
-          buf.append(((ParseException) error.getUserData()).getMessage());
-        } else {
-          buf.append(error.getMessage());
-        }
-      }
-    }
-
-    if (0 < buf.length()) {
-      errorLabel.setInnerText(buf.substring(1));
-      errorLabel.getStyle().setDisplay(Display.BLOCK);
-    } else {
-      errorLabel.setInnerText("");
-      errorLabel.getStyle().setDisplay(Display.NONE);
-    }
-  }
-
-  @Override
-  public void setAccessKey(char key) {
-    editChild.setAccessKey(key);
-  }
-
-  @Override
-  public void setFocus(boolean focused) {
-    editChild.setFocus(focused);
-    if (focused) {
-      editChild.setCursorPos(editChild.getText().length());
-    }
-  }
-
-  @Override
-  public int getTabIndex() {
-    return editChild.getTabIndex();
-  }
-
-  @Override
-  public void setTabIndex(int index) {
-    editChild.setTabIndex(index);
-  }
-
-  private class StartEditHandlers implements ClickHandler, DoubleClickHandler {
-    boolean enabled;
-
-    @Override
-    public void onClick(ClickEvent event) {
-      if (enabled && event.getNativeButton() == NativeEvent.BUTTON_LEFT) {
-        edit();
-      }
-    }
-
-    @Override
-    public void onDoubleClick(DoubleClickEvent event) {
-      if (enabled && event.getNativeButton() == NativeEvent.BUTTON_LEFT) {
-        edit();
-      }
-    }
-  }
-
-  private class EditorProxy extends ValueBoxEditor<T> {
-    EditorProxy() {
-      super(editChild);
-    }
-
-    @Override
-    public void setValue(T value) {
-      super.setValue(value);
-      if (textLabel == null) {
-        setDisplay(new Label());
-      }
-      textLabel.setText(editChild.getText());
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ValueEditor.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ValueEditor.ui.xml
deleted file mode 100644
index 137ad2b..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ValueEditor.ui.xml
+++ /dev/null
@@ -1,64 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder
-  xmlns:ui='urn:ui:com.google.gwt.uibinder'
-  xmlns:g='urn:import:com.google.gwt.user.client.ui'
-  >
-<ui:with field='ico' type='com.google.gerrit.client.GerritResources'/>
-<ui:style gss='false'>
-  .panel {
-    position: relative;
-    white-space: nowrap;
-  }
-
-  .textPanel {
-    width: 100%;
-    padding-right: 21px;
-  }
-
-  .editIcon {
-    position: absolute;
-    top: 0;
-    right: 5px;
-  }
-
-  .editPanel {
-    width: 100%;
-  }
-
-  .errorLabel {
-    display: none;
-    color: red;
-    white-space: pre;
-  }
-</ui:style>
-<g:HTMLPanel stylePrimaryName='{style.panel}'>
-  <g:Image
-      ui:field='editIcon'
-      resource='{ico.edit}'
-      stylePrimaryName='{style.editIcon}'
-      title='Edit'>
-    <ui:attribute name='title'/>
-  </g:Image>
-  <g:SimplePanel ui:field='textPanel' stylePrimaryName='{style.textPanel}'/>
-
-  <g:SimplePanel ui:field='editPanel' stylePrimaryName='{style.editPanel}'/>
-  <div
-      ui:field='errorLabel'
-      class='{style.errorLabel}'/>
-</g:HTMLPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/admin.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/admin.css
deleted file mode 100644
index eca4823..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/admin.css
+++ /dev/null
@@ -1,53 +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.
- */
-
-@eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
-@eval textColor com.google.gerrit.client.Gerrit.getTheme().textColor;
-@def deletedBackground #a9a9a9;
-
-@sprite .deleteIcon {
-  gwt-image: 'deleteNormal';
-  border: none;
-}
-
-@sprite .deleteIcon:hover {
-  gwt-image: 'deleteHover';
-  border: none;
-}
-
-@sprite .undoIcon {
-  gwt-image: 'undoNormal';
-  border: none;
-}
-
-.deleted {
-  background-color: deletedBackground;
-  color: #ffffff;
-  white-space: nowrap;
-  padding-left: 50px;
-}
-
-.deleted:hover {
-  background-color: selectionColor;
-  color: textColor;
- }
-
-.deletedBorder {
-  background: 1px solid deletedBackground;
-}
-
-.deleteSectionHover {
-  background-color: selectionColor !important;
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/arrow_undo.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/arrow_undo.png
deleted file mode 100644
index 6972c5e..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/arrow_undo.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/deleteHover.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/deleteHover.png
deleted file mode 100644
index 9fde3fa..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/deleteHover.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/deleteNormal.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/deleteNormal.png
deleted file mode 100644
index 47a1195..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/deleteNormal.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ActionContext.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ActionContext.java
deleted file mode 100644
index cf8de54..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ActionContext.java
+++ /dev/null
@@ -1,293 +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.client.api;
-
-import com.google.gerrit.client.actions.ActionButton;
-import com.google.gerrit.client.info.ActionInfo;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.info.ChangeInfo.EditInfo;
-import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
-import com.google.gerrit.client.projects.BranchInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.NativeString;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.JavaScriptObject;
-
-public class ActionContext extends JavaScriptObject {
-  static final native void init() /*-{
-    var Gerrit = $wnd.Gerrit;
-    var doc = $wnd.document;
-    var stopPropagation = function (e) {
-      if (e && e.stopPropagation) e.stopPropagation();
-      else $wnd.event.cancelBubble = true;
-    };
-
-    Gerrit.ActionContext = function(u){this._u=u};
-    Gerrit.ActionContext.prototype = {
-      go: Gerrit.go,
-      refresh: Gerrit.refresh,
-      refreshMenuBar: Gerrit.refreshMenuBar,
-      isSignedIn: Gerrit.isSignedIn,
-      showError: Gerrit.showError,
-
-      br: function(){return doc.createElement('br')},
-      hr: function(){return doc.createElement('hr')},
-      button: function(label, o) {
-        var e = doc.createElement('button');
-        e.appendChild(this.div(doc.createTextNode(label)));
-        if (o && o.onclick) e.onclick = o.onclick;
-        return e;
-      },
-      checkbox: function() {
-        var e = doc.createElement('input');
-        e.type = 'checkbox';
-        return e;
-      },
-      div: function() {
-        var e = doc.createElement('div');
-        for (var i = 0; i < arguments.length; i++)
-          e.appendChild(arguments[i]);
-        return e;
-      },
-      label: function(c,label) {
-        var e = doc.createElement('label');
-        e.appendChild(c);
-        e.appendChild(doc.createTextNode(label));
-        return e;
-      },
-      prependLabel: function(label,c) {
-        var e = doc.createElement('label');
-        e.appendChild(doc.createTextNode(label));
-        e.appendChild(c);
-        return e;
-      },
-      span: function() {
-        var e = doc.createElement('span');
-        for (var i = 0; i < arguments.length; i++)
-          e.appendChild(arguments[i]);
-        return e;
-      },
-      msg: function(label) {
-        var e = doc.createElement('span');
-        e.appendChild(doc.createTextNode(label));
-        return e;
-      },
-      textarea: function(o) {
-        var e = doc.createElement('textarea');
-        e.onkeypress = stopPropagation;
-        if (o && o.rows) e.rows = o.rows;
-        if (o && o.cols) e.cols = o.cols;
-        return e;
-      },
-      textfield: function() {
-        var e = doc.createElement('input');
-        e.type = 'text';
-        e.onkeypress = stopPropagation;
-        return e;
-      },
-      select: function(a,s) {
-        var e = doc.createElement('select');
-        for (var i = 0; i < a.length; i++) {
-          var o = doc.createElement('option');
-          if (i==s) {
-            o.setAttributeNode(doc.createAttribute("selected"));
-          }
-          o.appendChild(doc.createTextNode(a[i]));
-          e.appendChild(o);
-        }
-        return e;
-      },
-      selected: function(e) {
-        return e.options[e.selectedIndex].text;
-      },
-
-      popup: function(e){
-        this._p=@com.google.gerrit.client.api.PopupHelper::popup(
-          Lcom/google/gerrit/client/api/ActionContext;Lcom/google/gwt/dom/client/Element;)(this,e)},
-      hide: function() {
-        this._p.@com.google.gerrit.client.api.PopupHelper::hide()();
-        delete this['_p'];
-      },
-
-      call: function(i,b) {
-        var m = this.action.method.toLowerCase();
-        if (m == 'get' || m == 'delete' || i==null) this[m](b);
-        else this[m](i,b);
-      },
-      get: function(b){@com.google.gerrit.client.api.ActionContext::get(
-        Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;)(this._u,b)},
-      post: function(i,b){@com.google.gerrit.client.api.ActionContext::post(
-        Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;Lcom/google/gwt/core/client/JavaScriptObject;)(
-        this._u,i,b)},
-      put: function(i,b){@com.google.gerrit.client.api.ActionContext::put(
-        Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;Lcom/google/gwt/core/client/JavaScriptObject;)(
-        this._u,i,b)},
-      'delete': function(b){@com.google.gerrit.client.api.ActionContext::delete(
-        Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;)(this._u,b)},
-      del: function(b){@com.google.gerrit.client.api.ActionContext::delete(
-        Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;)(this._u,b)},
-    };
-  }-*/;
-
-  static final native ActionContext create(RestApi f) /*-{
-    return new $wnd.Gerrit.ActionContext(f);
-  }-*/;
-
-  final native void set(ActionInfo a) /*-{ this.action=a; }-*/;
-
-  final native void set(ChangeInfo c) /*-{ this.change=c; }-*/;
-
-  final native void set(EditInfo e) /*-{ this.edit=e; }-*/;
-
-  final native void set(Project.NameKey p) /*-{ this.project=p; }-*/;
-
-  final native void set(BranchInfo b) /*-{ this.branch=b }-*/;
-
-  final native void set(RevisionInfo r) /*-{ this.revision=r; }-*/;
-
-  final native void button(ActionButton b) /*-{ this._b=b; }-*/;
-
-  final native ActionButton button() /*-{ return this._b; }-*/;
-
-  public final native boolean has_popup() /*-{ return this.hasOwnProperty('_p') }-*/;
-
-  public final native void hide() /*-{ this.hide(); }-*/;
-
-  protected ActionContext() {}
-
-  static final void get(RestApi api, JavaScriptObject cb) {
-    api.get(wrap(cb));
-  }
-
-  /**
-   * The same as {@link #get(RestApi, JavaScriptObject)} but without converting a {@link
-   * NativeString} result to String.
-   */
-  static final void getRaw(RestApi api, JavaScriptObject cb) {
-    api.get(wrapRaw(cb));
-  }
-
-  static final void post(RestApi api, JavaScriptObject in, JavaScriptObject cb) {
-    if (NativeString.is(in)) {
-      post(api, ((NativeString) in).asString(), cb);
-    } else {
-      api.post(in, wrap(cb));
-    }
-  }
-
-  /**
-   * The same as {@link #post(RestApi, JavaScriptObject, JavaScriptObject)} but without converting a
-   * {@link NativeString} result to String.
-   */
-  static final void postRaw(RestApi api, JavaScriptObject in, JavaScriptObject cb) {
-    if (NativeString.is(in)) {
-      postRaw(api, ((NativeString) in).asString(), cb);
-    } else {
-      api.post(in, wrapRaw(cb));
-    }
-  }
-
-  static final void post(RestApi api, String in, JavaScriptObject cb) {
-    api.post(in, wrap(cb));
-  }
-
-  /**
-   * The same as {@link #post(RestApi, String, JavaScriptObject)} but without converting a {@link
-   * NativeString} result to String.
-   */
-  static final void postRaw(RestApi api, String in, JavaScriptObject cb) {
-    api.post(in, wrapRaw(cb));
-  }
-
-  static final void put(RestApi api, JavaScriptObject cb) {
-    api.put(wrap(cb));
-  }
-
-  /**
-   * The same as {@link #put(RestApi, JavaScriptObject)} but without converting a {@link
-   * NativeString} result to String.
-   */
-  static final void putRaw(RestApi api, JavaScriptObject cb) {
-    api.put(wrapRaw(cb));
-  }
-
-  static final void put(RestApi api, JavaScriptObject in, JavaScriptObject cb) {
-    if (NativeString.is(in)) {
-      put(api, ((NativeString) in).asString(), cb);
-    } else {
-      api.put(in, wrap(cb));
-    }
-  }
-
-  /**
-   * The same as {@link #put(RestApi, JavaScriptObject, JavaScriptObject)} but without converting a
-   * {@link NativeString} result to String.
-   */
-  static final void putRaw(RestApi api, JavaScriptObject in, JavaScriptObject cb) {
-    if (NativeString.is(in)) {
-      putRaw(api, ((NativeString) in).asString(), cb);
-    } else {
-      api.put(in, wrapRaw(cb));
-    }
-  }
-
-  static final void put(RestApi api, String in, JavaScriptObject cb) {
-    api.put(in, wrap(cb));
-  }
-
-  /**
-   * The same as {@link #put(RestApi, String, JavaScriptObject)} but without converting a {@link
-   * NativeString} result to String.
-   */
-  static final void putRaw(RestApi api, String in, JavaScriptObject cb) {
-    api.put(in, wrapRaw(cb));
-  }
-
-  static final void delete(RestApi api, JavaScriptObject cb) {
-    api.delete(wrap(cb));
-  }
-
-  /**
-   * The same as {@link #delete(RestApi, JavaScriptObject)} but without converting a {@link
-   * NativeString} result to String.
-   */
-  static final void deleteRaw(RestApi api, JavaScriptObject cb) {
-    api.delete(wrapRaw(cb));
-  }
-
-  private static GerritCallback<JavaScriptObject> wrap(JavaScriptObject cb) {
-    return new GerritCallback<JavaScriptObject>() {
-      @Override
-      public void onSuccess(JavaScriptObject result) {
-        if (NativeString.is(result)) {
-          NativeString s = result.cast();
-          ApiGlue.invoke(cb, s.asString());
-        } else {
-          ApiGlue.invoke(cb, result);
-        }
-      }
-    };
-  }
-
-  private static GerritCallback<JavaScriptObject> wrapRaw(JavaScriptObject cb) {
-    return new GerritCallback<JavaScriptObject>() {
-      @Override
-      public void onSuccess(JavaScriptObject result) {
-        ApiGlue.invoke(cb, result);
-      }
-    };
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java
deleted file mode 100644
index 294fa9b..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java
+++ /dev/null
@@ -1,322 +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.client.api;
-
-import com.google.gerrit.client.ErrorDialog;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.info.AccountInfo;
-import com.google.gerrit.client.info.GeneralPreferences;
-import com.google.gerrit.client.info.ServerInfo;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.user.client.History;
-import com.google.gwt.user.client.Window;
-
-public class ApiGlue {
-  private static String pluginName;
-
-  public static void init() {
-    init0();
-    ActionContext.init();
-    HtmlTemplate.init();
-    Plugin.init();
-  }
-
-  private static native void init0() /*-{
-    var serverUrl = @com.google.gwt.core.client.GWT::getHostPageBaseURL()();
-    var ScreenDefinition = @com.google.gerrit.client.api.ExtensionScreen.Definition::TYPE;
-    var SettingsScreenDefinition = @com.google.gerrit.client.api.ExtensionSettingsScreen.Definition::TYPE;
-    var PanelDefinition = @com.google.gerrit.client.api.ExtensionPanel.Definition::TYPE;
-    $wnd.Gerrit = {
-      JsonString: @com.google.gerrit.client.rpc.NativeString::TYPE,
-      events: {},
-      plugins: {},
-      screens: {},
-      settingsScreens: {},
-      panels: {},
-      change_actions: {},
-      edit_actions: {},
-      revision_actions: {},
-      project_actions: {},
-      branch_actions: {},
-
-      getPluginName: @com.google.gerrit.client.api.ApiGlue::getPluginName(),
-      injectCss: @com.google.gwt.dom.client.StyleInjector::inject(Ljava/lang/String;),
-      install: function (f) {
-        var p = this._getPluginByUrl(@com.google.gerrit.client.api.PluginName::getCallerUrl()());
-        @com.google.gerrit.client.api.ApiGlue::install(
-            Lcom/google/gwt/core/client/JavaScriptObject;
-            Lcom/google/gerrit/client/api/Plugin;)
-          (f,p);
-      },
-      installGwt: function(u){return this._getPluginByUrl(u)},
-      _getPluginByUrl: function(u) {
-        return u.indexOf(serverUrl) == 0
-          ? this.plugins[u.substring(serverUrl.length)]
-          : this.plugins[u]
-      },
-
-      go: @com.google.gerrit.client.api.ApiGlue::go(Ljava/lang/String;),
-      refresh: @com.google.gerrit.client.api.ApiGlue::refresh(),
-      refreshMenuBar: @com.google.gerrit.client.api.ApiGlue::refreshMenuBar(),
-      isSignedIn: @com.google.gerrit.client.api.ApiGlue::isSignedIn(),
-      showError: @com.google.gerrit.client.api.ApiGlue::showError(Ljava/lang/String;),
-      getServerInfo: @com.google.gerrit.client.api.ApiGlue::getServerInfo(),
-      getCurrentUser: @com.google.gerrit.client.api.ApiGlue::getCurrentUser(),
-      getUserPreferences: @com.google.gerrit.client.api.ApiGlue::getUserPreferences(),
-      refreshUserPreferences: @com.google.gerrit.client.api.ApiGlue::refreshUserPreferences(),
-
-      on: function (e,f){(this.events[e] || (this.events[e]=[])).push(f)},
-      onAction: function (t,n,c){this._onAction(this.getPluginName(),t,n,c)},
-      _onAction: function (p,t,n,c) {
-        var i = p+'~'+n;
-        if ('change' == t) this.change_actions[i]=c;
-        else if ('edit' == t) this.edit_actions[i]=c;
-        else if ('revision' == t) this.revision_actions[i]=c;
-        else if ('project' == t) this.project_actions[i]=c;
-        else if ('branch' == t) this.branch_actions[i]=c;
-        else if ('screen' == t) _screen(p,t,c);
-      },
-      screen: function(r,c){this._screen(this.getPluginName(),r,c)},
-      _screen: function(p,r,c){
-        var s = new ScreenDefinition(r,c);
-        (this.screens[p] || (this.screens[p]=[])).push(s);
-      },
-      settingsScreen: function(p,m,c){this._settingsScreen(this.getPluginName(),p,m,c)},
-      _settingsScreen: function(n,p,m,c){
-        var s = new SettingsScreenDefinition(p,m,c);
-        (this.settingsScreens[n] || (this.settingsScreens[n]=[])).push(s);
-      },
-      panel: function(i,c,n){this._panel(this.getPluginName(),i,c,n)},
-      _panel: function(n,i,c,x){
-        var p = new PanelDefinition(n,c,x);
-        (this.panels[i] || (this.panels[i]=[])).push(p);
-      },
-
-      url: function (d) {
-        if (d && d.length > 0)
-          return serverUrl + (d.charAt(0)=='/' ? d.substring(1) : d);
-        return serverUrl;
-      },
-
-      _api: function(u) {
-        return @com.google.gerrit.client.rpc.RestApi::new(Ljava/lang/String;)(u);
-      },
-      get: function(u,b) {
-        @com.google.gerrit.client.api.ActionContext::get(
-            Lcom/google/gerrit/client/rpc/RestApi;
-            Lcom/google/gwt/core/client/JavaScriptObject;)
-          (this._api(u), b);
-      },
-      get_raw: function(u,b) {
-        @com.google.gerrit.client.api.ActionContext::getRaw(
-            Lcom/google/gerrit/client/rpc/RestApi;
-            Lcom/google/gwt/core/client/JavaScriptObject;)
-          (this._api(u), b);
-      },
-      post: function(u,i,b) {
-        if (typeof i == 'string') {
-          @com.google.gerrit.client.api.ActionContext::post(
-              Lcom/google/gerrit/client/rpc/RestApi;
-              Ljava/lang/String;
-              Lcom/google/gwt/core/client/JavaScriptObject;)
-            (this._api(u), i, b);
-        } else {
-          @com.google.gerrit.client.api.ActionContext::post(
-              Lcom/google/gerrit/client/rpc/RestApi;
-              Lcom/google/gwt/core/client/JavaScriptObject;
-              Lcom/google/gwt/core/client/JavaScriptObject;)
-            (this._api(u), i, b);
-        }
-      },
-      post_raw: function(u,i,b) {
-        if (typeof i == 'string') {
-          @com.google.gerrit.client.api.ActionContext::postRaw(
-              Lcom/google/gerrit/client/rpc/RestApi;
-              Ljava/lang/String;
-              Lcom/google/gwt/core/client/JavaScriptObject;)
-            (this._api(u), i, b);
-        } else {
-          @com.google.gerrit.client.api.ActionContext::postRaw(
-              Lcom/google/gerrit/client/rpc/RestApi;
-              Lcom/google/gwt/core/client/JavaScriptObject;
-              Lcom/google/gwt/core/client/JavaScriptObject;)
-            (this._api(u), i, b);
-        }
-      },
-      put: function(u,i,b) {
-        if (b) {
-          if (typeof i == 'string') {
-            @com.google.gerrit.client.api.ActionContext::put(
-                Lcom/google/gerrit/client/rpc/RestApi;
-                Ljava/lang/String;
-                Lcom/google/gwt/core/client/JavaScriptObject;)
-              (this._api(u), i, b);
-          } else {
-            @com.google.gerrit.client.api.ActionContext::put(
-                Lcom/google/gerrit/client/rpc/RestApi;
-                Lcom/google/gwt/core/client/JavaScriptObject;
-                Lcom/google/gwt/core/client/JavaScriptObject;)
-              (this._api(u), i, b);
-          }
-        } else {
-          @com.google.gerrit.client.api.ActionContext::put(
-              Lcom/google/gerrit/client/rpc/RestApi;
-              Lcom/google/gwt/core/client/JavaScriptObject;)
-            (this._api(u), i);
-        }
-      },
-      put_raw: function(u,i,b) {
-        if (b) {
-          if (typeof i == 'string') {
-            @com.google.gerrit.client.api.ActionContext::putRaw(
-                Lcom/google/gerrit/client/rpc/RestApi;
-                Ljava/lang/String;
-                Lcom/google/gwt/core/client/JavaScriptObject;)
-              (this._api(u), i, b);
-          } else {
-            @com.google.gerrit.client.api.ActionContext::putRaw(
-                Lcom/google/gerrit/client/rpc/RestApi;
-                Lcom/google/gwt/core/client/JavaScriptObject;
-                Lcom/google/gwt/core/client/JavaScriptObject;)
-              (this._api(u), i, b);
-          }
-        } else {
-          @com.google.gerrit.client.api.ActionContext::putRaw(
-              Lcom/google/gerrit/client/rpc/RestApi;
-              Lcom/google/gwt/core/client/JavaScriptObject;)
-            (this._api(u), i);
-        }
-      },
-      'delete': function(u,b) {
-        @com.google.gerrit.client.api.ActionContext::delete(
-            Lcom/google/gerrit/client/rpc/RestApi;
-            Lcom/google/gwt/core/client/JavaScriptObject;)
-          (this._api(u), b);
-      },
-      del: function(u,b) {
-        @com.google.gerrit.client.api.ActionContext::delete(
-            Lcom/google/gerrit/client/rpc/RestApi;
-            Lcom/google/gwt/core/client/JavaScriptObject;)
-          (this._api(u), b);
-      },
-      del_raw: function(u,b) {
-        @com.google.gerrit.client.api.ActionContext::deleteRaw(
-            Lcom/google/gerrit/client/rpc/RestApi;
-            Lcom/google/gwt/core/client/JavaScriptObject;)
-          (this._api(u), b);
-      },
-    };
-  }-*/;
-
-  private static void install(JavaScriptObject cb, Plugin p) throws Exception {
-    try {
-      pluginName = p.name();
-      invoke(cb, p);
-      p._initialized();
-    } catch (Exception e) {
-      p.failure(e);
-      throw e;
-    } finally {
-      pluginName = null;
-      PluginLoader.loaded();
-    }
-  }
-
-  private static String getPluginName() {
-    if (pluginName != null) {
-      return pluginName;
-    }
-    return PluginName.fromUrl(PluginName.getCallerUrl());
-  }
-
-  private static void go(String urlOrToken) {
-    if (urlOrToken.startsWith("http:")
-        || urlOrToken.startsWith("https:")
-        || urlOrToken.startsWith("//")) {
-      Window.Location.assign(urlOrToken);
-    } else {
-      Gerrit.display(urlOrToken);
-    }
-  }
-
-  private static void refresh() {
-    Gerrit.display(History.getToken());
-  }
-
-  private static ServerInfo getServerInfo() {
-    return Gerrit.info();
-  }
-
-  private static AccountInfo getCurrentUser() {
-    return Gerrit.getUserAccount();
-  }
-
-  private static GeneralPreferences getUserPreferences() {
-    return Gerrit.getUserPreferences();
-  }
-
-  private static void refreshUserPreferences() {
-    Gerrit.refreshUserPreferences();
-  }
-
-  private static void refreshMenuBar() {
-    Gerrit.refreshMenuBar();
-  }
-
-  private static boolean isSignedIn() {
-    return Gerrit.isSignedIn();
-  }
-
-  private static void showError(String message) {
-    new ErrorDialog(message).center();
-  }
-
-  static final native void invoke(JavaScriptObject f) /*-{ f(); }-*/;
-
-  static final native void invoke(JavaScriptObject f, JavaScriptObject a) /*-{ f(a); }-*/;
-
-  static final native void invoke(
-      JavaScriptObject f, JavaScriptObject a, JavaScriptObject b) /*-{ f(a,b) }-*/;
-
-  static final native void invoke(JavaScriptObject f, String a) /*-{ f(a); }-*/;
-
-  public static final void fireEvent(String event, String a) {
-    JsArray<JavaScriptObject> h = getEventHandlers(event);
-    for (int i = 0; i < h.length(); i++) {
-      invoke(h.get(i), a);
-    }
-  }
-
-  public static final void fireEvent(String event, Element e) {
-    JsArray<JavaScriptObject> h = getEventHandlers(event);
-    for (int i = 0; i < h.length(); i++) {
-      invoke(h.get(i), e);
-    }
-  }
-
-  static final void fireEvent(String event, JavaScriptObject a, JavaScriptObject b) {
-    JsArray<JavaScriptObject> h = getEventHandlers(event);
-    for (int i = 0; i < h.length(); i++) {
-      invoke(h.get(i), a, b);
-    }
-  }
-
-  static final native JsArray<JavaScriptObject> getEventHandlers(String e)
-      /*-{ return $wnd.Gerrit.events[e] || [] }-*/ ;
-
-  private ApiGlue() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ChangeGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ChangeGlue.java
deleted file mode 100644
index c7f0051..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ChangeGlue.java
+++ /dev/null
@@ -1,63 +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.client.api;
-
-import com.google.gerrit.client.actions.ActionButton;
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.info.ActionInfo;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
-
-public class ChangeGlue {
-  public static void fireShowChange(ChangeInfo change, RevisionInfo rev) {
-    ApiGlue.fireEvent("showchange", change, rev);
-  }
-
-  public static boolean onSubmitChange(ChangeInfo change, RevisionInfo rev) {
-    JsArray<JavaScriptObject> h = ApiGlue.getEventHandlers("submitchange");
-    for (int i = 0; i < h.length(); i++) {
-      if (!invoke(h.get(i), change, rev)) {
-        return false;
-      }
-    }
-    return true;
-  }
-
-  public static void onAction(ChangeInfo change, ActionInfo action, ActionButton button) {
-    RestApi api = ChangeApi.change(change.project(), change.legacyId().get()).view(action.id());
-    JavaScriptObject f = get(action.id());
-    if (f != null) {
-      ActionContext c = ActionContext.create(api);
-      c.set(action);
-      c.set(change);
-      c.button(button);
-      ApiGlue.invoke(f, c);
-    } else {
-      DefaultActions.invoke(change, action, api);
-    }
-  }
-
-  private static native JavaScriptObject get(String id) /*-{
-    return $wnd.Gerrit.change_actions[id];
-  }-*/;
-
-  private static native boolean invoke(JavaScriptObject h, ChangeInfo a, RevisionInfo r)
-      /*-{ return h(a,r) }-*/ ;
-
-  private ChangeGlue() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java
deleted file mode 100644
index 0c4aacd..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java
+++ /dev/null
@@ -1,99 +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.client.api;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.info.ActionInfo;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.NativeString;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.user.client.Window;
-import com.google.gwt.user.client.Window.Location;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-
-class DefaultActions {
-  static void invoke(ChangeInfo change, ActionInfo action, RestApi api) {
-    invoke(action, api, callback(PageLinks.toChange(change.projectNameKey(), change.legacyId())));
-  }
-
-  static void invoke(Project.NameKey project, ActionInfo action, RestApi api) {
-    invoke(action, api, callback(PageLinks.toProject(project)));
-  }
-
-  private static AsyncCallback<JavaScriptObject> callback(String target) {
-    return new GerritCallback<JavaScriptObject>() {
-      @Override
-      public void onSuccess(JavaScriptObject in) {
-        UiResult result = asUiResult(in);
-        if (result == null) {
-          Gerrit.display(target);
-          return;
-        }
-
-        if (result.alert() != null) {
-          Window.alert(result.alert());
-        }
-
-        if (result.redirectUrl() != null && result.openWindow()) {
-          Window.open(result.redirectUrl(), "_blank", null);
-        } else if (result.redirectUrl() != null) {
-          Location.assign(result.redirectUrl());
-        } else {
-          Gerrit.display(target);
-        }
-      }
-
-      private UiResult asUiResult(JavaScriptObject in) {
-        if (NativeString.is(in)) {
-          String str = ((NativeString) in).asString();
-          return str.isEmpty() ? UiResult.none() : UiResult.alert(str);
-        }
-        return in.cast();
-      }
-    };
-  }
-
-  private static void invoke(ActionInfo action, RestApi api, AsyncCallback<JavaScriptObject> cb) {
-    if ("GET".equalsIgnoreCase(action.method())) {
-      api.get(cb);
-    } else if ("PUT".equalsIgnoreCase(action.method())) {
-      api.put(JavaScriptObject.createObject(), cb);
-    } else if ("DELETE".equalsIgnoreCase(action.method())) {
-      api.delete(cb);
-    } else {
-      api.post(JavaScriptObject.createObject(), cb);
-    }
-  }
-
-  private DefaultActions() {}
-
-  private static class UiResult extends JavaScriptObject {
-    static native UiResult alert(String m) /*-{ return {'alert':m} }-*/;
-
-    static native UiResult none() /*-{ return {} }-*/;
-
-    final native String alert() /*-{ return this.alert }-*/;
-
-    final native String redirectUrl() /*-{ return this.url }-*/;
-
-    final native boolean openWindow() /*-{ return this.open_window || false }-*/;
-
-    protected UiResult() {}
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/EditGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/EditGlue.java
deleted file mode 100644
index 85cfde6..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/EditGlue.java
+++ /dev/null
@@ -1,48 +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.client.api;
-
-import com.google.gerrit.client.actions.ActionButton;
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.info.ActionInfo;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.info.ChangeInfo.EditInfo;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gwt.core.client.JavaScriptObject;
-
-public class EditGlue {
-  public static void onAction(
-      ChangeInfo change, EditInfo edit, ActionInfo action, ActionButton button) {
-    RestApi api = ChangeApi.edit(change.project(), change.legacyId().get()).view(action.id());
-
-    JavaScriptObject f = get(action.id());
-    if (f != null) {
-      ActionContext c = ActionContext.create(api);
-      c.set(action);
-      c.set(change);
-      c.set(edit);
-      c.button(button);
-      ApiGlue.invoke(f, c);
-    } else {
-      DefaultActions.invoke(change, action, api);
-    }
-  }
-
-  private static native JavaScriptObject get(String id) /*-{
-    return $wnd.Gerrit.edit_actions[id];
-  }-*/;
-
-  private EditGlue() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionPanel.java
deleted file mode 100644
index 6d3dd60..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionPanel.java
+++ /dev/null
@@ -1,205 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.api;
-
-import com.google.gerrit.client.GerritUiExtensionPoint;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.SimplePanel;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-public class ExtensionPanel extends FlowPanel {
-  private static final Logger logger = Logger.getLogger(ExtensionPanel.class.getName());
-  private final GerritUiExtensionPoint extensionPoint;
-  private final List<Context> contexts;
-
-  public ExtensionPanel(GerritUiExtensionPoint extensionPoint) {
-    this(extensionPoint, new ArrayList<String>());
-  }
-
-  public ExtensionPanel(GerritUiExtensionPoint extensionPoint, List<String> panelNames) {
-    this.extensionPoint = extensionPoint;
-    this.contexts = create(panelNames);
-  }
-
-  private List<Context> create(List<String> panelNames) {
-    List<Context> contexts = new ArrayList<>();
-    for (Definition def : getOrderedDefs(panelNames)) {
-      SimplePanel p = new SimplePanel();
-      add(p);
-      contexts.add(Context.create(def, p));
-    }
-    return contexts;
-  }
-
-  private List<Definition> getOrderedDefs(List<String> panelNames) {
-    if (panelNames == null) {
-      panelNames = Collections.emptyList();
-    }
-    Map<String, List<Definition>> defsOrderedByName = new LinkedHashMap<>();
-    for (String name : panelNames) {
-      defsOrderedByName.put(name, new ArrayList<Definition>());
-    }
-    for (Definition def : Natives.asList(Definition.get(extensionPoint.name()))) {
-      addDef(def, defsOrderedByName);
-    }
-    List<Definition> orderedDefs = new ArrayList<>();
-    for (List<Definition> defList : defsOrderedByName.values()) {
-      orderedDefs.addAll(defList);
-    }
-    return orderedDefs;
-  }
-
-  private static void addDef(Definition def, Map<String, List<Definition>> defsOrderedByName) {
-    String panelName = def.getPanelName();
-    if (panelName.equals(def.getPluginName() + ".undefined")) {
-      /* Handle a partially undefined panel name from the
-      javascript layer by generating a random panel name.
-      This maintains support for panels that do not provide a name. */
-      panelName =
-          def.getPluginName() + "." + Long.toHexString(Double.doubleToLongBits(Math.random()));
-    }
-    if (defsOrderedByName.containsKey(panelName)) {
-      defsOrderedByName.get(panelName).add(def);
-    } else if (defsOrderedByName.containsKey(def.getPluginName())) {
-      defsOrderedByName.get(def.getPluginName()).add(def);
-    } else {
-      defsOrderedByName.put(panelName, Collections.singletonList(def));
-    }
-  }
-
-  public void put(GerritUiExtensionPoint.Key key, String value) {
-    for (Context ctx : contexts) {
-      ctx.put(key.name(), value);
-    }
-  }
-
-  public void putInt(GerritUiExtensionPoint.Key key, int value) {
-    for (Context ctx : contexts) {
-      ctx.putInt(key.name(), value);
-    }
-  }
-
-  public void putBoolean(GerritUiExtensionPoint.Key key, boolean value) {
-    for (Context ctx : contexts) {
-      ctx.putBoolean(key.name(), value);
-    }
-  }
-
-  public void putObject(GerritUiExtensionPoint.Key key, JavaScriptObject value) {
-    for (Context ctx : contexts) {
-      ctx.putObject(key.name(), value);
-    }
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    for (Context ctx : contexts) {
-      try {
-        ctx.onLoad();
-      } catch (RuntimeException e) {
-        logger.log(
-            Level.SEVERE,
-            "Failed to load extension panel for extension point "
-                + extensionPoint.name()
-                + " from plugin "
-                + ctx.getPluginName()
-                + ": "
-                + e.getMessage());
-      }
-    }
-  }
-
-  @Override
-  protected void onUnload() {
-    super.onUnload();
-    for (Context ctx : contexts) {
-      for (JavaScriptObject u : Natives.asList(ctx.unload())) {
-        ApiGlue.invoke(u);
-      }
-    }
-  }
-
-  static class Definition extends JavaScriptObject {
-    static final JavaScriptObject TYPE = init();
-
-    private static native JavaScriptObject init() /*-{
-      function PanelDefinition(n, c, x) {
-        this.pluginName = n;
-        this.onLoad = c;
-        this.name = x;
-      };
-      return PanelDefinition;
-    }-*/;
-
-    static native JsArray<Definition> get(String i) /*-{ return $wnd.Gerrit.panels[i] || [] }-*/;
-
-    protected Definition() {}
-
-    public final native String getPanelName() /*-{ return this.pluginName + "." + this.name; }-*/;
-
-    public final native String getPluginName() /*-{ return this.pluginName; }-*/;
-  }
-
-  static class Context extends JavaScriptObject {
-    static final Context create(Definition def, SimplePanel panel) {
-      return create(TYPE, def, panel.getElement());
-    }
-
-    final native void onLoad() /*-{ this._d.onLoad(this) }-*/;
-
-    final native JsArray<JavaScriptObject> unload() /*-{ return this._u }-*/;
-
-    final native String getPluginName() /*-{ return this._d.pluginName; }-*/;
-
-    final native void put(String k, String v) /*-{ this.p[k] = v; }-*/;
-
-    final native void putInt(String k, int v) /*-{ this.p[k] = v; }-*/;
-
-    final native void putBoolean(String k, boolean v) /*-{ this.p[k] = v; }-*/;
-
-    final native void putObject(String k, JavaScriptObject v) /*-{ this.p[k] = v; }-*/;
-
-    private static native Context create(JavaScriptObject T, Definition d, Element e)
-        /*-{ return new T(d,e) }-*/ ;
-
-    private static final JavaScriptObject TYPE = init();
-
-    private static native JavaScriptObject init() /*-{
-      var T = function(d,e) {
-        this._d = d;
-        this._u = [];
-        this.body = e;
-        this.p = {};
-      };
-      T.prototype = {
-        onUnload: function(f){this._u.push(f)},
-      };
-      return T;
-    }-*/;
-
-    protected Context() {}
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionScreen.java
deleted file mode 100644
index ff495b9..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionScreen.java
+++ /dev/null
@@ -1,134 +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.client.api;
-
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.ui.Screen;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.core.client.JsArrayString;
-import com.google.gwt.dom.client.Element;
-
-/** Screen contributed by a plugin. */
-public class ExtensionScreen extends Screen {
-  private Context ctx;
-
-  public ExtensionScreen(String token) {
-    if (token.contains("?")) {
-      token = token.substring(0, token.indexOf('?'));
-    }
-    String name;
-    String rest;
-    int s = token.indexOf('/');
-    if (0 < s) {
-      name = token.substring(0, s);
-      rest = token.substring(s + 1);
-    } else {
-      name = token;
-      rest = "";
-    }
-    ctx = create(name, rest);
-  }
-
-  private Context create(String name, String rest) {
-    for (Definition def : Natives.asList(Definition.get(name))) {
-      JsArrayString m = def.match(rest);
-      if (m != null) {
-        return Context.create(def, this, m);
-      }
-    }
-    return null;
-  }
-
-  public boolean isFound() {
-    return ctx != null;
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    setHeaderVisible(false);
-    ctx.onLoad();
-  }
-
-  @Override
-  protected void onUnload() {
-    super.onUnload();
-    for (JavaScriptObject u : Natives.asList(ctx.unload())) {
-      ApiGlue.invoke(u);
-    }
-  }
-
-  static class Definition extends JavaScriptObject {
-    static final JavaScriptObject TYPE = init();
-
-    private static native JavaScriptObject init() /*-{
-      function ScreenDefinition(r, c) {
-        this.pattern = r;
-        this.onLoad = c;
-      };
-      return ScreenDefinition;
-    }-*/;
-
-    static native JsArray<Definition> get(String n) /*-{ return $wnd.Gerrit.screens[n] || [] }-*/;
-
-    final native JsArrayString match(String t) /*-{
-      var p = this.pattern;
-      if (p instanceof $wnd.RegExp) {
-        var m = p.exec(t);
-        return m && m[0] == t ? m : null;
-      }
-      return p == t ? [t] : null;
-    }-*/;
-
-    protected Definition() {}
-  }
-
-  static class Context extends JavaScriptObject {
-    static final Context create(Definition def, ExtensionScreen view, JsArrayString match) {
-      return create(TYPE, def, view, view.getBody().getElement(), match);
-    }
-
-    final native void onLoad() /*-{ this._d.onLoad(this) }-*/;
-
-    final native JsArray<JavaScriptObject> unload() /*-{ return this._u }-*/;
-
-    private static native Context create(
-        JavaScriptObject T, Definition d, ExtensionScreen s, Element e, JsArrayString m)
-        /*-{ return new T(d,s,e,m) }-*/ ;
-
-    private static final JavaScriptObject TYPE = init();
-
-    private static native JavaScriptObject init() /*-{
-      var T = function(d,s,e,m) {
-        this._d = d;
-        this._s = s;
-        this._u = [];
-        this.body = e;
-        this.token = m[0];
-        this.token_match = m;
-      };
-      T.prototype = {
-        setTitle: function(t){this._s.@com.google.gerrit.client.ui.Screen::setPageTitle(Ljava/lang/String;)(t)},
-        setWindowTitle: function(t){this._s.@com.google.gerrit.client.ui.Screen::setWindowTitle(Ljava/lang/String;)(t)},
-        show: function(){$entry(this._s.@com.google.gwtexpui.user.client.View::display()())},
-        onUnload: function(f){this._u.push(f)},
-      };
-      return T;
-    }-*/;
-
-    protected Context() {}
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionSettingsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionSettingsScreen.java
deleted file mode 100644
index e7d1ed1..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionSettingsScreen.java
+++ /dev/null
@@ -1,139 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.api;
-
-import com.google.gerrit.client.account.SettingsScreen;
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.NativeString;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.dom.client.Element;
-import java.util.Set;
-
-/** SettingsScreen contributed by a plugin. */
-public class ExtensionSettingsScreen extends SettingsScreen {
-  private Context ctx;
-
-  public ExtensionSettingsScreen(String token) {
-    if (token.contains("?")) {
-      token = token.substring(0, token.indexOf('?'));
-    }
-    String name;
-    String rest;
-    int s = token.indexOf('/');
-    if (0 < s) {
-      name = token.substring(0, s);
-      rest = token.substring(s + 1);
-    } else {
-      name = token;
-      rest = "";
-    }
-    ctx = create(name, rest);
-  }
-
-  private Context create(String name, String rest) {
-    for (Definition def : Natives.asList(Definition.get(name))) {
-      if (def.matches(rest)) {
-        return Context.create(def, this);
-      }
-    }
-    return null;
-  }
-
-  public boolean isFound() {
-    return ctx != null;
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    setHeaderVisible(false);
-    ctx.onLoad();
-  }
-
-  @Override
-  protected void onUnload() {
-    super.onUnload();
-    for (JavaScriptObject u : Natives.asList(ctx.unload())) {
-      ApiGlue.invoke(u);
-    }
-  }
-
-  public static class Definition extends JavaScriptObject {
-    static final JavaScriptObject TYPE = init();
-
-    private static native JavaScriptObject init() /*-{
-      function SettingsScreenDefinition(p, m, c) {
-        this.path = p;
-        this.menu = m;
-        this.onLoad = c;
-      };
-      return SettingsScreenDefinition;
-    }-*/;
-
-    public static native JsArray<Definition> get(String n)
-        /*-{ return $wnd.Gerrit.settingsScreens[n] || [] }-*/ ;
-
-    public static final Set<String> plugins() {
-      return Natives.keys(settingsScreens());
-    }
-
-    private static native NativeMap<NativeString> settingsScreens()
-        /*-{ return $wnd.Gerrit.settingsScreens; }-*/ ;
-
-    public final native String getPath() /*-{ return this.path; }-*/;
-
-    public final native String getMenu() /*-{ return this.menu; }-*/;
-
-    final native boolean matches(String t) /*-{ return this.path == t; }-*/;
-
-    protected Definition() {}
-  }
-
-  static class Context extends JavaScriptObject {
-    static final Context create(Definition def, ExtensionSettingsScreen view) {
-      return create(TYPE, def, view, view.getBody().getElement());
-    }
-
-    final native void onLoad() /*-{ this._d.onLoad(this) }-*/;
-
-    final native JsArray<JavaScriptObject> unload() /*-{ return this._u }-*/;
-
-    private static native Context create(
-        JavaScriptObject T, Definition d, ExtensionSettingsScreen s, Element e)
-        /*-{ return new T(d,s,e) }-*/ ;
-
-    private static final JavaScriptObject TYPE = init();
-
-    private static native JavaScriptObject init() /*-{
-      var T = function(d,s,e) {
-        this._d = d;
-        this._s = s;
-        this._u = [];
-        this.body = e;
-      };
-      T.prototype = {
-        setTitle: function(t){this._s.@com.google.gerrit.client.ui.Screen::setPageTitle(Ljava/lang/String;)(t)},
-        setWindowTitle: function(t){this._s.@com.google.gerrit.client.ui.Screen::setWindowTitle(Ljava/lang/String;)(t)},
-        show: function(){$entry(this._s.@com.google.gwtexpui.user.client.View::display()())},
-        onUnload: function(f){this._u.push(f)},
-      };
-      return T;
-    }-*/;
-
-    protected Context() {}
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/HtmlTemplate.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/HtmlTemplate.java
deleted file mode 100644
index ba9c659..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/HtmlTemplate.java
+++ /dev/null
@@ -1,153 +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.client.api;
-
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.dom.client.Document;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.Node;
-import com.google.gwt.dom.client.StyleInjector;
-import com.google.gwt.user.client.DOM;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-
-final class HtmlTemplate {
-  static native void init() /*-{
-    var ElementSet = function(r,e) {
-      this.root = r;
-      this.elements = e;
-    };
-    ElementSet.prototype = {
-      clear: function() {
-        this.root = null;
-        this.elements = null;
-      },
-    };
-
-    $wnd.Gerrit.css = @com.google.gerrit.client.api.HtmlTemplate::css(Ljava/lang/String;);
-    $wnd.Gerrit.html = function(h,r,w) {
-      var i = {};
-      if (r) {
-        h = h.replace(
-          /\sid=['"]\{([a-z_][a-z0-9_]*)\}['"]|\{([a-z0-9._-]+)\}/gi,
-          function(m,a,b) {
-            if (a)
-              return @com.google.gerrit.client.api.HtmlTemplate::id(
-                  Lcom/google/gerrit/client/api/HtmlTemplate$IdMap;
-                  Ljava/lang/String;)
-                (i,a);
-            return @com.google.gerrit.client.api.HtmlTemplate::html(
-                Lcom/google/gerrit/client/api/HtmlTemplate$ReplacementMap;
-                Ljava/lang/String;)
-              (r,b);
-          });
-      }
-      var e = @com.google.gerrit.client.api.HtmlTemplate::parseHtml(
-          Ljava/lang/String;Lcom/google/gerrit/client/api/HtmlTemplate$IdMap;
-          Lcom/google/gerrit/client/api/HtmlTemplate$ReplacementMap;
-          Z)
-        (h,i,r,!!w);
-      return w ? new ElementSet(e,i) : e;
-    };
-  }-*/;
-
-  private static String css(String css) {
-    String name = DOM.createUniqueId();
-    StyleInjector.inject("." + name + "{" + css + "}");
-    return name;
-  }
-
-  private static String id(IdMap idMap, String key) {
-    String id = DOM.createUniqueId();
-    idMap.put(id, key);
-    return " id='" + id + "'";
-  }
-
-  private static String html(ReplacementMap opts, String id) {
-    int d = id.indexOf('.');
-    if (0 < d) {
-      String name = id.substring(0, d);
-      String rest = id.substring(d + 1);
-      return html(opts.map(name), rest);
-    }
-    return new SafeHtmlBuilder().append(opts.str(id)).asString();
-  }
-
-  private static Node parseHtml(String html, IdMap ids, ReplacementMap opts, boolean wantElements) {
-    Element div = Document.get().createDivElement();
-    div.setInnerHTML(html);
-    if (!ids.isEmpty()) {
-      attachHandlers(div, ids, opts, wantElements);
-    }
-    if (div.getChildCount() == 1) {
-      return div.getFirstChild();
-    }
-    return div;
-  }
-
-  private static void attachHandlers(
-      Element e, IdMap ids, ReplacementMap opts, boolean wantElements) {
-    if (e.getId() != null) {
-      String key = ids.get(e.getId());
-      if (key != null) {
-        ids.remove(e.getId());
-        if (wantElements) {
-          ids.put(key, e);
-        }
-        e.setId(null);
-        opts.map(key).attachHandlers(e);
-      }
-    }
-    for (Element c = e.getFirstChildElement(); c != null; ) {
-      attachHandlers(c, ids, opts, wantElements);
-      c = c.getNextSiblingElement();
-    }
-  }
-
-  private static class ReplacementMap extends JavaScriptObject {
-    final native ReplacementMap map(String n) /*-{ return this[n] }-*/;
-
-    final native String str(String n) /*-{ return ''+this[n] }-*/;
-
-    final native void attachHandlers(Element e) /*-{
-      for (var k in this) {
-        var f = this[k];
-        if (k.substring(0, 2) == 'on' && typeof f == 'function')
-          e[k] = f;
-      }
-    }-*/;
-
-    protected ReplacementMap() {}
-  }
-
-  private static class IdMap extends JavaScriptObject {
-    final native String get(String i) /*-{ return this[i] }-*/;
-
-    final native void remove(String i) /*-{ delete this[i] }-*/;
-
-    final native void put(String i, String k) /*-{ this[i] = k }-*/;
-
-    final native void put(String k, Element e) /*-{ this[k] = e }-*/;
-
-    final native boolean isEmpty() /*-{
-      for (var i in this)
-        return false;
-      return true;
-    }-*/;
-
-    protected IdMap() {}
-  }
-
-  private HtmlTemplate() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/Plugin.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/Plugin.java
deleted file mode 100644
index 48a812c1..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/Plugin.java
+++ /dev/null
@@ -1,99 +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.client.api;
-
-import com.google.gwt.core.client.JavaScriptObject;
-
-final class Plugin extends JavaScriptObject {
-  private static final JavaScriptObject TYPE = createType();
-
-  static Plugin create(String url) {
-    int s = "plugins/".length();
-    int e = url.indexOf('/', s);
-    String name = url.substring(s, e);
-    return create(TYPE, url, name);
-  }
-
-  native String url() /*-{ return this._scriptUrl }-*/;
-
-  native String name() /*-{ return this.name }-*/;
-
-  native boolean loaded() /*-{ return this._success || this._failure != null }-*/;
-
-  native Exception failure() /*-{ return this._failure }-*/;
-
-  native void failure(Exception e) /*-{ this._failure = e }-*/;
-
-  native boolean success() /*-{ return this._success || false }-*/;
-
-  native void _initialized() /*-{ this._success = true }-*/;
-
-  private static native Plugin create(JavaScriptObject T, String u, String n)
-      /*-{ return new T(u,n) }-*/ ;
-
-  private static native JavaScriptObject createType() /*-{
-    function Plugin(u, n) {
-      this._scriptUrl = u;
-      this.name = n;
-    }
-    return Plugin;
-  }-*/;
-
-  static native void init() /*-{
-    var G = $wnd.Gerrit;
-    @com.google.gerrit.client.api.Plugin::TYPE.prototype = {
-      getPluginName: function(){return this.name},
-      getServerInfo: @com.google.gerrit.client.api.ApiGlue::getServerInfo(),
-      getCurrentUser: @com.google.gerrit.client.api.ApiGlue::getCurrentUser(),
-      getUserPreferences: @com.google.gerrit.client.api.ApiGlue::getUserPreferences(),
-      refreshUserPreferences: @com.google.gerrit.client.api.ApiGlue::refreshUserPreferences(),
-      go: @com.google.gerrit.client.api.ApiGlue::go(Ljava/lang/String;),
-      refresh: @com.google.gerrit.client.api.ApiGlue::refresh(),
-      refreshMenuBar: @com.google.gerrit.client.api.ApiGlue::refreshMenuBar(),
-      isSignedIn: @com.google.gerrit.client.api.ApiGlue::isSignedIn(),
-      showError: @com.google.gerrit.client.api.ApiGlue::showError(Ljava/lang/String;),
-      on: function(e,f){G.on(e,f)},
-      onAction: function(t,n,c){G._onAction(this.name,t,n,c)},
-      screen: function(p,c){G._screen(this.name,p,c)},
-      settingsScreen: function(p,m,c){G._settingsScreen(this.name,p,m,c)},
-      panel: function(i,c,n){G._panel(this.name,i,c,n)},
-
-      url: function (u){return G.url(this._url(u))},
-      get: function(u,b){@com.google.gerrit.client.api.ActionContext::get(
-        Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;)(this._api(u),b)},
-      post: function(u,i,b){@com.google.gerrit.client.api.ActionContext::post(
-        Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;Lcom/google/gwt/core/client/JavaScriptObject;)(
-        this._api(u),i,b)},
-      put: function(u,i,b){@com.google.gerrit.client.api.ActionContext::put(
-        Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;Lcom/google/gwt/core/client/JavaScriptObject;)(
-        this._api(u),i,b)},
-      'delete': function(u,b){@com.google.gerrit.client.api.ActionContext::delete(
-        Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;)(this._api(u),b)},
-      del: function(u,b){@com.google.gerrit.client.api.ActionContext::delete(
-        Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;)(this._api(u),b)},
-
-      _loadedGwt: function(){@com.google.gerrit.client.api.PluginLoader::loaded()()},
-      _api: function(u){return @com.google.gerrit.client.rpc.RestApi::new(Ljava/lang/String;)(this._url(u))},
-      _url: function (d) {
-        var u = 'plugins/' + this.name + '/';
-        if (d && d.length > 0)
-          return u + (d.charAt(0)=='/' ? d.substring(1) : d);
-        return u;
-      },
-    };
-  }-*/;
-
-  protected Plugin() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginLoader.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginLoader.java
deleted file mode 100644
index de25ef0..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginLoader.java
+++ /dev/null
@@ -1,196 +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.client.api;
-
-import static java.util.stream.Collectors.toList;
-
-import com.google.gerrit.client.ErrorDialog;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.VoidResult;
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gwt.core.client.Callback;
-import com.google.gwt.core.client.CodeDownloadException;
-import com.google.gwt.core.client.ScriptInjector;
-import com.google.gwt.user.client.Timer;
-import com.google.gwt.user.client.Window;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.DialogBox;
-import com.google.gwtexpui.progress.client.ProgressBar;
-import java.util.List;
-
-/** Loads JavaScript plugins with a progress meter visible. */
-public class PluginLoader extends DialogBox {
-  private static PluginLoader self;
-
-  public static void load(
-      List<String> plugins, int loadTimeout, AsyncCallback<VoidResult> callback) {
-    if (plugins == null || plugins.isEmpty()) {
-      callback.onSuccess(VoidResult.create());
-    } else {
-      plugins = plugins.stream().filter(p -> p.endsWith(".js")).collect(toList());
-      if (plugins.isEmpty()) {
-        callback.onSuccess(VoidResult.create());
-      } else {
-        self = new PluginLoader(loadTimeout, callback);
-        self.load(plugins);
-        self.startTimers();
-        self.center();
-      }
-    }
-  }
-
-  static void loaded() {
-    self.loadedOne();
-  }
-
-  private final int loadTimeout;
-  private final AsyncCallback<VoidResult> callback;
-  private ProgressBar progress;
-  private Timer show;
-  private Timer update;
-  private Timer timeout;
-  private boolean visible;
-
-  private PluginLoader(int loadTimeout, AsyncCallback<VoidResult> cb) {
-    super(/* auto hide */ false, /* modal */ true);
-    callback = cb;
-    this.loadTimeout = loadTimeout;
-    progress = new ProgressBar(Gerrit.C.loadingPlugins());
-
-    setStyleName(Gerrit.RESOURCES.css().errorDialog());
-    addStyleName(Gerrit.RESOURCES.css().loadingPluginsDialog());
-  }
-
-  private void load(List<String> pluginUrls) {
-    for (String url : pluginUrls) {
-      Plugin plugin = Plugin.create(url);
-      plugins().put(url, plugin);
-      ScriptInjector.fromUrl(url)
-          .setWindow(ScriptInjector.TOP_WINDOW)
-          .setCallback(new LoadCallback(plugin))
-          .inject();
-    }
-  }
-
-  private void startTimers() {
-    show =
-        new Timer() {
-          @Override
-          public void run() {
-            setText(Window.getTitle());
-            setWidget(progress);
-            setGlassEnabled(true);
-            getGlassElement().addClassName(Gerrit.RESOURCES.css().errorDialogGlass());
-            hide(true);
-            center();
-            visible = true;
-          }
-        };
-    show.schedule(500);
-
-    update =
-        new Timer() {
-          private int cycle;
-
-          @Override
-          public void run() {
-            progress.setValue(100 * ++cycle * 250 / loadTimeout);
-          }
-        };
-    update.scheduleRepeating(250);
-
-    timeout =
-        new Timer() {
-          @Override
-          public void run() {
-            finish();
-          }
-        };
-    timeout.schedule(loadTimeout);
-  }
-
-  private void loadedOne() {
-    boolean done = true;
-    for (Plugin plugin : Natives.asList(plugins().values())) {
-      done &= plugin.loaded();
-    }
-    if (done) {
-      finish();
-    }
-  }
-
-  private void finish() {
-    show.cancel();
-    update.cancel();
-    timeout.cancel();
-    self = null;
-
-    if (!hadFailures()) {
-      if (visible) {
-        progress.setValue(100);
-        new Timer() {
-          @Override
-          public void run() {
-            hide(true);
-          }
-        }.schedule(250);
-      } else {
-        hide(true);
-      }
-    }
-
-    callback.onSuccess(VoidResult.create());
-  }
-
-  private boolean hadFailures() {
-    boolean failed = false;
-    for (Plugin plugin : Natives.asList(plugins().values())) {
-      if (!plugin.success()) {
-        failed = true;
-
-        Exception e = plugin.failure();
-        String msg;
-        if (e != null && e instanceof CodeDownloadException) {
-          msg = Gerrit.M.cannotDownloadPlugin(plugin.url());
-        } else {
-          msg = Gerrit.M.pluginFailed(plugin.name());
-        }
-        hide(true);
-        new ErrorDialog(msg).center();
-      }
-    }
-    return failed;
-  }
-
-  private static native NativeMap<Plugin> plugins() /*-{ return $wnd.Gerrit.plugins }-*/;
-
-  private class LoadCallback implements Callback<Void, Exception> {
-    private final Plugin plugin;
-
-    LoadCallback(Plugin plugin) {
-      this.plugin = plugin;
-    }
-
-    @Override
-    public void onSuccess(Void result) {}
-
-    @Override
-    public void onFailure(Exception reason) {
-      plugin.failure(reason);
-      loadedOne();
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginName.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginName.java
deleted file mode 100644
index 7cf4fbb..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginName.java
+++ /dev/null
@@ -1,107 +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.client.api;
-
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.core.client.JavaScriptException;
-import com.google.gwt.core.client.JsArrayString;
-
-/**
- * Determines the name a plugin has been installed under.
- *
- * <p>This implementation guesses the name a plugin runs under by looking at the JavaScript call
- * stack and identifying the URL of the script file calling {@code Gerrit.install()}. The simple
- * approach applied here is looking at the source URLs and extracting the name out of the string,
- * e.g.: {@code "http://localhost:8080/plugins/[name]/static/foo.js"}.
- */
-class PluginName {
-  private static final String UNKNOWN = "<unknown>";
-
-  private static String baseUrl() {
-    return GWT.getHostPageBaseURL() + "plugins/";
-  }
-
-  static String getCallerUrl() {
-    return GWT.<PluginName>create(PluginName.class).findCallerUrl();
-  }
-
-  static String fromUrl(String url) {
-    String baseUrl = baseUrl();
-    if (url != null && url.startsWith(baseUrl)) {
-      int s = url.indexOf('/', baseUrl.length());
-      if (s > 0) {
-        return url.substring(baseUrl.length(), s);
-      }
-    }
-    return UNKNOWN;
-  }
-
-  String findCallerUrl() {
-    JavaScriptException err = makeException();
-    if (hasStack(err)) {
-      return PluginNameMoz.getUrl(err);
-    }
-
-    String baseUrl = baseUrl();
-    StackTraceElement[] trace = getTrace(err);
-    for (int i = trace.length - 1; i >= 0; i--) {
-      String u = trace[i].getFileName();
-      if (u != null && u.startsWith(baseUrl)) {
-        return u;
-      }
-    }
-    return UNKNOWN;
-  }
-
-  private static StackTraceElement[] getTrace(JavaScriptException err) {
-    if (err.getStackTrace().length == 0) {
-      err.fillInStackTrace();
-    }
-    return err.getStackTrace();
-  }
-
-  protected static final native JavaScriptException makeException()
-      /*-{ try { null.a() } catch (e) { return e } }-*/ ;
-
-  private static native boolean hasStack(JavaScriptException e) /*-{ return !!e.stack }-*/;
-
-  /** Extracts URL from the stack frame. */
-  static class PluginNameMoz extends PluginName {
-    @Override
-    String findCallerUrl() {
-      return getUrl(makeException());
-    }
-
-    private static String getUrl(JavaScriptException e) {
-      String baseUrl = baseUrl();
-      JsArrayString stack = getStack(e);
-      for (int i = stack.length() - 1; i >= 0; i--) {
-        String frame = stack.get(i);
-        int at = frame.indexOf(baseUrl);
-        if (at >= 0) {
-          int end = frame.indexOf(':', at + baseUrl.length());
-          if (end < 0) {
-            end = frame.length();
-          }
-          return frame.substring(at, end);
-        }
-      }
-      return UNKNOWN;
-    }
-
-    private static native JsArrayString getStack(JavaScriptException e)
-        /*-{ return e.stack ? e.stack.split('\n') : [] }-*/ ;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PopupHelper.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PopupHelper.java
deleted file mode 100644
index 173b369..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PopupHelper.java
+++ /dev/null
@@ -1,72 +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.client.api;
-
-import com.google.gerrit.client.actions.ActionButton;
-import com.google.gerrit.client.change.Resources;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.event.logical.shared.CloseEvent;
-import com.google.gwt.event.logical.shared.CloseHandler;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.PopupPanel;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-
-class PopupHelper {
-  static PopupHelper popup(ActionContext ctx, Element panel) {
-    PopupHelper helper = new PopupHelper(ctx.button(), panel);
-    helper.show();
-    ctx.button().link(ctx);
-    return helper;
-  }
-
-  private final ActionButton activatingButton;
-  private final FlowPanel panel;
-  private PopupPanel popup;
-
-  PopupHelper(ActionButton button, Element child) {
-    activatingButton = button;
-    panel = new FlowPanel();
-    panel.setStyleName(Resources.I.style().popupContent());
-    panel.getElement().appendChild(child);
-  }
-
-  void show() {
-    final PopupPanel p = new PopupPanel(true);
-    p.setStyleName(Resources.I.style().popup());
-    p.addAutoHidePartner(activatingButton.getElement());
-    p.addCloseHandler(
-        new CloseHandler<PopupPanel>() {
-          @Override
-          public void onClose(CloseEvent<PopupPanel> event) {
-            activatingButton.unlink();
-            if (popup == p) {
-              popup = null;
-            }
-          }
-        });
-    p.add(panel);
-    p.showRelativeTo(activatingButton);
-    GlobalKey.dialog(p);
-    popup = p;
-  }
-
-  void hide() {
-    if (popup != null) {
-      activatingButton.unlink();
-      popup.hide();
-      popup = null;
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ProjectGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ProjectGlue.java
deleted file mode 100644
index 92070f8..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ProjectGlue.java
+++ /dev/null
@@ -1,65 +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.client.api;
-
-import com.google.gerrit.client.actions.ActionButton;
-import com.google.gerrit.client.info.ActionInfo;
-import com.google.gerrit.client.projects.BranchInfo;
-import com.google.gerrit.client.projects.ProjectApi;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.JavaScriptObject;
-
-public class ProjectGlue {
-  public static void onAction(
-      Project.NameKey project, BranchInfo branch, ActionInfo action, ActionButton button) {
-    RestApi api = ProjectApi.project(project).view("branches").id(branch.ref()).view(action.id());
-    JavaScriptObject f = branchAction(action.id());
-    if (f != null) {
-      ActionContext c = ActionContext.create(api);
-      c.set(action);
-      c.set(project);
-      c.set(branch);
-      c.button(button);
-      ApiGlue.invoke(f, c);
-    } else {
-      DefaultActions.invoke(project, action, api);
-    }
-  }
-
-  public static void onAction(Project.NameKey project, ActionInfo action, ActionButton button) {
-    RestApi api = ProjectApi.project(project).view(action.id());
-    JavaScriptObject f = projectAction(action.id());
-    if (f != null) {
-      ActionContext c = ActionContext.create(api);
-      c.set(action);
-      c.set(project);
-      c.button(button);
-      ApiGlue.invoke(f, c);
-    } else {
-      DefaultActions.invoke(project, action, api);
-    }
-  }
-
-  private static native JavaScriptObject projectAction(String id) /*-{
-    return $wnd.Gerrit.project_actions[id];
-  }-*/;
-
-  private static native JavaScriptObject branchAction(String id) /*-{
-    return $wnd.Gerrit.branch_actions[id];
-  }-*/;
-
-  private ProjectGlue() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/RevisionGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/RevisionGlue.java
deleted file mode 100644
index d1029b2..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/RevisionGlue.java
+++ /dev/null
@@ -1,50 +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.client.api;
-
-import com.google.gerrit.client.actions.ActionButton;
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.info.ActionInfo;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gwt.core.client.JavaScriptObject;
-
-public class RevisionGlue {
-  public static void onAction(
-      ChangeInfo change, RevisionInfo revision, ActionInfo action, ActionButton button) {
-    RestApi api =
-        ChangeApi.revision(change.project(), change.legacyId().get(), revision.name())
-            .view(action.id());
-
-    JavaScriptObject f = get(action.id());
-    if (f != null) {
-      ActionContext c = ActionContext.create(api);
-      c.set(action);
-      c.set(change);
-      c.set(revision);
-      c.button(button);
-      ApiGlue.invoke(f, c);
-    } else {
-      DefaultActions.invoke(change, action, api);
-    }
-  }
-
-  private static native JavaScriptObject get(String id) /*-{
-    return $wnd.Gerrit.revision_actions[id];
-  }-*/;
-
-  private RevisionGlue() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.java
deleted file mode 100644
index a0eaef7..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.java
+++ /dev/null
@@ -1,23 +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.client.auth.openid;
-
-import com.google.gwt.i18n.client.Constants;
-
-public interface OpenIdConstants extends Constants {
-  String nameLaunchpad();
-
-  String nameYahoo();
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.properties
deleted file mode 100644
index d6e8de6..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.properties
+++ /dev/null
@@ -1,2 +0,0 @@
-nameLaunchpad = Launchpad ID
-nameYahoo = Yahoo! ID
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdUtil.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdUtil.java
deleted file mode 100644
index d5b7684..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdUtil.java
+++ /dev/null
@@ -1,25 +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.client.auth.openid;
-
-import com.google.gwt.core.client.GWT;
-
-public class OpenIdUtil {
-  public static final OpenIdConstants C;
-
-  static {
-    C = GWT.create(OpenIdConstants.class);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/blame/BlameInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/blame/BlameInfo.java
deleted file mode 100644
index 77fddeb..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/blame/BlameInfo.java
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.blame;
-
-import com.google.gerrit.client.RangeInfo;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
-
-public class BlameInfo extends JavaScriptObject {
-  public final native String author() /*-{ return this.author; }-*/;
-
-  public final native String id() /*-{ return this.id; }-*/;
-
-  public final native String commitMsg() /*-{ return this.commit_msg; }-*/;
-
-  public final native int time() /*-{ return this.time; }-*/;
-
-  public final native JsArray<RangeInfo> ranges() /*-{ return this.ranges; }-*/;
-
-  protected BlameInfo() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AbandonAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AbandonAction.java
deleted file mode 100644
index fd58959..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AbandonAction.java
+++ /dev/null
@@ -1,50 +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.client.change;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.user.client.ui.Button;
-
-class AbandonAction extends ActionMessageBox {
-  private final Project.NameKey project;
-  private final Change.Id id;
-
-  AbandonAction(Button b, Project.NameKey project, Change.Id id) {
-    super(b);
-    this.project = project;
-    this.id = id;
-  }
-
-  @Override
-  void send(String message) {
-    ChangeApi.abandon(
-        project.get(),
-        id.get(),
-        message,
-        new GerritCallback<ChangeInfo>() {
-          @Override
-          public void onSuccess(ChangeInfo result) {
-            Gerrit.display(PageLinks.toChange(project, id));
-            hide();
-          }
-        });
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.java
deleted file mode 100644
index 5b3ee29..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.java
+++ /dev/null
@@ -1,105 +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.client.change;
-
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.logical.shared.CloseEvent;
-import com.google.gwt.event.logical.shared.CloseHandler;
-import com.google.gwt.resources.client.CssResource;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.uibinder.client.UiHandler;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwt.user.client.ui.PopupPanel;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-import com.google.gwtexpui.globalkey.client.NpTextArea;
-
-abstract class ActionMessageBox extends Composite {
-  interface Binder extends UiBinder<HTMLPanel, ActionMessageBox> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  interface Style extends CssResource {
-    String popup();
-  }
-
-  private final Button activatingButton;
-  private PopupPanel popup;
-
-  @UiField Style style;
-  @UiField NpTextArea message;
-  @UiField Button send;
-
-  ActionMessageBox(Button button) {
-    this.activatingButton = button;
-    initWidget(uiBinder.createAndBindUi(this));
-    send.setText(button.getText());
-  }
-
-  abstract void send(String message);
-
-  void show() {
-    if (popup != null) {
-      popup.hide();
-      popup = null;
-      return;
-    }
-
-    final PopupPanel p = new PopupPanel(true);
-    p.setStyleName(style.popup());
-    p.addAutoHidePartner(activatingButton.getElement());
-    p.addCloseHandler(
-        new CloseHandler<PopupPanel>() {
-          @Override
-          public void onClose(CloseEvent<PopupPanel> event) {
-            if (popup == p) {
-              popup = null;
-            }
-          }
-        });
-    p.add(this);
-    p.showRelativeTo(activatingButton);
-    GlobalKey.dialog(p);
-    message.setFocus(true);
-    popup = p;
-  }
-
-  void hide() {
-    if (popup != null) {
-      popup.hide();
-      popup = null;
-    }
-  }
-
-  @UiHandler("message")
-  void onMessageKey(KeyPressEvent event) {
-    if ((event.getCharCode() == '\n' || event.getCharCode() == KeyCodes.KEY_ENTER)
-        && event.isControlKeyDown()) {
-      event.preventDefault();
-      event.stopPropagation();
-      onSend(null);
-    }
-  }
-
-  @UiHandler("send")
-  void onSend(@SuppressWarnings("unused") ClickEvent e) {
-    send(message.getValue().trim());
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.ui.xml
deleted file mode 100644
index a9e1bb3..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.ui.xml
+++ /dev/null
@@ -1,47 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder
-    xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'
-    xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'>
-  <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
-  <ui:style gss='false' type='com.google.gerrit.client.change.ActionMessageBox.Style'>
-    @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
-
-    .popup { background-color: trimColor; }
-    .section {
-      padding: 5px 5px;
-      border-bottom: 1px solid #b8b8b8;
-    }
-  </ui:style>
-  <g:HTMLPanel>
-    <div class='{style.section}'>
-      <c:NpTextArea
-         visibleLines='3'
-         characterWidth='40'
-         ui:field='message'/>
-    </div>
-    <div class='{style.section}'>
-      <g:Button ui:field='send'
-          title='(Shortcut: Ctrl-Enter)'
-          styleName='{res.style.button}'>
-        <ui:attribute name='title'/>
-        <div><ui:msg>Send</ui:msg></div>
-      </g:Button>
-    </div>
-  </g:HTMLPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
deleted file mode 100644
index e4f5e576..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
+++ /dev/null
@@ -1,260 +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.client.change;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.actions.ActionButton;
-import com.google.gerrit.client.info.ActionInfo;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.info.ChangeInfo.CommitInfo;
-import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.uibinder.client.UiHandler;
-import com.google.gwt.user.client.Window;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-import java.util.TreeSet;
-
-class Actions extends Composite {
-  private static final String[] CORE = {
-    "abandon",
-    "assignee",
-    "cherrypick",
-    "description",
-    "followup",
-    "hashtags",
-    "move",
-    "publish",
-    "rebase",
-    "restore",
-    "revert",
-    "submit",
-    "topic",
-    "private",
-    "/",
-  };
-
-  interface Binder extends UiBinder<FlowPanel, Actions> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  @UiField Button cherrypick;
-  @UiField Button move;
-  @UiField Button rebase;
-  @UiField Button revert;
-  @UiField Button submit;
-
-  @UiField Button abandon;
-  private AbandonAction abandonAction;
-
-  @UiField Button deleteChange;
-
-  @UiField Button markPrivate;
-  @UiField Button unmarkPrivate;
-
-  @UiField Button restore;
-  private RestoreAction restoreAction;
-
-  @UiField Button followUp;
-  private FollowUpAction followUpAction;
-
-  private Change.Id changeId;
-  private ChangeInfo changeInfo;
-  private String revision;
-  private Project.NameKey project;
-  private String topic;
-  private String subject;
-  private String message;
-  private String branch;
-  private String key;
-
-  private boolean rebaseParentNotCurrent = true;
-
-  Actions() {
-    initWidget(uiBinder.createAndBindUi(this));
-    getElement().setId("change_actions");
-  }
-
-  void display(ChangeInfo info, String revision) {
-    this.revision = revision;
-
-    boolean hasUser = Gerrit.isSignedIn();
-    RevisionInfo revInfo = info.revision(revision);
-    CommitInfo commit = revInfo.commit();
-    changeId = info.legacyId();
-    project = info.projectNameKey();
-    topic = info.topic();
-    subject = commit.subject();
-    message = commit.message();
-    branch = info.branch();
-    key = info.changeId();
-    changeInfo = info;
-
-    initChangeActions(info, hasUser);
-
-    NativeMap<ActionInfo> actionMap =
-        revInfo.hasActions() ? revInfo.actions() : NativeMap.<ActionInfo>create();
-    actionMap.copyKeysIntoChildren("id");
-    reloadRevisionActions(actionMap);
-  }
-
-  private void initChangeActions(ChangeInfo info, boolean hasUser) {
-    NativeMap<ActionInfo> actions =
-        info.hasActions() ? info.actions() : NativeMap.<ActionInfo>create();
-    actions.copyKeysIntoChildren("id");
-
-    if (hasUser) {
-      a2b(actions, "abandon", abandon);
-      a2b(actions, "/", deleteChange);
-      a2b(actions, "move", move);
-      a2b(actions, "restore", restore);
-      a2b(actions, "revert", revert);
-      a2b(actions, "followup", followUp);
-      if (info.isPrivate()) {
-        a2b(actions, "private", unmarkPrivate);
-      } else {
-        a2b(actions, "private", markPrivate);
-      }
-      for (String id : filterNonCore(actions)) {
-        add(new ActionButton(info, actions.get(id)));
-      }
-    }
-  }
-
-  void reloadRevisionActions(NativeMap<ActionInfo> actions) {
-    if (!Gerrit.isSignedIn()) {
-      return;
-    }
-    boolean canSubmit = actions.containsKey("submit");
-    if (canSubmit) {
-      ActionInfo action = actions.get("submit");
-      submit.setTitle(action.title());
-      submit.setEnabled(action.enabled());
-      submit.setHTML(new SafeHtmlBuilder().openDiv().append(action.label()).closeDiv());
-      submit.setEnabled(action.enabled());
-    }
-    submit.setVisible(canSubmit);
-
-    a2b(actions, "cherrypick", cherrypick);
-    a2b(actions, "rebase", rebase);
-
-    // The rebase button on change screen is always enabled.
-    // It is the "Rebase" button in the RebaseDialog that might be disabled.
-    rebaseParentNotCurrent = rebase.isEnabled();
-    if (rebase.isVisible()) {
-      rebase.setEnabled(true);
-    }
-    RevisionInfo revInfo = changeInfo.revision(revision);
-    for (String id : filterNonCore(actions)) {
-      add(new ActionButton(changeInfo, revInfo, actions.get(id)));
-    }
-  }
-
-  private void add(ActionButton b) {
-    ((FlowPanel) getWidget()).add(b);
-  }
-
-  private static TreeSet<String> filterNonCore(NativeMap<ActionInfo> m) {
-    TreeSet<String> ids = new TreeSet<>(m.keySet());
-    for (String id : CORE) {
-      ids.remove(id);
-    }
-    return ids;
-  }
-
-  @UiHandler("followUp")
-  void onFollowUp(@SuppressWarnings("unused") ClickEvent e) {
-    if (followUpAction == null) {
-      followUpAction = new FollowUpAction(followUp, project.get(), branch, topic, key);
-    }
-    followUpAction.show();
-  }
-
-  @UiHandler("abandon")
-  void onAbandon(@SuppressWarnings("unused") ClickEvent e) {
-    if (abandonAction == null) {
-      abandonAction = new AbandonAction(abandon, project, changeId);
-    }
-    abandonAction.show();
-  }
-
-  @UiHandler("deleteChange")
-  void onDeleteChange(@SuppressWarnings("unused") ClickEvent e) {
-    if (Window.confirm(Resources.C.deleteChange())) {
-      ChangeActions.delete(project, changeId, deleteChange);
-    }
-  }
-
-  @UiHandler("markPrivate")
-  void onMarkPrivate(@SuppressWarnings("unused") ClickEvent e) {
-    ChangeActions.markPrivate(project, changeId, markPrivate);
-  }
-
-  @UiHandler("unmarkPrivate")
-  void onUnmarkPrivate(@SuppressWarnings("unused") ClickEvent e) {
-    ChangeActions.unmarkPrivate(project, changeId, unmarkPrivate);
-  }
-
-  @UiHandler("restore")
-  void onRestore(@SuppressWarnings("unused") ClickEvent e) {
-    if (restoreAction == null) {
-      restoreAction = new RestoreAction(restore, project, changeId);
-    }
-    restoreAction.show();
-  }
-
-  @UiHandler("rebase")
-  void onRebase(@SuppressWarnings("unused") ClickEvent e) {
-    RebaseAction.call(
-        rebase, project, changeInfo.branch(), changeId, revision, rebaseParentNotCurrent);
-  }
-
-  @UiHandler("submit")
-  void onSubmit(@SuppressWarnings("unused") ClickEvent e) {
-    SubmitAction.call(changeInfo, changeInfo.revision(revision));
-  }
-
-  @UiHandler("cherrypick")
-  void onCherryPick(@SuppressWarnings("unused") ClickEvent e) {
-    CherryPickAction.call(cherrypick, changeInfo, revision, project, message);
-  }
-
-  @UiHandler("move")
-  void onMove(@SuppressWarnings("unused") ClickEvent e) {
-    MoveAction.call(move, changeInfo, project);
-  }
-
-  @UiHandler("revert")
-  void onRevert(@SuppressWarnings("unused") ClickEvent e) {
-    RevertAction.call(revert, changeId, project, revision, subject);
-  }
-
-  private static void a2b(NativeMap<ActionInfo> actions, String a, Button b) {
-    if (actions.containsKey(a)) {
-      b.setVisible(true);
-      ActionInfo actionInfo = actions.get(a);
-      b.setTitle(actionInfo.title());
-      b.setEnabled(actionInfo.enabled());
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml
deleted file mode 100644
index 8aeba90..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml
+++ /dev/null
@@ -1,96 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder
-    xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:style gss='false'>
-    @def BUTTON_HEIGHT 14px;
-
-    #change_actions {
-      padding-top: 2px;
-      padding-bottom: 20px;
-    }
-
-    #change_actions button {
-      margin: 6px 3px 0 0;
-      text-align: center;
-      font-size: 8pt;
-      font-weight: bold;
-      border: 2px solid;
-      cursor: pointer;
-      color: rgba(0, 0, 0, 0.15);
-      background-color: #f5f5f5;
-      -webkit-border-radius: 2px;
-      -webkit-box-sizing: content-box;
-    }
-    #change_actions button div {
-      color: #444;
-      min-width: 54px;
-      white-space: nowrap;
-      height: BUTTON_HEIGHT;
-      line-height: BUTTON_HEIGHT;
-    }
-
-    #change_actions button.submit {
-      float: right;
-      background-color: #4d90fe;
-      background-image: -webkit-linear-gradient(top, #4d90fe, #4d90fe);
-    }
-    #change_actions button.submit div {color: #fff;}
-
-    #change_actions button:disabled {
-      font-weight: normal;
-      background-color: #999;
-      background-image: -webkit-linear-gradient(top, #999, #999);
-    }
-  </ui:style>
-
-  <g:FlowPanel>
-    <g:Button ui:field='cherrypick' styleName='' visible='false'>
-      <div><ui:msg>Cherry Pick</ui:msg></div>
-    </g:Button>
-    <g:Button ui:field='move' styleName='' visible='false'>
-      <div><ui:msg>Move Change</ui:msg></div>
-    </g:Button>
-    <g:Button ui:field='rebase' styleName='' visible='false'>
-      <div><ui:msg>Rebase</ui:msg></div>
-    </g:Button>
-    <g:Button ui:field='revert' styleName='' visible='false'>
-      <div><ui:msg>Revert</ui:msg></div>
-    </g:Button>
-    <g:Button ui:field='abandon' styleName='' visible='false'>
-      <div><ui:msg>Abandon</ui:msg></div>
-    </g:Button>
-    <g:Button ui:field='deleteChange' styleName='' visible='false'>
-      <div><ui:msg>Delete Change</ui:msg></div>
-    </g:Button>
-    <g:Button ui:field='restore' styleName='' visible='false'>
-      <div><ui:msg>Restore</ui:msg></div>
-    </g:Button>
-    <g:Button ui:field='followUp' styleName='' visible='false'>
-      <div><ui:msg>Follow-Up</ui:msg></div>
-    </g:Button>
-    <g:Button ui:field='markPrivate' styleName='' visible='false'>
-      <div><ui:msg>Mark Private</ui:msg></div>
-    </g:Button>
-    <g:Button ui:field='unmarkPrivate' styleName='' visible='false'>
-      <div><ui:msg>Unmark Private</ui:msg></div>
-    </g:Button>
-
-    <g:Button ui:field='submit' styleName='{style.submit}' visible='false'/>
-  </g:FlowPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileAction.java
deleted file mode 100644
index 2080a0e..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileAction.java
+++ /dev/null
@@ -1,82 +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.client.change;
-
-import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.event.logical.shared.CloseEvent;
-import com.google.gwt.event.logical.shared.CloseHandler;
-import com.google.gwt.user.client.ui.PopupPanel;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-
-class AddFileAction {
-  private final Project.NameKey project;
-  private final Change.Id changeId;
-  private final RevisionInfo revision;
-  private final ChangeScreen.Style style;
-  private final Widget addButton;
-  private final FileTable files;
-
-  private AddFileBox addBox;
-  private PopupPanel popup;
-
-  AddFileAction(
-      Project.NameKey project,
-      Change.Id changeId,
-      RevisionInfo revision,
-      ChangeScreen.Style style,
-      Widget addButton,
-      FileTable files) {
-    this.project = project;
-    this.changeId = changeId;
-    this.revision = revision;
-    this.style = style;
-    this.addButton = addButton;
-    this.files = files;
-  }
-
-  public void onEdit() {
-    if (popup != null) {
-      popup.hide();
-      return;
-    }
-
-    files.unregisterKeys();
-    if (addBox == null) {
-      addBox = new AddFileBox(project, changeId, revision, files);
-    }
-    addBox.clearPath();
-
-    final PopupPanel p = new PopupPanel(true);
-    p.setStyleName(style.replyBox());
-    p.addAutoHidePartner(addButton.getElement());
-    p.addCloseHandler(
-        new CloseHandler<PopupPanel>() {
-          @Override
-          public void onClose(CloseEvent<PopupPanel> event) {
-            if (popup == p) {
-              popup = null;
-            }
-          }
-        });
-    p.add(addBox);
-    p.showRelativeTo(addButton);
-    GlobalKey.dialog(p);
-    addBox.setFocus(true);
-    popup = p;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileBox.java
deleted file mode 100644
index cd862d2..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileBox.java
+++ /dev/null
@@ -1,114 +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.client.change;
-
-import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
-import com.google.gerrit.client.ui.RemoteSuggestBox;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.logical.shared.CloseEvent;
-import com.google.gwt.event.logical.shared.CloseHandler;
-import com.google.gwt.event.logical.shared.SelectionEvent;
-import com.google.gwt.event.logical.shared.SelectionHandler;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.uibinder.client.UiHandler;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwt.user.client.ui.PopupPanel;
-import com.google.gwt.user.client.ui.Widget;
-
-class AddFileBox extends Composite {
-  interface Binder extends UiBinder<HTMLPanel, AddFileBox> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  private final Project.NameKey project;
-  private final Change.Id changeId;
-  private final RevisionInfo revision;
-  private final FileTable fileTable;
-
-  @UiField Button open;
-  @UiField Button cancel;
-
-  @UiField(provided = true)
-  RemoteSuggestBox path;
-
-  AddFileBox(Project.NameKey project, Change.Id changeId, RevisionInfo revision, FileTable files) {
-    this.project = project;
-    this.changeId = changeId;
-    this.revision = revision;
-    this.fileTable = files;
-
-    path = new RemoteSuggestBox(new PathSuggestOracle(project, changeId, revision));
-    path.addSelectionHandler(
-        new SelectionHandler<String>() {
-          @Override
-          public void onSelection(SelectionEvent<String> event) {
-            open(event.getSelectedItem());
-          }
-        });
-    path.addCloseHandler(
-        new CloseHandler<RemoteSuggestBox>() {
-          @Override
-          public void onClose(CloseEvent<RemoteSuggestBox> event) {
-            hide();
-            fileTable.registerKeys();
-          }
-        });
-
-    initWidget(uiBinder.createAndBindUi(this));
-  }
-
-  void setFocus(boolean focus) {
-    path.setFocus(focus);
-  }
-
-  void clearPath() {
-    path.setText("");
-  }
-
-  @UiHandler("open")
-  void onOpen(@SuppressWarnings("unused") ClickEvent e) {
-    open(path.getText());
-  }
-
-  private void open(String path) {
-    hide();
-    Gerrit.display(
-        Dispatcher.toEditScreen(project, new PatchSet.Id(changeId, revision._number()), path));
-  }
-
-  @UiHandler("cancel")
-  void onCancel(@SuppressWarnings("unused") ClickEvent e) {
-    hide();
-    fileTable.registerKeys();
-  }
-
-  private void hide() {
-    for (Widget w = getParent(); w != null; w = w.getParent()) {
-      if (w instanceof PopupPanel) {
-        ((PopupPanel) w).hide();
-        break;
-      }
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileBox.ui.xml
deleted file mode 100644
index c3539bc..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileBox.ui.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder
-    xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:u='urn:import:com.google.gerrit.client.ui'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
-  <ui:style gss='false'>
-    .cancel { float: right; }
-  </ui:style>
-  <g:HTMLPanel>
-    <div class='{res.style.section}'>
-      <ui:msg>Path: <u:RemoteSuggestBox ui:field='path' visibleLength='86'/></ui:msg>
-    </div>
-    <div class='{res.style.section}'>
-      <g:Button ui:field='open'
-          title='Open file in editor'
-          styleName='{res.style.button}'>
-        <ui:attribute name='title'/>
-        <div><ui:msg>Open</ui:msg></div>
-      </g:Button>
-      <g:Button ui:field='cancel'
-          styleName='{res.style.button}'
-          addStyleNames='{style.cancel}'>
-          <div>Cancel</div>
-      </g:Button>
-    </div>
-  </g:HTMLPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Assignee.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Assignee.java
deleted file mode 100644
index a376782..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Assignee.java
+++ /dev/null
@@ -1,239 +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.client.change;
-
-import com.google.gerrit.client.FormatUtil;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.NotSignedInDialog;
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.Util;
-import com.google.gerrit.client.info.AccountInfo;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.InlineHyperlink;
-import com.google.gerrit.client.ui.RemoteSuggestBox;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.logical.shared.CloseEvent;
-import com.google.gwt.event.logical.shared.CloseHandler;
-import com.google.gwt.event.logical.shared.SelectionEvent;
-import com.google.gwt.event.logical.shared.SelectionHandler;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.uibinder.client.UiHandler;
-import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.EventListener;
-import com.google.gwt.user.client.rpc.StatusCodeException;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwt.user.client.ui.UIObject;
-
-/** Edit assignee using auto-completion. */
-public class Assignee extends Composite {
-  interface Binder extends UiBinder<HTMLPanel, Assignee> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  @UiField Element show;
-  @UiField InlineHyperlink assigneeLink;
-  @UiField Image editAssigneeIcon;
-  @UiField Element form;
-  @UiField Element error;
-
-  @UiField(provided = true)
-  RemoteSuggestBox suggestBox;
-
-  private AssigneeSuggestOracle assigneeSuggestOracle;
-  private Change.Id changeId;
-  private Project.NameKey project;
-  private boolean canEdit;
-  private AccountInfo currentAssignee;
-
-  Assignee() {
-    assigneeSuggestOracle = new AssigneeSuggestOracle();
-    suggestBox = new RemoteSuggestBox(assigneeSuggestOracle);
-    suggestBox.setVisibleLength(55);
-    suggestBox.setHintText(Util.C.approvalTableEditAssigneeHint());
-    suggestBox.addCloseHandler(
-        new CloseHandler<RemoteSuggestBox>() {
-          @Override
-          public void onClose(CloseEvent<RemoteSuggestBox> event) {
-            Assignee.this.onCancel(null);
-          }
-        });
-    suggestBox.addSelectionHandler(
-        new SelectionHandler<String>() {
-          @Override
-          public void onSelection(SelectionEvent<String> event) {
-            editAssignee(event.getSelectedItem());
-          }
-        });
-
-    initWidget(uiBinder.createAndBindUi(this));
-    editAssigneeIcon.addDomHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            onOpenForm();
-          }
-        },
-        ClickEvent.getType());
-  }
-
-  void set(ChangeInfo info) {
-    this.changeId = info.legacyId();
-    this.project = info.projectNameKey();
-    this.canEdit = info.hasActions() && info.actions().containsKey("assignee");
-    assigneeSuggestOracle.setChange(info);
-    setAssignee(info.assignee());
-    editAssigneeIcon.setVisible(canEdit);
-    if (!canEdit) {
-      show.setTitle(null);
-    }
-  }
-
-  void onOpenForm() {
-    UIObject.setVisible(form, true);
-    UIObject.setVisible(show, false);
-    UIObject.setVisible(error, false);
-    editAssigneeIcon.setVisible(false);
-    suggestBox.setFocus(true);
-    if (currentAssignee != null) {
-      suggestBox.setText(FormatUtil.nameEmail(currentAssignee));
-      suggestBox.selectAll();
-    } else {
-      suggestBox.setText("");
-    }
-  }
-
-  void onCloseForm() {
-    UIObject.setVisible(form, false);
-    UIObject.setVisible(show, true);
-    UIObject.setVisible(error, false);
-    editAssigneeIcon.setVisible(true);
-    suggestBox.setFocus(false);
-  }
-
-  @UiHandler("assign")
-  void onEditAssignee(@SuppressWarnings("unused") ClickEvent e) {
-    if (canEdit) {
-      editAssignee(suggestBox.getText());
-    }
-  }
-
-  @UiHandler("cancel")
-  void onCancel(@SuppressWarnings("unused") ClickEvent e) {
-    onCloseForm();
-  }
-
-  private void editAssignee(String assignee) {
-    if (assignee.trim().isEmpty()) {
-      ChangeApi.deleteAssignee(
-          project.get(),
-          changeId.get(),
-          new GerritCallback<AccountInfo>() {
-            @Override
-            public void onSuccess(AccountInfo result) {
-              onCloseForm();
-              setAssignee(null);
-            }
-
-            @Override
-            public void onFailure(Throwable err) {
-              if (isSigninFailure(err)) {
-                new NotSignedInDialog().center();
-              } else {
-                UIObject.setVisible(error, true);
-                error.setInnerText(
-                    err instanceof StatusCodeException
-                        ? ((StatusCodeException) err).getEncodedResponse()
-                        : err.getMessage());
-              }
-            }
-          });
-    } else {
-      ChangeApi.setAssignee(
-          project.get(),
-          changeId.get(),
-          assignee,
-          new GerritCallback<AccountInfo>() {
-            @Override
-            public void onSuccess(AccountInfo result) {
-              onCloseForm();
-              setAssignee(result);
-              Reviewers reviewers = getReviewers();
-              if (reviewers != null) {
-                reviewers.updateReviewerList();
-              }
-            }
-
-            @Override
-            public void onFailure(Throwable err) {
-              if (isSigninFailure(err)) {
-                new NotSignedInDialog().center();
-              } else {
-                UIObject.setVisible(error, true);
-                error.setInnerText(
-                    err instanceof StatusCodeException
-                        ? ((StatusCodeException) err).getEncodedResponse()
-                        : err.getMessage());
-              }
-            }
-          });
-    }
-  }
-
-  private void setAssignee(AccountInfo assignee) {
-    currentAssignee = assignee;
-    assigneeLink.setText(assignee != null ? getName(assignee) : null);
-    assigneeLink.setTargetHistoryToken(
-        assignee != null
-            ? PageLinks.toAssigneeQuery(
-                assignee.name() != null
-                    ? assignee.name()
-                    : assignee.email() != null
-                        ? assignee.email()
-                        : String.valueOf(assignee._accountId()))
-            : "");
-  }
-
-  private Reviewers getReviewers() {
-    Element e = DOM.getParent(getElement());
-    for (e = DOM.getParent(e); e != null; e = DOM.getParent(e)) {
-      EventListener l = DOM.getEventListener(e);
-      if (l instanceof ChangeScreen) {
-        ChangeScreen screen = (ChangeScreen) l;
-        return screen.reviewers;
-      }
-    }
-    return null;
-  }
-
-  private String getName(AccountInfo info) {
-    if (info.name() != null) {
-      return info.name();
-    }
-    if (info.email() != null) {
-      return info.email();
-    }
-    return Gerrit.info().user().anonymousCowardName();
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Assignee.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Assignee.ui.xml
deleted file mode 100644
index d5a7239..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Assignee.ui.xml
+++ /dev/null
@@ -1,66 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder
-    xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'
-    xmlns:u='urn:import:com.google.gerrit.client.ui'>
-  <ui:with field='ico' type='com.google.gerrit.client.GerritResources'/>
-  <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
-  <ui:style gss='false'>
-    .suggestBox {
-      margin-bottom: 2px;
-    }
-
-    .error {
-      color: #D33D3D;
-      font-weight: bold;
-    }
-
-    .editAssignee,
-    .cancel {
-      cursor: pointer;
-      float: right;
-    }
-  </ui:style>
-  <g:HTMLPanel>
-    <div ui:field='show'>
-      <u:InlineHyperlink ui:field='assigneeLink'
-          title='Search for changes assigned to this user'/>
-      <g:Image ui:field='editAssigneeIcon'
-          resource='{ico.editUser}'
-          styleName='{style.editAssignee}'
-          title='Assign User to Change'/>
-    </div>
-    <div ui:field='form' style='display: none' aria-hidden='true'>
-      <u:RemoteSuggestBox ui:field='suggestBox' styleName='{style.suggestBox}'/>
-      <div ui:field='error'
-           class='{style.error}'
-           style='display: none' aria-hidden='true'/>
-      <div>
-        <g:Button ui:field='assign' styleName='{res.style.button}'>
-          <div>Assign</div>
-        </g:Button>
-        <g:Button ui:field='cancel'
-            styleName='{res.style.button}'
-            addStyleNames='{style.cancel}'>
-          <div>Cancel</div>
-        </g:Button>
-      </div>
-    </div>
-   </g:HTMLPanel>
-  </ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AssigneeSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AssigneeSuggestOracle.java
deleted file mode 100644
index c8bbfc3..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AssigneeSuggestOracle.java
+++ /dev/null
@@ -1,69 +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.client.change;
-
-import com.google.gerrit.client.account.AccountApi;
-import com.google.gerrit.client.info.AccountInfo;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.ui.AccountSuggestOracle.AccountSuggestion;
-import com.google.gerrit.client.ui.SuggestAfterTypingNCharsOracle;
-import com.google.gwt.core.client.JsArray;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/** REST API based suggestion Oracle for assignee */
-public class AssigneeSuggestOracle extends SuggestAfterTypingNCharsOracle {
-
-  private ChangeInfo change;
-
-  public void setChange(ChangeInfo change) {
-    this.change = change;
-  }
-
-  @Override
-  protected void _onRequestSuggestions(Request req, Callback cb) {
-    AccountApi.suggest(
-        getQuery(req),
-        req.getLimit(),
-        new GerritCallback<JsArray<AccountInfo>>() {
-          @Override
-          public void onSuccess(JsArray<AccountInfo> result) {
-            List<AccountSuggestion> r = new ArrayList<>(result.length());
-            for (AccountInfo reviewer : Natives.asList(result)) {
-              r.add(new AccountSuggestion(reviewer, req.getQuery()));
-            }
-            cb.onSuggestionsReady(req, new Response(r));
-          }
-
-          @Override
-          public void onFailure(Throwable err) {
-            List<Suggestion> r = Collections.emptyList();
-            cb.onSuggestionsReady(req, new Response(r));
-          }
-        });
-  }
-
-  private String getQuery(Request req) {
-    StringBuilder query = new StringBuilder();
-    query.append(req.getQuery());
-    if (change != null) {
-      query.append(" cansee:").append(change._number());
-    }
-    return query.toString();
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeActions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeActions.java
deleted file mode 100644
index 0bc74e4..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeActions.java
+++ /dev/null
@@ -1,91 +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.client.change;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.Button;
-
-public class ChangeActions {
-
-  static void delete(Project.NameKey project, Change.Id id, Button... draftButtons) {
-    ChangeApi.deleteChange(project.get(), id.get(), mine(draftButtons));
-  }
-
-  static void markPrivate(Project.NameKey project, Change.Id id, Button... draftButtons) {
-    ChangeApi.markPrivate(project.get(), id.get(), cs(project, id, draftButtons));
-  }
-
-  static void unmarkPrivate(Project.NameKey project, Change.Id id, Button... draftButtons) {
-    ChangeApi.unmarkPrivate(project.get(), id.get(), cs(project, id, draftButtons));
-  }
-
-  public static GerritCallback<JavaScriptObject> cs(
-      Project.NameKey project, final Change.Id id, Button... draftButtons) {
-    setEnabled(false, draftButtons);
-    return new GerritCallback<JavaScriptObject>() {
-      @Override
-      public void onSuccess(JavaScriptObject result) {
-        Gerrit.display(PageLinks.toChange(project, id));
-      }
-
-      @Override
-      public void onFailure(Throwable err) {
-        setEnabled(true, draftButtons);
-        if (SubmitFailureDialog.isConflict(err)) {
-          new SubmitFailureDialog(err.getMessage()).center();
-          Gerrit.display(PageLinks.toChange(project, id));
-        } else {
-          super.onFailure(err);
-        }
-      }
-    };
-  }
-
-  private static AsyncCallback<JavaScriptObject> mine(Button... draftButtons) {
-    setEnabled(false, draftButtons);
-    return new GerritCallback<JavaScriptObject>() {
-      @Override
-      public void onSuccess(JavaScriptObject result) {
-        Gerrit.display(PageLinks.MINE);
-      }
-
-      @Override
-      public void onFailure(Throwable err) {
-        setEnabled(true, draftButtons);
-        if (SubmitFailureDialog.isConflict(err)) {
-          new SubmitFailureDialog(err.getMessage()).center();
-          Gerrit.display(PageLinks.MINE);
-        } else {
-          super.onFailure(err);
-        }
-      }
-    };
-  }
-
-  private static void setEnabled(boolean enabled, Button... draftButtons) {
-    if (draftButtons != null) {
-      for (Button b : draftButtons) {
-        b.setEnabled(enabled);
-      }
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.java
deleted file mode 100644
index ed67846..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.java
+++ /dev/null
@@ -1,79 +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.client.change;
-
-import com.google.gwt.i18n.client.Constants;
-
-public interface ChangeConstants extends Constants {
-  String previousChange();
-
-  String nextChange();
-
-  String openChange();
-
-  String reviewedFileTitle();
-
-  String editFileInline();
-
-  String removeFileInline();
-
-  String restoreFileInline();
-
-  String openLastFile();
-
-  String openCommitMessage();
-
-  String patchSet();
-
-  String commit();
-
-  String date();
-
-  String author();
-
-  String notAvailable();
-
-  String relatedChanges();
-
-  String relatedChangesTooltip();
-
-  String conflictingChanges();
-
-  String conflictingChangesTooltip();
-
-  String cherryPicks();
-
-  String cherryPicksTooltip();
-
-  String sameTopic();
-
-  String sameTopicTooltip();
-
-  String submittedTogether();
-
-  String submittedTogetherTooltip();
-
-  String noChanges();
-
-  String indirectAncestor();
-
-  String merged();
-
-  String abandoned();
-
-  String deleteChangeEdit();
-
-  String deleteChange();
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.properties
deleted file mode 100644
index 9000149..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.properties
+++ /dev/null
@@ -1,36 +0,0 @@
-previousChange = Previous related change
-nextChange = Next related change
-openChange = Open related change
-reviewedFileTitle = Mark file as reviewed (Shortcut: r)
-editFileInline = Edit file inline
-removeFileInline = Remove file inline
-restoreFileInline = Restore file inline
-
-openLastFile = Open last file
-openCommitMessage = Open commit message
-
-patchSet = Patch Set
-commit = Commit
-date = Date
-author = Author / Committer
-
-notAvailable = N/A
-relatedChanges = Related Changes
-relatedChangesTooltip = Same branch changes connected by Git history
-conflictingChanges = Conflicts With
-conflictingChangesTooltip = Open changes that conflict with this change
-cherryPicks = Cherry-Picks
-cherryPicksTooltip = Changes with the same Change-Id
-sameTopic = Same Topic
-sameTopicTooltip = Changes with the same topic
-submittedTogether = Submitted Together
-submittedTogetherTooltip = Changes submitted together with this change
-noChanges = No Changes
-indirectAncestor = Indirect ancestor
-merged = Merged
-abandoned = Abandoned
-
-deleteChangeEdit = Delete Change Edit?\n\
-  \n\
-  All changes made in the edit revision will be lost.
-deleteChange = Delete Change?
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.java
deleted file mode 100644
index 4eead56..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.java
+++ /dev/null
@@ -1,47 +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.client.change;
-
-import com.google.gwt.i18n.client.Messages;
-
-public interface ChangeMessages extends Messages {
-  String patchSets(String currentlyViewedPatchSet, int currentPatchSet);
-
-  String changeWithNoRevisions(int changeId);
-
-  String relatedChanges(int count);
-
-  String relatedChanges(String count);
-
-  String conflictingChanges(int count);
-
-  String conflictingChanges(String count);
-
-  String cherryPicks(int count);
-
-  String cherryPicks(String count);
-
-  String sameTopic(int count);
-
-  String sameTopic(String count);
-
-  String submittedTogether(int count);
-
-  String submittedTogether(String count);
-
-  String editPatchSet(int patchSet);
-
-  String failedToLoadFileList(String error);
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.properties
deleted file mode 100644
index 743945d..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.properties
+++ /dev/null
@@ -1,9 +0,0 @@
-patchSets = Patch Sets ({0}/{1})
-changeWithNoRevisions = Cannot display change {0} because it has no revisions.
-relatedChanges = Related Changes ({0})
-conflictingChanges = Conflicts With ({0})
-cherryPicks = Cherry-Picks ({0})
-sameTopic = Same Topic ({0})
-submittedTogether = Submitted Together ({0})
-editPatchSet = edit:{0}
-failedToLoadFileList = Failed to load file list: {0}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
deleted file mode 100644
index ec7bc4a..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
+++ /dev/null
@@ -1,1653 +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.client.change;
-
-import com.google.gerrit.client.AvatarImage;
-import com.google.gerrit.client.DiffObject;
-import com.google.gerrit.client.ErrorDialog;
-import com.google.gerrit.client.FormatUtil;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.GerritUiExtensionPoint;
-import com.google.gerrit.client.NotFoundScreen;
-import com.google.gerrit.client.api.ChangeGlue;
-import com.google.gerrit.client.api.ExtensionPanel;
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeList;
-import com.google.gerrit.client.changes.CommentInfo;
-import com.google.gerrit.client.changes.QueryScreen;
-import com.google.gerrit.client.changes.RevisionInfoCache;
-import com.google.gerrit.client.changes.StarredChanges;
-import com.google.gerrit.client.changes.Util;
-import com.google.gerrit.client.diff.DiffApi;
-import com.google.gerrit.client.info.AccountInfo;
-import com.google.gerrit.client.info.AccountInfo.AvatarInfo;
-import com.google.gerrit.client.info.ActionInfo;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.info.ChangeInfo.CommitInfo;
-import com.google.gerrit.client.info.ChangeInfo.EditInfo;
-import com.google.gerrit.client.info.ChangeInfo.LabelInfo;
-import com.google.gerrit.client.info.ChangeInfo.MessageInfo;
-import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
-import com.google.gerrit.client.info.FileInfo;
-import com.google.gerrit.client.info.GpgKeyInfo;
-import com.google.gerrit.client.info.PushCertificateInfo;
-import com.google.gerrit.client.projects.ConfigInfoCache;
-import com.google.gerrit.client.projects.ConfigInfoCache.Entry;
-import com.google.gerrit.client.rpc.CallbackGroup;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.client.ui.BranchLink;
-import com.google.gerrit.client.ui.ChangeLink;
-import com.google.gerrit.client.ui.CommentLinkProcessor;
-import com.google.gerrit.client.ui.Hyperlink;
-import com.google.gerrit.client.ui.InlineHyperlink;
-import com.google.gerrit.client.ui.Screen;
-import com.google.gerrit.client.ui.UserActivityMonitor;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.extensions.client.ListChangesOption;
-import com.google.gerrit.extensions.client.SubmitType;
-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.gwt.core.client.GWT;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.core.client.JsArrayString;
-import com.google.gwt.dom.client.AnchorElement;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.NativeEvent;
-import com.google.gwt.dom.client.SelectElement;
-import com.google.gwt.dom.client.Style.Display;
-import com.google.gwt.event.dom.client.ChangeEvent;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.resources.client.CssResource;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.uibinder.client.UiHandler;
-import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.Event;
-import com.google.gwt.user.client.EventListener;
-import com.google.gwt.user.client.Window;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwt.user.client.ui.InlineLabel;
-import com.google.gwt.user.client.ui.ListBox;
-import com.google.gwt.user.client.ui.Panel;
-import com.google.gwt.user.client.ui.SimplePanel;
-import com.google.gwt.user.client.ui.ToggleButton;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-import com.google.gwtexpui.globalkey.client.KeyCommand;
-import com.google.gwtexpui.globalkey.client.KeyCommandSet;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-import com.google.gwtorm.client.KeyUtil;
-import java.sql.Timestamp;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.EnumSet;
-import java.util.List;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import net.codemirror.lib.CodeMirror;
-
-public class ChangeScreen extends Screen {
-  private static final Logger logger = Logger.getLogger(ChangeScreen.class.getName());
-
-  interface Binder extends UiBinder<HTMLPanel, ChangeScreen> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  interface Style extends CssResource {
-    String avatar();
-
-    String hashtagName();
-
-    String hashtagIcon();
-
-    String highlight();
-
-    String labelName();
-
-    String label_may();
-
-    String label_need();
-
-    String label_ok();
-
-    String label_reject();
-
-    String label_user();
-
-    String pushCertStatus();
-
-    String replyBox();
-
-    String selected();
-
-    String notCurrentPatchSet();
-  }
-
-  static ChangeScreen get(NativeEvent in) {
-    Element e = in.getEventTarget().cast();
-    for (e = DOM.getParent(e); e != null; e = DOM.getParent(e)) {
-      EventListener l = DOM.getEventListener(e);
-      if (l instanceof ChangeScreen) {
-        return (ChangeScreen) l;
-      }
-    }
-    return null;
-  }
-
-  private final Change.Id changeId;
-  @Nullable private Project.NameKey project;
-  private DiffObject base;
-  private String revision;
-  private ChangeInfo changeInfo;
-  private boolean hasDraftComments;
-  private CommentLinkProcessor commentLinkProcessor;
-  private EditInfo edit;
-  private LocalComments lc;
-
-  private List<HandlerRegistration> handlers = new ArrayList<>(4);
-  private UpdateCheckTimer updateCheck;
-  private Timestamp lastDisplayedUpdate;
-  private UpdateAvailableBar updateAvailable;
-  private boolean openReplyBox;
-  private boolean loaded;
-  private FileTable.Mode fileTableMode;
-
-  @UiField HTMLPanel headerLine;
-  @UiField SimplePanel headerExtension;
-  @UiField SimplePanel headerExtensionMiddle;
-  @UiField SimplePanel headerExtensionRight;
-  @UiField Style style;
-  @UiField ToggleButton star;
-  @UiField Anchor permalink;
-
-  @UiField Assignee assignee;
-  @UiField Element assigneeRow;
-  @UiField Element ccText;
-  @UiField Reviewers reviewers;
-  @UiField Hashtags hashtags;
-  @UiField Element hashtagTableRow;
-
-  @UiField FlowPanel ownerPanel;
-  @UiField InlineHyperlink ownerLink;
-
-  @UiField Element uploaderRow;
-  @UiField FlowPanel uploaderPanel;
-  @UiField InlineLabel uploaderName;
-
-  @UiField Element statusText;
-  @UiField Element privateText;
-  @UiField Element wipText;
-  @UiField Image projectSettings;
-  @UiField AnchorElement projectSettingsLink;
-  @UiField InlineHyperlink projectDashboard;
-  @UiField InlineHyperlink branchLink;
-  @UiField Element strategy;
-  @UiField Element submitActionText;
-  @UiField Element notMergeable;
-  @UiField Topic topic;
-  @UiField Element actionText;
-  @UiField Element actionDate;
-  @UiField SimplePanel changeExtension;
-  @UiField SimplePanel relatedExtension;
-  @UiField SimplePanel commitExtension;
-
-  @UiField Actions actions;
-  @UiField Labels labels;
-  @UiField CommitBox commit;
-  @UiField RelatedChanges related;
-  @UiField FileTable files;
-  @UiField ListBox diffBase;
-  @UiField History history;
-  @UiField SimplePanel historyExtensionRight;
-
-  @UiField Button includedIn;
-  @UiField Button patchSets;
-  @UiField Element patchSetsText;
-  @UiField Button download;
-  @UiField Button reply;
-  @UiField Button publishEdit;
-  @UiField Button rebaseEdit;
-  @UiField Button deleteEdit;
-  @UiField Button openAll;
-  @UiField Button editMode;
-  @UiField Button reviewMode;
-  @UiField Button addFile;
-  @UiField Button deleteFile;
-  @UiField Button renameFile;
-  @UiField Button expandAll;
-  @UiField Button collapseAll;
-  @UiField Button hideTaggedComments;
-  @UiField Button showTaggedComments;
-  @UiField QuickApprove quickApprove;
-
-  private ReplyAction replyAction;
-  private IncludedInAction includedInAction;
-  private PatchSetsAction patchSetsAction;
-  private DownloadAction downloadAction;
-  private AddFileAction addFileAction;
-  private DeleteFileAction deleteFileAction;
-  private RenameFileAction renameFileAction;
-
-  public ChangeScreen(
-      @Nullable Project.NameKey project,
-      Change.Id changeId,
-      DiffObject base,
-      String revision,
-      boolean openReplyBox,
-      FileTable.Mode mode) {
-    this.project = project;
-    this.changeId = changeId;
-    this.base = base;
-    this.revision = normalize(revision);
-    this.openReplyBox = openReplyBox;
-    this.fileTableMode = mode;
-    this.lc = new LocalComments(project, changeId);
-    add(uiBinder.createAndBindUi(this));
-  }
-
-  public Project.NameKey getProject() {
-    return project;
-  }
-
-  PatchSet.Id getPatchSetId() {
-    return new PatchSet.Id(changeInfo.legacyId(), changeInfo.revisions().get(revision)._number());
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    loadChangeScreen();
-  }
-
-  private void loadChangeScreen() {
-    if (project == null) {
-      // Load the project if it is not already present. This is the case when the user used a URL
-      // that doesn't include the project. Setting it here will rewrite the URL token to include the
-      // project (visible to the user) and all future API calls made from the change screen will use
-      // project/+/changeId to identify the change.
-      String query = "change:" + changeId.get();
-      ChangeList.query(
-          query,
-          Collections.emptySet(),
-          new AsyncCallback<ChangeList>() {
-            @Override
-            public void onSuccess(ChangeList result) {
-              if (result.length() == 0) {
-                Gerrit.display(getToken(), new NotFoundScreen());
-              } else if (result.length() > 1) {
-                Gerrit.display(PageLinks.toChangeQuery(query), QueryScreen.forQuery(query));
-              } else {
-                // Initialize current screen with newly obtained project
-                project = result.get(0).projectNameKey();
-                loadChangeScreen();
-              }
-            }
-
-            @Override
-            public void onFailure(Throwable caught) {
-              GerritCallback.showFailure(caught);
-            }
-          });
-
-      return;
-    }
-    CallbackGroup group = new CallbackGroup();
-    if (Gerrit.isSignedIn()) {
-      ChangeList.query(
-          "change:" + changeId.get() + " has:draft",
-          Collections.emptySet(),
-          group.add(
-              new AsyncCallback<ChangeList>() {
-                @Override
-                public void onSuccess(ChangeList result) {
-                  hasDraftComments = result.length() > 0;
-                }
-
-                @Override
-                public void onFailure(Throwable caught) {}
-              }));
-      ChangeApi.editWithFiles(
-          Project.NameKey.asStringOrNull(project),
-          changeId.get(),
-          group.add(
-              new AsyncCallback<EditInfo>() {
-                @Override
-                public void onSuccess(EditInfo result) {
-                  edit = result;
-                }
-
-                @Override
-                public void onFailure(Throwable caught) {}
-              }));
-    }
-    loadChangeInfo(
-        true,
-        group.addFinal(
-            new GerritCallback<ChangeInfo>() {
-              @Override
-              public void onSuccess(ChangeInfo info) {
-                info.init();
-                initCurrentRevision(info);
-                final RevisionInfo rev = info.revision(revision);
-                CallbackGroup group = new CallbackGroup();
-                loadCommit(rev, group);
-
-                group.addListener(
-                    new GerritCallback<Void>() {
-                      @Override
-                      public void onSuccess(Void result) {
-                        if (base.isBase() && rev.isMerge()) {
-                          base =
-                              DiffObject.parse(
-                                  info.legacyId(),
-                                  Gerrit.getUserPreferences().defaultBaseForMerges().getBase());
-                        }
-                        loadConfigInfo(info, base);
-                        JsArray<MessageInfo> mAr = info.messages();
-                        for (int i = 0; i < mAr.length(); i++) {
-                          if (mAr.get(i).tag() != null) {
-                            hideTaggedComments.setVisible(true);
-                            break;
-                          }
-                        }
-                      }
-                    });
-                group.done();
-              }
-            }));
-  }
-
-  private RevisionInfo initCurrentRevision(ChangeInfo info) {
-    info.revisions().copyKeysIntoChildren("name");
-    if (edit != null) {
-      edit.setName(edit.commit().commit());
-      info.setEdit(edit);
-      if (edit.hasFiles()) {
-        edit.files().copyKeysIntoChildren("path");
-      }
-      info.revisions().put(edit.name(), RevisionInfo.fromEdit(edit));
-      JsArray<RevisionInfo> list = info.revisions().values();
-
-      // Edit is converted to a regular revision (with number = 0) and
-      // added to the list of revisions. Additionally under certain
-      // circumstances change edit is assigned to be the current revision
-      // and is selected to be shown on the change screen.
-      // We have two different strategies to assign edit to the current ps:
-      // 1. revision == null: no revision is selected, so use the edit only
-      //    if it is based on the latest patch set
-      // 2. edit was selected explicitly from ps drop down:
-      //    use the edit regardless of which patch set it is based on
-      if (revision == null) {
-        RevisionInfo.sortRevisionInfoByNumber(list);
-        RevisionInfo rev = list.get(list.length() - 1);
-        if (rev.isEdit()) {
-          info.setCurrentRevision(rev.name());
-        }
-      } else if (revision.equals("edit") || revision.equals("0")) {
-        for (int i = 0; i < list.length(); i++) {
-          RevisionInfo r = list.get(i);
-          if (r.isEdit()) {
-            info.setCurrentRevision(r.name());
-            break;
-          }
-        }
-      }
-    }
-    return resolveRevisionToDisplay(info);
-  }
-
-  private void addExtensionPoints(ChangeInfo change, RevisionInfo rev, Entry result) {
-    addExtensionPoint(GerritUiExtensionPoint.CHANGE_SCREEN_HEADER, headerExtension, change, rev);
-    addExtensionPoint(
-        GerritUiExtensionPoint.CHANGE_SCREEN_HEADER_RIGHT_OF_BUTTONS,
-        headerExtensionMiddle,
-        change,
-        rev);
-    addExtensionPoint(
-        GerritUiExtensionPoint.CHANGE_SCREEN_HEADER_RIGHT_OF_POP_DOWNS,
-        headerExtensionRight,
-        change,
-        rev);
-    addExtensionPoint(
-        GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK,
-        changeExtension,
-        change,
-        rev,
-        result.getExtensionPanelNames(
-            GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK.toString()));
-    addExtensionPoint(
-        GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_RELATED_INFO_BLOCK,
-        relatedExtension,
-        change,
-        rev);
-    addExtensionPoint(
-        GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_COMMIT_INFO_BLOCK, commitExtension, change, rev);
-    addExtensionPoint(
-        GerritUiExtensionPoint.CHANGE_SCREEN_HISTORY_RIGHT_OF_BUTTONS,
-        historyExtensionRight,
-        change,
-        rev);
-  }
-
-  private void addExtensionPoint(
-      GerritUiExtensionPoint extensionPoint,
-      Panel p,
-      ChangeInfo change,
-      RevisionInfo rev,
-      List<String> panelNames) {
-    ExtensionPanel extensionPanel = new ExtensionPanel(extensionPoint, panelNames);
-    extensionPanel.putObject(GerritUiExtensionPoint.Key.CHANGE_INFO, change);
-    extensionPanel.putObject(GerritUiExtensionPoint.Key.REVISION_INFO, rev);
-    p.add(extensionPanel);
-  }
-
-  private void addExtensionPoint(
-      GerritUiExtensionPoint extensionPoint, Panel p, ChangeInfo change, RevisionInfo rev) {
-    addExtensionPoint(extensionPoint, p, change, rev, Collections.emptyList());
-  }
-
-  private boolean enableSignedPush() {
-    return Gerrit.info().receive().enableSignedPush();
-  }
-
-  void loadChangeInfo(boolean fg, AsyncCallback<ChangeInfo> cb) {
-    RestApi call = ChangeApi.detail(Project.NameKey.asStringOrNull(project), changeId.get());
-    EnumSet<ListChangesOption> opts =
-        EnumSet.of(ListChangesOption.ALL_REVISIONS, ListChangesOption.CHANGE_ACTIONS);
-    if (enableSignedPush()) {
-      opts.add(ListChangesOption.PUSH_CERTIFICATES);
-    }
-    ChangeList.addOptions(call, opts);
-    if (!fg) {
-      call.background();
-    }
-    call.get(cb);
-  }
-
-  void loadRevisionInfo() {
-    RestApi call = ChangeApi.actions(getProject().get(), changeId.get(), revision);
-    call.background();
-    call.get(
-        new GerritCallback<NativeMap<ActionInfo>>() {
-          @Override
-          public void onSuccess(NativeMap<ActionInfo> actionMap) {
-            actionMap.copyKeysIntoChildren("id");
-            renderRevisionInfo(changeInfo, actionMap);
-          }
-        });
-  }
-
-  @Override
-  protected void onUnload() {
-    if (replyAction != null) {
-      replyAction.hide();
-    }
-    if (updateCheck != null) {
-      updateCheck.cancel();
-      updateCheck = null;
-    }
-    for (HandlerRegistration h : handlers) {
-      h.removeHandler();
-    }
-    handlers.clear();
-    super.onUnload();
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    setHeaderVisible(false);
-    Resources.I.style().ensureInjected();
-    star.setVisible(Gerrit.isSignedIn());
-    labels.init(style);
-    reviewers.init(style, ccText);
-    hashtags.init(style);
-  }
-
-  private void initReplyButton(ChangeInfo info, String revision) {
-    if (!info.revision(revision).isEdit()) {
-      reply.setTitle(Gerrit.info().change().replyLabel());
-      reply.setHTML(
-          new SafeHtmlBuilder().openDiv().append(Gerrit.info().change().replyLabel()).closeDiv());
-      if (hasDraftComments || lc.hasReplyComment()) {
-        reply.setStyleName(style.highlight());
-      }
-      reply.setVisible(true);
-    }
-  }
-
-  private void gotoSibling(int offset) {
-    if (offset > 0
-        && changeInfo.currentRevision() != null
-        && changeInfo.currentRevision().equals(revision)) {
-      return;
-    }
-
-    if (offset < 0 && changeInfo.revision(revision)._number() == 1) {
-      return;
-    }
-
-    JsArray<RevisionInfo> revisions = changeInfo.revisions().values();
-    RevisionInfo.sortRevisionInfoByNumber(revisions);
-    for (int i = 0; i < revisions.length(); i++) {
-      if (revision.equals(revisions.get(i).name())) {
-        if (0 <= i + offset && i + offset < revisions.length()) {
-          Gerrit.display(
-              PageLinks.toChange(
-                  project,
-                  new PatchSet.Id(changeInfo.legacyId(), revisions.get(i + offset)._number())));
-          return;
-        }
-        return;
-      }
-    }
-  }
-
-  private void initIncludedInAction(ChangeInfo info) {
-    if (info.status() == Status.MERGED) {
-      includedInAction =
-          new IncludedInAction(
-              info.projectNameKey(), info.legacyId(), style, headerLine, includedIn);
-      includedIn.setVisible(true);
-    }
-  }
-
-  private void updatePatchSetsTextStyle(boolean isPatchSetCurrent) {
-    if (isPatchSetCurrent) {
-      patchSetsText.removeClassName(style.notCurrentPatchSet());
-    } else {
-      patchSetsText.addClassName(style.notCurrentPatchSet());
-    }
-  }
-
-  private void initRevisionsAction(ChangeInfo info, String revision) {
-    int currentPatchSet;
-    if (info.currentRevision() != null && info.revisions().containsKey(info.currentRevision())) {
-      currentPatchSet = info.revision(info.currentRevision())._number();
-    } else {
-      JsArray<RevisionInfo> revList = info.revisions().values();
-      RevisionInfo.sortRevisionInfoByNumber(revList);
-      currentPatchSet = revList.get(revList.length() - 1)._number();
-    }
-
-    String currentlyViewedPatchSet;
-    boolean isPatchSetCurrent = true;
-    String revisionId = info.revision(revision).id();
-    if (revisionId.equals("edit")) {
-      currentlyViewedPatchSet =
-          Resources.M.editPatchSet(RevisionInfo.findEditParent(info.revisions().values()));
-      currentPatchSet = info.revisions().values().length() - 1;
-    } else {
-      currentlyViewedPatchSet = revisionId;
-      if (!currentlyViewedPatchSet.equals(Integer.toString(currentPatchSet))) {
-        isPatchSetCurrent = false;
-      }
-    }
-    patchSetsText.setInnerText(Resources.M.patchSets(currentlyViewedPatchSet, currentPatchSet));
-    updatePatchSetsTextStyle(isPatchSetCurrent);
-    patchSetsAction =
-        new PatchSetsAction(
-            info.projectNameKey(), info.legacyId(), revision, edit, style, headerLine, patchSets);
-  }
-
-  private void initDownloadAction(ChangeInfo info, String revision) {
-    downloadAction = new DownloadAction(info, revision, style, headerLine, download);
-  }
-
-  private void initProjectLinks(ChangeInfo info) {
-    projectSettingsLink.setHref("#" + PageLinks.toProject(info.projectNameKey()));
-    projectSettings.addDomHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            if (Hyperlink.impl.handleAsClick((Event) event.getNativeEvent())) {
-              event.stopPropagation();
-              event.preventDefault();
-              Gerrit.display(PageLinks.toProject(info.projectNameKey()));
-            }
-          }
-        },
-        ClickEvent.getType());
-    projectDashboard.setText(info.project());
-    projectDashboard.setTargetHistoryToken(
-        PageLinks.toProjectDefaultDashboard(info.projectNameKey()));
-  }
-
-  private void initBranchLink(ChangeInfo info) {
-    branchLink.setText(info.branch());
-    branchLink.setTargetHistoryToken(
-        PageLinks.toChangeQuery(
-            BranchLink.query(info.projectNameKey(), info.status(), info.branch(), null)));
-  }
-
-  private void initEditMode(ChangeInfo info, String revision) {
-    if (Gerrit.isSignedIn()) {
-      RevisionInfo rev = info.revision(revision);
-      if (info.status().isOpen()) {
-        if (isEditModeEnabled(info, rev)) {
-          editMode.setVisible(fileTableMode == FileTable.Mode.REVIEW);
-          addFile.setVisible(!editMode.isVisible());
-          deleteFile.setVisible(!editMode.isVisible());
-          renameFile.setVisible(!editMode.isVisible());
-          reviewMode.setVisible(!editMode.isVisible());
-          addFileAction =
-              new AddFileAction(
-                  info.projectNameKey(), changeId, info.revision(revision), style, addFile, files);
-          deleteFileAction =
-              new DeleteFileAction(
-                  info.projectNameKey(), changeId, info.revision(revision), style, addFile);
-          renameFileAction =
-              new RenameFileAction(
-                  info.projectNameKey(), changeId, info.revision(revision), style, addFile);
-        } else {
-          editMode.setVisible(false);
-          addFile.setVisible(false);
-          reviewMode.setVisible(false);
-        }
-
-        if (rev.isEdit()) {
-          if (info.hasEditBasedOnCurrentPatchSet()) {
-            publishEdit.setVisible(true);
-          } else {
-            rebaseEdit.setVisible(true);
-          }
-          deleteEdit.setVisible(true);
-        }
-      } else if (rev.isEdit()) {
-        deleteEdit.setStyleName(style.highlight());
-        deleteEdit.setVisible(true);
-      }
-    }
-  }
-
-  private boolean isEditModeEnabled(ChangeInfo info, RevisionInfo rev) {
-    if (rev.isEdit()) {
-      return true;
-    }
-    if (edit == null) {
-      return revision.equals(info.currentRevision());
-    }
-    return rev._number() == RevisionInfo.findEditParent(info.revisions().values());
-  }
-
-  @UiHandler("publishEdit")
-  void onPublishEdit(@SuppressWarnings("unused") ClickEvent e) {
-    EditActions.publishEdit(getProject(), changeId, publishEdit, rebaseEdit, deleteEdit);
-  }
-
-  @UiHandler("rebaseEdit")
-  void onRebaseEdit(@SuppressWarnings("unused") ClickEvent e) {
-    EditActions.rebaseEdit(getProject(), changeId, publishEdit, rebaseEdit, deleteEdit);
-  }
-
-  @UiHandler("deleteEdit")
-  void onDeleteEdit(@SuppressWarnings("unused") ClickEvent e) {
-    if (Window.confirm(Resources.C.deleteChangeEdit())) {
-      EditActions.deleteEdit(getProject(), changeId, publishEdit, rebaseEdit, deleteEdit);
-    }
-  }
-
-  @Override
-  public void registerKeys() {
-    super.registerKeys();
-
-    KeyCommandSet keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
-    keysNavigation.add(
-        new KeyCommand(0, 'u', Util.C.upToChangeList()) {
-          @Override
-          public void onKeyPress(KeyPressEvent event) {
-            Gerrit.displayLastChangeList();
-          }
-        });
-    keysNavigation.add(
-        new KeyCommand(0, 'R', Util.C.keyReloadChange()) {
-          @Override
-          public void onKeyPress(KeyPressEvent event) {
-            Gerrit.display(PageLinks.toChange(project, changeId));
-          }
-        });
-    keysNavigation.add(
-        new KeyCommand(0, 'n', Util.C.keyNextPatchSet()) {
-          @Override
-          public void onKeyPress(KeyPressEvent event) {
-            gotoSibling(1);
-          }
-        },
-        new KeyCommand(0, 'p', Util.C.keyPreviousPatchSet()) {
-          @Override
-          public void onKeyPress(KeyPressEvent event) {
-            gotoSibling(-1);
-          }
-        });
-    handlers.add(GlobalKey.add(this, keysNavigation));
-
-    KeyCommandSet keysAction = new KeyCommandSet(Gerrit.C.sectionActions());
-    keysAction.add(
-        new KeyCommand(0, 'a', Util.C.keyPublishComments()) {
-          @Override
-          public void onKeyPress(KeyPressEvent event) {
-            if (Gerrit.isSignedIn()) {
-              onReply(null);
-            } else {
-              Gerrit.doSignIn(getToken());
-            }
-          }
-        });
-    keysAction.add(
-        new KeyCommand(0, 'x', Util.C.keyExpandAllMessages()) {
-          @Override
-          public void onKeyPress(KeyPressEvent event) {
-            onExpandAll(null);
-          }
-        });
-    keysAction.add(
-        new KeyCommand(0, 'z', Util.C.keyCollapseAllMessages()) {
-          @Override
-          public void onKeyPress(KeyPressEvent event) {
-            onCollapseAll(null);
-          }
-        });
-    keysAction.add(
-        new KeyCommand(0, 's', Util.C.changeTableStar()) {
-          @Override
-          public void onKeyPress(KeyPressEvent event) {
-            if (Gerrit.isSignedIn()) {
-              star.setValue(!star.getValue(), true);
-            } else {
-              Gerrit.doSignIn(getToken());
-            }
-          }
-        });
-    keysAction.add(
-        new KeyCommand(0, 'c', Util.C.keyAddReviewers()) {
-          @Override
-          public void onKeyPress(KeyPressEvent event) {
-            if (Gerrit.isSignedIn()) {
-              reviewers.onOpenForm();
-            } else {
-              Gerrit.doSignIn(getToken());
-            }
-          }
-        });
-    keysAction.add(
-        new KeyCommand(0, 't', Util.C.keyEditTopic()) {
-          @Override
-          public void onKeyPress(KeyPressEvent event) {
-            if (Gerrit.isSignedIn()) {
-              // In Firefox this event is mistakenly called when F5 is pressed so
-              // differentiate F5 from 't' by checking the charCode(F5=0, t=116).
-              if (event.getNativeEvent().getCharCode() == 0) {
-                Window.Location.reload();
-                return;
-              }
-              if (topic.canEdit()) {
-                topic.onEdit();
-              }
-            } else {
-              Gerrit.doSignIn(getToken());
-            }
-          }
-        });
-    handlers.add(GlobalKey.add(this, keysAction));
-    files.registerKeys();
-  }
-
-  @Override
-  public void onShowView() {
-    super.onShowView();
-    commit.onShowView();
-    related.setMaxHeight(commit.getElement().getParentElement().getOffsetHeight());
-
-    if (openReplyBox) {
-      onReply();
-    } else {
-      String prior = Gerrit.getPriorView();
-      if (prior != null && prior.startsWith("/c/")) {
-        scrollToPath(prior.substring(3));
-      }
-    }
-
-    ChangeGlue.fireShowChange(changeInfo, changeInfo.revision(revision));
-    CodeMirror.preload();
-    startPoller();
-  }
-
-  private void scrollToPath(String token) {
-    ProjectChangeId cId;
-    try {
-      cId = ProjectChangeId.create(token);
-    } catch (IllegalArgumentException e) {
-      // Scrolling is best-effort.
-      return;
-    }
-    if (!changeId.equals(cId.getChangeId())) {
-      return; // Unrelated URL, do not scroll.
-    }
-
-    // Extract the start of a file path. The patch set is always contained in the URL and separated
-    // by from the changeId by a forward slash. Example: /c/project/+/123/1/folder/file.txt
-    int s = token.indexOf('/', cId.identifierLength() + 1);
-    if (s < 0) {
-      return; // URL does not name a file.
-    }
-
-    int c = token.lastIndexOf(',');
-    if (0 <= c) {
-      token = token.substring(s + 1, c);
-    } else {
-      token = token.substring(s + 1);
-    }
-
-    if (!token.isEmpty()) {
-      files.scrollToPath(KeyUtil.decode(token));
-    }
-  }
-
-  @UiHandler("star")
-  void onToggleStar(ValueChangeEvent<Boolean> e) {
-    StarredChanges.toggleStar(changeId, e.getValue());
-  }
-
-  @UiHandler("includedIn")
-  void onIncludedIn(@SuppressWarnings("unused") ClickEvent e) {
-    includedInAction.show();
-  }
-
-  @UiHandler("download")
-  void onDownload(@SuppressWarnings("unused") ClickEvent e) {
-    downloadAction.show();
-  }
-
-  @UiHandler("patchSets")
-  void onPatchSets(@SuppressWarnings("unused") ClickEvent e) {
-    patchSetsAction.show();
-  }
-
-  @UiHandler("reply")
-  void onReply(@SuppressWarnings("unused") ClickEvent e) {
-    onReply();
-  }
-
-  @UiHandler("permalink")
-  void onReload(ClickEvent e) {
-    e.preventDefault();
-    Gerrit.display(PageLinks.toChange(project, changeId));
-  }
-
-  private void onReply() {
-    if (Gerrit.isSignedIn()) {
-      replyAction.onReply(null);
-    } else {
-      Gerrit.doSignIn(getToken());
-    }
-  }
-
-  @UiHandler("openAll")
-  void onOpenAll(@SuppressWarnings("unused") ClickEvent e) {
-    files.openAll();
-  }
-
-  @UiHandler("editMode")
-  void onEditMode(@SuppressWarnings("unused") ClickEvent e) {
-    fileTableMode = FileTable.Mode.EDIT;
-    refreshFileTable();
-    editMode.setVisible(false);
-    addFile.setVisible(true);
-    deleteFile.setVisible(true);
-    renameFile.setVisible(true);
-    reviewMode.setVisible(true);
-  }
-
-  @UiHandler("reviewMode")
-  void onReviewMode(@SuppressWarnings("unused") ClickEvent e) {
-    fileTableMode = FileTable.Mode.REVIEW;
-    refreshFileTable();
-    editMode.setVisible(true);
-    addFile.setVisible(false);
-    deleteFile.setVisible(false);
-    renameFile.setVisible(false);
-    reviewMode.setVisible(false);
-  }
-
-  @UiHandler("addFile")
-  void onAddFile(@SuppressWarnings("unused") ClickEvent e) {
-    addFileAction.onEdit();
-  }
-
-  @UiHandler("deleteFile")
-  void onDeleteFile(@SuppressWarnings("unused") ClickEvent e) {
-    deleteFileAction.onDelete();
-  }
-
-  @UiHandler("renameFile")
-  void onRenameFile(@SuppressWarnings("unused") ClickEvent e) {
-    renameFileAction.onRename();
-  }
-
-  private void refreshFileTable() {
-    int idx = diffBase.getSelectedIndex();
-    if (0 <= idx) {
-      String n = diffBase.getValue(idx);
-      loadConfigInfo(changeInfo, DiffObject.parse(changeInfo.legacyId(), n));
-    }
-  }
-
-  @UiHandler("showTaggedComments")
-  void onShowTaggedComments(@SuppressWarnings("unused") ClickEvent e) {
-    showTaggedComments.setVisible(false);
-    hideTaggedComments.setVisible(true);
-    int n = history.getWidgetCount();
-    for (int i = 0; i < n; i++) {
-      Message m = ((Message) history.getWidget(i));
-      m.setVisible(true);
-    }
-  }
-
-  @UiHandler("hideTaggedComments")
-  void onHideTaggedComments(@SuppressWarnings("unused") ClickEvent e) {
-    hideTaggedComments.setVisible(false);
-    showTaggedComments.setVisible(true);
-    int n = history.getWidgetCount();
-    for (int i = 0; i < n; i++) {
-      Message m = ((Message) history.getWidget(i));
-      if (m.getMessageInfo().tag() != null) {
-        m.setVisible(false);
-      }
-    }
-  }
-
-  @UiHandler("expandAll")
-  void onExpandAll(@SuppressWarnings("unused") ClickEvent e) {
-    int n = history.getWidgetCount();
-    for (int i = 0; i < n; i++) {
-      ((Message) history.getWidget(i)).setOpen(true);
-    }
-    expandAll.setVisible(false);
-    collapseAll.setVisible(true);
-  }
-
-  @UiHandler("collapseAll")
-  void onCollapseAll(@SuppressWarnings("unused") ClickEvent e) {
-    int n = history.getWidgetCount();
-    for (int i = 0; i < n; i++) {
-      ((Message) history.getWidget(i)).setOpen(false);
-    }
-    expandAll.setVisible(true);
-    collapseAll.setVisible(false);
-  }
-
-  @UiHandler("diffBase")
-  void onChangeRevision(@SuppressWarnings("unused") ChangeEvent e) {
-    int idx = diffBase.getSelectedIndex();
-    if (0 <= idx) {
-      String n = diffBase.getValue(idx);
-      loadConfigInfo(changeInfo, DiffObject.parse(changeInfo.legacyId(), n));
-    }
-  }
-
-  private void loadConfigInfo(ChangeInfo info, DiffObject base) {
-    final RevisionInfo rev = info.revision(revision);
-    if (base.isAutoMerge() && !initCurrentRevision(info).isMerge()) {
-      Gerrit.display(getToken(), new NotFoundScreen());
-    }
-
-    updateToken(info, base, rev);
-
-    RevisionInfo baseRev = resolveRevisionOrPatchSetId(info, base.asString(), null);
-
-    CallbackGroup group = new CallbackGroup();
-    Timestamp lastReply = myLastReply(info);
-    if (rev.isEdit()) {
-      // Comments are filtered for the current revision. Use parent
-      // patch set for edits, as edits themself can never have comments.
-      RevisionInfo p = RevisionInfo.findEditParentRevision(info.revisions().values());
-      List<NativeMap<JsArray<CommentInfo>>> comments = loadComments(p, group);
-      loadFileList(base, baseRev, rev, lastReply, group, comments, null);
-    } else {
-      loadDiff(base, baseRev, rev, lastReply, group);
-    }
-    group.addListener(
-        new AsyncCallback<Void>() {
-          @Override
-          public void onSuccess(Void result) {
-            loadConfigInfo(info, rev);
-          }
-
-          @Override
-          public void onFailure(Throwable caught) {
-            logger.log(
-                Level.SEVERE,
-                "Loading file list and inline comments failed: " + caught.getMessage());
-            loadConfigInfo(info, rev);
-          }
-        });
-    group.done();
-  }
-
-  private void loadConfigInfo(ChangeInfo info, RevisionInfo rev) {
-    if (loaded) {
-      return;
-    }
-
-    RevisionInfoCache.add(changeId, rev);
-    ConfigInfoCache.add(info);
-    ConfigInfoCache.get(
-        info.projectNameKey(),
-        new ScreenLoadCallback<ConfigInfoCache.Entry>(this) {
-          @Override
-          protected void preDisplay(Entry result) {
-            loaded = true;
-            commentLinkProcessor = result.getCommentLinkProcessor();
-            setTheme(result.getTheme());
-            renderChangeInfo(info);
-            loadRevisionInfo();
-          }
-        });
-    ConfigInfoCache.get(
-        info.projectNameKey(),
-        new GerritCallback<Entry>() {
-          @Override
-          public void onSuccess(Entry entry) {
-            addExtensionPoints(info, rev, entry);
-          }
-        });
-  }
-
-  private void updateToken(ChangeInfo info, DiffObject base, RevisionInfo rev) {
-    StringBuilder token =
-        new StringBuilder("/c/")
-            .append(PageLinks.toChangeId(info.projectNameKey(), info.legacyId()))
-            .append("/");
-    if (base.asString() != null) {
-      token.append(base.asString()).append("..");
-    }
-    if (base.asString() != null || !rev.name().equals(info.currentRevision())) {
-      token.append(rev._number());
-    }
-    setToken(token.toString());
-  }
-
-  static Timestamp myLastReply(ChangeInfo info) {
-    if (Gerrit.isSignedIn() && info.messages() != null) {
-      int self = Gerrit.getUserAccount()._accountId();
-      for (int i = info.messages().length() - 1; i >= 0; i--) {
-        MessageInfo m = info.messages().get(i);
-        if (m.author() != null && m.author()._accountId() == self) {
-          return m.date();
-        }
-      }
-    }
-    return null;
-  }
-
-  private void loadDiff(
-      DiffObject base,
-      RevisionInfo baseRev,
-      RevisionInfo rev,
-      Timestamp myLastReply,
-      CallbackGroup group) {
-    List<NativeMap<JsArray<CommentInfo>>> comments = loadComments(rev, group);
-    List<NativeMap<JsArray<CommentInfo>>> drafts = loadDrafts(rev, group);
-    loadFileList(base, baseRev, rev, myLastReply, group, comments, drafts);
-
-    if (Gerrit.isSignedIn() && fileTableMode == FileTable.Mode.REVIEW) {
-      ChangeApi.revision(getProject().get(), changeId.get(), rev.name())
-          .view("files")
-          .addParameterTrue("reviewed")
-          .get(
-              group.add(
-                  new AsyncCallback<JsArrayString>() {
-                    @Override
-                    public void onSuccess(JsArrayString result) {
-                      files.markReviewed(result);
-                    }
-
-                    @Override
-                    public void onFailure(Throwable caught) {}
-                  }));
-    }
-  }
-
-  private void loadFileList(
-      final DiffObject base,
-      final RevisionInfo baseRev,
-      final RevisionInfo rev,
-      final Timestamp myLastReply,
-      CallbackGroup group,
-      final List<NativeMap<JsArray<CommentInfo>>> comments,
-      final List<NativeMap<JsArray<CommentInfo>>> drafts) {
-    DiffApi.list(
-        getProject().get(),
-        changeId.get(),
-        rev.name(),
-        baseRev,
-        group.add(
-            new AsyncCallback<NativeMap<FileInfo>>() {
-              @Override
-              public void onSuccess(NativeMap<FileInfo> m) {
-                files.set(
-                    base,
-                    new PatchSet.Id(changeId, rev._number()),
-                    getProject(),
-                    style,
-                    reply,
-                    fileTableMode,
-                    edit != null);
-                files.setValue(
-                    m,
-                    myLastReply,
-                    comments != null ? comments.get(0) : null,
-                    drafts != null ? drafts.get(0) : null);
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {
-                files.showError(caught);
-              }
-            }));
-  }
-
-  private List<NativeMap<JsArray<CommentInfo>>> loadComments(
-      final RevisionInfo rev, CallbackGroup group) {
-    final List<NativeMap<JsArray<CommentInfo>>> r = new ArrayList<>(1);
-    // TODO(dborowitz): Could eliminate this call by adding an option to include
-    // inline comments in the change detail.
-    ChangeApi.comments(getProject().get(), changeId.get())
-        .get(
-            group.add(
-                new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
-                  @Override
-                  public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
-                    // Return value is used for populating the file table, so only count
-                    // comments for the current revision. Still include all comments in
-                    // the history table.
-                    r.add(filterForRevision(result, rev._number()));
-                    history.addComments(result);
-                  }
-
-                  @Override
-                  public void onFailure(Throwable caught) {}
-                }));
-    return r;
-  }
-
-  private static NativeMap<JsArray<CommentInfo>> filterForRevision(
-      NativeMap<JsArray<CommentInfo>> comments, int id) {
-    NativeMap<JsArray<CommentInfo>> filtered = NativeMap.create();
-    for (String k : comments.keySet()) {
-      JsArray<CommentInfo> allRevisions = comments.get(k);
-      JsArray<CommentInfo> thisRevision = JsArray.createArray().cast();
-      for (int i = 0; i < allRevisions.length(); i++) {
-        CommentInfo c = allRevisions.get(i);
-        if (c.patchSet() == id) {
-          thisRevision.push(c);
-        }
-      }
-      filtered.put(k, thisRevision);
-    }
-    return filtered;
-  }
-
-  private List<NativeMap<JsArray<CommentInfo>>> loadDrafts(RevisionInfo rev, CallbackGroup group) {
-    final List<NativeMap<JsArray<CommentInfo>>> r = new ArrayList<>(1);
-    if (Gerrit.isSignedIn()) {
-      ChangeApi.revision(getProject().get(), changeId.get(), rev.name())
-          .view("drafts")
-          .get(
-              group.add(
-                  new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
-                    @Override
-                    public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
-                      r.add(result);
-                    }
-
-                    @Override
-                    public void onFailure(Throwable caught) {}
-                  }));
-    } else {
-      r.add(NativeMap.<JsArray<CommentInfo>>create());
-    }
-    return r;
-  }
-
-  private void loadCommit(RevisionInfo rev, CallbackGroup group) {
-    if (rev.isEdit() || rev.commit() != null) {
-      return;
-    }
-
-    ChangeApi.commitWithLinks(
-        getProject().get(),
-        changeId.get(),
-        rev.name(),
-        group.add(
-            new AsyncCallback<CommitInfo>() {
-              @Override
-              public void onSuccess(CommitInfo info) {
-                rev.setCommit(info);
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {}
-            }));
-  }
-
-  private void renderSubmitType(Change.Status status, boolean canSubmit, SubmitType submitType) {
-    if (status == Change.Status.NEW && !changeInfo.isWorkInProgress()) {
-      if (canSubmit) {
-        statusText.setInnerText(
-            changeInfo.mergeable() ? Util.C.readyToSubmit() : Util.C.mergeConflict());
-      }
-      setVisible(notMergeable, !changeInfo.mergeable());
-    }
-    submitActionText.setInnerText(com.google.gerrit.client.admin.Util.toLongString(submitType));
-  }
-
-  private RevisionInfo resolveRevisionToDisplay(ChangeInfo info) {
-    RevisionInfo rev = resolveRevisionOrPatchSetId(info, revision, info.currentRevision());
-    if (rev != null) {
-      revision = rev.name();
-      return rev;
-    }
-
-    // the revision is not visible to the calling user (maybe it is a draft?)
-    // or the change is corrupt, take the last revision that was returned,
-    // if no revision was returned display an error
-    JsArray<RevisionInfo> revisions = info.revisions().values();
-    if (revisions.length() > 0) {
-      RevisionInfo.sortRevisionInfoByNumber(revisions);
-      rev = revisions.get(revisions.length() - 1);
-      revision = rev.name();
-      return rev;
-    }
-    new ErrorDialog(Resources.M.changeWithNoRevisions(info.legacyId().get())).center();
-    throw new IllegalStateException("no revision, cannot proceed");
-  }
-
-  /**
-   * Resolve a revision or patch set id string to RevisionInfo. When this view is created from the
-   * changes table, revision is passed as a real revision. When this view is created from side by
-   * side (by closing it with 'u') patch set id is passed.
-   *
-   * @param info change info
-   * @param revOrId revision or patch set id
-   * @param defaultValue value returned when revOrId is null
-   * @return resolved revision or default value
-   */
-  private RevisionInfo resolveRevisionOrPatchSetId(
-      ChangeInfo info, String revOrId, String defaultValue) {
-    int parentNum;
-    if (revOrId == null) {
-      revOrId = defaultValue;
-    } else if ((parentNum = toParentNum(revOrId)) > 0) {
-      CommitInfo commitInfo = info.revision(revision).commit();
-      JsArray<CommitInfo> parents = commitInfo.parents();
-      if (parents.length() >= parentNum) {
-        return RevisionInfo.forParent(-parentNum, parents.get(parentNum - 1));
-      }
-    } else if (!info.revisions().containsKey(revOrId)) {
-      JsArray<RevisionInfo> list = info.revisions().values();
-      for (int i = 0; i < list.length(); i++) {
-        RevisionInfo r = list.get(i);
-        if (revOrId.equals(String.valueOf(r._number()))) {
-          revOrId = r.name();
-          break;
-        }
-      }
-    }
-    return revOrId != null ? info.revision(revOrId) : null;
-  }
-
-  private boolean isSubmittable(ChangeInfo info) {
-    boolean canSubmit = info.status().isOpen() && revision.equals(info.currentRevision());
-    if (canSubmit && info.status() == Change.Status.NEW) {
-      for (String name : info.labels()) {
-        LabelInfo label = info.label(name);
-        switch (label.status()) {
-          case NEED:
-            statusText.setInnerText(Util.M.needs(name));
-            canSubmit = false;
-            break;
-          case REJECT:
-          case IMPOSSIBLE:
-            if (label.blocking()) {
-              statusText.setInnerText(Util.M.blockedOn(name));
-              canSubmit = false;
-            }
-            break;
-          case MAY:
-          case OK:
-          default:
-            break;
-        }
-      }
-    }
-    return canSubmit;
-  }
-
-  private void renderChangeInfo(ChangeInfo info) {
-    RevisionInfo revisionInfo = info.revision(revision);
-    changeInfo = info;
-    lastDisplayedUpdate = info.updated();
-
-    labels.set(info);
-
-    renderOwner(info);
-    renderUploader(info, revisionInfo);
-    renderActionTextDate(info);
-    renderDiffBaseListBox(info);
-    initReplyButton(info, revision);
-    initIncludedInAction(info);
-    initDownloadAction(info, revision);
-    initProjectLinks(info);
-    initBranchLink(info);
-    initEditMode(info, revision);
-    actions.display(info, revision);
-
-    star.setValue(info.starred());
-    permalink.setHref(ChangeLink.permalink(changeId));
-    permalink.setText(String.valueOf(info.legacyId()));
-    topic.set(info, revision);
-    commit.set(commentLinkProcessor, info, revision);
-    related.set(info, revision);
-    reviewers.set(info);
-    assignee.set(info);
-    if (Gerrit.isNoteDbEnabled()) {
-      hashtags.set(info, revision);
-    } else {
-      setVisible(hashtagTableRow, false);
-    }
-
-    StringBuilder sb = new StringBuilder();
-    sb.append(Util.M.changeScreenTitleId(info.idAbbreviated()));
-    if (info.subject() != null) {
-      sb.append(": ");
-      sb.append(info.subject());
-    }
-    setWindowTitle(sb.toString());
-
-    // Although this is related to the revision, we can process it early to
-    // render it faster.
-    if (!info.status().isOpen()
-        || !revision.equals(info.currentRevision())
-        || revisionInfo.isEdit()) {
-      setVisible(strategy, false);
-    }
-
-    // Properly render revision actions initially while waiting for
-    // the callback to populate them correctly.
-    NativeMap<ActionInfo> emptyMap = NativeMap.<ActionInfo>create();
-    initRevisionsAction(info, revision);
-    quickApprove.setVisible(false);
-    actions.reloadRevisionActions(emptyMap);
-
-    boolean current = revision.equals(info.currentRevision()) && !revisionInfo.isEdit();
-
-    if (revisionInfo.isEdit()) {
-      statusText.setInnerText(Util.C.changeEdit());
-    } else if (!current) {
-      statusText.setInnerText(Util.C.notCurrent());
-      labels.setVisible(false);
-    } else {
-      statusText.setInnerText(Util.toLongString(info.status()));
-    }
-
-    if (info.isPrivate()) {
-      privateText.setInnerText(Util.C.isPrivate());
-    }
-
-    if (info.isWorkInProgress()) {
-      wipText.setInnerText(Util.C.isWorkInProgress());
-    }
-
-    if (Gerrit.isSignedIn()) {
-      replyAction =
-          new ReplyAction(
-              info, revision, hasDraftComments, style, commentLinkProcessor, reply, quickApprove);
-    }
-    history.set(commentLinkProcessor, replyAction, changeId, info);
-
-    if (current && info.status().isOpen()) {
-      quickApprove.set(info, revision, replyAction);
-      renderSubmitType(info.status(), isSubmittable(info), info.submitType());
-    } else {
-      quickApprove.setVisible(false);
-    }
-  }
-
-  private void renderRevisionInfo(ChangeInfo info, NativeMap<ActionInfo> actionMap) {
-    initRevisionsAction(info, revision);
-    commit.setParentNotCurrent(
-        actionMap.containsKey("rebase") && actionMap.get("rebase").enabled());
-    actions.reloadRevisionActions(actionMap);
-  }
-
-  private void renderOwner(ChangeInfo info) {
-    // TODO info card hover
-    String name = name(info.owner());
-    if (info.owner().avatar(AvatarInfo.DEFAULT_SIZE) != null) {
-      ownerPanel.insert(new AvatarImage(info.owner()), 0);
-    }
-    ownerLink.setText(name);
-    ownerLink.setTitle(email(info.owner(), name));
-    ownerLink.setTargetHistoryToken(
-        PageLinks.toAccountQuery(
-            info.owner().name() != null
-                ? info.owner().name()
-                : info.owner().email() != null
-                    ? info.owner().email()
-                    : String.valueOf(info.owner()._accountId()),
-            Change.Status.NEW));
-  }
-
-  private void renderUploader(ChangeInfo changeInfo, RevisionInfo revInfo) {
-    AccountInfo uploader = revInfo.uploader();
-    boolean isOwner = uploader == null || uploader._accountId() == changeInfo.owner()._accountId();
-    renderPushCertificate(revInfo, isOwner ? ownerPanel : uploaderPanel);
-    if (isOwner) {
-      uploaderRow.getStyle().setDisplay(Display.NONE);
-      return;
-    }
-    uploaderRow.getStyle().setDisplay(Display.TABLE_ROW);
-
-    if (uploader.avatar(AvatarInfo.DEFAULT_SIZE) != null) {
-      uploaderPanel.insert(new AvatarImage(uploader), 0);
-    }
-    String name = name(uploader);
-    uploaderName.setText(name);
-    uploaderName.setTitle(email(uploader, name));
-  }
-
-  private void renderPushCertificate(RevisionInfo revInfo, FlowPanel panel) {
-    if (!enableSignedPush()) {
-      return;
-    }
-    Image status = new Image();
-    panel.add(status);
-    status.setStyleName(style.pushCertStatus());
-    if (!revInfo.hasPushCertificate() || revInfo.pushCertificate().key() == null) {
-      status.setResource(Gerrit.RESOURCES.question());
-      status.setTitle(Util.C.pushCertMissing());
-      return;
-    }
-    PushCertificateInfo certInfo = revInfo.pushCertificate();
-    GpgKeyInfo.Status s = certInfo.key().status();
-    switch (s) {
-      case BAD:
-        status.setResource(Gerrit.RESOURCES.redNot());
-        status.setTitle(problems(Util.C.pushCertBad(), certInfo));
-        break;
-      case OK:
-        status.setResource(Gerrit.RESOURCES.warning());
-        status.setTitle(problems(Util.C.pushCertOk(), certInfo));
-        break;
-      case TRUSTED:
-        status.setResource(Gerrit.RESOURCES.greenCheck());
-        status.setTitle(Util.C.pushCertTrusted());
-        break;
-    }
-  }
-
-  private static String name(AccountInfo info) {
-    return info.name() != null ? info.name() : Gerrit.info().user().anonymousCowardName();
-  }
-
-  private static String email(AccountInfo info, String name) {
-    return info.email() != null ? info.email() : name;
-  }
-
-  private static String problems(String msg, PushCertificateInfo info) {
-    if (info.key() == null || !info.key().hasProblems() || info.key().problems().length() == 0) {
-      return msg;
-    }
-
-    StringBuilder sb = new StringBuilder();
-    sb.append(msg).append(':');
-    for (String problem : Natives.asList(info.key().problems())) {
-      sb.append('\n').append(problem);
-    }
-    return sb.toString();
-  }
-
-  private void renderActionTextDate(ChangeInfo info) {
-    String action;
-    if (info.created().equals(info.updated())) {
-      action = Util.C.changeInfoBlockUploaded();
-    } else {
-      action = Util.C.changeInfoBlockUpdated();
-    }
-    actionText.setInnerText(action);
-    actionDate.setInnerText(FormatUtil.relativeFormat(info.updated()));
-  }
-
-  private void renderDiffBaseListBox(ChangeInfo info) {
-    JsArray<RevisionInfo> list = info.revisions().values();
-    RevisionInfo.sortRevisionInfoByNumber(list);
-    int selectedIdx = list.length();
-    for (int i = list.length() - 1; i >= 0; i--) {
-      RevisionInfo r = list.get(i);
-      diffBase.addItem(r.id() + ": " + r.name().substring(0, 6), r.id());
-      if (r.name().equals(revision)) {
-        SelectElement.as(diffBase.getElement())
-            .getOptions()
-            .getItem(diffBase.getItemCount() - 1)
-            .setDisabled(true);
-      }
-      if (base.isPatchSet() && base.asPatchSetId().get() == r._number()) {
-        selectedIdx = diffBase.getItemCount() - 1;
-      }
-    }
-
-    RevisionInfo rev = info.revisions().get(revision);
-    JsArray<CommitInfo> parents = rev.commit().parents();
-    if (parents.length() > 1) {
-      diffBase.addItem(Util.C.autoMerge(), DiffObject.AUTO_MERGE);
-      for (int i = 0; i < parents.length(); i++) {
-        int parentNum = i + 1;
-        diffBase.addItem(Util.M.diffBaseParent(parentNum), String.valueOf(-parentNum));
-      }
-
-      if (base.isParent()) {
-        selectedIdx = list.length() + base.getParentNum();
-      }
-    } else {
-      diffBase.addItem(Util.C.baseDiffItem(), "");
-    }
-
-    diffBase.setSelectedIndex(selectedIdx);
-  }
-
-  void showUpdates(ChangeInfo newInfo) {
-    if (!isAttached() || newInfo.updated().equals(lastDisplayedUpdate)) {
-      return;
-    }
-
-    JsArray<MessageInfo> om = changeInfo.messages();
-    JsArray<MessageInfo> nm = newInfo.messages();
-
-    if (om == null) {
-      om = JsArray.createArray().cast();
-    }
-    if (nm == null) {
-      nm = JsArray.createArray().cast();
-    }
-
-    if (om.length() == nm.length()) {
-      return;
-    }
-
-    if (updateAvailable == null) {
-      updateAvailable =
-          new UpdateAvailableBar() {
-            @Override
-            void onShow() {
-              Gerrit.display(PageLinks.toChange(project, changeId));
-            }
-
-            @Override
-            void onIgnore(Timestamp newTime) {
-              lastDisplayedUpdate = newTime;
-            }
-          };
-    }
-    updateAvailable.set(Natives.asList(nm).subList(om.length(), nm.length()), newInfo.updated());
-    if (!updateAvailable.isAttached()) {
-      add(updateAvailable);
-    }
-  }
-
-  private void startPoller() {
-    if (Gerrit.isSignedIn() && 0 < Gerrit.info().change().updateDelay()) {
-      updateCheck = new UpdateCheckTimer(this);
-      updateCheck.schedule();
-      handlers.add(UserActivityMonitor.addValueChangeHandler(updateCheck));
-    }
-  }
-
-  private static String normalize(String r) {
-    return r != null && !r.isEmpty() ? r : null;
-  }
-
-  /**
-   * @param parentToken
-   * @return 1-based parentNum if parentToken is a String which can be parsed as a negative integer
-   *     i.e. "-1", "-2", etc. If parentToken cannot be parsed as a negative integer, return zero.
-   */
-  private static int toParentNum(String parentToken) {
-    try {
-      int n = Integer.parseInt(parentToken);
-      if (n < 0) {
-        return -n;
-      }
-      return 0;
-    } catch (NumberFormatException e) {
-      return 0;
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml
deleted file mode 100644
index d629fc2..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml
+++ /dev/null
@@ -1,634 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder
-    xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:c='urn:import:com.google.gerrit.client.change'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'
-    xmlns:x='urn:import:com.google.gerrit.client.ui'>
-  <ui:with field='ico' type='com.google.gerrit.client.GerritResources'/>
-  <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
-  <ui:style gss='false' type='com.google.gerrit.client.change.ChangeScreen.Style'>
-    @eval textColor com.google.gerrit.client.Gerrit.getTheme().textColor;
-    @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
-
-    @def COMMIT_WIDTH 560px;
-    @def HEADER_HEIGHT 30px;
-
-    @def BUTTON_HEIGHT 14px;
-
-    .cs2 {
-      margin-bottom: 1em;
-    }
-
-    .headerLine {
-      position: relative;
-      background-color: trimColor;
-      height: HEADER_HEIGHT;
-      margin: 0 -5px;
-      padding: 0 5px;
-    }
-
-    .subjectLine {
-      position: relative;
-      width: COMMIT_WIDTH;
-      height: HEADER_HEIGHT;
-      background-color: trimColor;
-      color: textColor;
-      font-family: sans-serif;
-    }
-    .subjectText {
-      width: 460px;
-      height: HEADER_HEIGHT;
-      line-height: HEADER_HEIGHT;
-      overflow: hidden;
-      text-overflow: ellipsis;
-      white-space: nowrap;
-    }
-
-    .infoLine {
-      position: absolute;
-      top: 0;
-      left: COMMIT_WIDTH;
-      height: HEADER_HEIGHT;
-      padding-left: 25px;
-    }
-
-    .infoLineHeaderButtons {
-      display: inline-block;
-      height: HEADER_HEIGHT;
-    }
-    .statusRight {
-      position: absolute;
-      top: 0;
-      right: 0;
-      height: HEADER_HEIGHT;
-    }
-    .idAndStatus {
-      display: inline-block;
-      position: relative;
-      height: HEADER_HEIGHT;
-    }
-    .star {
-      position: absolute;
-      top: 5px;
-      right: 2px;
-      cursor: pointer;
-      outline: none;
-    }
-    .changeId {
-      width: 300px;
-      white-space: nowrap;
-      line-height: HEADER_HEIGHT;
-      overflow: hidden;
-      text-overflow: ellipsis;
-    }
-    .statusText {
-      font-weight: bold;
-    }
-    .privateText {
-      font-weight: bold;
-    }
-
-    .wipText {
-      font-weight: bold;
-    }
-
-    div.popdown {
-      display: inline-block;
-      margin-top: 2px;
-      margin-left: 5px;
-      margin-right: 25px;
-    }
-
-    .popdown button {
-      cursor: pointer;
-      height: 25px;
-      border: none;
-      background-color: trimColor;
-      margin: 0 0 0 -2px;
-      padding-left: 2px;
-      padding-right: 2px;
-      min-width: 100px;
-    }
-    .popdown button div {
-      padding-left: 6px;
-      padding-right: 6px;
-    }
-    .popdown button div:after {
-      content: " \25bc";
-    }
-    .popdown button.selected {
-      font-weight: bold;
-    }
-    .popdown button:focus {
-      outline: none;
-    }
-
-    .headerButtons button:disabled,
-    #change_infoTable button:disabled,
-    .popdown button:disabled {
-      background-color: #999;
-      background-image: -webkit-linear-gradient(top, #999, #999);
-    }
-
-    .infoTable {
-      border-spacing: 0;
-    }
-
-    .infoTable th {
-      width: 60px;
-      color: #444;
-      font-weight: normal;
-      vertical-align: top;
-      text-align: left;
-      padding: 0 5px 0 0;
-    }
-
-    .projectSettings {
-      float: right;
-      cursor: pointer;
-    }
-
-    .infoColumn {
-      width: 440px;
-      padding-left: 17px;
-      padding-right: 17px;
-      vertical-align: top;
-    }
-
-    #change_infoTable {
-      border-spacing: 0;
-      width: 100%;
-      margin-left: 2px;
-      margin-right: 5px;
-    }
-
-    .notMergeable {
-      float: right;
-      font-weight: bold;
-      color: #d00;
-    }
-
-    .commitColumn, .relatedColumn {
-      padding: 0;
-      vertical-align: top;
-    }
-    .commitColumn { width: COMMIT_WIDTH; }
-    .relatedColumn { width: 375px; }
-
-    .labels {
-      border-spacing: 0;
-      padding: 0;
-    }
-    .labelName {
-      color: #444;
-      vertical-align: top;
-      text-align: left;
-      padding-top: 3px;
-      padding-right: 5px;
-      white-space: nowrap;
-    }
-
-    .label_user {
-      display: inline-block;
-      margin-bottom: 2px;
-      padding: 1px 3px 0px 3px;
-      border-radius: 5px;
-      -webkit-border-radius: 5px;
-      background: trimColor;
-      border: 1px solid trimColor;
-      white-space: nowrap;
-    }
-    .label_user img.avatar {
-      margin: 0 2px 0 0;
-      width: 16px;
-      height: 16px;
-      vertical-align: bottom;
-    }
-    .label_user button {
-      cursor: pointer;
-      padding: 0;
-      margin: 0 0 0 5px;
-      border: 0;
-      background-color: transparent;
-      white-space: nowrap;
-    }
-
-    .label_ok {color: #060;}
-    .label_reject {color: #d14836;}
-    .label_need {color: #000;}
-    .label_may {color: #777;}
-
-    .hashtagName {
-      display: inline-block;
-      height: 15px;
-      margin-bottom: 2px;
-      padding: 1px 3px 1px 3px;
-      border-radius: 5px;
-      -webkit-border-radius: 5px;
-      background: #E2F5FF;
-      border: 1px solid #579FDA;
-      white-space: nowrap;
-    }
-
-    .hashtagName a,
-    .hashtagName button {
-      position: relative;
-      top: -4px;
-    }
-
-    .hashtagName button {
-      cursor: pointer;
-      padding: 0;
-      margin: 0 0 0 5px;
-      border: 0;
-      background-color: transparent;
-      white-space: nowrap;
-    }
-
-    .hashtagIcon img {
-      position: relative;
-      top: 4px;
-    }
-
-    .headerButtons button {
-      margin: 5.286px 3px 0 0;
-      text-align: center;
-      font-size: 8pt;
-      font-weight: bold;
-      cursor: pointer;
-      border: 2px solid;
-      color: rgba(0, 0, 0, 0.15);
-      background-color: #f5f5f5;
-      -webkit-border-radius: 2px;
-      -webkit-box-sizing: content-box;
-    }
-    .headerButtons button div {
-      color: #444;
-      min-width: 54px;
-      white-space: nowrap;
-      height: BUTTON_HEIGHT;
-      line-height: BUTTON_HEIGHT;
-    }
-    button.highlight {
-      background-color: #4d90fe;
-    }
-    button.highlight div { color: #fff; }
-
-    .sectionHeader {
-      position: relative;
-      background-color: trimColor;
-      font-weight: bold;
-      color: textColor;
-      height: 20px;
-      line-height: 20px;
-      margin: 0 -5px;
-      padding: 5px 5px;
-    }
-    .sectionHeader .headerButtons {
-      position: absolute;
-      left: 300px;
-      top: 2px;
-      height: 18px;
-      line-height: 18px;
-      border-left: 1px inset #fff;
-      padding-left: 5px;
-      padding-top: 3px;
-      padding-bottom: 3px;
-    }
-    .sectionHeader button { margin-top: 0; }
-
-    .diffBase {
-      display: inline-block;
-      height: 18px;
-      line-height: 18px;
-      font-size: smaller;
-      font-weight: normal;
-      vertical-align: top;
-    }
-    .diffBase select {
-      margin: 0;
-      border: 2px solid rgba(0, 0, 0, 0.15);
-      height: 20px;
-      font-size: 8pt;
-      font-weight: bold;
-      border-radius: 2px;
-      background-color: #f5f5f5
-    }
-
-    .replyBox {
-      background-color: trimColor;
-    }
-
-    .ownerPanel img, .uploaderPanel img {
-      margin: 0 2px 0 0;
-      width: 16px;
-      height: 16px !important;
-      vertical-align: bottom;
-    }
-
-    .headerExtension {
-      display: inline-block;
-      float: right;
-    }
-
-    .headerExtension>div>div {
-      float: left;
-    }
-
-    .changeExtension {
-      padding-top: 5px;
-    }
-
-    .relatedExtension {
-      padding-top: 5px;
-    }
-
-    .commitExtension {
-      padding-top: 5px;
-    }
-
-    .historyExtension {
-      display: inline-block;
-      float: right;
-    }
-
-    .pushCertStatus {
-      padding-left: 5px;
-    }
-
-    .notCurrentPatchSet {
-      background-color: #FFA62F;
-    }
-  </ui:style>
-
-  <g:HTMLPanel styleName='{style.cs2}'>
-    <g:HTMLPanel styleName='{style.headerLine}' ui:field='headerLine'>
-      <div class='{style.subjectLine}'>
-        <div class='{style.idAndStatus}'>
-          <span class='{style.changeId}'>
-            <ui:msg>Change <g:Anchor ui:field='permalink' title='Reload the change (Shortcut: R)'>
-              <ui:attribute name='title'/>
-            </g:Anchor> - <span ui:field='statusText' class='{style.statusText}'/>
-              <span ui:field='privateText' class='{style.privateText}'/>
-              <span ui:field='wipText' class='{style.wipText}'/></ui:msg>
-          </span>
-          <g:SimplePanel ui:field='headerExtension' styleName='{style.headerExtension}'/>
-        </div>
-      </div>
-
-      <div class='{style.infoLine}'>
-        <div class='{style.headerButtons} {style.infoLineHeaderButtons}'>
-          <g:Button ui:field='reply'
-              styleName=''
-              title=''
-              visible='false'>
-            <ui:attribute name='title'/>
-          </g:Button>
-          <c:QuickApprove ui:field='quickApprove'
-              styleName='{style.highlight}'
-              title='Apply score with one click'>
-            <ui:attribute name='title'/>
-          </c:QuickApprove>
-          <g:Button ui:field='publishEdit'
-              styleName='{style.highlight}' visible='false'>
-            <div><ui:msg>Publish Edit</ui:msg></div>
-          </g:Button>
-          <g:Button ui:field='rebaseEdit'
-              styleName='{style.highlight}' visible='false'>
-            <div><ui:msg>Rebase Edit</ui:msg></div>
-          </g:Button>
-          <g:Button ui:field='deleteEdit' styleName='' visible='false'>
-            <div><ui:msg>Delete Edit</ui:msg></div>
-          </g:Button>
-          <g:SimplePanel ui:field='headerExtensionMiddle' styleName='{style.headerExtension}'/>
-        </div>
-      </div>
-
-      <div class='{style.statusRight}'>
-        <g:FlowPanel styleName='{style.popdown}'>
-          <g:Button ui:field='includedIn' styleName='' visible="false">
-            <div><ui:msg>Included in</ui:msg></div>
-          </g:Button>
-          <g:Button ui:field='patchSets' styleName=''>
-            <div ui:field='patchSetsText'/>
-          </g:Button>
-          <g:Button ui:field='download' styleName=''>
-            <div><ui:msg>Download</ui:msg></div>
-          </g:Button>
-          <g:SimplePanel ui:field='headerExtensionRight' styleName='{style.headerExtension}'/>
-        </g:FlowPanel>
-        <c:StarIcon ui:field='star' styleName='{style.star}' title='Star the change (Shortcut: s)'>
-          <ui:attribute name='title'/>
-        </c:StarIcon>
-      </div>
-    </g:HTMLPanel>
-
-    <table class='{style.infoTable}'>
-      <tr>
-        <td class='{style.commitColumn}'>
-          <c:CommitBox ui:field='commit'/>
-          <g:SimplePanel ui:field='commitExtension' styleName='{style.commitExtension}'/>
-        </td>
-        <td class='{style.infoColumn}'>
-          <table id='change_infoTable'>
-            <tr>
-              <th><ui:msg>Owner</ui:msg></th>
-              <td>
-                <g:FlowPanel ui:field='ownerPanel' styleName='{style.ownerPanel}'>
-                  <x:InlineHyperlink ui:field='ownerLink'/>
-                </g:FlowPanel>
-              </td>
-            </tr>
-            <tr ui:field='uploaderRow'>
-              <th><ui:msg>Uploader</ui:msg></th>
-              <td>
-                <g:FlowPanel ui:field='uploaderPanel' styleName='{style.uploaderPanel}'>
-                  <g:InlineLabel ui:field='uploaderName'/>
-                </g:FlowPanel>
-              </td>
-            </tr>
-            <tr ui:field='assigneeRow'>
-              <th><ui:msg>Assignee</ui:msg></th>
-              <td>
-                <c:Assignee ui:field='assignee'/>
-              </td>
-            </tr>
-            <tr>
-              <th><ui:msg>Reviewers</ui:msg></th>
-              <td>
-                <c:Reviewers ui:field='reviewers'/>
-              </td>
-            </tr>
-            <tr>
-              <th/>
-              <td ui:field='ccText'/>
-            </tr>
-            <tr>
-              <th><ui:msg>Project</ui:msg></th>
-              <td><x:InlineHyperlink ui:field='projectDashboard'
-                     title='Go to project dashboard'>
-                     <ui:attribute name='title'/>
-                  </x:InlineHyperlink>
-                  <a ui:field='projectSettingsLink'
-                     class='{style.projectSettings}'>
-                    <g:Image
-                       ui:field='projectSettings'
-                       resource='{ico.gear}'
-                       title='Go to project settings'>
-                      <ui:attribute name='title'/>
-                    </g:Image>
-                  </a>
-              </td>
-            </tr>
-            <tr>
-              <th><ui:msg>Branch</ui:msg></th>
-              <td><x:InlineHyperlink ui:field='branchLink'
-                     title='Search for changes on this branch'>
-                     <ui:attribute name='title'/>
-                  </x:InlineHyperlink>
-              </td>
-            </tr>
-            <tr>
-              <th><ui:msg>Topic</ui:msg></th>
-              <td><c:Topic ui:field='topic'/></td>
-            </tr>
-            <tr ui:field='strategy'>
-              <th><ui:msg>Strategy</ui:msg></th>
-              <td>
-                <span ui:field='submitActionText'/>
-                <div ui:field='notMergeable'
-                     class='{style.notMergeable}'
-                     style='display: none'
-                     aria-hidden='true'
-                     title='The change cannot be merged due to a path conflict. Rebase the change and upload the rebased commit for review.'>
-                  <ui:attribute name='title'/>
-                  <ui:msg>Cannot Merge</ui:msg>
-                </div>
-              </td>
-            </tr>
-            <tr>
-              <th ui:field='actionText'/>
-              <td ui:field='actionDate'/>
-            </tr>
-            <tr ui:field='hashtagTableRow'>
-              <th><ui:msg>Hashtags</ui:msg></th>
-              <td colspan='2'>
-                <c:Hashtags ui:field='hashtags'/>
-              </td>
-            </tr>
-            <tr><td colspan='2'><c:Actions ui:field='actions'/></td></tr>
-          </table>
-          <hr/>
-          <c:Labels ui:field='labels' styleName='{style.labels}'/>
-          <g:SimplePanel ui:field='changeExtension' styleName='{style.changeExtension}'/>
-          <div id='change_plugins'/>
-        </td>
-        <td class='{style.relatedColumn}'>
-          <c:RelatedChanges ui:field='related'/>
-          <g:SimplePanel ui:field='relatedExtension' styleName='{style.relatedExtension}'/>
-        </td>
-      </tr>
-    </table>
-
-    <div class='{style.sectionHeader} {style.headerButtons}'>
-      <ui:msg>Files</ui:msg>
-      <g:Button ui:field='addFile'
-         title='Add file to this change'
-         styleName=''
-         visible='false'>
-        <ui:attribute name='title'/>
-        <div><ui:msg>Add&#8230;</ui:msg></div>
-      </g:Button>
-      <g:Button ui:field='deleteFile'
-         title='Delete file from the repository'
-         styleName=''
-         visible='false'>
-        <ui:attribute name='title'/>
-        <div><ui:msg>Delete&#8230;</ui:msg></div>
-      </g:Button>
-      <g:Button ui:field='renameFile'
-         title='Rename file in the repository'
-         styleName=''
-         visible='false'>
-        <ui:attribute name='title'/>
-        <div><ui:msg>Rename&#8230;</ui:msg></div>
-      </g:Button>
-      <div class='{style.headerButtons}'>
-        <g:Button ui:field='openAll'
-            styleName=''
-            title='Open each file in a new tab'>
-          <ui:attribute name='title'/>
-          <div><ui:msg>Open All</ui:msg></div>
-        </g:Button>
-        <div class='{style.diffBase}'>
-          <ui:msg>Diff against: <g:ListBox ui:field='diffBase' styleName=''/></ui:msg>
-        </div>
-        <g:Button ui:field='editMode'
-            styleName=''
-            visible='false'
-            title='Switch file table to edit mode'>
-          <ui:attribute name='title'/>
-          <div><ui:msg>Edit</ui:msg></div>
-        </g:Button>
-        <g:Button ui:field='reviewMode'
-            styleName=''
-            visible='false'
-            title='Done with edit mode'>
-          <ui:attribute name='title'/>
-          <div><ui:msg>Done Editing</ui:msg></div>
-        </g:Button>
-      </div>
-    </div>
-    <c:FileTable ui:field='files'/>
-
-    <div class='{style.sectionHeader}'>
-      <ui:msg>History</ui:msg>
-      <div class='{style.headerButtons}'>
-        <g:Button ui:field='expandAll'
-            styleName=''
-            title='Expand all messages in the change history'>
-          <ui:attribute name='title'/>
-          <div><ui:msg>Expand All</ui:msg></div>
-        </g:Button>
-        <g:Button ui:field='collapseAll'
-            styleName=''
-            visible='false'
-            title='Collapse all messages in the change history'>
-          <ui:attribute name='title'/>
-          <div><ui:msg>Collapse All</ui:msg></div>
-        </g:Button>
-        <g:Button ui:field='hideTaggedComments'
-            styleName=''
-            visible='false'
-            title='Hide tagged comments'>
-          <ui:attribute name='title'/>
-          <div><ui:msg>Hide tagged comments</ui:msg></div>
-        </g:Button>
-        <g:Button ui:field='showTaggedComments'
-            styleName=''
-            visible='false'
-            title='Show tagged comments'>
-          <ui:attribute name='title'/>
-          <div><ui:msg>Show tagged comments</ui:msg></div>
-        </g:Button>
-        <g:SimplePanel ui:field='historyExtensionRight' styleName='{style.historyExtension}'/>
-      </div>
-    </div>
-    <c:History ui:field='history'/>
-  </g:HTMLPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CherryPickAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CherryPickAction.java
deleted file mode 100644
index be011d2..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CherryPickAction.java
+++ /dev/null
@@ -1,80 +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.client.change;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.Util;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.CherryPickDialog;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.event.logical.shared.CloseEvent;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.PopupPanel;
-
-class CherryPickAction {
-  static void call(
-      final Button b,
-      final ChangeInfo info,
-      final String revision,
-      final Project.NameKey project,
-      final String commitMessage) {
-    // TODO Replace CherryPickDialog with a nicer looking display.
-    b.setEnabled(false);
-    new CherryPickDialog(project) {
-      {
-        sendButton.setText(Util.C.buttonCherryPickChangeSend());
-        if (info.status() == Change.Status.MERGED) {
-          message.setText(Util.M.cherryPickedChangeDefaultMessage(commitMessage.trim(), revision));
-        } else {
-          message.setText(commitMessage.trim());
-        }
-      }
-
-      @Override
-      public void onSend() {
-        ChangeApi.cherrypick(
-            info.project(),
-            info.legacyId().get(),
-            revision,
-            getDestinationBranch(),
-            getMessageText(),
-            new GerritCallback<ChangeInfo>() {
-              @Override
-              public void onSuccess(ChangeInfo result) {
-                sent = true;
-                hide();
-                Gerrit.display(PageLinks.toChange(project, result.legacyId()));
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {
-                enableButtons(true);
-                super.onFailure(caught);
-              }
-            });
-      }
-
-      @Override
-      public void onClose(CloseEvent<PopupPanel> event) {
-        super.onClose(event);
-        b.setEnabled(true);
-      }
-    }.center();
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java
deleted file mode 100644
index 0112579..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java
+++ /dev/null
@@ -1,219 +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.client.change;
-
-import com.google.gerrit.client.AvatarImage;
-import com.google.gerrit.client.FormatUtil;
-import com.google.gerrit.client.info.AccountInfo;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.info.ChangeInfo.CommitInfo;
-import com.google.gerrit.client.info.ChangeInfo.GitPerson;
-import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
-import com.google.gerrit.client.info.WebLinkInfo;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.ui.CommentLinkProcessor;
-import com.google.gerrit.client.ui.InlineHyperlink;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.PageLinks;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.TableRowElement;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.resources.client.CssResource;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.uibinder.client.UiHandler;
-import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.HTML;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwt.user.client.ui.ScrollPanel;
-import com.google.gwt.user.client.ui.UIObject;
-import com.google.gwtexpui.clippy.client.CopyableLabel;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-
-class CommitBox extends Composite {
-  interface Binder extends UiBinder<HTMLPanel, CommitBox> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  interface Style extends CssResource {
-    String collapsed();
-
-    String expanded();
-
-    String clippy();
-
-    String parentWebLink();
-  }
-
-  @UiField Style style;
-  @UiField FlowPanel authorPanel;
-  @UiField FlowPanel committerPanel;
-  @UiField Image mergeCommit;
-  @UiField CopyableLabel commitName;
-  @UiField FlowPanel webLinkPanel;
-  @UiField TableRowElement firstParent;
-  @UiField FlowPanel parentCommits;
-  @UiField FlowPanel parentWebLinks;
-  @UiField InlineHyperlink authorNameEmail;
-  @UiField Element authorDate;
-  @UiField InlineHyperlink committerNameEmail;
-  @UiField Element committerDate;
-  @UiField CopyableLabel idText;
-  @UiField HTML text;
-  @UiField ScrollPanel scroll;
-  @UiField Button more;
-  @UiField Element parentNotCurrentText;
-  private boolean expanded;
-
-  CommitBox() {
-    initWidget(uiBinder.createAndBindUi(this));
-    addStyleName(style.collapsed());
-  }
-
-  void onShowView() {
-    more.setVisible(scroll.getMaximumVerticalScrollPosition() > 0);
-  }
-
-  @UiHandler("more")
-  void onMore(@SuppressWarnings("unused") ClickEvent e) {
-    if (expanded) {
-      removeStyleName(style.expanded());
-      addStyleName(style.collapsed());
-    } else {
-      removeStyleName(style.collapsed());
-      addStyleName(style.expanded());
-    }
-    expanded = !expanded;
-  }
-
-  void set(CommentLinkProcessor commentLinkProcessor, ChangeInfo change, String revision) {
-    RevisionInfo revInfo = change.revision(revision);
-    CommitInfo commit = revInfo.commit();
-
-    commitName.setText(revision);
-    idText.setText("Change-Id: " + change.changeId());
-    idText.setPreviewText(change.changeId());
-
-    formatLink(commit.author(), authorPanel, authorNameEmail, authorDate, change);
-    formatLink(commit.committer(), committerPanel, committerNameEmail, committerDate, change);
-    text.setHTML(
-        commentLinkProcessor.apply(new SafeHtmlBuilder().append(commit.message()).linkify()));
-    setWebLinks(webLinkPanel, revInfo.commit());
-
-    if (revInfo.commit().parents().length() > 1) {
-      mergeCommit.setVisible(true);
-    }
-
-    setParents(revInfo.commit().parents());
-  }
-
-  void setParentNotCurrent(boolean parentNotCurrent) {
-    // display the orange ball if parent has moved on (not current)
-    UIObject.setVisible(parentNotCurrentText, parentNotCurrent);
-    parentNotCurrentText.setInnerText(parentNotCurrent ? "\u25CF" : "");
-  }
-
-  private void setWebLinks(FlowPanel panel, CommitInfo commit) {
-    JsArray<WebLinkInfo> links = commit.webLinks();
-    if (links != null) {
-      for (WebLinkInfo link : Natives.asList(links)) {
-        panel.add(link.toAnchor());
-      }
-    }
-  }
-
-  private void setParents(JsArray<CommitInfo> commits) {
-    setVisible(firstParent, true);
-    TableRowElement next = firstParent;
-    TableRowElement previous = null;
-    for (CommitInfo c : Natives.asList(commits)) {
-      if (next == firstParent) {
-        CopyableLabel copyLabel = getCommitLabel(c);
-        parentCommits.add(copyLabel);
-        setWebLinks(parentWebLinks, c);
-      } else {
-        next.appendChild(DOM.createTD());
-        Element td1 = DOM.createTD();
-        td1.appendChild(getCommitLabel(c).getElement());
-        next.appendChild(td1);
-        FlowPanel linksPanel = new FlowPanel();
-        linksPanel.addStyleName(style.parentWebLink());
-        setWebLinks(linksPanel, c);
-        Element td2 = DOM.createTD();
-        td2.appendChild(linksPanel.getElement());
-        next.appendChild(td2);
-        previous.getParentElement().insertAfter(next, previous);
-      }
-      previous = next;
-      next = DOM.createTR().cast();
-    }
-  }
-
-  private CopyableLabel getCommitLabel(CommitInfo c) {
-    CopyableLabel copyLabel;
-    copyLabel = new CopyableLabel(c.commit());
-    copyLabel.setTitle(c.subject());
-    copyLabel.setStyleName(style.clippy());
-    return copyLabel;
-  }
-
-  private static void formatLink(
-      GitPerson person, FlowPanel p, InlineHyperlink name, Element date, ChangeInfo change) {
-    // only try to fetch the avatar image for author and committer if an avatar
-    // plugin is installed, if the change owner has no avatar info assume that
-    // no avatar plugin is installed
-    if (change.owner().hasAvatarInfo()) {
-      AvatarImage avatar;
-      if (sameEmail(change.owner(), person)) {
-        avatar = new AvatarImage(change.owner());
-      } else {
-        avatar = new AvatarImage(AccountInfo.create(0, person.name(), person.email(), null));
-      }
-      p.insert(avatar, 0);
-    }
-
-    name.setText(renderName(person));
-    name.setTargetHistoryToken(PageLinks.toAccountQuery(owner(person), change.status()));
-    date.setInnerText(FormatUtil.mediumFormat(person.date()));
-  }
-
-  private static String renderName(GitPerson person) {
-    return person.name() + " <" + person.email() + ">";
-  }
-
-  private static String owner(GitPerson person) {
-    if (person.email() != null) {
-      return person.email();
-    } else if (person.name() != null) {
-      return person.name();
-    } else {
-      return "";
-    }
-  }
-
-  private static boolean sameEmail(@Nullable AccountInfo p1, @Nullable GitPerson p2) {
-    return p1 != null
-        && p2 != null
-        && p1.email() != null
-        && p2.email() != null
-        && p1.email().equals(p2.email());
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.ui.xml
deleted file mode 100644
index 5f476be..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.ui.xml
+++ /dev/null
@@ -1,198 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder
-    xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'
-    xmlns:x='urn:import:com.google.gerrit.client.ui'
-    xmlns:clippy='urn:import:com.google.gwtexpui.clippy.client'>
-  <ui:with field='ico' type='com.google.gerrit.client.GerritResources'/>
-  <ui:image field="toggle" src="moreLess.png"/>
-  <ui:style gss='false' type='com.google.gerrit.client.change.CommitBox.Style'>
-    @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
-
-    .collapsed .scroll { height: 250px }
-    .scroll, .more { width: 560px }
-    .scroll {
-      border-right: 1px solid trimColor;
-      border-bottom: 1px solid trimColor;
-    }
-
-    .text {
-      font-family: monospace;
-      white-space: pre;
-    }
-
-    .more {
-      height: 8px;
-      line-height: 8px;
-      text-align: center;
-    }
-    .moreButton {
-      padding: 0 5px 0 5px;
-      margin: 0;
-      border: none;
-      height: 8px;
-      background-color: #F7F7F7;
-    }
-    .moreButton:focus {
-      outline: none;
-    }
-
-    @sprite .toggle {
-      gwt-image: "toggle";
-      width: 13px;
-      height: 8px;
-      padding: 0;
-    }
-    .collapsed .toggle { background-position: -13px -8px }
-    .expanded .toggle { background-position: 0px -8px }
-    .collapsed button:hover .toggle { background-position: -13px 0px }
-    .expanded button:hover .toggle { background-position: 0px 0px }
-
-    .header {
-      border-spacing: 0;
-      padding: 0;
-      width: 560px;
-    }
-    .header th { width: 72px; }
-    .header td { white-space: nowrap; }
-    .date { width: 132px; }
-
-    .clippy {
-      position: relative;
-    }
-    .clippy div {
-      position: absolute;
-      top: 0px;
-      right: -16px;
-    }
-    <!-- To make room for the copyableLabel from the adjacent column -->
-    .webLinkPanel a:first-child {
-      margin-left:16px;
-    }
-    .webLinkPanel>a {
-      margin-left:2px;
-    }
-
-    .parentWebLink a:first-child {
-      margin-left:16px;
-    }
-    .parentWebLink>a {
-      margin-left:2px;
-    }
-
-    .commit {
-      margin-right: 3px;
-      float: left;
-    }
-
-    .userPanel img {
-      margin: 0 2px 0 0;
-      width: 16px;
-      height: 16px !important;
-      vertical-align: bottom;
-    }
-
-    .parent {
-      margin-right: 3px;
-      float: left;
-    }
-    .parentNotCurrent {
-      color: #FFA62F;   <!-- orange -->
-      font-weight: bold;
-    }
-
-  </ui:style>
-  <g:HTMLPanel>
-    <g:ScrollPanel styleName='{style.scroll}' ui:field='scroll'>
-      <g:HTML styleName='{style.text}' ui:field='text'/>
-    </g:ScrollPanel>
-    <div class='{style.more}'>
-      <g:Button ui:field='more'
-          styleName='{style.moreButton}'
-          title='Expand/Collapse'>
-        <ui:attribute name='title'/>
-        <div class='{style.toggle}'/>
-      </g:Button>
-    </div>
-    <table class='{style.header}'>
-      <tr>
-        <th><ui:msg>Author</ui:msg></th>
-        <td>
-          <g:FlowPanel ui:field='authorPanel' styleName='{style.userPanel}'>
-            <x:InlineHyperlink ui:field='authorNameEmail'
-              title='Search for changes by this user'>
-              <ui:attribute name='title'/>
-            </x:InlineHyperlink>
-          </g:FlowPanel>
-        </td>
-        <td ui:field='authorDate' class='{style.date}' colspan="2"/>
-      </tr>
-      <tr>
-        <th><ui:msg>Committer</ui:msg></th>
-        <td>
-          <g:FlowPanel ui:field='committerPanel' styleName='{style.userPanel}'>
-            <x:InlineHyperlink ui:field='committerNameEmail'
-              title='Search for changes by this user'>
-              <ui:attribute name='title'/>
-            </x:InlineHyperlink>
-          </g:FlowPanel>
-        </td>
-        <td ui:field='committerDate' class='{style.date}' colspan="2"/>
-      </tr>
-      <tr>
-        <th>
-          <div class='{style.commit}'>
-            <ui:msg>Commit</ui:msg>
-          </div>
-          <g:Image
-              ui:field='mergeCommit'
-              resource='{ico.merge}'
-              visible='false'
-              title='Merge Commit'>
-            <ui:attribute name='title'/>
-          </g:Image>
-        </th>
-        <td><clippy:CopyableLabel styleName='{style.clippy}' ui:field='commitName'/></td>
-        <td>
-            <g:FlowPanel ui:field='webLinkPanel' styleName='{style.webLinkPanel}'/>
-        </td>
-      </tr>
-      <tr ui:field='firstParent' style='display: none'>
-        <th>
-          <div class='{style.parent}'>
-            <ui:msg>Parent(s)</ui:msg>
-          </div>
-          <div ui:field='parentNotCurrentText'
-              title='Not current - rebase possible'
-              class='{style.parentNotCurrent}'
-              style='display: none' aria-hidden='true'/>
-        </th>
-        <td>
-          <g:FlowPanel ui:field='parentCommits'/>
-        </td>
-        <td>
-          <g:FlowPanel ui:field='parentWebLinks' styleName='{style.parentWebLink}'/>
-        </td>
-      </tr>
-      <tr>
-        <th><ui:msg>Change-Id</ui:msg></th>
-        <td><clippy:CopyableLabel styleName='{style.clippy}' ui:field='idText'/></td>
-      </tr>
-    </table>
-  </g:HTMLPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DeleteFileAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DeleteFileAction.java
deleted file mode 100644
index 9369c18..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DeleteFileAction.java
+++ /dev/null
@@ -1,78 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.change;
-
-import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.event.logical.shared.CloseEvent;
-import com.google.gwt.event.logical.shared.CloseHandler;
-import com.google.gwt.user.client.ui.PopupPanel;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-
-class DeleteFileAction {
-  private final Project.NameKey project;
-  private final Change.Id changeId;
-  private final RevisionInfo revision;
-  private final ChangeScreen.Style style;
-  private final Widget deleteButton;
-
-  private DeleteFileBox deleteBox;
-  private PopupPanel popup;
-
-  DeleteFileAction(
-      Project.NameKey project,
-      Change.Id changeId,
-      RevisionInfo revision,
-      ChangeScreen.Style style,
-      Widget deleteButton) {
-    this.project = project;
-    this.changeId = changeId;
-    this.revision = revision;
-    this.style = style;
-    this.deleteButton = deleteButton;
-  }
-
-  void onDelete() {
-    if (popup != null) {
-      popup.hide();
-      return;
-    }
-
-    if (deleteBox == null) {
-      deleteBox = new DeleteFileBox(project, changeId, revision);
-    }
-    deleteBox.clearPath();
-
-    final PopupPanel p = new PopupPanel(true);
-    p.setStyleName(style.replyBox());
-    p.addAutoHidePartner(deleteButton.getElement());
-    p.addCloseHandler(
-        new CloseHandler<PopupPanel>() {
-          @Override
-          public void onClose(CloseEvent<PopupPanel> event) {
-            if (popup == p) {
-              popup = null;
-            }
-          }
-        });
-    p.add(deleteBox);
-    p.showRelativeTo(deleteButton);
-    GlobalKey.dialog(p);
-    deleteBox.setFocus(true);
-    popup = p;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DeleteFileBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DeleteFileBox.java
deleted file mode 100644
index 1885293..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DeleteFileBox.java
+++ /dev/null
@@ -1,121 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.change;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.VoidResult;
-import com.google.gerrit.client.changes.ChangeEditApi;
-import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
-import com.google.gerrit.client.ui.RemoteSuggestBox;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.logical.shared.CloseEvent;
-import com.google.gwt.event.logical.shared.CloseHandler;
-import com.google.gwt.event.logical.shared.SelectionEvent;
-import com.google.gwt.event.logical.shared.SelectionHandler;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.uibinder.client.UiHandler;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwt.user.client.ui.PopupPanel;
-import com.google.gwt.user.client.ui.Widget;
-
-class DeleteFileBox extends Composite {
-  interface Binder extends UiBinder<HTMLPanel, DeleteFileBox> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  private final Project.NameKey project;
-  private final Change.Id changeId;
-
-  @UiField Button delete;
-  @UiField Button cancel;
-
-  @UiField(provided = true)
-  RemoteSuggestBox path;
-
-  DeleteFileBox(Project.NameKey project, Change.Id changeId, RevisionInfo revision) {
-    this.project = project;
-    this.changeId = changeId;
-
-    path = new RemoteSuggestBox(new PathSuggestOracle(project, changeId, revision));
-    path.addSelectionHandler(
-        new SelectionHandler<String>() {
-          @Override
-          public void onSelection(SelectionEvent<String> event) {
-            delete(event.getSelectedItem());
-          }
-        });
-    path.addCloseHandler(
-        new CloseHandler<RemoteSuggestBox>() {
-          @Override
-          public void onClose(CloseEvent<RemoteSuggestBox> event) {
-            hide();
-          }
-        });
-
-    initWidget(uiBinder.createAndBindUi(this));
-  }
-
-  void setFocus(boolean focus) {
-    path.setFocus(focus);
-  }
-
-  void clearPath() {
-    path.setText("");
-  }
-
-  @UiHandler("delete")
-  void onDelete(@SuppressWarnings("unused") ClickEvent e) {
-    delete(path.getText());
-  }
-
-  private void delete(String path) {
-    hide();
-    ChangeEditApi.delete(
-        project.get(),
-        changeId.get(),
-        path,
-        new AsyncCallback<VoidResult>() {
-          @Override
-          public void onSuccess(VoidResult result) {
-            Gerrit.display(PageLinks.toChangeInEditMode(project, changeId));
-          }
-
-          @Override
-          public void onFailure(Throwable caught) {}
-        });
-  }
-
-  @UiHandler("cancel")
-  void onCancel(@SuppressWarnings("unused") ClickEvent e) {
-    hide();
-  }
-
-  private void hide() {
-    for (Widget w = getParent(); w != null; w = w.getParent()) {
-      if (w instanceof PopupPanel) {
-        ((PopupPanel) w).hide();
-        break;
-      }
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DeleteFileBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DeleteFileBox.ui.xml
deleted file mode 100644
index 9e79f752..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DeleteFileBox.ui.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder
-    xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:u='urn:import:com.google.gerrit.client.ui'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
-  <ui:style gss='false'>
-    .cancel { float: right; }
-  </ui:style>
-  <g:HTMLPanel>
-    <div class='{res.style.section}'>
-      <ui:msg>Path: <u:RemoteSuggestBox ui:field='path' visibleLength='86'/></ui:msg>
-    </div>
-    <div class='{res.style.section}'>
-      <g:Button ui:field='delete'
-          title='Delete file from the repository'
-          styleName='{res.style.button}'>
-        <ui:attribute name='title'/>
-        <div><ui:msg>Delete</ui:msg></div>
-      </g:Button>
-      <g:Button ui:field='cancel'
-          styleName='{res.style.button}'
-          addStyleNames='{style.cancel}'>
-          <div>Cancel</div>
-      </g:Button>
-    </div>
-  </g:HTMLPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadAction.java
deleted file mode 100644
index 8e4ea84..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadAction.java
+++ /dev/null
@@ -1,41 +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.client.change;
-
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwt.user.client.ui.UIObject;
-import com.google.gwt.user.client.ui.Widget;
-
-class DownloadAction extends RightSidePopdownAction {
-  private final DownloadBox downloadBox;
-
-  DownloadAction(
-      ChangeInfo info,
-      String revision,
-      ChangeScreen.Style style,
-      UIObject relativeTo,
-      Widget downloadButton) {
-    super(style, relativeTo, downloadButton);
-    this.downloadBox =
-        new DownloadBox(
-            info, revision, new PatchSet.Id(info.legacyId(), info.revision(revision)._number()));
-  }
-
-  @Override
-  Widget getWidget() {
-    return downloadBox;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java
deleted file mode 100644
index 547f3d5..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java
+++ /dev/null
@@ -1,261 +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.client.change;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.account.AccountApi;
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeList;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.info.ChangeInfo.EditInfo;
-import com.google.gerrit.client.info.ChangeInfo.FetchInfo;
-import com.google.gerrit.client.info.GeneralPreferences;
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.extensions.client.ListChangesOption;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.event.dom.client.ChangeEvent;
-import com.google.gwt.event.dom.client.ChangeHandler;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.FlexTable;
-import com.google.gwt.user.client.ui.HorizontalPanel;
-import com.google.gwt.user.client.ui.InlineLabel;
-import com.google.gwt.user.client.ui.ListBox;
-import com.google.gwt.user.client.ui.VerticalPanel;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtexpui.clippy.client.CopyableLabel;
-import java.util.ArrayList;
-import java.util.EnumSet;
-import java.util.Iterator;
-import java.util.List;
-
-class DownloadBox extends VerticalPanel {
-  private final ChangeInfo change;
-  private final String revision;
-  private final PatchSet.Id psId;
-  private final FlexTable commandTable;
-  private final ListBox scheme;
-  private NativeMap<FetchInfo> fetch;
-
-  DownloadBox(ChangeInfo change, String revision, PatchSet.Id psId) {
-    this.change = change;
-    this.revision = revision;
-    this.psId = psId;
-    this.commandTable = new FlexTable();
-    this.scheme = new ListBox();
-    this.scheme.addChangeHandler(
-        new ChangeHandler() {
-          @Override
-          public void onChange(ChangeEvent event) {
-            renderCommands();
-            if (Gerrit.isSignedIn()) {
-              saveScheme();
-            }
-          }
-        });
-
-    setStyleName(Gerrit.RESOURCES.css().downloadBox());
-    commandTable.setStyleName(Gerrit.RESOURCES.css().downloadBoxTable());
-    scheme.setStyleName(Gerrit.RESOURCES.css().downloadBoxScheme());
-    add(commandTable);
-  }
-
-  @Override
-  protected void onLoad() {
-    if (fetch == null) {
-      if (psId.get() == 0) {
-        ChangeApi.editWithCommands(change.project(), change.legacyId().get())
-            .get(
-                new AsyncCallback<EditInfo>() {
-                  @Override
-                  public void onSuccess(EditInfo result) {
-                    fetch = result.fetch();
-                    renderScheme();
-                  }
-
-                  @Override
-                  public void onFailure(Throwable caught) {}
-                });
-      } else {
-        RestApi call = ChangeApi.detail(change.project(), change.legacyId().get());
-        ChangeList.addOptions(
-            call,
-            EnumSet.of(
-                revision.equals(change.currentRevision())
-                    ? ListChangesOption.CURRENT_REVISION
-                    : ListChangesOption.ALL_REVISIONS,
-                ListChangesOption.DOWNLOAD_COMMANDS));
-        call.get(
-            new AsyncCallback<ChangeInfo>() {
-              @Override
-              public void onSuccess(ChangeInfo result) {
-                fetch = result.revision(revision).fetch();
-                renderScheme();
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {}
-            });
-      }
-    }
-  }
-
-  private void renderCommands() {
-    commandTable.removeAllRows();
-
-    if (scheme.getItemCount() > 0) {
-      FetchInfo fetchInfo = fetch.get(scheme.getValue(scheme.getSelectedIndex()));
-      for (String commandName : fetchInfo.commands().sortedKeys()) {
-        CopyableLabel copyLabel = new CopyableLabel(fetchInfo.command(commandName));
-        copyLabel.setStyleName(Gerrit.RESOURCES.css().downloadBoxCopyLabel());
-        insertCommand(commandName, copyLabel);
-      }
-    }
-    if (change.revision(revision).commit().parents().length() == 1) {
-      insertPatch();
-    }
-    insertArchive();
-    insertCommand(null, scheme);
-  }
-
-  private void insertPatch() {
-    String id = revision.substring(0, 7);
-    Anchor patchBase64 = new Anchor(id + ".diff.base64");
-    patchBase64.setHref(
-        new RestApi("/changes/")
-            .id(psId.getParentKey().get())
-            .view("revisions")
-            .id(revision)
-            .view("patch")
-            .addParameterTrue("download")
-            .url());
-
-    Anchor patchZip = new Anchor(id + ".diff.zip");
-    patchZip.setHref(
-        new RestApi("/changes/")
-            .id(psId.getParentKey().get())
-            .view("revisions")
-            .id(revision)
-            .view("patch")
-            .addParameterTrue("zip")
-            .url());
-
-    HorizontalPanel p = new HorizontalPanel();
-    p.add(patchBase64);
-    InlineLabel spacer = new InlineLabel("|");
-    spacer.setStyleName(Gerrit.RESOURCES.css().downloadBoxSpacer());
-    p.add(spacer);
-    p.add(patchZip);
-    insertCommand("Patch-File", p);
-  }
-
-  private void insertArchive() {
-    List<String> activated = Gerrit.info().download().archives();
-    if (activated.isEmpty()) {
-      return;
-    }
-
-    List<Anchor> anchors = new ArrayList<>(activated.size());
-    for (String f : activated) {
-      Anchor archive = new Anchor(f);
-      archive.setHref(
-          new RestApi("/changes/")
-              .id(psId.getParentKey().get())
-              .view("revisions")
-              .id(revision)
-              .view("archive")
-              .addParameter("format", f)
-              .url());
-      anchors.add(archive);
-    }
-
-    HorizontalPanel p = new HorizontalPanel();
-    Iterator<Anchor> it = anchors.iterator();
-    while (it.hasNext()) {
-      Anchor a = it.next();
-      p.add(a);
-      if (it.hasNext()) {
-        InlineLabel spacer = new InlineLabel("|");
-        spacer.setStyleName(Gerrit.RESOURCES.css().downloadBoxSpacer());
-        p.add(spacer);
-      }
-    }
-    insertCommand("Archive", p);
-  }
-
-  private void insertCommand(String commandName, Widget w) {
-    int row = commandTable.getRowCount();
-    commandTable.insertRow(row);
-    commandTable
-        .getCellFormatter()
-        .addStyleName(row, 0, Gerrit.RESOURCES.css().downloadBoxTableCommandColumn());
-    if (commandName != null) {
-      commandTable.setText(row, 0, commandName);
-    }
-    if (w != null) {
-      commandTable.setWidget(row, 1, w);
-    }
-  }
-
-  private void renderScheme() {
-    for (String id : fetch.sortedKeys()) {
-      scheme.addItem(id);
-    }
-    if (scheme.getItemCount() == 0) {
-      scheme.setVisible(false);
-    } else {
-      if (scheme.getItemCount() == 1) {
-        scheme.setSelectedIndex(0);
-        scheme.setVisible(false);
-      } else {
-        int select = 0;
-        String find = Gerrit.getUserPreferences().downloadScheme();
-        if (find != null) {
-          for (int i = 0; i < scheme.getItemCount(); i++) {
-            if (find.equals(scheme.getValue(i))) {
-              select = i;
-              break;
-            }
-          }
-        }
-        scheme.setSelectedIndex(select);
-      }
-    }
-    renderCommands();
-  }
-
-  private void saveScheme() {
-    String schemeStr = scheme.getValue(scheme.getSelectedIndex());
-    GeneralPreferences prefs = Gerrit.getUserPreferences();
-    if (Gerrit.isSignedIn() && !schemeStr.equals(prefs.downloadScheme())) {
-      prefs.downloadScheme(schemeStr);
-      GeneralPreferences in = GeneralPreferences.create();
-      in.downloadScheme(schemeStr);
-      AccountApi.self()
-          .view("preferences")
-          .put(
-              in,
-              new AsyncCallback<JavaScriptObject>() {
-                @Override
-                public void onSuccess(JavaScriptObject result) {}
-
-                @Override
-                public void onFailure(Throwable caught) {}
-              });
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditActions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditActions.java
deleted file mode 100644
index f075c16..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditActions.java
+++ /dev/null
@@ -1,69 +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.client.change;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.user.client.ui.Button;
-
-public class EditActions {
-
-  static void deleteEdit(Project.NameKey project, Change.Id id, Button... editButtons) {
-    ChangeApi.deleteEdit(project.get(), id.get(), cs(project, id, editButtons));
-  }
-
-  static void publishEdit(Project.NameKey project, Change.Id id, Button... editButtons) {
-    ChangeApi.publishEdit(project.get(), id.get(), cs(project, id, editButtons));
-  }
-
-  static void rebaseEdit(Project.NameKey project, Change.Id id, Button... editButtons) {
-    ChangeApi.rebaseEdit(project.get(), id.get(), cs(project, id, editButtons));
-  }
-
-  public static GerritCallback<JavaScriptObject> cs(
-      Project.NameKey project, final Change.Id id, Button... editButtons) {
-    setEnabled(false, editButtons);
-    return new GerritCallback<JavaScriptObject>() {
-      @Override
-      public void onSuccess(JavaScriptObject result) {
-        Gerrit.display(PageLinks.toChange(project, id));
-      }
-
-      @Override
-      public void onFailure(Throwable err) {
-        setEnabled(true, editButtons);
-        if (SubmitFailureDialog.isConflict(err)) {
-          new SubmitFailureDialog(err.getMessage()).center();
-          Gerrit.display(PageLinks.toChange(project, id));
-        } else {
-          super.onFailure(err);
-        }
-      }
-    };
-  }
-
-  private static void setEnabled(boolean enabled, Button... editButtons) {
-    if (editButtons != null) {
-      for (Button b : editButtons) {
-        b.setEnabled(enabled);
-      }
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileComments.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileComments.java
deleted file mode 100644
index 083c824..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileComments.java
+++ /dev/null
@@ -1,57 +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.client.change;
-
-import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.client.changes.CommentInfo;
-import com.google.gerrit.client.ui.CommentLinkProcessor;
-import com.google.gerrit.client.ui.InlineHyperlink;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import java.util.List;
-
-class FileComments extends Composite {
-  interface Binder extends UiBinder<HTMLPanel, FileComments> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  @UiField InlineHyperlink path;
-  @UiField FlowPanel comments;
-
-  FileComments(
-      CommentLinkProcessor clp,
-      Project.NameKey project,
-      PatchSet.Id defaultPs,
-      String title,
-      List<CommentInfo> list) {
-    initWidget(uiBinder.createAndBindUi(this));
-
-    path.setTargetHistoryToken(url(project, defaultPs, list.get(0)));
-    path.setText(title);
-    for (CommentInfo c : list) {
-      comments.add(new LineComment(clp, project, defaultPs, c));
-    }
-  }
-
-  private static String url(Project.NameKey project, PatchSet.Id ps, CommentInfo info) {
-    return Dispatcher.toPatch(project, null, ps, info.path());
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileComments.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileComments.ui.xml
deleted file mode 100644
index e463e95..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileComments.ui.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder
-    xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:c='urn:import:com.google.gerrit.client.ui'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:style gss='false'>
-    .box {
-    }
-    .path {
-      display: block;
-      white-space: nowrap;
-    }
-    .comments {
-      margin-left: 1em;
-    }
-  </ui:style>
-
-  <g:HTMLPanel styleName='{style.box}'>
-    <c:InlineHyperlink styleName='{style.path}' ui:field='path'/>
-    <g:FlowPanel styleName='{style.comments}' ui:field='comments'/>
-  </g:HTMLPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
deleted file mode 100644
index 30554b6..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
+++ /dev/null
@@ -1,940 +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.client.change;
-
-import static com.google.gerrit.client.FormatUtil.formatAbsBytes;
-import static com.google.gerrit.client.FormatUtil.formatAbsPercentage;
-import static com.google.gerrit.client.FormatUtil.formatBytes;
-import static com.google.gerrit.client.FormatUtil.formatPercentage;
-
-import com.google.gerrit.client.DiffObject;
-import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.VoidResult;
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeEditApi;
-import com.google.gerrit.client.changes.CommentInfo;
-import com.google.gerrit.client.changes.ReviewInfo;
-import com.google.gerrit.client.changes.Util;
-import com.google.gerrit.client.info.FileInfo;
-import com.google.gerrit.client.patches.PatchUtil;
-import com.google.gerrit.client.rpc.CallbackGroup;
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.client.ui.NavigationTable;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.extensions.client.Side;
-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.reviewdb.client.Project;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.core.client.JsArrayString;
-import com.google.gwt.core.client.Scheduler;
-import com.google.gwt.core.client.Scheduler.RepeatingCommand;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.InputElement;
-import com.google.gwt.dom.client.NativeEvent;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.resources.client.ClientBundle;
-import com.google.gwt.resources.client.CssResource;
-import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.Event;
-import com.google.gwt.user.client.EventListener;
-import com.google.gwt.user.client.Window;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
-import com.google.gwt.user.client.ui.ImageResourceRenderer;
-import com.google.gwt.user.client.ui.Label;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwt.user.client.ui.impl.HyperlinkImpl;
-import com.google.gwtexpui.globalkey.client.KeyCommand;
-import com.google.gwtexpui.progress.client.ProgressBar;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-import java.sql.Timestamp;
-
-public class FileTable extends FlowPanel {
-  private static final FileTableResources R = GWT.create(FileTableResources.class);
-
-  interface FileTableResources extends ClientBundle {
-    @Source("file_table.css")
-    FileTableCss css();
-  }
-
-  interface FileTableCss extends CssResource {
-    String table();
-
-    String nohover();
-
-    String pointer();
-
-    String reviewed();
-
-    String status();
-
-    String pathColumn();
-
-    String commonPrefix();
-
-    String renameCopySource();
-
-    String draftColumn();
-
-    String newColumn();
-
-    String commentColumn();
-
-    String deltaColumn1();
-
-    String deltaColumn2();
-
-    String inserted();
-
-    String deleted();
-
-    String restoreDelete();
-
-    String error();
-  }
-
-  public enum Mode {
-    REVIEW,
-    EDIT
-  }
-
-  private static final String DELETE;
-  private static final String RESTORE;
-  private static final String REVIEWED;
-  private static final String OPEN;
-  private static final int C_PATH = 3;
-  private static final HyperlinkImpl link = GWT.create(HyperlinkImpl.class);
-
-  static {
-    DELETE = DOM.createUniqueId().replace('-', '_');
-    RESTORE = DOM.createUniqueId().replace('-', '_');
-    REVIEWED = DOM.createUniqueId().replace('-', '_');
-    OPEN = DOM.createUniqueId().replace('-', '_');
-    init(DELETE, RESTORE, REVIEWED, OPEN);
-  }
-
-  private static native void init(String d, String t, String r, String o) /*-{
-    $wnd[d] = $entry(function(e,i) {
-      @com.google.gerrit.client.change.FileTable::onDelete(Lcom/google/gwt/dom/client/NativeEvent;I)(e,i)
-    });
-    $wnd[t] = $entry(function(e,i) {
-      @com.google.gerrit.client.change.FileTable::onRestore(Lcom/google/gwt/dom/client/NativeEvent;I)(e,i)
-    });
-    $wnd[r] = $entry(function(e,i) {
-      @com.google.gerrit.client.change.FileTable::onReviewed(Lcom/google/gwt/dom/client/NativeEvent;I)(e,i)
-    });
-    $wnd[o] = $entry(function(e,i) {
-      return @com.google.gerrit.client.change.FileTable::onOpen(Lcom/google/gwt/dom/client/NativeEvent;I)(e,i);
-    });
-  }-*/;
-
-  private static void onDelete(NativeEvent e, int idx) {
-    MyTable t = getMyTable(e);
-    if (t != null) {
-      t.onDelete(idx);
-    }
-  }
-
-  private static boolean onRestore(NativeEvent e, int idx) {
-    MyTable t = getMyTable(e);
-    if (t != null) {
-      t.onRestore(idx);
-      e.preventDefault();
-      e.stopPropagation();
-      return false;
-    }
-    return true;
-  }
-
-  private static void onReviewed(NativeEvent e, int idx) {
-    MyTable t = getMyTable(e);
-    if (t != null) {
-      t.onReviewed(InputElement.as(Element.as(e.getEventTarget())), idx);
-    }
-  }
-
-  private static boolean onOpen(NativeEvent e, int idx) {
-    if (link.handleAsClick(e.<Event>cast())) {
-      MyTable t = getMyTable(e);
-      if (t != null) {
-        t.onOpenRow(1 + idx);
-        e.preventDefault();
-        e.stopPropagation();
-        return false;
-      }
-    }
-    return true;
-  }
-
-  private static MyTable getMyTable(NativeEvent event) {
-    Element e = event.getEventTarget().cast();
-    for (e = DOM.getParent(e); e != null; e = DOM.getParent(e)) {
-      EventListener l = DOM.getEventListener(e);
-      if (l instanceof MyTable) {
-        return (MyTable) l;
-      }
-    }
-    return null;
-  }
-
-  private DiffObject base;
-  private PatchSet.Id curr;
-  private Project.NameKey project;
-  private MyTable table;
-  private boolean register;
-  private JsArrayString reviewed;
-  private String scrollToPath;
-  private ChangeScreen.Style style;
-  private Widget replyButton;
-  private boolean editExists;
-  private Mode mode;
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    R.css().ensureInjected();
-  }
-
-  public void set(
-      DiffObject base,
-      PatchSet.Id curr,
-      Project.NameKey project,
-      ChangeScreen.Style style,
-      Widget replyButton,
-      Mode mode,
-      boolean editExists) {
-    this.base = base;
-    this.curr = curr;
-    this.project = project;
-    this.style = style;
-    this.replyButton = replyButton;
-    this.mode = mode;
-    this.editExists = editExists;
-  }
-
-  void setValue(
-      NativeMap<FileInfo> fileMap,
-      Timestamp myLastReply,
-      @Nullable NativeMap<JsArray<CommentInfo>> comments,
-      @Nullable NativeMap<JsArray<CommentInfo>> drafts) {
-    JsArray<FileInfo> list = fileMap.values();
-    FileInfo.sortFileInfoByPath(list);
-
-    DisplayCommand cmd = new DisplayCommand(fileMap, list, myLastReply, comments, drafts);
-    if (cmd.execute()) {
-      cmd.showProgressBar();
-      Scheduler.get().scheduleIncremental(cmd);
-    }
-  }
-
-  void showError(Throwable t) {
-    clear();
-    Label l = new Label(Resources.M.failedToLoadFileList(t.getMessage()));
-    add(l);
-    l.setStyleName(R.css().error());
-  }
-
-  void markReviewed(JsArrayString reviewed) {
-    if (table != null) {
-      table.markReviewed(reviewed);
-    } else {
-      this.reviewed = reviewed;
-    }
-  }
-
-  void unregisterKeys() {
-    register = false;
-
-    if (table != null) {
-      table.setRegisterKeys(false);
-    }
-  }
-
-  void registerKeys() {
-    register = true;
-
-    if (table != null) {
-      table.setRegisterKeys(true);
-    }
-  }
-
-  void scrollToPath(String path) {
-    if (table != null) {
-      table.scrollToPath(path);
-    } else {
-      scrollToPath = path;
-    }
-  }
-
-  void openAll() {
-    if (table != null) {
-      String self = Gerrit.selfRedirect(null);
-      for (FileInfo info : Natives.asList(table.list)) {
-        if (canOpen(info.path())) {
-          Window.open(self + "#" + url(info), "_blank", null);
-        }
-      }
-    }
-  }
-
-  private boolean canOpen(String path) {
-    return mode != Mode.EDIT || !Patch.isMagic(path) || Patch.COMMIT_MSG.equals(path);
-  }
-
-  private void setTable(MyTable table) {
-    clear();
-    add(table);
-    this.table = table;
-
-    if (register) {
-      table.setRegisterKeys(true);
-    }
-    if (reviewed != null) {
-      table.markReviewed(reviewed);
-      reviewed = null;
-    }
-    if (scrollToPath != null) {
-      table.scrollToPath(scrollToPath);
-      scrollToPath = null;
-    }
-  }
-
-  private String url(FileInfo info) {
-    return info.binary()
-        ? Dispatcher.toUnified(project, base, curr, info.path())
-        : mode == Mode.REVIEW
-            ? Dispatcher.toPatch(project, base, curr, info.path())
-            : Dispatcher.toEditScreen(project, curr, info.path());
-  }
-
-  private final class MyTable extends NavigationTable<FileInfo> {
-    private final NativeMap<FileInfo> map;
-    private final JsArray<FileInfo> list;
-
-    MyTable(NativeMap<FileInfo> map, JsArray<FileInfo> list) {
-      this.map = map;
-      this.list = list;
-      table.setWidth("");
-
-      keysNavigation.add(
-          new PrevKeyCommand(0, 'k', Util.C.patchTablePrev()),
-          new NextKeyCommand(0, 'j', Util.C.patchTableNext()));
-      keysNavigation.add(new OpenKeyCommand(0, 'o', Util.C.patchTableOpenDiff()));
-      keysNavigation.add(new OpenKeyCommand(0, KeyCodes.KEY_ENTER, Util.C.patchTableOpenDiff()));
-
-      keysNavigation.add(
-          new OpenFileCommand(list.length() - 1, 0, '[', Resources.C.openLastFile()),
-          new OpenFileCommand(0, 0, ']', Resources.C.openCommitMessage()));
-
-      keysAction.add(
-          new KeyCommand(0, 'r', PatchUtil.C.toggleReviewed()) {
-            @Override
-            public void onKeyPress(KeyPressEvent event) {
-              int row = getCurrentRow();
-              if (1 <= row && row <= MyTable.this.list.length()) {
-                FileInfo info = MyTable.this.list.get(row - 1);
-                InputElement b = getReviewed(info);
-                boolean c = !b.isChecked();
-                setReviewed(info, c);
-                b.setChecked(c);
-              }
-            }
-          });
-
-      setSavePointerId((!base.isBase() ? base.asString() + ".." : "") + curr.toString());
-    }
-
-    void onDelete(int idx) {
-      String path = list.get(idx).path();
-      ChangeEditApi.delete(
-          project.get(),
-          curr.getParentKey().get(),
-          path,
-          new AsyncCallback<VoidResult>() {
-            @Override
-            public void onSuccess(VoidResult result) {
-              Gerrit.display(PageLinks.toChangeInEditMode(project, curr.getParentKey()));
-            }
-
-            @Override
-            public void onFailure(Throwable caught) {}
-          });
-    }
-
-    void onRestore(int idx) {
-      String path = list.get(idx).path();
-      ChangeEditApi.restore(
-          project.get(),
-          curr.getParentKey().get(),
-          path,
-          new AsyncCallback<VoidResult>() {
-            @Override
-            public void onSuccess(VoidResult result) {
-              Gerrit.display(PageLinks.toChangeInEditMode(project, curr.getParentKey()));
-            }
-
-            @Override
-            public void onFailure(Throwable caught) {}
-          });
-    }
-
-    void onReviewed(InputElement checkbox, int idx) {
-      setReviewed(list.get(idx), checkbox.isChecked());
-    }
-
-    private void setReviewed(FileInfo info, boolean r) {
-      RestApi api =
-          ChangeApi.revision(project.get(), curr).view("files").id(info.path()).view("reviewed");
-      if (r) {
-        api.put(CallbackGroup.<ReviewInfo>emptyCallback());
-      } else {
-        api.delete(CallbackGroup.<ReviewInfo>emptyCallback());
-      }
-    }
-
-    void markReviewed(JsArrayString reviewed) {
-      for (int i = 0; i < reviewed.length(); i++) {
-        FileInfo info = map.get(reviewed.get(i));
-        if (info != null) {
-          getReviewed(info).setChecked(true);
-        }
-      }
-    }
-
-    private InputElement getReviewed(FileInfo info) {
-      CellFormatter fmt = table.getCellFormatter();
-      Element e = fmt.getElement(1 + info._row(), 1);
-      return InputElement.as(e.getFirstChildElement());
-    }
-
-    void scrollToPath(String path) {
-      FileInfo info = map.get(path);
-      if (info != null) {
-        movePointerTo(1 + info._row(), true);
-      }
-    }
-
-    @Override
-    protected Object getRowItemKey(FileInfo item) {
-      return item.path();
-    }
-
-    @Override
-    protected int findRow(Object id) {
-      FileInfo info = map.get((String) id);
-      return info != null ? 1 + info._row() : -1;
-    }
-
-    @Override
-    protected FileInfo getRowItem(int row) {
-      if (1 <= row && row <= list.length()) {
-        return list.get(row - 1);
-      }
-      return null;
-    }
-
-    @Override
-    protected void onOpenRow(int row) {
-      if (1 <= row && row <= list.length()) {
-        FileInfo info = list.get(row - 1);
-        if (canOpen(info.path())) {
-          Gerrit.display(url(info));
-        }
-      }
-    }
-
-    @Override
-    protected void onCellSingleClick(Event event, int row, int column) {
-      if (column == C_PATH && link.handleAsClick(event)) {
-        onOpenRow(row);
-      } else {
-        super.onCellSingleClick(event, row, column);
-      }
-    }
-
-    private class OpenFileCommand extends KeyCommand {
-      private final int index;
-
-      OpenFileCommand(int index, int modifiers, char c, String helpText) {
-        super(modifiers, c, helpText);
-        this.index = index;
-      }
-
-      @Override
-      public void onKeyPress(KeyPressEvent event) {
-        FileInfo info = list.get(index);
-        if (canOpen(info.path())) {
-          Gerrit.display(url(info));
-        }
-      }
-    }
-  }
-
-  private final class DisplayCommand implements RepeatingCommand {
-    private final SafeHtmlBuilder sb = new SafeHtmlBuilder();
-    private final MyTable myTable;
-    private final JsArray<FileInfo> list;
-    private final Timestamp myLastReply;
-    private final NativeMap<JsArray<CommentInfo>> comments;
-    private final NativeMap<JsArray<CommentInfo>> drafts;
-    private final boolean hasUser;
-    private final boolean showChangeSizeBars;
-    private boolean attached;
-    private int row;
-    private double start;
-    private ProgressBar meter;
-    private String lastPath = "";
-
-    private boolean hasBinaryFile;
-    private boolean hasNonBinaryFile;
-    private int inserted;
-    private int deleted;
-    private long binOldSize;
-    private long bytesInserted;
-    private long bytesDeleted;
-
-    private DisplayCommand(
-        NativeMap<FileInfo> map,
-        JsArray<FileInfo> list,
-        Timestamp myLastReply,
-        @Nullable NativeMap<JsArray<CommentInfo>> comments,
-        @Nullable NativeMap<JsArray<CommentInfo>> drafts) {
-      this.myTable = new MyTable(map, list);
-      this.list = list;
-      this.myLastReply = myLastReply;
-      this.comments = comments;
-      this.drafts = drafts;
-      this.hasUser = Gerrit.isSignedIn();
-      this.showChangeSizeBars = Gerrit.getUserPreferences().sizeBarInChangeTable();
-      myTable.addStyleName(R.css().table());
-    }
-
-    @Override
-    public boolean execute() {
-      boolean attachedNow = isAttached();
-      if (!attached && attachedNow) {
-        // Remember that we have been attached at least once. If
-        // later we find we aren't attached we should stop running.
-        attached = true;
-      } else if (attached && !attachedNow) {
-        // If the user navigated away, we aren't in the DOM anymore.
-        // Don't continue to render.
-        return false;
-      }
-
-      start = System.currentTimeMillis();
-      if (row == 0) {
-        header(sb);
-        computeInsertedDeleted();
-      }
-      while (row < list.length()) {
-        FileInfo info = list.get(row);
-        info._row(row);
-        render(sb, info);
-        if ((++row % 10) == 0 && longRunning()) {
-          updateMeter();
-          return true;
-        }
-      }
-      footer(sb);
-      myTable.resetHtml(sb);
-      myTable.finishDisplay();
-      setTable(myTable);
-      return false;
-    }
-
-    private void computeInsertedDeleted() {
-      inserted = 0;
-      deleted = 0;
-      binOldSize = 0;
-      bytesInserted = 0;
-      bytesDeleted = 0;
-      for (int i = 0; i < list.length(); i++) {
-        FileInfo info = list.get(i);
-        if (!Patch.isMagic(info.path())) {
-          if (!info.binary()) {
-            hasNonBinaryFile = true;
-            inserted += info.linesInserted();
-            deleted += info.linesDeleted();
-          } else {
-            hasBinaryFile = true;
-            binOldSize += info.size() - info.sizeDelta();
-            if (info.sizeDelta() >= 0) {
-              bytesInserted += info.sizeDelta();
-            } else {
-              bytesDeleted += info.sizeDelta();
-            }
-          }
-        }
-      }
-    }
-
-    void showProgressBar() {
-      if (meter == null) {
-        meter = new ProgressBar(Util.M.loadingPatchSet(curr.get()));
-        FileTable.this.clear();
-        FileTable.this.add(meter);
-      }
-      updateMeter();
-    }
-
-    void updateMeter() {
-      if (meter != null) {
-        int n = list.length();
-        meter.setValue((100 * row) / n);
-      }
-    }
-
-    private boolean longRunning() {
-      return System.currentTimeMillis() - start > 200;
-    }
-
-    private void header(SafeHtmlBuilder sb) {
-      sb.openTr().setStyleName(R.css().nohover());
-      sb.openTh().setStyleName(R.css().pointer()).closeTh();
-      if (mode == Mode.REVIEW) {
-        sb.openTh().setStyleName(R.css().reviewed()).closeTh();
-      } else {
-        sb.openTh().setStyleName(R.css().restoreDelete()).closeTh();
-      }
-      sb.openTh().setStyleName(R.css().status()).closeTh();
-      sb.openTh().append(Util.C.patchTableColumnName()).closeTh();
-      sb.openTh().setAttribute("colspan", 3).append(Util.C.patchTableColumnComments()).closeTh();
-      sb.openTh().setAttribute("colspan", 2).append(Util.C.patchTableColumnSize()).closeTh();
-      sb.closeTr();
-    }
-
-    private void render(SafeHtmlBuilder sb, FileInfo info) {
-      sb.openTr();
-      sb.openTd().setStyleName(R.css().pointer()).closeTd();
-      if (mode == Mode.REVIEW) {
-        columnReviewed(sb, info);
-      } else {
-        columnDeleteRestore(sb, info);
-      }
-      columnStatus(sb, info);
-      columnPath(sb, info);
-      columnComments(sb, info);
-      columnDelta1(sb, info);
-      columnDelta2(sb, info);
-      sb.closeTr();
-    }
-
-    private void columnReviewed(SafeHtmlBuilder sb, FileInfo info) {
-      sb.openTd().setStyleName(R.css().reviewed());
-      if (hasUser) {
-        sb.openElement("input")
-            .setAttribute("title", Resources.C.reviewedFileTitle())
-            .setAttribute("type", "checkbox")
-            .setAttribute("onclick", REVIEWED + "(event," + info._row() + ")")
-            .closeSelf();
-      }
-      sb.closeTd();
-    }
-
-    private void columnDeleteRestore(SafeHtmlBuilder sb, FileInfo info) {
-      sb.openTd().setStyleName(R.css().restoreDelete());
-      if (hasUser) {
-        if (!Patch.isMagic(info.path())) {
-          boolean editable = isEditable(info);
-          sb.openDiv()
-              .openElement("button")
-              .setAttribute("title", Resources.C.restoreFileInline())
-              .setAttribute("onclick", RESTORE + "(event," + info._row() + ")")
-              .append(new ImageResourceRenderer().render(Gerrit.RESOURCES.editUndo()))
-              .closeElement("button");
-          if (editable) {
-            sb.openElement("button")
-                .setAttribute("title", Resources.C.removeFileInline())
-                .setAttribute("onclick", DELETE + "(event," + info._row() + ")")
-                .append(new ImageResourceRenderer().render(Gerrit.RESOURCES.redNot()))
-                .closeElement("button");
-          }
-          sb.closeDiv();
-        }
-      }
-      sb.closeTd();
-    }
-
-    private boolean isEditable(FileInfo info) {
-      String status = info.status();
-      return status == null || !ChangeType.DELETED.matches(status);
-    }
-
-    private void columnStatus(SafeHtmlBuilder sb, FileInfo info) {
-      sb.openTd().setStyleName(R.css().status());
-      if (!Patch.isMagic(info.path())
-          && info.status() != null
-          && !ChangeType.MODIFIED.matches(info.status())) {
-        sb.append(info.status());
-      }
-      sb.closeTd();
-    }
-
-    private void columnPath(SafeHtmlBuilder sb, FileInfo info) {
-      String path = info.path();
-
-      sb.openTd().setStyleName(R.css().pathColumn());
-
-      if (!canOpen(path)) {
-        sb.openDiv();
-        appendPath(path);
-        sb.closeDiv();
-        sb.closeTd();
-        return;
-      }
-
-      sb.openAnchor();
-
-      if (mode == Mode.EDIT && !isEditable(info)) {
-        sb.setAttribute("onclick", RESTORE + "(event," + info._row() + ")");
-      } else {
-        sb.setAttribute("href", "#" + url(info))
-            .setAttribute("onclick", OPEN + "(event," + info._row() + ")");
-      }
-      appendPath(path);
-      sb.closeAnchor();
-      if (info.oldPath() != null) {
-        sb.br();
-        sb.openSpan().setStyleName(R.css().renameCopySource()).append(info.oldPath()).closeSpan();
-      }
-      sb.closeTd();
-    }
-
-    private void appendPath(String path) {
-      if (Patch.COMMIT_MSG.equals(path)) {
-        sb.append(Util.C.commitMessage());
-      } else if (Patch.MERGE_LIST.equals(path)) {
-        sb.append(Util.C.mergeList());
-      } else if (Gerrit.getUserPreferences().muteCommonPathPrefixes()) {
-        int commonPrefixLen = commonPrefix(path);
-        if (commonPrefixLen > 0) {
-          sb.openSpan()
-              .setStyleName(R.css().commonPrefix())
-              .append(path.substring(0, commonPrefixLen))
-              .closeSpan();
-        }
-        sb.append(path.substring(commonPrefixLen));
-        lastPath = path;
-      } else {
-        sb.append(path);
-      }
-    }
-
-    private int commonPrefix(String path) {
-      for (int n = path.length(); n > 0; ) {
-        int s = path.lastIndexOf('/', n);
-        if (s < 0) {
-          return 0;
-        }
-
-        String p = path.substring(0, s + 1);
-        if (lastPath.startsWith(p)) {
-          return s + 1;
-        }
-        n = s - 1;
-      }
-      return 0;
-    }
-
-    private void columnComments(SafeHtmlBuilder sb, FileInfo info) {
-      JsArray<CommentInfo> cList = filterForParent(get(info.path(), comments));
-      JsArray<CommentInfo> dList = filterForParent(get(info.path(), drafts));
-
-      sb.openTd().setStyleName(R.css().draftColumn());
-      if (dList.length() > 0) {
-        sb.append("drafts: ").append(dList.length());
-      }
-      sb.closeTd();
-
-      int cntAll = cList.length();
-      int cntNew = 0;
-      if (myLastReply != null) {
-        for (int i = cntAll - 1; i >= 0; i--) {
-          CommentInfo m = cList.get(i);
-          if (m.updated().compareTo(myLastReply) > 0) {
-            cntNew++;
-          } else {
-            break;
-          }
-        }
-      }
-
-      sb.openTd().setStyleName(R.css().newColumn());
-      if (cntNew > 0) {
-        sb.append("new: ").append(cntNew);
-      }
-      sb.closeTd();
-
-      sb.openTd().setStyleName(R.css().commentColumn());
-      if (cntAll - cntNew > 0) {
-        sb.append("comments: ").append(cntAll - cntNew);
-      }
-      sb.closeTd();
-    }
-
-    private JsArray<CommentInfo> filterForParent(JsArray<CommentInfo> list) {
-      JsArray<CommentInfo> result = JsArray.createArray().cast();
-      for (CommentInfo c : Natives.asList(list)) {
-        if (c.side() == Side.REVISION) {
-          result.push(c);
-        } else if (base.isBaseOrAutoMerge() && !c.hasParent()) {
-          result.push(c);
-        } else if (base.isParent() && c.parent() == base.getParentNum()) {
-          result.push(c);
-        }
-      }
-      return result;
-    }
-
-    private JsArray<CommentInfo> get(String p, NativeMap<JsArray<CommentInfo>> m) {
-      JsArray<CommentInfo> r = null;
-      if (m != null) {
-        r = m.get(p);
-      }
-      if (r == null) {
-        r = JsArray.createArray().cast();
-      }
-      return r;
-    }
-
-    private void columnDelta1(SafeHtmlBuilder sb, FileInfo info) {
-      sb.openTd().setStyleName(R.css().deltaColumn1());
-      if (!Patch.isMagic(info.path()) && !info.binary()) {
-        if (showChangeSizeBars) {
-          sb.append(info.linesInserted() + info.linesDeleted());
-        } else if (!ChangeType.DELETED.matches(info.status())) {
-          if (ChangeType.ADDED.matches(info.status())) {
-            sb.append(info.linesInserted()).append(" lines");
-          } else {
-            sb.append("+").append(info.linesInserted()).append(", -").append(info.linesDeleted());
-          }
-        }
-      } else if (info.binary()) {
-        sb.append(formatBytes(info.sizeDelta()));
-        long oldSize = info.size() - info.sizeDelta();
-        if (oldSize != 0) {
-          sb.append(" (").append(formatPercentage(oldSize, info.sizeDelta())).append(")");
-        }
-      }
-      sb.closeTd();
-    }
-
-    private void columnDelta2(SafeHtmlBuilder sb, FileInfo info) {
-      sb.openTd().setStyleName(R.css().deltaColumn2());
-      if (showChangeSizeBars
-          && !Patch.isMagic(info.path())
-          && !info.binary()
-          && (info.linesInserted() != 0 || info.linesDeleted() != 0)) {
-        int w = 80;
-        int t = inserted + deleted;
-        int i = Math.max(5, (int) (((double) w) * info.linesInserted() / t));
-        int d = Math.max(5, (int) (((double) w) * info.linesDeleted() / t));
-
-        sb.setAttribute(
-            "title", Util.M.patchTableSize_LongModify(info.linesInserted(), info.linesDeleted()));
-
-        if (0 < info.linesInserted()) {
-          sb.openDiv()
-              .setStyleName(R.css().inserted())
-              .setAttribute("style", "width:" + i + "px")
-              .closeDiv();
-        }
-        if (0 < info.linesDeleted()) {
-          sb.openDiv()
-              .setStyleName(R.css().deleted())
-              .setAttribute("style", "width:" + d + "px")
-              .closeDiv();
-        }
-      }
-      sb.closeTd();
-    }
-
-    private void footer(SafeHtmlBuilder sb) {
-      sb.openTr().setStyleName(R.css().nohover());
-      sb.openTh().setStyleName(R.css().pointer()).closeTh();
-      if (mode == Mode.REVIEW) {
-        sb.openTh().setStyleName(R.css().reviewed()).closeTh();
-      } else {
-        sb.openTh().setStyleName(R.css().restoreDelete()).closeTh();
-      }
-      sb.openTh().setStyleName(R.css().status()).closeTh();
-      sb.openTd().closeTd(); // path
-      sb.openTd().setAttribute("colspan", 3).closeTd(); // comments
-
-      // delta1
-      sb.openTh().setStyleName(R.css().deltaColumn1());
-      if (hasNonBinaryFile) {
-        sb.append(Util.M.patchTableSize_Modify(inserted, deleted));
-      }
-      if (hasBinaryFile) {
-        if (hasNonBinaryFile) {
-          sb.br();
-        }
-        if (binOldSize != 0) {
-          sb.append(
-              Util.M.patchTableSize_ModifyBinaryFilesWithPercentages(
-                  formatAbsBytes(bytesInserted),
-                  formatAbsPercentage(binOldSize, bytesInserted),
-                  formatAbsBytes(bytesDeleted),
-                  formatAbsPercentage(binOldSize, bytesDeleted)));
-        } else {
-          sb.append(
-              Util.M.patchTableSize_ModifyBinaryFiles(
-                  formatAbsBytes(bytesInserted), formatAbsBytes(bytesDeleted)));
-        }
-      }
-      sb.closeTh();
-
-      // delta2
-      sb.openTh().setStyleName(R.css().deltaColumn2());
-      if (showChangeSizeBars) {
-        int w = 80;
-        int t = inserted + deleted;
-        int i = Math.max(1, (int) (((double) w) * inserted / t));
-        int d = Math.max(1, (int) (((double) w) * deleted / t));
-        if (i + d > w && i > d) {
-          i = w - d;
-        } else if (i + d > w && d > i) {
-          d = w - i;
-        }
-        if (0 < inserted) {
-          sb.openDiv()
-              .setStyleName(R.css().inserted())
-              .setAttribute("style", "width:" + i + "px")
-              .closeDiv();
-        }
-        if (0 < deleted) {
-          sb.openDiv()
-              .setStyleName(R.css().deleted())
-              .setAttribute("style", "width:" + d + "px")
-              .closeDiv();
-        }
-      }
-      sb.closeTh();
-
-      sb.closeTr();
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FollowUpAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FollowUpAction.java
deleted file mode 100644
index a4c90b8..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FollowUpAction.java
+++ /dev/null
@@ -1,54 +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.client.change;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.common.PageLinks;
-import com.google.gwt.user.client.ui.Button;
-
-class FollowUpAction extends ActionMessageBox {
-  private final String project;
-  private final String branch;
-  private final String topic;
-  private final String base;
-
-  FollowUpAction(Button b, String project, String branch, String topic, String key) {
-    super(b);
-    this.project = project;
-    this.branch = branch;
-    this.topic = topic;
-    this.base = project + "~" + branch + "~" + key;
-  }
-
-  @Override
-  void send(String message) {
-    ChangeApi.createChange(
-        project,
-        branch,
-        topic,
-        message,
-        base,
-        new GerritCallback<ChangeInfo>() {
-          @Override
-          public void onSuccess(ChangeInfo result) {
-            Gerrit.display(PageLinks.toChange(result.projectNameKey(), result.legacyId()));
-            hide();
-          }
-        });
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.java
deleted file mode 100644
index 1044828..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.java
+++ /dev/null
@@ -1,272 +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.client.change;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArrayString;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.NativeEvent;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyDownEvent;
-import com.google.gwt.event.dom.client.KeyDownHandler;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.uibinder.client.UiHandler;
-import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.rpc.StatusCodeException;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwt.user.client.ui.ImageResourceRenderer;
-import com.google.gwt.user.client.ui.UIObject;
-import com.google.gwtexpui.globalkey.client.NpTextBox;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-import java.util.Iterator;
-
-public class Hashtags extends Composite {
-
-  interface Binder extends UiBinder<HTMLPanel, Hashtags> {}
-
-  private static final int VISIBLE_LENGTH = 55;
-  private static final Binder uiBinder = GWT.create(Binder.class);
-  private static final String REMOVE;
-  private static final String DATA_ID = "data-id";
-
-  private PatchSet.Id psId;
-  private boolean canEdit;
-
-  static {
-    REMOVE = DOM.createUniqueId().replace('-', '_');
-    init(REMOVE);
-  }
-
-  private static native void init(String r) /*-{
-    $wnd[r] = $entry(function(e) {
-      @com.google.gerrit.client.change.Hashtags::onRemove(Lcom/google/gwt/dom/client/NativeEvent;)(e)
-    });
-  }-*/;
-
-  private static void onRemove(NativeEvent event) {
-    String hashtags = getDataId(event);
-    if (hashtags != null) {
-      final ChangeScreen screen = ChangeScreen.get(event);
-      final PatchSet.Id psId = screen.getPatchSetId();
-      ChangeApi.hashtags(screen.getProject().get(), psId.getParentKey().get())
-          .post(
-              PostInput.create(null, hashtags),
-              new GerritCallback<JavaScriptObject>() {
-                @Override
-                public void onSuccess(JavaScriptObject result) {
-                  if (screen.isCurrentView()) {
-                    Gerrit.display(PageLinks.toChange(screen.getProject(), psId));
-                  }
-                }
-              });
-    }
-  }
-
-  private static String getDataId(NativeEvent event) {
-    Element e = event.getEventTarget().cast();
-    while (e != null) {
-      String v = e.getAttribute(DATA_ID);
-      if (!v.isEmpty()) {
-        return v;
-      }
-      e = e.getParentElement();
-    }
-    return null;
-  }
-
-  @UiField Element hashtagsText;
-  @UiField Image addHashtagIcon;
-  @UiField Element form;
-  @UiField Element error;
-  @UiField NpTextBox hashtagTextBox;
-
-  private ChangeScreen.Style style;
-  private Change.Id changeId;
-  private Project.NameKey project;
-
-  public Hashtags() {
-
-    initWidget(uiBinder.createAndBindUi(this));
-
-    hashtagTextBox.setVisibleLength(VISIBLE_LENGTH);
-    hashtagTextBox.addKeyDownHandler(
-        new KeyDownHandler() {
-          @Override
-          public void onKeyDown(KeyDownEvent e) {
-            if (e.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) {
-              onCancel(null);
-            } else if (e.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
-              onAdd(null);
-            }
-          }
-        });
-
-    addHashtagIcon.addDomHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            onOpenForm();
-          }
-        },
-        ClickEvent.getType());
-  }
-
-  void init(ChangeScreen.Style style) {
-    this.style = style;
-  }
-
-  void set(ChangeInfo info, String revision) {
-    psId = new PatchSet.Id(info.legacyId(), info.revisions().get(revision)._number());
-    project = info.projectNameKey();
-
-    canEdit = info.hasActions() && info.actions().containsKey("hashtags");
-    this.changeId = info.legacyId();
-    display(info);
-    addHashtagIcon.setVisible(canEdit);
-  }
-
-  void onOpenForm() {
-    UIObject.setVisible(form, true);
-    UIObject.setVisible(error, false);
-    addHashtagIcon.setVisible(false);
-    hashtagTextBox.setFocus(true);
-  }
-
-  private void display(ChangeInfo info) {
-    hashtagsText.setInnerSafeHtml(formatHashtags(info));
-  }
-
-  private void display(JsArrayString hashtags) {
-    hashtagsText.setInnerSafeHtml(formatHashtags(hashtags));
-  }
-
-  private SafeHtmlBuilder formatHashtags(ChangeInfo info) {
-    if (info.hashtags() != null) {
-      return formatHashtags(info.hashtags());
-    }
-    return new SafeHtmlBuilder();
-  }
-
-  private SafeHtmlBuilder formatHashtags(JsArrayString hashtags) {
-    SafeHtmlBuilder html = new SafeHtmlBuilder();
-    Iterator<String> itr = Natives.asList(hashtags).iterator();
-    while (itr.hasNext()) {
-      String hashtagName = itr.next();
-      html.openSpan()
-          .setAttribute(DATA_ID, hashtagName)
-          .setStyleName(style.hashtagName())
-          .openAnchor()
-          .setAttribute("href", "#" + PageLinks.toChangeQuery("hashtag:\"" + hashtagName + "\""))
-          .setAttribute("role", "listitem")
-          .openSpan()
-          .setStyleName(style.hashtagIcon())
-          .append(new ImageResourceRenderer().render(Gerrit.RESOURCES.hashtag()))
-          .closeSpan()
-          .append(" ")
-          .append(hashtagName)
-          .closeAnchor();
-      if (canEdit) {
-        html.openElement("button")
-            .setAttribute("title", "Remove hashtag")
-            .setAttribute("onclick", REMOVE + "(event)")
-            .append("×")
-            .closeElement("button");
-      }
-      html.closeSpan();
-      if (itr.hasNext()) {
-        html.append(' ');
-      }
-    }
-    return html;
-  }
-
-  @UiHandler("cancel")
-  void onCancel(@SuppressWarnings("unused") ClickEvent e) {
-    addHashtagIcon.setVisible(true);
-    UIObject.setVisible(form, false);
-    hashtagTextBox.setFocus(false);
-  }
-
-  @UiHandler("add")
-  void onAdd(@SuppressWarnings("unused") ClickEvent e) {
-    String hashtag = hashtagTextBox.getText();
-    if (!hashtag.isEmpty()) {
-      addHashtag(hashtag);
-    }
-  }
-
-  private void addHashtag(String hashtags) {
-    ChangeApi.hashtags(project.get(), changeId.get())
-        .post(
-            PostInput.create(hashtags, null),
-            new GerritCallback<JsArrayString>() {
-              @Override
-              public void onSuccess(JsArrayString result) {
-                Gerrit.display(
-                    PageLinks.toChange(project, psId.getParentKey(), String.valueOf(psId.get())));
-              }
-
-              @Override
-              public void onFailure(Throwable err) {
-                UIObject.setVisible(error, true);
-                error.setInnerText(
-                    err instanceof StatusCodeException
-                        ? ((StatusCodeException) err).getEncodedResponse()
-                        : err.getMessage());
-                hashtagTextBox.setEnabled(true);
-              }
-            });
-  }
-
-  public static class PostInput extends JavaScriptObject {
-    public static PostInput create(String add, String remove) {
-      PostInput input = createObject().cast();
-      input.init(toJsArrayString(add), toJsArrayString(remove));
-      return input;
-    }
-
-    private static JsArrayString toJsArrayString(String commaSeparated) {
-      if (commaSeparated == null || commaSeparated.equals("")) {
-        return null;
-      }
-      JsArrayString array = JsArrayString.createArray().cast();
-      for (String hashtag : commaSeparated.split(",")) {
-        array.push(hashtag.trim());
-      }
-      return array;
-    }
-
-    private native void init(JsArrayString add, JsArrayString remove) /*-{
-      this.add = add;
-      this.remove = remove;
-    }-*/;
-
-    protected PostInput() {}
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.ui.xml
deleted file mode 100644
index c0bfd1c..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.ui.xml
+++ /dev/null
@@ -1,82 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder
-    xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:with field='ico' type='com.google.gerrit.client.GerritResources'/>
-  <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
-  <ui:style gss='false'>
-    button.openAdd {
-      margin: 3px 3px 0 0;
-      float: right;
-      color: rgba(0, 0, 0, 0.15);
-      background-color: #f5f5f5;
-      background-image: -webkit-linear-gradient(top, #f5f5f5, #f1f1f1);
-      -webkit-border-radius: 2px;
-      -moz-border-radius: 2px;
-      border-radius: 2px;
-      -webkit-box-sizing: content-box;
-      -moz-box-sizing: content-box;
-      box-sizing: content-box;
-    }
-    button.openAdd div {
-      width: auto;
-      color: #444;
-    }
-
-    .hashtagTextBox {
-      margin-bottom: 2px;
-    }
-
-    .error {
-      color: #D33D3D;
-      font-weight: bold;
-    }
-
-    .addHashtag,
-    .cancel {
-      cursor: pointer;
-      float: right;
-    }
-  </ui:style>
-  <g:HTMLPanel>
-    <div>
-      <span ui:field='hashtagsText'/>
-      <g:Image ui:field='addHashtagIcon'
-        resource='{ico.addHashtag}'
-        styleName='{style.addHashtag}'
-        title='Add Hashtag'/>
-    </div>
-    <div ui:field='form' style='display: none' aria-hidden='true'>
-      <c:NpTextBox ui:field='hashtagTextBox' styleName='{style.hashtagTextBox}'/>
-      <div ui:field='error'
-           class='{style.error}'
-           style='display: none' aria-hidden='true'/>
-      <div>
-        <g:Button ui:field='add' styleName='{res.style.button}'>
-          <div>Add</div>
-        </g:Button>
-        <g:Button ui:field='cancel'
-            styleName='{res.style.button}'
-            addStyleNames='{style.cancel}'>
-          <div>Cancel</div>
-        </g:Button>
-      </div>
-    </div>
-   </g:HTMLPanel>
-  </ui:UiBinder>
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/History.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/History.java
deleted file mode 100644
index 55e021f..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/History.java
+++ /dev/null
@@ -1,141 +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.client.change;
-
-import com.google.gerrit.client.changes.CommentInfo;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.info.ChangeInfo.MessageInfo;
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.ui.CommentLinkProcessor;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.Widget;
-import java.sql.Timestamp;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-class History extends FlowPanel {
-  private CommentLinkProcessor clp;
-  private ReplyAction replyAction;
-  private Change.Id changeId;
-  private Project.NameKey project;
-
-  private final Map<Integer, List<CommentInfo>> byAuthor = new HashMap<>();
-
-  void set(CommentLinkProcessor clp, ReplyAction ra, Change.Id id, ChangeInfo info) {
-    this.clp = clp;
-    this.replyAction = ra;
-    this.changeId = id;
-    this.project = info.projectNameKey();
-
-    JsArray<MessageInfo> messages = info.messages();
-    if (messages != null) {
-      for (MessageInfo msg : Natives.asList(messages)) {
-        Message ui = new Message(this, msg);
-        ui.addComments(comments(msg));
-        add(ui);
-      }
-      autoOpen(ChangeScreen.myLastReply(info));
-    }
-  }
-
-  private void autoOpen(Timestamp lastReply) {
-    if (lastReply == null) {
-      for (Widget child : getChildren()) {
-        ((Message) child).autoOpen();
-      }
-    } else {
-      for (int i = getChildren().size() - 1; i >= 0; i--) {
-        Message ui = (Message) getChildren().get(i);
-        MessageInfo msg = ui.getMessageInfo();
-        if (lastReply.compareTo(msg.date()) < 0) {
-          ui.autoOpen();
-        } else {
-          break;
-        }
-      }
-    }
-  }
-
-  CommentLinkProcessor getCommentLinkProcessor() {
-    return clp;
-  }
-
-  Change.Id getChangeId() {
-    return changeId;
-  }
-
-  Project.NameKey getProject() {
-    return project;
-  }
-
-  void replyTo(MessageInfo info) {
-    replyAction.onReply(info);
-  }
-
-  void addComments(NativeMap<JsArray<CommentInfo>> map) {
-    for (String path : map.keySet()) {
-      for (CommentInfo c : Natives.asList(map.get(path))) {
-        c.path(path);
-        if (c.author() != null) {
-          int authorId = c.author()._accountId();
-          List<CommentInfo> l = byAuthor.get(authorId);
-          if (l == null) {
-            l = new ArrayList<>();
-            byAuthor.put(authorId, l);
-          }
-          l.add(c);
-        }
-      }
-    }
-  }
-
-  private List<CommentInfo> comments(MessageInfo msg) {
-    if (msg.author() == null) {
-      return Collections.emptyList();
-    }
-
-    int authorId = msg.author()._accountId();
-    List<CommentInfo> list = byAuthor.get(authorId);
-    if (list == null) {
-      return Collections.emptyList();
-    }
-
-    Timestamp when = msg.date();
-    List<CommentInfo> match = new ArrayList<>();
-    List<CommentInfo> other = new ArrayList<>();
-    for (CommentInfo c : list) {
-      if (c.updated().compareTo(when) <= 0) {
-        match.add(c);
-      } else {
-        other.add(c);
-      }
-    }
-    if (match.isEmpty()) {
-      return Collections.emptyList();
-    } else if (other.isEmpty()) {
-      byAuthor.remove(authorId);
-    } else {
-      byAuthor.put(authorId, other);
-    }
-    return match;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInAction.java
deleted file mode 100644
index 5557f90..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInAction.java
+++ /dev/null
@@ -1,39 +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.client.change;
-
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.user.client.ui.UIObject;
-import com.google.gwt.user.client.ui.Widget;
-
-class IncludedInAction extends RightSidePopdownAction {
-  private final IncludedInBox includedInBox;
-
-  IncludedInAction(
-      Project.NameKey project,
-      Change.Id changeId,
-      ChangeScreen.Style style,
-      UIObject relativeTo,
-      Widget includedInButton) {
-    super(style, relativeTo, includedInButton);
-    this.includedInBox = new IncludedInBox(project, changeId);
-  }
-
-  @Override
-  Widget getWidget() {
-    return includedInBox;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInBox.java
deleted file mode 100644
index 9751f54..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInBox.java
+++ /dev/null
@@ -1,106 +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.client.change;
-
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.info.ChangeInfo.IncludedInInfo;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.core.client.JsArrayString;
-import com.google.gwt.dom.client.Document;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.TableCellElement;
-import com.google.gwt.dom.client.TableElement;
-import com.google.gwt.dom.client.TableRowElement;
-import com.google.gwt.resources.client.CssResource;
-import com.google.gwt.safehtml.shared.SafeHtml;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-
-class IncludedInBox extends Composite {
-  interface Binder extends UiBinder<HTMLPanel, IncludedInBox> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  interface Style extends CssResource {
-    String includedInElement();
-  }
-
-  private final Project.NameKey project;
-  private final Change.Id changeId;
-  private boolean loaded;
-
-  @UiField Style style;
-  @UiField TableElement table;
-  @UiField Element branches;
-  @UiField Element tags;
-
-  IncludedInBox(Project.NameKey project, Change.Id changeId) {
-    this.project = project;
-    this.changeId = changeId;
-    initWidget(uiBinder.createAndBindUi(this));
-  }
-
-  @Override
-  protected void onLoad() {
-    if (!loaded) {
-      ChangeApi.includedIn(
-          project.get(),
-          changeId.get(),
-          new AsyncCallback<IncludedInInfo>() {
-            @Override
-            public void onSuccess(IncludedInInfo r) {
-              branches.setInnerSafeHtml(formatList(r.branches()));
-              tags.setInnerSafeHtml(formatList(r.tags()));
-              for (String n : r.externalNames()) {
-                JsArrayString external = r.external(n);
-                if (external.length() > 0) {
-                  appendRow(n, external);
-                }
-              }
-              loaded = true;
-            }
-
-            @Override
-            public void onFailure(Throwable caught) {}
-          });
-    }
-  }
-
-  private SafeHtml formatList(JsArrayString l) {
-    SafeHtmlBuilder html = new SafeHtmlBuilder();
-    int size = l.length();
-    for (int i = 0; i < size; i++) {
-      html.openSpan().addStyleName(style.includedInElement()).append(l.get(i)).closeSpan();
-      if (i < size - 1) {
-        html.append(", ");
-      }
-    }
-    return html;
-  }
-
-  private void appendRow(String title, JsArrayString l) {
-    TableRowElement row = table.insertRow(-1);
-    TableCellElement th = Document.get().createTHElement();
-    th.setInnerText(title);
-    row.appendChild(th);
-    row.insertCell(-1).setInnerSafeHtml(formatList(l));
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInBox.ui.xml
deleted file mode 100644
index 36ac734..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInBox.ui.xml
+++ /dev/null
@@ -1,77 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder
-    xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:style gss='false' type='com.google.gerrit.client.change.IncludedInBox.Style'>
-    .includedInBox {
-      min-width: 300px;
-      max-width: 580px;
-      margin: 5px;
-    }
-
-    .includedInTable {
-      border-spacing: 0;
-    }
-
-    .includedInTable th {
-      width: 60px;
-      color: #444;
-      font-weight: normal;
-      vertical-align: top;
-      text-align: left;
-      padding-right: 5px;
-    }
-
-    .includedInElement {
-      font-size: smaller;
-      font-family: monospace;
-    }
-
-    .includedInElement span {
-      width: 500px;
-      white-space: nowrap;
-      display: inline-block;
-      overflow: hidden;
-      text-overflow: ellipsis;
-    }
-
-    .includedInElement .gwt-TextBox {
-      padding: 0;
-      margin: 0;
-      border: 0;
-      max-height: 18px;
-      width: 500px;
-    }
-
-    .includedInElement div {
-      float: right;
-    }
-  </ui:style>
-  <g:HTMLPanel styleName='{style.includedInBox}'>
-    <table class='{style.includedInTable}' ui:field='table'>
-      <tr>
-        <th><ui:msg>Branches</ui:msg></th>
-          <td ui:field='branches'/>
-      </tr>
-      <tr>
-        <th><ui:msg>Tags</ui:msg></th>
-          <td ui:field='tags'/>
-      </tr>
-    </table>
-  </g:HTMLPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java
deleted file mode 100644
index 267a983..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java
+++ /dev/null
@@ -1,353 +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.client.change;
-
-import static java.util.stream.Collectors.collectingAndThen;
-import static java.util.stream.Collectors.toCollection;
-import static java.util.stream.Collectors.toList;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.Util;
-import com.google.gerrit.client.info.AccountInfo;
-import com.google.gerrit.client.info.AccountInfo.AvatarInfo;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.info.ChangeInfo.ApprovalInfo;
-import com.google.gerrit.client.info.ChangeInfo.LabelInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.LabelValue;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.NativeEvent;
-import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtexpui.safehtml.client.SafeHtml;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-/** Displays a table of label and reviewer scores. */
-class Labels extends Grid {
-  private static final String DATA_ID = "data-id";
-  private static final String DATA_VOTE = "data-vote";
-  private static final String REMOVE_REVIEWER;
-  private static final String REMOVE_VOTE;
-
-  static {
-    REMOVE_REVIEWER = DOM.createUniqueId().replace('-', '_');
-    REMOVE_VOTE = DOM.createUniqueId().replace('-', '_');
-    init(REMOVE_REVIEWER, REMOVE_VOTE);
-  }
-
-  private static native void init(String r, String v) /*-{
-    $wnd[r] = $entry(function(e) {
-      @com.google.gerrit.client.change.Labels::onRemoveReviewer(Lcom/google/gwt/dom/client/NativeEvent;)(e)
-    });
-    $wnd[v] = $entry(function(e) {
-      @com.google.gerrit.client.change.Labels::onRemoveVote(Lcom/google/gwt/dom/client/NativeEvent;)(e)
-    });
-  }-*/;
-
-  private static void onRemoveReviewer(NativeEvent event) {
-    Integer user = getDataId(event);
-    if (user != null) {
-      final ChangeScreen screen = ChangeScreen.get(event);
-      final Change.Id changeId = screen.getPatchSetId().getParentKey();
-      ChangeApi.reviewer(screen.getProject().get(), changeId.get(), user)
-          .delete(
-              new GerritCallback<JavaScriptObject>() {
-                @Override
-                public void onSuccess(JavaScriptObject result) {
-                  if (screen.isCurrentView()) {
-                    Gerrit.display(PageLinks.toChange(screen.getProject(), changeId));
-                  }
-                }
-              });
-    }
-  }
-
-  private static void onRemoveVote(NativeEvent event) {
-    Integer user = getDataId(event);
-    String vote = getVoteId(event);
-    if (user != null && vote != null) {
-      final ChangeScreen screen = ChangeScreen.get(event);
-      final Change.Id changeId = screen.getPatchSetId().getParentKey();
-      ChangeApi.vote(screen.getProject().get(), changeId.get(), user, vote)
-          .delete(
-              new GerritCallback<JavaScriptObject>() {
-                @Override
-                public void onSuccess(JavaScriptObject result) {
-                  if (screen.isCurrentView()) {
-                    Gerrit.display(PageLinks.toChange(screen.getProject(), changeId));
-                  }
-                }
-              });
-    }
-  }
-
-  private static Integer getDataId(NativeEvent event) {
-    Element e = event.getEventTarget().cast();
-    while (e != null) {
-      String v = e.getAttribute(DATA_ID);
-      if (!v.isEmpty()) {
-        return Integer.parseInt(v);
-      }
-      e = e.getParentElement();
-    }
-    return null;
-  }
-
-  private static String getVoteId(NativeEvent event) {
-    Element e = event.getEventTarget().cast();
-    while (e != null) {
-      String v = e.getAttribute(DATA_VOTE);
-      if (!v.isEmpty()) {
-        return v;
-      }
-      e = e.getParentElement();
-    }
-    return null;
-  }
-
-  private ChangeScreen.Style style;
-
-  void init(ChangeScreen.Style style) {
-    this.style = style;
-  }
-
-  void set(ChangeInfo info) {
-    List<String> names =
-        info.labels().stream()
-            .sorted()
-            .collect(collectingAndThen(toList(), Collections::unmodifiableList));
-    Set<Integer> removable = info.removableReviewerIds();
-
-    resize(names.size(), 2);
-
-    for (int row = 0; row < names.size(); row++) {
-      String name = names.get(row);
-      LabelInfo label = info.label(name);
-      setText(row, 0, name);
-      if (label.all() != null) {
-        setWidget(row, 1, renderUsers(label, removable));
-      }
-      getCellFormatter().setStyleName(row, 0, style.labelName());
-      getCellFormatter().addStyleName(row, 0, getStyleForLabel(label));
-    }
-  }
-
-  private Widget renderUsers(LabelInfo label, Set<Integer> removable) {
-    Map<Integer, List<ApprovalInfo>> m = new HashMap<>(4);
-    int approved = 0;
-    int rejected = 0;
-
-    for (ApprovalInfo ai : Natives.asList(label.all())) {
-      if (ai.value() != 0) {
-        List<ApprovalInfo> l = m.get(Integer.valueOf(ai.value()));
-        if (l == null) {
-          l = new ArrayList<>(label.all().length());
-          m.put(Integer.valueOf(ai.value()), l);
-        }
-        l.add(ai);
-
-        if (isRejected(label, ai)) {
-          rejected = ai.value();
-        } else if (isApproved(label, ai)) {
-          approved = ai.value();
-        }
-      }
-    }
-
-    SafeHtmlBuilder html = new SafeHtmlBuilder();
-    for (Integer v : sort(m.keySet(), approved, rejected)) {
-      if (!html.isEmpty()) {
-        html.br();
-      }
-
-      String val = LabelValue.formatValue(v.shortValue());
-      html.openSpan();
-      html.setAttribute("title", label.valueText(val));
-      if (v.intValue() == approved) {
-        html.setStyleName(style.label_ok());
-      } else if (v.intValue() == rejected) {
-        html.setStyleName(style.label_reject());
-      }
-      html.append(val).append(" ");
-      html.append(formatUserList(style, m.get(v), removable, label.name(), null));
-      html.closeSpan();
-    }
-    return html.toBlockWidget();
-  }
-
-  private static List<Integer> sort(Set<Integer> keySet, int a, int b) {
-    List<Integer> r = keySet.stream().sorted().collect(toCollection(ArrayList::new));
-    if (keySet.contains(a)) {
-      r.remove(Integer.valueOf(a));
-      r.add(0, a);
-    } else if (keySet.contains(b)) {
-      r.remove(Integer.valueOf(b));
-      r.add(0, b);
-    }
-    return r;
-  }
-
-  private static boolean isApproved(LabelInfo label, ApprovalInfo ai) {
-    return label.approved() != null && label.approved()._accountId() == ai._accountId();
-  }
-
-  private static boolean isRejected(LabelInfo label, ApprovalInfo ai) {
-    return label.rejected() != null && label.rejected()._accountId() == ai._accountId();
-  }
-
-  private String getStyleForLabel(LabelInfo label) {
-    switch (label.status()) {
-      case OK:
-        return style.label_ok();
-      case NEED:
-        return style.label_need();
-      case REJECT:
-      case IMPOSSIBLE:
-        return style.label_reject();
-      default:
-      case MAY:
-        return style.label_may();
-    }
-  }
-
-  static SafeHtml formatUserList(
-      ChangeScreen.Style style,
-      Collection<? extends AccountInfo> in,
-      Set<Integer> removable,
-      String label,
-      Map<Integer, VotableInfo> votable) {
-    List<AccountInfo> users =
-        in.stream()
-            .sorted(
-                new Comparator<AccountInfo>() {
-                  @Override
-                  public int compare(AccountInfo a, AccountInfo b) {
-                    String as = name(a);
-                    String bs = name(b);
-                    if (as.isEmpty()) {
-                      return 1;
-                    } else if (bs.isEmpty()) {
-                      return -1;
-                    }
-                    return as.compareTo(bs);
-                  }
-
-                  private String name(AccountInfo a) {
-                    if (a.name() != null) {
-                      return a.name();
-                    } else if (a.email() != null) {
-                      return a.email();
-                    }
-                    return "";
-                  }
-                })
-            .collect(collectingAndThen(toList(), Collections::unmodifiableList));
-
-    SafeHtmlBuilder html = new SafeHtmlBuilder();
-    Iterator<? extends AccountInfo> itr = users.iterator();
-    while (itr.hasNext()) {
-      AccountInfo ai = itr.next();
-      AvatarInfo img = ai.avatar(AvatarInfo.DEFAULT_SIZE);
-      String name;
-      if (ai.name() != null) {
-        name = ai.name();
-      } else if (ai.email() != null) {
-        name = ai.email();
-      } else {
-        name = Integer.toString(ai._accountId());
-      }
-
-      String votableCategories = "";
-      if (votable != null) {
-        VotableInfo vi = votable.get(ai._accountId());
-        if (vi != null) {
-          Set<String> s = vi.votableLabels();
-          if (!s.isEmpty()) {
-            StringBuilder sb = new StringBuilder(Util.C.votable());
-            sb.append(" ");
-            for (Iterator<String> it = vi.votableLabels().iterator(); it.hasNext(); ) {
-              sb.append(it.next());
-              if (it.hasNext()) {
-                sb.append(", ");
-              }
-            }
-            votableCategories = sb.toString();
-          }
-        }
-      }
-      html.openSpan()
-          .setAttribute("role", "listitem")
-          .setAttribute(DATA_ID, ai._accountId())
-          .setAttribute("title", getTitle(ai, votableCategories))
-          .setStyleName(style.label_user());
-      if (label != null) {
-        html.setAttribute(DATA_VOTE, label);
-      }
-      if (img != null) {
-        html.openElement("img").setStyleName(style.avatar()).setAttribute("src", img.url());
-        if (img.width() > 0) {
-          html.setAttribute("width", img.width());
-        }
-        if (img.height() > 0) {
-          html.setAttribute("height", img.height());
-        }
-        html.closeSelf();
-      }
-      html.append(name);
-      if (removable.contains(ai._accountId())) {
-        html.openElement("button");
-        if (label != null) {
-          html.setAttribute("title", Util.M.removeVote(label))
-              .setAttribute("onclick", REMOVE_VOTE + "(event)");
-        } else {
-          html.setAttribute("title", Util.M.removeReviewer(name))
-              .setAttribute("onclick", REMOVE_REVIEWER + "(event)");
-        }
-        html.append("×").closeElement("button");
-      }
-      html.closeSpan();
-      if (itr.hasNext()) {
-        html.append(' ');
-      }
-    }
-    return html;
-  }
-
-  private static String getTitle(AccountInfo ai, String votableCategories) {
-    String title = ai.email() != null ? ai.email() : "";
-    if (!votableCategories.isEmpty()) {
-      if (!title.isEmpty()) {
-        title += " ";
-      }
-      title += votableCategories;
-    }
-    return title;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.java
deleted file mode 100644
index 5a0cc59..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.java
+++ /dev/null
@@ -1,100 +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.client.change;
-
-import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.client.api.ApiGlue;
-import com.google.gerrit.client.changes.CommentInfo;
-import com.google.gerrit.client.diff.DisplaySide;
-import com.google.gerrit.client.ui.CommentLinkProcessor;
-import com.google.gerrit.client.ui.InlineHyperlink;
-import com.google.gerrit.extensions.client.Side;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-
-class LineComment extends Composite {
-  interface Binder extends UiBinder<HTMLPanel, LineComment> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  @UiField Element sideLoc;
-  @UiField Element psLoc;
-  @UiField Element psNum;
-  @UiField Element fileLoc;
-  @UiField Element lineLoc;
-  @UiField InlineHyperlink line;
-  @UiField Element message;
-
-  LineComment(
-      CommentLinkProcessor clp, Project.NameKey project, PatchSet.Id defaultPs, CommentInfo info) {
-    initWidget(uiBinder.createAndBindUi(this));
-
-    PatchSet.Id ps;
-    if (info.patchSet() != defaultPs.get()) {
-      ps = new PatchSet.Id(defaultPs.getParentKey(), info.patchSet());
-      psNum.setInnerText(Integer.toString(info.patchSet()));
-      sideLoc.removeFromParent();
-      sideLoc = null;
-    } else if (info.side() == Side.PARENT) {
-      ps = defaultPs;
-      psLoc.removeFromParent();
-      psLoc = null;
-      psNum = null;
-    } else {
-      ps = defaultPs;
-      sideLoc.removeFromParent();
-      sideLoc = null;
-      psLoc.removeFromParent();
-      psLoc = null;
-      psNum = null;
-    }
-
-    if (info.hasLine()) {
-      fileLoc.removeFromParent();
-      fileLoc = null;
-
-      line.setTargetHistoryToken(url(project, ps, info));
-      line.setText(Integer.toString(info.line()));
-
-    } else {
-      lineLoc.removeFromParent();
-      lineLoc = null;
-      line = null;
-    }
-
-    if (info.message() != null) {
-      message.setInnerSafeHtml(
-          clp.apply(new SafeHtmlBuilder().append(info.message().trim()).wikify()));
-      ApiGlue.fireEvent("comment", message);
-    }
-  }
-
-  private static String url(Project.NameKey project, PatchSet.Id ps, CommentInfo info) {
-    return Dispatcher.toPatch(
-        project,
-        null,
-        ps,
-        info.path(),
-        info.side() == Side.PARENT ? DisplaySide.A : DisplaySide.B,
-        info.line());
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.ui.xml
deleted file mode 100644
index f33ba51..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.ui.xml
+++ /dev/null
@@ -1,45 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder
-    xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:c='urn:import:com.google.gerrit.client.ui'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:style gss='false'>
-    .box {
-      position: relative;
-    }
-    .location {
-      position: absolute;
-      top: 0;
-      left: 0;
-      font-weight: bold;
-    }
-    .message {
-      margin-left: 135px;
-    }
-  </ui:style>
-
-  <g:HTMLPanel styleName='{style.box}'>
-    <div class='{style.location}'>
-      <span ui:field='sideLoc'><ui:msg>Base, </ui:msg></span>
-      <span ui:field='psLoc'><ui:msg>PS<span ui:field='psNum'/>, </ui:msg></span>
-      <span ui:field='fileLoc'><ui:msg>File Comment</ui:msg></span>
-      <span ui:field='lineLoc'><ui:msg>Line <c:InlineHyperlink ui:field='line'/>:</ui:msg></span>
-    </div>
-    <div class='{style.message}' ui:field='message'/>
-  </g:HTMLPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LocalComments.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LocalComments.java
deleted file mode 100644
index d2f031a..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LocalComments.java
+++ /dev/null
@@ -1,276 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.change;
-
-import com.google.gerrit.client.changes.CommentApi;
-import com.google.gerrit.client.changes.CommentInfo;
-import com.google.gerrit.client.diff.CommentRange;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.extensions.client.Side;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.storage.client.Storage;
-import com.google.gwt.user.client.Cookies;
-import java.util.ArrayList;
-import java.util.Collection;
-
-public class LocalComments {
-  @Nullable private final Project.NameKey project;
-  private final Change.Id changeId;
-  private final PatchSet.Id psId;
-  private final StorageBackend storage;
-
-  private static class InlineComment {
-    @Nullable final Project.NameKey project;
-    final PatchSet.Id psId;
-    final CommentInfo commentInfo;
-
-    InlineComment(@Nullable Project.NameKey project, PatchSet.Id psId, CommentInfo commentInfo) {
-      this.project = project;
-      this.psId = psId;
-      this.commentInfo = commentInfo;
-    }
-  }
-
-  private static class StorageBackend {
-    private final Storage storageBackend;
-
-    StorageBackend() {
-      storageBackend =
-          (Storage.isLocalStorageSupported())
-              ? Storage.getLocalStorageIfSupported()
-              : Storage.getSessionStorageIfSupported();
-    }
-
-    String getItem(String key) {
-      if (storageBackend == null) {
-        return Cookies.getCookie(key);
-      }
-      return storageBackend.getItem(key);
-    }
-
-    void setItem(String key, String value) {
-      if (storageBackend == null) {
-        Cookies.setCookie(key, value);
-        return;
-      }
-      storageBackend.setItem(key, value);
-    }
-
-    void removeItem(String key) {
-      if (storageBackend == null) {
-        Cookies.removeCookie(key);
-        return;
-      }
-      storageBackend.removeItem(key);
-    }
-
-    Collection<String> getKeys() {
-      if (storageBackend == null) {
-        return Cookies.getCookieNames();
-      }
-      ArrayList<String> result = new ArrayList<>(storageBackend.getLength());
-      for (int i = 0; i < storageBackend.getLength(); i++) {
-        result.add(storageBackend.key(i));
-      }
-      return result;
-    }
-  }
-
-  public LocalComments(@Nullable Project.NameKey project, Change.Id changeId) {
-    this.project = project;
-    this.changeId = changeId;
-    this.psId = null;
-    this.storage = new StorageBackend();
-  }
-
-  public LocalComments(@Nullable Project.NameKey project, PatchSet.Id psId) {
-    this.project = project;
-    this.changeId = psId.getParentKey();
-    this.psId = psId;
-    this.storage = new StorageBackend();
-  }
-
-  public String getReplyComment() {
-    String comment = storage.getItem(getReplyCommentName());
-    storage.removeItem(getReplyCommentName());
-    return comment;
-  }
-
-  public void setReplyComment(String comment) {
-    storage.setItem(getReplyCommentName(), comment.trim());
-  }
-
-  public boolean hasReplyComment() {
-    return storage.getKeys().contains(getReplyCommentName());
-  }
-
-  public void removeReplyComment() {
-    if (hasReplyComment()) {
-      storage.removeItem(getReplyCommentName());
-    }
-  }
-
-  private String getReplyCommentName() {
-    return "savedReplyComment~" + PageLinks.toChangeId(project, changeId);
-  }
-
-  public static void saveInlineComments() {
-    final StorageBackend storage = new StorageBackend();
-    for (String cookie : storage.getKeys()) {
-      if (isInlineComment(cookie)) {
-        InlineComment input = getInlineComment(cookie);
-        if (input.commentInfo.id() == null) {
-          CommentApi.createDraft(
-              Project.NameKey.asStringOrNull(input.project),
-              input.psId,
-              input.commentInfo,
-              new GerritCallback<CommentInfo>() {
-                @Override
-                public void onSuccess(CommentInfo result) {
-                  storage.removeItem(cookie);
-                }
-              });
-        } else {
-          CommentApi.updateDraft(
-              Project.NameKey.asStringOrNull(input.project),
-              input.psId,
-              input.commentInfo.id(),
-              input.commentInfo,
-              new GerritCallback<CommentInfo>() {
-                @Override
-                public void onSuccess(CommentInfo result) {
-                  storage.removeItem(cookie);
-                }
-
-                @Override
-                public void onFailure(Throwable caught) {
-                  if (RestApi.isNotFound(caught)) {
-                    // the draft comment, that was supposed to be updated,
-                    // was deleted in the meantime
-                    storage.removeItem(cookie);
-                  } else {
-                    super.onFailure(caught);
-                  }
-                }
-              });
-        }
-      }
-    }
-  }
-
-  public void setInlineComment(CommentInfo comment) {
-    String name = getInlineCommentName(comment);
-    if (name == null) {
-      // Failed to get the store key -- so we can't continue.
-      return;
-    }
-    storage.setItem(name, comment.message().trim());
-  }
-
-  public boolean hasInlineComments() {
-    for (String cookie : storage.getKeys()) {
-      if (isInlineComment(cookie)) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  private static boolean isInlineComment(String key) {
-    return key.startsWith("patchCommentEdit~")
-        || key.startsWith("patchReply~")
-        || key.startsWith("patchComment~");
-  }
-
-  private static InlineComment getInlineComment(String key) {
-    String path;
-    Side side;
-    int line;
-    CommentRange range;
-    StorageBackend storage = new StorageBackend();
-
-    String[] elements = key.split("~");
-    int offset = 1;
-    if (key.startsWith("patchReply~") || key.startsWith("patchCommentEdit~")) {
-      offset = 2;
-    }
-    ProjectChangeId id = ProjectChangeId.create(elements[offset + 0]);
-    PatchSet.Id psId = new PatchSet.Id(id.getChangeId(), Integer.parseInt(elements[offset + 1]));
-    path = atob(elements[offset + 2]);
-    side = (Side.PARENT.toString().equals(elements[offset + 3])) ? Side.PARENT : Side.REVISION;
-    range = null;
-    if (elements[offset + 4].startsWith("R")) {
-      String rangeStart = elements[offset + 4].substring(1);
-      String rangeEnd = elements[offset + 5];
-      String[] split = rangeStart.split(",");
-      int sl = Integer.parseInt(split[0]);
-      int sc = Integer.parseInt(split[1]);
-      split = rangeEnd.split(",");
-      int el = Integer.parseInt(split[0]);
-      int ec = Integer.parseInt(split[1]);
-      range = CommentRange.create(sl, sc, el, ec);
-      line = sl;
-    } else {
-      line = Integer.parseInt(elements[offset + 4]);
-    }
-    CommentInfo info = CommentInfo.create(path, side, line, range, false);
-    info.message(storage.getItem(key));
-    if (key.startsWith("patchReply~")) {
-      info.inReplyTo(elements[1]);
-    } else if (key.startsWith("patchCommentEdit~")) {
-      info.id(elements[1]);
-    }
-    InlineComment inlineComment = new InlineComment(id.getProject(), psId, info);
-    return inlineComment;
-  }
-
-  private String getInlineCommentName(CommentInfo comment) {
-    if (psId == null) {
-      return null;
-    }
-    String result = "patchComment~";
-    if (comment.id() != null) {
-      result = "patchCommentEdit~" + comment.id() + "~";
-    } else if (comment.inReplyTo() != null) {
-      result = "patchReply~" + comment.inReplyTo() + "~";
-    }
-
-    result += PageLinks.toChangeId(project, changeId);
-    result += "~" + psId.getId() + "~" + btoa(comment.path()) + "~" + comment.side() + "~";
-    if (comment.hasRange()) {
-      result +=
-          "R"
-              + comment.range().startLine()
-              + ","
-              + comment.range().startCharacter()
-              + "~"
-              + comment.range().endLine()
-              + ","
-              + comment.range().endCharacter();
-    } else {
-      result += comment.line();
-    }
-    return result;
-  }
-
-  private static native String btoa(String a) /*-{ return btoa(a); }-*/;
-
-  private static native String atob(String b) /*-{ return atob(b); }-*/;
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java
deleted file mode 100644
index cadaf97..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java
+++ /dev/null
@@ -1,209 +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.client.change;
-
-import com.google.gerrit.client.AvatarImage;
-import com.google.gerrit.client.FormatUtil;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.api.ApiGlue;
-import com.google.gerrit.client.changes.CommentInfo;
-import com.google.gerrit.client.changes.Util;
-import com.google.gerrit.client.info.ChangeInfo.MessageInfo;
-import com.google.gerrit.client.ui.CommentLinkProcessor;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.Style.Visibility;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.resources.client.CssResource;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.uibinder.client.UiHandler;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwt.user.client.ui.UIObject;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.TreeMap;
-
-class Message extends Composite {
-  interface Binder extends UiBinder<HTMLPanel, Message> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  interface Style extends CssResource {
-    String closed();
-  }
-
-  @UiField Style style;
-  @UiField HTMLPanel header;
-  @UiField Element name;
-  @UiField Element summary;
-  @UiField Element date;
-  @UiField Button reply;
-  @UiField Element message;
-  @UiField FlowPanel comments;
-
-  private final History history;
-  private final MessageInfo info;
-  private List<CommentInfo> commentList;
-  private boolean autoOpen;
-
-  @UiField(provided = true)
-  AvatarImage avatar;
-
-  Message(History parent, MessageInfo info) {
-    if (info.author() != null) {
-      avatar = new AvatarImage(info.author());
-      avatar.setSize("", "");
-    } else {
-      avatar = new AvatarImage();
-    }
-
-    initWidget(uiBinder.createAndBindUi(this));
-    header.addDomHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            setOpen(!isOpen());
-          }
-        },
-        ClickEvent.getType());
-
-    this.history = parent;
-    this.info = info;
-
-    setName(false);
-    date.setInnerText(FormatUtil.shortFormatDayTime(info.date()));
-    if (info.message() != null) {
-      String msg = info.message().trim();
-      summary.setInnerText(msg);
-      message.setInnerSafeHtml(
-          history.getCommentLinkProcessor().apply(new SafeHtmlBuilder().append(msg).wikify()));
-      ApiGlue.fireEvent("comment", message);
-    } else {
-      reply.getElement().getStyle().setVisibility(Visibility.HIDDEN);
-    }
-  }
-
-  @UiHandler("reply")
-  void onReply(ClickEvent e) {
-    e.stopPropagation();
-
-    if (Gerrit.isSignedIn()) {
-      history.replyTo(info);
-    } else {
-      Gerrit.doSignIn(com.google.gwt.user.client.History.getToken());
-    }
-  }
-
-  MessageInfo getMessageInfo() {
-    return info;
-  }
-
-  private boolean isOpen() {
-    return UIObject.isVisible(message);
-  }
-
-  void setOpen(boolean open) {
-    if (open && info._revisionNumber() > 0 && !commentList.isEmpty()) {
-      renderComments(commentList);
-      commentList = Collections.emptyList();
-    }
-    setName(open);
-
-    UIObject.setVisible(summary, !open);
-    UIObject.setVisible(message, open);
-    comments.setVisible(open && comments.getWidgetCount() > 0);
-    if (open) {
-      removeStyleName(style.closed());
-    } else {
-      addStyleName(style.closed());
-    }
-  }
-
-  private void setName(boolean open) {
-    name.setInnerText(
-        open ? authorName(info) : com.google.gerrit.common.FormatUtil.elide(authorName(info), 20));
-  }
-
-  void autoOpen() {
-    if (commentList == null) {
-      autoOpen = true;
-    } else if (!commentList.isEmpty()) {
-      setOpen(true);
-    }
-  }
-
-  void addComments(List<CommentInfo> list) {
-    if (isOpen()) {
-      renderComments(list);
-      comments.setVisible(comments.getWidgetCount() > 0);
-      commentList = Collections.emptyList();
-    } else {
-      commentList = list;
-      if (autoOpen && !commentList.isEmpty()) {
-        setOpen(true);
-      }
-    }
-  }
-
-  private void renderComments(List<CommentInfo> list) {
-    CommentLinkProcessor clp = history.getCommentLinkProcessor();
-    PatchSet.Id ps = new PatchSet.Id(history.getChangeId(), info._revisionNumber());
-    TreeMap<String, List<CommentInfo>> m = byPath(list);
-    List<CommentInfo> l = m.remove(Patch.COMMIT_MSG);
-    if (l != null) {
-      comments.add(new FileComments(clp, history.getProject(), ps, Util.C.commitMessage(), l));
-    }
-    l = m.remove(Patch.MERGE_LIST);
-    if (l != null) {
-      comments.add(new FileComments(clp, history.getProject(), ps, Util.C.mergeList(), l));
-    }
-    for (Map.Entry<String, List<CommentInfo>> e : m.entrySet()) {
-      comments.add(new FileComments(clp, history.getProject(), ps, e.getKey(), e.getValue()));
-    }
-  }
-
-  private static TreeMap<String, List<CommentInfo>> byPath(List<CommentInfo> list) {
-    TreeMap<String, List<CommentInfo>> m = new TreeMap<>();
-    for (CommentInfo c : list) {
-      List<CommentInfo> l = m.get(c.path());
-      if (l == null) {
-        l = new ArrayList<>();
-        m.put(c.path(), l);
-      }
-      l.add(c);
-    }
-    return m;
-  }
-
-  static String authorName(MessageInfo info) {
-    if (info.author() != null) {
-      if (info.author().name() != null) {
-        return info.author().name();
-      }
-      return Gerrit.info().user().anonymousCowardName();
-    }
-    return Util.C.messageNoAuthor();
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.ui.xml
deleted file mode 100644
index e362c07..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.ui.xml
+++ /dev/null
@@ -1,136 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder
-    xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:c='urn:import:com.google.gerrit.client'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:style gss='false' type='com.google.gerrit.client.change.Message.Style'>
-    .messageBox {
-      position: relative;
-      width: 1168px;
-      padding: 2px 0 2px 0;
-      border-left: 1px solid #e3e9ff;
-      border-right: 1px solid #e3e9ff;
-      border-bottom: 1px solid #e3e9ff;
-      -webkit-border-bottom-left-radius: 8px;
-      -webkit-border-bottom-right-radius: 8px;
-    }
-
-    .header {
-      cursor: pointer;
-    }
-
-    .avatar {
-      position: absolute;
-      width: 26px;
-      height: 26px;
-    }
-    .closed .avatar {
-      position: absolute;
-      top: 0;
-      left: -1px;
-      width: 20px;
-      height: 20px;
-    }
-
-    .contents {
-      margin-left: 28px;
-      position: relative;
-    }
-
-    .contents p,
-    .contents blockquote {
-      -webkit-margin-before: 0;
-      -webkit-margin-after: 0.3em;
-       white-space: pre-wrap;
-    }
-
-    .name {
-      white-space: nowrap;
-      font-weight: bold;
-    }
-    .closed .name {
-      width: 150px;
-      overflow: hidden;
-      font-weight: normal;
-    }
-
-    .summary {
-      color: #777;
-      position: absolute;
-      top: 0;
-      left: 150px;
-      width: 880px;
-      overflow: hidden;
-      text-overflow: ellipsis;
-      white-space: nowrap;
-    }
-
-    .date {
-      white-space: nowrap;
-      position: absolute;
-      top: 0;
-      right: 18px;
-    }
-
-    .reply {
-      position: absolute;
-      top: 0;
-      right: 1px;
-      cursor: pointer;
-      outline: none;
-      border: none;
-      background: transparent;
-      margin: 0;
-      padding: 0;
-      line-height: 15px;
-      font-family: Arial Unicode MS, sans-serif;
-      font-size: 18px;
-    }
-    .closed .reply {
-      visibility: hidden;
-    }
-    .comment {
-    }
-  </ui:style>
-
-  <g:HTMLPanel
-      styleName='{style.messageBox}'
-      addStyleNames='{style.closed}'>
-    <c:AvatarImage ui:field='avatar' styleName='{style.avatar}'/>
-    <div class='{style.contents}'>
-      <g:HTMLPanel ui:field='header' styleName='{style.header}'>
-        <div class='{style.name}' ui:field='name'/>
-        <div ui:field='summary' class='{style.summary}'/>
-        <div class='{style.date}' ui:field='date'/>
-        <g:Button styleName='{style.reply}'
-            ui:field='reply'
-            title='Reply to this message'>
-          <ui:attribute name='title'/>
-          <div>&#x21a9;</div>
-        </g:Button>
-      </g:HTMLPanel>
-      <div style='overflow: auto'>
-        <div ui:field='message'
-            aria-hidden='true'
-            style='display: NONE'
-            styleName='{style.comment}'/>
-        <g:FlowPanel ui:field='comments' visible='false'/>
-      </div>
-    </div>
-  </g:HTMLPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/MoveAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/MoveAction.java
deleted file mode 100644
index e3e9525..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/MoveAction.java
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.change;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.Util;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.MoveDialog;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.event.logical.shared.CloseEvent;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.PopupPanel;
-
-class MoveAction {
-  static void call(Button b, ChangeInfo info, Project.NameKey project) {
-    b.setEnabled(false);
-    new MoveDialog(project) {
-      {
-        sendButton.setText(Util.C.moveChangeSend());
-      }
-
-      @Override
-      public void onSend() {
-        ChangeApi.move(
-            info.project(),
-            info.legacyId().get(),
-            getDestinationBranch(),
-            getMessageText(),
-            new GerritCallback<ChangeInfo>() {
-              @Override
-              public void onSuccess(ChangeInfo result) {
-                sent = true;
-                hide();
-                Gerrit.display(PageLinks.toChange(project, result.legacyId()));
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {
-                enableButtons(true);
-                super.onFailure(caught);
-              }
-            });
-      }
-
-      @Override
-      public void onClose(CloseEvent<PopupPanel> event) {
-        super.onClose(event);
-        b.setEnabled(true);
-      }
-    }.center();
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsAction.java
deleted file mode 100644
index faf2516..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsAction.java
+++ /dev/null
@@ -1,42 +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.client.change;
-
-import com.google.gerrit.client.info.ChangeInfo.EditInfo;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.user.client.ui.UIObject;
-import com.google.gwt.user.client.ui.Widget;
-
-class PatchSetsAction extends RightSidePopdownAction {
-  private final PatchSetsBox revisionBox;
-
-  PatchSetsAction(
-      Project.NameKey project,
-      Change.Id changeId,
-      String revision,
-      EditInfo edit,
-      ChangeScreen.Style style,
-      UIObject relativeTo,
-      Widget downloadButton) {
-    super(style, relativeTo, downloadButton);
-    this.revisionBox = new PatchSetsBox(project, changeId, revision, edit);
-  }
-
-  @Override
-  Widget getWidget() {
-    return revisionBox;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsBox.java
deleted file mode 100644
index 35cab4e..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsBox.java
+++ /dev/null
@@ -1,233 +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.client.change;
-
-import com.google.gerrit.client.FormatUtil;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeList;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.info.ChangeInfo.CommitInfo;
-import com.google.gerrit.client.info.ChangeInfo.EditInfo;
-import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.client.ui.FancyFlexTableImpl;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.extensions.client.ListChangesOption;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.NativeEvent;
-import com.google.gwt.resources.client.CssResource;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.Event;
-import com.google.gwt.user.client.EventListener;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlexTable;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwt.user.client.ui.PopupPanel;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwt.user.client.ui.impl.HyperlinkImpl;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-import java.util.Collections;
-import java.util.EnumSet;
-
-class PatchSetsBox extends Composite {
-  interface Binder extends UiBinder<HTMLPanel, PatchSetsBox> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  private static final String OPEN;
-  private static final HyperlinkImpl link = GWT.create(HyperlinkImpl.class);
-
-  static {
-    OPEN = DOM.createUniqueId().replace('-', '_');
-    init(OPEN);
-  }
-
-  private static native void init(String o) /*-{
-    $wnd[o] = $entry(function(e,i) {
-      return @com.google.gerrit.client.change.PatchSetsBox::onOpen(Lcom/google/gwt/dom/client/NativeEvent;I)(e,i);
-    });
-  }-*/;
-
-  private static boolean onOpen(NativeEvent e, int idx) {
-    if (link.handleAsClick(e.<Event>cast())) {
-      PatchSetsBox t = getRevisionBox(e);
-      if (t != null) {
-        t.onOpenRow(idx);
-        e.preventDefault();
-        return false;
-      }
-    }
-    return true;
-  }
-
-  private static PatchSetsBox getRevisionBox(NativeEvent event) {
-    Element e = event.getEventTarget().cast();
-    for (e = DOM.getParent(e); e != null; e = DOM.getParent(e)) {
-      EventListener l = DOM.getEventListener(e);
-      if (l instanceof PatchSetsBox) {
-        return (PatchSetsBox) l;
-      }
-    }
-    return null;
-  }
-
-  interface Style extends CssResource {
-    String current();
-
-    String legacy_id();
-
-    String commit();
-
-    String draft_comment();
-  }
-
-  private final Change.Id changeId;
-  private final Project.NameKey project;
-  private final String revision;
-  private final EditInfo edit;
-  private boolean loaded;
-  private JsArray<RevisionInfo> revisions;
-
-  @UiField FlexTable table;
-  @UiField Style style;
-
-  PatchSetsBox(Project.NameKey project, Change.Id changeId, String revision, EditInfo edit) {
-    this.project = project;
-    this.changeId = changeId;
-    this.revision = revision;
-    this.edit = edit;
-    initWidget(uiBinder.createAndBindUi(this));
-  }
-
-  @Override
-  protected void onLoad() {
-    if (!loaded) {
-      RestApi call = ChangeApi.detail(project.get(), changeId.get());
-      ChangeList.addOptions(
-          call, EnumSet.of(ListChangesOption.ALL_COMMITS, ListChangesOption.ALL_REVISIONS));
-      call.get(
-          new AsyncCallback<ChangeInfo>() {
-            @Override
-            public void onSuccess(ChangeInfo result) {
-              if (edit != null) {
-                edit.setName(edit.commit().commit());
-                result.revisions().put(edit.name(), RevisionInfo.fromEdit(edit));
-              }
-              render(result.revisions());
-              loaded = true;
-            }
-
-            @Override
-            public void onFailure(Throwable caught) {}
-          });
-    }
-  }
-
-  private void onOpenRow(int idx) {
-    closeParent();
-    Gerrit.display(url(revisions.get(idx)));
-  }
-
-  private void render(NativeMap<RevisionInfo> map) {
-    map.copyKeysIntoChildren("name");
-
-    revisions = map.values();
-    RevisionInfo.sortRevisionInfoByNumber(revisions);
-    Collections.reverse(Natives.asList(revisions));
-
-    SafeHtmlBuilder sb = new SafeHtmlBuilder();
-    header(sb);
-    for (int i = 0; i < revisions.length(); i++) {
-      revision(sb, i, revisions.get(i));
-    }
-
-    GWT.<FancyFlexTableImpl>create(FancyFlexTableImpl.class).resetHtml(table, sb);
-  }
-
-  private void header(SafeHtmlBuilder sb) {
-    sb.openTr()
-        .openTh()
-        .setStyleName(style.legacy_id())
-        .append(Resources.C.patchSet())
-        .closeTh()
-        .openTh()
-        .append(Resources.C.commit())
-        .closeTh()
-        .openTh()
-        .append(Resources.C.date())
-        .closeTh()
-        .openTh()
-        .append(Resources.C.author())
-        .closeTh()
-        .closeTr();
-  }
-
-  private void revision(SafeHtmlBuilder sb, int index, RevisionInfo r) {
-    CommitInfo c = r.commit();
-    sb.openTr();
-    if (revision.equals(r.name())) {
-      sb.setStyleName(style.current());
-    }
-
-    sb.openTd().setStyleName(style.legacy_id());
-    sb.append(r.id());
-    sb.closeTd();
-
-    sb.openTd()
-        .setStyleName(style.commit())
-        .openAnchor()
-        .setAttribute("href", "#" + url(r))
-        .setAttribute("onclick", OPEN + "(event," + index + ")")
-        .append(r.name().substring(0, 10))
-        .closeAnchor()
-        .closeTd();
-
-    sb.openTd().append(FormatUtil.shortFormatDayTime(c.committer().date())).closeTd();
-
-    String an = c.author() != null ? c.author().name() : "";
-    String cn = c.committer() != null ? c.committer().name() : "";
-    sb.openTd();
-    sb.append(an);
-    if (!"".equals(an) && !"".equals(cn) && !an.equals(cn)) {
-      sb.append(" / ").append(cn);
-    }
-    sb.closeTd();
-
-    sb.closeTr();
-  }
-
-  private String url(RevisionInfo r) {
-    return PageLinks.toChange(project, changeId, r.id());
-  }
-
-  private void closeParent() {
-    for (Widget w = getParent(); w != null; w = w.getParent()) {
-      if (w instanceof PopupPanel) {
-        ((PopupPanel) w).hide(true);
-        break;
-      }
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsBox.ui.xml
deleted file mode 100644
index 7537aa4..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsBox.ui.xml
+++ /dev/null
@@ -1,76 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder
-    xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
-  <ui:style gss='false' type='com.google.gerrit.client.change.PatchSetsBox.Style'>
-    @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
-
-    .revisionBox {
-      min-width: 300px;
-      margin: 10px 0px 5px 5px;
-    }
-
-    .scroll {
-      min-width: 300px;
-      height: 200px;
-    }
-
-    .table {
-      border-spacing: 0;
-      width: 100%;
-    }
-
-    .table td, .table th {
-      padding-left: 5px;
-      padding-right: 5px;
-      border-right: 2px solid #ddd;
-      white-space: nowrap;
-    }
-
-    .table tr.current {
-      background-color: selectionColor;
-    }
-    .table tr.current a {
-      pointer-events: none;
-      color: #000;
-    }
-
-    .legacy_id {
-      min-width: 50px;
-      text-align: right;
-      font-weight: bold;
-    }
-
-    .commit {
-      font-family: monospace;
-    }
-
-    .draft_comment {
-      margin: 0 2px 0 0;
-      width: 16px;
-      height: 16px;
-      vertical-align: bottom;
-    }
-  </ui:style>
-  <g:HTMLPanel styleName='{style.revisionBox}'>
-    <g:ScrollPanel styleName='{style.scroll}'>
-      <g:FlexTable ui:field='table' styleName='{style.table}'/>
-    </g:ScrollPanel>
-  </g:HTMLPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PathSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PathSuggestOracle.java
deleted file mode 100644
index 7668f0f..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PathSuggestOracle.java
+++ /dev/null
@@ -1,85 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.change;
-
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.JsArrayString;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-class PathSuggestOracle extends HighlightSuggestOracle {
-
-  private final Project.NameKey project;
-  private final Change.Id changeId;
-  private final RevisionInfo revision;
-
-  PathSuggestOracle(Project.NameKey project, Change.Id changeId, RevisionInfo revision) {
-    this.project = project;
-    this.changeId = changeId;
-    this.revision = revision;
-  }
-
-  @Override
-  protected void onRequestSuggestions(Request req, Callback cb) {
-    RestApi api = ChangeApi.revision(project.get(), changeId.get(), revision.name()).view("files");
-    if (req.getQuery() != null) {
-      api.addParameter("q", req.getQuery() == null ? "" : req.getQuery());
-    }
-    api.background()
-        .get(
-            new AsyncCallback<JsArrayString>() {
-              @Override
-              public void onSuccess(JsArrayString result) {
-                List<Suggestion> r = new ArrayList<>();
-                for (String path : Natives.asList(result)) {
-                  r.add(new PathSuggestion(path));
-                }
-                cb.onSuggestionsReady(req, new Response(r));
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {
-                List<Suggestion> none = Collections.emptyList();
-                cb.onSuggestionsReady(req, new Response(none));
-              }
-            });
-  }
-
-  private static class PathSuggestion implements Suggestion {
-    private final String path;
-
-    PathSuggestion(String path) {
-      this.path = path;
-    }
-
-    @Override
-    public String getDisplayString() {
-      return path;
-    }
-
-    @Override
-    public String getReplacementString() {
-      return path;
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ProjectChangeId.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ProjectChangeId.java
deleted file mode 100644
index 684867b..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ProjectChangeId.java
+++ /dev/null
@@ -1,117 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.change;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import java.util.Objects;
-
-/** Provides logic for parsing a numeric change id and project from a URL. */
-public class ProjectChangeId {
-
-  /** Parses a {@link ProjectChangeId} from it's string representation. */
-  public static ProjectChangeId create(String token) {
-    String mutableToken = token;
-    // Try parsing /c/project/+/numericChangeId where token is project/+/numericChangeId
-    int delimiter = mutableToken.indexOf(PageLinks.PROJECT_CHANGE_DELIMITER);
-    Project.NameKey project = null;
-    if (delimiter > 0) {
-      project = new Project.NameKey(token.substring(0, delimiter));
-      mutableToken =
-          mutableToken.substring(delimiter + PageLinks.PROJECT_CHANGE_DELIMITER.length());
-    }
-
-    // Try parsing /c/numericChangeId where token is numericChangeId
-    int s = mutableToken.indexOf('/');
-    if (s > 0) {
-      mutableToken = mutableToken.substring(0, s);
-    }
-    // Special case: project/+/1233,edit/
-    s = mutableToken.indexOf(",edit");
-    if (s > 0) {
-      mutableToken = mutableToken.substring(0, s);
-    }
-    Integer cId = tryParse(mutableToken);
-    if (cId != null) {
-      return new ProjectChangeId(project, new Change.Id(cId));
-    }
-
-    throw new IllegalArgumentException(token + " is not a valid change identifier");
-  }
-
-  @Nullable private final Project.NameKey project;
-  private final Change.Id changeId;
-
-  @VisibleForTesting
-  ProjectChangeId(@Nullable Project.NameKey project, Change.Id changeId) {
-    this.project = project;
-    this.changeId = changeId;
-  }
-
-  @Nullable
-  public Project.NameKey getProject() {
-    return project;
-  }
-
-  public Change.Id getChangeId() {
-    return changeId;
-  }
-
-  /**
-   * Calculate the length of the string representation of the change ID that was parsed from the
-   * token.
-   *
-   * @return the length of the {@link com.google.gerrit.reviewdb.client.Change.Id} if no project was
-   *     parsed from the token. The length of {@link
-   *     com.google.gerrit.reviewdb.client.Project.NameKey} + the delimiter + the length of {@link
-   *     com.google.gerrit.reviewdb.client.Change.Id} otherwise.
-   */
-  public int identifierLength() {
-    if (project == null) {
-      return String.valueOf(changeId).length();
-    }
-    return PageLinks.toChangeId(project, changeId).length();
-  }
-
-  @Override
-  public boolean equals(Object obj) {
-    if (obj instanceof ProjectChangeId) {
-      ProjectChangeId other = (ProjectChangeId) obj;
-      return Objects.equals(changeId, other.changeId) && Objects.equals(project, other.project);
-    }
-    return false;
-  }
-
-  @Override
-  public int hashCode() {
-    return Objects.hash(changeId, project);
-  }
-
-  @Override
-  public String toString() {
-    return "ProjectChangeId.Result{changeId: " + changeId + ", project: " + project + "}";
-  }
-
-  private static Integer tryParse(String s) {
-    try {
-      return Integer.parseInt(s);
-    } catch (NumberFormatException e) {
-      return null;
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/QuickApprove.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/QuickApprove.java
deleted file mode 100644
index 56cc7a7..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/QuickApprove.java
+++ /dev/null
@@ -1,111 +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.client.change;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ReviewInput;
-import com.google.gerrit.client.changes.ReviewInput.DraftHandling;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.info.ChangeInfo.LabelInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.JsArrayString;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-
-/** Applies a label with one mouse click. */
-class QuickApprove extends Button implements ClickHandler {
-  private Change.Id changeId;
-  private Project.NameKey project;
-  private String revision;
-  private ReviewInput input;
-  private ReplyAction replyAction;
-
-  QuickApprove() {
-    addClickHandler(this);
-  }
-
-  void set(ChangeInfo info, String commit, ReplyAction action) {
-    if (!info.hasPermittedLabels() || !info.status().isOpen()) {
-      // Quick approve needs at least one label on an open change.
-      setVisible(false);
-      return;
-    }
-    if (info.revision(commit).isEdit()) {
-      setVisible(false);
-      return;
-    }
-
-    String qName = null;
-    String qValueStr = null;
-    short qValue = 0;
-
-    int index = info.getMissingLabelIndex();
-    if (index != -1) {
-      LabelInfo label = Natives.asList(info.allLabels().values()).get(index);
-      JsArrayString values = info.permittedValues(label.name());
-      String s = values.get(values.length() - 1);
-      short v = LabelInfo.parseValue(s);
-      if (v > 0 && s.equals(label.maxValue())) {
-        qName = label.name();
-        qValueStr = s;
-        qValue = v;
-      }
-    }
-
-    if (qName != null) {
-      changeId = info.legacyId();
-      project = info.projectNameKey();
-      revision = commit;
-      input = ReviewInput.create();
-      input.drafts(DraftHandling.PUBLISH_ALL_REVISIONS);
-      input.label(qName, qValue);
-      replyAction = action;
-      setText(qName + qValueStr);
-      setVisible(true);
-    } else {
-      setVisible(false);
-    }
-  }
-
-  @Override
-  public void setText(String text) {
-    setHTML(new SafeHtmlBuilder().openDiv().append(text).closeDiv());
-  }
-
-  @Override
-  public void onClick(ClickEvent event) {
-    if (replyAction != null && replyAction.isVisible()) {
-      replyAction.quickApprove(input);
-    } else {
-      ChangeApi.revision(project.get(), changeId.get(), revision)
-          .view("review")
-          .post(
-              input,
-              new GerritCallback<ReviewInput>() {
-                @Override
-                public void onSuccess(ReviewInput result) {
-                  Gerrit.display(PageLinks.toChange(project, changeId));
-                }
-              });
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RebaseAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RebaseAction.java
deleted file mode 100644
index 0e3e835..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RebaseAction.java
+++ /dev/null
@@ -1,70 +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.client.change;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.RebaseDialog;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.event.logical.shared.CloseEvent;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.PopupPanel;
-
-class RebaseAction {
-  static void call(
-      final Button b,
-      final Project.NameKey project,
-      final String branch,
-      final Change.Id id,
-      final String revision,
-      final boolean enabled) {
-    b.setEnabled(false);
-
-    new RebaseDialog(project, branch, id, enabled) {
-      @Override
-      public void onSend() {
-        ChangeApi.rebase(
-            project.get(),
-            id.get(),
-            revision,
-            getBase(),
-            new GerritCallback<ChangeInfo>() {
-              @Override
-              public void onSuccess(ChangeInfo result) {
-                sent = true;
-                hide();
-                Gerrit.display(PageLinks.toChange(project, id));
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {
-                enableButtons(true);
-                super.onFailure(caught);
-              }
-            });
-      }
-
-      @Override
-      public void onClose(CloseEvent<PopupPanel> event) {
-        super.onClose(event);
-        b.setEnabled(true);
-      }
-    }.center();
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java
deleted file mode 100644
index 2c52800..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java
+++ /dev/null
@@ -1,459 +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.client.change;
-
-import static com.google.gerrit.common.PageLinks.op;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeList;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.info.ChangeInfo.CommitInfo;
-import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.extensions.client.ListChangesOption;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.event.logical.shared.SelectionEvent;
-import com.google.gwt.event.logical.shared.SelectionHandler;
-import com.google.gwt.resources.client.ClientBundle;
-import com.google.gwt.resources.client.CssResource;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.TabBar;
-import com.google.gwt.user.client.ui.TabPanel;
-import java.util.ArrayList;
-import java.util.EnumSet;
-import java.util.List;
-
-public class RelatedChanges extends TabPanel {
-  static final RelatedChangesResources R = GWT.create(RelatedChangesResources.class);
-
-  interface RelatedChangesResources extends ClientBundle {
-    @Source("related_changes.css")
-    RelatedChangesCss css();
-  }
-
-  interface RelatedChangesCss extends CssResource {
-    String activeRow();
-
-    String current();
-
-    String gitweb();
-
-    String indirect();
-
-    String notCurrent();
-
-    String pointer();
-
-    String row();
-
-    String subject();
-
-    String strikedSubject();
-
-    String submittable();
-
-    String tabPanel();
-  }
-
-  enum Tab {
-    RELATED_CHANGES(Resources.C.relatedChanges(), Resources.C.relatedChangesTooltip()) {
-      @Override
-      String getTitle(int count) {
-        return Resources.M.relatedChanges(count);
-      }
-
-      @Override
-      String getTitle(String count) {
-        return Resources.M.relatedChanges(count);
-      }
-    },
-
-    SUBMITTED_TOGETHER(Resources.C.submittedTogether(), Resources.C.submittedTogether()) {
-      @Override
-      String getTitle(int count) {
-        return Resources.M.submittedTogether(count);
-      }
-
-      @Override
-      String getTitle(String count) {
-        return Resources.M.submittedTogether(count);
-      }
-    },
-
-    SAME_TOPIC(Resources.C.sameTopic(), Resources.C.sameTopicTooltip()) {
-      @Override
-      String getTitle(int count) {
-        return Resources.M.sameTopic(count);
-      }
-
-      @Override
-      String getTitle(String count) {
-        return Resources.M.sameTopic(count);
-      }
-    },
-
-    CONFLICTING_CHANGES(Resources.C.conflictingChanges(), Resources.C.conflictingChangesTooltip()) {
-      @Override
-      String getTitle(int count) {
-        return Resources.M.conflictingChanges(count);
-      }
-
-      @Override
-      String getTitle(String count) {
-        return Resources.M.conflictingChanges(count);
-      }
-    },
-
-    CHERRY_PICKS(Resources.C.cherryPicks(), Resources.C.cherryPicksTooltip()) {
-      @Override
-      String getTitle(int count) {
-        return Resources.M.cherryPicks(count);
-      }
-
-      @Override
-      String getTitle(String count) {
-        return Resources.M.cherryPicks(count);
-      }
-    };
-
-    final String defaultTitle;
-    final String tooltip;
-
-    abstract String getTitle(int count);
-
-    abstract String getTitle(String count);
-
-    Tab(String defaultTitle, String tooltip) {
-      this.defaultTitle = defaultTitle;
-      this.tooltip = tooltip;
-    }
-  }
-
-  private static Tab savedTab;
-
-  private final List<RelatedChangesTab> tabs;
-  private int maxHeightWithHeader;
-  private int selectedTab;
-  private int outstandingCallbacks;
-
-  RelatedChanges() {
-    tabs = new ArrayList<>(Tab.values().length);
-    selectedTab = -1;
-
-    setVisible(false);
-    addStyleName(R.css().tabPanel());
-    initTabBar();
-  }
-
-  private void initTabBar() {
-    TabBar tabBar = getTabBar();
-    tabBar.addSelectionHandler(
-        new SelectionHandler<Integer>() {
-          @Override
-          public void onSelection(SelectionEvent<Integer> event) {
-            if (selectedTab >= 0) {
-              tabs.get(selectedTab).registerKeys(false);
-            }
-            selectedTab = event.getSelectedItem();
-            tabs.get(selectedTab).registerKeys(true);
-          }
-        });
-
-    for (Tab tabInfo : Tab.values()) {
-      RelatedChangesTab panel = new RelatedChangesTab(tabInfo);
-      add(panel, tabInfo.defaultTitle);
-      tabs.add(panel);
-
-      TabBar.Tab tab = tabBar.getTab(tabInfo.ordinal());
-      tab.setWordWrap(false);
-      ((Composite) tab).setTitle(tabInfo.tooltip);
-
-      setTabEnabled(tabInfo, false);
-    }
-    getTab(Tab.RELATED_CHANGES).setShowIndirectAncestors(true);
-    getTab(Tab.CHERRY_PICKS).setShowBranches(true);
-    getTab(Tab.SAME_TOPIC).setShowBranches(true);
-    getTab(Tab.SAME_TOPIC).setShowProjects(true);
-    getTab(Tab.SAME_TOPIC).setShowSubmittable(true);
-    getTab(Tab.SUBMITTED_TOGETHER).setShowBranches(true);
-    getTab(Tab.SUBMITTED_TOGETHER).setShowProjects(true);
-    getTab(Tab.SUBMITTED_TOGETHER).setShowSubmittable(true);
-  }
-
-  void set(ChangeInfo info, String revision) {
-    if (info.status().isOpen()) {
-      setForOpenChange(info, revision);
-    }
-
-    ChangeApi.revision(info.project(), info.legacyId().get(), revision)
-        .view("related")
-        .get(
-            new TabCallback<RelatedInfo>(Tab.RELATED_CHANGES, info.project(), revision) {
-              @Override
-              public JsArray<ChangeAndCommit> convert(RelatedInfo result) {
-                return result.changes();
-              }
-            });
-
-    StringBuilder cherryPicksQuery = new StringBuilder();
-    cherryPicksQuery.append(op("project", info.project()));
-    cherryPicksQuery.append(" ").append(op("change", info.changeId()));
-    cherryPicksQuery.append(" ").append(op("-change", info.legacyId().get()));
-    cherryPicksQuery.append(" -is:abandoned");
-    ChangeList.query(
-        cherryPicksQuery.toString(),
-        EnumSet.of(ListChangesOption.CURRENT_REVISION, ListChangesOption.CURRENT_COMMIT),
-        new TabChangeListCallback(Tab.CHERRY_PICKS, info.project(), revision));
-
-    if (info.currentRevision() != null && info.currentRevision().equals(revision)) {
-      ChangeApi.change(info.project(), info.legacyId().get())
-          .view("submitted_together")
-          .addParameter("o", "CURRENT_COMMIT")
-          .get(new TabChangeListCallback(Tab.SUBMITTED_TOGETHER, info.project(), revision));
-    }
-
-    if (!Gerrit.info().change().isSubmitWholeTopicEnabled()
-        && info.topic() != null
-        && !"".equals(info.topic())) {
-      StringBuilder topicQuery =
-          new StringBuilder()
-              .append("status:open")
-              .append(" ")
-              .append(op("-change", info.legacyId().get()))
-              .append(" ")
-              .append(op("topic", info.topic()));
-      ChangeList.query(
-          topicQuery.toString(),
-          EnumSet.of(
-              ListChangesOption.CURRENT_REVISION,
-              ListChangesOption.CURRENT_COMMIT,
-              ListChangesOption.DETAILED_LABELS,
-              ListChangesOption.LABELS),
-          new TabChangeListCallback(Tab.SAME_TOPIC, info.project(), revision));
-    }
-  }
-
-  private void setForOpenChange(ChangeInfo info, String revision) {
-    if (info.mergeable()) {
-      StringBuilder conflictsQuery = new StringBuilder();
-      conflictsQuery.append("status:open");
-      conflictsQuery.append(" is:mergeable");
-      conflictsQuery.append(" ").append(op("conflicts", info.legacyId().get()));
-      ChangeList.query(
-          conflictsQuery.toString(),
-          EnumSet.of(ListChangesOption.CURRENT_REVISION, ListChangesOption.CURRENT_COMMIT),
-          new TabChangeListCallback(Tab.CONFLICTING_CHANGES, info.project(), revision));
-    }
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    R.css().ensureInjected();
-  }
-
-  static void setSavedTab(Tab subject) {
-    savedTab = subject;
-  }
-
-  private RelatedChangesTab getTab(Tab tabInfo) {
-    return tabs.get(tabInfo.ordinal());
-  }
-
-  private void setTabTitle(Tab tabInfo, String title) {
-    getTabBar().setTabText(tabInfo.ordinal(), title);
-  }
-
-  private void setTabEnabled(Tab tabInfo, boolean enabled) {
-    getTabBar().setTabEnabled(tabInfo.ordinal(), enabled);
-  }
-
-  void setMaxHeight(int height) {
-    maxHeightWithHeader = height;
-    if (isVisible()) {
-      applyMaxHeight();
-    }
-  }
-
-  private void applyMaxHeight() {
-    int header = getTabBar().getOffsetHeight() + 2 /* padding */;
-    for (int i = 0; i < getTabBar().getTabCount(); i++) {
-      tabs.get(i).setMaxHeight(maxHeightWithHeader - header);
-    }
-  }
-
-  private abstract class TabCallback<T> implements AsyncCallback<T> {
-    private final Tab tabInfo;
-    private final String project;
-    private final String revision;
-
-    TabCallback(Tab tabInfo, String project, String revision) {
-      this.tabInfo = tabInfo;
-      this.project = project;
-      this.revision = revision;
-      outstandingCallbacks++;
-    }
-
-    protected abstract JsArray<ChangeAndCommit> convert(T result);
-
-    @Override
-    public void onSuccess(T result) {
-      if (isAttached()) {
-        JsArray<ChangeAndCommit> changes = convert(result);
-        if (changes.length() > 0) {
-          setTabTitle(tabInfo, tabInfo.getTitle(changes.length()));
-          getTab(tabInfo).setChanges(project, revision, changes);
-        }
-        onDone(changes.length() > 0);
-      }
-    }
-
-    @Override
-    public void onFailure(Throwable err) {
-      if (isAttached()) {
-        setTabTitle(tabInfo, tabInfo.getTitle(Resources.C.notAvailable()));
-        getTab(tabInfo).setError(err.getMessage());
-        onDone(true);
-      }
-    }
-
-    private void onDone(boolean enabled) {
-      setTabEnabled(tabInfo, enabled);
-      outstandingCallbacks--;
-      if (outstandingCallbacks == 0 || (enabled && tabInfo == Tab.RELATED_CHANGES)) {
-        outstandingCallbacks = 0; // Only execute this block once
-        for (int i = 0; i < getTabBar().getTabCount(); i++) {
-          if (getTabBar().isTabEnabled(i)) {
-            selectTab(i);
-            setVisible(true);
-            applyMaxHeight();
-            break;
-          }
-        }
-      }
-
-      if (tabInfo == savedTab && enabled) {
-        selectTab(savedTab.ordinal());
-      }
-    }
-  }
-
-  private class TabChangeListCallback extends TabCallback<ChangeList> {
-    TabChangeListCallback(Tab tabInfo, String project, String revision) {
-      super(tabInfo, project, revision);
-    }
-
-    @Override
-    protected JsArray<ChangeAndCommit> convert(ChangeList l) {
-      JsArray<ChangeAndCommit> arr = JavaScriptObject.createArray().cast();
-      for (ChangeInfo i : Natives.asList(l)) {
-        if (i.currentRevision() != null && i.revisions().containsKey(i.currentRevision())) {
-          RevisionInfo currentRevision = i.revision(i.currentRevision());
-          ChangeAndCommit c = ChangeAndCommit.create();
-          c.setId(i.id());
-          c.setCommit(currentRevision.commit());
-          c.setChangeNumber(i.legacyId().get());
-          c.setRevisionNumber(currentRevision._number());
-          c.setBranch(i.branch());
-          c.setProject(i.project());
-          c.setSubmittable(i.submittable() && i.mergeable());
-          c.setStatus(i.status().asChangeStatus().toString());
-          arr.push(c);
-        }
-      }
-      return arr;
-    }
-  }
-
-  public static class RelatedInfo extends JavaScriptObject {
-    public final native JsArray<ChangeAndCommit> changes() /*-{ return this.changes }-*/;
-
-    protected RelatedInfo() {}
-  }
-
-  public static class ChangeAndCommit extends JavaScriptObject {
-    static ChangeAndCommit create() {
-      return (ChangeAndCommit) createObject();
-    }
-
-    public final native String id() /*-{ return this.change_id }-*/;
-
-    public final native CommitInfo commit() /*-{ return this.commit }-*/;
-
-    final native String branch() /*-{ return this.branch }-*/;
-
-    final native String project() /*-{ return this.project }-*/;
-
-    final native boolean submittable() /*-{ return this._submittable ? true : false; }-*/;
-
-    final Change.Status status() {
-      String s = statusRaw();
-      return s != null ? Change.Status.valueOf(s) : null;
-    }
-
-    private native String statusRaw() /*-{ return this.status; }-*/;
-
-    final native void setId(String i) /*-{ if(i)this.change_id=i; }-*/;
-
-    final native void setCommit(CommitInfo c) /*-{ if(c)this.commit=c; }-*/;
-
-    final native void setBranch(String b) /*-{ if(b)this.branch=b; }-*/;
-
-    final native void setProject(String b) /*-{ if(b)this.project=b; }-*/;
-
-    public final Change.Id legacyId() {
-      return hasChangeNumber() ? new Change.Id(_changeNumber()) : null;
-    }
-
-    public final PatchSet.Id patchSetId() {
-      return hasChangeNumber() && hasRevisionNumber()
-          ? new PatchSet.Id(legacyId(), _revisionNumber())
-          : null;
-    }
-
-    public final native boolean hasChangeNumber()
-        /*-{ return this.hasOwnProperty('_change_number') }-*/ ;
-
-    final native boolean hasRevisionNumber()
-        /*-{ return this.hasOwnProperty('_revision_number') }-*/ ;
-
-    final native boolean hasCurrentRevisionNumber()
-        /*-{ return this.hasOwnProperty('_current_revision_number') }-*/ ;
-
-    final native int _changeNumber() /*-{ return this._change_number }-*/;
-
-    final native int _revisionNumber() /*-{ return this._revision_number }-*/;
-
-    final native int _currentRevisionNumber() /*-{ return this._current_revision_number }-*/;
-
-    final native void setChangeNumber(int n) /*-{ this._change_number=n; }-*/;
-
-    final native void setRevisionNumber(int n) /*-{ this._revision_number=n; }-*/;
-
-    final native void setCurrentRevisionNumber(int n) /*-{ this._current_revision_number=n; }-*/;
-
-    final native void setSubmittable(boolean s) /*-{ this._submittable=s; }-*/;
-
-    final native void setStatus(String s) /*-{ if(s)this.status=s; }-*/;
-
-    protected ChangeAndCommit() {}
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChangesTab.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChangesTab.java
deleted file mode 100644
index c53427b..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChangesTab.java
+++ /dev/null
@@ -1,594 +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.client.change;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.change.RelatedChanges.ChangeAndCommit;
-import com.google.gerrit.client.changes.Util;
-import com.google.gerrit.client.info.ChangeInfo.CommitInfo;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.core.client.Scheduler;
-import com.google.gwt.core.client.Scheduler.RepeatingCommand;
-import com.google.gwt.dom.client.AnchorElement;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.NativeEvent;
-import com.google.gwt.dom.client.Node;
-import com.google.gwt.dom.client.NodeList;
-import com.google.gwt.dom.client.Style;
-import com.google.gwt.dom.client.Style.Visibility;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.DoubleClickEvent;
-import com.google.gwt.event.dom.client.DoubleClickHandler;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.ScrollEvent;
-import com.google.gwt.event.dom.client.ScrollHandler;
-import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.safehtml.shared.SafeHtml;
-import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.Event;
-import com.google.gwt.user.client.Window;
-import com.google.gwt.user.client.ui.AbstractImagePrototype;
-import com.google.gwt.user.client.ui.InlineLabel;
-import com.google.gwt.user.client.ui.IsWidget;
-import com.google.gwt.user.client.ui.ScrollPanel;
-import com.google.gwt.user.client.ui.SimplePanel;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwt.user.client.ui.impl.HyperlinkImpl;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-import com.google.gwtexpui.globalkey.client.KeyCommand;
-import com.google.gwtexpui.globalkey.client.KeyCommandSet;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-class RelatedChangesTab implements IsWidget {
-  private static final String OPEN = init(DOM.createUniqueId().replace('-', '_'));
-  private static final HyperlinkImpl LINK = GWT.create(HyperlinkImpl.class);
-  private static final SafeHtml POINTER_HTML =
-      AbstractImagePrototype.create(Gerrit.RESOURCES.arrowRight()).getSafeHtml();
-
-  private static native String init(String o) /*-{
-    $wnd[o] = $entry(@com.google.gerrit.client.change.RelatedChangesTab::onOpen(
-      Lcom/google/gwt/dom/client/NativeEvent;Lcom/google/gwt/dom/client/Element;));
-    return o + '(event,this)';
-  }-*/;
-
-  private static boolean onOpen(NativeEvent evt, Element e) {
-    if (LINK.handleAsClick(evt.<Event>cast())) {
-      Gerrit.display(e.getAttribute("href").substring(1));
-      evt.preventDefault();
-      return false;
-    }
-    return true;
-  }
-
-  private final SimplePanel panel;
-  private final RelatedChanges.Tab subject;
-
-  private boolean showBranches;
-  private boolean showProjects;
-  private boolean showSubmittable;
-  private boolean showIndirectAncestors;
-  private boolean registerKeys;
-  private int maxHeight;
-
-  private String project;
-  private NavigationList view;
-
-  RelatedChangesTab(RelatedChanges.Tab subject) {
-    panel = new SimplePanel();
-    this.subject = subject;
-  }
-
-  @Override
-  public Widget asWidget() {
-    return panel;
-  }
-
-  void setShowBranches(boolean showBranches) {
-    this.showBranches = showBranches;
-  }
-
-  void setShowProjects(boolean showProjects) {
-    this.showProjects = showProjects;
-  }
-
-  void setShowSubmittable(boolean submittable) {
-    this.showSubmittable = submittable;
-  }
-
-  void setShowIndirectAncestors(boolean showIndirectAncestors) {
-    this.showIndirectAncestors = showIndirectAncestors;
-  }
-
-  void setMaxHeight(int height) {
-    maxHeight = height;
-    if (view != null) {
-      view.setHeight(height + "px");
-      view.ensureRowMeasurements();
-      view.movePointerTo(view.selectedRow, true);
-    }
-  }
-
-  void registerKeys(boolean on) {
-    registerKeys = on;
-    if (view != null) {
-      view.setRegisterKeys(on);
-    }
-  }
-
-  void setError(String message) {
-    panel.setWidget(new InlineLabel(message));
-    view = null;
-    project = null;
-  }
-
-  void setChanges(String project, String revision, JsArray<ChangeAndCommit> changes) {
-    if (0 == changes.length()) {
-      setError(Resources.C.noChanges());
-      return;
-    }
-
-    this.project = project;
-    view = new NavigationList();
-    panel.setWidget(view);
-
-    DisplayCommand display = new DisplayCommand(revision, changes, view);
-    if (display.execute()) {
-      Scheduler.get().scheduleIncremental(display);
-    }
-  }
-
-  private final class DisplayCommand implements RepeatingCommand {
-    private final String revision;
-    private final JsArray<ChangeAndCommit> changes;
-    private final List<SafeHtml> rows;
-    private final Set<String> connected;
-    private final NavigationList navList;
-
-    private double start;
-    private int row;
-    private int connectedPos;
-    private int selected;
-
-    private DisplayCommand(
-        String revision, JsArray<ChangeAndCommit> changes, NavigationList navList) {
-      this.revision = revision;
-      this.changes = changes;
-      this.navList = navList;
-      rows = new ArrayList<>(changes.length());
-      connectedPos = changes.length() - 1;
-      connected =
-          showIndirectAncestors ? new HashSet<>(Math.max(changes.length() * 4 / 3, 16)) : null;
-    }
-
-    private boolean computeConnected() {
-      // Since TOPO sorted, when can walk the list in reverse and find all
-      // the connections.
-      if (!connected.contains(revision)) {
-        while (connectedPos >= 0) {
-          CommitInfo c = changes.get(connectedPos).commit();
-          connected.add(c.commit());
-          if (longRunning(--connectedPos)) {
-            return true;
-          }
-          if (c.commit().equals(revision)) {
-            break;
-          }
-        }
-      }
-      while (connectedPos >= 0) {
-        CommitInfo c = changes.get(connectedPos).commit();
-        for (int j = 0; j < c.parents().length(); j++) {
-          if (connected.contains(c.parents().get(j).commit())) {
-            connected.add(c.commit());
-            break;
-          }
-        }
-        if (longRunning(--connectedPos)) {
-          return true;
-        }
-      }
-      return false;
-    }
-
-    @Override
-    public boolean execute() {
-      if (navList != view || !panel.isAttached()) {
-        // If the user navigated away, we aren't in the DOM anymore.
-        // Don't continue to render.
-        return false;
-      }
-
-      start = System.currentTimeMillis();
-
-      if (connected != null && computeConnected()) {
-        return true;
-      }
-
-      while (row < changes.length()) {
-        ChangeAndCommit info = changes.get(row);
-        String commit = info.commit().commit();
-        rows.add(new RowSafeHtml(info, connected != null && !connected.contains(commit)));
-        if (revision.equals(commit)) {
-          selected = row;
-        }
-        if (longRunning(++row)) {
-          return true;
-        }
-      }
-
-      navList.rows = rows;
-      navList.ensureRowMeasurements();
-      navList.movePointerTo(selected, true);
-      return false;
-    }
-
-    private boolean longRunning(int i) {
-      return (i % 10) == 0 && System.currentTimeMillis() - start > 50;
-    }
-  }
-
-  private class RowSafeHtml implements SafeHtml {
-    private static final long serialVersionUID = 1L;
-
-    private String html;
-    private ChangeAndCommit info;
-    private final boolean notConnected;
-
-    RowSafeHtml(ChangeAndCommit info, boolean notConnected) {
-      this.info = info;
-      this.notConnected = notConnected;
-    }
-
-    @Override
-    public String asString() {
-      if (html == null) {
-        SafeHtmlBuilder sb = new SafeHtmlBuilder();
-        renderRow(sb);
-        html = sb.asString();
-        info = null;
-      }
-      return html;
-    }
-
-    private void renderRow(SafeHtmlBuilder sb) {
-      sb.openDiv().setStyleName(RelatedChanges.R.css().row());
-
-      sb.openSpan().setStyleName(RelatedChanges.R.css().pointer());
-      sb.append(POINTER_HTML);
-      sb.closeSpan();
-
-      if (info.status() == Change.Status.ABANDONED) {
-        sb.openSpan().setStyleName(RelatedChanges.R.css().strikedSubject());
-      } else {
-        sb.openSpan().setStyleName(RelatedChanges.R.css().subject());
-      }
-      sb.setAttribute("data-branch", info.branch());
-      sb.setAttribute("data-project", info.project());
-      String url = url();
-      if (url != null) {
-        sb.openAnchor().setAttribute("href", url);
-        if (url.startsWith("#")) {
-          sb.setAttribute("onclick", OPEN);
-        }
-        sb.setAttribute("title", info.commit().subject());
-        if (showProjects) {
-          sb.append(info.project()).append(": ");
-        }
-        if (showBranches) {
-          sb.append(info.branch()).append(": ");
-        }
-        sb.append(info.commit().subject());
-        sb.closeAnchor();
-      } else {
-        sb.append(info.commit().subject());
-      }
-      sb.closeSpan();
-
-      sb.openSpan();
-      if (info.status() != null && !info.status().isOpen()) {
-        sb.setStyleName(RelatedChanges.R.css().gitweb());
-        sb.setAttribute("title", Util.toLongString(info.status()));
-        sb.append('\u25CF'); // Unicode 'BLACK CIRCLE'
-      } else if (notConnected) {
-        sb.setStyleName(RelatedChanges.R.css().indirect());
-        sb.setAttribute("title", Resources.C.indirectAncestor());
-        sb.append('~');
-      } else if (info.hasCurrentRevisionNumber()
-          && info.hasRevisionNumber()
-          && info._currentRevisionNumber() != info._revisionNumber()) {
-        sb.setStyleName(RelatedChanges.R.css().notCurrent());
-        sb.setAttribute("title", Util.C.notCurrent());
-        sb.append('\u25CF'); // Unicode 'BLACK CIRCLE'
-      } else if (showSubmittable && info.submittable()) {
-        sb.setStyleName(RelatedChanges.R.css().submittable());
-        sb.setAttribute("title", Util.C.submittable());
-        sb.append('\u2713'); // Unicode 'CHECK MARK'
-      } else {
-        sb.setStyleName(RelatedChanges.R.css().current());
-      }
-      sb.closeSpan();
-
-      sb.closeDiv();
-    }
-
-    private String url() {
-      if (info.hasChangeNumber() && info.hasRevisionNumber()) {
-        return "#" + PageLinks.toChange(new Project.NameKey(info.project()), info.patchSetId());
-      }
-      return null;
-    }
-  }
-
-  private class NavigationList extends ScrollPanel
-      implements ClickHandler, DoubleClickHandler, ScrollHandler {
-    private final KeyCommandSet keysNavigation;
-    private final Element body;
-    private final Element surrogate;
-    private final Node fragment = createDocumentFragment();
-
-    List<SafeHtml> rows;
-    private HandlerRegistration regNavigation;
-    private int selectedRow;
-    private int startRow;
-    private int rowHeight;
-    private int rowWidth;
-
-    NavigationList() {
-      addDomHandler(this, ClickEvent.getType());
-      addDomHandler(this, DoubleClickEvent.getType());
-      addScrollHandler(this);
-
-      keysNavigation = new KeyCommandSet(Resources.C.relatedChanges());
-      keysNavigation.add(
-          new KeyCommand(0, 'K', Resources.C.previousChange()) {
-            @Override
-            public void onKeyPress(KeyPressEvent event) {
-              movePointerTo(selectedRow - 1, true);
-            }
-          },
-          new KeyCommand(0, 'J', Resources.C.nextChange()) {
-            @Override
-            public void onKeyPress(KeyPressEvent event) {
-              movePointerTo(selectedRow + 1, true);
-            }
-          });
-      keysNavigation.add(
-          new KeyCommand(0, 'O', Resources.C.openChange()) {
-            @Override
-            public void onKeyPress(KeyPressEvent event) {
-              onOpenRow(getRow(selectedRow));
-            }
-          });
-
-      if (maxHeight > 0) {
-        setHeight(maxHeight + "px");
-      }
-
-      body = DOM.createDiv();
-      body.getStyle().setPosition(Style.Position.RELATIVE);
-      body.getStyle().setVisibility(Visibility.HIDDEN);
-      getContainerElement().appendChild(body);
-
-      surrogate = DOM.createDiv();
-      surrogate.getStyle().setVisibility(Visibility.HIDDEN);
-    }
-
-    private boolean ensureRowMeasurements() {
-      if (rowHeight == 0 && rows != null) {
-        surrogate.setInnerSafeHtml(rows.get(0));
-        getContainerElement().appendChild(surrogate);
-        rowHeight = surrogate.getOffsetHeight();
-        rowWidth = surrogate.getOffsetWidth();
-        getContainerElement().removeChild(surrogate);
-        getContainerElement().getStyle().setHeight(rowHeight * rows.size(), Style.Unit.PX);
-        return true;
-      }
-      return false;
-    }
-
-    public void movePointerTo(int row, boolean scroll) {
-      if (rows != null && 0 <= row && row < rows.size()) {
-        renderSelected(selectedRow, false);
-        selectedRow = row;
-
-        if (scroll && rowHeight != 0) {
-          // Position the selected row in the middle.
-          setVerticalScrollPosition(Math.max(rowHeight * selectedRow - maxHeight / 2, 0));
-          render();
-        }
-        renderSelected(selectedRow, true);
-      }
-    }
-
-    private void renderSelected(int row, boolean selected) {
-      Element e = getRow(row);
-      if (e != null) {
-        if (selected) {
-          e.addClassName(RelatedChanges.R.css().activeRow());
-        } else {
-          e.removeClassName(RelatedChanges.R.css().activeRow());
-        }
-      }
-    }
-
-    private void render() {
-      if (rows == null || rowHeight == 0) {
-        return;
-      }
-
-      int currStart = startRow;
-      int currEnd = startRow + body.getChildCount();
-
-      int vpos = getVerticalScrollPosition();
-      int start = Math.max(vpos / rowHeight - 5, 0);
-      int end = Math.min((vpos + maxHeight) / rowHeight + 5, rows.size());
-      if (currStart <= start && end <= currEnd) {
-        return; // All of the required nodes are already in the DOM.
-      }
-
-      if (end <= currStart) {
-        renderRange(start, end, true, true);
-      } else if (start < currStart) {
-        renderRange(start, currStart, false, true);
-      } else if (start >= currEnd) {
-        renderRange(start, end, true, false);
-      } else if (end > currEnd) {
-        renderRange(currEnd, end, false, false);
-      }
-
-      renderSelected(selectedRow, true);
-
-      if (currEnd == 0) {
-        // Account for the scroll bars
-        int width = body.getOffsetWidth();
-        if (rowWidth > width) {
-          int w = 2 * rowWidth - width;
-          setWidth(w + "px");
-        }
-        body.getStyle().clearVisibility();
-      }
-    }
-
-    private void renderRange(int start, int end, boolean removeAll, boolean insertFirst) {
-      SafeHtmlBuilder sb = new SafeHtmlBuilder();
-      for (int i = start; i < end; i++) {
-        sb.append(rows.get(i));
-      }
-
-      if (removeAll) {
-        body.setInnerSafeHtml(sb);
-      } else {
-        surrogate.setInnerSafeHtml(sb);
-        for (int cnt = surrogate.getChildCount(); cnt > 0; cnt--) {
-          fragment.appendChild(surrogate.getFirstChild());
-        }
-        if (insertFirst) {
-          body.insertFirst(fragment);
-        } else {
-          body.appendChild(fragment);
-        }
-      }
-
-      if (insertFirst || removeAll) {
-        startRow = start;
-        body.getStyle().setTop(start * rowHeight, Style.Unit.PX);
-      }
-    }
-
-    @Override
-    public void onClick(ClickEvent event) {
-      Element row = getRow(event.getNativeEvent().getEventTarget().<Element>cast());
-      if (row != null) {
-        movePointerTo(startRow + DOM.getChildIndex(body, row), false);
-        event.stopPropagation();
-      }
-      saveSelectedTab();
-    }
-
-    @Override
-    public void onDoubleClick(DoubleClickEvent event) {
-      Element row = getRow(event.getNativeEvent().getEventTarget().<Element>cast());
-      if (row != null) {
-        movePointerTo(startRow + DOM.getChildIndex(body, row), false);
-        onOpenRow(row);
-        event.stopPropagation();
-      }
-    }
-
-    @Override
-    public void onScroll(ScrollEvent event) {
-      render();
-    }
-
-    private Element getRow(Element e) {
-      for (Element prev = e; e != null; prev = e) {
-        if ((e = DOM.getParent(e)) == body) {
-          return prev;
-        }
-      }
-      return null;
-    }
-
-    private Element getRow(int row) {
-      if (startRow <= row && row < startRow + body.getChildCount()) {
-        return body.getChild(row - startRow).cast();
-      }
-      return null;
-    }
-
-    private void onOpenRow(Element row) {
-      // Find the first HREF of the anchor of the select row (if any)
-      if (row != null) {
-        NodeList<Element> nodes = row.getElementsByTagName(AnchorElement.TAG);
-        for (int i = 0; i < nodes.getLength(); i++) {
-          String url = nodes.getItem(i).getAttribute("href");
-          if (!url.isEmpty()) {
-            if (url.startsWith("#")) {
-              Gerrit.display(url.substring(1));
-            } else {
-              Window.Location.assign(url);
-            }
-            break;
-          }
-        }
-      }
-
-      saveSelectedTab();
-    }
-
-    private void saveSelectedTab() {
-      RelatedChanges.setSavedTab(subject);
-    }
-
-    @Override
-    protected void onLoad() {
-      super.onLoad();
-      setRegisterKeys(registerKeys);
-    }
-
-    @Override
-    protected void onUnload() {
-      setRegisterKeys(false);
-      super.onUnload();
-    }
-
-    public void setRegisterKeys(boolean on) {
-      if (on && isAttached()) {
-        if (regNavigation == null) {
-          regNavigation = GlobalKey.add(this, keysNavigation);
-        }
-        if (view.ensureRowMeasurements()) {
-          view.movePointerTo(view.selectedRow, true);
-        }
-      } else if (regNavigation != null) {
-        regNavigation.removeHandler();
-        regNavigation = null;
-      }
-    }
-  }
-
-  private static native Node createDocumentFragment() /*-{
-    return $doc.createDocumentFragment();
-  }-*/;
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileAction.java
deleted file mode 100644
index 1e7063a..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileAction.java
+++ /dev/null
@@ -1,78 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.change;
-
-import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.event.logical.shared.CloseEvent;
-import com.google.gwt.event.logical.shared.CloseHandler;
-import com.google.gwt.user.client.ui.PopupPanel;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-
-class RenameFileAction {
-  private final Project.NameKey project;
-  private final Change.Id changeId;
-  private final RevisionInfo revision;
-  private final ChangeScreen.Style style;
-  private final Widget renameButton;
-
-  private RenameFileBox renameBox;
-  private PopupPanel popup;
-
-  RenameFileAction(
-      Project.NameKey project,
-      Change.Id changeId,
-      RevisionInfo revision,
-      ChangeScreen.Style style,
-      Widget renameButton) {
-    this.project = project;
-    this.changeId = changeId;
-    this.revision = revision;
-    this.style = style;
-    this.renameButton = renameButton;
-  }
-
-  void onRename() {
-    if (popup != null) {
-      popup.hide();
-      return;
-    }
-
-    if (renameBox == null) {
-      renameBox = new RenameFileBox(project, changeId, revision);
-    }
-    renameBox.clearPath();
-
-    final PopupPanel p = new PopupPanel(true);
-    p.setStyleName(style.replyBox());
-    p.addAutoHidePartner(renameButton.getElement());
-    p.addCloseHandler(
-        new CloseHandler<PopupPanel>() {
-          @Override
-          public void onClose(CloseEvent<PopupPanel> event) {
-            if (popup == p) {
-              popup = null;
-            }
-          }
-        });
-    p.add(renameBox);
-    p.showRelativeTo(renameButton);
-    GlobalKey.dialog(p);
-    renameBox.setFocus(true);
-    popup = p;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileBox.java
deleted file mode 100644
index f288dbe..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileBox.java
+++ /dev/null
@@ -1,116 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.change;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.VoidResult;
-import com.google.gerrit.client.changes.ChangeEditApi;
-import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
-import com.google.gerrit.client.ui.RemoteSuggestBox;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.logical.shared.CloseEvent;
-import com.google.gwt.event.logical.shared.CloseHandler;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.uibinder.client.UiHandler;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwt.user.client.ui.PopupPanel;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtexpui.globalkey.client.NpTextBox;
-
-class RenameFileBox extends Composite {
-  interface Binder extends UiBinder<HTMLPanel, RenameFileBox> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  private final Project.NameKey project;
-  private final Change.Id changeId;
-
-  @UiField Button rename;
-  @UiField Button cancel;
-
-  @UiField(provided = true)
-  RemoteSuggestBox path;
-
-  @UiField NpTextBox newPath;
-
-  RenameFileBox(Project.NameKey project, Change.Id changeId, RevisionInfo revision) {
-    this.project = project;
-    this.changeId = changeId;
-
-    path = new RemoteSuggestBox(new PathSuggestOracle(project, changeId, revision));
-    path.addCloseHandler(
-        new CloseHandler<RemoteSuggestBox>() {
-          @Override
-          public void onClose(CloseEvent<RemoteSuggestBox> event) {
-            hide();
-          }
-        });
-
-    initWidget(uiBinder.createAndBindUi(this));
-  }
-
-  void setFocus(boolean focus) {
-    path.setFocus(focus);
-  }
-
-  void clearPath() {
-    path.setText("");
-  }
-
-  @UiHandler("rename")
-  void onRename(@SuppressWarnings("unused") ClickEvent e) {
-    rename(path.getText(), newPath.getText());
-  }
-
-  private void rename(String path, String newPath) {
-    hide();
-    ChangeEditApi.rename(
-        project.get(),
-        changeId.get(),
-        path,
-        newPath,
-        new AsyncCallback<VoidResult>() {
-          @Override
-          public void onSuccess(VoidResult result) {
-            Gerrit.display(PageLinks.toChangeInEditMode(project, changeId));
-          }
-
-          @Override
-          public void onFailure(Throwable caught) {}
-        });
-  }
-
-  @UiHandler("cancel")
-  void onCancel(@SuppressWarnings("unused") ClickEvent e) {
-    hide();
-  }
-
-  private void hide() {
-    for (Widget w = getParent(); w != null; w = w.getParent()) {
-      if (w instanceof PopupPanel) {
-        ((PopupPanel) w).hide();
-        break;
-      }
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileBox.ui.xml
deleted file mode 100644
index 17e8797..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileBox.ui.xml
+++ /dev/null
@@ -1,47 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder
-    xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'
-    xmlns:u='urn:import:com.google.gerrit.client.ui'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
-  <ui:style gss='false'>
-    .cancel { float: right; }
-  </ui:style>
-  <g:HTMLPanel>
-    <div class='{res.style.section}'>
-      <ui:msg>Old: <u:RemoteSuggestBox ui:field='path' visibleLength='86'/></ui:msg>
-    </div>
-    <div class='{res.style.section}'>
-      <ui:msg>New: <c:NpTextBox ui:field='newPath' visibleLength='86'/></ui:msg>
-    </div>
-    <div class='{res.style.section}'>
-      <g:Button ui:field='rename'
-          title='Rename file in the repository'
-          styleName='{res.style.button}'>
-        <ui:attribute name='title'/>
-        <div><ui:msg>Rename</ui:msg></div>
-      </g:Button>
-      <g:Button ui:field='cancel'
-          styleName='{res.style.button}'
-          addStyleNames='{style.cancel}'>
-          <div>Cancel</div>
-      </g:Button>
-    </div>
-  </g:HTMLPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyAction.java
deleted file mode 100644
index ff09ff5..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyAction.java
+++ /dev/null
@@ -1,127 +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.client.change;
-
-import com.google.gerrit.client.changes.ReviewInput;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.info.ChangeInfo.LabelInfo;
-import com.google.gerrit.client.info.ChangeInfo.MessageInfo;
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.ui.CommentLinkProcessor;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.JsArrayString;
-import com.google.gwt.event.logical.shared.CloseEvent;
-import com.google.gwt.event.logical.shared.CloseHandler;
-import com.google.gwt.user.client.Window;
-import com.google.gwt.user.client.ui.PopupPanel;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-
-class ReplyAction {
-  private final PatchSet.Id psId;
-  private final Project.NameKey project;
-  private final String revision;
-  private final boolean hasDraftComments;
-  private final ChangeScreen.Style style;
-  private final CommentLinkProcessor clp;
-  private final Widget replyButton;
-  private final Widget quickApproveButton;
-
-  private NativeMap<LabelInfo> allLabels;
-  private NativeMap<JsArrayString> permittedLabels;
-
-  private ReplyBox replyBox;
-  private PopupPanel popup;
-
-  ReplyAction(
-      ChangeInfo info,
-      String revision,
-      boolean hasDraftComments,
-      ChangeScreen.Style style,
-      CommentLinkProcessor clp,
-      Widget replyButton,
-      Widget quickApproveButton) {
-    this.psId = new PatchSet.Id(info.legacyId(), info.revisions().get(revision)._number());
-    this.project = info.projectNameKey();
-    this.revision = revision;
-    this.hasDraftComments = hasDraftComments;
-    this.style = style;
-    this.clp = clp;
-    this.replyButton = replyButton;
-    this.quickApproveButton = quickApproveButton;
-
-    boolean current = revision.equals(info.currentRevision());
-    allLabels = info.allLabels();
-    permittedLabels =
-        current && info.hasPermittedLabels()
-            ? info.permittedLabels()
-            : NativeMap.<JsArrayString>create();
-  }
-
-  boolean isVisible() {
-    return popup != null;
-  }
-
-  void quickApprove(ReviewInput input) {
-    replyBox.quickApprove(input);
-  }
-
-  void hide() {
-    if (popup != null) {
-      popup.hide();
-    }
-    return;
-  }
-
-  void onReply(MessageInfo msg) {
-    if (popup != null) {
-      popup.hide();
-      return;
-    }
-
-    if (replyBox == null) {
-      replyBox = new ReplyBox(clp, project, psId, revision, allLabels, permittedLabels);
-      allLabels = null;
-      permittedLabels = null;
-    }
-    if (msg != null) {
-      replyBox.replyTo(msg);
-    }
-
-    final PopupPanel p = new PopupPanel(true, false);
-    p.setStyleName(style.replyBox());
-    p.addAutoHidePartner(replyButton.getElement());
-    p.addAutoHidePartner(quickApproveButton.getElement());
-    p.addCloseHandler(
-        new CloseHandler<PopupPanel>() {
-          @Override
-          public void onClose(CloseEvent<PopupPanel> event) {
-            if (popup == p) {
-              popup = null;
-              if (hasDraftComments || replyBox.hasMessage()) {
-                replyButton.setStyleName(style.highlight());
-              }
-            }
-          }
-        });
-    p.add(replyBox);
-    Window.scrollTo(0, 0);
-    replyButton.removeStyleName(style.highlight());
-    p.showRelativeTo(replyButton);
-    GlobalKey.dialog(p);
-    popup = p;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
deleted file mode 100644
index addd692..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
+++ /dev/null
@@ -1,541 +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.client.change;
-
-import static com.google.gwt.event.dom.client.KeyCodes.KEY_ENTER;
-import static com.google.gwt.event.dom.client.KeyCodes.KEY_MAC_ENTER;
-import static java.util.stream.Collectors.collectingAndThen;
-import static java.util.stream.Collectors.toList;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.CommentInfo;
-import com.google.gerrit.client.changes.ReviewInput;
-import com.google.gerrit.client.changes.ReviewInput.DraftHandling;
-import com.google.gerrit.client.changes.Util;
-import com.google.gerrit.client.info.ChangeInfo.ApprovalInfo;
-import com.google.gerrit.client.info.ChangeInfo.LabelInfo;
-import com.google.gerrit.client.info.ChangeInfo.MessageInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.client.ui.CommentLinkProcessor;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.LabelValue;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.core.client.JsArrayString;
-import com.google.gwt.core.client.Scheduler;
-import com.google.gwt.core.client.Scheduler.RepeatingCommand;
-import com.google.gwt.core.client.Scheduler.ScheduledCommand;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyDownEvent;
-import com.google.gwt.event.dom.client.KeyDownHandler;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwt.event.dom.client.MouseOutEvent;
-import com.google.gwt.event.dom.client.MouseOutHandler;
-import com.google.gwt.event.dom.client.MouseOverEvent;
-import com.google.gwt.event.dom.client.MouseOverHandler;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.logical.shared.ValueChangeHandler;
-import com.google.gwt.resources.client.CssResource;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.uibinder.client.UiHandler;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
-import com.google.gwt.user.client.ui.PopupPanel;
-import com.google.gwt.user.client.ui.RadioButton;
-import com.google.gwt.user.client.ui.ScrollPanel;
-import com.google.gwt.user.client.ui.TextArea;
-import com.google.gwt.user.client.ui.UIObject;
-import com.google.gwt.user.client.ui.Widget;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Set;
-import java.util.TreeSet;
-
-public class ReplyBox extends Composite {
-  interface Binder extends UiBinder<HTMLPanel, ReplyBox> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  interface Styles extends CssResource {
-    String label_name();
-
-    String label_value();
-
-    String label_help();
-  }
-
-  private final CommentLinkProcessor clp;
-  private final Project.NameKey project;
-  private final PatchSet.Id psId;
-  private final String revision;
-  private ReviewInput in = ReviewInput.create();
-  private int labelHelpColumn;
-  private LocalComments lc;
-
-  @UiField Styles style;
-  @UiField TextArea message;
-  @UiField Element labelsParent;
-  @UiField Grid labelsTable;
-  @UiField Button post;
-  @UiField Button cancel;
-  @UiField ScrollPanel commentsPanel;
-  @UiField FlowPanel comments;
-
-  ReplyBox(
-      CommentLinkProcessor clp,
-      Project.NameKey project,
-      PatchSet.Id psId,
-      String revision,
-      NativeMap<LabelInfo> all,
-      NativeMap<JsArrayString> permitted) {
-    this.clp = clp;
-    this.project = project;
-    this.psId = psId;
-    this.revision = revision;
-    this.lc = new LocalComments(project, psId.getParentKey());
-    initWidget(uiBinder.createAndBindUi(this));
-
-    List<String> names =
-        permitted.keySet().stream()
-            .sorted()
-            .collect(collectingAndThen(toList(), Collections::unmodifiableList));
-    if (names.isEmpty()) {
-      UIObject.setVisible(labelsParent, false);
-    } else {
-      renderLabels(names, all, permitted);
-    }
-
-    addDomHandler(
-        new KeyDownHandler() {
-          @Override
-          public void onKeyDown(KeyDownEvent e) {
-            e.stopPropagation();
-            if ((e.getNativeKeyCode() == KEY_ENTER || e.getNativeKeyCode() == KEY_MAC_ENTER)
-                && (e.isControlKeyDown() || e.isMetaKeyDown())) {
-              e.preventDefault();
-              if (post.isEnabled()) {
-                onPost(null);
-              }
-            }
-          }
-        },
-        KeyDownEvent.getType());
-    addDomHandler(
-        new KeyPressHandler() {
-          @Override
-          public void onKeyPress(KeyPressEvent e) {
-            e.stopPropagation();
-          }
-        },
-        KeyPressEvent.getType());
-  }
-
-  @Override
-  protected void onLoad() {
-    commentsPanel.setVisible(false);
-    post.setEnabled(false);
-    if (lc.hasReplyComment()) {
-      message.setText(lc.getReplyComment());
-      lc.removeReplyComment();
-    }
-    ChangeApi.drafts(project.get(), psId.getParentKey().get())
-        .get(
-            new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
-              @Override
-              public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
-                displayComments(result);
-                post.setEnabled(true);
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {
-                post.setEnabled(true);
-              }
-            });
-
-    Scheduler.get()
-        .scheduleDeferred(
-            new ScheduledCommand() {
-              @Override
-              public void execute() {
-                message.setFocus(true);
-              }
-            });
-    Scheduler.get()
-        .scheduleFixedDelay(
-            new RepeatingCommand() {
-              @Override
-              public boolean execute() {
-                String t = message.getText();
-                if (t != null) {
-                  message.setCursorPos(t.length());
-                }
-                return false;
-              }
-            },
-            0);
-  }
-
-  @UiHandler("post")
-  void onPost(@SuppressWarnings("unused") ClickEvent e) {
-    postReview();
-  }
-
-  void quickApprove(ReviewInput quickApproveInput) {
-    in.mergeLabels(quickApproveInput);
-    postReview();
-  }
-
-  boolean hasMessage() {
-    return !message.getText().trim().isEmpty();
-  }
-
-  private void postReview() {
-    in.message(message.getText().trim());
-    // Don't send any comments in the request; just publish everything, even if
-    // e.g. a draft was modified in another tab since we last looked it up.
-    in.drafts(DraftHandling.PUBLISH_ALL_REVISIONS);
-    in.prePost();
-    ChangeApi.revision(project.get(), psId.getParentKey().get(), revision)
-        .view("review")
-        .post(
-            in,
-            new GerritCallback<ReviewInput>() {
-              @Override
-              public void onSuccess(ReviewInput result) {
-                Gerrit.display(PageLinks.toChange(project, psId));
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {
-                if (RestApi.isNotSignedIn(caught)) {
-                  lc.setReplyComment(message.getText());
-                }
-                super.onFailure(caught);
-              }
-            });
-    hide();
-  }
-
-  @UiHandler("cancel")
-  void onCancel(@SuppressWarnings("unused") ClickEvent e) {
-    message.setText("");
-    hide();
-  }
-
-  void replyTo(MessageInfo msg) {
-    if (msg.message() != null) {
-      String t = message.getText();
-      String m = quote(removePatchSetHeaderLine(msg.message()));
-      if (t == null || t.isEmpty()) {
-        t = m;
-      } else if (t.endsWith("\n\n")) {
-        t += m;
-      } else if (t.endsWith("\n")) {
-        t += "\n" + m;
-      } else {
-        t += "\n\n" + m;
-      }
-      message.setText(t);
-    }
-  }
-
-  private static String removePatchSetHeaderLine(String msg) {
-    msg = msg.trim();
-    if (msg.startsWith("Patch Set ")) {
-      int i = msg.indexOf('\n');
-      if (i > 0) {
-        msg = msg.substring(i + 1).trim();
-      }
-    }
-    return msg;
-  }
-
-  public static String quote(String msg) {
-    msg = msg.trim();
-    StringBuilder quotedMsg = new StringBuilder();
-    for (String line : msg.split("\\n")) {
-      line = line.trim();
-      while (line.length() > 67) {
-        int i = line.lastIndexOf(' ', 67);
-        if (i < 50) {
-          i = line.indexOf(' ', 67);
-        }
-        if (i > 0) {
-          quotedMsg.append(" > ").append(line.substring(0, i)).append("\n");
-          line = line.substring(i + 1);
-        } else {
-          break;
-        }
-      }
-      quotedMsg.append(" > ").append(line).append("\n");
-    }
-    quotedMsg.append("\n");
-    return quotedMsg.toString();
-  }
-
-  private void hide() {
-    for (Widget w = getParent(); w != null; w = w.getParent()) {
-      if (w instanceof PopupPanel) {
-        ((PopupPanel) w).hide();
-        break;
-      }
-    }
-  }
-
-  private void renderLabels(
-      List<String> names, NativeMap<LabelInfo> all, NativeMap<JsArrayString> permitted) {
-    TreeSet<Short> values = new TreeSet<>();
-    List<LabelAndValues> labels = new ArrayList<>(permitted.size());
-    for (String id : names) {
-      JsArrayString p = permitted.get(id);
-      if (p != null) {
-        if (!all.containsKey(id)) {
-          continue;
-        }
-        Set<Short> a = new TreeSet<>();
-        for (int i = 0; i < p.length(); i++) {
-          a.add(LabelInfo.parseValue(p.get(i)));
-        }
-        labels.add(new LabelAndValues(all.get(id), a));
-        values.addAll(a);
-      }
-    }
-    List<Short> columns = new ArrayList<>(values);
-
-    labelsTable.resize(1 + labels.size(), 2 + values.size());
-    for (int c = 0; c < columns.size(); c++) {
-      labelsTable.setText(0, 1 + c, LabelValue.formatValue(columns.get(c)));
-      labelsTable.getCellFormatter().setStyleName(0, 1 + c, style.label_value());
-    }
-
-    List<LabelAndValues> checkboxes = new ArrayList<>(labels.size());
-    int row = 1;
-    for (LabelAndValues lv : labels) {
-      if (isCheckBox(lv.info.valueSet())) {
-        checkboxes.add(lv);
-      } else {
-        renderRadio(row++, columns, lv);
-      }
-    }
-    for (LabelAndValues lv : checkboxes) {
-      renderCheckBox(row++, lv);
-    }
-  }
-
-  private Short normalizeDefaultValue(Short defaultValue, Set<Short> permittedValues) {
-    Short pmin = Collections.min(permittedValues);
-    Short pmax = Collections.max(permittedValues);
-    Short dv = defaultValue;
-    if (dv > pmax) {
-      dv = pmax;
-    } else if (dv < pmin) {
-      dv = pmin;
-    }
-    return dv;
-  }
-
-  private void renderRadio(int row, List<Short> columns, LabelAndValues lv) {
-    String id = lv.info.name();
-    Short dv = normalizeDefaultValue(lv.info.defaultValue(), lv.permitted);
-
-    labelHelpColumn = 1 + columns.size();
-    labelsTable.setText(row, 0, id);
-
-    CellFormatter fmt = labelsTable.getCellFormatter();
-    fmt.setStyleName(row, 0, style.label_name());
-    fmt.setStyleName(row, labelHelpColumn, style.label_help());
-
-    ApprovalInfo self =
-        Gerrit.isSignedIn() ? lv.info.forUser(Gerrit.getUserAccount()._accountId()) : null;
-
-    final LabelRadioGroup group = new LabelRadioGroup(row, id, lv.permitted.size());
-    for (int i = 0; i < columns.size(); i++) {
-      Short v = columns.get(i);
-      if (lv.permitted.contains(v)) {
-        String text = lv.info.valueText(LabelValue.formatValue(v));
-        LabelRadioButton b = new LabelRadioButton(group, text, v);
-        if ((self != null && v == self.value()) || (self == null && v.equals(dv))) {
-          b.setValue(true);
-          group.select(b);
-          in.label(group.label, v);
-          labelsTable.setText(row, labelHelpColumn, b.text);
-        }
-        group.buttons.add(b);
-        labelsTable.setWidget(row, 1 + i, b);
-      }
-    }
-  }
-
-  private void renderCheckBox(int row, LabelAndValues lv) {
-    ApprovalInfo self =
-        Gerrit.isSignedIn() ? lv.info.forUser(Gerrit.getUserAccount()._accountId()) : null;
-
-    final String id = lv.info.name();
-    final CheckBox b = new CheckBox();
-    b.setText(id);
-    b.setEnabled(lv.permitted.contains((short) 1));
-    if (self != null && self.value() == 1) {
-      b.setValue(true);
-    }
-    b.addValueChangeHandler(
-        new ValueChangeHandler<Boolean>() {
-          @Override
-          public void onValueChange(ValueChangeEvent<Boolean> event) {
-            in.label(id, event.getValue() ? (short) 1 : (short) 0);
-          }
-        });
-    b.setStyleName(style.label_name());
-    labelsTable.setWidget(row, 0, b);
-
-    CellFormatter fmt = labelsTable.getCellFormatter();
-    fmt.setStyleName(row, labelHelpColumn, style.label_help());
-    labelsTable.setText(row, labelHelpColumn, lv.info.valueText("+1"));
-  }
-
-  private static boolean isCheckBox(Set<Short> values) {
-    return values.size() == 2 && values.contains((short) 0) && values.contains((short) 1);
-  }
-
-  private void displayComments(NativeMap<JsArray<CommentInfo>> m) {
-    comments.clear();
-
-    JsArray<CommentInfo> l = m.get(Patch.COMMIT_MSG);
-    if (l != null) {
-      comments.add(
-          new FileComments(
-              clp, project, psId, Util.C.commitMessage(), copyPath(Patch.COMMIT_MSG, l)));
-    }
-    l = m.get(Patch.MERGE_LIST);
-    if (l != null) {
-      comments.add(
-          new FileComments(
-              clp, project, psId, Util.C.commitMessage(), copyPath(Patch.MERGE_LIST, l)));
-    }
-
-    List<String> paths =
-        m.keySet().stream()
-            .sorted()
-            .collect(collectingAndThen(toList(), Collections::unmodifiableList));
-
-    for (String path : paths) {
-      if (!Patch.isMagic(path)) {
-        comments.add(new FileComments(clp, project, psId, path, copyPath(path, m.get(path))));
-      }
-    }
-
-    commentsPanel.setVisible(comments.getWidgetCount() > 0);
-  }
-
-  private static List<CommentInfo> copyPath(String path, JsArray<CommentInfo> l) {
-    for (int i = 0; i < l.length(); i++) {
-      l.get(i).path(path);
-    }
-    return Natives.asList(l);
-  }
-
-  private static class LabelAndValues {
-    final LabelInfo info;
-    final Set<Short> permitted;
-
-    LabelAndValues(LabelInfo info, Set<Short> permitted) {
-      this.info = info;
-      this.permitted = permitted;
-    }
-  }
-
-  private class LabelRadioGroup {
-    final int row;
-    final String label;
-    final List<LabelRadioButton> buttons;
-    LabelRadioButton selected;
-
-    LabelRadioGroup(int row, String label, int cnt) {
-      this.row = row;
-      this.label = label;
-      this.buttons = new ArrayList<>(cnt);
-    }
-
-    void select(LabelRadioButton b) {
-      selected = b;
-      labelsTable.setText(row, labelHelpColumn, b.text);
-    }
-  }
-
-  private class LabelRadioButton extends RadioButton
-      implements ValueChangeHandler<Boolean>, ClickHandler, MouseOverHandler, MouseOutHandler {
-    private final LabelRadioGroup group;
-    private final String text;
-    private final short value;
-
-    LabelRadioButton(LabelRadioGroup group, String text, short value) {
-      super(group.label);
-      this.group = group;
-      this.text = text;
-      this.value = value;
-      addValueChangeHandler(this);
-      addClickHandler(this);
-      addMouseOverHandler(this);
-      addMouseOutHandler(this);
-    }
-
-    @Override
-    public void onValueChange(ValueChangeEvent<Boolean> event) {
-      if (event.getValue()) {
-        select();
-      }
-    }
-
-    @Override
-    public void onClick(ClickEvent event) {
-      select();
-    }
-
-    void select() {
-      group.select(this);
-      in.label(group.label, value);
-    }
-
-    @Override
-    public void onMouseOver(MouseOverEvent event) {
-      labelsTable.setText(group.row, labelHelpColumn, text);
-    }
-
-    @Override
-    public void onMouseOut(MouseOutEvent event) {
-      LabelRadioButton b = group.selected;
-      String s = b != null ? b.text : "";
-      labelsTable.setText(group.row, labelHelpColumn, s);
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.ui.xml
deleted file mode 100644
index 6903b91..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.ui.xml
+++ /dev/null
@@ -1,85 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder
-    xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
-  <ui:style gss='false' type='com.google.gerrit.client.change.ReplyBox.Styles'>
-    .replyBox {
-    }
-    .label_name {
-      font-weight: bold;
-      text-align: left;
-      white-space: nowrap;
-    }
-    .label_name input { margin-left: 0; }
-    .label_help {
-      padding-left: 5px;
-      white-space: nowrap;
-    }
-    .label_value {
-      text-align: center;
-    }
-    .cancel {
-      position: absolute;
-      bottom: 5px;
-      right: 5px;
-      background-color: #eee;
-      background-image: -webkit-linear-gradient(top, #eee, #eee);
-    }
-    .cancel div { color: #444; }
-    .comments {
-      max-height: 275px;
-      width: 526px;
-    }
-    .comments p {
-      margin: 5px 0 5px 0;
-    }
-  </ui:style>
-  <g:HTMLPanel styleName='{style.replyBox}'>
-    <div class='{res.style.section}'>
-      <g:TextArea
-         visibleLines='5'
-         characterWidth='70'
-         ui:field='message'/>
-    </div>
-    <div class='{res.style.section}' ui:field='labelsParent'>
-      <g:Grid ui:field='labelsTable'/>
-    </div>
-    <g:ScrollPanel ui:field='commentsPanel'
-        styleName='{style.comments}'
-        addStyleNames='{res.style.section}'>
-      <g:FlowPanel ui:field='comments'/>
-    </g:ScrollPanel>
-    <div class='{res.style.section}' style='position: relative'>
-      <g:Button ui:field='post'
-          title='Post reply (Shortcut: Ctrl-Enter)'
-          styleName='{res.style.button}'>
-        <ui:attribute name='title'/>
-        <div><ui:msg>Post</ui:msg></div>
-      </g:Button>
-
-      <g:Button ui:field='cancel'
-          title='Close reply form (Shortcut: Esc)'
-          styleName='{res.style.button}'
-          addStyleNames='{style.cancel}'>
-        <ui:attribute name='title'/>
-        <div><ui:msg>Cancel</ui:msg></div>
-      </g:Button>
-    </div>
-  </g:HTMLPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Resources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Resources.java
deleted file mode 100644
index cfc4e23..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Resources.java
+++ /dev/null
@@ -1,38 +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.client.change;
-
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.resources.client.ClientBundle;
-import com.google.gwt.resources.client.CssResource;
-
-public interface Resources extends ClientBundle {
-  Resources I = GWT.create(Resources.class);
-  ChangeConstants C = GWT.create(ChangeConstants.class);
-  ChangeMessages M = GWT.create(ChangeMessages.class);
-
-  @Source("common.css")
-  Style style();
-
-  public interface Style extends CssResource {
-    String button();
-
-    String popup();
-
-    String popupContent();
-
-    String section();
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RestoreAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RestoreAction.java
deleted file mode 100644
index aa3a9ef..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RestoreAction.java
+++ /dev/null
@@ -1,50 +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.client.change;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.user.client.ui.Button;
-
-class RestoreAction extends ActionMessageBox {
-  private final Project.NameKey project;
-  private final Change.Id id;
-
-  RestoreAction(Button b, Project.NameKey project, Change.Id id) {
-    super(b);
-    this.project = project;
-    this.id = id;
-  }
-
-  @Override
-  void send(String message) {
-    ChangeApi.restore(
-        project.get(),
-        id.get(),
-        message,
-        new GerritCallback<ChangeInfo>() {
-          @Override
-          public void onSuccess(ChangeInfo result) {
-            Gerrit.display(PageLinks.toChange(project, id));
-            hide();
-          }
-        });
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevertAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevertAction.java
deleted file mode 100644
index 3fba125..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevertAction.java
+++ /dev/null
@@ -1,75 +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.client.change;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.Util;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.TextAreaActionDialog;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.event.logical.shared.CloseEvent;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.PopupPanel;
-
-class RevertAction {
-
-  static void call(
-      final Button b,
-      Change.Id id,
-      Project.NameKey project,
-      String revision,
-      String commitSubject) {
-    // TODO Replace ActionDialog with a nicer looking display.
-    b.setEnabled(false);
-    new TextAreaActionDialog(Util.C.revertChangeTitle(), Util.C.headingRevertMessage()) {
-      {
-        sendButton.setText(Util.C.buttonRevertChangeSend());
-        message.setText(Util.M.revertChangeDefaultMessage(commitSubject, revision));
-      }
-
-      @Override
-      public void onSend() {
-        ChangeApi.revert(
-            project.get(),
-            id.get(),
-            getMessageText(),
-            new GerritCallback<ChangeInfo>() {
-              @Override
-              public void onSuccess(ChangeInfo result) {
-                sent = true;
-                hide();
-                Gerrit.display(PageLinks.toChange(result.projectNameKey(), result.legacyId()));
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {
-                enableButtons(true);
-                super.onFailure(caught);
-              }
-            });
-      }
-
-      @Override
-      public void onClose(CloseEvent<PopupPanel> event) {
-        super.onClose(event);
-        b.setEnabled(true);
-      }
-    }.center();
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReviewerSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReviewerSuggestOracle.java
deleted file mode 100644
index 4e464df..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReviewerSuggestOracle.java
+++ /dev/null
@@ -1,104 +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.client.change;
-
-import com.google.gerrit.client.admin.AdminConstants;
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.info.AccountInfo;
-import com.google.gerrit.client.info.GroupBaseInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.ui.AccountSuggestOracle;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/** REST API based suggestion Oracle for reviewers. */
-public class ReviewerSuggestOracle extends HighlightSuggestOracle {
-  private Project.NameKey project;
-  private Change.Id changeId;
-
-  @Override
-  protected void onRequestSuggestions(Request req, Callback cb) {
-    ChangeApi.suggestReviewers(project.get(), changeId.get(), req.getQuery(), req.getLimit(), false)
-        .get(
-            new GerritCallback<JsArray<SuggestReviewerInfo>>() {
-              @Override
-              public void onSuccess(JsArray<SuggestReviewerInfo> result) {
-                List<RestReviewerSuggestion> r = new ArrayList<>(result.length());
-                for (SuggestReviewerInfo reviewer : Natives.asList(result)) {
-                  r.add(new RestReviewerSuggestion(reviewer, req.getQuery()));
-                }
-                cb.onSuggestionsReady(req, new Response(r));
-              }
-
-              @Override
-              public void onFailure(Throwable err) {
-                List<Suggestion> r = Collections.emptyList();
-                cb.onSuggestionsReady(req, new Response(r));
-              }
-            });
-  }
-
-  @Override
-  public void requestDefaultSuggestions(Request req, Callback cb) {
-    requestSuggestions(req, cb);
-  }
-
-  public void setChange(Project.NameKey project, Change.Id changeId) {
-    this.project = project;
-    this.changeId = changeId;
-  }
-
-  public static class RestReviewerSuggestion implements Suggestion {
-    private final String displayString;
-    private final String replacementString;
-
-    RestReviewerSuggestion(SuggestReviewerInfo reviewer, String query) {
-      if (reviewer.account() != null) {
-        this.replacementString =
-            AccountSuggestOracle.AccountSuggestion.format(reviewer.account(), query);
-        this.displayString = replacementString;
-      } else {
-        this.replacementString = reviewer.group().name();
-        this.displayString =
-            replacementString + " (" + AdminConstants.I.suggestedGroupLabel() + ")";
-      }
-    }
-
-    @Override
-    public String getDisplayString() {
-      return displayString;
-    }
-
-    @Override
-    public String getReplacementString() {
-      return replacementString;
-    }
-  }
-
-  public static class SuggestReviewerInfo extends JavaScriptObject {
-    public final native AccountInfo account() /*-{ return this.account; }-*/;
-
-    public final native GroupBaseInfo group() /*-{ return this.group; }-*/;
-
-    protected SuggestReviewerInfo() {}
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
deleted file mode 100644
index 859af19..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
+++ /dev/null
@@ -1,328 +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.client.change;
-
-import com.google.gerrit.client.ConfirmationCallback;
-import com.google.gerrit.client.ConfirmationDialog;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.NotSignedInDialog;
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.Util;
-import com.google.gerrit.client.info.AccountInfo;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.info.ChangeInfo.ApprovalInfo;
-import com.google.gerrit.client.info.ChangeInfo.LabelInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.NativeString;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.ui.RemoteSuggestBox;
-import com.google.gerrit.extensions.client.ReviewerState;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.logical.shared.CloseEvent;
-import com.google.gwt.event.logical.shared.CloseHandler;
-import com.google.gwt.event.logical.shared.SelectionEvent;
-import com.google.gwt.event.logical.shared.SelectionHandler;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.uibinder.client.UiHandler;
-import com.google.gwt.user.client.rpc.StatusCodeException;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwt.user.client.ui.UIObject;
-import com.google.gwtexpui.safehtml.client.SafeHtml;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-/** Add reviewers. */
-public class Reviewers extends Composite {
-  interface Binder extends UiBinder<HTMLPanel, Reviewers> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  @UiField Element reviewersText;
-  @UiField Image addReviewerIcon;
-  @UiField Button addMe;
-  @UiField Element form;
-  @UiField Element error;
-
-  @UiField(provided = true)
-  RemoteSuggestBox suggestBox;
-
-  private ChangeScreen.Style style;
-  private Element ccText;
-
-  private ReviewerSuggestOracle reviewerSuggestOracle;
-  private Change.Id changeId;
-  private Project.NameKey project;
-
-  Reviewers() {
-    reviewerSuggestOracle = new ReviewerSuggestOracle();
-    suggestBox = new RemoteSuggestBox(reviewerSuggestOracle);
-    suggestBox.enableDefaultSuggestions();
-    suggestBox.setVisibleLength(55);
-    suggestBox.setHintText(Util.C.approvalTableAddReviewerHint());
-    suggestBox.addCloseHandler(
-        new CloseHandler<RemoteSuggestBox>() {
-          @Override
-          public void onClose(CloseEvent<RemoteSuggestBox> event) {
-            Reviewers.this.onCancel(null);
-          }
-        });
-    suggestBox.addSelectionHandler(
-        new SelectionHandler<String>() {
-          @Override
-          public void onSelection(SelectionEvent<String> event) {
-            addReviewer(event.getSelectedItem(), false);
-          }
-        });
-
-    initWidget(uiBinder.createAndBindUi(this));
-    addReviewerIcon.addDomHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            onOpenForm();
-          }
-        },
-        ClickEvent.getType());
-  }
-
-  void init(ChangeScreen.Style style, Element ccText) {
-    this.style = style;
-    this.ccText = ccText;
-  }
-
-  void set(ChangeInfo info) {
-    this.changeId = info.legacyId();
-    this.project = info.projectNameKey();
-    display(info);
-    reviewerSuggestOracle.setChange(project, changeId);
-    addReviewerIcon.setVisible(Gerrit.isSignedIn());
-  }
-
-  void onOpenForm() {
-    UIObject.setVisible(form, true);
-    UIObject.setVisible(error, false);
-    addReviewerIcon.setVisible(false);
-    suggestBox.setServeSuggestionsOnOracle(true);
-    suggestBox.setFocus(true);
-  }
-
-  @UiHandler("add")
-  void onAdd(@SuppressWarnings("unused") ClickEvent e) {
-    addReviewer(suggestBox.getText(), false);
-  }
-
-  @UiHandler("addMe")
-  void onAddMe(@SuppressWarnings("unused") ClickEvent e) {
-    String accountId = String.valueOf(Gerrit.getUserAccount()._accountId());
-    addReviewer(accountId, false);
-  }
-
-  @UiHandler("cancel")
-  void onCancel(@SuppressWarnings("unused") ClickEvent e) {
-    addReviewerIcon.setVisible(true);
-    UIObject.setVisible(form, false);
-    suggestBox.setFocus(false);
-    suggestBox.setText("");
-    suggestBox.setServeSuggestionsOnOracle(false);
-  }
-
-  private void addReviewer(String reviewer, boolean confirmed) {
-    if (reviewer.isEmpty()) {
-      return;
-    }
-
-    ChangeApi.reviewers(project.get(), changeId.get())
-        .post(
-            PostInput.create(reviewer, confirmed),
-            new GerritCallback<PostResult>() {
-              @Override
-              public void onSuccess(PostResult result) {
-                if (result.confirm()) {
-                  askForConfirmation(result.error());
-                } else if (result.error() != null) {
-                  UIObject.setVisible(error, true);
-                  error.setInnerText(result.error());
-                } else {
-                  UIObject.setVisible(error, false);
-                  error.setInnerText("");
-                  suggestBox.setText("");
-
-                  if (result.reviewers() != null && result.reviewers().length() > 0) {
-                    updateReviewerList();
-                  }
-                }
-              }
-
-              private void askForConfirmation(String text) {
-                new ConfirmationDialog(
-                        Util.C.approvalTableAddManyReviewersConfirmationDialogTitle(),
-                        new SafeHtmlBuilder().append(text),
-                        new ConfirmationCallback() {
-                          @Override
-                          public void onOk() {
-                            addReviewer(reviewer, true);
-                          }
-                        })
-                    .center();
-              }
-
-              @Override
-              public void onFailure(Throwable err) {
-                if (isSigninFailure(err)) {
-                  new NotSignedInDialog().center();
-                } else {
-                  UIObject.setVisible(error, true);
-                  error.setInnerText(
-                      err instanceof StatusCodeException
-                          ? ((StatusCodeException) err).getEncodedResponse()
-                          : err.getMessage());
-                }
-              }
-            });
-  }
-
-  void updateReviewerList() {
-    ChangeApi.detail(
-        project.get(),
-        changeId.get(),
-        new GerritCallback<ChangeInfo>() {
-          @Override
-          public void onSuccess(ChangeInfo result) {
-            display(result);
-          }
-        });
-  }
-
-  private void display(ChangeInfo info) {
-    Map<ReviewerState, List<AccountInfo>> reviewers = info.reviewers();
-    Map<Integer, AccountInfo> r = byAccount(reviewers, ReviewerState.REVIEWER);
-    Map<Integer, AccountInfo> cc = byAccount(reviewers, ReviewerState.CC);
-    for (Integer i : r.keySet()) {
-      cc.remove(i);
-    }
-    cc.remove(info.owner()._accountId());
-    Set<Integer> removable = info.removableReviewerIds();
-    Map<Integer, VotableInfo> votable = votable(info);
-
-    SafeHtml rHtml = Labels.formatUserList(style, r.values(), removable, null, votable);
-    SafeHtml ccHtml = Labels.formatUserList(style, cc.values(), removable, null, votable);
-
-    reviewersText.setInnerSafeHtml(rHtml);
-    ccText.setInnerSafeHtml(ccHtml);
-    if (Gerrit.isSignedIn()) {
-      int currentUser = Gerrit.getUserAccount()._accountId();
-      boolean showAddMeButton =
-          info.owner()._accountId() != currentUser
-              && !cc.containsKey(currentUser)
-              && !r.containsKey(currentUser);
-      addMe.setVisible(showAddMeButton);
-    }
-  }
-
-  private static Map<Integer, AccountInfo> byAccount(
-      Map<ReviewerState, List<AccountInfo>> reviewers, ReviewerState state) {
-    List<AccountInfo> accounts = reviewers.get(state);
-    if (accounts == null) {
-      return Collections.emptyMap();
-    }
-    Map<Integer, AccountInfo> result = new HashMap<>();
-    for (AccountInfo a : accounts) {
-      result.put(a._accountId(), a);
-    }
-    return result;
-  }
-
-  private static Map<Integer, VotableInfo> votable(ChangeInfo change) {
-    Map<Integer, VotableInfo> d = new HashMap<>();
-    for (String name : change.labels()) {
-      LabelInfo label = change.label(name);
-      Short labelMaxValue =
-          label.valueSet().isEmpty() ? null : LabelInfo.parseValue(label.maxValue());
-      if (label.all() != null) {
-        for (ApprovalInfo ai : Natives.asList(label.all())) {
-          int id = ai._accountId();
-          VotableInfo ad = d.get(id);
-          if (ad == null) {
-            ad = new VotableInfo();
-            d.put(id, ad);
-          }
-          if (labelMaxValue != null
-              && ai.permittedVotingRange() != null
-              && ai.permittedVotingRange().max() == labelMaxValue) {
-            ad.votable(name + " (" + label.maxValue() + ") ");
-          } else if (ai.hasValue()) {
-            ad.votable(name);
-          }
-        }
-      }
-    }
-    return d;
-  }
-
-  public static class PostInput extends JavaScriptObject {
-    public static PostInput create(String reviewer, boolean confirmed) {
-      PostInput input = createObject().cast();
-      input.init(reviewer, confirmed);
-      return input;
-    }
-
-    private native void init(String reviewer, boolean confirmed) /*-{
-      this.reviewer = reviewer;
-      if (confirmed) {
-        this.confirmed = true;
-      }
-    }-*/;
-
-    protected PostInput() {}
-  }
-
-  public static class ReviewerInfo extends AccountInfo {
-    final Set<String> approvals() {
-      return Natives.keys(_approvals());
-    }
-
-    final native String approval(String l) /*-{ return this.approvals[l]; }-*/;
-
-    private native NativeMap<NativeString> _approvals() /*-{ return this.approvals; }-*/;
-
-    protected ReviewerInfo() {}
-  }
-
-  public static class PostResult extends JavaScriptObject {
-    public final native JsArray<ReviewerInfo> reviewers() /*-{ return this.reviewers; }-*/;
-
-    public final native boolean confirm() /*-{ return this.confirm || false; }-*/;
-
-    public final native String error() /*-{ return this.error; }-*/;
-
-    protected PostResult() {}
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.ui.xml
deleted file mode 100644
index cf506e5..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.ui.xml
+++ /dev/null
@@ -1,69 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder
-    xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'
-    xmlns:u='urn:import:com.google.gerrit.client.ui'>
-  <ui:with field='ico' type='com.google.gerrit.client.GerritResources'/>
-  <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
-  <ui:style gss='false'>
-    .suggestBox {
-      margin-bottom: 2px;
-    }
-
-    .error {
-      color: #D33D3D;
-      font-weight: bold;
-    }
-
-    .addReviewer,
-    .cancel {
-      cursor: pointer;
-      float: right;
-    }
-  </ui:style>
-  <g:HTMLPanel>
-    <div>
-      <span ui:field='reviewersText'/>
-      <g:Image ui:field='addReviewerIcon'
-          resource='{ico.addUser}'
-          styleName='{style.addReviewer}'
-          title='Add Reviewer'/>
-    </div>
-    <div ui:field='form' style='display: none' aria-hidden='true'>
-      <u:RemoteSuggestBox ui:field='suggestBox' styleName='{style.suggestBox}'/>
-      <div ui:field='error'
-           class='{style.error}'
-           style='display: none' aria-hidden='true'/>
-      <div>
-        <g:Button ui:field='add' styleName='{res.style.button}'>
-          <div>Add</div>
-        </g:Button>
-        <g:Button ui:field='addMe'
-            styleName='{res.style.button}' visible='false'>
-          <div>Add Me</div>
-        </g:Button>
-        <g:Button ui:field='cancel'
-            styleName='{res.style.button}'
-            addStyleNames='{style.cancel}'>
-          <div>Cancel</div>
-        </g:Button>
-      </div>
-    </div>
-   </g:HTMLPanel>
-  </ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RightSidePopdownAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RightSidePopdownAction.java
deleted file mode 100644
index 1383c5d..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RightSidePopdownAction.java
+++ /dev/null
@@ -1,81 +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.client.change;
-
-import com.google.gwt.dom.client.Document;
-import com.google.gwt.dom.client.Style;
-import com.google.gwt.event.logical.shared.CloseEvent;
-import com.google.gwt.event.logical.shared.CloseHandler;
-import com.google.gwt.user.client.Window;
-import com.google.gwt.user.client.ui.PopupPanel;
-import com.google.gwt.user.client.ui.UIObject;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-
-abstract class RightSidePopdownAction {
-  private final ChangeScreen.Style style;
-  private final Widget button;
-  private final UIObject relativeTo;
-  private PopupPanel popup;
-
-  RightSidePopdownAction(ChangeScreen.Style style, UIObject relativeTo, Widget button) {
-    this.style = style;
-    this.relativeTo = relativeTo;
-    this.button = button;
-  }
-
-  abstract Widget getWidget();
-
-  void show() {
-    if (popup != null) {
-      button.removeStyleName(style.selected());
-      popup.hide();
-      return;
-    }
-
-    final PopupPanel p =
-        new PopupPanel(true) {
-          @Override
-          public void setPopupPosition(int left, int top) {
-            top -= Document.get().getBodyOffsetTop();
-
-            int w = Window.getScrollLeft() + Window.getClientWidth();
-            int r = relativeTo.getAbsoluteLeft() + relativeTo.getOffsetWidth();
-            int right = w - r;
-            Style style = getElement().getStyle();
-            style.clearProperty("left");
-            style.setPropertyPx("right", right);
-            style.setPropertyPx("top", top);
-          }
-        };
-    p.setStyleName(style.replyBox());
-    p.addAutoHidePartner(button.getElement());
-    p.addCloseHandler(
-        new CloseHandler<PopupPanel>() {
-          @Override
-          public void onClose(CloseEvent<PopupPanel> event) {
-            if (popup == p) {
-              button.removeStyleName(style.selected());
-              popup = null;
-            }
-          }
-        });
-    p.add(getWidget());
-    p.showRelativeTo(relativeTo);
-    GlobalKey.dialog(p);
-    button.addStyleName(style.selected());
-    popup = p;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/StarIcon.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/StarIcon.java
deleted file mode 100644
index b7bf8de..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/StarIcon.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.change;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwt.user.client.ui.ToggleButton;
-
-class StarIcon extends ToggleButton {
-  StarIcon() {
-    super(new Image(Gerrit.RESOURCES.starOpen()), new Image(Gerrit.RESOURCES.starFilled()));
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/SubmitAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/SubmitAction.java
deleted file mode 100644
index 4446e65..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/SubmitAction.java
+++ /dev/null
@@ -1,57 +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.client.change;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.api.ChangeGlue;
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.SubmitInfo;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Change;
-
-class SubmitAction {
-  static void call(ChangeInfo changeInfo, RevisionInfo revisionInfo) {
-    if (ChangeGlue.onSubmitChange(changeInfo, revisionInfo)) {
-      final Change.Id changeId = changeInfo.legacyId();
-      ChangeApi.submit(
-          changeInfo.project(),
-          changeId.get(),
-          revisionInfo.name(),
-          new GerritCallback<SubmitInfo>() {
-            @Override
-            public void onSuccess(SubmitInfo result) {
-              redisplay();
-            }
-
-            @Override
-            public void onFailure(Throwable err) {
-              if (SubmitFailureDialog.isConflict(err)) {
-                new SubmitFailureDialog(err.getMessage()).center();
-              } else {
-                super.onFailure(err);
-              }
-              redisplay();
-            }
-
-            private void redisplay() {
-              Gerrit.display(PageLinks.toChange(changeInfo.projectNameKey(), changeId));
-            }
-          });
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/SubmitFailureDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/SubmitFailureDialog.java
deleted file mode 100644
index 77bf217..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/SubmitFailureDialog.java
+++ /dev/null
@@ -1,31 +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.client.change;
-
-import com.google.gerrit.client.ErrorDialog;
-import com.google.gerrit.client.changes.Util;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-import com.google.gwtjsonrpc.client.RemoteJsonException;
-
-class SubmitFailureDialog extends ErrorDialog {
-  static boolean isConflict(Throwable err) {
-    return err instanceof RemoteJsonException && 409 == ((RemoteJsonException) err).getCode();
-  }
-
-  SubmitFailureDialog(String msg) {
-    super(new SafeHtmlBuilder().append(msg.trim()).wikify());
-    setText(Util.C.submitFailed());
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.java
deleted file mode 100644
index f5c921b..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.java
+++ /dev/null
@@ -1,148 +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.client.change;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.InlineHyperlink;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyDownEvent;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.uibinder.client.UiHandler;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwt.user.client.ui.UIObject;
-import com.google.gwtexpui.globalkey.client.NpTextBox;
-
-/** Displays (and edits) the change topic string. */
-class Topic extends Composite {
-  interface Binder extends UiBinder<HTMLPanel, Topic> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  private PatchSet.Id psId;
-  private Project.NameKey project;
-  private boolean canEdit;
-
-  @UiField Element show;
-  @UiField InlineHyperlink text;
-  @UiField Image editIcon;
-
-  @UiField Element form;
-  @UiField NpTextBox input;
-  @UiField Button save;
-  @UiField Button cancel;
-
-  Topic() {
-    initWidget(uiBinder.createAndBindUi(this));
-    editIcon.addDomHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            onEdit();
-          }
-        },
-        ClickEvent.getType());
-  }
-
-  void set(ChangeInfo info, String revision) {
-    canEdit = info.hasActions() && info.actions().containsKey("topic");
-
-    psId = new PatchSet.Id(info.legacyId(), info.revisions().get(revision)._number());
-    project = info.projectNameKey();
-
-    initTopicLink(info);
-    editIcon.setVisible(canEdit);
-    if (!canEdit) {
-      show.setTitle(null);
-    }
-  }
-
-  private void initTopicLink(ChangeInfo info) {
-    if (info.topic() != null && !info.topic().isEmpty()) {
-      String topic = info.topic();
-      text.setText(topic);
-      text.setTargetHistoryToken(PageLinks.topicQuery(info.status(), topic));
-    }
-  }
-
-  boolean canEdit() {
-    return canEdit;
-  }
-
-  void onEdit() {
-    if (canEdit) {
-      UIObject.setVisible(show, false);
-      UIObject.setVisible(form, true);
-
-      input.setText(text.getText());
-      input.setFocus(true);
-      input.selectAll();
-    }
-  }
-
-  @UiHandler("cancel")
-  void onCancel(@SuppressWarnings("unused") ClickEvent e) {
-    input.setFocus(false);
-    UIObject.setVisible(form, false);
-    UIObject.setVisible(show, true);
-  }
-
-  @UiHandler("input")
-  void onKeyDownInput(KeyDownEvent e) {
-    if (e.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) {
-      onCancel(null);
-    } else if (e.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
-      e.stopPropagation();
-      e.preventDefault();
-      onSave(null);
-    }
-  }
-
-  @UiHandler("save")
-  void onSave(@SuppressWarnings("unused") ClickEvent e) {
-    ChangeApi.topic(
-        project.get(),
-        psId.getParentKey().get(),
-        input.getValue().trim(),
-        new GerritCallback<String>() {
-          @Override
-          public void onSuccess(String result) {
-            Gerrit.display(PageLinks.toChange(project, psId));
-          }
-        });
-    onCancel(null);
-  }
-
-  @UiHandler("save")
-  void onSaveKeyPress(KeyPressEvent e) {
-    if (e.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
-      e.stopPropagation();
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.ui.xml
deleted file mode 100644
index c2a6fbd..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.ui.xml
+++ /dev/null
@@ -1,54 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder
-    xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'
-    xmlns:x='urn:import:com.google.gerrit.client.ui'>
-  <ui:with field='ico' type='com.google.gerrit.client.GerritResources'/>
-  <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
-  <ui:style gss='false'>
-    .edit { cursor: pointer; }
-    .edit, .cancel { float: right; }
-  </ui:style>
-  <g:HTMLPanel>
-    <div ui:field='show'>
-      <x:InlineHyperlink ui:field='text'
-          title='Search for changes on this topic'/>
-      <g:Image ui:field='editIcon'
-          resource='{ico.edit}'
-          styleName='{style.edit}'
-          title='Edit topic (Shortcut: t)'/>
-    </div>
-
-    <div ui:field='form' style='display: none' aria-hidden='true'>
-      <div>
-        <c:NpTextBox ui:field='input' visibleLength='55'/>
-      </div>
-      <div>
-        <g:Button ui:field='save' styleName='{res.style.button}'>
-          <div>Update</div>
-        </g:Button>
-        <g:Button ui:field='cancel'
-            styleName='{res.style.button}'
-            addStyleNames='{style.cancel}'>
-          <div>Cancel</div>
-        </g:Button>
-      </div>
-    </div>
-  </g:HTMLPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.java
deleted file mode 100644
index 520dc69..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.java
+++ /dev/null
@@ -1,77 +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.client.change;
-
-import com.google.gerrit.client.info.ChangeInfo.MessageInfo;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.uibinder.client.UiHandler;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import java.sql.Timestamp;
-import java.util.HashSet;
-import java.util.List;
-
-/** Displays the "New Message From ..." panel in bottom right on updates. */
-abstract class UpdateAvailableBar extends Composite {
-  interface Binder extends UiBinder<HTMLPanel, UpdateAvailableBar> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  private Timestamp updated;
-
-  @UiField Element author;
-  @UiField Anchor show;
-  @UiField Anchor ignore;
-
-  UpdateAvailableBar() {
-    initWidget(uiBinder.createAndBindUi(this));
-  }
-
-  void set(List<MessageInfo> newMessages, Timestamp newTime) {
-    HashSet<Integer> seen = new HashSet<>();
-    StringBuilder r = new StringBuilder();
-    for (MessageInfo m : newMessages) {
-      int a = m.author() != null ? m.author()._accountId() : 0;
-      if (seen.add(a)) {
-        if (r.length() > 0) {
-          r.append(", ");
-        }
-        r.append(Message.authorName(m));
-      }
-    }
-    author.setInnerText(r.toString());
-    updated = newTime;
-  }
-
-  @UiHandler("show")
-  void onShow(@SuppressWarnings("unused") ClickEvent e) {
-    onShow();
-  }
-
-  @UiHandler("ignore")
-  void onIgnore(@SuppressWarnings("unused") ClickEvent e) {
-    onIgnore(updated);
-    removeFromParent();
-  }
-
-  abstract void onShow();
-
-  abstract void onIgnore(Timestamp newTime);
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.ui.xml
deleted file mode 100644
index 1d5592b..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.ui.xml
+++ /dev/null
@@ -1,65 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder
-    xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:style gss='false'>
-    .popup {
-      position: fixed;
-      bottom: 0;
-      right: 0;
-      z-index: 10;
-      padding: 5px;
-    }
-    .bar {
-      background-color: #fff1a8;
-      border: 1px solid #ccc;
-      padding: 5px 10px;
-      font-size: 80%;
-      color: #222;
-      white-space: nowrap;
-      width: auto;
-      height: auto;
-    }
-    a.action {
-      color: #222;
-      text-decoration: underline;
-      display: inline-block;
-      margin-left: 0.5em;
-    }
-  </ui:style>
-  <g:HTMLPanel styleName='{style.popup}'>
-    <div class='{style.bar}'>
-      <ui:msg>Update from <span ui:field='author'/></ui:msg>
-      <g:Anchor ui:field='show'
-          styleName='{style.action}'
-          href='javascript:;'
-          title='Refresh screen and display updates'>
-        <ui:attribute name='title'/>
-        <ui:msg>Show</ui:msg>
-      </g:Anchor>
-      <g:Anchor ui:field='ignore'
-          styleName='{style.action}'
-          href='javascript:;'
-          title='Ignore this update'>
-        <ui:attribute name='title'/>
-        <ui:msg>Ignore</ui:msg>
-      </g:Anchor>
-    </div>
-  </g:HTMLPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateCheckTimer.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateCheckTimer.java
deleted file mode 100644
index 4a5af0551..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateCheckTimer.java
+++ /dev/null
@@ -1,94 +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.client.change;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.ui.UserActivityMonitor;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.logical.shared.ValueChangeHandler;
-import com.google.gwt.user.client.Timer;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-
-class UpdateCheckTimer extends Timer implements ValueChangeHandler<Boolean> {
-  private static final int MAX_PERIOD = 3 * 60 * 1000;
-  private static final int IDLE_PERIOD = 2 * 3600 * 1000;
-  private static final int POLL_PERIOD = Gerrit.info().change().updateDelay() * 1000;
-
-  private final ChangeScreen screen;
-  private int delay;
-  private boolean running;
-
-  UpdateCheckTimer(ChangeScreen screen) {
-    this.screen = screen;
-    this.delay = POLL_PERIOD;
-  }
-
-  void schedule() {
-    scheduleRepeating(delay);
-  }
-
-  @Override
-  public void run() {
-    if (!screen.isAttached()) {
-      // screen should have cancelled this timer.
-      cancel();
-      return;
-    } else if (running) {
-      return;
-    }
-
-    running = true;
-    screen.loadChangeInfo(
-        false,
-        new AsyncCallback<ChangeInfo>() {
-          @Override
-          public void onSuccess(ChangeInfo info) {
-            running = false;
-            screen.showUpdates(info);
-
-            int d = UserActivityMonitor.isActive() ? POLL_PERIOD : IDLE_PERIOD;
-            if (d != delay) {
-              delay = d;
-              schedule();
-            }
-          }
-
-          @Override
-          public void onFailure(Throwable caught) {
-            // On failures increase the delay time and try again,
-            // but place an upper bound on the delay.
-            running = false;
-            delay =
-                (int)
-                    Math.max(
-                        delay * (1.5 + Math.random()),
-                        UserActivityMonitor.isActive() ? MAX_PERIOD : IDLE_PERIOD + MAX_PERIOD);
-            schedule();
-          }
-        });
-  }
-
-  @Override
-  public void onValueChange(ValueChangeEvent<Boolean> event) {
-    if (event.getValue()) {
-      delay = POLL_PERIOD;
-      run();
-    } else {
-      delay = IDLE_PERIOD;
-    }
-    schedule();
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/VotableInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/VotableInfo.java
deleted file mode 100644
index 33d8d12..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/VotableInfo.java
+++ /dev/null
@@ -1,37 +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.client.change;
-
-import java.util.HashSet;
-import java.util.Set;
-
-class VotableInfo {
-  private Set<String> votable;
-
-  void votable(String label) {
-    if (votable == null) {
-      votable = new HashSet<>();
-    }
-    votable.add(label);
-  }
-
-  Set<String> votableLabels() {
-    Set<String> s = new HashSet<>();
-    if (votable != null) {
-      s.addAll(votable);
-    }
-    return s;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/common.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/common.css
deleted file mode 100644
index bb7cb27..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/common.css
+++ /dev/null
@@ -1,63 +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.
- */
-
-@eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
-
-.popup {
-  background-color: trimColor;
-  min-width: 300px;
-  min-height: 90px;
-}
-
-.popupContent {
-  padding: 5px;
-}
-
-.button,
-.popup button,
-.popup input[type='button'] {
-  margin: 0 3px 0 0;
-  border-color: rgba(0, 0, 0, 0.15) !important;
-  text-align: center;
-  font-size: 11px;
-  font-weight: bold;
-  border: 2px solid;
-  cursor: pointer;
-  color: #fff;
-  background-color: #4d90fe;
-  background-image: -webkit-linear-gradient(top, #4d90fe, #4d90fe);
-  -webkit-border-radius: 2px;
-  -webkit-box-sizing: content-box;
-}
-
-.button:disabled,
-.popup button:disabled,
-.popup input[type='button']:disabled {
-  background-color: #999;
-  background-image: -webkit-linear-gradient(top, #999, #999);
-}
-
-.button div, .popup button div {
-  width: 54px;
-  white-space: nowrap;
-  color: #fff;
-  height: 14px;
-  line-height: 14px;
-}
-
-.section {
-  padding: 5px 5px;
-  border-bottom: 1px solid #b8b8b8;
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css
deleted file mode 100644
index 6f514df..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css
+++ /dev/null
@@ -1,115 +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.
- */
-
-.pointer, .reviewed, .restoreDelete {
-  padding: 0px;
-  vertical-align: top;
-}
-.pointer {
-  width: 12px;
-}
-.reviewed {
-  height: 19px;
-  width: 20px;
-}
-
-.table th {
-  vertical-align: top;
-  text-align: left;
-}
-.table tr {
-  vertical-align: top;
-}
-.table tr:hover {
-  background: rgba(209, 245, 248, 0.32);
-}
-.table tr.nohover:hover {
-  background: transparent;
-}
-
-.status {
-  padding-right: 4px;
-  color: #888;
-}
-
-.pathColumn {
-  white-space: nowrap;
-  min-width: 600px;
-}
-.pathColumn a {
-  color: #000;
-  cursor: pointer;
-}
-.commonPrefix {
-  color: #888;
-}
-.renameCopySource {
-  color: #888;
-  font-size: smaller;
-}
-
-.draftColumn,
-.newColumn,
-.commentColumn {
-  white-space: nowrap;
-}
-.draftColumn {
-  color: #d44;
-  font-weight: bold;
-}
-.newColumn {
-  font-weight: bold;
-}
-
-.deltaColumn1 {
-  white-space: nowrap;
-  text-align: right !important;
-}
-
-.deltaColumn2 {
-  padding-left: 5px;
-  white-space: nowrap;
-  text-align: right;
-}
-
-.inserted {
-  height: 10px;
-  display: inline-block;
-  background-color: #4d4;
-}
-
-.deleted {
-  height: 10px;
-  display: inline-block;
-  background-color: #d44;
-}
-
-.restoreDelete div {
-  white-space: nowrap;
-}
-
-.restoreDelete button {
-  cursor: pointer;
-  padding: 0;
-  margin: 0 0 0 5px;
-  border: 0;
-  background-color: transparent;
-  white-space: nowrap;
-}
-
-.error {
-  color: #D33D3D;
-  font-weight: bold;
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/moreLess.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/moreLess.png
deleted file mode 100644
index 298514f..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/moreLess.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/related_changes.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/related_changes.css
deleted file mode 100644
index 5e0e402..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/related_changes.css
+++ /dev/null
@@ -1,98 +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.
- */
-
-@external .gwt-TabBarItem;
-@external .gwt-TabBarItem-disabled;
-@external .gwt-TabBarItem-selected;
-@external .gwt-TabBarRest;
-@external .gwt-TabPanelBottom;
-@eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
-
-.row {
-  white-space: nowrap;
-}
-
-.activeRow {
-  background-color: selectionColor !important;
-}
-
-.activeRow .pointer {
-  visibility: visible;
-}
-
-.pointer {
-  display: inline-block;
-  vertical-align: top;
-  visibility: hidden;
-}
-
-.subject, .strikedSubject {
-  display: inline-block;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-  width: 355px;
-}
-.strikedSubject {
-  text-decoration: line-through;
-}
-
-.tabPanel .gwt-TabBarItem,
-.tabPanel .gwt-TabBarItem-selected,
-.tabPanel .gwt-TabBarRest {
-  background: none repeat scroll 0 0 #FFF !important;
-}
-
-.tabPanel .gwt-TabPanelBottom {
-  padding: 2px 0 0 0;
-}
-
-.tabPanel .gwt-TabBarItem-selected {
-  border-bottom-color: #000 !important;
-  color: #000 !important;
-}
-
-.tabPanel .gwt-TabBarItem-disabled {
-  display: none;
-}
-
-.current,
-.gitweb,
-.indirect,
-.notCurrent,
-.submittable {
-  display: inline-block;
-  text-align: center;
-  vertical-align: top;
-  width: 12px;
-}
-
-.gitweb {
-  color: #000;
-}
-
-.indirect {
-  color: #090;      /* green */
-  font-weight: bold;
-}
-
-.notCurrent {
-  color: #FFA62F;   /* orange */
-}
-
-.submittable {
-  color: #090;      /* green */
-  font-weight: bold;
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
deleted file mode 100644
index 7ec1102..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
+++ /dev/null
@@ -1,212 +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.client.changes;
-
-import static java.util.Comparator.comparing;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.NotFoundScreen;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.client.ui.InlineHyperlink;
-import com.google.gerrit.client.ui.Screen;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.extensions.client.ListChangesOption;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwtexpui.globalkey.client.KeyCommand;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.EnumSet;
-import java.util.Set;
-
-public class AccountDashboardScreen extends Screen implements ChangeListScreen {
-  // If changing default options, also update in
-  // ChangeIT#defaultSearchDoesNotTouchDatabase().
-  private static final Set<ListChangesOption> MY_DASHBOARD_OPTIONS;
-
-  static {
-    EnumSet<ListChangesOption> options = EnumSet.copyOf(ChangeTable.OPTIONS);
-    options.add(ListChangesOption.REVIEWED);
-    MY_DASHBOARD_OPTIONS = Collections.unmodifiableSet(options);
-  }
-
-  private final Integer ownerId;
-  private final boolean mine;
-  private ChangeTable table;
-  private ChangeTable.Section workInProgress;
-  private ChangeTable.Section outgoing;
-  private ChangeTable.Section incoming;
-  private ChangeTable.Section closed;
-
-  public AccountDashboardScreen(Integer accountId) {
-    ownerId = accountId;
-    mine = Gerrit.isSignedIn() && ownerId == Gerrit.getUserAccount()._accountId();
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    table =
-        new ChangeTable() {
-          {
-            keysNavigation.add(
-                new KeyCommand(0, 'R', Util.C.keyReloadSearch()) {
-                  @Override
-                  public void onKeyPress(KeyPressEvent event) {
-                    Gerrit.display(getToken());
-                  }
-                });
-          }
-        };
-    table.addStyleName(Gerrit.RESOURCES.css().accountDashboard());
-
-    workInProgress = new ChangeTable.Section();
-    outgoing = new ChangeTable.Section();
-    incoming = new ChangeTable.Section();
-    closed = new ChangeTable.Section();
-
-    String who = mine ? "self" : ownerId.toString();
-    workInProgress.setTitleWidget(
-        new InlineHyperlink(
-            Util.C.workInProgress(), PageLinks.toChangeQuery(queryWorkInProgress(who))));
-    outgoing.setTitleWidget(
-        new InlineHyperlink(Util.C.outgoingReviews(), PageLinks.toChangeQuery(queryOutgoing(who))));
-    incoming.setTitleWidget(
-        new InlineHyperlink(Util.C.incomingReviews(), PageLinks.toChangeQuery(queryIncoming(who))));
-    incoming.setHighlightUnreviewed(mine);
-    closed.setTitleWidget(
-        new InlineHyperlink(Util.C.recentlyClosed(), PageLinks.toChangeQuery(queryClosed(who))));
-
-    table.addSection(workInProgress);
-    table.addSection(outgoing);
-    table.addSection(incoming);
-    table.addSection(closed);
-    add(table);
-    table.setSavePointerId("owner:" + ownerId);
-  }
-
-  private static String queryWorkInProgress(String who) {
-    return "is:open is:wip owner:" + who;
-  }
-
-  private static String queryOutgoing(String who) {
-    return "is:open -is:wip owner:" + who;
-  }
-
-  private static String queryIncoming(String who) {
-    return "is:open ((reviewer:"
-        + who
-        + " -owner:"
-        + who
-        + " -is:ignored) OR assignee:"
-        + who
-        + ")";
-  }
-
-  private static String queryClosed(String who) {
-    return "is:closed (owner:" + who + " OR reviewer:" + who + " OR assignee:" + who + ")";
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-
-    String who = mine ? "self" : ownerId.toString();
-    ChangeList.queryMultiple(
-        new ScreenLoadCallback<JsArray<ChangeList>>(this) {
-          @Override
-          protected void preDisplay(JsArray<ChangeList> result) {
-            display(result);
-          }
-        },
-        mine ? MY_DASHBOARD_OPTIONS : DashboardTable.OPTIONS,
-        queryWorkInProgress(who),
-        queryOutgoing(who),
-        queryIncoming(who),
-        queryClosed(who) + " -age:4w limit:10");
-  }
-
-  @Override
-  public void registerKeys() {
-    super.registerKeys();
-    table.setRegisterKeys(true);
-  }
-
-  private void display(JsArray<ChangeList> result) {
-    if (!mine && !hasChanges(result)) {
-      // When no results are returned and the data is not for the
-      // current user, the target user is presumed to not exist.
-      Gerrit.display(getToken(), new NotFoundScreen());
-      return;
-    }
-
-    ChangeList wip = result.get(0);
-    ChangeList out = result.get(1);
-    ChangeList in = result.get(2);
-    ChangeList done = result.get(3);
-
-    if (mine) {
-      setWindowTitle(Util.C.myDashboardTitle());
-      setPageTitle(Util.C.myDashboardTitle());
-    } else {
-      // The server doesn't tell us who the dashboard is for. Try to guess
-      // by looking at a change started by the owner and extract the name.
-      String name = guessName(out);
-      if (name == null) {
-        name = guessName(done);
-      }
-      if (name != null) {
-        setWindowTitle(name);
-        setPageTitle(Util.M.accountDashboardTitle(name));
-      } else {
-        setWindowTitle(Util.C.unknownDashboardTitle());
-        setWindowTitle(Util.C.unknownDashboardTitle());
-      }
-    }
-
-    Natives.asList(out).sort(outComparator());
-
-    table.updateColumnsForLabels(wip, out, in, done);
-    workInProgress.display(wip);
-    outgoing.display(out);
-    incoming.display(in);
-    closed.display(done);
-    table.finishDisplay();
-  }
-
-  private Comparator<ChangeInfo> outComparator() {
-    return comparing(ChangeInfo::created).thenComparing(ChangeInfo::_number);
-  }
-
-  private boolean hasChanges(JsArray<ChangeList> result) {
-    for (ChangeList list : Natives.asList(result)) {
-      if (list.length() != 0) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  private static String guessName(ChangeList list) {
-    for (ChangeInfo change : Natives.asList(list)) {
-      if (change.owner() != null && change.owner().name() != null) {
-        return change.owner().name();
-      }
-    }
-    return null;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
deleted file mode 100644
index 02be8c7..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
+++ /dev/null
@@ -1,401 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.changes;
-
-import com.google.gerrit.client.info.AccountInfo;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.info.ChangeInfo.CommitInfo;
-import com.google.gerrit.client.info.ChangeInfo.EditInfo;
-import com.google.gerrit.client.info.ChangeInfo.IncludedInInfo;
-import com.google.gerrit.client.rpc.CallbackGroup.Callback;
-import com.google.gerrit.client.rpc.NativeString;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-
-/** A collection of static methods which work on the Gerrit REST API for specific changes. */
-public class ChangeApi {
-  /** Abandon the change, ending its review. */
-  public static void abandon(
-      @Nullable String project, int id, String msg, AsyncCallback<ChangeInfo> cb) {
-    MessageInput input = MessageInput.create();
-    input.message(emptyToNull(msg));
-    call(project, id, "abandon").post(input, cb);
-  }
-
-  /** Create a new work-in-progress change. */
-  public static void createChange(
-      String project,
-      String branch,
-      String topic,
-      String subject,
-      String base,
-      AsyncCallback<ChangeInfo> cb) {
-    CreateChangeInput input = CreateChangeInput.create();
-    input.project(emptyToNull(project));
-    input.branch(emptyToNull(branch));
-    input.topic(emptyToNull(topic));
-    input.subject(emptyToNull(subject));
-    input.baseChange(emptyToNull(base));
-    input.workInProgress(true);
-
-    new RestApi("/changes/").post(input, cb);
-  }
-
-  /** Restore a previously abandoned change to be open again. */
-  public static void restore(
-      @Nullable String project, int id, String msg, AsyncCallback<ChangeInfo> cb) {
-    MessageInput input = MessageInput.create();
-    input.message(emptyToNull(msg));
-    call(project, id, "restore").post(input, cb);
-  }
-
-  /** Create a new change that reverts the delta caused by this change. */
-  public static void revert(
-      @Nullable String project, int id, String msg, AsyncCallback<ChangeInfo> cb) {
-    MessageInput input = MessageInput.create();
-    input.message(emptyToNull(msg));
-    call(project, id, "revert").post(input, cb);
-  }
-
-  /** Update the topic of a change. */
-  public static void topic(
-      @Nullable String project, int id, String topic, AsyncCallback<String> cb) {
-    RestApi call = call(project, id, "topic");
-    topic = emptyToNull(topic);
-    if (topic != null) {
-      TopicInput input = TopicInput.create();
-      input.topic(topic);
-      call.put(input, NativeString.unwrap(cb));
-    } else {
-      call.delete(NativeString.unwrap(cb));
-    }
-  }
-
-  public static void detail(@Nullable String project, int id, AsyncCallback<ChangeInfo> cb) {
-    detail(project, id).get(cb);
-  }
-
-  public static RestApi detail(@Nullable String project, int id) {
-    return call(project, id, "detail");
-  }
-
-  public static RestApi blame(@Nullable String project, PatchSet.Id id, String path, boolean base) {
-    return revision(project, id).view("files").id(path).view("blame").addParameter("base", base);
-  }
-
-  public static RestApi actions(@Nullable String project, int id, String revision) {
-    if (revision == null || revision.equals("")) {
-      revision = "current";
-    }
-    return call(project, id, revision, "actions");
-  }
-
-  public static void deleteAssignee(
-      @Nullable String project, int id, AsyncCallback<AccountInfo> cb) {
-    change(project, id).view("assignee").delete(cb);
-  }
-
-  public static void setAssignee(
-      @Nullable String project, int id, String user, AsyncCallback<AccountInfo> cb) {
-    AssigneeInput input = AssigneeInput.create();
-    input.assignee(user);
-    change(project, id).view("assignee").put(input, cb);
-  }
-
-  public static void markPrivate(
-      @Nullable String project, int id, AsyncCallback<JavaScriptObject> cb) {
-    change(project, id).view("private").post(PrivateInput.create(), cb);
-  }
-
-  public static void unmarkPrivate(
-      @Nullable String project, int id, AsyncCallback<JavaScriptObject> cb) {
-    change(project, id).view("private.delete").post(PrivateInput.create(), cb);
-  }
-
-  public static RestApi comments(@Nullable String project, int id) {
-    return call(project, id, "comments");
-  }
-
-  public static RestApi drafts(@Nullable String project, int id) {
-    return call(project, id, "drafts");
-  }
-
-  public static void edit(@Nullable String project, int id, AsyncCallback<EditInfo> cb) {
-    edit(project, id).get(cb);
-  }
-
-  public static void editWithFiles(@Nullable String project, int id, AsyncCallback<EditInfo> cb) {
-    edit(project, id).addParameterTrue("list").get(cb);
-  }
-
-  public static RestApi edit(@Nullable String project, int id) {
-    return change(project, id).view("edit");
-  }
-
-  public static RestApi editWithCommands(@Nullable String project, int id) {
-    return edit(project, id).addParameterTrue("download-commands");
-  }
-
-  public static void includedIn(
-      @Nullable String project, int id, AsyncCallback<IncludedInInfo> cb) {
-    call(project, id, "in").get(cb);
-  }
-
-  public static RestApi revision(@Nullable String project, int id, String revision) {
-    return change(project, id).view("revisions").id(revision);
-  }
-
-  public static RestApi revision(@Nullable String project, PatchSet.Id id) {
-    int cn = id.getParentKey().get();
-    String revision = RevisionInfoCache.get(id);
-    if (revision != null) {
-      return revision(project, cn, revision);
-    }
-    return change(project, cn).view("revisions").id(id.get());
-  }
-
-  public static RestApi reviewers(@Nullable String project, int id) {
-    return change(project, id).view("reviewers");
-  }
-
-  public static RestApi suggestReviewers(
-      @Nullable String project, int id, String q, int n, boolean e) {
-    RestApi api =
-        change(project, id).view("suggest_reviewers").addParameter("n", n).addParameter("e", e);
-    if (q != null) {
-      api.addParameter("q", q);
-    }
-    return api;
-  }
-
-  public static RestApi vote(@Nullable String project, int id, int reviewer, String vote) {
-    return reviewer(project, id, reviewer).view("votes").id(vote);
-  }
-
-  public static RestApi reviewer(@Nullable String project, int id, int reviewer) {
-    return change(project, id).view("reviewers").id(reviewer);
-  }
-
-  public static RestApi reviewer(@Nullable String project, int id, String reviewer) {
-    return change(project, id).view("reviewers").id(reviewer);
-  }
-
-  public static RestApi hashtags(@Nullable String project, int changeId) {
-    return change(project, changeId).view("hashtags");
-  }
-
-  public static RestApi hashtag(@Nullable String project, int changeId, String hashtag) {
-    return change(project, changeId).view("hashtags").id(hashtag);
-  }
-
-  /** Submit a specific revision of a change. */
-  public static void cherrypick(
-      String project,
-      int id,
-      String commit,
-      String destination,
-      String message,
-      AsyncCallback<ChangeInfo> cb) {
-    CherryPickInput cherryPickInput = CherryPickInput.create();
-    cherryPickInput.setMessage(message);
-    cherryPickInput.setDestination(destination);
-    call(project, id, commit, "cherrypick").post(cherryPickInput, cb);
-  }
-
-  /** Move change to another branch. */
-  public static void move(
-      String project, int id, String destination, String message, AsyncCallback<ChangeInfo> cb) {
-    MoveInput moveInput = MoveInput.create();
-    moveInput.setMessage(message);
-    moveInput.setDestinationBranch(destination);
-    change(project, id).view("move").post(moveInput, cb);
-  }
-
-  /** Edit commit message for specific revision of a change. */
-  public static void message(
-      @Nullable String project,
-      int id,
-      String commit,
-      String message,
-      AsyncCallback<JavaScriptObject> cb) {
-    CherryPickInput input = CherryPickInput.create();
-    input.setMessage(message);
-    call(project, id, commit, "message").post(input, cb);
-  }
-
-  /** Submit a specific revision of a change. */
-  public static void submit(
-      @Nullable String project, int id, String commit, AsyncCallback<SubmitInfo> cb) {
-    JavaScriptObject in = JavaScriptObject.createObject();
-    call(project, id, commit, "submit").post(in, cb);
-  }
-
-  /** Delete a specific draft change. */
-  public static void deleteChange(
-      @Nullable String project, int id, AsyncCallback<JavaScriptObject> cb) {
-    change(project, id).delete(cb);
-  }
-
-  /** Delete change edit. */
-  public static void deleteEdit(
-      @Nullable String project, int id, AsyncCallback<JavaScriptObject> cb) {
-    edit(project, id).delete(cb);
-  }
-
-  /** Publish change edit. */
-  public static void publishEdit(
-      @Nullable String project, int id, AsyncCallback<JavaScriptObject> cb) {
-    JavaScriptObject in = JavaScriptObject.createObject();
-    change(project, id).view("edit:publish").post(in, cb);
-  }
-
-  /** Rebase change edit on latest patch set. */
-  public static void rebaseEdit(
-      @Nullable String project, int id, AsyncCallback<JavaScriptObject> cb) {
-    JavaScriptObject in = JavaScriptObject.createObject();
-    change(project, id).view("edit:rebase").post(in, cb);
-  }
-
-  /** Rebase a revision onto the branch tip or another change. */
-  public static void rebase(
-      @Nullable String project, int id, String commit, String base, AsyncCallback<ChangeInfo> cb) {
-    RebaseInput rebaseInput = RebaseInput.create();
-    rebaseInput.setBase(base);
-    call(project, id, commit, "rebase").post(rebaseInput, cb);
-  }
-
-  private static class MessageInput extends JavaScriptObject {
-    final native void message(String m) /*-{ if(m)this.message=m; }-*/;
-
-    static MessageInput create() {
-      return (MessageInput) createObject();
-    }
-
-    protected MessageInput() {}
-  }
-
-  private static class AssigneeInput extends JavaScriptObject {
-    final native void assignee(String a) /*-{ if(a)this.assignee=a; }-*/;
-
-    static AssigneeInput create() {
-      return (AssigneeInput) createObject();
-    }
-
-    protected AssigneeInput() {}
-  }
-
-  private static class TopicInput extends JavaScriptObject {
-    final native void topic(String t) /*-{ if(t)this.topic=t; }-*/;
-
-    static TopicInput create() {
-      return (TopicInput) createObject();
-    }
-
-    protected TopicInput() {}
-  }
-
-  private static class CreateChangeInput extends JavaScriptObject {
-    static CreateChangeInput create() {
-      return (CreateChangeInput) createObject();
-    }
-
-    public final native void branch(String b) /*-{ if(b)this.branch=b; }-*/;
-
-    public final native void topic(String t) /*-{ if(t)this.topic=t; }-*/;
-
-    public final native void project(String p) /*-{ if(p)this.project=p; }-*/;
-
-    public final native void subject(String s) /*-{ if(s)this.subject=s; }-*/;
-
-    public final native void status(String s) /*-{ if(s)this.status=s; }-*/;
-
-    public final native void baseChange(String b) /*-{ if(b)this.base_change=b; }-*/;
-
-    public final native void workInProgress(Boolean b) /*-{ if(b)this.work_in_progress=b; }-*/;
-
-    protected CreateChangeInput() {}
-  }
-
-  private static class CherryPickInput extends JavaScriptObject {
-    static CherryPickInput create() {
-      return (CherryPickInput) createObject();
-    }
-
-    final native void setDestination(String d) /*-{ this.destination = d; }-*/;
-
-    final native void setMessage(String m) /*-{ this.message = m; }-*/;
-
-    protected CherryPickInput() {}
-  }
-
-  private static class MoveInput extends JavaScriptObject {
-    static MoveInput create() {
-      return (MoveInput) createObject();
-    }
-
-    final native void setDestinationBranch(String d) /*-{ this.destination_branch = d; }-*/;
-
-    final native void setMessage(String m) /*-{ this.message = m; }-*/;
-
-    protected MoveInput() {}
-  }
-
-  private static class PrivateInput extends JavaScriptObject {
-    static PrivateInput create() {
-      return (PrivateInput) createObject();
-    }
-
-    final native void setMessage(String m) /*-{ this.message = m; }-*/;
-
-    protected PrivateInput() {}
-  }
-
-  private static class RebaseInput extends JavaScriptObject {
-    final native void setBase(String b) /*-{ this.base = b; }-*/;
-
-    static RebaseInput create() {
-      return (RebaseInput) createObject();
-    }
-
-    protected RebaseInput() {}
-  }
-
-  private static RestApi call(@Nullable String project, int id, String action) {
-    return change(project, id).view(action);
-  }
-
-  private static RestApi call(@Nullable String project, int id, String commit, String action) {
-    return change(project, id).view("revisions").id(commit).view(action);
-  }
-
-  public static RestApi change(@Nullable String project, int id) {
-    if (project == null) {
-      return new RestApi("/changes/").id(String.valueOf(id));
-    }
-    return new RestApi("/changes/").id(project, id);
-  }
-
-  public static String emptyToNull(String str) {
-    return str == null || str.isEmpty() ? null : str;
-  }
-
-  public static void commitWithLinks(
-      @Nullable String project, int changeId, String revision, Callback<CommitInfo> callback) {
-    revision(project, changeId, revision).view("commit").addParameterTrue("links").get(callback);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
deleted file mode 100644
index aa6c4ec..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
+++ /dev/null
@@ -1,187 +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.client.changes;
-
-import com.google.gwt.i18n.client.Constants;
-
-public interface ChangeConstants extends Constants {
-  String statusLongNew();
-
-  String statusLongMerged();
-
-  String statusLongAbandoned();
-
-  String statusLongDraft();
-
-  String submittable();
-
-  String readyToSubmit();
-
-  String mergeConflict();
-
-  String notCurrent();
-
-  String isPrivate();
-
-  String isWorkInProgress();
-
-  String changeEdit();
-
-  String myDashboardTitle();
-
-  String unknownDashboardTitle();
-
-  String workInProgress();
-
-  String incomingReviews();
-
-  String outgoingReviews();
-
-  String recentlyClosed();
-
-  String changeTableColumnSubject();
-
-  String changeTableColumnSize();
-
-  String changeTableColumnStatus();
-
-  String changeTableColumnOwner();
-
-  String changeTableColumnAssignee();
-
-  String changeTableColumnProject();
-
-  String changeTableColumnBranch();
-
-  String changeTableColumnLastUpdate();
-
-  String changeTableColumnID();
-
-  String changeTableNone();
-
-  String changeTableNotMergeable();
-
-  String changeItemHelp();
-
-  String changeTableStar();
-
-  String changeTablePagePrev();
-
-  String changeTablePageNext();
-
-  String upToChangeList();
-
-  String keyReloadChange();
-
-  String keyNextPatchSet();
-
-  String keyPreviousPatchSet();
-
-  String keyReloadSearch();
-
-  String keyPublishComments();
-
-  String keyEditTopic();
-
-  String keyAddReviewers();
-
-  String keyExpandAllMessages();
-
-  String keyCollapseAllMessages();
-
-  String patchTableColumnName();
-
-  String patchTableColumnComments();
-
-  String patchTableColumnSize();
-
-  String commitMessage();
-
-  String mergeList();
-
-  String patchTablePrev();
-
-  String patchTableNext();
-
-  String patchTableOpenDiff();
-
-  String approvalTableEditAssigneeHint();
-
-  String approvalTableAddReviewerHint();
-
-  String approvalTableAddManyReviewersConfirmationDialogTitle();
-
-  String changeInfoBlockUploaded();
-
-  String changeInfoBlockUpdated();
-
-  String messageNoAuthor();
-
-  String sideBySide();
-
-  String unifiedDiff();
-
-  String buttonRevertChangeSend();
-
-  String headingRevertMessage();
-
-  String revertChangeTitle();
-
-  String buttonCherryPickChangeSend();
-
-  String headingCherryPickBranch();
-
-  String cherryPickCommitMessage();
-
-  String cherryPickTitle();
-
-  String moveChangeSend();
-
-  String headingMoveBranch();
-
-  String moveChangeMessage();
-
-  String moveTitle();
-
-  String buttonRebaseChangeSend();
-
-  String rebaseConfirmMessage();
-
-  String rebaseNotPossibleMessage();
-
-  String rebasePlaceholderMessage();
-
-  String rebaseTitle();
-
-  String baseDiffItem();
-
-  String autoMerge();
-
-  String pagedChangeListPrev();
-
-  String pagedChangeListNext();
-
-  String submitFailed();
-
-  String votable();
-
-  String pushCertMissing();
-
-  String pushCertBad();
-
-  String pushCertOk();
-
-  String pushCertTrusted();
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
deleted file mode 100644
index 2d5a9f9..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
+++ /dev/null
@@ -1,103 +0,0 @@
-statusLongNew = Review in Progress
-statusLongMerged = Merged
-statusLongAbandoned = Abandoned
-statusLongDraft = Draft
-submittable = Submittable
-readyToSubmit = Ready to Submit
-mergeConflict = Merge Conflict
-notCurrent = Not Current
-changeEdit = Change Edit
-isPrivate = (Private)
-isWorkInProgress = (Work in Progress)
-
-myDashboardTitle = My Reviews
-unknownDashboardTitle = Code Review Dashboard
-workInProgress Work in progress
-incomingReviews = Incoming reviews
-outgoingReviews = Outgoing reviews
-recentlyClosed = Recently closed
-
-changeTableColumnSubject = Subject
-changeTableColumnSize = Size
-changeTableColumnStatus = Status
-changeTableColumnOwner = Owner
-changeTableColumnAssignee = Assignee
-changeTableColumnProject = Project
-changeTableColumnBranch = Branch
-changeTableColumnLastUpdate = Updated
-changeTableColumnID = ID
-changeTableNone = (None)
-changeTableNotMergeable = Merge Conflict
-
-changeItemHelp = change
-changeTableStar = Star (or unstar) change
-changeTablePagePrev = Previous page of changes
-changeTablePageNext = Next page of changes
-upToChangeList = Up to change list
-keyReloadChange = Reload change
-keyNextPatchSet = Next patch set
-keyPreviousPatchSet = Previous patch set
-keyReloadSearch = Reload change list
-keyPublishComments = Review and publish comments
-keyEditTopic = Edit change topic
-keyAddReviewers = Add reviewers
-keyExpandAllMessages = Expand all messages
-keyCollapseAllMessages = Collapse all messages
-
-patchTableColumnName = File Path
-patchTableColumnComments = Comments
-patchTableColumnSize = Size
-commitMessage = Commit Message
-mergeList = Merge List
-
-patchTablePrev = Previous file
-patchTableNext = Next file
-patchTableOpenDiff = Open diff
-
-approvalTableEditAssigneeHint = Name or Email
-
-approvalTableAddReviewerHint = Name or Email or Group
-approvalTableAddManyReviewersConfirmationDialogTitle = Adding Group Members as Reviewers
-
-changeInfoBlockUploaded = Uploaded
-changeInfoBlockUpdated = Updated
-
-messageNoAuthor = Gerrit Code Review
-
-sideBySide = Side by Side
-unifiedDiff = Unified Diff
-
-baseDiffItem = Base
-autoMerge = Auto Merge
-
-buttonRevertChangeSend = Revert Change
-headingRevertMessage = Revert Commit Message:
-revertChangeTitle = Code Review - Revert Merged Change
-
-buttonCherryPickChangeSend = Cherry Pick Change
-headingCherryPickBranch = Cherry Pick to Branch:
-cherryPickCommitMessage = Cherry Pick Commit Message:
-cherryPickTitle = Code Review - Cherry Pick Change to Another Branch
-
-headingMoveBranch = Move Change to Branch:
-moveChangeSend = Move Change
-moveChangeMessage = Move Change Message:
-moveTitle = Code Review - Move Change to Another Branch
-
-buttonRebaseChangeSend = Rebase
-rebaseConfirmMessage = Change parent revision
-rebaseNotPossibleMessage = Change is already up to date
-rebasePlaceholderMessage = (subject, change number, or leave empty)
-rebaseTitle = Code Review - Rebase Change
-
-pagedChangeListPrev = &#x21e6;Prev
-pagedChangeListNext = Next&#x21e8;
-
-submitFailed = Submit Failed
-
-votable = Votable:
-
-pushCertMissing = This patch set was created without a push certificate
-pushCertBad = Push certificate is invalid
-pushCertOk = Push certificate is valid, but key is not trusted
-pushCertTrusted = Push certificate is valid and key is trusted
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeEditApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeEditApi.java
deleted file mode 100644
index 71b54f7d..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeEditApi.java
+++ /dev/null
@@ -1,142 +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.client.changes;
-
-import com.google.gerrit.client.VoidResult;
-import com.google.gerrit.client.editor.EditFileInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.HttpCallback;
-import com.google.gerrit.client.rpc.NativeString;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-
-/** REST API helpers to remotely edit a change. */
-public class ChangeEditApi {
-  /** Get file (or commit message) contents. */
-  public static void get(
-      @Nullable Project.NameKey project,
-      PatchSet.Id id,
-      String path,
-      boolean base,
-      HttpCallback<NativeString> cb) {
-    RestApi api;
-    if (id.get() != 0) {
-      // Read from a published revision, when change edit doesn't
-      // exist for the caller, or is not currently active.
-      api =
-          ChangeApi.revision(Project.NameKey.asStringOrNull(project), id)
-              .view("files")
-              .id(path)
-              .view("content");
-    } else if (Patch.COMMIT_MSG.equals(path)) {
-      api =
-          editMessage(Project.NameKey.asStringOrNull(project), id.getParentKey().get())
-              .addParameter("base", base);
-    } else {
-      api =
-          editFile(Project.NameKey.asStringOrNull(project), id.getParentKey().get(), path)
-              .addParameter("base", base);
-    }
-    api.get(cb);
-  }
-
-  /** Get file (or commit message) contents of the edit. */
-  public static void get(
-      @Nullable Project.NameKey project,
-      PatchSet.Id id,
-      String path,
-      HttpCallback<NativeString> cb) {
-    get(project, id, path, false, cb);
-  }
-
-  /** Get meta info for change edit. */
-  public static void getMeta(
-      @Nullable String project, PatchSet.Id id, String path, AsyncCallback<EditFileInfo> cb) {
-    if (id.get() != 0) {
-      throw new IllegalStateException("only supported for edits");
-    }
-    editFile(project, id.getParentKey().get(), path).view("meta").get(cb);
-  }
-
-  /** Put message into a change edit. */
-  public static void putMessage(
-      @Nullable String project, int id, String m, GerritCallback<VoidResult> cb) {
-    editMessage(project, id).put(m, cb);
-  }
-
-  /** Put contents into a file or commit message in a change edit. */
-  public static void put(
-      @Nullable String project,
-      int id,
-      String path,
-      String content,
-      GerritCallback<VoidResult> cb) {
-    if (Patch.COMMIT_MSG.equals(path)) {
-      putMessage(project, id, content, cb);
-    } else {
-      editFile(project, id, path).put(content, cb);
-    }
-  }
-
-  /** Delete a file in the pending edit. */
-  public static void delete(
-      @Nullable String project, int id, String path, AsyncCallback<VoidResult> cb) {
-    editFile(project, id, path).delete(cb);
-  }
-
-  /** Rename a file in the pending edit. */
-  public static void rename(
-      @Nullable String project, int id, String path, String newPath, AsyncCallback<VoidResult> cb) {
-    Input in = Input.create();
-    in.oldPath(path);
-    in.newPath(newPath);
-    ChangeApi.edit(project, id).post(in, cb);
-  }
-
-  /** Restore (undo delete/modify) a file in the pending edit. */
-  public static void restore(
-      @Nullable String project, int id, String path, AsyncCallback<VoidResult> cb) {
-    Input in = Input.create();
-    in.restorePath(path);
-    ChangeApi.edit(project, id).post(in, cb);
-  }
-
-  private static RestApi editMessage(@Nullable String project, int id) {
-    return ChangeApi.change(project, id).view("edit:message");
-  }
-
-  private static RestApi editFile(@Nullable String project, int id, String path) {
-    return ChangeApi.edit(project, id).id(path);
-  }
-
-  private static class Input extends JavaScriptObject {
-    static Input create() {
-      return createObject().cast();
-    }
-
-    final native void restorePath(String p) /*-{ this.restore_path=p }-*/;
-
-    final native void oldPath(String p) /*-{ this.old_path=p }-*/;
-
-    final native void newPath(String p) /*-{ this.new_path=p }-*/;
-
-    protected Input() {}
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java
deleted file mode 100644
index 5d525b6..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java
+++ /dev/null
@@ -1,100 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.changes;
-
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.extensions.client.ListChangesOption;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwtorm.client.KeyUtil;
-import java.util.Set;
-
-/** List of changes available from {@code /changes/}. */
-public class ChangeList extends JsArray<ChangeInfo> {
-  private static final String URI = "/changes/";
-
-  /** Run multiple queries in a single remote invocation. */
-  public static void queryMultiple(
-      final AsyncCallback<JsArray<ChangeList>> callback,
-      Set<ListChangesOption> options,
-      String... queries) {
-    if (queries.length == 0) {
-      return;
-    }
-    RestApi call = new RestApi(URI);
-    for (String q : queries) {
-      call.addParameterRaw("q", KeyUtil.encode(q));
-    }
-    addOptions(call, options);
-    if (queries.length == 1) {
-      // Server unwraps a single query, so wrap it back in an array for the
-      // callback.
-      call.get(
-          new AsyncCallback<ChangeList>() {
-            @Override
-            public void onSuccess(ChangeList result) {
-              JsArray<ChangeList> wrapped = JsArray.createArray(1).cast();
-              wrapped.set(0, result);
-              callback.onSuccess(wrapped);
-            }
-
-            @Override
-            public void onFailure(Throwable caught) {
-              callback.onFailure(caught);
-            }
-          });
-    } else {
-      call.get(callback);
-    }
-  }
-
-  public static void query(
-      String query, Set<ListChangesOption> options, AsyncCallback<ChangeList> callback) {
-    query(query, options, callback, 0, 0);
-  }
-
-  public static void query(
-      String query,
-      Set<ListChangesOption> options,
-      AsyncCallback<ChangeList> callback,
-      int start,
-      int limit) {
-    RestApi call = newQuery(query);
-    if (limit > 0) {
-      call.addParameter("n", limit);
-    }
-    addOptions(call, options);
-    if (start != 0) {
-      call.addParameter("S", start);
-    }
-    call.get(callback);
-  }
-
-  public static void addOptions(RestApi call, Set<ListChangesOption> s) {
-    call.addParameterRaw("O", Integer.toHexString(ListChangesOption.toBits(s)));
-  }
-
-  private static RestApi newQuery(String query) {
-    RestApi call = new RestApi(URI);
-    // The server default is ?q=status:open so don't repeat it.
-    if (!"status:open".equals(query) && !"is:open".equals(query)) {
-      call.addParameterRaw("q", KeyUtil.encode(query));
-    }
-    return call;
-  }
-
-  protected ChangeList() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeListScreen.java
deleted file mode 100644
index ed5a6f2..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeListScreen.java
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT 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.client.changes;
-
-public interface ChangeListScreen {}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
deleted file mode 100644
index c64fe91..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
+++ /dev/null
@@ -1,57 +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.client.changes;
-
-import com.google.gwt.i18n.client.Messages;
-
-public interface ChangeMessages extends Messages {
-  String accountDashboardTitle(String fullName);
-
-  String revertChangeDefaultMessage(String commitMsg, String commitId);
-
-  String cherryPickedChangeDefaultMessage(String commitMsg, String commitId);
-
-  String changeScreenTitleId(String changeId);
-
-  String loadingPatchSet(int id);
-
-  String patchTableSize_Modify(int insertions, int deletions);
-
-  String patchTableSize_ModifyBinaryFiles(String bytesInserted, String bytesDeleted);
-
-  String patchTableSize_ModifyBinaryFilesWithPercentages(
-      String bytesInserted,
-      String percentageInserted,
-      String bytesDeleted,
-      String percentageDeleted);
-
-  String patchTableSize_LongModify(int insertions, int deletions);
-
-  String removeReviewer(String fullName);
-
-  String removeVote(String label);
-
-  String blockedOn(String labelName);
-
-  String needs(String labelName);
-
-  String changeQueryWindowTitle(String query);
-
-  String changeQueryPageTitle(String query);
-
-  String insertionsAndDeletions(int insertions, int deletions);
-
-  String diffBaseParent(int parentNum);
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
deleted file mode 100644
index 2b68492..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
+++ /dev/null
@@ -1,27 +0,0 @@
-# Changes to this file should also be made in
-# gerrit-server/src/main/resources/com/google/gerrit/server/change/ChangeMessages.properties
-accountDashboardTitle = Code Review Dashboard for {0}
-
-revertChangeDefaultMessage = Revert \"{0}\"\n\nThis reverts commit {1}.
-cherryPickedChangeDefaultMessage = {0}\n(cherry picked from commit {1})
-
-changeScreenTitleId = Change {0}
-loadingPatchSet = Loading Patch Set {0} ...
-
-patchTableSize_Modify = +{0}, -{1}
-patchTableSize_ModifyBinaryFiles = +{0}, -{1}
-patchTableSize_ModifyBinaryFilesWithPercentages = +{0} (+{1}), -{2} (-{3})
-patchTableSize_LongModify = {0} inserted, {1} deleted
-
-removeReviewer = Remove reviewer {0}
-removeVote = Remove vote {0}
-
-blockedOn = Blocked on {0} Label
-needs = Needs {0} Label
-
-changeQueryWindowTitle = {0}
-changeQueryPageTitle = Search for {0}
-
-insertionsAndDeletions = +{0}, -{1}
-
-diffBaseParent = Parent {0}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
deleted file mode 100644
index 4fda78b..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
+++ /dev/null
@@ -1,558 +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.client.changes;
-
-import static com.google.gerrit.client.FormatUtil.relativeFormat;
-import static com.google.gerrit.client.FormatUtil.shortFormat;
-import static java.util.stream.Collectors.toList;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.info.AccountInfo;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.info.ChangeInfo.LabelInfo;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.ui.AccountLinkPanel;
-import com.google.gerrit.client.ui.BranchLink;
-import com.google.gerrit.client.ui.ChangeLink;
-import com.google.gerrit.client.ui.NavigationTable;
-import com.google.gerrit.client.ui.NeedsSignInKeyCommand;
-import com.google.gerrit.client.ui.ProjectLink;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.extensions.client.GeneralPreferencesInfo.ReviewCategoryStrategy;
-import com.google.gerrit.extensions.client.ListChangesOption;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.HTMLTable.Cell;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwt.user.client.ui.InlineLabel;
-import com.google.gwt.user.client.ui.SimplePanel;
-import com.google.gwt.user.client.ui.UIObject;
-import com.google.gwt.user.client.ui.Widget;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.EnumSet;
-import java.util.List;
-import java.util.Objects;
-import java.util.Set;
-
-public class ChangeTable extends NavigationTable<ChangeInfo> {
-  // If changing default options, also update in
-  // ChangeIT#defaultSearchDoesNotTouchDatabase().
-  static final Set<ListChangesOption> OPTIONS =
-      Collections.unmodifiableSet(
-          EnumSet.of(ListChangesOption.LABELS, ListChangesOption.DETAILED_ACCOUNTS));
-
-  private static final int C_STAR = 1;
-  private static final int C_ID = 2;
-  private static final int C_SUBJECT = 3;
-  private static final int C_STATUS = 4;
-  private static final int C_OWNER = 5;
-  private static final int C_ASSIGNEE = 6;
-  private static final int C_PROJECT = 7;
-  private static final int C_BRANCH = 8;
-  private static final int C_LAST_UPDATE = 9;
-  private static final int C_SIZE = 10;
-  private static final int BASE_COLUMNS = 11;
-
-  private final List<Section> sections;
-  private int columns;
-  private final boolean showAssignee;
-  private final boolean showLegacyId;
-  private List<String> labelNames;
-
-  public ChangeTable() {
-    super(Util.C.changeItemHelp());
-    columns = BASE_COLUMNS;
-    labelNames = Collections.emptyList();
-    showAssignee = Gerrit.info().change().showAssigneeInChangesTable();
-    showLegacyId = Gerrit.getUserPreferences().legacycidInChangeTable();
-
-    if (Gerrit.isSignedIn()) {
-      keysAction.add(new StarKeyCommand(0, 's', Util.C.changeTableStar()));
-    }
-
-    sections = new ArrayList<>();
-    table.setText(0, C_STAR, "");
-    table.setText(0, C_ID, Util.C.changeTableColumnID());
-    table.setText(0, C_SUBJECT, Util.C.changeTableColumnSubject());
-    table.setText(0, C_STATUS, Util.C.changeTableColumnStatus());
-    table.setText(0, C_OWNER, Util.C.changeTableColumnOwner());
-    table.setText(0, C_ASSIGNEE, Util.C.changeTableColumnAssignee());
-    table.setText(0, C_PROJECT, Util.C.changeTableColumnProject());
-    table.setText(0, C_BRANCH, Util.C.changeTableColumnBranch());
-    table.setText(0, C_LAST_UPDATE, Util.C.changeTableColumnLastUpdate());
-    table.setText(0, C_SIZE, Util.C.changeTableColumnSize());
-
-    final FlexCellFormatter fmt = table.getFlexCellFormatter();
-    fmt.addStyleName(0, C_STAR, Gerrit.RESOURCES.css().iconHeader());
-    for (int i = C_ID; i < columns; i++) {
-      fmt.addStyleName(0, i, Gerrit.RESOURCES.css().dataHeader());
-    }
-    if (!showLegacyId) {
-      fmt.addStyleName(0, C_ID, Gerrit.RESOURCES.css().dataHeaderHidden());
-    }
-    if (!showAssignee) {
-      fmt.addStyleName(0, C_ASSIGNEE, Gerrit.RESOURCES.css().dataHeaderHidden());
-    }
-
-    table.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            final Cell cell = table.getCellForEvent(event);
-            if (cell == null) {
-              return;
-            }
-            if (cell.getCellIndex() == C_STAR) {
-              // Don't do anything (handled by star itself).
-            } else if (cell.getCellIndex() == C_STATUS) {
-              // Don't do anything.
-            } else if (cell.getCellIndex() == C_OWNER) {
-              // Don't do anything.
-            } else if (getRowItem(cell.getRowIndex()) != null) {
-              movePointerTo(cell.getRowIndex());
-            }
-          }
-        });
-  }
-
-  @Override
-  protected Object getRowItemKey(ChangeInfo item) {
-    return item.legacyId();
-  }
-
-  @Override
-  protected void onOpenRow(int row) {
-    final ChangeInfo c = getRowItem(row);
-    Gerrit.display(PageLinks.toChange(c.projectNameKey(), c.legacyId()));
-  }
-
-  private void insertNoneRow(int row) {
-    insertRow(row);
-    table.setText(row, 0, Util.C.changeTableNone());
-    final FlexCellFormatter fmt = table.getFlexCellFormatter();
-    fmt.setColSpan(row, 0, columns);
-    fmt.setStyleName(row, 0, Gerrit.RESOURCES.css().emptySection());
-  }
-
-  private void insertChangeRow(int row) {
-    insertRow(row);
-    applyDataRowStyle(row);
-  }
-
-  @Override
-  protected void applyDataRowStyle(int row) {
-    super.applyDataRowStyle(row);
-    final CellFormatter fmt = table.getCellFormatter();
-    fmt.addStyleName(row, C_STAR, Gerrit.RESOURCES.css().iconCell());
-    for (int i = C_ID; i < columns; i++) {
-      fmt.addStyleName(row, i, Gerrit.RESOURCES.css().dataCell());
-    }
-    if (!showLegacyId) {
-      fmt.addStyleName(row, C_ID, Gerrit.RESOURCES.css().dataCellHidden());
-    }
-    fmt.addStyleName(row, C_SUBJECT, Gerrit.RESOURCES.css().cSUBJECT());
-    fmt.addStyleName(row, C_STATUS, Gerrit.RESOURCES.css().cSTATUS());
-    fmt.addStyleName(row, C_OWNER, Gerrit.RESOURCES.css().cOWNER());
-    fmt.addStyleName(
-        row,
-        C_ASSIGNEE,
-        showAssignee
-            ? Gerrit.RESOURCES.css().cASSIGNEE()
-            : Gerrit.RESOURCES.css().dataCellHidden());
-    fmt.addStyleName(row, C_LAST_UPDATE, Gerrit.RESOURCES.css().cLastUpdate());
-    fmt.addStyleName(row, C_SIZE, Gerrit.RESOURCES.css().cSIZE());
-
-    for (int i = C_SIZE + 1; i < columns; i++) {
-      fmt.addStyleName(row, i, Gerrit.RESOURCES.css().cAPPROVAL());
-    }
-  }
-
-  public void updateColumnsForLabels(ChangeList... lists) {
-    labelNames =
-        Arrays.stream(lists)
-            .flatMap(l -> Natives.asList(l).stream())
-            .flatMap(c -> c.labels().stream())
-            .distinct()
-            .sorted()
-            .collect(toList());
-
-    int baseColumns = BASE_COLUMNS;
-    if (baseColumns + labelNames.size() < columns) {
-      int n = columns - (baseColumns + labelNames.size());
-      for (int row = 0; row < table.getRowCount(); row++) {
-        table.removeCells(row, columns, n);
-      }
-    }
-    columns = baseColumns + labelNames.size();
-
-    FlexCellFormatter fmt = table.getFlexCellFormatter();
-    for (int i = 0; i < labelNames.size(); i++) {
-      String name = labelNames.get(i);
-      int col = baseColumns + i;
-
-      String abbrev = getAbbreviation(name, "-");
-      table.setText(0, col, abbrev);
-      table.getCellFormatter().getElement(0, col).setTitle(name);
-      fmt.addStyleName(0, col, Gerrit.RESOURCES.css().dataHeader());
-    }
-
-    for (Section s : sections) {
-      if (s.titleRow >= 0) {
-        fmt.setColSpan(s.titleRow, 0, columns);
-      }
-    }
-  }
-
-  private void populateChangeRow(int row, ChangeInfo c, boolean highlightUnreviewed) {
-    CellFormatter fmt = table.getCellFormatter();
-    if (Gerrit.isSignedIn()) {
-      table.setWidget(row, C_STAR, StarredChanges.createIcon(c.legacyId(), c.starred()));
-    }
-    table.setWidget(row, C_ID, new TableChangeLink(String.valueOf(c.legacyId()), c));
-
-    String subject = Util.cropSubject(c.subject());
-    table.setWidget(row, C_SUBJECT, new TableChangeLink(subject, c));
-
-    Change.Status status = c.status();
-    if (status != Change.Status.NEW) {
-      table.setText(
-          row,
-          C_STATUS,
-          Util.toLongString(status) + (c.isPrivate() ? (" " + Util.C.isPrivate()) : ""));
-    } else if (c.isWorkInProgress()) {
-      table.setText(
-          row,
-          C_STATUS,
-          Util.C.workInProgress() + (c.isPrivate() ? (" " + Util.C.isPrivate()) : ""));
-    } else if (!c.mergeable()) {
-      table.setText(
-          row,
-          C_STATUS,
-          Util.C.changeTableNotMergeable() + (c.isPrivate() ? (" " + Util.C.isPrivate()) : ""));
-    } else if (c.isPrivate()) {
-      table.setText(row, C_STATUS, Util.C.isPrivate());
-    }
-
-    if (c.owner() != null) {
-      table.setWidget(row, C_OWNER, AccountLinkPanel.withStatus(c.owner(), status));
-    } else {
-      table.setText(row, C_OWNER, "");
-    }
-
-    if (showAssignee) {
-      if (c.assignee() != null) {
-        table.setWidget(row, C_ASSIGNEE, AccountLinkPanel.forAssignee(c.assignee()));
-        if (Gerrit.getUserPreferences().highlightAssigneeInChangeTable()
-            && Objects.equals(c.assignee()._accountId(), Gerrit.getUserAccount()._accountId())) {
-          table.getRowFormatter().addStyleName(row, Gerrit.RESOURCES.css().cASSIGNEDTOME());
-        }
-      } else {
-        table.setText(row, C_ASSIGNEE, "");
-      }
-    }
-
-    table.setWidget(row, C_PROJECT, new ProjectLink(c.projectNameKey()));
-    table.setWidget(
-        row, C_BRANCH, new BranchLink(c.projectNameKey(), c.status(), c.branch(), c.topic()));
-    if (Gerrit.getUserPreferences().relativeDateInChangeTable()) {
-      table.setText(row, C_LAST_UPDATE, relativeFormat(c.updated()));
-    } else {
-      table.setText(row, C_LAST_UPDATE, shortFormat(c.updated()));
-    }
-
-    int col = C_SIZE;
-    if (!Gerrit.getUserPreferences().sizeBarInChangeTable()) {
-      table.setText(row, col, Util.M.insertionsAndDeletions(c.insertions(), c.deletions()));
-    } else {
-      table.setWidget(row, col, getSizeWidget(c));
-      fmt.getElement(row, col)
-          .setTitle(Util.M.insertionsAndDeletions(c.insertions(), c.deletions()));
-    }
-    col++;
-
-    for (int idx = 0; idx < labelNames.size(); idx++, col++) {
-      String name = labelNames.get(idx);
-
-      LabelInfo label = c.label(name);
-      if (label == null) {
-        fmt.getElement(row, col).setTitle(Gerrit.C.labelNotApplicable());
-        fmt.addStyleName(row, col, Gerrit.RESOURCES.css().labelNotApplicable());
-        continue;
-      }
-
-      String user;
-      String info;
-      ReviewCategoryStrategy reviewCategoryStrategy =
-          Gerrit.getUserPreferences().reviewCategoryStrategy();
-      if (label.rejected() != null) {
-        user = label.rejected().name();
-        info = getReviewCategoryDisplayInfo(reviewCategoryStrategy, label.rejected());
-        if (info != null) {
-          FlowPanel panel = new FlowPanel();
-          panel.add(new Image(Gerrit.RESOURCES.redNot()));
-          panel.add(new InlineLabel(info));
-          table.setWidget(row, col, panel);
-        } else {
-          table.setWidget(row, col, new Image(Gerrit.RESOURCES.redNot()));
-        }
-      } else if (label.approved() != null) {
-        user = label.approved().name();
-        info = getReviewCategoryDisplayInfo(reviewCategoryStrategy, label.approved());
-        if (info != null) {
-          FlowPanel panel = new FlowPanel();
-          panel.add(new Image(Gerrit.RESOURCES.greenCheck()));
-          panel.add(new InlineLabel(info));
-          table.setWidget(row, col, panel);
-        } else {
-          table.setWidget(row, col, new Image(Gerrit.RESOURCES.greenCheck()));
-        }
-      } else if (label.disliked() != null) {
-        user = label.disliked().name();
-        info = getReviewCategoryDisplayInfo(reviewCategoryStrategy, label.disliked());
-        String vstr = String.valueOf(label._value());
-        if (info != null) {
-          vstr = vstr + " " + info;
-        }
-        fmt.addStyleName(row, col, Gerrit.RESOURCES.css().negscore());
-        table.setText(row, col, vstr);
-      } else if (label.recommended() != null) {
-        user = label.recommended().name();
-        info = getReviewCategoryDisplayInfo(reviewCategoryStrategy, label.recommended());
-        String vstr = "+" + label._value();
-        if (info != null) {
-          vstr = vstr + " " + info;
-        }
-        fmt.addStyleName(row, col, Gerrit.RESOURCES.css().posscore());
-        table.setText(row, col, vstr);
-      } else {
-        table.clearCell(row, col);
-        continue;
-      }
-      fmt.addStyleName(row, col, Gerrit.RESOURCES.css().singleLine());
-
-      if (user != null) {
-        // Some web browsers ignore the embedded newline; some like it;
-        // so we include a space before the newline to accommodate both.
-        fmt.getElement(row, col).setTitle(name + " \nby " + user);
-      }
-    }
-
-    boolean needHighlight = false;
-    if (highlightUnreviewed && !c.reviewed()) {
-      needHighlight = true;
-    }
-    final Element tr = fmt.getElement(row, 0).getParentElement();
-    UIObject.setStyleName(tr, Gerrit.RESOURCES.css().needsReview(), needHighlight);
-
-    setRowItem(row, c);
-  }
-
-  private static String getReviewCategoryDisplayInfo(
-      ReviewCategoryStrategy reviewCategoryStrategy, AccountInfo accountInfo) {
-    switch (reviewCategoryStrategy) {
-      case NAME:
-        return accountInfo.name();
-      case EMAIL:
-        return accountInfo.email();
-      case USERNAME:
-        return accountInfo.username();
-      case ABBREV:
-        return getAbbreviation(accountInfo.name(), " ");
-      case NONE:
-      default:
-        return null;
-    }
-  }
-
-  private static String getAbbreviation(String name, String token) {
-    StringBuilder abbrev = new StringBuilder();
-    if (name != null) {
-      for (String t : name.split(token)) {
-        abbrev.append(t.substring(0, 1).toUpperCase());
-      }
-    }
-    return abbrev.toString();
-  }
-
-  private static Widget getSizeWidget(ChangeInfo c) {
-    int largeChangeSize = Gerrit.info().change().largeChange();
-    int changedLines = c.insertions() + c.deletions();
-    int p = 100;
-    if (changedLines < largeChangeSize) {
-      p = changedLines * 100 / largeChangeSize;
-    }
-
-    int width = Math.max(2, 70 * p / 100);
-    int red = p >= 50 ? 255 : (int) Math.round((p) * 5.12);
-    int green = p <= 50 ? 255 : (int) Math.round(256 - (p - 50) * 5.12);
-    String bg = "#" + toHex(red) + toHex(green) + "00";
-
-    SimplePanel panel = new SimplePanel();
-    panel.setStyleName(Gerrit.RESOURCES.css().changeSize());
-    panel.setWidth(width + "px");
-    panel.getElement().getStyle().setBackgroundColor(bg);
-    return panel;
-  }
-
-  private static String toHex(int i) {
-    String hex = Integer.toHexString(i);
-    return hex.length() == 1 ? "0" + hex : hex;
-  }
-
-  public void addSection(Section s) {
-    assert s.parent == null;
-
-    s.parent = this;
-    s.titleRow = table.getRowCount();
-    if (s.displayTitle()) {
-      final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      fmt.setColSpan(s.titleRow, 0, columns);
-      fmt.addStyleName(s.titleRow, 0, Gerrit.RESOURCES.css().sectionHeader());
-    } else {
-      s.titleRow = -1;
-    }
-
-    s.dataBegin = table.getRowCount();
-    insertNoneRow(s.dataBegin);
-    sections.add(s);
-  }
-
-  private int insertRow(int beforeRow) {
-    for (Section s : sections) {
-      if (beforeRow <= s.titleRow) {
-        s.titleRow++;
-      }
-      if (beforeRow < s.dataBegin) {
-        s.dataBegin++;
-      }
-    }
-    return table.insertRow(beforeRow);
-  }
-
-  private void removeRow(int row) {
-    for (Section s : sections) {
-      if (row < s.titleRow) {
-        s.titleRow--;
-      }
-      if (row < s.dataBegin) {
-        s.dataBegin--;
-      }
-    }
-    table.removeRow(row);
-  }
-
-  public class StarKeyCommand extends NeedsSignInKeyCommand {
-    public StarKeyCommand(int mask, char key, String help) {
-      super(mask, key, help);
-    }
-
-    @Override
-    public void onKeyPress(KeyPressEvent event) {
-      int row = getCurrentRow();
-      ChangeInfo c = getRowItem(row);
-      if (c != null && Gerrit.isSignedIn()) {
-        ((StarredChanges.Icon) table.getWidget(row, C_STAR)).toggleStar();
-      }
-    }
-  }
-
-  private final class TableChangeLink extends ChangeLink {
-    private TableChangeLink(String text, ChangeInfo c) {
-      super(c.projectNameKey(), c.legacyId(), text);
-    }
-
-    @Override
-    public void go() {
-      movePointerTo(cid);
-      super.go();
-    }
-  }
-
-  public static class Section {
-    ChangeTable parent;
-    String titleText;
-    Widget titleWidget;
-    int titleRow = -1;
-    int dataBegin;
-    int rows;
-    private boolean highlightUnreviewed;
-
-    public void setHighlightUnreviewed(boolean value) {
-      this.highlightUnreviewed = value;
-    }
-
-    public void setTitleText(String text) {
-      titleText = text;
-      titleWidget = null;
-      if (titleRow >= 0) {
-        parent.table.setText(titleRow, 0, titleText);
-      }
-    }
-
-    public void setTitleWidget(Widget title) {
-      titleWidget = title;
-      titleText = null;
-      if (titleRow >= 0) {
-        parent.table.setWidget(titleRow, 0, title);
-      }
-    }
-
-    public boolean displayTitle() {
-      if (titleText != null) {
-        setTitleText(titleText);
-        return true;
-      } else if (titleWidget != null) {
-        setTitleWidget(titleWidget);
-        return true;
-      }
-      return false;
-    }
-
-    public void display(ChangeList changeList) {
-      final int sz = changeList != null ? changeList.length() : 0;
-      final boolean hadData = rows > 0;
-
-      if (hadData) {
-        while (sz < rows) {
-          parent.removeRow(dataBegin);
-          rows--;
-        }
-      } else {
-        parent.removeRow(dataBegin);
-      }
-
-      if (sz == 0) {
-        parent.insertNoneRow(dataBegin);
-        return;
-      }
-
-      while (rows < sz) {
-        parent.insertChangeRow(dataBegin + rows);
-        rows++;
-      }
-      for (int i = 0; i < sz; i++) {
-        parent.populateChangeRow(dataBegin + i, changeList.get(i), highlightUnreviewed);
-      }
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentApi.java
deleted file mode 100644
index 987b382..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentApi.java
+++ /dev/null
@@ -1,77 +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.client.changes;
-
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-
-public class CommentApi {
-
-  public static void comments(
-      @Nullable String project, PatchSet.Id id, AsyncCallback<NativeMap<JsArray<CommentInfo>>> cb) {
-    revision(project, id, "comments").get(cb);
-  }
-
-  public static void comment(
-      @Nullable String project, PatchSet.Id id, String commentId, AsyncCallback<CommentInfo> cb) {
-    revision(project, id, "comments").id(commentId).get(cb);
-  }
-
-  public static void drafts(
-      @Nullable String project, PatchSet.Id id, AsyncCallback<NativeMap<JsArray<CommentInfo>>> cb) {
-    revision(project, id, "drafts").get(cb);
-  }
-
-  public static void draft(
-      @Nullable String project, PatchSet.Id id, String draftId, AsyncCallback<CommentInfo> cb) {
-    revision(project, id, "drafts").id(draftId).get(cb);
-  }
-
-  public static void createDraft(
-      @Nullable String project,
-      PatchSet.Id id,
-      CommentInfo content,
-      AsyncCallback<CommentInfo> cb) {
-    revision(project, id, "drafts").put(content, cb);
-  }
-
-  public static void updateDraft(
-      @Nullable String project,
-      PatchSet.Id id,
-      String draftId,
-      CommentInfo content,
-      AsyncCallback<CommentInfo> cb) {
-    revision(project, id, "drafts").id(draftId).put(content, cb);
-  }
-
-  public static void deleteDraft(
-      @Nullable String project,
-      PatchSet.Id id,
-      String draftId,
-      AsyncCallback<JavaScriptObject> cb) {
-    revision(project, id, "drafts").id(draftId).delete(cb);
-  }
-
-  private static RestApi revision(@Nullable String project, PatchSet.Id id, String type) {
-    return ChangeApi.revision(project, id).view(type);
-  }
-
-  private CommentApi() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java
deleted file mode 100644
index a111860..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java
+++ /dev/null
@@ -1,154 +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.client.changes;
-
-import com.google.gerrit.client.diff.CommentRange;
-import com.google.gerrit.client.info.AccountInfo;
-import com.google.gerrit.extensions.client.Side;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwtjsonrpc.client.impl.ser.JavaSqlTimestamp_JsonSerializer;
-import java.sql.Timestamp;
-
-public class CommentInfo extends JavaScriptObject {
-  public static CommentInfo create(
-      String path, Side side, int line, CommentRange range, Boolean unresolved) {
-    return create(path, side, 0, line, range, unresolved);
-  }
-
-  public static CommentInfo create(
-      String path, Side side, int parent, int line, CommentRange range, boolean unresolved) {
-    CommentInfo n = createObject().cast();
-    n.path(path);
-    n.side(side);
-    n.parent(parent);
-    if (range != null) {
-      n.line(range.endLine());
-      n.range(range);
-    } else if (line > 0) {
-      n.line(line);
-    }
-    n.unresolved(unresolved);
-    return n;
-  }
-
-  public static CommentInfo createReply(CommentInfo r) {
-    CommentInfo n = createObject().cast();
-    n.path(r.path());
-    n.side(r.side());
-    n.parent(r.parent());
-    n.inReplyTo(r.id());
-    if (r.hasRange()) {
-      n.line(r.range().endLine());
-      n.range(r.range());
-    } else if (r.hasLine()) {
-      n.line(r.line());
-    }
-    n.unresolved(r.unresolved());
-    return n;
-  }
-
-  public static CommentInfo copy(CommentInfo s) {
-    CommentInfo n = createObject().cast();
-    n.path(s.path());
-    n.side(s.side());
-    n.parent(s.parent());
-    n.id(s.id());
-    n.inReplyTo(s.inReplyTo());
-    n.message(s.message());
-    if (s.hasRange()) {
-      n.line(s.range().endLine());
-      n.range(s.range());
-    } else if (s.hasLine()) {
-      n.line(s.line());
-    }
-    n.unresolved(s.unresolved());
-    return n;
-  }
-
-  public final native void path(String p) /*-{ this.path = p }-*/;
-
-  public final native void id(String i) /*-{ this.id = i }-*/;
-
-  public final native void line(int n) /*-{ this.line = n }-*/;
-
-  public final native void range(CommentRange r) /*-{ this.range = r }-*/;
-
-  public final native void inReplyTo(String i) /*-{ this.in_reply_to = i }-*/;
-
-  public final native void message(String m) /*-{ this.message = m }-*/;
-
-  public final native void unresolved(boolean b) /*-{ this.unresolved = b }-*/;
-
-  public final void side(Side side) {
-    sideRaw(side.toString());
-  }
-
-  private native void sideRaw(String s) /*-{ this.side = s }-*/;
-
-  public final native void parent(int n) /*-{ this.parent = n }-*/;
-
-  public final native boolean hasParent() /*-{ return this.hasOwnProperty('parent') }-*/;
-
-  public final native String path() /*-{ return this.path }-*/;
-
-  public final native String id() /*-{ return this.id }-*/;
-
-  public final native String inReplyTo() /*-{ return this.in_reply_to }-*/;
-
-  public final native int patchSet() /*-{ return this.patch_set }-*/;
-
-  public final native boolean unresolved() /*-{ return this.unresolved }-*/;
-
-  public final Side side() {
-    String s = sideRaw();
-    return s != null ? Side.valueOf(s) : Side.REVISION;
-  }
-
-  private native String sideRaw() /*-{ return this.side }-*/;
-
-  public final native int parent() /*-{ return this.parent }-*/;
-
-  public final Timestamp updated() {
-    Timestamp r = updatedTimestamp();
-    if (r == null) {
-      String s = updatedRaw();
-      if (s != null) {
-        r = JavaSqlTimestamp_JsonSerializer.parseTimestamp(s);
-        updatedTimestamp(r);
-      }
-    }
-    return r;
-  }
-
-  private native String updatedRaw() /*-{ return this.updated }-*/;
-
-  private native Timestamp updatedTimestamp() /*-{ return this._ts }-*/;
-
-  private native void updatedTimestamp(Timestamp t) /*-{ this._ts = t }-*/;
-
-  public final native AccountInfo author() /*-{ return this.author }-*/;
-
-  public final native int line() /*-{ return this.line || 0 }-*/;
-
-  public final native boolean hasLine() /*-{ return this.hasOwnProperty('line') }-*/;
-
-  public final native boolean hasRange() /*-{ return this.hasOwnProperty('range') }-*/;
-
-  public final native CommentRange range() /*-{ return this.range }-*/;
-
-  public final native String message() /*-{ return this.message }-*/;
-
-  protected CommentInfo() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CustomDashboardScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CustomDashboardScreen.java
deleted file mode 100644
index 802e56c..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CustomDashboardScreen.java
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.changes;
-
-import com.google.gerrit.client.ui.Screen;
-
-public class CustomDashboardScreen extends Screen implements ChangeListScreen {
-  private DashboardTable table;
-  private String params;
-
-  public CustomDashboardScreen(String params) {
-    this.params = params;
-  }
-
-  @Override
-  protected void onInitUI() {
-    table =
-        new DashboardTable(this, params) {
-          @Override
-          public void finishDisplay() {
-            super.finishDisplay();
-            display();
-          }
-        };
-
-    super.onInitUI();
-
-    String title = table.getTitle();
-    if (title != null) {
-      setWindowTitle(title);
-      setPageTitle(title);
-    }
-
-    add(table);
-  }
-
-  @Override
-  public void registerKeys() {
-    super.registerKeys();
-    table.setRegisterKeys(true);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DashboardTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DashboardTable.java
deleted file mode 100644
index aba4ee0..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DashboardTable.java
+++ /dev/null
@@ -1,119 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.changes;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.ui.InlineHyperlink;
-import com.google.gerrit.client.ui.Screen;
-import com.google.gerrit.common.PageLinks;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.http.client.URL;
-import com.google.gwtexpui.globalkey.client.KeyCommand;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.ListIterator;
-
-public class DashboardTable extends ChangeTable {
-  private List<Section> sections;
-  private String title;
-  private List<String> titles;
-  private List<String> queries;
-
-  public DashboardTable(Screen screen, String params) {
-    titles = new ArrayList<>();
-    queries = new ArrayList<>();
-    String foreach = null;
-    for (String kvPair : params.split("[,;&]")) {
-      String[] kv = kvPair.split("=", 2);
-      if (kv.length != 2 || kv[0].isEmpty()) {
-        continue;
-      }
-
-      if ("title".equals(kv[0])) {
-        title = URL.decodeQueryString(kv[1]);
-      } else if ("foreach".equals(kv[0])) {
-        foreach = URL.decodeQueryString(kv[1]);
-      } else {
-        titles.add(URL.decodeQueryString(kv[0]));
-        queries.add(URL.decodeQueryString(kv[1]));
-      }
-    }
-
-    if (foreach != null) {
-      ListIterator<String> it = queries.listIterator();
-      while (it.hasNext()) {
-        it.set(it.next() + " " + foreach);
-      }
-    }
-
-    addStyleName(Gerrit.RESOURCES.css().accountDashboard());
-
-    sections = new ArrayList<>();
-    int i = 0;
-    for (String title : titles) {
-      Section s = new Section();
-      String query = removeLimitAndAge(queries.get(i++));
-      s.setTitleWidget(new InlineHyperlink(title, PageLinks.toChangeQuery(query)));
-      addSection(s);
-      sections.add(s);
-    }
-
-    keysNavigation.add(
-        new KeyCommand(0, 'R', Util.C.keyReloadSearch()) {
-          @Override
-          public void onKeyPress(KeyPressEvent event) {
-            Gerrit.display(screen.getToken());
-          }
-        });
-  }
-
-  private String removeLimitAndAge(String query) {
-    StringBuilder unlimitedQuery = new StringBuilder();
-    String[] operators = query.split(" ");
-    for (String o : operators) {
-      if (!o.startsWith("limit:") && !o.startsWith("age:") && !o.startsWith("-age:")) {
-        unlimitedQuery.append(o).append(" ");
-      }
-    }
-    return unlimitedQuery.toString().trim();
-  }
-
-  @Override
-  public String getTitle() {
-    return title;
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    ChangeList.queryMultiple(
-        new GerritCallback<JsArray<ChangeList>>() {
-          @Override
-          public void onSuccess(JsArray<ChangeList> result) {
-            List<ChangeList> cls = Natives.asList(result);
-            updateColumnsForLabels(cls.toArray(new ChangeList[cls.size()]));
-            for (int i = 0; i < cls.size(); i++) {
-              sections.get(i).display(cls.get(i));
-            }
-            finishDisplay();
-          }
-        },
-        OPTIONS,
-        queries.toArray(new String[queries.size()]));
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
deleted file mode 100644
index 1695eb9..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
+++ /dev/null
@@ -1,135 +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.client.changes;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.client.ui.Hyperlink;
-import com.google.gerrit.client.ui.Screen;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.user.client.History;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.HorizontalPanel;
-import com.google.gwtexpui.globalkey.client.KeyCommand;
-
-public abstract class PagedSingleListScreen extends Screen {
-  protected final int pageSize;
-  protected final int start;
-  private final String anchorPrefix;
-
-  protected ChangeList changes;
-  private ChangeTable table;
-  private ChangeTable.Section section;
-  private Hyperlink prev;
-  private Hyperlink next;
-
-  protected PagedSingleListScreen(String anchorToken, int start) {
-    anchorPrefix = anchorToken;
-    this.start = start;
-    pageSize = Gerrit.getUserPreferences().changesPerPage();
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    prev = new Hyperlink(Util.C.pagedChangeListPrev(), true, "");
-    prev.setVisible(false);
-
-    next = new Hyperlink(Util.C.pagedChangeListNext(), true, "");
-    next.setVisible(false);
-
-    table =
-        new ChangeTable() {
-          {
-            keysNavigation.add(
-                new DoLinkCommand(0, 'p', Util.C.changeTablePagePrev(), prev),
-                new DoLinkCommand(0, 'n', Util.C.changeTablePageNext(), next));
-
-            keysNavigation.add(
-                new DoLinkCommand(0, '[', Util.C.changeTablePagePrev(), prev),
-                new DoLinkCommand(0, ']', Util.C.changeTablePageNext(), next));
-
-            keysNavigation.add(
-                new KeyCommand(0, 'R', Util.C.keyReloadSearch()) {
-                  @Override
-                  public void onKeyPress(KeyPressEvent event) {
-                    Gerrit.display(getToken());
-                  }
-                });
-          }
-        };
-    section = new ChangeTable.Section();
-    table.addSection(section);
-    table.setSavePointerId(anchorPrefix);
-    add(table);
-
-    final HorizontalPanel buttons = new HorizontalPanel();
-    buttons.setStyleName(Gerrit.RESOURCES.css().changeTablePrevNextLinks());
-    buttons.add(prev);
-    buttons.add(next);
-    add(buttons);
-  }
-
-  @Override
-  public void registerKeys() {
-    super.registerKeys();
-    table.setRegisterKeys(true);
-  }
-
-  protected AsyncCallback<ChangeList> loadCallback() {
-    return new ScreenLoadCallback<ChangeList>(this) {
-      @Override
-      protected void preDisplay(ChangeList result) {
-        display(result);
-      }
-    };
-  }
-
-  protected void display(ChangeList result) {
-    changes = result;
-    if (changes.length() != 0) {
-      if (start > 0) {
-        int p = start - pageSize;
-        prev.setTargetHistoryToken(anchorPrefix + (p > 0 ? "," + p : ""));
-        prev.setVisible(true);
-      } else {
-        prev.setVisible(false);
-      }
-
-      int n = start + changes.length();
-      next.setTargetHistoryToken(anchorPrefix + "," + n);
-      next.setVisible(changes.get(changes.length() - 1)._more_changes());
-    }
-    table.updateColumnsForLabels(result);
-    section.display(result);
-    table.finishDisplay();
-  }
-
-  private static final class DoLinkCommand extends KeyCommand {
-    private final Hyperlink link;
-
-    private DoLinkCommand(int mask, char key, String help, Hyperlink l) {
-      super(mask, key, help);
-      link = l;
-    }
-
-    @Override
-    public void onKeyPress(KeyPressEvent event) {
-      if (link.isVisible()) {
-        History.newItem(link.getTargetHistoryToken());
-      }
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ProjectDashboardScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ProjectDashboardScreen.java
deleted file mode 100644
index f511308..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ProjectDashboardScreen.java
+++ /dev/null
@@ -1,62 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.changes;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.admin.ProjectScreen;
-import com.google.gerrit.client.ui.InlineHyperlink;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.user.client.ui.FlowPanel;
-
-public class ProjectDashboardScreen extends ProjectScreen implements ChangeListScreen {
-  private DashboardTable table;
-  private String params;
-
-  public ProjectDashboardScreen(Project.NameKey toShow, String params) {
-    super(toShow);
-    this.params = params;
-  }
-
-  @Override
-  protected void onInitUI() {
-    table =
-        new DashboardTable(this, params) {
-          @Override
-          public void finishDisplay() {
-            super.finishDisplay();
-            display();
-          }
-        };
-
-    super.onInitUI();
-
-    String title = table.getTitle();
-    if (title != null) {
-      FlowPanel fp = new FlowPanel();
-      fp.setStyleName(Gerrit.RESOURCES.css().screenHeader());
-      fp.add(new InlineHyperlink(title, PageLinks.toCustomDashboard(params)));
-      add(fp);
-    }
-
-    add(table);
-  }
-
-  @Override
-  public void registerKeys() {
-    super.registerKeys();
-    table.setRegisterKeys(true);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
deleted file mode 100644
index 8d580a3..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
+++ /dev/null
@@ -1,96 +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.client.changes;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gwt.regexp.shared.RegExp;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwtorm.client.KeyUtil;
-
-public class QueryScreen extends PagedSingleListScreen implements ChangeListScreen {
-  // Legacy numeric identifier.
-  private static final RegExp NUMERIC_ID = RegExp.compile("^[1-9][0-9]*$");
-  // Commit SHA1 hash
-  private static final RegExp COMMIT_SHA1 = RegExp.compile("^([0-9a-fA-F]{4," + RevId.LEN + "})$");
-  // Change-Id
-  private static final String ID_PATTERN = "[iI][0-9a-f]{4,}$";
-  private static final RegExp CHANGE_ID = RegExp.compile("^" + ID_PATTERN);
-  private static final RegExp CHANGE_ID_TRIPLET = RegExp.compile("^(.)+~(.)+~" + ID_PATTERN);
-
-  public static QueryScreen forQuery(String query) {
-    return forQuery(query, 0);
-  }
-
-  public static QueryScreen forQuery(String query, int start) {
-    return new QueryScreen(KeyUtil.encode(query), start);
-  }
-
-  private final String query;
-
-  public QueryScreen(String encQuery, int start) {
-    super(PageLinks.QUERY + encQuery, start);
-    query = KeyUtil.decode(encQuery);
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    setWindowTitle(Util.M.changeQueryWindowTitle(query));
-    setPageTitle(Util.M.changeQueryPageTitle(query));
-  }
-
-  @Override
-  protected AsyncCallback<ChangeList> loadCallback() {
-    return new GerritCallback<ChangeList>() {
-      @Override
-      public void onSuccess(ChangeList result) {
-        if (isAttached()) {
-          if (result.length() == 1 && isSingleQuery(query)) {
-            ChangeInfo c = result.get(0);
-            Change.Id id = c.legacyId();
-            Gerrit.display(PageLinks.toChange(c.projectNameKey(), id));
-          } else {
-            display(result);
-            QueryScreen.this.display();
-          }
-        }
-      }
-    };
-  }
-
-  @Override
-  public void onShowView() {
-    super.onShowView();
-    Gerrit.setQueryString(query);
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    ChangeList.query(query, ChangeTable.OPTIONS, loadCallback(), start, pageSize);
-  }
-
-  private static boolean isSingleQuery(String query) {
-    return NUMERIC_ID.test(query)
-        || CHANGE_ID.test(query)
-        || CHANGE_ID_TRIPLET.test(query)
-        || COMMIT_SHA1.test(query);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInfo.java
deleted file mode 100644
index 06d2484..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInfo.java
+++ /dev/null
@@ -1,25 +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.client.changes;
-
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gwt.core.client.JavaScriptObject;
-
-public class ReviewInfo extends JavaScriptObject {
-
-  public final native NativeMap<?> labels() /*-{ return this.labels }-*/;
-
-  protected ReviewInfo() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInput.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInput.java
deleted file mode 100644
index f851d5e..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInput.java
+++ /dev/null
@@ -1,88 +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.client.changes;
-
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
-
-public class ReviewInput extends JavaScriptObject {
-  public enum NotifyHandling {
-    NONE,
-    OWNER,
-    OWNER_REVIEWERS,
-    ALL
-  }
-
-  public enum DraftHandling {
-    DELETE,
-    PUBLISH,
-    KEEP,
-    PUBLISH_ALL_REVISIONS
-  }
-
-  public static ReviewInput create() {
-    ReviewInput r = createObject().cast();
-    r.init();
-    r.drafts(DraftHandling.PUBLISH);
-    return r;
-  }
-
-  public final native void message(String m) /*-{ if(m)this.message=m; }-*/;
-
-  public final native void label(String n, short v) /*-{ this.labels[n]=v; }-*/;
-
-  public final native void comments(NativeMap<JsArray<CommentInfo>> m) /*-{ this.comments=m }-*/;
-
-  public final void notify(NotifyHandling e) {
-    _notify(e.name());
-  }
-
-  private native void _notify(String n) /*-{ this.notify=n; }-*/;
-
-  public final void drafts(DraftHandling e) {
-    _drafts(e.name());
-  }
-
-  private native void _drafts(String n) /*-{ this.drafts=n; }-*/;
-
-  private native void init() /*-{
-    this.labels = {};
-  }-*/;
-
-  public final native void prePost() /*-{
-    var m=this.comments;
-    if (m) {
-      for (var p in m) {
-        var l=m[p];
-        for (var i=0;i<l.length;i++) {
-          var c=l[i];
-          delete c['path'];
-          delete c['updated'];
-        }
-      }
-    }
-  }-*/;
-
-  public final native void mergeLabels(ReviewInput o) /*-{
-    var l=o.labels;
-    if (l) {
-      for (var n in l)
-        this.labels[n]=l[n];
-    }
-  }-*/;
-
-  protected ReviewInput() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/RevisionInfoCache.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/RevisionInfoCache.java
deleted file mode 100644
index fde2b05..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/RevisionInfoCache.java
+++ /dev/null
@@ -1,49 +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.client.changes;
-
-import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-/** Cache of PatchSet.Id to revision SHA-1 strings. */
-public class RevisionInfoCache {
-  private static final int LIMIT = 10;
-  private static final RevisionInfoCache IMPL = new RevisionInfoCache();
-
-  public static void add(Change.Id change, RevisionInfo info) {
-    IMPL.psToCommit.put(new PatchSet.Id(change, info._number()), info.name());
-  }
-
-  static String get(PatchSet.Id id) {
-    return IMPL.psToCommit.get(id);
-  }
-
-  private final LinkedHashMap<PatchSet.Id, String> psToCommit;
-
-  private RevisionInfoCache() {
-    psToCommit =
-        new LinkedHashMap<PatchSet.Id, String>(LIMIT) {
-          private static final long serialVersionUID = 1L;
-
-          @Override
-          protected boolean removeEldestEntry(Map.Entry<PatchSet.Id, String> e) {
-            return size() > LIMIT;
-          }
-        };
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java
deleted file mode 100644
index b1028420..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java
+++ /dev/null
@@ -1,201 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.changes;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.account.AccountApi;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.resources.client.ImageResource;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwtexpui.globalkey.client.KeyCommand;
-import com.google.web.bindery.event.shared.Event;
-import com.google.web.bindery.event.shared.HandlerRegistration;
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-/** Supports the star icon displayed on changes and tracking the status. */
-public class StarredChanges {
-  private static final Event.Type<ChangeStarHandler> TYPE = new Event.Type<>();
-
-  /** Handler that can receive notifications of a change's starred status. */
-  public interface ChangeStarHandler {
-    void onChangeStar(ChangeStarEvent event);
-  }
-
-  /** Event fired when a star changes status. The new status is reported. */
-  public static class ChangeStarEvent extends Event<ChangeStarHandler> {
-    private boolean starred;
-
-    public ChangeStarEvent(Change.Id source, boolean starred) {
-      setSource(source);
-      this.starred = starred;
-    }
-
-    public boolean isStarred() {
-      return starred;
-    }
-
-    @Override
-    public Type<ChangeStarHandler> getAssociatedType() {
-      return TYPE;
-    }
-
-    @Override
-    protected void dispatch(ChangeStarHandler handler) {
-      handler.onChangeStar(this);
-    }
-  }
-
-  /**
-   * Create a star icon for the given change, and current status. Returns null if the user is not
-   * signed in and cannot support starred changes.
-   */
-  public static Icon createIcon(Change.Id source, boolean starred) {
-    return Gerrit.isSignedIn() ? new Icon(source, starred) : null;
-  }
-
-  /** Make a key command that toggles the star for a change. */
-  public static KeyCommand newKeyCommand(Icon icon) {
-    return new KeyCommand(0, 's', Util.C.changeTableStar()) {
-      @Override
-      public void onKeyPress(KeyPressEvent event) {
-        icon.toggleStar();
-      }
-    };
-  }
-
-  /** Add a handler to listen for starred status to change. */
-  public static HandlerRegistration addHandler(Change.Id source, ChangeStarHandler handler) {
-    return Gerrit.EVENT_BUS.addHandlerToSource(TYPE, source, handler);
-  }
-
-  /**
-   * Broadcast the current starred value of a change to UI widgets. This does not RPC to the server
-   * and does not alter the starred status of a change.
-   */
-  public static void fireChangeStarEvent(Change.Id id, boolean starred) {
-    Gerrit.EVENT_BUS.fireEventFromSource(new ChangeStarEvent(id, starred), id);
-  }
-
-  /**
-   * Set the starred status of a change. This method broadcasts to all interested UI widgets and
-   * sends an RPC to the server to record the updated status.
-   */
-  public static void toggleStar(Change.Id changeId, boolean newValue) {
-    pending.put(changeId, newValue);
-    fireChangeStarEvent(changeId, newValue);
-    if (!busy) {
-      startRequest();
-    }
-  }
-
-  private static boolean busy;
-  private static final Map<Change.Id, Boolean> pending = new LinkedHashMap<>(4);
-
-  private static void startRequest() {
-    busy = true;
-
-    final Change.Id id = pending.keySet().iterator().next();
-    final boolean starred = pending.remove(id);
-    RestApi call = AccountApi.self().view("starred.changes").id(id.get());
-    AsyncCallback<JavaScriptObject> cb =
-        new AsyncCallback<JavaScriptObject>() {
-          @Override
-          public void onSuccess(JavaScriptObject none) {
-            if (pending.isEmpty()) {
-              busy = false;
-            } else {
-              startRequest();
-            }
-          }
-
-          @Override
-          public void onFailure(Throwable caught) {
-            if (!starred && RestApi.isStatus(caught, 404)) {
-              onSuccess(null);
-              return;
-            }
-
-            fireChangeStarEvent(id, !starred);
-            for (Map.Entry<Change.Id, Boolean> e : pending.entrySet()) {
-              fireChangeStarEvent(e.getKey(), !e.getValue());
-            }
-            pending.clear();
-            busy = false;
-          }
-        };
-    if (starred) {
-      call.put(cb);
-    } else {
-      call.delete(cb);
-    }
-  }
-
-  public static class Icon extends Image implements ChangeStarHandler, ClickHandler {
-    private final Change.Id changeId;
-    private boolean starred;
-    private HandlerRegistration handler;
-
-    Icon(Change.Id changeId, boolean starred) {
-      super(resource(starred));
-      this.changeId = changeId;
-      this.starred = starred;
-      addClickHandler(this);
-    }
-
-    /**
-     * Toggles the state of the star, as if the user clicked on the image. This will broadcast the
-     * new star status to all interested UI widgets, and RPC to the server to store the changed
-     * value.
-     */
-    public void toggleStar() {
-      StarredChanges.toggleStar(changeId, !starred);
-    }
-
-    @Override
-    protected void onLoad() {
-      handler = StarredChanges.addHandler(changeId, this);
-    }
-
-    @Override
-    protected void onUnload() {
-      handler.removeHandler();
-      handler = null;
-    }
-
-    @Override
-    public void onChangeStar(ChangeStarEvent event) {
-      setResource(resource(event.isStarred()));
-      starred = event.isStarred();
-    }
-
-    @Override
-    public void onClick(ClickEvent event) {
-      toggleStar();
-    }
-
-    private static ImageResource resource(boolean starred) {
-      return starred ? Gerrit.RESOURCES.starFilled() : Gerrit.RESOURCES.starOpen();
-    }
-  }
-
-  private StarredChanges() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitInfo.java
deleted file mode 100644
index 9027c5b..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitInfo.java
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.changes;
-
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gwt.core.client.JavaScriptObject;
-
-public class SubmitInfo extends JavaScriptObject {
-  final Change.Status status() {
-    return Change.Status.valueOf(statusRaw());
-  }
-
-  private native String statusRaw() /*-{ return this.status; }-*/;
-
-  protected SubmitInfo() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java
deleted file mode 100644
index 8d949d1..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java
+++ /dev/null
@@ -1,79 +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.client.changes;
-
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gwt.core.client.GWT;
-
-public class Util {
-  public static final ChangeConstants C = GWT.create(ChangeConstants.class);
-  public static final ChangeMessages M = GWT.create(ChangeMessages.class);
-
-  private static final int SUBJECT_MAX_LENGTH = 80;
-  private static final String SUBJECT_CROP_APPENDIX = "...";
-  private static final int SUBJECT_CROP_RANGE = 10;
-
-  public static String toLongString(Change.Status status) {
-    if (status == null) {
-      return "";
-    }
-    switch (status) {
-      case NEW:
-        return C.statusLongNew();
-      case MERGED:
-        return C.statusLongMerged();
-      case ABANDONED:
-        return C.statusLongAbandoned();
-      default:
-        return status.name();
-    }
-  }
-
-  /**
-   * Crops the given change subject if needed so that it has at most {@link #SUBJECT_MAX_LENGTH}
-   * characters.
-   *
-   * <p>If the given subject is not longer than {@link #SUBJECT_MAX_LENGTH} characters it is
-   * returned unchanged.
-   *
-   * <p>If the length of the given subject exceeds {@link #SUBJECT_MAX_LENGTH} characters it is
-   * cropped. In this case {@link #SUBJECT_CROP_APPENDIX} is appended to the cropped subject, the
-   * cropped subject including the appendix has at most {@link #SUBJECT_MAX_LENGTH} characters.
-   *
-   * <p>If cropping is needed, the subject will be cropped after the last space character that is
-   * found within the last {@link #SUBJECT_CROP_RANGE} characters of the potentially visible
-   * characters. If no such space is found, the subject will be cropped so that the cropped subject
-   * including the appendix has exactly {@link #SUBJECT_MAX_LENGTH} characters.
-   *
-   * @return the subject, cropped if needed
-   */
-  @SuppressWarnings("deprecation")
-  public static String cropSubject(String subject) {
-    if (subject.length() > SUBJECT_MAX_LENGTH) {
-      final int maxLength = SUBJECT_MAX_LENGTH - SUBJECT_CROP_APPENDIX.length();
-      for (int cropPosition = maxLength;
-          cropPosition > maxLength - SUBJECT_CROP_RANGE;
-          cropPosition--) {
-        // Character.isWhitespace(char) can't be used because this method is not supported by GWT,
-        // see https://developers.google.com/web-toolkit/doc/1.6/RefJreEmulation#Package_java_lang
-        if (Character.isSpace(subject.charAt(cropPosition - 1))) {
-          return subject.substring(0, cropPosition) + SUBJECT_CROP_APPENDIX;
-        }
-      }
-      return subject.substring(0, maxLength) + SUBJECT_CROP_APPENDIX;
-    }
-    return subject;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/CapabilityInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/CapabilityInfo.java
deleted file mode 100644
index 1d7f4ab..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/CapabilityInfo.java
+++ /dev/null
@@ -1,25 +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.client.config;
-
-import com.google.gwt.core.client.JavaScriptObject;
-
-public class CapabilityInfo extends JavaScriptObject {
-  public final native String id() /*-{ return this.id; }-*/;
-
-  public final native String name() /*-{ return this.name; }-*/;
-
-  protected CapabilityInfo() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ConfigServerApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ConfigServerApi.java
deleted file mode 100644
index e71929c..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ConfigServerApi.java
+++ /dev/null
@@ -1,60 +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.client.config;
-
-import com.google.gerrit.client.VoidResult;
-import com.google.gerrit.client.info.GeneralPreferences;
-import com.google.gerrit.client.info.ServerInfo;
-import com.google.gerrit.client.info.TopMenuList;
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-
-/** A collection of static methods which work on the Gerrit REST API for server configuration. */
-public class ConfigServerApi {
-  /** map of the server wide capabilities (core & plugins). */
-  public static void capabilities(AsyncCallback<NativeMap<CapabilityInfo>> cb) {
-    new RestApi("/config/server/capabilities/").get(cb);
-  }
-
-  public static void topMenus(AsyncCallback<TopMenuList> cb) {
-    new RestApi("/config/server/top-menus").get(cb);
-  }
-
-  public static void defaultPreferences(AsyncCallback<GeneralPreferences> cb) {
-    new RestApi("/config/server/preferences").get(cb);
-  }
-
-  public static void serverInfo(AsyncCallback<ServerInfo> cb) {
-    new RestApi("/config/server/info").get(cb);
-  }
-
-  public static void confirmEmail(String token, AsyncCallback<VoidResult> cb) {
-    EmailConfirmationInput input = EmailConfirmationInput.create();
-    input.setToken(token);
-    new RestApi("/config/server/email.confirm").put(input, cb);
-  }
-
-  private static class EmailConfirmationInput extends JavaScriptObject {
-    final native void setToken(String token) /*-{ this.token = token; }-*/;
-
-    static EmailConfirmationInput create() {
-      return createObject().cast();
-    }
-
-    protected EmailConfirmationInput() {}
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardConstants.java
deleted file mode 100644
index ecb2938..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardConstants.java
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.dashboards;
-
-import com.google.gwt.i18n.client.Constants;
-
-public interface DashboardConstants extends Constants {
-  String dashboardName();
-
-  String dashboardTitle();
-
-  String dashboardDescription();
-
-  String dashboardInherited();
-
-  String dashboardItem();
-
-  String dashboardDefaultToolTip();
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardConstants.properties
deleted file mode 100644
index ac4de7c..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardConstants.properties
+++ /dev/null
@@ -1,6 +0,0 @@
-dashboardName = Dashboard Name
-dashboardTitle = Dashboard Title
-dashboardDescription = Dashboard Description
-dashboardInherited = Inherited From
-dashboardItem = dashboard
-dashboardDefaultToolTip = Project Default Dashboard
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardInfo.java
deleted file mode 100644
index 5c6b51a..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardInfo.java
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.dashboards;
-
-import com.google.gwt.core.client.JavaScriptObject;
-
-public class DashboardInfo extends JavaScriptObject {
-  public final native String id() /*-{ return this.id; }-*/;
-
-  public final native String title() /*-{ return this.title; }-*/;
-
-  public final native String project() /*-{ return this.project; }-*/;
-
-  public final native String definingProject() /*-{ return this.defining_project; }-*/;
-
-  public final native String ref() /*-{ return this.ref; }-*/;
-
-  public final native String path() /*-{ return this.path; }-*/;
-
-  public final native String description() /*-{ return this.description; }-*/;
-
-  public final native String foreach() /*-{ return this.foreach; }-*/;
-
-  public final native String url() /*-{ return this.url; }-*/;
-
-  private final native boolean isDefaultLegacy() /*-{ return this['default'] ? true : false; }-*/;
-
-  private final native boolean isDefaultNew() /*-{ return this.is_default ? true : false; }-*/;
-
-  public final boolean isDefault() {
-    return isDefaultLegacy() || isDefaultNew();
-  }
-
-  protected DashboardInfo() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardList.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardList.java
deleted file mode 100644
index 7ba3580..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardList.java
+++ /dev/null
@@ -1,53 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.dashboards;
-
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.http.client.URL;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-
-/** Project dashboards from {@code /projects/<name>/dashboards/}. */
-public class DashboardList extends JsArray<DashboardInfo> {
-  public static void all(Project.NameKey project, AsyncCallback<JsArray<DashboardList>> callback) {
-    base(project).addParameterTrue("inherited").get(callback);
-  }
-
-  public static void getDefault(Project.NameKey project, AsyncCallback<DashboardInfo> callback) {
-    base(project).view("default").addParameterTrue("inherited").get(callback);
-  }
-
-  public static void get(
-      Project.NameKey project, String id, AsyncCallback<DashboardInfo> callback) {
-    base(project).idRaw(encodeDashboardId(id)).get(callback);
-  }
-
-  private static RestApi base(Project.NameKey project) {
-    return new RestApi("/projects/").id(project.get()).view("dashboards");
-  }
-
-  private static String encodeDashboardId(String id) {
-    int c = id.indexOf(':');
-    if (0 <= c) {
-      String ref = URL.encodeQueryString(id.substring(0, c));
-      String path = URL.encodeQueryString(id.substring(c + 1));
-      return ref + ':' + path;
-    }
-    return URL.encodeQueryString(id);
-  }
-
-  protected DashboardList() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardsTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardsTable.java
deleted file mode 100644
index dcb9c01..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardsTable.java
+++ /dev/null
@@ -1,154 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.dashboards;
-
-import static java.util.Comparator.comparing;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.ui.NavigationTable;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.user.client.History;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
-import com.google.gwt.user.client.ui.Image;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-public class DashboardsTable extends NavigationTable<DashboardInfo> {
-  Project.NameKey project;
-
-  public DashboardsTable(Project.NameKey project) {
-    super(Util.C.dashboardItem());
-    this.project = project;
-    initColumnHeaders();
-  }
-
-  protected void initColumnHeaders() {
-    final FlexCellFormatter fmt = table.getFlexCellFormatter();
-    fmt.setColSpan(0, 0, 2);
-    fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().dataHeader());
-    fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
-    fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader());
-    fmt.addStyleName(0, 4, Gerrit.RESOURCES.css().dataHeader());
-
-    table.setText(0, 1, Util.C.dashboardName());
-    table.setText(0, 2, Util.C.dashboardTitle());
-    table.setText(0, 3, Util.C.dashboardDescription());
-    table.setText(0, 4, Util.C.dashboardInherited());
-  }
-
-  public void display(DashboardList dashes) {
-    display(Natives.asList(dashes));
-  }
-
-  public void display(JsArray<DashboardList> in) {
-    Map<String, DashboardInfo> map = new HashMap<>();
-    for (DashboardList list : Natives.asList(in)) {
-      for (DashboardInfo d : Natives.asList(list)) {
-        if (!map.containsKey(d.id())) {
-          map.put(d.id(), d);
-        }
-      }
-    }
-    display(new ArrayList<>(map.values()));
-  }
-
-  public void display(List<DashboardInfo> list) {
-    while (1 < table.getRowCount()) {
-      table.removeRow(table.getRowCount() - 1);
-    }
-
-    list.sort(comparing(DashboardInfo::id));
-
-    String ref = null;
-    for (DashboardInfo d : list) {
-      if (!d.ref().equals(ref)) {
-        ref = d.ref();
-        insertTitleRow(table.getRowCount(), ref);
-      }
-      insert(table.getRowCount(), d);
-    }
-
-    finishDisplay();
-  }
-
-  protected void insertTitleRow(int row, String section) {
-    table.insertRow(row);
-
-    table.setText(row, 0, section);
-
-    final FlexCellFormatter fmt = table.getFlexCellFormatter();
-    fmt.setColSpan(row, 0, 6);
-    fmt.addStyleName(row, 0, Gerrit.RESOURCES.css().sectionHeader());
-  }
-
-  protected void insert(int row, DashboardInfo k) {
-    table.insertRow(row);
-
-    applyDataRowStyle(row);
-
-    final FlexCellFormatter fmt = table.getFlexCellFormatter();
-    fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().dataCell());
-    fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell());
-    fmt.addStyleName(row, 3, Gerrit.RESOURCES.css().dataCell());
-    fmt.addStyleName(row, 4, Gerrit.RESOURCES.css().dataCell());
-    fmt.addStyleName(row, 5, Gerrit.RESOURCES.css().dataCell());
-
-    populate(row, k);
-  }
-
-  protected void populate(int row, DashboardInfo k) {
-    if (k.isDefault()) {
-      table.setWidget(row, 1, new Image(Gerrit.RESOURCES.greenCheck()));
-      final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      fmt.getElement(row, 1).setTitle(Util.C.dashboardDefaultToolTip());
-    }
-    table.setWidget(
-        row,
-        2,
-        new Anchor(
-            k.path(),
-            "#" + PageLinks.toProjectDashboard(new Project.NameKey(k.project()), k.id())));
-    table.setText(row, 3, k.title() != null ? k.title() : k.path());
-    table.setText(row, 4, k.description());
-    if (k.definingProject() != null && !k.definingProject().equals(k.project())) {
-      table.setWidget(
-          row,
-          5,
-          new Anchor(
-              k.definingProject(),
-              "#" + PageLinks.toProjectDashboards(new Project.NameKey(k.definingProject()))));
-    }
-    setRowItem(row, k);
-  }
-
-  @Override
-  protected Object getRowItemKey(DashboardInfo item) {
-    return item.id();
-  }
-
-  @Override
-  protected void onOpenRow(int row) {
-    if (row > 0) {
-      movePointerTo(row);
-    }
-    History.newItem(getRowItem(row).url());
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/Util.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/Util.java
deleted file mode 100644
index b15bf73..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/Util.java
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.dashboards;
-
-import com.google.gwt.core.client.GWT;
-
-public class Util {
-  public static final DashboardConstants C = GWT.create(DashboardConstants.class);
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java
deleted file mode 100644
index 0091f53..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java
+++ /dev/null
@@ -1,137 +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.client.diff;
-
-import static com.google.gerrit.client.diff.DisplaySide.A;
-
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.dom.client.Element;
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.List;
-import net.codemirror.lib.CodeMirror;
-import net.codemirror.lib.CodeMirror.LineClassWhere;
-import net.codemirror.lib.Pos;
-import net.codemirror.lib.TextMarker;
-
-/** Colors modified regions for {@link SideBySide} and {@link Unified}. */
-abstract class ChunkManager {
-  static final native void onClick(Element e, JavaScriptObject f) /*-{ e.onclick = f }-*/;
-
-  final Scrollbar scrollbar;
-  final LineMapper lineMapper;
-
-  private List<TextMarker> markers;
-  private List<Runnable> undo;
-
-  ChunkManager(Scrollbar scrollbar) {
-    this.scrollbar = scrollbar;
-    this.lineMapper = new LineMapper();
-  }
-
-  abstract DiffChunkInfo getFirst();
-
-  List<TextMarker> getMarkers() {
-    return markers;
-  }
-
-  void reset() {
-    lineMapper.reset();
-    for (TextMarker m : markers) {
-      m.clear();
-    }
-    for (Runnable r : undo) {
-      r.run();
-    }
-  }
-
-  abstract void render(DiffInfo diff);
-
-  void render() {
-    markers = new ArrayList<>();
-    undo = new ArrayList<>();
-  }
-
-  void colorLines(CodeMirror cm, String color, int line, int cnt) {
-    colorLines(cm, LineClassWhere.WRAP, color, line, line + cnt);
-  }
-
-  void colorLines(CodeMirror cm, LineClassWhere where, String className, int start, int end) {
-    if (start < end) {
-      for (int line = start; line < end; line++) {
-        cm.addLineClass(line, where, className);
-      }
-      undo.add(
-          () -> {
-            for (int line = start; line < end; line++) {
-              cm.removeLineClass(line, where, className);
-            }
-          });
-    }
-  }
-
-  abstract Runnable diffChunkNav(CodeMirror cm, Direction dir);
-
-  void diffChunkNavHelper(
-      List<? extends DiffChunkInfo> chunks, DiffScreen host, int res, Direction dir) {
-    if (res < 0) {
-      res = -res - (dir == Direction.PREV ? 1 : 2);
-    }
-    res = res + (dir == Direction.PREV ? -1 : 1);
-    if (res < 0 || chunks.size() <= res) {
-      return;
-    }
-
-    DiffChunkInfo lookUp = chunks.get(res);
-    // If edit, skip the deletion chunk and set focus on the insertion one.
-    if (lookUp.isEdit() && lookUp.getSide() == A) {
-      res = res + (dir == Direction.PREV ? -1 : 1);
-      if (res < 0 || chunks.size() <= res) {
-        return;
-      }
-    }
-
-    DiffChunkInfo target = chunks.get(res);
-    CodeMirror targetCm = host.getCmFromSide(target.getSide());
-    int cmLine = getCmLine(target.getStart(), target.getSide());
-    targetCm.setCursor(Pos.create(cmLine));
-    targetCm.focus();
-    targetCm.scrollToY(
-        targetCm.heightAtLine(cmLine, "local") - 0.5 * targetCm.scrollbarV().getClientHeight());
-  }
-
-  Comparator<DiffChunkInfo> getDiffChunkComparator() {
-    // Chunks are ordered by their starting line. If it's a deletion,
-    // use its corresponding line on the revision side for comparison.
-    // In the edit case, put the deletion chunk right before the
-    // insertion chunk. This placement guarantees well-ordering.
-    return new Comparator<DiffChunkInfo>() {
-      @Override
-      public int compare(DiffChunkInfo a, DiffChunkInfo b) {
-        if (a.getSide() == b.getSide()) {
-          return a.getStart() - b.getStart();
-        } else if (a.getSide() == A) {
-          int comp = lineMapper.lineOnOther(a.getSide(), a.getStart()).getLine() - b.getStart();
-          return comp == 0 ? -1 : comp;
-        } else {
-          int comp = a.getStart() - lineMapper.lineOnOther(b.getSide(), b.getStart()).getLine();
-          return comp == 0 ? 1 : comp;
-        }
-      }
-    };
-  }
-
-  abstract int getCmLine(int line, DisplaySide side);
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.css
deleted file mode 100644
index b4216eb..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.css
+++ /dev/null
@@ -1,147 +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.
- */
-
-@external .cm-s-midnight;
-@external .cm-s-night;
-@external .cm-s-twilight;
-@external .com-google-gerrit-client-diff-PublishedBox_BinderImpl_GenCss_style-name;
-@external .com-google-gerrit-client-diff-CommentBox-Style-message;
-@external .com-google-gerrit-client-diff-CommentBox-Style-date;
-@external .com-google-gerrit-client-diff-DiffTable_BinderImpl_GenCss_style-range;
-@external .com-google-gerrit-client-diff-DiffTable_BinderImpl_GenCss_style-rangeHighlight;
-@external .net-codemirror-lib-CodeMirror-Style-activeLine;
-@external .CodeMirror-linenumber;
-
-.cm-s-midnight .com-google-gerrit-client-diff-PublishedBox_BinderImpl_GenCss_style-name { color: black }
-.cm-s-midnight .com-google-gerrit-client-diff-CommentBox-Style-message { color: black }
-.cm-s-midnight .com-google-gerrit-client-diff-CommentBox-Style-date { color: black }
-.cm-s-midnight .com-google-gerrit-client-diff-DiffTable_BinderImpl_GenCss_style-range { color: #777 }
-.cm-s-midnight .com-google-gerrit-client-diff-DiffTable_BinderImpl_GenCss_style-rangeHighlight { color: #777 }
-.cm-s-midnight .net-codemirror-lib-CodeMirror-Style-activeLine .CodeMirror-linenumber { color: black }
-
-.cm-s-night .com-google-gerrit-client-diff-PublishedBox_BinderImpl_GenCss_style-name { color: black }
-.cm-s-night .com-google-gerrit-client-diff-CommentBox-Style-message { color: black }
-.cm-s-night .com-google-gerrit-client-diff-CommentBox-Style-date { color: black }
-.cm-s-night .com-google-gerrit-client-diff-DiffTable_BinderImpl_GenCss_style-range { color: #777 }
-.cm-s-night .com-google-gerrit-client-diff-DiffTable_BinderImpl_GenCss_style-rangeHighlight { color: #777 }
-.cm-s-night .net-codemirror-lib-CodeMirror-Style-activeLine .CodeMirror-linenumber { color: black }
-
-.cm-s-twilight .com-google-gerrit-client-diff-PublishedBox_BinderImpl_GenCss_style-name { color: black }
-.cm-s-twilight .com-google-gerrit-client-diff-CommentBox-Style-message { color: black }
-.cm-s-twilight .com-google-gerrit-client-diff-CommentBox-Style-date { color: black }
-.cm-s-twilight .com-google-gerrit-client-diff-DiffTable_BinderImpl_GenCss_style-range { color: #777 }
-.cm-s-twilight .com-google-gerrit-client-diff-DiffTable_BinderImpl_GenCss_style-rangeHighlight { color: #777 }
-.cm-s-twilight .net-codemirror-lib-CodeMirror-Style-activeLine .CodeMirror-linenumber { color: black }
-
-.commentWidgets {
-  max-width: 650px;
-
-  font-family: sans-serif;
-  background-color: #fcfa96;
-  border: 1px solid black;
-  -webkit-box-shadow: 3px 3px 3px #888888;
-  -moz-box-shadow: 3px 3px 3px #888888;
-  box-shadow: 3px 3px 3px #888888;
-
-  /* margin-bottom is fixed in CommentGroup.computeHeight() */
-  margin-bottom: 5px;
-  margin-right: 5px;
-
-  -webkit-touch-callout: initial;
-  -webkit-user-select: text;
-  -khtml-user-select: text;
-  -moz-user-select: text;
-  -ms-user-select: text;
-  user-select: text;
-}
-
-.commentBox {
-  position: relative;
-  min-height: 16px;
-}
-
-.header {
-  cursor: pointer;
-}
-
-.summary {
-  color: #777;
-  position: absolute;
-  top: 1px;
-  left: 120px;
-  width: 408px;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-  padding-bottom: 0.1em;
-}
-
-.date {
-  white-space: nowrap;
-  position: absolute;
-  top: 2px;
-  right: 5px;
-}
-
-.contents {
-  margin-left: 28px;
-  padding-top: 2px;
-  position: relative;
-}
-.message {
-  overflow-x: auto;
-}
-.message p,
-.message ul,
-.message blockquote {
-  -webkit-margin-before: 0.2em;
-  -webkit-margin-after: 0.3em;
-}
-.message {
-  white-space: pre-wrap;
-}
-.commentBox button {
-  margin-right: 3px;
-  margin-bottom: 1px;
-  padding: 1px;
-  text-align: center;
-  font-size: 8px;
-  font-weight: bold;
-  border: 1px solid black;
-  cursor: pointer;
-  color: #fff;
-  background-color: #4d90fe;
-  background-image: -webkit-linear-gradient(top, #4d90fe, #4d90fe);
-  -webkit-border-radius: 2px;
-  -webkit-box-sizing: content-box;
-}
-.commentBox button div {
-  width: 25px;
-  white-space: nowrap;
-  color: #fff;
-}
-
-@sprite .goPrev {
-  gwt-image: "goPrev";
-  display: inline-block;
-}
-@sprite .goNext {
-  gwt-image: "goNext";
-  display: inline-block;
-}
-@sprite .goUp {
-  gwt-image: "goUp";
-  display: inline-block;
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java
deleted file mode 100644
index 6f9e694..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java
+++ /dev/null
@@ -1,158 +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.client.diff;
-
-import com.google.gerrit.client.changes.CommentInfo;
-import com.google.gwt.event.dom.client.MouseOutEvent;
-import com.google.gwt.event.dom.client.MouseOutHandler;
-import com.google.gwt.event.dom.client.MouseOverEvent;
-import com.google.gwt.event.dom.client.MouseOverHandler;
-import com.google.gwt.resources.client.CssResource;
-import com.google.gwt.user.client.ui.Composite;
-import net.codemirror.lib.CodeMirror;
-import net.codemirror.lib.Configuration;
-import net.codemirror.lib.Pos;
-import net.codemirror.lib.TextMarker;
-import net.codemirror.lib.TextMarker.FromTo;
-
-/** An HtmlPanel for displaying a comment */
-abstract class CommentBox extends Composite {
-  static {
-    Resources.I.style().ensureInjected();
-  }
-
-  interface Style extends CssResource {
-    String commentWidgets();
-
-    String commentBox();
-
-    String contents();
-
-    String message();
-
-    String header();
-
-    String summary();
-
-    String date();
-
-    String goPrev();
-
-    String goNext();
-
-    String goUp();
-  }
-
-  private final CommentGroup group;
-  private ScrollbarAnnotation annotation;
-  private FromTo fromTo;
-  private TextMarker rangeMarker;
-  private TextMarker rangeHighlightMarker;
-
-  CommentBox(CommentGroup group, CommentRange range) {
-    this.group = group;
-    if (range != null) {
-      DiffScreen screen = group.getManager().host;
-      int startCmLine = screen.getCmLine(range.startLine() - 1, group.getSide());
-      int endCmLine = screen.getCmLine(range.endLine() - 1, group.getSide());
-      fromTo =
-          FromTo.create(
-              Pos.create(startCmLine, range.startCharacter()),
-              Pos.create(endCmLine, range.endCharacter()));
-      rangeMarker =
-          group
-              .getCm()
-              .markText(
-                  fromTo.from(),
-                  fromTo.to(),
-                  Configuration.create().set("className", Resources.I.diffTableStyle().range()));
-    }
-    addDomHandler(
-        new MouseOverHandler() {
-          @Override
-          public void onMouseOver(MouseOverEvent event) {
-            setRangeHighlight(true);
-          }
-        },
-        MouseOverEvent.getType());
-    addDomHandler(
-        new MouseOutHandler() {
-          @Override
-          public void onMouseOut(MouseOutEvent event) {
-            setRangeHighlight(isOpen());
-          }
-        },
-        MouseOutEvent.getType());
-  }
-
-  abstract CommentInfo getCommentInfo();
-
-  abstract boolean isOpen();
-
-  void setOpen(boolean open) {
-    group.resize();
-    setRangeHighlight(open);
-    getCm().focus();
-  }
-
-  CommentGroup getCommentGroup() {
-    return group;
-  }
-
-  CommentManager getCommentManager() {
-    return group.getCommentManager();
-  }
-
-  ScrollbarAnnotation getAnnotation() {
-    return annotation;
-  }
-
-  void setAnnotation(ScrollbarAnnotation mh) {
-    annotation = mh;
-  }
-
-  void setRangeHighlight(boolean highlight) {
-    if (fromTo != null) {
-      if (highlight && rangeHighlightMarker == null) {
-        rangeHighlightMarker =
-            group
-                .getCm()
-                .markText(
-                    fromTo.from(),
-                    fromTo.to(),
-                    Configuration.create()
-                        .set("className", Resources.I.diffTableStyle().rangeHighlight()));
-      } else if (!highlight && rangeHighlightMarker != null) {
-        rangeHighlightMarker.clear();
-        rangeHighlightMarker = null;
-      }
-    }
-  }
-
-  void clearRange() {
-    if (rangeMarker != null) {
-      rangeMarker.clear();
-      rangeMarker = null;
-    }
-  }
-
-  CodeMirror getCm() {
-    return group.getCm();
-  }
-
-  FromTo getFromTo() {
-    return fromTo;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentGroup.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentGroup.java
deleted file mode 100644
index 414e82e..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentGroup.java
+++ /dev/null
@@ -1,202 +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.client.diff;
-
-import com.google.gwt.user.client.Timer;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.SimplePanel;
-import net.codemirror.lib.CodeMirror;
-import net.codemirror.lib.Configuration;
-import net.codemirror.lib.LineWidget;
-import net.codemirror.lib.TextMarker.FromTo;
-
-/**
- * LineWidget attached to a CodeMirror container.
- *
- * <p>When a comment is placed on a line a CommentWidget is created.
- */
-abstract class CommentGroup extends Composite {
-
-  final DisplaySide side;
-  final int line;
-
-  private final CommentManager manager;
-  private final CodeMirror cm;
-  private final FlowPanel comments;
-  private LineWidget lineWidget;
-  private Timer resizeTimer;
-
-  CommentGroup(CommentManager manager, CodeMirror cm, DisplaySide side, int line) {
-    this.manager = manager;
-    this.cm = cm;
-    this.side = side;
-    this.line = line;
-
-    comments = new FlowPanel();
-    comments.setStyleName(Resources.I.style().commentWidgets());
-    comments.setVisible(false);
-    initWidget(new SimplePanel(comments));
-  }
-
-  CommentManager getCommentManager() {
-    return manager;
-  }
-
-  CodeMirror getCm() {
-    return cm;
-  }
-
-  int getLine() {
-    return line;
-  }
-
-  DisplaySide getSide() {
-    return side;
-  }
-
-  void add(PublishedBox box) {
-    comments.add(box);
-    comments.setVisible(true);
-  }
-
-  void add(DraftBox box) {
-    PublishedBox p = box.getReplyToBox();
-    if (p != null) {
-      for (int i = 0; i < getBoxCount(); i++) {
-        if (p == getCommentBox(i)) {
-          comments.insert(box, i + 1);
-          comments.setVisible(true);
-          resize();
-          return;
-        }
-      }
-    }
-    comments.add(box);
-    comments.setVisible(true);
-    resize();
-  }
-
-  CommentBox getCommentBox(int i) {
-    return (CommentBox) comments.getWidget(i);
-  }
-
-  int getBoxCount() {
-    return comments.getWidgetCount();
-  }
-
-  void openCloseLast() {
-    if (0 < getBoxCount()) {
-      CommentBox box = getCommentBox(getBoxCount() - 1);
-      box.setOpen(!box.isOpen());
-    }
-  }
-
-  void openCloseAll() {
-    boolean open = false;
-    for (int i = 0; i < getBoxCount(); i++) {
-      if (!getCommentBox(i).isOpen()) {
-        open = true;
-        break;
-      }
-    }
-    setOpenAll(open);
-  }
-
-  void setOpenAll(boolean open) {
-    for (int i = 0; i < getBoxCount(); i++) {
-      getCommentBox(i).setOpen(open);
-    }
-  }
-
-  void remove(DraftBox box) {
-    comments.remove(box);
-    comments.setVisible(0 < getBoxCount());
-  }
-
-  void detach() {
-    if (lineWidget != null) {
-      lineWidget.clear();
-      lineWidget = null;
-      updateSelection();
-    }
-    manager.clearLine(side, line, this);
-    removeFromParent();
-  }
-
-  void attach(DiffTable parent) {
-    parent.add(this);
-    lineWidget =
-        cm.addLineWidget(
-            Math.max(0, line - 1),
-            getElement(),
-            Configuration.create()
-                .set("coverGutter", true)
-                .set("noHScroll", true)
-                .set("above", line <= 0)
-                .set("insertAt", 0));
-  }
-
-  @Override
-  protected void onUnload() {
-    super.onUnload();
-    if (resizeTimer != null) {
-      resizeTimer.cancel();
-    }
-  }
-
-  void updateSelection() {
-    if (cm.somethingSelected()) {
-      FromTo r = cm.getSelectedRange();
-      if (r.to().line() >= line) {
-        cm.setSelection(r.from(), r.to());
-      }
-    }
-  }
-
-  boolean canComputeHeight() {
-    return !comments.isVisible() || comments.getOffsetHeight() > 0;
-  }
-
-  LineWidget getLineWidget() {
-    return lineWidget;
-  }
-
-  void setLineWidget(LineWidget widget) {
-    lineWidget = widget;
-  }
-
-  Timer getResizeTimer() {
-    return resizeTimer;
-  }
-
-  void setResizeTimer(Timer timer) {
-    resizeTimer = timer;
-  }
-
-  FlowPanel getComments() {
-    return comments;
-  }
-
-  CommentManager getManager() {
-    return manager;
-  }
-
-  abstract void init(DiffTable parent);
-
-  abstract void handleRedraw();
-
-  abstract void resize();
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java
deleted file mode 100644
index ef1ec1e..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java
+++ /dev/null
@@ -1,451 +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.client.diff;
-
-import com.google.gerrit.client.DiffObject;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.CommentInfo;
-import com.google.gerrit.client.patches.SkippedLine;
-import com.google.gerrit.client.rpc.CallbackGroup;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.ui.CommentLinkProcessor;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.extensions.client.Side;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.JsArray;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.SortedMap;
-import java.util.TreeMap;
-import net.codemirror.lib.CodeMirror;
-import net.codemirror.lib.Pos;
-import net.codemirror.lib.TextMarker.FromTo;
-
-/** Tracks comment widgets for {@link DiffScreen}. */
-abstract class CommentManager {
-  @Nullable private final Project.NameKey project;
-  private final DiffObject base;
-  private final PatchSet.Id revision;
-  private final String path;
-  private final CommentLinkProcessor commentLinkProcessor;
-  final SortedMap<Integer, CommentGroup> sideA;
-  final SortedMap<Integer, CommentGroup> sideB;
-  private final Map<String, PublishedBox> published;
-  private final Set<DraftBox> unsavedDrafts;
-  final DiffScreen host;
-  private boolean attached;
-  private boolean expandAll;
-  private boolean open;
-
-  CommentManager(
-      DiffScreen host,
-      @Nullable Project.NameKey project,
-      DiffObject base,
-      PatchSet.Id revision,
-      String path,
-      CommentLinkProcessor clp,
-      boolean open) {
-    this.host = host;
-    this.project = project;
-    this.base = base;
-    this.revision = revision;
-    this.path = path;
-    this.commentLinkProcessor = clp;
-    this.open = open;
-
-    published = new HashMap<>();
-    unsavedDrafts = new HashSet<>();
-    sideA = new TreeMap<>();
-    sideB = new TreeMap<>();
-  }
-
-  void setAttached(boolean attached) {
-    this.attached = attached;
-  }
-
-  boolean isAttached() {
-    return attached;
-  }
-
-  void setExpandAll(boolean expandAll) {
-    this.expandAll = expandAll;
-  }
-
-  boolean isExpandAll() {
-    return expandAll;
-  }
-
-  boolean isOpen() {
-    return open;
-  }
-
-  String getPath() {
-    return path;
-  }
-
-  Map<String, PublishedBox> getPublished() {
-    return published;
-  }
-
-  CommentLinkProcessor getCommentLinkProcessor() {
-    return commentLinkProcessor;
-  }
-
-  void renderDrafts(DisplaySide forSide, JsArray<CommentInfo> in) {
-    for (CommentInfo info : Natives.asList(in)) {
-      DisplaySide side = displaySide(info, forSide);
-      if (side != null) {
-        addDraftBox(side, info);
-      }
-    }
-  }
-
-  void setUnsaved(DraftBox box, boolean isUnsaved) {
-    if (isUnsaved) {
-      unsavedDrafts.add(box);
-    } else {
-      unsavedDrafts.remove(box);
-    }
-  }
-
-  void saveAllDrafts(CallbackGroup cb) {
-    for (DraftBox box : unsavedDrafts) {
-      box.save(cb);
-    }
-  }
-
-  Side getStoredSideFromDisplaySide(DisplaySide side) {
-    if (side == DisplaySide.A && (base.isBaseOrAutoMerge() || base.isParent())) {
-      return Side.PARENT;
-    }
-    return Side.REVISION;
-  }
-
-  int getParentNumFromDisplaySide(DisplaySide side) {
-    if (side == DisplaySide.A) {
-      return base.getParentNum();
-    }
-    return 0;
-  }
-
-  PatchSet.Id getPatchSetIdFromSide(DisplaySide side) {
-    if (side == DisplaySide.A && (base.isPatchSet() || base.isEdit())) {
-      return base.asPatchSetId();
-    }
-    return revision;
-  }
-
-  DisplaySide displaySide(CommentInfo info, DisplaySide forSide) {
-    if (info.side() == Side.PARENT) {
-      return (base.isBaseOrAutoMerge() || base.isParent()) ? DisplaySide.A : null;
-    }
-    return forSide;
-  }
-
-  static FromTo adjustSelection(CodeMirror cm) {
-    FromTo fromTo = cm.getSelectedRange();
-    Pos to = fromTo.to();
-    if (to.ch() == 0) {
-      to.line(to.line() - 1);
-      to.ch(cm.getLine(to.line()).length());
-    }
-    return fromTo;
-  }
-
-  abstract CommentGroup group(DisplaySide side, int cmLinePlusOne);
-
-  /**
-   * Create a new {@link DraftBox} at the specified line and focus it.
-   *
-   * @param side which side the draft will appear on.
-   * @param line the line the draft will be at. Lines are 1-based. Line 0 is a special case creating
-   *     a file level comment.
-   */
-  void insertNewDraft(DisplaySide side, int line) {
-    if (line == 0) {
-      host.skipManager.ensureFirstLineIsVisible();
-    }
-
-    CommentGroup group = group(side, line);
-    if (0 < group.getBoxCount()) {
-      CommentBox last = group.getCommentBox(group.getBoxCount() - 1);
-      if (last instanceof DraftBox) {
-        ((DraftBox) last).setEdit(true);
-      } else {
-        ((PublishedBox) last).doReply();
-      }
-    } else {
-      addDraftBox(
-              side,
-              CommentInfo.create(
-                  getPath(),
-                  getStoredSideFromDisplaySide(side),
-                  getParentNumFromDisplaySide(side),
-                  line,
-                  null,
-                  false))
-          .setEdit(true);
-    }
-  }
-
-  abstract String getTokenSuffixForActiveLine(CodeMirror cm);
-
-  Runnable signInCallback(CodeMirror cm) {
-    return () -> {
-      String token = host.getToken();
-      if (cm.extras().hasActiveLine()) {
-        token += "@" + getTokenSuffixForActiveLine(cm);
-      }
-      Gerrit.doSignIn(token);
-    };
-  }
-
-  abstract void newDraft(CodeMirror cm);
-
-  Runnable newDraftCallback(CodeMirror cm) {
-    if (!Gerrit.isSignedIn()) {
-      return signInCallback(cm);
-    }
-
-    return () -> {
-      if (cm.extras().hasActiveLine()) {
-        newDraft(cm);
-      }
-    };
-  }
-
-  DraftBox addDraftBox(DisplaySide side, CommentInfo info) {
-    int cmLinePlusOne = host.getCmLine(info.line() - 1, side) + 1;
-    CommentGroup group = group(side, cmLinePlusOne);
-    DraftBox box =
-        new DraftBox(
-            group,
-            getCommentLinkProcessor(),
-            project,
-            getPatchSetIdFromSide(side),
-            info,
-            isExpandAll());
-
-    if (info.inReplyTo() != null) {
-      PublishedBox r = getPublished().get(info.inReplyTo());
-      if (r != null) {
-        r.setReplyBox(box);
-      }
-    }
-
-    group.add(box);
-    box.setAnnotation(
-        host.getDiffTable()
-            .scrollbar
-            .draft(host.getCmFromSide(side), Math.max(0, cmLinePlusOne - 1)));
-    return box;
-  }
-
-  void setExpandAllComments(boolean b) {
-    setExpandAll(b);
-    for (CommentGroup g : sideA.values()) {
-      g.setOpenAll(b);
-    }
-    for (CommentGroup g : sideB.values()) {
-      g.setOpenAll(b);
-    }
-  }
-
-  abstract SortedMap<Integer, CommentGroup> getMapForNav(DisplaySide side);
-
-  Runnable commentNav(CodeMirror src, Direction dir) {
-    return () -> {
-      // Every comment appears in both side maps as a linked pair.
-      // It is only necessary to search one side to find a comment
-      // on either side of the editor pair.
-      SortedMap<Integer, CommentGroup> map = getMapForNav(src.side());
-      int line =
-          src.extras().hasActiveLine() ? src.getLineNumber(src.extras().activeLine()) + 1 : 0;
-
-      CommentGroup g;
-      if (dir == Direction.NEXT) {
-        map = map.tailMap(line + 1);
-        if (map.isEmpty()) {
-          return;
-        }
-        g = map.get(map.firstKey());
-        while (g.getBoxCount() == 0) {
-          map = map.tailMap(map.firstKey() + 1);
-          if (map.isEmpty()) {
-            return;
-          }
-          g = map.get(map.firstKey());
-        }
-      } else {
-        map = map.headMap(line);
-        if (map.isEmpty()) {
-          return;
-        }
-        g = map.get(map.lastKey());
-        while (g.getBoxCount() == 0) {
-          map = map.headMap(map.lastKey());
-          if (map.isEmpty()) {
-            return;
-          }
-          g = map.get(map.lastKey());
-        }
-      }
-
-      CodeMirror cm = g.getCm();
-      double y = cm.heightAtLine(g.getLine() - 1, "local");
-      cm.setCursor(Pos.create(g.getLine() - 1));
-      cm.scrollToY(y - 0.5 * cm.scrollbarV().getClientHeight());
-      cm.focus();
-    };
-  }
-
-  void clearLine(DisplaySide side, int line, CommentGroup group) {
-    SortedMap<Integer, CommentGroup> map = map(side);
-    if (map.get(line) == group) {
-      map.remove(line);
-    }
-  }
-
-  void render(CommentsCollections in, boolean expandAll) {
-    if (in.publishedBase != null) {
-      renderPublished(DisplaySide.A, in.publishedBase);
-    }
-    if (in.publishedRevision != null) {
-      renderPublished(DisplaySide.B, in.publishedRevision);
-    }
-    if (in.draftsBase != null) {
-      renderDrafts(DisplaySide.A, in.draftsBase);
-    }
-    if (in.draftsRevision != null) {
-      renderDrafts(DisplaySide.B, in.draftsRevision);
-    }
-    if (expandAll) {
-      setExpandAllComments(true);
-    }
-    for (CommentGroup g : sideA.values()) {
-      g.init(host.getDiffTable());
-    }
-    for (CommentGroup g : sideB.values()) {
-      g.init(host.getDiffTable());
-      g.handleRedraw();
-    }
-    setAttached(true);
-  }
-
-  void renderPublished(DisplaySide forSide, JsArray<CommentInfo> in) {
-    for (CommentInfo info : Natives.asList(in)) {
-      DisplaySide side = displaySide(info, forSide);
-      if (side != null) {
-        int cmLinePlusOne = host.getCmLine(info.line() - 1, side) + 1;
-        CommentGroup group = group(side, cmLinePlusOne);
-        PublishedBox box =
-            new PublishedBox(
-                group,
-                getCommentLinkProcessor(),
-                project,
-                getPatchSetIdFromSide(side),
-                info,
-                side,
-                isOpen());
-        group.add(box);
-        box.setAnnotation(
-            host.getDiffTable().scrollbar.comment(host.getCmFromSide(side), cmLinePlusOne - 1));
-        getPublished().put(info.id(), box);
-      }
-    }
-  }
-
-  abstract Collection<Integer> getLinesWithCommentGroups();
-
-  private static void checkAndAddSkip(List<SkippedLine> out, SkippedLine s) {
-    if (s.getSize() > 1) {
-      out.add(s);
-    }
-  }
-
-  List<SkippedLine> splitSkips(int context, List<SkippedLine> skips) {
-    if (sideA.containsKey(0) || sideB.containsKey(0)) {
-      // Special case of file comment; cannot skip first line.
-      for (SkippedLine skip : skips) {
-        if (skip.getStartA() == 0) {
-          skip.incrementStart(1);
-          break;
-        }
-      }
-    }
-
-    for (int boxLine : getLinesWithCommentGroups()) {
-      List<SkippedLine> temp = new ArrayList<>(skips.size() + 2);
-      for (SkippedLine skip : skips) {
-        int startLine = host.getCmLine(skip.getStartB(), DisplaySide.B);
-        int deltaBefore = boxLine - startLine;
-        int deltaAfter = startLine + skip.getSize() - boxLine;
-        if (deltaBefore < -context || deltaAfter < -context) {
-          temp.add(skip); // Size guaranteed to be greater than 1
-        } else if (deltaBefore > context && deltaAfter > context) {
-          SkippedLine before =
-              new SkippedLine(
-                  skip.getStartA(), skip.getStartB(), skip.getSize() - deltaAfter - context);
-          skip.incrementStart(deltaBefore + context);
-          checkAndAddSkip(temp, before);
-          checkAndAddSkip(temp, skip);
-        } else if (deltaAfter > context) {
-          skip.incrementStart(deltaBefore + context);
-          checkAndAddSkip(temp, skip);
-        } else if (deltaBefore > context) {
-          skip.reduceSize(deltaAfter + context);
-          checkAndAddSkip(temp, skip);
-        }
-      }
-      if (temp.isEmpty()) {
-        return temp;
-      }
-      skips = temp;
-    }
-    return skips;
-  }
-
-  abstract void newDraftOnGutterClick(CodeMirror cm, String gutterClass, int line);
-
-  abstract CommentGroup getCommentGroupOnActiveLine(CodeMirror cm);
-
-  Runnable toggleOpenBox(CodeMirror cm) {
-    return () -> {
-      CommentGroup group = getCommentGroupOnActiveLine(cm);
-      if (group != null) {
-        group.openCloseLast();
-      }
-    };
-  }
-
-  Runnable openCloseAll(CodeMirror cm) {
-    return () -> {
-      CommentGroup group = getCommentGroupOnActiveLine(cm);
-      if (group != null) {
-        group.openCloseAll();
-      }
-    };
-  }
-
-  SortedMap<Integer, CommentGroup> map(DisplaySide side) {
-    return side == DisplaySide.A ? sideA : sideB;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentRange.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentRange.java
deleted file mode 100644
index 0f357d5..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentRange.java
+++ /dev/null
@@ -1,56 +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.client.diff;
-
-import com.google.gwt.core.client.JavaScriptObject;
-import net.codemirror.lib.Pos;
-import net.codemirror.lib.TextMarker.FromTo;
-
-public class CommentRange extends JavaScriptObject {
-  public static CommentRange create(int sl, int sc, int el, int ec) {
-    CommentRange r = createObject().cast();
-    r.set(sl, sc, el, ec);
-    return r;
-  }
-
-  public static CommentRange create(FromTo fromTo) {
-    if (fromTo == null) {
-      return null;
-    }
-
-    Pos from = fromTo.from();
-    Pos to = fromTo.to();
-    return create(
-        from.line() + 1, from.ch(),
-        to.line() + 1, to.ch());
-  }
-
-  public final native int startLine() /*-{ return this.start_line; }-*/;
-
-  public final native int startCharacter() /*-{ return this.start_character; }-*/;
-
-  public final native int endLine() /*-{ return this.end_line; }-*/;
-
-  public final native int endCharacter() /*-{ return this.end_character; }-*/;
-
-  private native void set(int sl, int sc, int el, int ec) /*-{
-    this.start_line = sl;
-    this.start_character = sc;
-    this.end_line = el;
-    this.end_character = ec;
-  }-*/;
-
-  protected CommentRange() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentsCollections.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentsCollections.java
deleted file mode 100644
index 2698584..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentsCollections.java
+++ /dev/null
@@ -1,165 +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.client.diff;
-
-import static java.util.Comparator.comparing;
-
-import com.google.gerrit.client.DiffObject;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.CommentApi;
-import com.google.gerrit.client.changes.CommentInfo;
-import com.google.gerrit.client.rpc.CallbackGroup;
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.extensions.client.Side;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-
-/** Collection of published and draft comments loaded from the server. */
-class CommentsCollections {
-  @Nullable private final Project.NameKey project;
-  private final String path;
-  private final DiffObject base;
-  private final PatchSet.Id revision;
-  private NativeMap<JsArray<CommentInfo>> publishedBaseAll;
-  private NativeMap<JsArray<CommentInfo>> publishedRevisionAll;
-  JsArray<CommentInfo> publishedBase;
-  JsArray<CommentInfo> publishedRevision;
-  JsArray<CommentInfo> draftsBase;
-  JsArray<CommentInfo> draftsRevision;
-
-  CommentsCollections(
-      @Nullable Project.NameKey project, DiffObject base, PatchSet.Id revision, String path) {
-    this.project = project;
-    this.path = path;
-    this.base = base;
-    this.revision = revision;
-  }
-
-  void load(CallbackGroup group) {
-    if (base.isPatchSet()) {
-      CommentApi.comments(
-          Project.NameKey.asStringOrNull(project), base.asPatchSetId(), group.add(publishedBase()));
-    }
-    CommentApi.comments(
-        Project.NameKey.asStringOrNull(project), revision, group.add(publishedRevision()));
-
-    if (Gerrit.isSignedIn()) {
-      if (base.isPatchSet()) {
-        CommentApi.drafts(
-            Project.NameKey.asStringOrNull(project), base.asPatchSetId(), group.add(draftsBase()));
-      }
-      CommentApi.drafts(
-          Project.NameKey.asStringOrNull(project), revision, group.add(draftsRevision()));
-    }
-  }
-
-  boolean hasCommentForPath(String filePath) {
-    if (base.isPatchSet()) {
-      JsArray<CommentInfo> forBase = publishedBaseAll.get(filePath);
-      if (forBase != null && forBase.length() > 0) {
-        return true;
-      }
-    }
-    JsArray<CommentInfo> forRevision = publishedRevisionAll.get(filePath);
-    if (forRevision != null && forRevision.length() > 0) {
-      return true;
-    }
-    return false;
-  }
-
-  private AsyncCallback<NativeMap<JsArray<CommentInfo>>> publishedBase() {
-    return new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
-      @Override
-      public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
-        publishedBaseAll = result;
-        publishedBase = sort(result.get(path));
-      }
-
-      @Override
-      public void onFailure(Throwable caught) {}
-    };
-  }
-
-  private AsyncCallback<NativeMap<JsArray<CommentInfo>>> publishedRevision() {
-    return new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
-      @Override
-      public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
-        for (String k : result.keySet()) {
-          result.put(k, filterForParent(result.get(k)));
-        }
-        publishedRevisionAll = result;
-        publishedRevision = sort(result.get(path));
-      }
-
-      @Override
-      public void onFailure(Throwable caught) {}
-    };
-  }
-
-  private JsArray<CommentInfo> filterForParent(JsArray<CommentInfo> list) {
-    JsArray<CommentInfo> result = JsArray.createArray().cast();
-    for (CommentInfo c : Natives.asList(list)) {
-      if (c.side() == Side.REVISION) {
-        result.push(c);
-      } else if (base.isBaseOrAutoMerge() && !c.hasParent()) {
-        result.push(c);
-      } else if (base.isParent() && c.parent() == base.getParentNum()) {
-        result.push(c);
-      }
-    }
-    return result;
-  }
-
-  private AsyncCallback<NativeMap<JsArray<CommentInfo>>> draftsBase() {
-    return new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
-      @Override
-      public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
-        draftsBase = sort(result.get(path));
-      }
-
-      @Override
-      public void onFailure(Throwable caught) {}
-    };
-  }
-
-  private AsyncCallback<NativeMap<JsArray<CommentInfo>>> draftsRevision() {
-    return new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
-      @Override
-      public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
-        for (String k : result.keySet()) {
-          result.put(k, filterForParent(result.get(k)));
-        }
-        draftsRevision = sort(result.get(path));
-      }
-
-      @Override
-      public void onFailure(Throwable caught) {}
-    };
-  }
-
-  private JsArray<CommentInfo> sort(JsArray<CommentInfo> in) {
-    if (in != null) {
-      for (CommentInfo c : Natives.asList(in)) {
-        c.path(path);
-      }
-      Natives.asList(in).sort(comparing(CommentInfo::updated));
-    }
-    return in;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java
deleted file mode 100644
index 1815920..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java
+++ /dev/null
@@ -1,116 +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.client.diff;
-
-import static com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace.IGNORE_ALL;
-
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
-import com.google.gerrit.client.info.FileInfo;
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-
-public class DiffApi {
-  public static void list(
-      @Nullable String project,
-      int id,
-      String revision,
-      RevisionInfo base,
-      AsyncCallback<NativeMap<FileInfo>> cb) {
-    RestApi api = ChangeApi.revision(project, id, revision).view("files");
-    if (base != null) {
-      if (base._number() < 0) {
-        api.addParameter("parent", -base._number());
-      } else {
-        api.addParameter("base", base.name());
-      }
-    }
-    api.get(NativeMap.copyKeysIntoChildren("path", cb));
-  }
-
-  public static void list(
-      @Nullable String project,
-      PatchSet.Id id,
-      PatchSet.Id base,
-      AsyncCallback<NativeMap<FileInfo>> cb) {
-    RestApi api = ChangeApi.revision(project, id).view("files");
-    if (base != null) {
-      if (base.get() < 0) {
-        api.addParameter("parent", -base.get());
-      } else {
-        api.addParameter("base", base.get());
-      }
-    }
-    api.get(NativeMap.copyKeysIntoChildren("path", cb));
-  }
-
-  public static DiffApi diff(@Nullable String project, PatchSet.Id id, String path) {
-    return new DiffApi(ChangeApi.revision(project, id).view("files").id(path).view("diff"));
-  }
-
-  private final RestApi call;
-
-  private DiffApi(RestApi call) {
-    this.call = call;
-  }
-
-  public DiffApi base(PatchSet.Id id) {
-    if (id != null) {
-      if (id.get() < 0) {
-        call.addParameter("parent", -id.get());
-      } else {
-        call.addParameter("base", id.get());
-      }
-    }
-    return this;
-  }
-
-  public DiffApi webLinksOnly() {
-    call.addParameterTrue("weblinks-only");
-    return this;
-  }
-
-  public DiffApi ignoreWhitespace(DiffPreferencesInfo.Whitespace w) {
-    if (w != null && w != IGNORE_ALL) {
-      call.addParameter("whitespace", w);
-    }
-    return this;
-  }
-
-  public DiffApi intraline(boolean intraline) {
-    if (intraline) {
-      call.addParameterTrue("intraline");
-    }
-    return this;
-  }
-
-  public DiffApi wholeFile() {
-    call.addParameter("context", "ALL");
-    return this;
-  }
-
-  public DiffApi context(int lines) {
-    call.addParameter("context", lines);
-    return this;
-  }
-
-  public void get(AsyncCallback<DiffInfo> cb) {
-    call.get(cb);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffChunkInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffChunkInfo.java
deleted file mode 100644
index 3b1b346..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffChunkInfo.java
+++ /dev/null
@@ -1,46 +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.client.diff;
-
-/** Object recording the position of a diff chunk and whether it's an edit */
-class DiffChunkInfo {
-  private DisplaySide side;
-  private int start;
-  private int end;
-  private boolean edit;
-
-  DiffChunkInfo(DisplaySide side, int start, int end, boolean edit) {
-    this.side = side;
-    this.start = start;
-    this.end = end;
-    this.edit = edit;
-  }
-
-  DisplaySide getSide() {
-    return side;
-  }
-
-  int getStart() {
-    return start;
-  }
-
-  int getEnd() {
-    return end;
-  }
-
-  boolean isEdit() {
-    return edit;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java
deleted file mode 100644
index d942c2e..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java
+++ /dev/null
@@ -1,193 +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.client.diff;
-
-import com.google.gerrit.client.DiffWebLinkInfo;
-import com.google.gerrit.client.info.WebLinkInfo;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DiffView;
-import com.google.gerrit.reviewdb.client.Patch.ChangeType;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.core.client.JsArrayString;
-import java.util.ArrayList;
-import java.util.List;
-
-public class DiffInfo extends JavaScriptObject {
-  public final native FileMeta metaA() /*-{ return this.meta_a; }-*/;
-
-  public final native FileMeta metaB() /*-{ return this.meta_b; }-*/;
-
-  public final native JsArrayString diffHeader() /*-{ return this.diff_header; }-*/;
-
-  public final native JsArray<Region> content() /*-{ return this.content; }-*/;
-
-  public final native JsArray<DiffWebLinkInfo> webLinks() /*-{ return this.web_links; }-*/;
-
-  public final native boolean binary() /*-{ return this.binary || false; }-*/;
-
-  public final List<WebLinkInfo> sideBySideWebLinks() {
-    return filterWebLinks(DiffView.SIDE_BY_SIDE);
-  }
-
-  public final List<WebLinkInfo> unifiedWebLinks() {
-    return filterWebLinks(DiffView.UNIFIED_DIFF);
-  }
-
-  private List<WebLinkInfo> filterWebLinks(DiffView diffView) {
-    List<WebLinkInfo> filteredDiffWebLinks = new ArrayList<>();
-    List<DiffWebLinkInfo> allDiffWebLinks = Natives.asList(webLinks());
-    if (allDiffWebLinks != null) {
-      for (DiffWebLinkInfo webLink : allDiffWebLinks) {
-        if (diffView == DiffView.SIDE_BY_SIDE && webLink.showOnSideBySideDiffView()) {
-          filteredDiffWebLinks.add(webLink);
-        }
-        if (diffView == DiffView.UNIFIED_DIFF && webLink.showOnUnifiedDiffView()) {
-          filteredDiffWebLinks.add(webLink);
-        }
-      }
-    }
-    return filteredDiffWebLinks;
-  }
-
-  public final ChangeType changeType() {
-    return ChangeType.valueOf(changeTypeRaw());
-  }
-
-  private native String changeTypeRaw() /*-{ return this.change_type }-*/;
-
-  public final IntraLineStatus intralineStatus() {
-    String s = intralineStatusRaw();
-    return s != null ? IntraLineStatus.valueOf(s) : IntraLineStatus.OFF;
-  }
-
-  private native String intralineStatusRaw() /*-{ return this.intraline_status }-*/;
-
-  public final boolean hasSkip() {
-    JsArray<Region> c = content();
-    for (int i = 0; i < c.length(); i++) {
-      if (c.get(i).skip() != 0) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  public final String textA() {
-    StringBuilder s = new StringBuilder();
-    JsArray<Region> c = content();
-    for (int i = 0; i < c.length(); i++) {
-      Region r = c.get(i);
-      if (r.ab() != null) {
-        append(s, r.ab());
-      } else if (r.a() != null) {
-        append(s, r.a());
-      }
-      // TODO skip may need to be handled
-    }
-    return s.toString();
-  }
-
-  public final String textB() {
-    StringBuilder s = new StringBuilder();
-    JsArray<Region> c = content();
-    for (int i = 0; i < c.length(); i++) {
-      Region r = c.get(i);
-      if (r.ab() != null) {
-        append(s, r.ab());
-      } else if (r.b() != null) {
-        append(s, r.b());
-      }
-      // TODO skip may need to be handled
-    }
-    return s.toString();
-  }
-
-  public final String textUnified() {
-    StringBuilder s = new StringBuilder();
-    JsArray<Region> c = content();
-    for (int i = 0; i < c.length(); i++) {
-      Region r = c.get(i);
-      if (r.ab() != null) {
-        append(s, r.ab());
-      } else {
-        if (r.a() != null) {
-          append(s, r.a());
-        }
-        if (r.b() != null) {
-          append(s, r.b());
-        }
-      }
-      // TODO skip may need to be handled
-    }
-    return s.toString();
-  }
-
-  private static void append(StringBuilder s, JsArrayString lines) {
-    for (int i = 0; i < lines.length(); i++) {
-      if (s.length() > 0) {
-        s.append('\n');
-      }
-      s.append(lines.get(i));
-    }
-  }
-
-  protected DiffInfo() {}
-
-  public enum IntraLineStatus {
-    OFF,
-    OK,
-    TIMEOUT,
-    FAILURE
-  }
-
-  public static class FileMeta extends JavaScriptObject {
-    public final native String name() /*-{ return this.name; }-*/;
-
-    public final native String contentType() /*-{ return this.content_type; }-*/;
-
-    public final native int lines() /*-{ return this.lines || 0 }-*/;
-
-    public final native JsArray<WebLinkInfo> webLinks() /*-{ return this.web_links; }-*/;
-
-    protected FileMeta() {}
-  }
-
-  public static class Region extends JavaScriptObject {
-    public final native JsArrayString ab() /*-{ return this.ab; }-*/;
-
-    public final native JsArrayString a() /*-{ return this.a; }-*/;
-
-    public final native JsArrayString b() /*-{ return this.b; }-*/;
-
-    public final native int skip() /*-{ return this.skip || 0; }-*/;
-
-    public final native boolean common() /*-{ return this.common || false; }-*/;
-
-    public final native JsArray<Span> editA() /*-{ return this.edit_a }-*/;
-
-    public final native JsArray<Span> editB() /*-{ return this.edit_b }-*/;
-
-    protected Region() {}
-  }
-
-  public static class Span extends JavaScriptObject {
-    public final native int skip() /*-{ return this[0]; }-*/;
-
-    public final native int mark() /*-{ return this[1]; }-*/;
-
-    protected Span() {}
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java
deleted file mode 100644
index b4221ca..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java
+++ /dev/null
@@ -1,931 +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 impl ied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.diff;
-
-import static com.google.gerrit.extensions.client.DiffPreferencesInfo.WHOLE_FILE_CONTEXT;
-import static java.lang.Double.POSITIVE_INFINITY;
-
-import com.google.gerrit.client.DiffObject;
-import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.account.DiffPreferences;
-import com.google.gerrit.client.change.ChangeScreen;
-import com.google.gerrit.client.change.FileTable;
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeList;
-import com.google.gerrit.client.diff.DiffInfo.FileMeta;
-import com.google.gerrit.client.diff.LineMapper.LineOnOtherInfo;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.info.ChangeInfo.CommitInfo;
-import com.google.gerrit.client.info.ChangeInfo.EditInfo;
-import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
-import com.google.gerrit.client.info.FileInfo;
-import com.google.gerrit.client.patches.PatchUtil;
-import com.google.gerrit.client.projects.ConfigInfoCache;
-import com.google.gerrit.client.rpc.CallbackGroup;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.client.ui.Screen;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DiffView;
-import com.google.gerrit.extensions.client.ListChangesOption;
-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.gwt.core.client.JsArray;
-import com.google.gwt.core.client.Scheduler;
-import com.google.gwt.core.client.Scheduler.RepeatingCommand;
-import com.google.gwt.core.client.Scheduler.ScheduledCommand;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.NativeEvent;
-import com.google.gwt.event.dom.client.FocusHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.logical.shared.ResizeEvent;
-import com.google.gwt.event.logical.shared.ResizeHandler;
-import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.user.client.Window;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-import com.google.gwtexpui.globalkey.client.KeyCommand;
-import com.google.gwtexpui.globalkey.client.KeyCommandSet;
-import com.google.gwtexpui.globalkey.client.ShowHelpCommand;
-import java.util.ArrayList;
-import java.util.EnumSet;
-import java.util.List;
-import net.codemirror.lib.CodeMirror;
-import net.codemirror.lib.CodeMirror.BeforeSelectionChangeHandler;
-import net.codemirror.lib.CodeMirror.GutterClickHandler;
-import net.codemirror.lib.CodeMirror.LineHandle;
-import net.codemirror.lib.KeyMap;
-import net.codemirror.lib.Pos;
-import net.codemirror.mode.ModeInfo;
-import net.codemirror.mode.ModeInjector;
-import net.codemirror.theme.ThemeLoader;
-
-/** Base class for SideBySide and Unified */
-abstract class DiffScreen extends Screen {
-  private static final KeyMap RENDER_ENTIRE_FILE_KEYMAP =
-      KeyMap.create().propagate("Ctrl-F").propagate("Ctrl-G").propagate("Shift-Ctrl-G");
-
-  enum FileSize {
-    SMALL(0),
-    LARGE(500),
-    HUGE(4000);
-
-    final int lines;
-
-    FileSize(int n) {
-      this.lines = n;
-    }
-  }
-
-  @Nullable private Project.NameKey project;
-  private final Change.Id changeId;
-  final DiffObject base;
-  final PatchSet.Id revision;
-  final String path;
-  final DiffPreferences prefs;
-  final SkipManager skipManager;
-
-  private DisplaySide startSide;
-  private int startLine;
-  private Change.Status changeStatus;
-
-  private HandlerRegistration resizeHandler;
-  private DiffInfo diff;
-  private FileSize fileSize;
-  private EditInfo edit;
-
-  private KeyCommandSet keysNavigation;
-  private KeyCommandSet keysAction;
-  private KeyCommandSet keysComment;
-  private List<HandlerRegistration> handlers;
-  private PreferencesAction prefsAction;
-  private int reloadVersionId;
-  private int parents;
-
-  @UiField(provided = true)
-  Header header;
-
-  DiffScreen(
-      @Nullable Project.NameKey project,
-      DiffObject base,
-      DiffObject revision,
-      String path,
-      DisplaySide startSide,
-      int startLine,
-      DiffView diffScreenType) {
-    this.project = project;
-    this.base = base;
-    this.revision = revision.asPatchSetId();
-    this.changeId = revision.asPatchSetId().getParentKey();
-    this.path = path;
-    this.startSide = startSide;
-    this.startLine = startLine;
-
-    prefs = DiffPreferences.create(Gerrit.getDiffPreferences());
-    handlers = new ArrayList<>(6);
-    keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
-    header = new Header(keysNavigation, project, base, revision, path, diffScreenType, prefs);
-    skipManager = new SkipManager(this);
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    setHeaderVisible(false);
-    setWindowTitle(FileInfo.getFileName(path));
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-
-    CallbackGroup group1 = new CallbackGroup();
-    final CallbackGroup group2 = new CallbackGroup();
-
-    CodeMirror.initLibrary(
-        group1.add(
-            new AsyncCallback<Void>() {
-              final AsyncCallback<Void> themeCallback = group2.addEmpty();
-
-              @Override
-              public void onSuccess(Void result) {
-                // Load theme after CM library to ensure theme can override CSS.
-                ThemeLoader.loadTheme(prefs.theme(), themeCallback);
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {}
-            }));
-
-    DiffApi.diff(Project.NameKey.asStringOrNull(project), revision, path)
-        .base(base.asPatchSetId())
-        .wholeFile()
-        .intraline(prefs.intralineDifference())
-        .ignoreWhitespace(prefs.ignoreWhitespace())
-        .get(
-            group1.addFinal(
-                new GerritCallback<DiffInfo>() {
-                  final AsyncCallback<Void> modeInjectorCb = group2.addEmpty();
-
-                  @Override
-                  public void onSuccess(DiffInfo diffInfo) {
-                    diff = diffInfo;
-                    fileSize = bucketFileSize(diffInfo);
-
-                    if (prefs.syntaxHighlighting()) {
-                      if (fileSize.compareTo(FileSize.SMALL) > 0) {
-                        modeInjectorCb.onSuccess(null);
-                      } else {
-                        injectMode(diffInfo, modeInjectorCb);
-                      }
-                    } else {
-                      modeInjectorCb.onSuccess(null);
-                    }
-                  }
-                }));
-
-    if (Gerrit.isSignedIn()) {
-      ChangeApi.edit(
-          Project.NameKey.asStringOrNull(project),
-          changeId.get(),
-          group2.add(
-              new AsyncCallback<EditInfo>() {
-                @Override
-                public void onSuccess(EditInfo result) {
-                  edit = result;
-                }
-
-                @Override
-                public void onFailure(Throwable caught) {}
-              }));
-    }
-
-    final CommentsCollections comments = new CommentsCollections(project, base, revision, path);
-    comments.load(group2);
-
-    countParents(group2);
-
-    RestApi call = ChangeApi.detail(Project.NameKey.asStringOrNull(project), changeId.get());
-    ChangeList.addOptions(call, EnumSet.of(ListChangesOption.ALL_REVISIONS));
-    call.get(
-        group2.add(
-            new AsyncCallback<ChangeInfo>() {
-              @Override
-              public void onSuccess(ChangeInfo info) {
-                changeStatus = info.status();
-                project = info.projectNameKey();
-                info.revisions().copyKeysIntoChildren("name");
-                if (edit != null) {
-                  edit.setName(edit.commit().commit());
-                  info.setEdit(edit);
-                  info.revisions().put(edit.name(), RevisionInfo.fromEdit(edit));
-                }
-                String currentRevision = info.currentRevision();
-                boolean current =
-                    currentRevision != null
-                        && revision.get() == info.revision(currentRevision)._number();
-                JsArray<RevisionInfo> list = info.revisions().values();
-                RevisionInfo.sortRevisionInfoByNumber(list);
-                getDiffTable()
-                    .set(
-                        prefs,
-                        list,
-                        parents,
-                        diff,
-                        edit != null,
-                        current,
-                        changeStatus.isOpen(),
-                        diff.binary());
-                header.setChangeInfo(info);
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {}
-            }));
-
-    ConfigInfoCache.get(changeId, group2.addFinal(getScreenLoadCallback(comments)));
-  }
-
-  private void countParents(CallbackGroup cbg) {
-    ChangeApi.revision(Project.NameKey.asStringOrNull(project), changeId.get(), revision.getId())
-        .view("commit")
-        .get(
-            cbg.add(
-                new AsyncCallback<CommitInfo>() {
-                  @Override
-                  public void onSuccess(CommitInfo info) {
-                    parents = info.parents().length();
-                  }
-
-                  @Override
-                  public void onFailure(Throwable caught) {
-                    parents = 0;
-                  }
-                }));
-  }
-
-  @Override
-  public void onShowView() {
-    super.onShowView();
-
-    Window.enableScrolling(false);
-    if (prefs.hideTopMenu()) {
-      Gerrit.setHeaderVisible(false);
-    }
-    resizeHandler =
-        Window.addResizeHandler(
-            new ResizeHandler() {
-              @Override
-              public void onResize(ResizeEvent event) {
-                resizeCodeMirror();
-              }
-            });
-  }
-
-  KeyCommandSet getKeysNavigation() {
-    return keysNavigation;
-  }
-
-  KeyCommandSet getKeysAction() {
-    return keysAction;
-  }
-
-  @Override
-  protected void onUnload() {
-    super.onUnload();
-
-    removeKeyHandlerRegistrations();
-    if (getCommentManager() != null) {
-      CallbackGroup group = new CallbackGroup();
-      getCommentManager().saveAllDrafts(group);
-      group.done();
-    }
-    if (resizeHandler != null) {
-      resizeHandler.removeHandler();
-      resizeHandler = null;
-    }
-    for (CodeMirror cm : getCms()) {
-      if (cm != null) {
-        cm.getWrapperElement().removeFromParent();
-      }
-    }
-    if (prefsAction != null) {
-      prefsAction.hide();
-    }
-
-    Window.enableScrolling(true);
-    Gerrit.setHeaderVisible(true);
-  }
-
-  private void removeKeyHandlerRegistrations() {
-    for (HandlerRegistration h : handlers) {
-      h.removeHandler();
-    }
-    handlers.clear();
-  }
-
-  void registerCmEvents(CodeMirror cm) {
-    cm.on("cursorActivity", updateActiveLine(cm));
-    cm.on("focus", updateActiveLine(cm));
-    KeyMap keyMap =
-        KeyMap.create()
-            .on("A", upToChange(true))
-            .on("U", upToChange(false))
-            .on("'['", header.navigate(Direction.PREV))
-            .on("']'", header.navigate(Direction.NEXT))
-            .on("R", header.toggleReviewed())
-            .on("O", getCommentManager().toggleOpenBox(cm))
-            .on("N", maybeNextVimSearch(cm))
-            .on("Ctrl-Alt-E", openEditScreen(cm))
-            .on("P", getChunkManager().diffChunkNav(cm, Direction.PREV))
-            .on("Shift-M", header.reviewedAndNext())
-            .on("Shift-N", maybePrevVimSearch(cm))
-            .on("Shift-P", getCommentManager().commentNav(cm, Direction.PREV))
-            .on("Shift-O", getCommentManager().openCloseAll(cm))
-            .on(
-                "I",
-                () -> {
-                  switch (getIntraLineStatus()) {
-                    case OFF:
-                    case OK:
-                      toggleShowIntraline();
-                      break;
-                    case FAILURE:
-                    case TIMEOUT:
-                    default:
-                      break;
-                  }
-                })
-            .on("','", prefsAction::show)
-            .on("Shift-/", () -> new ShowHelpCommand().onKeyPress(null))
-            .on("Space", () -> cm.vim().handleKey("<C-d>"))
-            .on("Shift-Space", () -> cm.vim().handleKey("<C-u>"))
-            .on("Ctrl-F", () -> cm.execCommand("find"))
-            .on("Ctrl-G", () -> cm.execCommand("findNext"))
-            .on("Enter", maybeNextCmSearch(cm))
-            .on("Shift-Ctrl-G", () -> cm.execCommand("findPrev"))
-            .on("Shift-Enter", () -> cm.execCommand("findPrev"))
-            .on(
-                "Esc",
-                () -> {
-                  cm.setCursor(cm.getCursor());
-                  cm.execCommand("clearSearch");
-                  cm.vim().handleEx("nohlsearch");
-                })
-            .on("Ctrl-A", () -> cm.execCommand("selectAll"))
-            .on("G O", () -> Gerrit.display(PageLinks.toChangeQuery("status:open")))
-            .on("G M", () -> Gerrit.display(PageLinks.toChangeQuery("status:merged")))
-            .on("G A", () -> Gerrit.display(PageLinks.toChangeQuery("status:abandoned")));
-    if (Gerrit.isSignedIn()) {
-      keyMap
-          .on("G I", () -> Gerrit.display(PageLinks.MINE))
-          .on("G C", () -> Gerrit.display(PageLinks.toChangeQuery("has:draft")))
-          .on("G W", () -> Gerrit.display(PageLinks.toChangeQuery("is:watched status:open")))
-          .on("G S", () -> Gerrit.display(PageLinks.toChangeQuery("is:starred")));
-    }
-
-    if (revision.get() != 0) {
-      cm.on("beforeSelectionChange", onSelectionChange(cm));
-      cm.on("gutterClick", onGutterClick(cm));
-      keyMap.on("C", getCommentManager().newDraftCallback(cm));
-    }
-    CodeMirror.normalizeKeyMap(keyMap); // Needed to for multi-stroke keymaps
-    cm.addKeyMap(keyMap);
-  }
-
-  void maybeRegisterRenderEntireFileKeyMap(CodeMirror cm) {
-    if (renderEntireFile()) {
-      cm.addKeyMap(RENDER_ENTIRE_FILE_KEYMAP);
-    }
-  }
-
-  private BeforeSelectionChangeHandler onSelectionChange(CodeMirror cm) {
-    return new BeforeSelectionChangeHandler() {
-      private InsertCommentBubble bubble;
-
-      @Override
-      public void handle(CodeMirror cm, Pos anchor, Pos head) {
-        if (anchor.equals(head)) {
-          if (bubble != null) {
-            bubble.setVisible(false);
-          }
-          return;
-        } else if (bubble == null) {
-          init(anchor);
-        } else {
-          bubble.setVisible(true);
-        }
-        bubble.position(cm.charCoords(head, "local"));
-      }
-
-      private void init(Pos anchor) {
-        bubble = new InsertCommentBubble(getCommentManager(), cm);
-        add(bubble);
-        cm.addWidget(anchor, bubble.getElement());
-      }
-    };
-  }
-
-  @Override
-  public void registerKeys() {
-    super.registerKeys();
-
-    keysNavigation.add(new UpToChangeCommand(project, revision, 0, 'u'));
-    keysNavigation.add(
-        new NoOpKeyCommand(0, 'j', PatchUtil.C.lineNext()),
-        new NoOpKeyCommand(0, 'k', PatchUtil.C.linePrev()));
-    keysNavigation.add(
-        new NoOpKeyCommand(0, 'n', PatchUtil.C.chunkNext()),
-        new NoOpKeyCommand(0, 'p', PatchUtil.C.chunkPrev()));
-    keysNavigation.add(
-        new NoOpKeyCommand(KeyCommand.M_SHIFT, 'n', PatchUtil.C.commentNext()),
-        new NoOpKeyCommand(KeyCommand.M_SHIFT, 'p', PatchUtil.C.commentPrev()));
-    keysNavigation.add(new NoOpKeyCommand(KeyCommand.M_CTRL, 'f', Gerrit.C.keySearch()));
-
-    keysAction = new KeyCommandSet(Gerrit.C.sectionActions());
-    keysAction.add(new NoOpKeyCommand(0, KeyCodes.KEY_ENTER, PatchUtil.C.expandComment()));
-    keysAction.add(new NoOpKeyCommand(0, 'o', PatchUtil.C.expandComment()));
-    keysAction.add(
-        new NoOpKeyCommand(KeyCommand.M_SHIFT, 'o', PatchUtil.C.expandAllCommentsOnCurrentLine()));
-    if (Gerrit.isSignedIn()) {
-      keysAction.add(
-          new KeyCommand(0, 'r', PatchUtil.C.toggleReviewed()) {
-            @Override
-            public void onKeyPress(KeyPressEvent event) {
-              header.toggleReviewed().run();
-            }
-          });
-      keysAction.add(
-          new NoOpKeyCommand(KeyCommand.M_CTRL | KeyCommand.M_ALT, 'e', Gerrit.C.keyEditor()));
-    }
-    keysAction.add(
-        new KeyCommand(KeyCommand.M_SHIFT, 'm', PatchUtil.C.markAsReviewedAndGoToNext()) {
-          @Override
-          public void onKeyPress(KeyPressEvent event) {
-            header.reviewedAndNext().run();
-          }
-        });
-    keysAction.add(
-        new KeyCommand(0, 'a', PatchUtil.C.openReply()) {
-          @Override
-          public void onKeyPress(KeyPressEvent event) {
-            upToChange(true).run();
-          }
-        });
-    keysAction.add(
-        new KeyCommand(0, ',', PatchUtil.C.showPreferences()) {
-          @Override
-          public void onKeyPress(KeyPressEvent event) {
-            prefsAction.show();
-          }
-        });
-    if (getIntraLineStatus() == DiffInfo.IntraLineStatus.OFF
-        || getIntraLineStatus() == DiffInfo.IntraLineStatus.OK) {
-      keysAction.add(
-          new KeyCommand(0, 'i', PatchUtil.C.toggleIntraline()) {
-            @Override
-            public void onKeyPress(KeyPressEvent event) {
-              toggleShowIntraline();
-            }
-          });
-    }
-
-    if (Gerrit.isSignedIn()) {
-      keysAction.add(new NoOpKeyCommand(0, 'c', PatchUtil.C.commentInsert()));
-      keysComment = new KeyCommandSet(PatchUtil.C.commentEditorSet());
-      keysComment.add(new NoOpKeyCommand(KeyCommand.M_CTRL, 's', PatchUtil.C.commentSaveDraft()));
-      keysComment.add(new NoOpKeyCommand(0, KeyCodes.KEY_ESCAPE, PatchUtil.C.commentCancelEdit()));
-    } else {
-      keysComment = null;
-    }
-  }
-
-  @Nullable
-  public Project.NameKey getProject() {
-    return project;
-  }
-
-  void registerHandlers() {
-    removeKeyHandlerRegistrations();
-    handlers.add(GlobalKey.add(this, keysAction));
-    handlers.add(GlobalKey.add(this, keysNavigation));
-    if (keysComment != null) {
-      handlers.add(GlobalKey.add(this, keysComment));
-    }
-    handlers.add(ShowHelpCommand.addFocusHandler(getFocusHandler()));
-  }
-
-  void setupSyntaxHighlighting() {
-    if (prefs.syntaxHighlighting() && fileSize.compareTo(FileSize.SMALL) > 0) {
-      Scheduler.get()
-          .scheduleFixedDelay(
-              new RepeatingCommand() {
-                @Override
-                public boolean execute() {
-                  if (prefs.syntaxHighlighting() && isAttached()) {
-                    setSyntaxHighlighting(prefs.syntaxHighlighting());
-                  }
-                  return false;
-                }
-              },
-              250);
-    }
-  }
-
-  abstract CodeMirror newCm(DiffInfo.FileMeta meta, String contents, Element parent);
-
-  void render(DiffInfo diff) {
-    header.setNoDiff(diff);
-    getChunkManager().render(diff);
-  }
-
-  void setShowLineNumbers(boolean b) {
-    if (b) {
-      getDiffTable().addStyleName(Resources.I.diffTableStyle().showLineNumbers());
-    } else {
-      getDiffTable().removeStyleName(Resources.I.diffTableStyle().showLineNumbers());
-    }
-  }
-
-  void setShowIntraline(boolean b) {
-    if (b && getIntraLineStatus() == DiffInfo.IntraLineStatus.OFF) {
-      reloadDiffInfo();
-    } else if (b) {
-      getDiffTable().removeStyleName(Resources.I.diffTableStyle().noIntraline());
-    } else {
-      getDiffTable().addStyleName(Resources.I.diffTableStyle().noIntraline());
-    }
-  }
-
-  private void toggleShowIntraline() {
-    prefs.intralineDifference(!Boolean.valueOf(prefs.intralineDifference()));
-    setShowIntraline(prefs.intralineDifference());
-    prefsAction.update();
-  }
-
-  abstract void setSyntaxHighlighting(boolean b);
-
-  void setContext(int context) {
-    operation(
-        () -> {
-          skipManager.removeAll();
-          skipManager.render(context, diff);
-          updateRenderEntireFile();
-        });
-  }
-
-  private int adjustCommitMessageLine(int line) {
-    /* When commit messages are shown in the diff screen they include
-      a header block that looks like this:
-
-      1 Parent:     deadbeef (Parent commit title)
-      2 Author:     A. U. Thor <author@example.com>
-      3 AuthorDate: 2015-02-27 19:20:52 +0900
-      4 Commit:     A. U. Thor <author@example.com>
-      5 CommitDate: 2015-02-27 19:20:52 +0900
-      6 [blank line]
-      7 Commit message title
-      8
-      9 Commit message body
-     10 ...
-     11 ...
-
-    If the commit is a merge commit, both parent commits are listed in the
-    first two lines instead of a 'Parent' line:
-
-      1 Merge Of:   deadbeef (Parent 1 commit title)
-      2             beefdead (Parent 2 commit title)
-
-    */
-
-    // Offset to compensate for header lines until the blank line
-    // after 'CommitDate'
-    int offset = 6;
-
-    // Adjust for merge commits, which have two parent lines
-    if (diff.textB().startsWith("Merge")) {
-      offset += 1;
-    }
-
-    // If the cursor is inside the header line, reset to the first line of the
-    // commit message. Otherwise if the cursor is on an actual line of the commit
-    // message, adjust the line number to compensate for the header lines, so the
-    // focus is on the correct line.
-    if (line <= offset) {
-      return 1;
-    }
-    return line - offset;
-  }
-
-  private Runnable openEditScreen(CodeMirror cm) {
-    return () -> {
-      LineHandle handle = cm.extras().activeLine();
-      int line = cm.getLineNumber(handle) + 1;
-      if (Patch.COMMIT_MSG.equals(path)) {
-        line = adjustCommitMessageLine(line);
-      }
-      String token = Dispatcher.toEditScreen(project, revision, path, line);
-      if (!Gerrit.isSignedIn()) {
-        Gerrit.doSignIn(token);
-      } else {
-        Gerrit.display(token);
-      }
-    };
-  }
-
-  void updateRenderEntireFile() {
-    boolean entireFile = renderEntireFile();
-    for (CodeMirror cm : getCms()) {
-      cm.removeKeyMap(RENDER_ENTIRE_FILE_KEYMAP);
-      if (entireFile) {
-        cm.addKeyMap(RENDER_ENTIRE_FILE_KEYMAP);
-      }
-      cm.setOption("viewportMargin", entireFile ? POSITIVE_INFINITY : 10);
-    }
-  }
-
-  void resizeCodeMirror() {
-    int height = header.getOffsetHeight() + getDiffTable().getHeaderHeight();
-    for (CodeMirror cm : getCms()) {
-      cm.adjustHeight(height);
-    }
-  }
-
-  abstract ChunkManager getChunkManager();
-
-  abstract CommentManager getCommentManager();
-
-  Change.Status getChangeStatus() {
-    return changeStatus;
-  }
-
-  int getStartLine() {
-    return startLine;
-  }
-
-  void setStartLine(int startLine) {
-    this.startLine = startLine;
-  }
-
-  DisplaySide getStartSide() {
-    return startSide;
-  }
-
-  void setStartSide(DisplaySide startSide) {
-    this.startSide = startSide;
-  }
-
-  DiffInfo getDiff() {
-    return diff;
-  }
-
-  FileSize getFileSize() {
-    return fileSize;
-  }
-
-  PreferencesAction getPrefsAction() {
-    return prefsAction;
-  }
-
-  void setPrefsAction(PreferencesAction prefsAction) {
-    this.prefsAction = prefsAction;
-  }
-
-  abstract void operation(Runnable apply);
-
-  private Runnable upToChange(boolean openReplyBox) {
-    return () -> {
-      CallbackGroup group = new CallbackGroup();
-      getCommentManager().saveAllDrafts(group);
-      group.done();
-      group.addListener(
-          new GerritCallback<Void>() {
-            @Override
-            public void onSuccess(Void result) {
-              String rev = String.valueOf(revision.get());
-              Gerrit.display(
-                  PageLinks.toChange(project, changeId, base.asString(), rev),
-                  new ChangeScreen(
-                      project, changeId, base, rev, openReplyBox, FileTable.Mode.REVIEW));
-            }
-          });
-    };
-  }
-
-  private Runnable maybePrevVimSearch(CodeMirror cm) {
-    return () -> {
-      if (cm.vim().hasSearchHighlight()) {
-        cm.vim().handleKey("N");
-      } else {
-        getCommentManager().commentNav(cm, Direction.NEXT).run();
-      }
-    };
-  }
-
-  private Runnable maybeNextVimSearch(CodeMirror cm) {
-    return () -> {
-      if (cm.vim().hasSearchHighlight()) {
-        cm.vim().handleKey("n");
-      } else {
-        getChunkManager().diffChunkNav(cm, Direction.NEXT).run();
-      }
-    };
-  }
-
-  Runnable maybeNextCmSearch(CodeMirror cm) {
-    return () -> {
-      if (cm.hasSearchHighlight()) {
-        cm.execCommand("findNext");
-      } else {
-        cm.execCommand("clearSearch");
-        getCommentManager().toggleOpenBox(cm).run();
-      }
-    };
-  }
-
-  boolean renderEntireFile() {
-    return prefs.renderEntireFile() && canRenderEntireFile(prefs);
-  }
-
-  boolean canRenderEntireFile(DiffPreferences prefs) {
-    // CodeMirror is too slow to layout an entire huge file.
-    return fileSize.compareTo(FileSize.HUGE) < 0
-        || (prefs.context() != WHOLE_FILE_CONTEXT && prefs.context() < 100);
-  }
-
-  DiffInfo.IntraLineStatus getIntraLineStatus() {
-    return diff.intralineStatus();
-  }
-
-  void setThemeStyles(boolean d) {
-    if (d) {
-      getDiffTable().addStyleName(Resources.I.diffTableStyle().dark());
-    } else {
-      getDiffTable().removeStyleName(Resources.I.diffTableStyle().dark());
-    }
-  }
-
-  void setShowTabs(boolean show) {
-    for (CodeMirror cm : getCms()) {
-      cm.extras().showTabs(show);
-    }
-  }
-
-  void setLineLength(int columns) {
-    for (CodeMirror cm : getCms()) {
-      cm.extras().lineLength(columns);
-    }
-  }
-
-  String getContentType(DiffInfo.FileMeta meta) {
-    if (prefs.syntaxHighlighting() && meta != null && meta.contentType() != null) {
-      ModeInfo m = ModeInfo.findMode(meta.contentType(), path);
-      return m != null ? m.mime() : null;
-    }
-    return null;
-  }
-
-  String getContentType() {
-    return getContentType(diff.metaB());
-  }
-
-  void injectMode(DiffInfo diffInfo, AsyncCallback<Void> cb) {
-    new ModeInjector()
-        .add(getContentType(diffInfo.metaA()))
-        .add(getContentType(diffInfo.metaB()))
-        .inject(cb);
-  }
-
-  abstract void setAutoHideDiffHeader(boolean hide);
-
-  void prefetchNextFile() {
-    String nextPath = header.getNextPath();
-    if (nextPath != null) {
-      DiffApi.diff(Project.NameKey.asStringOrNull(project), revision, nextPath)
-          .base(base.asPatchSetId())
-          .wholeFile()
-          .intraline(prefs.intralineDifference())
-          .ignoreWhitespace(prefs.ignoreWhitespace())
-          .get(
-              new AsyncCallback<DiffInfo>() {
-                @Override
-                public void onSuccess(DiffInfo info) {
-                  new ModeInjector()
-                      .add(getContentType(info.metaA()))
-                      .add(getContentType(info.metaB()))
-                      .inject(CallbackGroup.<Void>emptyCallback());
-                }
-
-                @Override
-                public void onFailure(Throwable caught) {}
-              });
-    }
-  }
-
-  void reloadDiffInfo() {
-    int id = ++reloadVersionId;
-    DiffApi.diff(Project.NameKey.asStringOrNull(project), revision, path)
-        .base(base.asPatchSetId())
-        .wholeFile()
-        .intraline(prefs.intralineDifference())
-        .ignoreWhitespace(prefs.ignoreWhitespace())
-        .get(
-            new GerritCallback<DiffInfo>() {
-              @Override
-              public void onSuccess(DiffInfo diffInfo) {
-                if (id == reloadVersionId && isAttached()) {
-                  diff = diffInfo;
-                  operation(
-                      () -> {
-                        skipManager.removeAll();
-                        getChunkManager().reset();
-                        getDiffTable().scrollbar.removeDiffAnnotations();
-                        setShowIntraline(prefs.intralineDifference());
-                        render(diff);
-                        skipManager.render(prefs.context(), diff);
-                      });
-                }
-              }
-            });
-  }
-
-  private static FileSize bucketFileSize(DiffInfo diff) {
-    FileMeta a = diff.metaA();
-    FileMeta b = diff.metaB();
-    FileSize[] sizes = FileSize.values();
-    for (int i = sizes.length - 1; 0 <= i; i--) {
-      FileSize s = sizes[i];
-      if ((a != null && s.lines <= a.lines()) || (b != null && s.lines <= b.lines())) {
-        return s;
-      }
-    }
-    return FileSize.SMALL;
-  }
-
-  abstract Runnable updateActiveLine(CodeMirror cm);
-
-  private GutterClickHandler onGutterClick(CodeMirror cm) {
-    return new GutterClickHandler() {
-      @Override
-      public void handle(
-          CodeMirror instance, int line, String gutterClass, NativeEvent clickEvent) {
-        if (Element.as(clickEvent.getEventTarget()).hasClassName(getLineNumberClassName())
-            && clickEvent.getButton() == NativeEvent.BUTTON_LEFT
-            && !clickEvent.getMetaKey()
-            && !clickEvent.getAltKey()
-            && !clickEvent.getCtrlKey()
-            && !clickEvent.getShiftKey()) {
-          cm.setCursor(Pos.create(line));
-          Scheduler.get()
-              .scheduleDeferred(
-                  new ScheduledCommand() {
-                    @Override
-                    public void execute() {
-                      getCommentManager().newDraftOnGutterClick(cm, gutterClass, line + 1);
-                    }
-                  });
-        }
-      }
-    };
-  }
-
-  abstract FocusHandler getFocusHandler();
-
-  abstract CodeMirror[] getCms();
-
-  abstract CodeMirror getCmFromSide(DisplaySide side);
-
-  abstract DiffTable getDiffTable();
-
-  abstract int getCmLine(int line, DisplaySide side);
-
-  abstract String getLineNumberClassName();
-
-  LineOnOtherInfo lineOnOther(DisplaySide side, int line) {
-    return getChunkManager().lineMapper.lineOnOther(side, line);
-  }
-
-  abstract ScreenLoadCallback<ConfigInfoCache.Entry> getScreenLoadCallback(
-      CommentsCollections comments);
-
-  abstract boolean isSideBySide();
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.css
deleted file mode 100644
index 7569cf5..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.css
+++ /dev/null
@@ -1,41 +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.
- */
-.range {
-  background-color: #ffd500 !important;
-}
-.rangeHighlight {
-  background-color: #ffff00 !important;
-}
-
-.fullscreen {
-  background-color: #f7f7f7;
-  border-bottom: 1px solid #ddd;
-}
-
-@external .diffHeader;
-.diffHeader {
-  font-size: 12px;
-  font-weight: bold;
-  color: #5252ad;
-}
-
-.diffHeader pre {
-  margin: 0 0 3px 0;
-}
-
-@external .dark, .noIntraline, .showLineNumbers;
-.dark {}
-.noIntraline {}
-.showLineNumbers {}
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
deleted file mode 100644
index a91f8e6..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
+++ /dev/null
@@ -1,184 +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.client.diff;
-
-import com.google.gerrit.client.DiffObject;
-import com.google.gerrit.client.account.DiffPreferences;
-import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
-import com.google.gerrit.reviewdb.client.Patch.ChangeType;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.core.client.JsArrayString;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.Style.Unit;
-import com.google.gwt.resources.client.CssResource;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.UIObject;
-import com.google.gwt.user.client.ui.Widget;
-import net.codemirror.lib.CodeMirror;
-
-/** Base class for SideBySideTable2 and UnifiedTable2 */
-abstract class DiffTable extends Composite {
-  static {
-    Resources.I.diffTableStyle().ensureInjected();
-  }
-
-  interface Style extends CssResource {
-    String fullscreen();
-
-    String dark();
-
-    String noIntraline();
-
-    String range();
-
-    String rangeHighlight();
-
-    String diffHeader();
-
-    String showLineNumbers();
-  }
-
-  @UiField Element patchSetNavRow;
-  @UiField Element patchSetNavCellA;
-  @UiField Element patchSetNavCellB;
-  @UiField Element diffHeaderRow;
-  @UiField Element diffHeaderText;
-  @UiField FlowPanel widgets;
-
-  @UiField(provided = true)
-  PatchSetSelectBox patchSetSelectBoxA;
-
-  @UiField(provided = true)
-  PatchSetSelectBox patchSetSelectBoxB;
-
-  private boolean header;
-  private ChangeType changeType;
-  Scrollbar scrollbar;
-
-  DiffTable(DiffScreen parent, DiffObject base, DiffObject revision, String path) {
-    patchSetSelectBoxA =
-        new PatchSetSelectBox(
-            parent,
-            DisplaySide.A,
-            parent.getProject(),
-            revision.asPatchSetId().getParentKey(),
-            base,
-            path);
-    patchSetSelectBoxB =
-        new PatchSetSelectBox(
-            parent,
-            DisplaySide.B,
-            parent.getProject(),
-            revision.asPatchSetId().getParentKey(),
-            revision,
-            path);
-    PatchSetSelectBox.link(patchSetSelectBoxA, patchSetSelectBoxB);
-
-    this.scrollbar = new Scrollbar(this);
-  }
-
-  abstract boolean isVisibleA();
-
-  void setHeaderVisible(boolean show) {
-    DiffScreen parent = getDiffScreen();
-    if (show != UIObject.isVisible(patchSetNavRow)) {
-      UIObject.setVisible(patchSetNavRow, show);
-      UIObject.setVisible(diffHeaderRow, show && header);
-      if (show) {
-        parent.header.removeStyleName(Resources.I.diffTableStyle().fullscreen());
-      } else {
-        parent.header.addStyleName(Resources.I.diffTableStyle().fullscreen());
-      }
-      parent.resizeCodeMirror();
-    }
-  }
-
-  abstract int getHeaderHeight();
-
-  ChangeType getChangeType() {
-    return changeType;
-  }
-
-  void setUpBlameIconA(CodeMirror cm, boolean isBase, PatchSet.Id rev, String path) {
-    patchSetSelectBoxA.setUpBlame(cm, isBase, rev, path);
-  }
-
-  void setUpBlameIconB(CodeMirror cm, PatchSet.Id rev, String path) {
-    patchSetSelectBoxB.setUpBlame(cm, false, rev, path);
-  }
-
-  void set(
-      DiffPreferences prefs,
-      JsArray<RevisionInfo> list,
-      int parents,
-      DiffInfo info,
-      boolean editExists,
-      boolean current,
-      boolean open,
-      boolean binary) {
-    this.changeType = info.changeType();
-    patchSetSelectBoxA.setUpPatchSetNav(
-        list, parents, info.metaA(), editExists, current, open, binary);
-    patchSetSelectBoxB.setUpPatchSetNav(
-        list, parents, info.metaB(), editExists, current, open, binary);
-
-    JsArrayString hdr = info.diffHeader();
-    if (hdr != null) {
-      StringBuilder b = new StringBuilder();
-      for (int i = 1; i < hdr.length(); i++) {
-        String s = hdr.get(i);
-        if (!info.binary()
-            && (s.startsWith("diff --git ")
-                || s.startsWith("index ")
-                || s.startsWith("+++ ")
-                || s.startsWith("--- "))) {
-          continue;
-        }
-        b.append(s).append('\n');
-      }
-
-      String hdrTxt = b.toString().trim();
-      header = !hdrTxt.isEmpty();
-      diffHeaderText.setInnerText(hdrTxt);
-      UIObject.setVisible(diffHeaderRow, header);
-    } else {
-      header = false;
-      UIObject.setVisible(diffHeaderRow, false);
-    }
-    setHideEmptyPane(prefs.hideEmptyPane());
-  }
-
-  abstract void setHideEmptyPane(boolean hide);
-
-  void refresh() {
-    if (header) {
-      CodeMirror cm = getDiffScreen().getCmFromSide(DisplaySide.A);
-      diffHeaderText.getStyle().setMarginLeft(cm.getGutterElement().getOffsetWidth(), Unit.PX);
-    }
-  }
-
-  void add(Widget widget) {
-    widgets.add(widget);
-  }
-
-  abstract DiffScreen getDiffScreen();
-
-  boolean hasHeader() {
-    return header;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Direction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Direction.java
deleted file mode 100644
index b1dd87e1..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Direction.java
+++ /dev/null
@@ -1,21 +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.client.diff;
-
-/** Direction of traversal in an ordered list. */
-public enum Direction {
-  PREV,
-  NEXT
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DisplaySide.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DisplaySide.java
deleted file mode 100644
index 6cee174..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DisplaySide.java
+++ /dev/null
@@ -1,25 +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.client.diff;
-
-/** Enum representing the side on a side-by-side view */
-public enum DisplaySide {
-  A,
-  B;
-
-  DisplaySide otherSide() {
-    return this == A ? B : A;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java
deleted file mode 100644
index 33d1ac4..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java
+++ /dev/null
@@ -1,466 +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.client.diff;
-
-import com.google.gerrit.client.FormatUtil;
-import com.google.gerrit.client.change.LocalComments;
-import com.google.gerrit.client.changes.CommentApi;
-import com.google.gerrit.client.changes.CommentInfo;
-import com.google.gerrit.client.rpc.CallbackGroup;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.client.ui.CommentLinkProcessor;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.Scheduler;
-import com.google.gwt.core.client.Scheduler.ScheduledCommand;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.event.dom.client.BlurEvent;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.DoubleClickEvent;
-import com.google.gwt.event.dom.client.DoubleClickHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyDownEvent;
-import com.google.gwt.event.dom.client.MouseMoveEvent;
-import com.google.gwt.event.dom.client.MouseMoveHandler;
-import com.google.gwt.event.dom.client.MouseUpEvent;
-import com.google.gwt.event.dom.client.MouseUpHandler;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.uibinder.client.UiHandler;
-import com.google.gwt.user.client.Timer;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.HTML;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwt.user.client.ui.UIObject;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtexpui.globalkey.client.NpTextArea;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-import net.codemirror.lib.CodeMirror;
-
-/** An HtmlPanel for displaying and editing a draft */
-class DraftBox extends CommentBox {
-  interface Binder extends UiBinder<HTMLPanel, DraftBox> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  private static final int INITIAL_LINES = 5;
-  private static final int MAX_LINES = 30;
-
-  private final CommentLinkProcessor linkProcessor;
-  private final PatchSet.Id psId;
-  @Nullable private final Project.NameKey project;
-  private final boolean expandAll;
-  private CommentInfo comment;
-  private PublishedBox replyToBox;
-  private Timer expandTimer;
-  private Timer resizeTimer;
-  private int editAreaHeight;
-  private boolean autoClosed;
-  private CallbackGroup pendingGroup;
-
-  @UiField Widget header;
-  @UiField Element summary;
-  @UiField Element date;
-
-  @UiField Element p_view;
-  @UiField HTML message;
-  @UiField Button edit;
-  @UiField Button discard1;
-
-  @UiField Element p_edit;
-  @UiField NpTextArea editArea;
-  @UiField Button save;
-  @UiField Button cancel;
-  @UiField Button discard2;
-
-  DraftBox(
-      CommentGroup group,
-      CommentLinkProcessor clp,
-      @Nullable Project.NameKey pj,
-      PatchSet.Id id,
-      CommentInfo info,
-      boolean expandAllComments) {
-    super(group, info.range());
-
-    linkProcessor = clp;
-    psId = id;
-    project = pj;
-    expandAll = expandAllComments;
-    initWidget(uiBinder.createAndBindUi(this));
-
-    expandTimer =
-        new Timer() {
-          @Override
-          public void run() {
-            expandText();
-          }
-        };
-    set(info);
-
-    header.addDomHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            if (!isEdit()) {
-              if (autoClosed && !isOpen()) {
-                setOpen(true);
-                setEdit(true);
-              } else {
-                setOpen(!isOpen());
-              }
-            }
-          }
-        },
-        ClickEvent.getType());
-
-    addDomHandler(
-        new DoubleClickHandler() {
-          @Override
-          public void onDoubleClick(DoubleClickEvent event) {
-            if (isEdit()) {
-              editArea.setFocus(true);
-            } else {
-              setOpen(true);
-              setEdit(true);
-            }
-          }
-        },
-        DoubleClickEvent.getType());
-
-    initResizeHandler();
-  }
-
-  private void set(CommentInfo info) {
-    autoClosed = !expandAll && info.message() != null && info.message().length() < 70;
-    date.setInnerText(FormatUtil.shortFormatDayTime(info.updated()));
-    if (info.message() != null) {
-      String msg = info.message().trim();
-      summary.setInnerText(msg);
-      message.setHTML(linkProcessor.apply(new SafeHtmlBuilder().append(msg).wikify()));
-    }
-    comment = info;
-  }
-
-  @Override
-  CommentInfo getCommentInfo() {
-    return comment;
-  }
-
-  @Override
-  boolean isOpen() {
-    return UIObject.isVisible(p_view);
-  }
-
-  @Override
-  void setOpen(boolean open) {
-    UIObject.setVisible(summary, !open);
-    UIObject.setVisible(p_view, open);
-    super.setOpen(open);
-  }
-
-  private void expandText() {
-    double cols = editArea.getCharacterWidth();
-    int rows = 2;
-    for (String line : editArea.getValue().split("\n")) {
-      rows += Math.ceil((1.0 + line.length()) / cols);
-    }
-    rows = Math.max(INITIAL_LINES, Math.min(rows, MAX_LINES));
-    if (editArea.getVisibleLines() != rows) {
-      editArea.setVisibleLines(rows);
-    }
-    editAreaHeight = editArea.getOffsetHeight();
-    getCommentGroup().resize();
-  }
-
-  boolean isEdit() {
-    return UIObject.isVisible(p_edit);
-  }
-
-  void setEdit(boolean edit) {
-    UIObject.setVisible(summary, false);
-    UIObject.setVisible(p_view, !edit);
-    UIObject.setVisible(p_edit, edit);
-
-    setRangeHighlight(edit);
-    if (edit) {
-      String msg = comment.message() != null ? comment.message() : "";
-      editArea.setValue(msg);
-      cancel.setVisible(!isNew());
-      expandText();
-      editAreaHeight = editArea.getOffsetHeight();
-
-      final int len = msg.length();
-      Scheduler.get()
-          .scheduleDeferred(
-              new ScheduledCommand() {
-                @Override
-                public void execute() {
-                  editArea.setFocus(true);
-                  if (len > 0) {
-                    editArea.setCursorPos(len);
-                  }
-                }
-              });
-    } else {
-      expandTimer.cancel();
-      resizeTimer.cancel();
-    }
-    getCommentManager().setUnsaved(this, edit);
-    getCommentGroup().resize();
-  }
-
-  PublishedBox getReplyToBox() {
-    return replyToBox;
-  }
-
-  void setReplyToBox(PublishedBox box) {
-    replyToBox = box;
-  }
-
-  @Override
-  protected void onUnload() {
-    expandTimer.cancel();
-    resizeTimer.cancel();
-    super.onUnload();
-  }
-
-  private void removeUI() {
-    if (replyToBox != null) {
-      replyToBox.unregisterReplyBox();
-    }
-
-    getCommentManager().setUnsaved(this, false);
-    setRangeHighlight(false);
-    clearRange();
-    getAnnotation().remove();
-    getCommentGroup().remove(this);
-    getCm().focus();
-  }
-
-  private void restoreSelection() {
-    if (getFromTo() != null && comment.inReplyTo() == null) {
-      getCm().setSelection(getFromTo().from(), getFromTo().to());
-    }
-  }
-
-  @UiHandler("message")
-  void onMessageClick(ClickEvent e) {
-    e.stopPropagation();
-  }
-
-  @UiHandler("message")
-  void onMessageDoubleClick(@SuppressWarnings("unused") DoubleClickEvent e) {
-    setEdit(true);
-  }
-
-  @UiHandler("edit")
-  void onEdit(ClickEvent e) {
-    e.stopPropagation();
-    setEdit(true);
-  }
-
-  @UiHandler("save")
-  void onSave(ClickEvent e) {
-    e.stopPropagation();
-    CallbackGroup group = new CallbackGroup();
-    save(group);
-    group.done();
-  }
-
-  void save(CallbackGroup group) {
-    if (pendingGroup != null) {
-      pendingGroup.addListener(group);
-      return;
-    }
-
-    String message = editArea.getValue().trim();
-    if (message.length() == 0) {
-      return;
-    }
-
-    CommentInfo input = CommentInfo.copy(comment);
-    input.message(message);
-    enableEdit(false);
-
-    pendingGroup = group;
-    final LocalComments lc = new LocalComments(project, psId);
-    GerritCallback<CommentInfo> cb =
-        new GerritCallback<CommentInfo>() {
-          @Override
-          public void onSuccess(CommentInfo result) {
-            enableEdit(true);
-            pendingGroup = null;
-            set(result);
-            setEdit(false);
-            if (autoClosed) {
-              setOpen(false);
-            }
-            getCommentManager().setUnsaved(DraftBox.this, false);
-          }
-
-          @Override
-          public void onFailure(Throwable e) {
-            enableEdit(true);
-            pendingGroup = null;
-            if (RestApi.isNotSignedIn(e)) {
-              CommentInfo saved = CommentInfo.copy(comment);
-              saved.message(editArea.getValue().trim());
-              lc.setInlineComment(saved);
-            }
-            super.onFailure(e);
-          }
-        };
-    if (input.id() == null) {
-      CommentApi.createDraft(Project.NameKey.asStringOrNull(project), psId, input, group.add(cb));
-    } else {
-      CommentApi.updateDraft(
-          Project.NameKey.asStringOrNull(project), psId, input.id(), input, group.add(cb));
-    }
-    CodeMirror cm = getCm();
-    cm.vim().handleKey("<Esc>");
-    cm.focus();
-  }
-
-  private void enableEdit(boolean on) {
-    editArea.setEnabled(on);
-    save.setEnabled(on);
-    cancel.setEnabled(on);
-    discard2.setEnabled(on);
-  }
-
-  @UiHandler("cancel")
-  void onCancel(ClickEvent e) {
-    e.stopPropagation();
-    if (isNew() && !isDirty()) {
-      removeUI();
-      restoreSelection();
-    } else {
-      setEdit(false);
-      if (autoClosed) {
-        setOpen(false);
-      }
-      getCm().focus();
-    }
-  }
-
-  @UiHandler({"discard1", "discard2"})
-  void onDiscard(ClickEvent e) {
-    e.stopPropagation();
-    if (isNew()) {
-      removeUI();
-      restoreSelection();
-    } else {
-      setEdit(false);
-      pendingGroup = new CallbackGroup();
-      CommentApi.deleteDraft(
-          Project.NameKey.asStringOrNull(project),
-          psId,
-          comment.id(),
-          pendingGroup.addFinal(
-              new GerritCallback<JavaScriptObject>() {
-                @Override
-                public void onSuccess(JavaScriptObject result) {
-                  pendingGroup = null;
-                  removeUI();
-                }
-              }));
-    }
-  }
-
-  @UiHandler("editArea")
-  void onKeyDown(KeyDownEvent e) {
-    resizeTimer.cancel();
-    if ((e.isControlKeyDown() || e.isMetaKeyDown()) && !e.isAltKeyDown() && !e.isShiftKeyDown()) {
-      switch (e.getNativeKeyCode()) {
-        case 's':
-        case 'S':
-          e.preventDefault();
-          CallbackGroup group = new CallbackGroup();
-          save(group);
-          group.done();
-          return;
-      }
-    } else if (e.getNativeKeyCode() == KeyCodes.KEY_ESCAPE && !isDirty()) {
-      if (isNew()) {
-        removeUI();
-        restoreSelection();
-        return;
-      }
-      setEdit(false);
-      if (autoClosed) {
-        setOpen(false);
-      }
-      getCm().focus();
-      return;
-    }
-    expandTimer.schedule(250);
-  }
-
-  @UiHandler("editArea")
-  void onBlur(@SuppressWarnings("unused") BlurEvent e) {
-    resizeTimer.cancel();
-  }
-
-  private void initResizeHandler() {
-    resizeTimer =
-        new Timer() {
-          @Override
-          public void run() {
-            getCommentGroup().resize();
-          }
-        };
-
-    addDomHandler(
-        new MouseMoveHandler() {
-          @Override
-          public void onMouseMove(MouseMoveEvent event) {
-            int h = editArea.getOffsetHeight();
-            if (isEdit() && h != editAreaHeight) {
-              getCommentGroup().resize();
-              resizeTimer.scheduleRepeating(50);
-              editAreaHeight = h;
-            }
-          }
-        },
-        MouseMoveEvent.getType());
-
-    addDomHandler(
-        new MouseUpHandler() {
-          @Override
-          public void onMouseUp(MouseUpEvent event) {
-            resizeTimer.cancel();
-            getCommentGroup().resize();
-          }
-        },
-        MouseUpEvent.getType());
-  }
-
-  private boolean isNew() {
-    return comment.id() == null;
-  }
-
-  private boolean isDirty() {
-    String msg = editArea.getValue().trim();
-    if (isNew()) {
-      return msg.length() > 0;
-    }
-    return !msg.equals(comment.message() != null ? comment.message().trim() : "");
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.ui.xml
deleted file mode 100644
index a363c06..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.ui.xml
+++ /dev/null
@@ -1,96 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder
-    xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:c='urn:import:com.google.gerrit.client'
-    xmlns:e='urn:import:com.google.gwtexpui.globalkey.client'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:with field='res' type='com.google.gerrit.client.diff.Resources'/>
-  <ui:style gss='false'>
-    .draft {
-      width: 45px;
-      text-align: center;
-      color: #fff;
-      background-color: #aaa;
-      -webkit-border-radius: 2px;
-    }
-    .editArea { max-width: 637px; }
-    button.button div {
-      width: 35px;
-    }
-    button.discard {
-      color: #d14836;
-      background-color: #d14836;
-      background-image: -webkit-linear-gradient(top, #d14836, #d14836);
-      position: absolute;
-      left: 150px;
-    }
-  </ui:style>
-
-  <g:HTMLPanel styleName='{res.style.commentBox}'>
-    <div class='{res.style.contents}'>
-      <g:HTMLPanel ui:field='header' styleName='{res.style.header}'>
-        <div class='{style.draft}'>Draft</div>
-        <div ui:field='summary' class='{res.style.summary}'/>
-        <div ui:field='date' class='{res.style.date}'/>
-      </g:HTMLPanel>
-      <div ui:field='p_view' aria-hidden='true' style='display: NONE'>
-        <g:HTML ui:field='message' styleName='{res.style.message}'/>
-        <div style='position: relative'>
-          <g:Button ui:field='edit'
-              title='Edit this draft comment'
-              styleName='{style.button}'>
-            <ui:attribute name='title'/>
-            <div><ui:msg>Edit</ui:msg></div>
-          </g:Button>
-          <g:Button ui:field='discard1'
-              title='Discard this draft comment'
-              styleName='{style.button}'
-              addStyleNames='{style.discard}'>
-            <ui:attribute name='title'/>
-            <div><ui:msg>Discard</ui:msg></div>
-          </g:Button>
-        </div>
-      </div>
-      <div ui:field='p_edit' aria-hidden='true' style='display: NONE'>
-        <e:NpTextArea ui:field='editArea'
-            characterWidth='60'
-            visibleLines='5'
-            spellCheck='true'
-            styleName='{style.editArea}'/>
-        <div style='position: relative'>
-          <g:Button ui:field='save'
-              title='Save this draft comment'
-              styleName='{style.button}'>
-            <ui:attribute name='title'/>
-            <div><ui:msg>Save</ui:msg></div>
-          </g:Button>
-          <g:Button ui:field='cancel' styleName='{style.button}'>
-            <div><ui:msg>Cancel</ui:msg></div>
-          </g:Button>
-          <g:Button ui:field='discard2'
-              title='Discard this draft comment'
-              styleName='{style.button}'
-              addStyleNames='{style.discard}'>
-            <ui:attribute name='title'/>
-            <div><ui:msg>Discard</ui:msg></div>
-          </g:Button>
-        </div>
-      </div>
-    </div>
-  </g:HTMLPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/EditIterator.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/EditIterator.java
deleted file mode 100644
index 4cf78c7..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/EditIterator.java
+++ /dev/null
@@ -1,71 +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.client.diff;
-
-import com.google.gwt.core.client.JsArrayString;
-import net.codemirror.lib.Pos;
-
-/** An iterator for intraline edits */
-class EditIterator {
-  private final JsArrayString lines;
-  private final int startLine;
-  private int line;
-  private int pos;
-
-  EditIterator(JsArrayString lineArray, int start) {
-    lines = lineArray;
-    startLine = start;
-  }
-
-  Pos advance(int numOfChar) {
-    numOfChar = adjustForNegativeDelta(numOfChar);
-
-    while (line < lines.length()) {
-      int len = lines.get(line).length() - pos + 1; // + 1 for LF
-      if (numOfChar < len) {
-        Pos at = Pos.create(startLine + line, numOfChar + pos);
-        pos += numOfChar;
-        return at;
-      }
-
-      numOfChar -= len;
-      line++;
-      pos = 0;
-
-      if (numOfChar == 0) {
-        return Pos.create(startLine + line, 0);
-      }
-    }
-
-    throw new IllegalStateException("EditIterator index out of bounds");
-  }
-
-  private int adjustForNegativeDelta(int n) {
-    while (n < 0) {
-      if (-n <= pos) {
-        pos += n;
-        return 0;
-      }
-
-      n += pos;
-      line--;
-      if (line < 0) {
-        throw new IllegalStateException("EditIterator index out of bounds");
-      }
-      pos = lines.get(line).length() + 1;
-    }
-    return n;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
deleted file mode 100644
index 7a97df1..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
+++ /dev/null
@@ -1,367 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT 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.client.diff;
-
-import com.google.gerrit.client.DiffObject;
-import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.account.DiffPreferences;
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ReviewInfo;
-import com.google.gerrit.client.changes.Util;
-import com.google.gerrit.client.diff.DiffInfo.Region;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.info.FileInfo;
-import com.google.gerrit.client.info.WebLinkInfo;
-import com.google.gerrit.client.patches.PatchUtil;
-import com.google.gerrit.client.rpc.CallbackGroup;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.client.ui.InlineHyperlink;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DiffView;
-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.reviewdb.client.Project;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.core.client.JsArrayString;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.Style.Visibility;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.uibinder.client.UiHandler;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwt.user.client.ui.UIObject;
-import com.google.gwtexpui.globalkey.client.KeyCommand;
-import com.google.gwtexpui.globalkey.client.KeyCommandSet;
-import com.google.gwtexpui.safehtml.client.SafeHtml;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-import java.util.List;
-
-public class Header extends Composite {
-  interface Binder extends UiBinder<HTMLPanel, Header> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  static {
-    Resources.I.style().ensureInjected();
-  }
-
-  private enum ReviewedState {
-    AUTO_REVIEW,
-    LOADED
-  }
-
-  @UiField CheckBox reviewed;
-  @UiField Element project;
-  @UiField Element filePath;
-  @UiField Element fileNumber;
-  @UiField Element fileCount;
-
-  @UiField Element noDiff;
-  @UiField FlowPanel linkPanel;
-
-  @UiField InlineHyperlink prev;
-  @UiField InlineHyperlink up;
-  @UiField InlineHyperlink next;
-  @UiField Image preferences;
-
-  private final KeyCommandSet keys;
-  @Nullable private final Project.NameKey projectKey;
-  private final DiffObject base;
-  private final PatchSet.Id patchSetId;
-  private final String path;
-  private final DiffView diffScreenType;
-  private final DiffPreferences prefs;
-  private boolean hasPrev;
-  private boolean hasNext;
-  private String nextPath;
-  private JsArray<FileInfo> files;
-  private PreferencesAction prefsAction;
-  private ReviewedState reviewedState;
-
-  Header(
-      KeyCommandSet keys,
-      @Nullable Project.NameKey project,
-      DiffObject base,
-      DiffObject patchSetId,
-      String path,
-      DiffView diffSreenType,
-      DiffPreferences prefs) {
-    initWidget(uiBinder.createAndBindUi(this));
-    this.keys = keys;
-    this.projectKey = project;
-    this.base = base;
-    this.patchSetId = patchSetId.asPatchSetId();
-    this.path = path;
-    this.diffScreenType = diffSreenType;
-    this.prefs = prefs;
-
-    if (!Gerrit.isSignedIn()) {
-      reviewed.getElement().getStyle().setVisibility(Visibility.HIDDEN);
-    }
-    SafeHtml.setInnerHTML(filePath, formatPath(path));
-    up.setTargetHistoryToken(
-        PageLinks.toChange(
-            project,
-            patchSetId.asPatchSetId().getParentKey(),
-            base.asString(),
-            patchSetId.asPatchSetId().getId()));
-  }
-
-  public static SafeHtml formatPath(String path) {
-    SafeHtmlBuilder b = new SafeHtmlBuilder();
-    if (Patch.COMMIT_MSG.equals(path)) {
-      return b.append(Util.C.commitMessage());
-    } else if (Patch.MERGE_LIST.equals(path)) {
-      return b.append(Util.C.mergeList());
-    }
-
-    int s = path.lastIndexOf('/') + 1;
-    b.append(path.substring(0, s));
-    b.openElement("b");
-    b.append(path.substring(s));
-    b.closeElement("b");
-    return b;
-  }
-
-  private int findCurrentFileIndex(JsArray<FileInfo> files) {
-    int currIndex = 0;
-    for (int i = 0; i < files.length(); i++) {
-      if (path.equals(files.get(i).path())) {
-        currIndex = i;
-        break;
-      }
-    }
-    return currIndex;
-  }
-
-  @Override
-  protected void onLoad() {
-    DiffApi.list(
-        Project.NameKey.asStringOrNull(projectKey),
-        patchSetId,
-        base.asPatchSetId(),
-        new GerritCallback<NativeMap<FileInfo>>() {
-          @Override
-          public void onSuccess(NativeMap<FileInfo> result) {
-            files = result.values();
-            FileInfo.sortFileInfoByPath(files);
-            fileNumber.setInnerText(
-                Integer.toString(Natives.asList(files).indexOf(result.get(path)) + 1));
-            fileCount.setInnerText(Integer.toString(files.length()));
-          }
-        });
-
-    if (Gerrit.isSignedIn()) {
-      ChangeApi.revision(Project.NameKey.asStringOrNull(projectKey), patchSetId)
-          .view("files")
-          .addParameterTrue("reviewed")
-          .get(
-              new AsyncCallback<JsArrayString>() {
-                @Override
-                public void onSuccess(JsArrayString result) {
-                  boolean b = Natives.asList(result).contains(path);
-                  reviewed.setValue(b, false);
-                  if (!b && reviewedState == ReviewedState.AUTO_REVIEW) {
-                    postAutoReviewed();
-                  }
-                  reviewedState = ReviewedState.LOADED;
-                }
-
-                @Override
-                public void onFailure(Throwable caught) {}
-              });
-    }
-  }
-
-  void autoReview() {
-    if (reviewedState == ReviewedState.LOADED && !reviewed.getValue()) {
-      postAutoReviewed();
-    } else {
-      reviewedState = ReviewedState.AUTO_REVIEW;
-    }
-  }
-
-  void setChangeInfo(ChangeInfo info) {
-    project.setInnerText(info.project());
-  }
-
-  void init(PreferencesAction pa, List<InlineHyperlink> links, List<WebLinkInfo> webLinks) {
-    prefsAction = pa;
-    prefsAction.setPartner(preferences);
-
-    for (InlineHyperlink link : links) {
-      linkPanel.add(link);
-    }
-    for (WebLinkInfo webLink : webLinks) {
-      linkPanel.add(webLink.toAnchor());
-    }
-  }
-
-  @UiHandler("reviewed")
-  void onValueChange(ValueChangeEvent<Boolean> event) {
-    if (event.getValue()) {
-      reviewed().put(CallbackGroup.<ReviewInfo>emptyCallback());
-    } else {
-      reviewed().delete(CallbackGroup.<ReviewInfo>emptyCallback());
-    }
-  }
-
-  private void postAutoReviewed() {
-    reviewed()
-        .background()
-        .put(
-            new AsyncCallback<ReviewInfo>() {
-              @Override
-              public void onSuccess(ReviewInfo result) {
-                reviewed.setValue(true, false);
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {}
-            });
-  }
-
-  private RestApi reviewed() {
-    return ChangeApi.revision(Project.NameKey.asStringOrNull(projectKey), patchSetId)
-        .view("files")
-        .id(path)
-        .view("reviewed");
-  }
-
-  @UiHandler("preferences")
-  void onPreferences(@SuppressWarnings("unused") ClickEvent e) {
-    prefsAction.show();
-  }
-
-  private String url(FileInfo info) {
-    return diffScreenType == DiffView.UNIFIED_DIFF
-        ? Dispatcher.toUnified(projectKey, base, patchSetId, info.path())
-        : Dispatcher.toSideBySide(projectKey, base, patchSetId, info.path());
-  }
-
-  private KeyCommand setupNav(InlineHyperlink link, char key, String help, FileInfo info) {
-    if (info != null) {
-      final String url = url(info);
-      link.setTargetHistoryToken(url);
-      link.setTitle(
-          PatchUtil.M.fileNameWithShortcutKey(
-              FileInfo.getFileName(info.path()), Character.toString(key)));
-      KeyCommand k =
-          new KeyCommand(0, key, help) {
-            @Override
-            public void onKeyPress(KeyPressEvent event) {
-              Gerrit.display(url);
-            }
-          };
-      keys.add(k);
-      if (link == prev) {
-        hasPrev = true;
-      } else {
-        hasNext = true;
-      }
-      return k;
-    }
-    link.getElement().getStyle().setVisibility(Visibility.HIDDEN);
-    keys.add(new UpToChangeCommand(projectKey, patchSetId, 0, key));
-    return null;
-  }
-
-  private boolean shouldSkipFile(FileInfo curr, CommentsCollections comments) {
-    return prefs.skipDeleted() && ChangeType.DELETED.matches(curr.status())
-        || prefs.skipUnchanged() && ChangeType.RENAMED.matches(curr.status())
-        || prefs.skipUncommented() && !comments.hasCommentForPath(curr.path());
-  }
-
-  void setupPrevNextFiles(CommentsCollections comments) {
-    FileInfo prevInfo = null;
-    FileInfo nextInfo = null;
-    int currIndex = findCurrentFileIndex(files);
-    for (int i = currIndex - 1; i >= 0; i--) {
-      FileInfo curr = files.get(i);
-      if (shouldSkipFile(curr, comments)) {
-        continue;
-      }
-      prevInfo = curr;
-      break;
-    }
-    for (int i = currIndex + 1; i < files.length(); i++) {
-      FileInfo curr = files.get(i);
-      if (shouldSkipFile(curr, comments)) {
-        continue;
-      }
-      nextInfo = curr;
-      break;
-    }
-    KeyCommand p = setupNav(prev, '[', PatchUtil.C.previousFileHelp(), prevInfo);
-    KeyCommand n = setupNav(next, ']', PatchUtil.C.nextFileHelp(), nextInfo);
-    if (p != null && n != null) {
-      keys.pair(p, n);
-    }
-    nextPath = nextInfo != null ? nextInfo.path() : null;
-  }
-
-  Runnable toggleReviewed() {
-    return () -> reviewed.setValue(!reviewed.getValue(), true);
-  }
-
-  Runnable navigate(Direction dir) {
-    switch (dir) {
-      case PREV:
-        return () -> (hasPrev ? prev : up).go();
-      case NEXT:
-        return () -> (hasNext ? next : up).go();
-      default:
-        return () -> {};
-    }
-  }
-
-  Runnable reviewedAndNext() {
-    return () -> {
-      if (Gerrit.isSignedIn()) {
-        reviewed.setValue(true, true);
-      }
-      navigate(Direction.NEXT).run();
-    };
-  }
-
-  String getNextPath() {
-    return nextPath;
-  }
-
-  void setNoDiff(DiffInfo diff) {
-    if (diff.binary()) {
-      UIObject.setVisible(noDiff, false); // Don't bother showing "No Differences"
-    } else {
-      JsArray<Region> regions = diff.content();
-      boolean b = regions.length() == 0 || (regions.length() == 1 && regions.get(0).ab() != null);
-      UIObject.setVisible(noDiff, b);
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.ui.xml
deleted file mode 100644
index 39eb6cb..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.ui.xml
+++ /dev/null
@@ -1,95 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder
-    xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'
-    xmlns:x='urn:import:com.google.gerrit.client.ui'>
-  <ui:with field='ico' type='com.google.gerrit.client.GerritResources'/>
-  <ui:with field='res' type='com.google.gerrit.client.diff.Resources'/>
-  <ui:style gss='false'>
-  .header {
-    position: relative;
-    height: 16px;
-    line-height: 16px;
-  }
-  .reviewed input {
-    margin: 0;
-    padding: 0;
-    vertical-align: middle;
-  }
-  .path {
-    white-space: nowrap;
-  }
-  .fileCount {
-    white-space: nowrap;
-    position: relative;
-    bottom: 4px;
-  }
-  .navigation {
-    position: absolute;
-    top: 0;
-    right: 10px;
-    height: 16px;
-    line-height: 16px;
-  }
-  .nodiff {
-    white-space: nowrap;
-    color: #B00000;
-    vertical-align: top;
-    font-weight: bold;
-    margin-right: 1em;
-    float: left;
-  }
-  .linkPanel {
-    float: left;
-  }
-  .linkPanel img {
-    padding-right: 3px;
-  }
-  .preferences {
-    cursor: pointer;
-  }
-  </ui:style>
-  <g:HTMLPanel styleName='{style.header}'>
-    <g:CheckBox ui:field='reviewed'
-        styleName='{style.reviewed}'
-        title='Mark file as reviewed (Shortcut: r)'>
-      <ui:attribute name='title'/>
-    </g:CheckBox>
-    <span class='{style.path}'><span ui:field='project'/> / <span ui:field='filePath'/></span>
-    <div class='{style.navigation}'>
-      <span ui:field='noDiff' class='{style.nodiff}'><ui:msg>No Differences</ui:msg></span>
-      <g:FlowPanel ui:field='linkPanel' styleName='{style.linkPanel}'/>
-      <span class='{style.fileCount}'>
-        <ui:msg>File <span ui:field='fileNumber'/> of <span ui:field='fileCount'/></ui:msg>
-      </span>
-      <x:InlineHyperlink ui:field='prev' styleName='{res.style.goPrev}'/>
-      <x:InlineHyperlink ui:field='up'
-          styleName='{res.style.goUp}'
-          title='Up to change (Shortcut: u)'>
-        <ui:attribute name='title'/>
-      </x:InlineHyperlink>
-      <x:InlineHyperlink ui:field='next' styleName='{res.style.goNext}'/>
-      <g:Image ui:field='preferences'
-           styleName='{style.preferences}'
-           resource='{ico.gear}'
-           title='Diff preferences (Shortcut: ,)'>
-         <ui:attribute name='title'/>
-      </g:Image>
-    </div>
-  </g:HTMLPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/InsertCommentBubble.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/InsertCommentBubble.java
deleted file mode 100644
index f8eab91..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/InsertCommentBubble.java
+++ /dev/null
@@ -1,62 +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.client.diff;
-
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.dom.client.Style;
-import com.google.gwt.dom.client.Style.Unit;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwt.user.client.ui.Image;
-import net.codemirror.lib.CodeMirror;
-import net.codemirror.lib.Rect;
-
-/** Bubble displayed near a selected region to create a comment. */
-class InsertCommentBubble extends Composite {
-  interface Binder extends UiBinder<HTMLPanel, InsertCommentBubble> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  @UiField Image icon;
-
-  InsertCommentBubble(CommentManager commentManager, CodeMirror cm) {
-    initWidget(uiBinder.createAndBindUi(this));
-    addDomHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            setVisible(false);
-            commentManager.newDraftCallback(cm).run();
-          }
-        },
-        ClickEvent.getType());
-  }
-
-  void position(Rect r) {
-    Style s = getElement().getStyle();
-    int top = (int) (r.top() - (getOffsetHeight() - 8));
-    if (top < 0) {
-      s.setTop(-3, Unit.PX);
-      s.setLeft(r.right() + 2, Unit.PX);
-    } else {
-      s.setTop(top, Unit.PX);
-      s.setLeft((int) (r.right() - 14), Unit.PX);
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/InsertCommentBubble.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/InsertCommentBubble.ui.xml
deleted file mode 100644
index 6a18c4d..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/InsertCommentBubble.ui.xml
+++ /dev/null
@@ -1,51 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder
-    xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:with field='res' type='com.google.gerrit.client.GerritResources'/>
-  <ui:style gss='false'>
-    .bubble {
-      z-index: 150;
-      white-space: nowrap;
-      line-height: 16px;
-      cursor: pointer;
-    }
-    .message {
-      background: #fff1a8;
-      padding-left: 5px;
-      padding-right: 5px;
-      border-radius: 5px;
-      border: 1px solid #aaa;
-      font-family: sans-serif;
-      font-size: smaller;
-      font-style: italic;
-      vertical-align: top;
-    }
-    .message b {
-      vertical-align: top;
-    }
-  </ui:style>
-  <g:HTMLPanel styleName='{style.bubble}'>
-    <g:Image ui:field='icon'
-        styleName=''
-        resource='{res.draftComments}'
-        title='Create a new inline comment'>
-      <ui:attribute name='title'/>
-    </g:Image><span class='{style.message}'><ui:msg>press <b>c</b> to comment</ui:msg></span>
-  </g:HTMLPanel>
-</ui:UiBinder>
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/LineMapper.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/LineMapper.java
deleted file mode 100644
index fc83a14..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/LineMapper.java
+++ /dev/null
@@ -1,225 +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.client.diff;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Objects;
-
-/** Helper class to handle calculations involving line gaps. */
-class LineMapper {
-  private int lineA;
-  private int lineB;
-  private List<LineGap> lineMapAtoB;
-  private List<LineGap> lineMapBtoA;
-
-  LineMapper() {
-    reset();
-  }
-
-  void reset() {
-    lineA = 0;
-    lineB = 0;
-    lineMapAtoB = new ArrayList<>();
-    lineMapBtoA = new ArrayList<>();
-  }
-
-  int getLineA() {
-    return lineA;
-  }
-
-  int getLineB() {
-    return lineB;
-  }
-
-  void appendCommon(int numLines) {
-    lineA += numLines;
-    lineB += numLines;
-  }
-
-  void appendReplace(int aLen, int bLen) {
-    appendCommon(Math.min(aLen, bLen));
-    if (aLen < bLen) { // Edit with insertion
-      appendInsert(bLen - aLen);
-    } else if (aLen > bLen) { // Edit with deletion
-      appendDelete(aLen - bLen);
-    }
-  }
-
-  void appendInsert(int numLines) {
-    int origLineB = lineB;
-    lineB += numLines;
-    int bAheadOfA = lineB - lineA;
-    lineMapAtoB.add(new LineGap(lineA, -1, bAheadOfA));
-    lineMapBtoA.add(new LineGap(origLineB, lineB - 1, -bAheadOfA));
-  }
-
-  void appendDelete(int numLines) {
-    int origLineA = lineA;
-    lineA += numLines;
-    int aAheadOfB = lineA - lineB;
-    lineMapAtoB.add(new LineGap(origLineA, lineA - 1, -aAheadOfB));
-    lineMapBtoA.add(new LineGap(lineB, -1, aAheadOfB));
-  }
-
-  /**
-   * Helper method to retrieve the line number on the other side.
-   *
-   * <p>Given a line number on one side, performs a binary search in the lineMap to find the
-   * corresponding LineGap record.
-   *
-   * <p>A LineGap records gap information from the start of an actual gap up to the start of the
-   * next gap. In the following example, lineMapAtoB will have LineGap: {start: 1, end: -1, delta:
-   * 3} (end set to -1 to represent a dummy gap of length zero. The binary search only looks at
-   * start so setting it to -1 has no effect here.) lineMapBtoA will have LineGap: {start: 1, end:
-   * 3, delta: -3} These LineGaps control lines between 1 and 5.
-   *
-   * <p>The "delta" is computed as the number to add on our side to get the line number on the other
-   * side given a line after the actual gap, so the result will be (line + delta). All lines within
-   * the actual gap (1 to 3) are considered corresponding to the last line above the region on the
-   * other side, which is 0 in this case. For these lines, we do (end + delta).
-   *
-   * <p>For example, to get the line number on the left corresponding to 1 on the right
-   * (lineOnOther(REVISION, 1)), the method looks up in lineMapBtoA, finds the "delta" to be -3, and
-   * returns 3 + (-3) = 0 since 1 falls in the actual gap. On the other hand, the line corresponding
-   * to 5 on the right will be 5 + (-3) = 2, since 5 is in the region after the gap (but still
-   * controlled by the current LineGap).
-   *
-   * <p>PARENT REVISION 0 | 0 - | 1 \ \ - | 2 | Actual insertion gap | - | 3 / | Region controlled
-   * by one LineGap 1 | 4 <- delta = 4 - 1 = 3 | 2 | 5 / - | 6 ...
-   */
-  LineOnOtherInfo lineOnOther(DisplaySide mySide, int line) {
-    List<LineGap> lineGaps = gapList(mySide);
-    // Create a dummy LineGap for the search.
-    int ret = Collections.binarySearch(lineGaps, new LineGap(line));
-    if (ret == -1) {
-      return new LineOnOtherInfo(line, true);
-    }
-    LineGap lookup = lineGaps.get(0 <= ret ? ret : -ret - 2);
-    int start = lookup.start;
-    int end = lookup.end;
-    int delta = lookup.delta;
-    if (start <= line && line <= end && end != -1) { // Line falls within gap
-      return new LineOnOtherInfo(end + delta, false);
-    }
-    // Line after gap
-    return new LineOnOtherInfo(line + delta, true);
-  }
-
-  AlignedPair align(DisplaySide mySide, int line) {
-    List<LineGap> gaps = gapList(mySide);
-    int idx = Collections.binarySearch(gaps, new LineGap(line));
-    if (idx == -1) {
-      return new AlignedPair(line, line);
-    }
-
-    LineGap g = gaps.get(0 <= idx ? idx : -idx - 2);
-    if (g.start <= line && line <= g.end && g.end != -1) {
-      if (0 < g.start) {
-        // Line falls within this gap, use alignment before.
-        return new AlignedPair(g.start - 1, g.end + g.delta);
-      }
-      return new AlignedPair(g.end, g.end + g.delta + 1);
-    }
-    return new AlignedPair(line, line + g.delta);
-  }
-
-  private List<LineGap> gapList(DisplaySide mySide) {
-    return mySide == DisplaySide.A ? lineMapAtoB : lineMapBtoA;
-  }
-
-  static class AlignedPair {
-    final int src;
-    final int dst;
-
-    AlignedPair(int s, int d) {
-      src = s;
-      dst = d;
-    }
-  }
-
-  /**
-   * @field line The line number on the other side.
-   * @field aligned Whether the two lines are at the same height when displayed.
-   */
-  static class LineOnOtherInfo {
-    private int line;
-    private boolean aligned;
-
-    LineOnOtherInfo(int line, boolean aligned) {
-      this.line = line;
-      this.aligned = aligned;
-    }
-
-    int getLine() {
-      return line;
-    }
-
-    boolean isAligned() {
-      return aligned;
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-      if (obj instanceof LineOnOtherInfo) {
-        LineOnOtherInfo other = (LineOnOtherInfo) obj;
-        return aligned == other.aligned && line == other.line;
-      }
-      return false;
-    }
-
-    @Override
-    public int hashCode() {
-      return Objects.hash(line, aligned);
-    }
-
-    @Override
-    public String toString() {
-      return line + " " + aligned;
-    }
-  }
-
-  /**
-   * Helper class to record line gap info and assist in calculation of line number on the other
-   * side.
-   *
-   * <p>For a mapping from A to B, where A is the side with an insertion:
-   *
-   * @field start The start line of the insertion in A.
-   * @field end The exclusive end line of the insertion in A.
-   * @field delta The offset added to A to get the line number in B calculated from end.
-   */
-  private static class LineGap implements Comparable<LineGap> {
-    private final int start;
-    private final int end;
-    private final int delta;
-
-    private LineGap(int start, int end, int delta) {
-      this.start = start;
-      this.end = end;
-      this.delta = delta;
-    }
-
-    private LineGap(int line) {
-      this(line, 0, 0);
-    }
-
-    @Override
-    public int compareTo(LineGap o) {
-      return start - o.start;
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/NoOpKeyCommand.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/NoOpKeyCommand.java
deleted file mode 100644
index 584232d..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/NoOpKeyCommand.java
+++ /dev/null
@@ -1,28 +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.client.diff;
-
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwtexpui.globalkey.client.KeyCommand;
-
-/** A KeyCommand that does nothing, used to display a help message */
-class NoOpKeyCommand extends KeyCommand {
-  NoOpKeyCommand(int mask, int key, String help) {
-    super(mask, key, help);
-  }
-
-  @Override
-  public void onKeyPress(KeyPressEvent event) {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java
deleted file mode 100644
index 292773c..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java
+++ /dev/null
@@ -1,240 +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.client.diff;
-
-import com.google.gerrit.client.DiffObject;
-import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.blame.BlameInfo;
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.Util;
-import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
-import com.google.gerrit.client.info.WebLinkInfo;
-import com.google.gerrit.client.patches.PatchUtil;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.ui.InlineHyperlink;
-import com.google.gerrit.common.Nullable;
-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.gwt.core.client.GWT;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.resources.client.CssResource;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.uibinder.client.UiHandler;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwt.user.client.ui.ImageResourceRenderer;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtorm.client.KeyUtil;
-import java.util.List;
-import net.codemirror.lib.CodeMirror;
-
-/** HTMLPanel to select among patch sets */
-class PatchSetSelectBox extends Composite {
-  interface Binder extends UiBinder<HTMLPanel, PatchSetSelectBox> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  interface BoxStyle extends CssResource {
-    String selected();
-  }
-
-  @UiField Image icon;
-  @UiField HTMLPanel linkPanel;
-  @UiField BoxStyle style;
-
-  @Nullable private final Project.NameKey project;
-  private final Change.Id changeId;
-
-  private DiffScreen parent;
-  private DisplaySide side;
-  private boolean sideA;
-  private String path;
-  private PatchSet.Id revision;
-  private DiffObject idActive;
-  private PatchSetSelectBox other;
-
-  PatchSetSelectBox(
-      DiffScreen parent,
-      DisplaySide side,
-      @Nullable Project.NameKey project,
-      Change.Id changeId,
-      DiffObject diffObject,
-      String path) {
-    initWidget(uiBinder.createAndBindUi(this));
-    icon.setTitle(PatchUtil.C.addFileCommentToolTip());
-    icon.addStyleName(Gerrit.RESOURCES.css().link());
-
-    this.parent = parent;
-    this.side = side;
-    this.sideA = side == DisplaySide.A;
-    this.project = project;
-    this.changeId = changeId;
-    this.revision = diffObject.asPatchSetId();
-    this.idActive = diffObject;
-    this.path = path;
-  }
-
-  void setUpPatchSetNav(
-      JsArray<RevisionInfo> list,
-      int parents,
-      DiffInfo.FileMeta meta,
-      boolean editExists,
-      boolean current,
-      boolean open,
-      boolean binary) {
-    InlineHyperlink selectedLink = null;
-    if (sideA) {
-      if (parents <= 1) {
-        InlineHyperlink link = createLink(PatchUtil.C.patchBase(), DiffObject.base());
-        linkPanel.add(link);
-        selectedLink = link;
-      } else {
-        for (int i = parents; i > 0; i--) {
-          PatchSet.Id id = new PatchSet.Id(changeId, -i);
-          InlineHyperlink link = createLink(Util.M.diffBaseParent(i), DiffObject.patchSet(id));
-          linkPanel.add(link);
-          if (revision != null && id.equals(revision)) {
-            selectedLink = link;
-          }
-        }
-        InlineHyperlink link = createLink(Util.C.autoMerge(), DiffObject.autoMerge());
-        linkPanel.add(link);
-        if (selectedLink == null) {
-          selectedLink = link;
-        }
-      }
-    }
-    for (int i = 0; i < list.length(); i++) {
-      RevisionInfo r = list.get(i);
-      InlineHyperlink link =
-          createLink(r.id(), DiffObject.patchSet(new PatchSet.Id(changeId, r._number())));
-      linkPanel.add(link);
-      if (revision != null && r.id().equals(revision.getId())) {
-        selectedLink = link;
-      }
-    }
-    if (selectedLink != null) {
-      selectedLink.setStyleName(style.selected());
-    }
-
-    if (meta == null) {
-      return;
-    }
-    if (!Patch.isMagic(path)) {
-      linkPanel.add(createDownloadLink());
-    }
-    if (!binary && open && !idActive.isBaseOrAutoMerge() && Gerrit.isSignedIn()) {
-      if ((editExists && idActive.isEdit()) || (!editExists && current)) {
-        linkPanel.add(createEditIcon());
-      }
-    }
-    List<WebLinkInfo> webLinks = Natives.asList(meta.webLinks());
-    if (webLinks != null) {
-      for (WebLinkInfo webLink : webLinks) {
-        linkPanel.add(webLink.toAnchor());
-      }
-    }
-  }
-
-  void setUpBlame(final CodeMirror cm, boolean isBase, PatchSet.Id rev, String path) {
-    if (!Patch.isMagic(path) && Gerrit.isSignedIn() && Gerrit.info().change().allowBlame()) {
-      Anchor blameIcon = createBlameIcon();
-      blameIcon.addClickHandler(
-          new ClickHandler() {
-            @Override
-            public void onClick(ClickEvent clickEvent) {
-              if (cm.extras().getBlameInfo() != null) {
-                cm.extras().toggleAnnotation();
-              } else {
-                ChangeApi.blame(Project.NameKey.asStringOrNull(project), rev, path, isBase)
-                    .get(
-                        new GerritCallback<JsArray<BlameInfo>>() {
-
-                          @Override
-                          public void onSuccess(JsArray<BlameInfo> lines) {
-                            cm.extras().toggleAnnotation(lines);
-                          }
-                        });
-              }
-            }
-          });
-      linkPanel.add(blameIcon);
-    }
-  }
-
-  private Widget createEditIcon() {
-    PatchSet.Id id =
-        idActive.isBaseOrAutoMerge() ? other.idActive.asPatchSetId() : idActive.asPatchSetId();
-    Anchor anchor =
-        new Anchor(
-            new ImageResourceRenderer().render(Gerrit.RESOURCES.edit()),
-            "#" + Dispatcher.toEditScreen(project, id, path));
-    anchor.setTitle(PatchUtil.C.edit());
-    return anchor;
-  }
-
-  private Anchor createBlameIcon() {
-    Anchor anchor = new Anchor(new ImageResourceRenderer().render(Gerrit.RESOURCES.blame()));
-    anchor.setTitle(PatchUtil.C.blame());
-    return anchor;
-  }
-
-  static void link(PatchSetSelectBox a, PatchSetSelectBox b) {
-    a.other = b;
-    b.other = a;
-  }
-
-  private InlineHyperlink createLink(String label, DiffObject id) {
-    assert other != null;
-    if (sideA) {
-      assert !other.idActive.isBaseOrAutoMerge();
-    }
-    DiffObject diffBase = sideA ? id : other.idActive;
-    DiffObject revision = sideA ? other.idActive : id;
-
-    return new InlineHyperlink(
-        label,
-        parent.isSideBySide()
-            ? Dispatcher.toSideBySide(project, diffBase, revision.asPatchSetId(), path)
-            : Dispatcher.toUnified(project, diffBase, revision.asPatchSetId(), path));
-  }
-
-  private Anchor createDownloadLink() {
-    DiffObject diffObject = idActive.isBaseOrAutoMerge() ? other.idActive : idActive;
-    String sideURL = idActive.isBaseOrAutoMerge() ? "1" : "0";
-    String base = GWT.getHostPageBaseURL() + "cat/";
-    Anchor anchor =
-        new Anchor(
-            new ImageResourceRenderer().render(Gerrit.RESOURCES.downloadIcon()),
-            base + KeyUtil.encode(diffObject.asPatchSetId() + "," + path) + "^" + sideURL);
-    anchor.setTitle(PatchUtil.C.download());
-    return anchor;
-  }
-
-  @UiHandler("icon")
-  void onIconClick(@SuppressWarnings("unused") ClickEvent e) {
-    parent.getCmFromSide(side).scrollToY(0);
-    parent.getCommentManager().insertNewDraft(side, 0);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.ui.xml
deleted file mode 100644
index 6e526ec..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.ui.xml
+++ /dev/null
@@ -1,74 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder
-    xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:with field='res' type='com.google.gerrit.client.GerritResources'/>
-  <ui:with field='patchConstants'
-      type='com.google.gerrit.client.patches.PatchConstants'/>
-  <ui:style gss='false' type='com.google.gerrit.client.diff.PatchSetSelectBox.BoxStyle'>
-    @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
-    @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
-    .table {
-      width: 100%;
-    }
-    .linkCell {
-      text-align: center;
-      font-size: 12px;
-      white-space: normal;
-      font-family: sans-serif;
-      font-weight: bold;
-    }
-    .linkCell div {
-      padding-left: 3px;
-      padding-right: 3px;
-      vertical-align: middle;
-      display: inline-block;
-    }
-    .linkCell a {
-      padding-left: 3px;
-      padding-right: 3px;
-      text-decoration: none;
-      vertical-align: middle;
-      display: inline-block;
-    }
-    .selected {
-      font-weight: bold;
-      background-color: selectionColor;
-    }
-    .hidden {
-      visibility: hidden;
-    }
-    .iconCell {
-      width: 16px;
-    }
-  </ui:style>
-  <g:HTMLPanel>
-    <table class='{style.table}'>
-      <td class='{style.iconCell}'>
-        <g:Image ui:field='icon' resource='{res.addFileComment}'/>
-      </td>
-      <td class='{style.linkCell}'>
-        <g:HTMLPanel ui:field='linkPanel'>
-          <g:Label>
-            <ui:text from='{patchConstants.patchSet}'/>
-          </g:Label>
-        </g:HTMLPanel>
-      </td>
-    </table>
-  </g:HTMLPanel>
-</ui:UiBinder>
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesAction.java
deleted file mode 100644
index 2d4a4c4..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesAction.java
+++ /dev/null
@@ -1,86 +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.client.diff;
-
-import com.google.gerrit.client.account.DiffPreferences;
-import com.google.gwt.event.logical.shared.CloseEvent;
-import com.google.gwt.event.logical.shared.CloseHandler;
-import com.google.gwt.user.client.ui.PopupPanel;
-import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;
-import com.google.gwt.user.client.ui.Widget;
-
-class PreferencesAction {
-  private final DiffScreen view;
-  private final DiffPreferences prefs;
-  private PopupPanel popup;
-  private PreferencesBox current;
-  private Widget partner;
-
-  PreferencesAction(DiffScreen view, DiffPreferences prefs) {
-    this.view = view;
-    this.prefs = prefs;
-  }
-
-  void update() {
-    if (current != null) {
-      current.set(prefs);
-    }
-  }
-
-  void show() {
-    if (popup != null) {
-      // Already open? Close the dialog.
-      hide();
-      return;
-    }
-
-    current = new PreferencesBox(view);
-    current.set(prefs);
-
-    popup = new PopupPanel(true, false);
-    popup.setStyleName(current.style.dialog());
-    popup.add(current);
-    popup.addAutoHidePartner(partner.getElement());
-    popup.addCloseHandler(
-        new CloseHandler<PopupPanel>() {
-          @Override
-          public void onClose(CloseEvent<PopupPanel> event) {
-            view.getCmFromSide(DisplaySide.B).focus();
-            popup = null;
-            current = null;
-          }
-        });
-    popup.setPopupPositionAndShow(
-        new PositionCallback() {
-          @Override
-          public void setPosition(int offsetWidth, int offsetHeight) {
-            popup.setPopupPosition(300, 120);
-          }
-        });
-    current.setFocus(true);
-  }
-
-  void hide() {
-    if (popup != null) {
-      popup.hide();
-      popup = null;
-      current = null;
-    }
-  }
-
-  void setPartner(Widget w) {
-    partner = w;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java
deleted file mode 100644
index ed4ac25..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java
+++ /dev/null
@@ -1,644 +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.client.diff;
-
-import static com.google.gerrit.extensions.client.DiffPreferencesInfo.DEFAULT_CONTEXT;
-import static com.google.gerrit.extensions.client.DiffPreferencesInfo.WHOLE_FILE_CONTEXT;
-import static com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace.IGNORE_ALL;
-import static com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace.IGNORE_LEADING_AND_TRAILING;
-import static com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace.IGNORE_NONE;
-import static com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace.IGNORE_TRAILING;
-import static com.google.gwt.event.dom.client.KeyCodes.KEY_ESCAPE;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.account.AccountApi;
-import com.google.gerrit.client.account.DiffPreferences;
-import com.google.gerrit.client.patches.PatchUtil;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.ui.NpIntTextBox;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
-import com.google.gerrit.extensions.client.Theme;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.Patch.ChangeType;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.Style.Visibility;
-import com.google.gwt.event.dom.client.ChangeEvent;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.KeyDownEvent;
-import com.google.gwt.event.dom.client.KeyDownHandler;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.resources.client.CssResource;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.uibinder.client.UiHandler;
-import com.google.gwt.user.client.Timer;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwt.user.client.ui.ListBox;
-import com.google.gwt.user.client.ui.PopupPanel;
-import com.google.gwt.user.client.ui.ToggleButton;
-import com.google.gwt.user.client.ui.UIObject;
-import java.util.Objects;
-import net.codemirror.lib.CodeMirror;
-import net.codemirror.mode.ModeInfo;
-import net.codemirror.mode.ModeInjector;
-import net.codemirror.theme.ThemeLoader;
-
-/** Displays current diff preferences. */
-public class PreferencesBox extends Composite {
-  interface Binder extends UiBinder<HTMLPanel, PreferencesBox> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  public interface Style extends CssResource {
-    String dialog();
-  }
-
-  private final DiffScreen view;
-  private DiffPreferences prefs;
-  private int contextLastValue;
-  private Timer updateContextTimer;
-
-  @UiField Style style;
-  @UiField Element header;
-  @UiField Anchor close;
-  @UiField ListBox ignoreWhitespace;
-  @UiField NpIntTextBox tabWidth;
-  @UiField NpIntTextBox lineLength;
-  @UiField NpIntTextBox context;
-  @UiField NpIntTextBox cursorBlinkRate;
-  @UiField CheckBox contextEntireFile;
-  @UiField ToggleButton intralineDifference;
-  @UiField ToggleButton syntaxHighlighting;
-  @UiField ToggleButton whitespaceErrors;
-  @UiField ToggleButton showTabs;
-  @UiField ToggleButton lineNumbers;
-  @UiField Element leftSideLabel;
-  @UiField ToggleButton leftSide;
-  @UiField ToggleButton emptyPane;
-  @UiField ToggleButton topMenu;
-  @UiField ToggleButton autoHideDiffTableHeader;
-  @UiField ToggleButton manualReview;
-  @UiField ToggleButton expandAllComments;
-  @UiField ToggleButton renderEntireFile;
-  @UiField ToggleButton matchBrackets;
-  @UiField ToggleButton lineWrapping;
-  @UiField ToggleButton skipDeleted;
-  @UiField ToggleButton skipUnchanged;
-  @UiField ToggleButton skipUncommented;
-  @UiField ListBox theme;
-  @UiField Element modeLabel;
-  @UiField ListBox mode;
-  @UiField Button apply;
-  @UiField Button save;
-
-  public PreferencesBox(DiffScreen view) {
-    this.view = view;
-
-    initWidget(uiBinder.createAndBindUi(this));
-    initIgnoreWhitespace();
-    initTheme();
-
-    if (view != null) {
-      initMode();
-    } else {
-      UIObject.setVisible(header, false);
-      apply.getElement().getStyle().setVisibility(Visibility.HIDDEN);
-    }
-  }
-
-  @Override
-  public void onLoad() {
-    super.onLoad();
-
-    save.setVisible(Gerrit.isSignedIn());
-
-    if (view != null) {
-      addDomHandler(
-          new KeyDownHandler() {
-            @Override
-            public void onKeyDown(KeyDownEvent event) {
-              if (event.getNativeKeyCode() == KEY_ESCAPE || event.getNativeKeyCode() == ',') {
-                close();
-              }
-            }
-          },
-          KeyDownEvent.getType());
-
-      updateContextTimer =
-          new Timer() {
-            @Override
-            public void run() {
-              if (prefs.context() == WHOLE_FILE_CONTEXT) {
-                contextEntireFile.setValue(true);
-              }
-              if (view.canRenderEntireFile(prefs)) {
-                renderEntireFile.setEnabled(true);
-                renderEntireFile.setValue(prefs.renderEntireFile());
-              } else {
-                renderEntireFile.setValue(false);
-                renderEntireFile.setEnabled(false);
-              }
-              view.setContext(prefs.context());
-            }
-          };
-    }
-  }
-
-  public Style getStyle() {
-    return style;
-  }
-
-  public void set(DiffPreferences prefs) {
-    this.prefs = prefs;
-
-    setIgnoreWhitespace(prefs.ignoreWhitespace());
-    tabWidth.setIntValue(prefs.tabSize());
-    if (view != null && Patch.COMMIT_MSG.equals(view.path)) {
-      lineLength.setEnabled(false);
-      lineLength.setIntValue(72);
-    } else {
-      lineLength.setEnabled(true);
-      lineLength.setIntValue(prefs.lineLength());
-    }
-    cursorBlinkRate.setIntValue(prefs.cursorBlinkRate());
-    syntaxHighlighting.setValue(prefs.syntaxHighlighting());
-    whitespaceErrors.setValue(prefs.showWhitespaceErrors());
-    showTabs.setValue(prefs.showTabs());
-    lineNumbers.setValue(prefs.showLineNumbers());
-    emptyPane.setValue(!prefs.hideEmptyPane());
-    if (view != null) {
-      leftSide.setValue(view.getDiffTable().isVisibleA());
-      leftSide.setEnabled(
-          !(prefs.hideEmptyPane() && view.getDiffTable().getChangeType() == ChangeType.ADDED));
-    } else {
-      UIObject.setVisible(leftSideLabel, false);
-      leftSide.setVisible(false);
-    }
-    topMenu.setValue(!prefs.hideTopMenu());
-    autoHideDiffTableHeader.setValue(!prefs.autoHideDiffTableHeader());
-    manualReview.setValue(prefs.manualReview());
-    expandAllComments.setValue(prefs.expandAllComments());
-    matchBrackets.setValue(prefs.matchBrackets());
-    lineWrapping.setValue(prefs.lineWrapping());
-    skipDeleted.setValue(!prefs.skipDeleted());
-    skipUnchanged.setValue(!prefs.skipUnchanged());
-    skipUncommented.setValue(!prefs.skipUncommented());
-    setTheme(prefs.theme());
-
-    if (view == null || view.canRenderEntireFile(prefs)) {
-      renderEntireFile.setValue(prefs.renderEntireFile());
-      renderEntireFile.setEnabled(true);
-    } else {
-      renderEntireFile.setValue(false);
-      renderEntireFile.setEnabled(false);
-    }
-
-    if (view != null) {
-      mode.setEnabled(prefs.syntaxHighlighting());
-      if (prefs.syntaxHighlighting()) {
-        setMode(view.getCmFromSide(DisplaySide.B).getStringOption("mode"));
-      }
-    } else {
-      UIObject.setVisible(modeLabel, false);
-      mode.setVisible(false);
-    }
-
-    if (view != null) {
-      switch (view.getIntraLineStatus()) {
-        case OFF:
-        case OK:
-          intralineDifference.setValue(prefs.intralineDifference());
-          break;
-
-        case TIMEOUT:
-        case FAILURE:
-          intralineDifference.setValue(false);
-          intralineDifference.setEnabled(false);
-          break;
-      }
-    } else {
-      intralineDifference.setValue(prefs.intralineDifference());
-    }
-
-    if (prefs.context() == WHOLE_FILE_CONTEXT) {
-      contextLastValue = DEFAULT_CONTEXT;
-      context.setText("");
-      contextEntireFile.setValue(true);
-    } else {
-      context.setIntValue(prefs.context());
-      contextEntireFile.setValue(false);
-    }
-  }
-
-  @UiHandler("ignoreWhitespace")
-  void onIgnoreWhitespace(@SuppressWarnings("unused") ChangeEvent e) {
-    prefs.ignoreWhitespace(
-        Whitespace.valueOf(ignoreWhitespace.getValue(ignoreWhitespace.getSelectedIndex())));
-    if (view != null) {
-      view.reloadDiffInfo();
-    }
-  }
-
-  @UiHandler("intralineDifference")
-  void onIntralineDifference(ValueChangeEvent<Boolean> e) {
-    prefs.intralineDifference(Boolean.valueOf(e.getValue()));
-    if (view != null) {
-      view.setShowIntraline(prefs.intralineDifference());
-    }
-  }
-
-  @UiHandler("context")
-  void onContextKey(KeyPressEvent e) {
-    if (contextEntireFile.getValue()) {
-      char c = e.getCharCode();
-      if ('0' <= c && c <= '9') {
-        contextEntireFile.setValue(false);
-      }
-    }
-  }
-
-  @UiHandler("context")
-  void onContext(ValueChangeEvent<String> e) {
-    String v = e.getValue();
-    int c;
-    if (v != null && v.length() > 0) {
-      c = Math.min(Math.max(0, Integer.parseInt(v)), 32767);
-      contextEntireFile.setValue(false);
-    } else if (v == null || v.isEmpty()) {
-      c = WHOLE_FILE_CONTEXT;
-    } else {
-      return;
-    }
-    prefs.context(c);
-    if (view != null) {
-      updateContextTimer.schedule(200);
-    }
-  }
-
-  @UiHandler("contextEntireFile")
-  void onContextEntireFile(ValueChangeEvent<Boolean> e) {
-    // If a click arrives too fast after onContext applied an update
-    // the user committed the context line update by clicking on the
-    // whole file checkmark. Drop this event, but transfer focus.
-    if (e.getValue()) {
-      contextLastValue = context.getIntValue();
-      context.setText("");
-      prefs.context(WHOLE_FILE_CONTEXT);
-    } else {
-      prefs.context(contextLastValue > 0 ? contextLastValue : DEFAULT_CONTEXT);
-      context.setIntValue(prefs.context());
-      context.setFocus(true);
-      context.setSelectionRange(0, context.getText().length());
-    }
-    if (view != null) {
-      updateContextTimer.schedule(200);
-    }
-  }
-
-  @UiHandler("tabWidth")
-  void onTabWidth(ValueChangeEvent<String> e) {
-    String v = e.getValue();
-    if (v != null && v.length() > 0) {
-      prefs.tabSize(Math.max(1, Integer.parseInt(v)));
-      if (view != null) {
-        view.operation(
-            () -> {
-              int size = prefs.tabSize();
-              for (CodeMirror cm : view.getCms()) {
-                cm.setOption("tabSize", size);
-              }
-            });
-      }
-    }
-  }
-
-  @UiHandler("lineLength")
-  void onLineLength(ValueChangeEvent<String> e) {
-    String v = e.getValue();
-    if (v != null && v.length() > 0) {
-      prefs.lineLength(Math.max(1, Integer.parseInt(v)));
-      if (view != null) {
-        view.operation(() -> view.setLineLength(prefs.lineLength()));
-      }
-    }
-  }
-
-  @UiHandler("expandAllComments")
-  void onExpandAllComments(ValueChangeEvent<Boolean> e) {
-    prefs.expandAllComments(e.getValue());
-    if (view != null) {
-      view.getCommentManager().setExpandAllComments(prefs.expandAllComments());
-    }
-  }
-
-  @UiHandler("cursorBlinkRate")
-  void onCursoBlinkRate(ValueChangeEvent<String> e) {
-    String v = e.getValue();
-    if (v != null && v.length() > 0) {
-      // A negative value hides the cursor entirely:
-      // don't let user shoot himself in the foot.
-      prefs.cursorBlinkRate(Math.max(0, Integer.parseInt(v)));
-      view.getCmFromSide(DisplaySide.A).setOption("cursorBlinkRate", prefs.cursorBlinkRate());
-      view.getCmFromSide(DisplaySide.B).setOption("cursorBlinkRate", prefs.cursorBlinkRate());
-    }
-  }
-
-  @UiHandler("showTabs")
-  void onShowTabs(ValueChangeEvent<Boolean> e) {
-    prefs.showTabs(e.getValue());
-    if (view != null) {
-      view.setShowTabs(prefs.showTabs());
-    }
-  }
-
-  @UiHandler("lineNumbers")
-  void onLineNumbers(ValueChangeEvent<Boolean> e) {
-    prefs.showLineNumbers(e.getValue());
-    if (view != null) {
-      view.setShowLineNumbers(prefs.showLineNumbers());
-    }
-  }
-
-  @UiHandler("leftSide")
-  void onLeftSide(ValueChangeEvent<Boolean> e) {
-    if (view.getDiffTable() instanceof SideBySideTable) {
-      ((SideBySideTable) view.getDiffTable()).setVisibleA(e.getValue());
-    }
-  }
-
-  @UiHandler("emptyPane")
-  void onHideEmptyPane(ValueChangeEvent<Boolean> e) {
-    prefs.hideEmptyPane(!e.getValue());
-    if (view != null) {
-      view.getDiffTable().setHideEmptyPane(prefs.hideEmptyPane());
-      if (prefs.hideEmptyPane()) {
-        if (view.getDiffTable().getChangeType() == ChangeType.ADDED) {
-          leftSide.setValue(false);
-          leftSide.setEnabled(false);
-        }
-      } else {
-        leftSide.setValue(view.getDiffTable().isVisibleA());
-        leftSide.setEnabled(true);
-      }
-    }
-  }
-
-  @UiHandler("topMenu")
-  void onTopMenu(ValueChangeEvent<Boolean> e) {
-    prefs.hideTopMenu(!e.getValue());
-    if (view != null) {
-      Gerrit.setHeaderVisible(!prefs.hideTopMenu());
-      view.resizeCodeMirror();
-    }
-  }
-
-  @UiHandler("autoHideDiffTableHeader")
-  void onAutoHideDiffTableHeader(ValueChangeEvent<Boolean> e) {
-    prefs.autoHideDiffTableHeader(!e.getValue());
-    if (view != null) {
-      view.setAutoHideDiffHeader(!e.getValue());
-    }
-  }
-
-  @UiHandler("manualReview")
-  void onManualReview(ValueChangeEvent<Boolean> e) {
-    prefs.manualReview(e.getValue());
-  }
-
-  @UiHandler("syntaxHighlighting")
-  void onSyntaxHighlighting(ValueChangeEvent<Boolean> e) {
-    prefs.syntaxHighlighting(e.getValue());
-    if (view != null) {
-      mode.setEnabled(prefs.syntaxHighlighting());
-      if (prefs.syntaxHighlighting()) {
-        setMode(view.getContentType());
-      }
-      view.setSyntaxHighlighting(prefs.syntaxHighlighting());
-    }
-  }
-
-  @UiHandler("mode")
-  void onMode(@SuppressWarnings("unused") ChangeEvent e) {
-    String mode = getSelectedMode();
-    prefs.syntaxHighlighting(true);
-    syntaxHighlighting.setValue(true, false);
-    new ModeInjector()
-        .add(mode)
-        .inject(
-            new GerritCallback<Void>() {
-              @Override
-              public void onSuccess(Void result) {
-                if (prefs.syntaxHighlighting()
-                    && Objects.equals(mode, getSelectedMode())
-                    && view.isAttached()) {
-                  view.operation(
-                      () -> {
-                        view.getCmFromSide(DisplaySide.A).setOption("mode", mode);
-                        view.getCmFromSide(DisplaySide.B).setOption("mode", mode);
-                      });
-                }
-              }
-            });
-  }
-
-  private String getSelectedMode() {
-    String m = mode.getValue(mode.getSelectedIndex());
-    return m != null && !m.isEmpty() ? m : null;
-  }
-
-  @UiHandler("whitespaceErrors")
-  void onWhitespaceErrors(ValueChangeEvent<Boolean> e) {
-    prefs.showWhitespaceErrors(e.getValue());
-    if (view != null) {
-      view.operation(
-          () -> {
-            boolean s = prefs.showWhitespaceErrors();
-            for (CodeMirror cm : view.getCms()) {
-              cm.setOption("showTrailingSpace", s);
-            }
-          });
-    }
-  }
-
-  @UiHandler("renderEntireFile")
-  void onRenderEntireFile(ValueChangeEvent<Boolean> e) {
-    prefs.renderEntireFile(e.getValue());
-    if (view != null) {
-      view.updateRenderEntireFile();
-    }
-  }
-
-  @UiHandler("matchBrackets")
-  void onMatchBrackets(ValueChangeEvent<Boolean> e) {
-    prefs.matchBrackets(e.getValue());
-    view.getCmFromSide(DisplaySide.A).setOption("matchBrackets", prefs.matchBrackets());
-    view.getCmFromSide(DisplaySide.B).setOption("matchBrackets", prefs.matchBrackets());
-  }
-
-  @UiHandler("lineWrapping")
-  void onLineWrapping(ValueChangeEvent<Boolean> e) {
-    prefs.lineWrapping(e.getValue());
-    view.getCmFromSide(DisplaySide.A).setOption("lineWrapping", prefs.lineWrapping());
-    view.getCmFromSide(DisplaySide.B).setOption("lineWrapping", prefs.lineWrapping());
-  }
-
-  @UiHandler("skipDeleted")
-  void onSkipDeleted(ValueChangeEvent<Boolean> e) {
-    prefs.skipDeleted(!e.getValue());
-    // TODO: Update the navigation links on the current DiffScreen
-  }
-
-  @UiHandler("skipUnchanged")
-  void onSkipUnchanged(ValueChangeEvent<Boolean> e) {
-    prefs.skipUnchanged(!e.getValue());
-    // TODO: Update the navigation links on the current DiffScreen
-  }
-
-  @UiHandler("skipUncommented")
-  void onSkipUncommented(ValueChangeEvent<Boolean> e) {
-    prefs.skipUncommented(!e.getValue());
-    // TODO: Update the navigation links on the current DiffScreen
-  }
-
-  @UiHandler("theme")
-  void onTheme(@SuppressWarnings("unused") ChangeEvent e) {
-    Theme newTheme = getSelectedTheme();
-    prefs.theme(newTheme);
-    if (view != null) {
-      ThemeLoader.loadTheme(
-          newTheme,
-          new GerritCallback<Void>() {
-            @Override
-            public void onSuccess(Void result) {
-              view.operation(
-                  () -> {
-                    if (getSelectedTheme() == newTheme && isAttached()) {
-                      String t = newTheme.name().toLowerCase();
-                      view.getCmFromSide(DisplaySide.A).setOption("theme", t);
-                      view.getCmFromSide(DisplaySide.B).setOption("theme", t);
-                      view.setThemeStyles(newTheme.isDark());
-                    }
-                  });
-            }
-          });
-    }
-  }
-
-  private Theme getSelectedTheme() {
-    return Theme.valueOf(theme.getValue(theme.getSelectedIndex()));
-  }
-
-  @UiHandler("apply")
-  void onApply(@SuppressWarnings("unused") ClickEvent e) {
-    close();
-  }
-
-  @UiHandler("save")
-  void onSave(@SuppressWarnings("unused") ClickEvent e) {
-    AccountApi.putDiffPreferences(
-        prefs,
-        new GerritCallback<DiffPreferences>() {
-          @Override
-          public void onSuccess(DiffPreferences result) {
-            DiffPreferencesInfo p = new DiffPreferencesInfo();
-            result.copyTo(p);
-            Gerrit.setDiffPreferences(p);
-          }
-        });
-    if (view != null) {
-      close();
-    }
-  }
-
-  @UiHandler("close")
-  void onClose(ClickEvent e) {
-    e.preventDefault();
-    close();
-  }
-
-  void setFocus(boolean focus) {
-    ignoreWhitespace.setFocus(focus);
-  }
-
-  private void close() {
-    ((PopupPanel) getParent()).hide();
-  }
-
-  private void setIgnoreWhitespace(Whitespace v) {
-    String name = v != null ? v.name() : IGNORE_NONE.name();
-    for (int i = 0; i < ignoreWhitespace.getItemCount(); i++) {
-      if (ignoreWhitespace.getValue(i).equals(name)) {
-        ignoreWhitespace.setSelectedIndex(i);
-        return;
-      }
-    }
-    ignoreWhitespace.setSelectedIndex(0);
-  }
-
-  private void initIgnoreWhitespace() {
-    ignoreWhitespace.addItem(PatchUtil.C.whitespaceIGNORE_NONE(), IGNORE_NONE.name());
-    ignoreWhitespace.addItem(PatchUtil.C.whitespaceIGNORE_TRAILING(), IGNORE_TRAILING.name());
-    ignoreWhitespace.addItem(
-        PatchUtil.C.whitespaceIGNORE_LEADING_AND_TRAILING(), IGNORE_LEADING_AND_TRAILING.name());
-    ignoreWhitespace.addItem(PatchUtil.C.whitespaceIGNORE_ALL(), IGNORE_ALL.name());
-  }
-
-  private void initMode() {
-    mode.addItem("", "");
-    for (ModeInfo m : Natives.asList(ModeInfo.all())) {
-      mode.addItem(m.name(), m.mime());
-    }
-  }
-
-  private void setMode(String modeType) {
-    if (modeType != null && !modeType.isEmpty()) {
-      ModeInfo m = ModeInfo.findModeByMIME(modeType);
-      if (m != null) {
-        for (int i = 0; i < mode.getItemCount(); i++) {
-          if (mode.getValue(i).equals(m.mime())) {
-            mode.setSelectedIndex(i);
-            return;
-          }
-        }
-      }
-    }
-    mode.setSelectedIndex(0);
-  }
-
-  private void setTheme(Theme v) {
-    String name = v != null ? v.name() : Theme.DEFAULT.name();
-    for (int i = 0; i < theme.getItemCount(); i++) {
-      if (theme.getValue(i).equals(name)) {
-        theme.setSelectedIndex(i);
-        return;
-      }
-    }
-    theme.setSelectedIndex(0);
-  }
-
-  private void initTheme() {
-    for (Theme t : Theme.values()) {
-      theme.addItem(t.name().toLowerCase(), t.name());
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.ui.xml
deleted file mode 100644
index 4465d63..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.ui.xml
+++ /dev/null
@@ -1,341 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'
-    xmlns:x='urn:import:com.google.gerrit.client.ui'>
-  <ui:style gss='false' type='com.google.gerrit.client.diff.PreferencesBox.Style'>
-    @external .gwt-TextBox;
-    @external .gwt-ToggleButton .html-face;
-    @external .gwt-ToggleButton-up;
-    @external .gwt-ToggleButton-up-hovering;
-    @external .gwt-ToggleButton-up-disabled;
-    @external .gwt-ToggleButton-down;
-    @external .gwt-ToggleButton-down-hovering;
-    @external .gwt-ToggleButton-down-disabled;
-
-    .dialog {
-      background: rgba(0, 0, 0, 0.85) none repeat scroll 0 50%;
-      color: #ffffff;
-      font-family: arial,sans-serif;
-      font-weight: bold;
-      overflow: auto !important;
-      bottom: 0;
-      text-align: left;
-      text-shadow: 1px 1px 7px #000000;
-      min-width: 300px;
-      z-index: 200;
-      border-radius: 10px;
-    }
-
-    @if user.agent safari {
-      .dialog {
-        \-webkit-border-radius: 10px;
-      }
-    }
-
-    @if user.agent gecko1_8 {
-      .dialog {
-        \-moz-border-radius: 10px;
-      }
-    }
-
-    .box { margin: 10px; }
-    .box .gwt-TextBox { padding: 0; }
-    .context { vertical-align: bottom; }
-
-    .table tr { min-height: 23px; }
-    .table th,
-    .table td {
-      white-space: nowrap;
-      color: #ffffff;
-    }
-    .table th {
-      padding-right: 8px;
-      text-align: right;
-    }
-
-    .box a,
-    .box a:visited,
-    .box a:hover {
-      color: #dddd00;
-    }
-
-    .box input.gwt-TextBox:disabled {
-      background-color: #cacaca;
-    }
-
-    .box .gwt-ToggleButton {
-      position: relative;
-      height: 19px;
-      width: 140px;
-      background: #fff;
-      color: #000;
-      text-shadow: none;
-    }
-    .box .gwt-ToggleButton .html-face {
-      position: absolute;
-      top: 0;
-      width: 68px;
-      height: 17px;
-      line-height: 17px;
-      text-align: center;
-      border-width: 1px;
-    }
-
-    .box .gwt-ToggleButton-up,
-    .box .gwt-ToggleButton-up-hovering,
-    .box .gwt-ToggleButton-up-disabled,
-    .box .gwt-ToggleButton-down,
-    .box .gwt-ToggleButton-down-hovering,
-    .box .gwt-ToggleButton-down-disabled {
-      padding: 0;
-      border: 0;
-    }
-    .box .gwt-ToggleButton-up .html-face,
-    .box .gwt-ToggleButton-up-hovering .html-face {
-      left: 0;
-      background: #cacaca;
-      border-style: outset;
-    }
-    .box .gwt-ToggleButton-down .html-face,
-    .box .gwt-ToggleButton-down-hovering .html-face {
-      right: 0;
-      background: #bcf;
-      border-style: inset;
-    }
-
-    .box button {
-      margin: 6px 3px 0 0;
-      border-color: rgba(0, 0, 0, 0.1);
-      text-align: center;
-      font-size: 8pt;
-      font-weight: bold;
-      border: 1px solid;
-      cursor: pointer;
-      color: #444;
-      background-color: #f5f5f5;
-      background-image: -webkit-linear-gradient(top, #f5f5f5, #f1f1f1);
-      -webkit-border-radius: 2px;
-      -webkit-box-sizing: content-box;
-    }
-    .box button div {
-      color: #444;
-      height: 10px;
-      min-width: 54px;
-      line-height: 10px;
-      white-space: nowrap;
-    }
-
-    button.apply {
-      background-color: #4d90fe;
-      background-image: -webkit-linear-gradient(top, #4d90fe, #4d90fe);
-    }
-    button.apply div { color: #fff; }
-
-    button.save {
-      margin-left: 10px;
-      color: #d14836;
-      background-color: #d14836;
-      background-image: -webkit-linear-gradient(top, #d14836, #d14836);
-    }
-    button.save div { color: #fff; }
-  </ui:style>
-
-  <g:HTMLPanel styleName='{style.box}'>
-    <div ui:field='header'>
-      <table style='width: 100%'>
-        <tr>
-          <td><ui:msg>Diff Preferences</ui:msg></td>
-          <td style='text-align: right'>
-            <g:Anchor ui:field='close' href='javascript:;'><ui:msg>Close</ui:msg></g:Anchor>
-          </td>
-        </tr>
-      </table>
-      <hr/>
-    </div>
-    <table class='{style.table}'>
-      <tr>
-        <th><ui:msg>Theme</ui:msg></th>
-        <td><g:ListBox ui:field='theme'/></td>
-      </tr>
-      <tr>
-        <th><ui:msg>Ignore Whitespace</ui:msg></th>
-        <td><g:ListBox ui:field='ignoreWhitespace'/></td>
-      </tr>
-      <tr>
-        <th><ui:msg>Tab Width</ui:msg></th>
-        <td><x:NpIntTextBox ui:field='tabWidth'
-            visibleLength='4'
-            alignment='RIGHT'/></td>
-      </tr>
-      <tr>
-        <th><ui:msg>Columns</ui:msg></th>
-        <td><x:NpIntTextBox ui:field='lineLength'
-            visibleLength='4'
-            alignment='RIGHT'/></td>
-      </tr>
-      <tr>
-        <th><ui:msg>Lines of Context</ui:msg></th>
-        <td><ui:msg><x:NpIntTextBox ui:field='context'
-            addStyleNames='{style.context}'
-            visibleLength='4'
-            alignment='RIGHT'/>
-          or <g:CheckBox ui:field='contextEntireFile'>entire file</g:CheckBox></ui:msg></td>
-      </tr>
-      <tr>
-        <th><ui:msg>Cursor Blink Rate</ui:msg></th>
-        <td><x:NpIntTextBox ui:field='cursorBlinkRate'
-            visibleLength='4'
-            alignment='RIGHT'/></td>
-      </tr>
-      <tr>
-        <th><ui:msg>Intraline Difference</ui:msg></th>
-        <td><g:ToggleButton ui:field='intralineDifference'>
-          <g:upFace><ui:msg>Hide</ui:msg></g:upFace>
-          <g:downFace><ui:msg>Show</ui:msg></g:downFace>
-        </g:ToggleButton></td>
-      </tr>
-      <tr>
-        <th><ui:msg>Syntax Highlighting</ui:msg></th>
-        <td><g:ToggleButton ui:field='syntaxHighlighting'>
-          <g:upFace><ui:msg>Hide</ui:msg></g:upFace>
-          <g:downFace><ui:msg>Show</ui:msg></g:downFace>
-        </g:ToggleButton></td>
-      </tr>
-      <tr>
-        <th><div ui:field='modeLabel'><ui:msg>Language</ui:msg></div></th>
-        <td><g:ListBox ui:field='mode'/></td>
-      </tr>
-      <tr>
-        <th><ui:msg>Whitespace Errors</ui:msg></th>
-        <td><g:ToggleButton ui:field='whitespaceErrors'>
-          <g:upFace><ui:msg>Hide</ui:msg></g:upFace>
-          <g:downFace><ui:msg>Show</ui:msg></g:downFace>
-        </g:ToggleButton></td>
-      </tr>
-      <tr>
-        <th><ui:msg>Show Tabs</ui:msg></th>
-        <td><g:ToggleButton ui:field='showTabs'>
-          <g:upFace><ui:msg>Hide</ui:msg></g:upFace>
-          <g:downFace><ui:msg>Show</ui:msg></g:downFace>
-        </g:ToggleButton></td>
-      </tr>
-      <tr>
-        <th><ui:msg>Line Numbers</ui:msg></th>
-        <td><g:ToggleButton ui:field='lineNumbers'>
-          <g:upFace><ui:msg>Hide</ui:msg></g:upFace>
-          <g:downFace><ui:msg>Show</ui:msg></g:downFace>
-        </g:ToggleButton></td>
-      </tr>
-      <tr>
-        <th><ui:msg>Empty Pane</ui:msg></th>
-        <td><g:ToggleButton ui:field='emptyPane'>
-          <g:upFace><ui:msg>Hide</ui:msg></g:upFace>
-          <g:downFace><ui:msg>Show</ui:msg></g:downFace>
-        </g:ToggleButton></td>
-      </tr>
-      <tr>
-        <th><div ui:field='leftSideLabel'><ui:msg>Left Side</ui:msg></div></th>
-        <td><g:ToggleButton ui:field='leftSide'>
-          <g:upFace><ui:msg>Hide</ui:msg></g:upFace>
-          <g:downFace><ui:msg>Show</ui:msg></g:downFace>
-        </g:ToggleButton></td>
-      </tr>
-      <tr>
-        <th><ui:msg>Top Menu</ui:msg></th>
-        <td><g:ToggleButton ui:field='topMenu'>
-          <g:upFace><ui:msg>Hide</ui:msg></g:upFace>
-          <g:downFace><ui:msg>Show</ui:msg></g:downFace>
-        </g:ToggleButton></td>
-      </tr>
-      <tr>
-        <th><ui:msg>Auto Hide Diff Table Header</ui:msg></th>
-        <td><g:ToggleButton ui:field='autoHideDiffTableHeader'>
-          <g:upFace><ui:msg>Yes</ui:msg></g:upFace>
-          <g:downFace><ui:msg>No</ui:msg></g:downFace>
-        </g:ToggleButton></td>
-      </tr>
-      <tr>
-        <th><ui:msg>Mark Reviewed</ui:msg></th>
-        <td><g:ToggleButton ui:field='manualReview'>
-          <g:upFace><ui:msg>Automatic</ui:msg></g:upFace>
-          <g:downFace><ui:msg>Manual</ui:msg></g:downFace>
-        </g:ToggleButton></td>
-      </tr>
-      <tr>
-        <th><ui:msg>Expand All Comments</ui:msg></th>
-        <td><g:ToggleButton ui:field='expandAllComments'>
-          <g:upFace><ui:msg>Collapse</ui:msg></g:upFace>
-          <g:downFace><ui:msg>Expand</ui:msg></g:downFace>
-        </g:ToggleButton></td>
-      </tr>
-      <tr>
-        <th><ui:msg>Render</ui:msg></th>
-        <td><g:ToggleButton ui:field='renderEntireFile'>
-          <g:upFace><ui:msg>Fast</ui:msg></g:upFace>
-          <g:downFace><ui:msg>Slow</ui:msg></g:downFace>
-        </g:ToggleButton></td>
-      </tr>
-      <tr>
-        <th><ui:msg>Match Brackets</ui:msg></th>
-        <td><g:ToggleButton ui:field='matchBrackets'>
-          <g:upFace><ui:msg>Off</ui:msg></g:upFace>
-          <g:downFace><ui:msg>On</ui:msg></g:downFace>
-        </g:ToggleButton></td>
-      </tr>
-      <tr>
-        <th><ui:msg>Line Wrapping</ui:msg></th>
-        <td><g:ToggleButton ui:field='lineWrapping'>
-          <g:upFace><ui:msg>Off</ui:msg></g:upFace>
-          <g:downFace><ui:msg>On</ui:msg></g:downFace>
-        </g:ToggleButton></td>
-      </tr>
-      <tr>
-        <th><ui:msg>Skip Deleted Files</ui:msg></th>
-        <td><g:ToggleButton ui:field='skipDeleted'>
-          <g:upFace><ui:msg>Yes</ui:msg></g:upFace>
-          <g:downFace><ui:msg>No</ui:msg></g:downFace>
-        </g:ToggleButton></td>
-      </tr>
-      <tr>
-        <th><ui:msg>Skip Unchanged Files</ui:msg></th>
-        <td><g:ToggleButton ui:field='skipUnchanged'>
-          <g:upFace><ui:msg>Yes</ui:msg></g:upFace>
-          <g:downFace><ui:msg>No</ui:msg></g:downFace>
-        </g:ToggleButton></td>
-      </tr>
-      <tr>
-        <th><ui:msg>Skip Uncommented Files</ui:msg></th>
-        <td><g:ToggleButton ui:field='skipUncommented'>
-          <g:upFace><ui:msg>Yes</ui:msg></g:upFace>
-          <g:downFace><ui:msg>No</ui:msg></g:downFace>
-        </g:ToggleButton></td>
-      </tr>
-      <tr>
-        <td></td>
-        <td>
-          <g:Button ui:field='apply' styleName='{style.apply}'>
-            <div><ui:msg>Apply</ui:msg></div>
-          </g:Button>
-          <g:Button ui:field='save' styleName='{style.save}'>
-            <div><ui:msg>Save</ui:msg></div>
-          </g:Button>
-        </td>
-      </tr>
-    </table>
-  </g:HTMLPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java
deleted file mode 100644
index 1ddf895..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.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.client.diff;
-
-import com.google.gerrit.client.AvatarImage;
-import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.client.FormatUtil;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.api.ApiGlue;
-import com.google.gerrit.client.change.ReplyBox;
-import com.google.gerrit.client.changes.CommentApi;
-import com.google.gerrit.client.changes.CommentInfo;
-import com.google.gerrit.client.changes.Util;
-import com.google.gerrit.client.patches.PatchUtil;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.CommentLinkProcessor;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.resources.client.CssResource;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.uibinder.client.UiHandler;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwt.user.client.ui.UIObject;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-
-/** An HtmlPanel for displaying a published comment */
-class PublishedBox extends CommentBox {
-  interface Binder extends UiBinder<HTMLPanel, PublishedBox> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  interface Style extends CssResource {
-    String closed();
-  }
-
-  private final PatchSet.Id psId;
-  @Nullable private final Project.NameKey project;
-  private final CommentInfo comment;
-  private final DisplaySide displaySide;
-  private DraftBox replyBox;
-
-  @UiField Style style;
-  @UiField Widget header;
-  @UiField Element name;
-  @UiField Element summary;
-  @UiField Element date;
-  @UiField Element message;
-  @UiField Element buttons;
-  @UiField Button reply;
-  @UiField Button done;
-  @UiField Button fix;
-
-  @UiField(provided = true)
-  AvatarImage avatar;
-
-  PublishedBox(
-      CommentGroup group,
-      CommentLinkProcessor clp,
-      @Nullable Project.NameKey project,
-      PatchSet.Id psId,
-      CommentInfo info,
-      DisplaySide displaySide,
-      boolean open) {
-    super(group, info.range());
-
-    this.psId = psId;
-    this.project = project;
-    this.comment = info;
-    this.displaySide = displaySide;
-
-    if (info.author() != null) {
-      avatar = new AvatarImage(info.author());
-      avatar.setSize("", "");
-    } else {
-      avatar = new AvatarImage();
-    }
-
-    initWidget(uiBinder.createAndBindUi(this));
-    header.addDomHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            setOpen(!isOpen());
-          }
-        },
-        ClickEvent.getType());
-
-    name.setInnerText(authorName(info));
-    date.setInnerText(FormatUtil.shortFormatDayTime(info.updated()));
-    if (info.message() != null) {
-      String msg = info.message().trim();
-      summary.setInnerText(msg);
-      message.setInnerSafeHtml(clp.apply(new SafeHtmlBuilder().append(msg).wikify()));
-      ApiGlue.fireEvent("comment", message);
-    }
-
-    fix.setVisible(open);
-  }
-
-  @Override
-  CommentInfo getCommentInfo() {
-    return comment;
-  }
-
-  @Override
-  boolean isOpen() {
-    return UIObject.isVisible(message);
-  }
-
-  @Override
-  void setOpen(boolean open) {
-    UIObject.setVisible(summary, !open);
-    UIObject.setVisible(message, open);
-    UIObject.setVisible(buttons, open && replyBox == null);
-    if (open) {
-      removeStyleName(style.closed());
-    } else {
-      addStyleName(style.closed());
-    }
-    super.setOpen(open);
-  }
-
-  void setReplyBox(DraftBox box) {
-    replyBox = box;
-    UIObject.setVisible(buttons, false);
-    box.setReplyToBox(this);
-  }
-
-  void unregisterReplyBox() {
-    replyBox = null;
-    UIObject.setVisible(buttons, isOpen());
-  }
-
-  private void openReplyBox() {
-    replyBox.setOpen(true);
-    replyBox.setEdit(true);
-  }
-
-  void addReplyBox(boolean quote) {
-    CommentInfo commentReply = CommentInfo.createReply(comment);
-    if (quote) {
-      commentReply.message(ReplyBox.quote(comment.message()));
-    }
-    getCommentManager().addDraftBox(displaySide, commentReply).setEdit(true);
-  }
-
-  void doReply() {
-    if (!Gerrit.isSignedIn()) {
-      Gerrit.doSignIn(getCommentManager().host.getToken());
-    } else if (replyBox == null) {
-      addReplyBox(false);
-    } else {
-      openReplyBox();
-    }
-  }
-
-  @UiHandler("reply")
-  void onReply(ClickEvent e) {
-    e.stopPropagation();
-    doReply();
-  }
-
-  @UiHandler("quote")
-  void onQuote(ClickEvent e) {
-    e.stopPropagation();
-    if (!Gerrit.isSignedIn()) {
-      Gerrit.doSignIn(getCommentManager().host.getToken());
-    }
-    addReplyBox(true);
-  }
-
-  @UiHandler("done")
-  void onReplyDone(ClickEvent e) {
-    e.stopPropagation();
-    if (!Gerrit.isSignedIn()) {
-      Gerrit.doSignIn(getCommentManager().host.getToken());
-    } else if (replyBox == null) {
-      done.setEnabled(false);
-      CommentInfo input = CommentInfo.createReply(comment);
-      input.message(PatchUtil.C.cannedReplyDone());
-      CommentApi.createDraft(
-          Project.NameKey.asStringOrNull(project),
-          psId,
-          input,
-          new GerritCallback<CommentInfo>() {
-            @Override
-            public void onSuccess(CommentInfo result) {
-              done.setEnabled(true);
-              setOpen(false);
-              getCommentManager().addDraftBox(displaySide, result);
-            }
-          });
-    } else {
-      openReplyBox();
-      setOpen(false);
-    }
-  }
-
-  @UiHandler("fix")
-  void onFix(ClickEvent e) {
-    e.stopPropagation();
-    String t = Dispatcher.toEditScreen(project, psId, comment.path(), comment.line());
-    if (!Gerrit.isSignedIn()) {
-      Gerrit.doSignIn(t);
-    } else {
-      Gerrit.display(t);
-    }
-  }
-
-  private static String authorName(CommentInfo info) {
-    if (info.author() != null) {
-      if (info.author().name() != null) {
-        return info.author().name();
-      }
-      return Gerrit.info().user().anonymousCowardName();
-    }
-    return Util.C.messageNoAuthor();
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml
deleted file mode 100644
index cbea847..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml
+++ /dev/null
@@ -1,82 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder
-    xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:c='urn:import:com.google.gerrit.client'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:with field='res' type='com.google.gerrit.client.diff.Resources'/>
-  <ui:style gss='false' type='com.google.gerrit.client.diff.PublishedBox.Style'>
-    .avatar {
-      position: absolute;
-      width: 26px;
-      height: 26px;
-    }
-    .closed .avatar {
-      position: absolute;
-      width: 16px;
-      height: 16px;
-    }
-
-    .name {
-      white-space: nowrap;
-      font-weight: bold;
-    }
-    .closed .name {
-      width: 120px;
-      overflow: hidden;
-      text-overflow: ellipsis;
-      font-weight: normal;
-    }
-  </ui:style>
-
-  <g:HTMLPanel
-      styleName='{res.style.commentBox}'
-      addStyleNames='{style.closed}'>
-    <c:AvatarImage ui:field='avatar' styleName='{style.avatar}'/>
-    <div class='{res.style.contents}'>
-      <g:HTMLPanel ui:field='header' styleName='{res.style.header}'>
-        <div ui:field='name' class='{style.name}'/>
-        <div ui:field='summary' class='{res.style.summary}'/>
-        <div ui:field='date' class='{res.style.date}'/>
-      </g:HTMLPanel>
-      <div ui:field='message' class='{res.style.message}'
-           aria-hidden='true' style='display: NONE'/>
-      <div ui:field='buttons' aria-hidden='true' style='display: NONE'>
-        <g:Button ui:field='reply' styleName=''
-            title='Reply to this comment'>
-          <ui:attribute name='title'/>
-          <div><ui:msg>Reply</ui:msg></div>
-        </g:Button>
-        <g:Button ui:field='quote' styleName=''
-            title='Reply to this comment with quoting it'>
-          <ui:attribute name='title'/>
-          <div><ui:msg>Quote</ui:msg></div>
-        </g:Button>
-        <g:Button ui:field='done' styleName=''
-            title='Reply "Done" to this comment'>
-          <ui:attribute name='title'/>
-          <div><ui:msg>Done</ui:msg></div>
-        </g:Button>
-        <g:Button ui:field='fix' styleName='' visible='false'
-            title='Fix this comment in the inline editor'>
-          <ui:attribute name='title'/>
-          <div><ui:msg>Fix</ui:msg></div>
-        </g:Button>
-      </div>
-    </div>
-  </g:HTMLPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Resources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Resources.java
deleted file mode 100644
index e590333..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Resources.java
+++ /dev/null
@@ -1,43 +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.client.diff;
-
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.resources.client.ClientBundle;
-import com.google.gwt.resources.client.ImageResource;
-
-/** Resources used by diff. */
-interface Resources extends ClientBundle {
-  Resources I = GWT.create(Resources.class);
-
-  @Source("CommentBox.css")
-  CommentBox.Style style();
-
-  @Source("Scrollbar.css")
-  Scrollbar.Style scrollbarStyle();
-
-  @Source("DiffTable.css")
-  DiffTable.Style diffTableStyle();
-
-  /** tango icon library (public domain): http://tango.freedesktop.org/Tango_Icon_Library */
-  @Source("goPrev.png")
-  ImageResource goPrev();
-
-  @Source("goNext.png")
-  ImageResource goNext();
-
-  @Source("goUp.png")
-  ImageResource goUp();
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollSynchronizer.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollSynchronizer.java
deleted file mode 100644
index 35e3e7d..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollSynchronizer.java
+++ /dev/null
@@ -1,141 +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.client.diff;
-
-import com.google.gwt.user.client.Timer;
-import net.codemirror.lib.CodeMirror;
-import net.codemirror.lib.ScrollInfo;
-
-class ScrollSynchronizer {
-  private SideBySideTable diffTable;
-  private LineMapper mapper;
-  private ScrollCallback active;
-  private ScrollCallback callbackA;
-  private ScrollCallback callbackB;
-  private CodeMirror cmB;
-  private boolean autoHideDiffTableHeader;
-
-  ScrollSynchronizer(SideBySideTable diffTable, CodeMirror cmA, CodeMirror cmB, LineMapper mapper) {
-    this.diffTable = diffTable;
-    this.mapper = mapper;
-    this.cmB = cmB;
-
-    callbackA = new ScrollCallback(cmA, cmB, DisplaySide.A);
-    callbackB = new ScrollCallback(cmB, cmA, DisplaySide.B);
-    cmA.on("scroll", callbackA);
-    cmB.on("scroll", callbackB);
-  }
-
-  void setAutoHideDiffTableHeader(boolean autoHide) {
-    if (autoHide) {
-      updateDiffTableHeader(cmB.getScrollInfo());
-    } else {
-      diffTable.setHeaderVisible(true);
-    }
-    autoHideDiffTableHeader = autoHide;
-  }
-
-  void syncScroll(DisplaySide masterSide) {
-    (masterSide == DisplaySide.A ? callbackA : callbackB).sync();
-  }
-
-  private void updateDiffTableHeader(ScrollInfo si) {
-    if (si.top() == 0) {
-      diffTable.setHeaderVisible(true);
-    } else if (si.top() > 0.5 * si.clientHeight()) {
-      diffTable.setHeaderVisible(false);
-    }
-  }
-
-  class ScrollCallback implements Runnable {
-    private final CodeMirror src;
-    private final CodeMirror dst;
-    private final DisplaySide srcSide;
-    private final Timer fixup;
-    private int state;
-
-    ScrollCallback(CodeMirror src, CodeMirror dst, DisplaySide srcSide) {
-      this.src = src;
-      this.dst = dst;
-      this.srcSide = srcSide;
-      this.fixup =
-          new Timer() {
-            @Override
-            public void run() {
-              if (active == ScrollCallback.this) {
-                fixup();
-              }
-            }
-          };
-    }
-
-    void sync() {
-      dst.scrollToY(align(src.getScrollInfo().top()));
-    }
-
-    @Override
-    public void run() {
-      if (active == null) {
-        active = this;
-        fixup.scheduleRepeating(20);
-      }
-      if (active == this) {
-        ScrollInfo si = src.getScrollInfo();
-        if (autoHideDiffTableHeader) {
-          updateDiffTableHeader(si);
-        }
-        dst.scrollTo(si.left(), align(si.top()));
-        state = 0;
-      }
-    }
-
-    private void fixup() {
-      switch (state) {
-        case 0:
-          state = 1;
-          dst.scrollToY(align(src.getScrollInfo().top()));
-          break;
-        case 1:
-          state = 2;
-          break;
-        case 2:
-          active = null;
-          fixup.cancel();
-          break;
-      }
-    }
-
-    private double align(double srcTop) {
-      // Since CM doesn't always take the height of line widgets into
-      // account when calculating scrollInfo when scrolling too fast (e.g.
-      // throw scrolling), simply setting scrollTop to be the same doesn't
-      // guarantee alignment.
-
-      int line = src.lineAtHeight(srcTop, "local");
-      if (line == 0) {
-        // Padding for insert at start of file occurs above line 0,
-        // and CM3 doesn't always compute heightAtLine correctly.
-        return srcTop;
-      }
-
-      // Find a pair of lines that are aligned and near the top of
-      // the viewport. Use that distance to correct the Y coordinate.
-      LineMapper.AlignedPair p = mapper.align(srcSide, line);
-      double sy = src.heightAtLine(p.src, "local");
-      double dy = dst.heightAtLine(p.dst, "local");
-      return Math.max(0, dy + (srcTop - sy));
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Scrollbar.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Scrollbar.css
deleted file mode 100644
index 26f8ff5..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Scrollbar.css
+++ /dev/null
@@ -1,40 +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.
- */
-
-.comment, .draft, .insert, .delete, .edit {
-  min-height: 5px;
-  position: absolute;
-  right: 0;
-  z-index: 7;
-}
-
-.comment, .draft {
-  color: #0d0d0d;
-  font-size: 9px;
-}
-
-.delete {
-  background-color: #faa;
-  min-width: 12px;
-}
-.insert {
-  background-color: #9f9;
-  min-width: 12px;
-}
-.edit {
-  border-left: 6px solid #faa;
-  width: 6px !important;
-  background-color: #9f9;
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Scrollbar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Scrollbar.java
deleted file mode 100644
index 83ada90..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Scrollbar.java
+++ /dev/null
@@ -1,101 +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.client.diff;
-
-import com.google.gwt.resources.client.CssResource;
-import java.util.ArrayList;
-import java.util.List;
-import net.codemirror.lib.CodeMirror;
-import net.codemirror.lib.Pos;
-
-/** Displays overview of all edits and comments in this file. */
-class Scrollbar {
-  static {
-    Resources.I.scrollbarStyle().ensureInjected();
-  }
-
-  interface Style extends CssResource {
-    String comment();
-
-    String draft();
-
-    String insert();
-
-    String delete();
-
-    String edit();
-  }
-
-  private final List<ScrollbarAnnotation> diff = new ArrayList<>();
-  private final DiffTable parent;
-
-  Scrollbar(DiffTable d) {
-    parent = d;
-  }
-
-  ScrollbarAnnotation comment(CodeMirror cm, int line) {
-    ScrollbarAnnotation a = new ScrollbarAnnotation(cm);
-    a.setStyleName(Resources.I.scrollbarStyle().comment());
-    a.at(line);
-    a.getElement().setInnerText("\u2736"); // Six pointed black star
-    parent.add(a);
-    return a;
-  }
-
-  ScrollbarAnnotation draft(CodeMirror cm, int line) {
-    ScrollbarAnnotation a = new ScrollbarAnnotation(cm);
-    a.setStyleName(Resources.I.scrollbarStyle().draft());
-    a.at(line);
-    a.getElement().setInnerText("\u270D"); // Writing hand
-    parent.add(a);
-    return a;
-  }
-
-  ScrollbarAnnotation insert(CodeMirror cm, int line, int len) {
-    ScrollbarAnnotation a = diff(cm, line, len);
-    a.setStyleName(Resources.I.scrollbarStyle().insert());
-    parent.add(a);
-    return a;
-  }
-
-  ScrollbarAnnotation delete(CodeMirror cmA, CodeMirror cmB, int line, int len) {
-    ScrollbarAnnotation a = diff(cmA, line, len);
-    a.setStyleName(Resources.I.scrollbarStyle().delete());
-    a.renderOn(cmB);
-    parent.add(a);
-    return a;
-  }
-
-  ScrollbarAnnotation edit(CodeMirror cm, int line, int len) {
-    ScrollbarAnnotation a = diff(cm, line, len);
-    a.setStyleName(Resources.I.scrollbarStyle().edit());
-    parent.add(a);
-    return a;
-  }
-
-  private ScrollbarAnnotation diff(CodeMirror cm, int s, int n) {
-    ScrollbarAnnotation a = new ScrollbarAnnotation(cm);
-    a.at(Pos.create(s), Pos.create(s + n));
-    diff.add(a);
-    return a;
-  }
-
-  void removeDiffAnnotations() {
-    for (ScrollbarAnnotation a : diff) {
-      a.remove();
-    }
-    diff.clear();
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollbarAnnotation.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollbarAnnotation.java
deleted file mode 100644
index ecdac46..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollbarAnnotation.java
+++ /dev/null
@@ -1,118 +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.client.diff;
-
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.Style.Unit;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.ui.Widget;
-import net.codemirror.lib.CodeMirror;
-import net.codemirror.lib.CodeMirror.RegisteredHandler;
-import net.codemirror.lib.Pos;
-
-/** Displayed on the vertical scrollbar to place a chunk or comment. */
-class ScrollbarAnnotation extends Widget implements ClickHandler {
-  private final CodeMirror cm;
-  private CodeMirror cmB;
-  private RegisteredHandler refresh;
-  private Pos from;
-  private Pos to;
-  private double scale;
-
-  ScrollbarAnnotation(CodeMirror cm) {
-    setElement((Element) DOM.createDiv());
-    getElement().setAttribute("not-content", "true");
-    addDomHandler(this, ClickEvent.getType());
-    this.cm = cm;
-    this.cmB = cm;
-  }
-
-  void remove() {
-    removeFromParent();
-  }
-
-  void at(int line) {
-    at(Pos.create(line), Pos.create(line + 1));
-  }
-
-  void at(Pos from, Pos to) {
-    this.from = from;
-    this.to = to;
-  }
-
-  void renderOn(CodeMirror cm) {
-    this.cmB = cm;
-  }
-
-  @Override
-  protected void onLoad() {
-    cmB.getWrapperElement().appendChild(getElement());
-    refresh =
-        cmB.on(
-            "refresh",
-            () -> {
-              if (updateScale()) {
-                updatePosition();
-              }
-            });
-    updateScale();
-    updatePosition();
-  }
-
-  @Override
-  protected void onUnload() {
-    cmB.off("refresh", refresh);
-  }
-
-  private boolean updateScale() {
-    double old = scale;
-    double docHeight = cmB.getWrapperElement().getClientHeight();
-    double lineHeight = cmB.heightAtLine(cmB.lastLine() + 1, "local");
-    scale = (docHeight - cmB.barHeight()) / lineHeight;
-    return old != scale;
-  }
-
-  private void updatePosition() {
-    double top = cm.charCoords(from, "local").top() * scale;
-    double bottom = cm.charCoords(to, "local").bottom() * scale;
-
-    Element e = getElement();
-    e.getStyle().setTop(top, Unit.PX);
-    e.getStyle().setWidth(Math.max(2, cm.barWidth() - 1), Unit.PX);
-    e.getStyle().setHeight(Math.max(3, bottom - top), Unit.PX);
-  }
-
-  @Override
-  public void onClick(ClickEvent event) {
-    event.stopPropagation();
-
-    int line = from.line();
-    int h = to.line() - line;
-    if (h > 5) {
-      // Map click inside of the annotation to the relative position
-      // within the region covered by the annotation.
-      double s = ((double) event.getY()) / getElement().getOffsetHeight();
-      line += (int) (s * h);
-    }
-
-    double y = cm.heightAtLine(line, "local");
-    double viewport = cm.getScrollInfo().clientHeight();
-    cm.setCursor(from);
-    cm.scrollTo(0, y - 0.5 * viewport);
-    cm.focus();
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java
deleted file mode 100644
index d052323..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java
+++ /dev/null
@@ -1,414 +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.client.diff;
-
-import static java.lang.Double.POSITIVE_INFINITY;
-
-import com.google.gerrit.client.DiffObject;
-import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.diff.LineMapper.LineOnOtherInfo;
-import com.google.gerrit.client.patches.PatchUtil;
-import com.google.gerrit.client.projects.ConfigInfoCache;
-import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.client.ui.InlineHyperlink;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DiffView;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.core.client.Scheduler;
-import com.google.gwt.core.client.Scheduler.ScheduledCommand;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.event.dom.client.FocusEvent;
-import com.google.gwt.event.dom.client.FocusHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.user.client.Window;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.ImageResourceRenderer;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-import com.google.gwtexpui.globalkey.client.KeyCommand;
-import java.util.Collections;
-import java.util.List;
-import net.codemirror.lib.CodeMirror;
-import net.codemirror.lib.CodeMirror.LineHandle;
-import net.codemirror.lib.Configuration;
-import net.codemirror.lib.KeyMap;
-import net.codemirror.lib.Pos;
-
-public class SideBySide extends DiffScreen {
-  interface Binder extends UiBinder<FlowPanel, SideBySide> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-  private static final String LINE_NUMBER_CLASSNAME = "CodeMirror-linenumber";
-
-  @UiField(provided = true)
-  SideBySideTable diffTable;
-
-  private CodeMirror cmA;
-  private CodeMirror cmB;
-
-  private ScrollSynchronizer scrollSynchronizer;
-
-  private SideBySideChunkManager chunkManager;
-  private SideBySideCommentManager commentManager;
-
-  public SideBySide(
-      @Nullable Project.NameKey project,
-      DiffObject base,
-      DiffObject revision,
-      String path,
-      DisplaySide startSide,
-      int startLine) {
-    super(project, base, revision, path, startSide, startLine, DiffView.SIDE_BY_SIDE);
-
-    diffTable = new SideBySideTable(this, base, revision, path);
-    add(uiBinder.createAndBindUi(this));
-    addDomHandler(GlobalKey.STOP_PROPAGATION, KeyPressEvent.getType());
-  }
-
-  @Override
-  ScreenLoadCallback<ConfigInfoCache.Entry> getScreenLoadCallback(
-      final CommentsCollections comments) {
-    return new ScreenLoadCallback<ConfigInfoCache.Entry>(SideBySide.this) {
-      @Override
-      protected void preDisplay(ConfigInfoCache.Entry result) {
-        commentManager =
-            new SideBySideCommentManager(
-                SideBySide.this,
-                getProject(),
-                base,
-                revision,
-                path,
-                result.getCommentLinkProcessor(),
-                getChangeStatus().isOpen());
-        setTheme(result.getTheme());
-        display(comments);
-        header.setupPrevNextFiles(comments);
-      }
-    };
-  }
-
-  @Override
-  public void onShowView() {
-    super.onShowView();
-
-    operation(
-        () -> {
-          resizeCodeMirror();
-          chunkManager.adjustPadding();
-          cmA.refresh();
-          cmB.refresh();
-        });
-    setLineLength(Patch.COMMIT_MSG.equals(path) ? 72 : prefs.lineLength());
-    diffTable.refresh();
-
-    if (getStartLine() == 0) {
-      DiffChunkInfo d = chunkManager.getFirst();
-      if (d != null) {
-        if (d.isEdit() && d.getSide() == DisplaySide.A) {
-          setStartSide(DisplaySide.B);
-          setStartLine(lineOnOther(d.getSide(), d.getStart()).getLine() + 1);
-        } else {
-          setStartSide(d.getSide());
-          setStartLine(d.getStart() + 1);
-        }
-      }
-    }
-    if (getStartSide() != null && getStartLine() > 0) {
-      CodeMirror cm = getCmFromSide(getStartSide());
-      cm.scrollToLine(getStartLine() - 1);
-      cm.focus();
-    } else {
-      cmA.setCursor(Pos.create(0));
-      cmA.focus();
-    }
-    if (Gerrit.isSignedIn() && prefs.autoReview()) {
-      header.autoReview();
-    }
-    prefetchNextFile();
-  }
-
-  @Override
-  void registerCmEvents(CodeMirror cm) {
-    super.registerCmEvents(cm);
-
-    KeyMap keyMap =
-        KeyMap.create()
-            .on("Shift-A", diffTable.toggleA())
-            .on("Shift-Left", moveCursorToSide(cm, DisplaySide.A))
-            .on("Shift-Right", moveCursorToSide(cm, DisplaySide.B));
-    cm.addKeyMap(keyMap);
-    maybeRegisterRenderEntireFileKeyMap(cm);
-  }
-
-  @Override
-  public void registerKeys() {
-    super.registerKeys();
-
-    getKeysNavigation()
-        .add(
-            new NoOpKeyCommand(KeyCommand.M_SHIFT, KeyCodes.KEY_LEFT, PatchUtil.C.focusSideA()),
-            new NoOpKeyCommand(KeyCommand.M_SHIFT, KeyCodes.KEY_RIGHT, PatchUtil.C.focusSideB()));
-    getKeysAction()
-        .add(
-            new KeyCommand(KeyCommand.M_SHIFT, 'a', PatchUtil.C.toggleSideA()) {
-              @Override
-              public void onKeyPress(KeyPressEvent event) {
-                diffTable.toggleA().run();
-              }
-            });
-
-    registerHandlers();
-  }
-
-  @Override
-  FocusHandler getFocusHandler() {
-    return new FocusHandler() {
-      @Override
-      public void onFocus(FocusEvent event) {
-        cmB.focus();
-      }
-    };
-  }
-
-  private void display(CommentsCollections comments) {
-    DiffInfo diff = getDiff();
-    setThemeStyles(prefs.theme().isDark());
-    setShowIntraline(prefs.intralineDifference());
-    if (prefs.showLineNumbers()) {
-      diffTable.addStyleName(Resources.I.diffTableStyle().showLineNumbers());
-    }
-
-    cmA = newCm(diff.metaA(), diff.textA(), diffTable.cmA);
-    cmB = newCm(diff.metaB(), diff.textB(), diffTable.cmB);
-
-    getDiffTable()
-        .setUpBlameIconA(
-            cmA,
-            base.isBaseOrAutoMerge(),
-            base.isBaseOrAutoMerge() ? revision : base.asPatchSetId(),
-            path);
-    getDiffTable().setUpBlameIconB(cmB, revision, path);
-
-    cmA.extras().side(DisplaySide.A);
-    cmB.extras().side(DisplaySide.B);
-    setShowTabs(prefs.showTabs());
-
-    chunkManager = new SideBySideChunkManager(this, cmA, cmB, diffTable.scrollbar);
-
-    operation(
-        () -> {
-          // Estimate initial CodeMirror height, fixed up in onShowView.
-          int height = Window.getClientHeight() - (Gerrit.getHeaderFooterHeight() + 18);
-          cmA.setHeight(height);
-          cmB.setHeight(height);
-
-          render(diff);
-          commentManager.render(comments, prefs.expandAllComments());
-          skipManager.render(prefs.context(), diff);
-        });
-
-    registerCmEvents(cmA);
-    registerCmEvents(cmB);
-    scrollSynchronizer = new ScrollSynchronizer(diffTable, cmA, cmB, chunkManager.lineMapper);
-
-    setPrefsAction(new PreferencesAction(this, prefs));
-    header.init(getPrefsAction(), getUnifiedDiffLink(), diff.sideBySideWebLinks());
-    scrollSynchronizer.setAutoHideDiffTableHeader(prefs.autoHideDiffTableHeader());
-
-    setupSyntaxHighlighting();
-  }
-
-  private List<InlineHyperlink> getUnifiedDiffLink() {
-    InlineHyperlink toUnifiedDiffLink = new InlineHyperlink();
-    toUnifiedDiffLink.setHTML(new ImageResourceRenderer().render(Gerrit.RESOURCES.unifiedDiff()));
-    toUnifiedDiffLink.setTargetHistoryToken(
-        Dispatcher.toUnified(getProject(), base, revision, path));
-    toUnifiedDiffLink.setTitle(PatchUtil.C.unifiedDiff());
-    return Collections.singletonList(toUnifiedDiffLink);
-  }
-
-  @Override
-  CodeMirror newCm(DiffInfo.FileMeta meta, String contents, Element parent) {
-    return CodeMirror.create(
-        parent,
-        Configuration.create()
-            .set("cursorBlinkRate", prefs.cursorBlinkRate())
-            .set("cursorHeight", 0.85)
-            .set("inputStyle", "textarea")
-            .set("keyMap", "vim_ro")
-            .set("lineNumbers", prefs.showLineNumbers())
-            .set("matchBrackets", prefs.matchBrackets())
-            .set("lineWrapping", prefs.lineWrapping())
-            .set("mode", getFileSize() == FileSize.SMALL ? getContentType(meta) : null)
-            .set("readOnly", true)
-            .set("scrollbarStyle", "overlay")
-            .set("showTrailingSpace", prefs.showWhitespaceErrors())
-            .set("styleSelectedText", true)
-            .set("tabSize", prefs.tabSize())
-            .set("theme", prefs.theme().name().toLowerCase())
-            .set("value", meta != null ? contents : "")
-            .set("viewportMargin", renderEntireFile() ? POSITIVE_INFINITY : 10));
-  }
-
-  @Override
-  void setShowLineNumbers(boolean b) {
-    super.setShowLineNumbers(b);
-
-    cmA.setOption("lineNumbers", b);
-    cmB.setOption("lineNumbers", b);
-  }
-
-  @Override
-  void setSyntaxHighlighting(boolean b) {
-    final DiffInfo diff = getDiff();
-    if (b) {
-      injectMode(
-          diff,
-          new AsyncCallback<Void>() {
-            @Override
-            public void onSuccess(Void result) {
-              if (prefs.syntaxHighlighting()) {
-                cmA.setOption("mode", getContentType(diff.metaA()));
-                cmB.setOption("mode", getContentType(diff.metaB()));
-              }
-            }
-
-            @Override
-            public void onFailure(Throwable caught) {
-              prefs.syntaxHighlighting(false);
-            }
-          });
-    } else {
-      cmA.setOption("mode", (String) null);
-      cmB.setOption("mode", (String) null);
-    }
-  }
-
-  @Override
-  void setAutoHideDiffHeader(boolean hide) {
-    scrollSynchronizer.setAutoHideDiffTableHeader(hide);
-  }
-
-  CodeMirror otherCm(CodeMirror me) {
-    return me == cmA ? cmB : cmA;
-  }
-
-  @Override
-  CodeMirror getCmFromSide(DisplaySide side) {
-    return side == DisplaySide.A ? cmA : cmB;
-  }
-
-  @Override
-  int getCmLine(int line, DisplaySide side) {
-    return line;
-  }
-
-  @Override
-  Runnable updateActiveLine(CodeMirror cm) {
-    CodeMirror other = otherCm(cm);
-    return () -> {
-      // The rendering of active lines has to be deferred. Reflow
-      // caused by adding and removing styles chokes Firefox when arrow
-      // key (or j/k) is held down. Performance on Chrome is fine
-      // without the deferral.
-      //
-      Scheduler.get()
-          .scheduleDeferred(
-              new ScheduledCommand() {
-                @Override
-                public void execute() {
-                  operation(
-                      () -> {
-                        LineHandle handle = cm.getLineHandleVisualStart(cm.getCursor("end").line());
-                        if (!cm.extras().activeLine(handle)) {
-                          return;
-                        }
-
-                        LineOnOtherInfo info = lineOnOther(cm.side(), cm.getLineNumber(handle));
-                        if (info.isAligned()) {
-                          other.extras().activeLine(other.getLineHandle(info.getLine()));
-                        } else {
-                          other.extras().clearActiveLine();
-                        }
-                      });
-                }
-              });
-    };
-  }
-
-  private Runnable moveCursorToSide(CodeMirror cmSrc, DisplaySide sideDst) {
-    CodeMirror cmDst = getCmFromSide(sideDst);
-    if (cmDst == cmSrc) {
-      return () -> {};
-    }
-
-    DisplaySide sideSrc = cmSrc.side();
-    return () -> {
-      if (cmSrc.extras().hasActiveLine()) {
-        cmDst.setCursor(
-            Pos.create(
-                lineOnOther(sideSrc, cmSrc.getLineNumber(cmSrc.extras().activeLine())).getLine()));
-      }
-      cmDst.focus();
-    };
-  }
-
-  void syncScroll(DisplaySide masterSide) {
-    if (scrollSynchronizer != null) {
-      scrollSynchronizer.syncScroll(masterSide);
-    }
-  }
-
-  @Override
-  void operation(Runnable apply) {
-    cmA.operation(() -> cmB.operation(apply::run));
-  }
-
-  @Override
-  CodeMirror[] getCms() {
-    return new CodeMirror[] {cmA, cmB};
-  }
-
-  @Override
-  SideBySideTable getDiffTable() {
-    return diffTable;
-  }
-
-  @Override
-  SideBySideChunkManager getChunkManager() {
-    return chunkManager;
-  }
-
-  @Override
-  SideBySideCommentManager getCommentManager() {
-    return commentManager;
-  }
-
-  @Override
-  boolean isSideBySide() {
-    return true;
-  }
-
-  @Override
-  String getLineNumberClassName() {
-    return LINE_NUMBER_CLASSNAME;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.ui.xml
deleted file mode 100644
index 55c9de0..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.ui.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'
-    xmlns:d='urn:import:com.google.gerrit.client.diff'>
-  <ui:style gss='false'>
-    .sbs {
-      margin-left: -5px;
-      margin-right: -5px;
-    }
-  </ui:style>
-  <g:FlowPanel styleName='{style.sbs}'>
-    <d:Header ui:field='header'/>
-    <d:SideBySideTable ui:field='diffTable'/>
-  </g:FlowPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideChunkManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideChunkManager.java
deleted file mode 100644
index 2877794..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideChunkManager.java
+++ /dev/null
@@ -1,262 +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.client.diff;
-
-import static com.google.gerrit.client.diff.DisplaySide.A;
-import static com.google.gerrit.client.diff.DisplaySide.B;
-
-import com.google.gerrit.client.diff.DiffInfo.Region;
-import com.google.gerrit.client.diff.DiffInfo.Span;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.core.client.JsArrayString;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.NativeEvent;
-import com.google.gwt.dom.client.Style.Unit;
-import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.EventListener;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import net.codemirror.lib.CodeMirror;
-import net.codemirror.lib.CodeMirror.LineClassWhere;
-import net.codemirror.lib.Configuration;
-import net.codemirror.lib.LineWidget;
-import net.codemirror.lib.Pos;
-
-/** Colors modified regions for {@link SideBySide}. */
-class SideBySideChunkManager extends ChunkManager {
-  private static final String DATA_LINES = "_cs2h";
-  private static double guessedLineHeightPx = 15;
-  private static final JavaScriptObject focusA = initOnClick(A);
-  private static final JavaScriptObject focusB = initOnClick(B);
-
-  private static native JavaScriptObject initOnClick(DisplaySide s) /*-{
-    return $entry(function(e){
-      @com.google.gerrit.client.diff.SideBySideChunkManager::focus(
-        Lcom/google/gwt/dom/client/NativeEvent;
-        Lcom/google/gerrit/client/diff/DisplaySide;)(e,s)
-    });
-  }-*/;
-
-  private static void focus(NativeEvent event, DisplaySide side) {
-    Element e = Element.as(event.getEventTarget());
-    for (e = DOM.getParent(e); e != null; e = DOM.getParent(e)) {
-      EventListener l = DOM.getEventListener(e);
-      if (l instanceof SideBySide) {
-        ((SideBySide) l).getCmFromSide(side).focus();
-        event.stopPropagation();
-      }
-    }
-  }
-
-  static void focusOnClick(Element e, DisplaySide side) {
-    onClick(e, side == A ? focusA : focusB);
-  }
-
-  private final SideBySide host;
-  private final CodeMirror cmA;
-  private final CodeMirror cmB;
-
-  private List<DiffChunkInfo> chunks;
-  private List<LineWidget> padding;
-  private List<Element> paddingDivs;
-
-  SideBySideChunkManager(SideBySide host, CodeMirror cmA, CodeMirror cmB, Scrollbar scrollbar) {
-    super(scrollbar);
-
-    this.host = host;
-    this.cmA = cmA;
-    this.cmB = cmB;
-  }
-
-  @Override
-  DiffChunkInfo getFirst() {
-    return !chunks.isEmpty() ? chunks.get(0) : null;
-  }
-
-  @Override
-  void reset() {
-    super.reset();
-
-    for (LineWidget w : padding) {
-      w.clear();
-    }
-  }
-
-  @Override
-  void render(DiffInfo diff) {
-    super.render();
-
-    chunks = new ArrayList<>();
-    padding = new ArrayList<>();
-    paddingDivs = new ArrayList<>();
-
-    String diffColor =
-        diff.metaA() == null || diff.metaB() == null
-            ? SideBySideTable.style.intralineBg()
-            : SideBySideTable.style.diff();
-
-    for (Region current : Natives.asList(diff.content())) {
-      if (current.ab() != null) {
-        lineMapper.appendCommon(current.ab().length());
-      } else if (current.skip() > 0) {
-        lineMapper.appendCommon(current.skip());
-      } else if (current.common()) {
-        lineMapper.appendCommon(current.b().length());
-      } else {
-        render(current, diffColor);
-      }
-    }
-
-    if (paddingDivs.isEmpty()) {
-      paddingDivs = null;
-    }
-  }
-
-  void adjustPadding() {
-    if (paddingDivs != null) {
-      double h = cmB.extras().lineHeightPx();
-      for (Element div : paddingDivs) {
-        int lines = div.getPropertyInt(DATA_LINES);
-        div.getStyle().setHeight(lines * h, Unit.PX);
-      }
-      for (LineWidget w : padding) {
-        w.changed();
-      }
-      paddingDivs = null;
-      guessedLineHeightPx = h;
-    }
-  }
-
-  private void render(Region region, String diffColor) {
-    int startA = lineMapper.getLineA();
-    int startB = lineMapper.getLineB();
-
-    JsArrayString a = region.a();
-    JsArrayString b = region.b();
-    int aLen = a != null ? a.length() : 0;
-    int bLen = b != null ? b.length() : 0;
-
-    String color = a == null || b == null ? diffColor : SideBySideTable.style.intralineBg();
-
-    colorLines(cmA, color, startA, aLen);
-    colorLines(cmB, color, startB, bLen);
-    markEdit(cmA, startA, a, region.editA());
-    markEdit(cmB, startB, b, region.editB());
-    addPadding(cmA, startA + aLen - 1, bLen - aLen);
-    addPadding(cmB, startB + bLen - 1, aLen - bLen);
-    addGutterTag(region, startA, startB);
-    lineMapper.appendReplace(aLen, bLen);
-
-    int endA = lineMapper.getLineA() - 1;
-    int endB = lineMapper.getLineB() - 1;
-    if (aLen > 0) {
-      addDiffChunk(cmB, endA, aLen, bLen > 0);
-    }
-    if (bLen > 0) {
-      addDiffChunk(cmA, endB, bLen, aLen > 0);
-    }
-  }
-
-  private void addGutterTag(Region region, int startA, int startB) {
-    if (region.a() == null) {
-      scrollbar.insert(cmB, startB, region.b().length());
-    } else if (region.b() == null) {
-      scrollbar.delete(cmA, cmB, startA, region.a().length());
-    } else {
-      scrollbar.edit(cmB, startB, region.b().length());
-    }
-  }
-
-  private void markEdit(CodeMirror cm, int startLine, JsArrayString lines, JsArray<Span> edits) {
-    if (lines == null || edits == null) {
-      return;
-    }
-
-    EditIterator iter = new EditIterator(lines, startLine);
-    Configuration bg =
-        Configuration.create()
-            .set("className", SideBySideTable.style.intralineBg())
-            .set("readOnly", true);
-
-    Configuration diff =
-        Configuration.create().set("className", SideBySideTable.style.diff()).set("readOnly", true);
-
-    Pos last = Pos.create(0, 0);
-    for (Span span : Natives.asList(edits)) {
-      Pos from = iter.advance(span.skip());
-      Pos to = iter.advance(span.mark());
-      if (from.line() == last.line()) {
-        getMarkers().add(cm.markText(last, from, bg));
-      } else {
-        getMarkers().add(cm.markText(Pos.create(from.line(), 0), from, bg));
-      }
-      getMarkers().add(cm.markText(from, to, diff));
-      last = to;
-      colorLines(
-          cm, LineClassWhere.BACKGROUND, SideBySideTable.style.diff(), from.line(), to.line());
-    }
-  }
-
-  /**
-   * Insert a new padding div below the given line.
-   *
-   * @param cm parent CodeMirror to add extra space into.
-   * @param line line to put the padding below.
-   * @param len number of lines to pad. Padding is inserted only if {@code len >= 1}.
-   */
-  private void addPadding(CodeMirror cm, int line, int len) {
-    if (0 < len) {
-      Element pad = DOM.createDiv();
-      pad.setClassName(SideBySideTable.style.padding());
-      pad.setPropertyInt(DATA_LINES, len);
-      pad.getStyle().setHeight(guessedLineHeightPx * len, Unit.PX);
-      focusOnClick(pad, cm.side());
-      paddingDivs.add(pad);
-      padding.add(
-          cm.addLineWidget(
-              line == -1 ? 0 : line,
-              pad,
-              Configuration.create()
-                  .set("coverGutter", true)
-                  .set("noHScroll", true)
-                  .set("above", line == -1)));
-    }
-  }
-
-  private void addDiffChunk(CodeMirror cmToPad, int lineOnOther, int chunkSize, boolean edit) {
-    chunks.add(
-        new DiffChunkInfo(
-            host.otherCm(cmToPad).side(), lineOnOther - chunkSize + 1, lineOnOther, edit));
-  }
-
-  @Override
-  Runnable diffChunkNav(CodeMirror cm, Direction dir) {
-    return () -> {
-      int line = cm.extras().hasActiveLine() ? cm.getLineNumber(cm.extras().activeLine()) : 0;
-      int res =
-          Collections.binarySearch(
-              chunks, new DiffChunkInfo(cm.side(), line, 0, false), getDiffChunkComparator());
-      diffChunkNavHelper(chunks, host, res, dir);
-    };
-  }
-
-  @Override
-  int getCmLine(int line, DisplaySide side) {
-    return line;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentGroup.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentGroup.java
deleted file mode 100644
index c728f6f..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentGroup.java
+++ /dev/null
@@ -1,159 +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.client.diff;
-
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.Style.Unit;
-import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.Timer;
-import java.util.PriorityQueue;
-import net.codemirror.lib.CodeMirror;
-
-/**
- * LineWidget attached to a CodeMirror container.
- *
- * <p>When a comment is placed on a line a CommentWidget is created on both sides. The group tracks
- * all comment boxes on that same line, and also includes an empty padding element to keep
- * subsequent lines vertically aligned.
- */
-class SideBySideCommentGroup extends CommentGroup implements Comparable<SideBySideCommentGroup> {
-  static void pair(SideBySideCommentGroup a, SideBySideCommentGroup b) {
-    a.peers.add(b);
-    b.peers.add(a);
-  }
-
-  private final Element padding;
-  private final PriorityQueue<SideBySideCommentGroup> peers;
-
-  SideBySideCommentGroup(
-      SideBySideCommentManager manager, CodeMirror cm, DisplaySide side, int line) {
-    super(manager, cm, side, line);
-
-    padding = DOM.createDiv();
-    padding.setClassName(SideBySideTable.style.padding());
-    SideBySideChunkManager.focusOnClick(padding, cm.side());
-    getElement().appendChild(padding);
-    peers = new PriorityQueue<>();
-  }
-
-  SideBySideCommentGroup getPeer() {
-    return peers.peek();
-  }
-
-  @Override
-  void remove(DraftBox box) {
-    super.remove(box);
-
-    if (getBoxCount() == 0 && peers.size() == 1 && peers.peek().peers.size() > 1) {
-      SideBySideCommentGroup peer = peers.peek();
-      peer.peers.remove(this);
-      detach();
-      if (peer.getBoxCount() == 0
-          && peer.peers.size() == 1
-          && peer.peers.peek().getBoxCount() == 0) {
-        peer.detach();
-      } else {
-        peer.resize();
-      }
-    } else {
-      resize();
-    }
-  }
-
-  @Override
-  void init(DiffTable parent) {
-    if (getLineWidget() == null) {
-      attach(parent);
-    }
-    for (CommentGroup peer : peers) {
-      if (peer.getLineWidget() == null) {
-        peer.attach(parent);
-      }
-    }
-  }
-
-  @Override
-  void handleRedraw() {
-    getLineWidget()
-        .onRedraw(
-            () -> {
-              if (canComputeHeight() && peers.peek().canComputeHeight()) {
-                if (getResizeTimer() != null) {
-                  getResizeTimer().cancel();
-                  setResizeTimer(null);
-                }
-                adjustPadding(SideBySideCommentGroup.this, peers.peek());
-              } else if (getResizeTimer() == null) {
-                setResizeTimer(
-                    new Timer() {
-                      @Override
-                      public void run() {
-                        if (canComputeHeight() && peers.peek().canComputeHeight()) {
-                          cancel();
-                          setResizeTimer(null);
-                          adjustPadding(SideBySideCommentGroup.this, peers.peek());
-                        }
-                      }
-                    });
-                getResizeTimer().scheduleRepeating(5);
-              }
-            });
-  }
-
-  @Override
-  void resize() {
-    if (getLineWidget() != null) {
-      adjustPadding(this, peers.peek());
-    }
-  }
-
-  private int computeHeight() {
-    if (getComments().isVisible()) {
-      // Include margin-bottom: 5px from CSS class.
-      return getComments().getOffsetHeight() + 5;
-    }
-    return 0;
-  }
-
-  private static void adjustPadding(SideBySideCommentGroup a, SideBySideCommentGroup b) {
-    int apx = a.computeHeight();
-    int bpx = b.computeHeight();
-    for (SideBySideCommentGroup otherPeer : a.peers) {
-      if (otherPeer != b) {
-        bpx += otherPeer.computeHeight();
-      }
-    }
-    for (SideBySideCommentGroup otherPeer : b.peers) {
-      if (otherPeer != a) {
-        apx += otherPeer.computeHeight();
-      }
-    }
-    int h = Math.max(apx, bpx);
-    a.padding.getStyle().setHeight(Math.max(0, h - apx), Unit.PX);
-    b.padding.getStyle().setHeight(Math.max(0, h - bpx), Unit.PX);
-    a.getLineWidget().changed();
-    b.getLineWidget().changed();
-    a.updateSelection();
-    b.updateSelection();
-  }
-
-  @Override
-  public int compareTo(SideBySideCommentGroup o) {
-    if (side == o.side) {
-      return line - o.line;
-    }
-    throw new IllegalStateException("Cannot compare SideBySideCommentGroup with different sides");
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentManager.java
deleted file mode 100644
index 09c5b07..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentManager.java
+++ /dev/null
@@ -1,136 +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.client.diff;
-
-import com.google.gerrit.client.DiffObject;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.CommentInfo;
-import com.google.gerrit.client.ui.CommentLinkProcessor;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import java.util.Collection;
-import java.util.Map;
-import java.util.SortedMap;
-import net.codemirror.lib.CodeMirror;
-import net.codemirror.lib.TextMarker.FromTo;
-
-/** Tracks comment widgets for {@link SideBySide}. */
-class SideBySideCommentManager extends CommentManager {
-  SideBySideCommentManager(
-      SideBySide host,
-      @Nullable Project.NameKey project,
-      DiffObject base,
-      PatchSet.Id revision,
-      String path,
-      CommentLinkProcessor clp,
-      boolean open) {
-    super(host, project, base, revision, path, clp, open);
-  }
-
-  @Override
-  SortedMap<Integer, CommentGroup> getMapForNav(DisplaySide side) {
-    return map(side);
-  }
-
-  @Override
-  void clearLine(DisplaySide side, int line, CommentGroup group) {
-    super.clearLine(side, line, group);
-  }
-
-  @Override
-  void newDraftOnGutterClick(CodeMirror cm, String gutterClass, int line) {
-    if (!Gerrit.isSignedIn()) {
-      signInCallback(cm).run();
-    } else {
-      insertNewDraft(cm.side(), line);
-    }
-  }
-
-  @Override
-  CommentGroup getCommentGroupOnActiveLine(CodeMirror cm) {
-    CommentGroup group = null;
-    if (cm.extras().hasActiveLine()) {
-      group = map(cm.side()).get(cm.getLineNumber(cm.extras().activeLine()) + 1);
-    }
-    return group;
-  }
-
-  @Override
-  Collection<Integer> getLinesWithCommentGroups() {
-    return sideB.tailMap(1).keySet();
-  }
-
-  @Override
-  String getTokenSuffixForActiveLine(CodeMirror cm) {
-    return (cm.side() == DisplaySide.A ? "a" : "")
-        + (cm.getLineNumber(cm.extras().activeLine()) + 1);
-  }
-
-  @Override
-  void newDraft(CodeMirror cm) {
-    int line = cm.getLineNumber(cm.extras().activeLine()) + 1;
-    if (cm.somethingSelected()) {
-      FromTo fromTo = adjustSelection(cm);
-      addDraftBox(
-              cm.side(),
-              CommentInfo.create(
-                  getPath(),
-                  getStoredSideFromDisplaySide(cm.side()),
-                  getParentNumFromDisplaySide(cm.side()),
-                  line,
-                  CommentRange.create(fromTo),
-                  false))
-          .setEdit(true);
-      cm.setCursor(fromTo.to());
-      cm.setSelection(cm.getCursor());
-    } else {
-      insertNewDraft(cm.side(), line);
-    }
-  }
-
-  @Override
-  CommentGroup group(DisplaySide side, int line) {
-    CommentGroup existing = map(side).get(line);
-    if (existing != null) {
-      return existing;
-    }
-
-    SideBySideCommentGroup newGroup = newGroup(side, line);
-    Map<Integer, CommentGroup> map = side == DisplaySide.A ? sideA : sideB;
-    Map<Integer, CommentGroup> otherMap = side == DisplaySide.A ? sideB : sideA;
-    map.put(line, newGroup);
-    int otherLine = host.lineOnOther(side, line - 1).getLine() + 1;
-    existing = map(side.otherSide()).get(otherLine);
-    CommentGroup otherGroup;
-    if (existing != null) {
-      otherGroup = existing;
-    } else {
-      otherGroup = newGroup(side.otherSide(), otherLine);
-      otherMap.put(otherLine, otherGroup);
-    }
-    SideBySideCommentGroup.pair(newGroup, (SideBySideCommentGroup) otherGroup);
-
-    if (isAttached()) {
-      newGroup.init(host.getDiffTable());
-      otherGroup.handleRedraw();
-    }
-    return newGroup;
-  }
-
-  private SideBySideCommentGroup newGroup(DisplaySide side, int line) {
-    return new SideBySideCommentGroup(this, host.getCmFromSide(side), side, line);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.java
deleted file mode 100644
index c65dcf0..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.java
+++ /dev/null
@@ -1,112 +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.client.diff;
-
-import com.google.gerrit.client.DiffObject;
-import com.google.gerrit.reviewdb.client.Patch.ChangeType;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.resources.client.CssResource;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.user.client.ui.HTMLPanel;
-
-/**
- * A table with one row and two columns to hold the two CodeMirrors displaying the files to be
- * compared.
- */
-class SideBySideTable extends DiffTable {
-  interface Binder extends UiBinder<HTMLPanel, SideBySideTable> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  interface DiffTableStyle extends CssResource {
-    String intralineBg();
-
-    String diff();
-
-    String hideA();
-
-    String hideB();
-
-    String padding();
-  }
-
-  private SideBySide parent;
-  @UiField Element cmA;
-  @UiField Element cmB;
-  @UiField static DiffTableStyle style;
-
-  private boolean visibleA;
-
-  SideBySideTable(SideBySide parent, DiffObject base, DiffObject revision, String path) {
-    super(parent, base, revision, path);
-
-    initWidget(uiBinder.createAndBindUi(this));
-    this.visibleA = true;
-    this.parent = parent;
-  }
-
-  @Override
-  boolean isVisibleA() {
-    return visibleA;
-  }
-
-  void setVisibleA(boolean show) {
-    visibleA = show;
-    if (show) {
-      removeStyleName(style.hideA());
-      parent.syncScroll(DisplaySide.B); // match B's viewport
-    } else {
-      addStyleName(style.hideA());
-    }
-  }
-
-  Runnable toggleA() {
-    return () -> setVisibleA(!isVisibleA());
-  }
-
-  void setVisibleB(boolean show) {
-    if (show) {
-      removeStyleName(style.hideB());
-      parent.syncScroll(DisplaySide.A); // match A's viewport
-    } else {
-      addStyleName(style.hideB());
-    }
-  }
-
-  @Override
-  void setHideEmptyPane(boolean hide) {
-    if (getChangeType() == ChangeType.ADDED) {
-      setVisibleA(!hide);
-    } else if (getChangeType() == ChangeType.DELETED) {
-      setVisibleB(!hide);
-    }
-  }
-
-  @Override
-  SideBySide getDiffScreen() {
-    return parent;
-  }
-
-  @Override
-  int getHeaderHeight() {
-    int h = patchSetSelectBoxA.getOffsetHeight();
-    if (hasHeader()) {
-      h += diffHeaderRow.getOffsetHeight();
-    }
-    return h;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.ui.xml
deleted file mode 100644
index b2e3f43..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.ui.xml
+++ /dev/null
@@ -1,147 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'
-    xmlns:d='urn:import:com.google.gerrit.client.diff'>
-  <ui:with field='res' type='com.google.gerrit.client.diff.Resources'/>
-  <ui:style gss='false' type='com.google.gerrit.client.diff.SideBySideTable.DiffTableStyle'>
-    @external .CodeMirror, .CodeMirror-selectedtext;
-    @external .CodeMirror-linenumber;
-    @external .CodeMirror-overlayscroll-vertical, .CodeMirror-scroll;
-    @external .CodeMirror-dialog-bottom;
-    @external .CodeMirror-cursor;
-
-    @external .dark, .noIntraline, .showLineNumbers;
-
-    .difftable .patchSetNav,
-    .difftable .CodeMirror {
-      -webkit-touch-callout: none;
-      -webkit-user-select: none;
-      -khtml-user-select: none;
-      -moz-user-select: none;
-      -ms-user-select: none;
-    }
-
-    .difftable .CodeMirror pre {
-      overflow: visible;
-      border-right: 0;
-      width: auto;
-    }
-
-    /* Preserve space for underscores. If this changes
-     * see ChunkManager.addPadding() and adjust there.
-     */
-    .difftable .CodeMirror pre,
-    .difftable .CodeMirror pre span {
-      padding-bottom: 1px;
-    }
-
-    .hideA .psNavA,
-    .hideA .a {
-      display: none;
-    }
-
-    .hideB .psNavB,
-    .hideB .b {
-      display: none;
-    }
-
-    .table {
-      width: 100%;
-      table-layout: fixed;
-      border-spacing: 0;
-    }
-    .table td { padding: 0 }
-    .a, .b { width: 50% }
-    .hideA .psNavB, .hideA .b { width: 100% }
-    .hideB .psNavA, .hideB .a { width: 100% }
-
-    /* Hide scrollbars on A, B controls both views. */
-    .a .CodeMirror-scroll { margin-right: -36px; }
-    .a .CodeMirror-overlayscroll-vertical { display: none !important; }
-
-    .showLineNumbers .b { border-left: none; }
-    .b { border-left: 1px solid #ddd; }
-
-    .a .diff { background-color: #faa; }
-    /* Set min-width for lineWrapping to make sure it gets enough width
-       before lineWrapping and to make sure it dosent do a ugly line wrap */
-    .b .diff { background-color: #9f9; min-width: 60em; }
-    .a .intralineBg { background-color: #fee; }
-    .b .intralineBg { background-color: #dfd; }
-    .noIntraline .a .intralineBg { background-color: #faa; }
-    .noIntraline .b .intralineBg { background-color: #9f9; }
-
-    .dark .a .diff { background-color: #400; }
-    .dark .b .diff { background-color: #444; }
-
-    .dark .a .intralineBg { background-color: #888; }
-    .dark .b .intralineBg { background-color: #bbb; }
-    .dark .noIntraline .a .intralineBg { background-color: #400; }
-    .dark .noIntraline .b .intralineBg { background-color: #444; }
-
-    .patchSetNav, .diff_header {
-      background-color: #f7f7f7;
-      line-height: 1;
-    }
-
-    .difftable .CodeMirror-selectedtext {
-      background-color: inherit !important;
-    }
-    .difftable .CodeMirror-linenumber {
-      height: 1.11em;
-      cursor: pointer;
-    }
-    .difftable .CodeMirror div.CodeMirror-cursor {
-      border-left: 2px solid black;
-    }
-    .difftable .CodeMirror-dialog-bottom {
-      border-top: 0;
-      border-left: 1px solid #000;
-      border-bottom: 1px solid #000;
-      background-color: #f7f7f7;
-      top: 0;
-      right: 0;
-      bottom: auto;
-      left: auto;
-    }
-    .showLineNumbers .padding {
-      margin-left: 21px;
-      border-left: 2px solid #d64040;
-    }
-  </ui:style>
-  <g:HTMLPanel styleName='{style.difftable}'>
-    <table class='{style.table}'>
-      <tr ui:field='patchSetNavRow' class='{style.patchSetNav}'>
-        <td ui:field='patchSetNavCellA' class='{style.psNavA}'>
-          <d:PatchSetSelectBox ui:field='patchSetSelectBoxA' />
-        </td>
-        <td ui:field='patchSetNavCellB' class='{style.psNavB}'>
-          <d:PatchSetSelectBox ui:field='patchSetSelectBoxB' />
-        </td>
-      </tr>
-      <tr ui:field='diffHeaderRow' class='{res.diffTableStyle.diffHeader}'>
-        <td colspan='2'><pre ui:field='diffHeaderText' /></td>
-      </tr>
-      <tr>
-        <td ui:field='cmA' class='{style.a}' />
-        <td ui:field='cmB' class='{style.b}' />
-      </tr>
-    </table>
-    <g:FlowPanel ui:field='widgets' visible='false'/>
-  </g:HTMLPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java
deleted file mode 100644
index c138f37..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java
+++ /dev/null
@@ -1,217 +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.client.diff;
-
-import com.google.gerrit.client.patches.PatchUtil;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.dom.client.Style.Unit;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.resources.client.CssResource;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.uibinder.client.UiHandler;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import net.codemirror.lib.CodeMirror;
-import net.codemirror.lib.Configuration;
-import net.codemirror.lib.LineWidget;
-import net.codemirror.lib.Pos;
-import net.codemirror.lib.TextMarker;
-import net.codemirror.lib.TextMarker.FromTo;
-
-class SkipBar extends Composite {
-  interface Binder extends UiBinder<HTMLPanel, SkipBar> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-  private static final int NUM_ROWS_TO_EXPAND = 10;
-  private static final int UP_DOWN_THRESHOLD = 30;
-
-  interface SkipBarStyle extends CssResource {
-    String noExpand();
-  }
-
-  @UiField(provided = true)
-  Anchor skipNum;
-
-  @UiField(provided = true)
-  Anchor upArrow;
-
-  @UiField(provided = true)
-  Anchor downArrow;
-
-  @UiField SkipBarStyle style;
-
-  private final SkipManager manager;
-  private final CodeMirror cm;
-
-  private LineWidget lineWidget;
-  private TextMarker textMarker;
-  private SkipBar otherBar;
-
-  SkipBar(SkipManager manager, CodeMirror cm) {
-    this.manager = manager;
-    this.cm = cm;
-
-    skipNum = new Anchor(true);
-    upArrow = new Anchor(true);
-    downArrow = new Anchor(true);
-    initWidget(uiBinder.createAndBindUi(this));
-    addDomHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            cm.focus();
-          }
-        },
-        ClickEvent.getType());
-  }
-
-  void collapse(int start, int end, boolean attach) {
-    if (attach) {
-      boolean isNew = lineWidget == null;
-      Configuration cfg = Configuration.create().set("coverGutter", true).set("noHScroll", true);
-      if (start == 0) { // First line workaround
-        lineWidget = cm.addLineWidget(end + 1, getElement(), cfg.set("above", true));
-      } else {
-        lineWidget = cm.addLineWidget(start - 1, getElement(), cfg);
-      }
-      if (isNew) {
-        lineWidget.onFirstRedraw(
-            () -> {
-              int w = cm.getGutterElement().getOffsetWidth();
-              getElement().getStyle().setPaddingLeft(w, Unit.PX);
-            });
-      }
-    }
-
-    textMarker =
-        cm.markText(
-            Pos.create(start, 0),
-            Pos.create(end),
-            Configuration.create()
-                .set("collapsed", true)
-                .set("inclusiveLeft", true)
-                .set("inclusiveRight", true));
-
-    textMarker.on("beforeCursorEnter", this::expandAll);
-
-    int skipped = end - start + 1;
-    if (skipped <= UP_DOWN_THRESHOLD) {
-      addStyleName(style.noExpand());
-    } else {
-      upArrow.setHTML(PatchUtil.M.expandBefore(NUM_ROWS_TO_EXPAND));
-      downArrow.setHTML(PatchUtil.M.expandAfter(NUM_ROWS_TO_EXPAND));
-    }
-    skipNum.setText(PatchUtil.M.patchSkipRegion(Integer.toString(skipped)));
-  }
-
-  static void link(SkipBar barA, SkipBar barB) {
-    barA.otherBar = barB;
-    barB.otherBar = barA;
-  }
-
-  private void clearMarkerAndWidget() {
-    textMarker.clear();
-    lineWidget.clear();
-  }
-
-  void expandBefore(int cnt) {
-    expandSideBefore(cnt);
-
-    if (otherBar != null) {
-      otherBar.expandSideBefore(cnt);
-    }
-  }
-
-  private void expandSideBefore(int cnt) {
-    FromTo range = textMarker.find();
-    int oldStart = range.from().line();
-    int newStart = oldStart + cnt;
-    int end = range.to().line();
-    clearMarkerAndWidget();
-    collapse(newStart, end, true);
-    updateSelection();
-  }
-
-  void expandSideAll() {
-    clearMarkerAndWidget();
-    removeFromParent();
-  }
-
-  private void expandAfter() {
-    FromTo range = textMarker.find();
-    int start = range.from().line();
-    int oldEnd = range.to().line();
-    int newEnd = oldEnd - NUM_ROWS_TO_EXPAND;
-    boolean attach = start == 0;
-    if (attach) {
-      clearMarkerAndWidget();
-    } else {
-      textMarker.clear();
-    }
-    collapse(start, newEnd, attach);
-    updateSelection();
-  }
-
-  private void updateSelection() {
-    if (cm.somethingSelected()) {
-      FromTo sel = cm.getSelectedRange();
-      cm.setSelection(sel.from(), sel.to());
-    }
-  }
-
-  @UiHandler("skipNum")
-  void onExpandAll(@SuppressWarnings("unused") ClickEvent e) {
-    expandAll();
-    updateSelection();
-    if (otherBar != null) {
-      otherBar.expandAll();
-      otherBar.updateSelection();
-    }
-    cm.refresh();
-    cm.focus();
-  }
-
-  private void expandAll() {
-    expandSideAll();
-    if (otherBar != null) {
-      otherBar.expandSideAll();
-    }
-    manager.remove(this, otherBar);
-  }
-
-  @UiHandler("upArrow")
-  void onExpandBefore(@SuppressWarnings("unused") ClickEvent e) {
-    expandBefore(NUM_ROWS_TO_EXPAND);
-    if (otherBar != null) {
-      otherBar.expandBefore(NUM_ROWS_TO_EXPAND);
-    }
-    cm.refresh();
-    cm.focus();
-  }
-
-  @UiHandler("downArrow")
-  void onExpandAfter(@SuppressWarnings("unused") ClickEvent e) {
-    expandAfter();
-
-    if (otherBar != null) {
-      otherBar.expandAfter();
-    }
-    cm.refresh();
-    cm.focus();
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.ui.xml
deleted file mode 100644
index bf3c425..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.ui.xml
+++ /dev/null
@@ -1,52 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:style gss='false' type='com.google.gerrit.client.diff.SkipBar.SkipBarStyle'>
-    .skipBar {
-      background-color: #def;
-      height: 1.3em;
-      overflow: hidden;
-    }
-    .text {
-      display: table;
-      margin: 0 auto;
-      color: #777;
-      font-style: italic;
-      overflow: hidden;
-    }
-    .anchor {
-      color: inherit;
-      text-decoration: none;
-    }
-    .noExpand .arrow {
-      display: none;
-    }
-    .arrow {
-      font-family: Arial Unicode MS, sans-serif;
-    }
-  </ui:style>
-  <g:HTMLPanel addStyleNames='{style.skipBar}'>
-  <div class='{style.text}'>
-    <ui:msg>
-      <g:Anchor ui:field='upArrow' addStyleNames='{style.arrow} {style.anchor}' />
-      <g:Anchor ui:field='skipNum' addStyleNames='{style.anchor}' />
-      <g:Anchor ui:field='downArrow' addStyleNames=' {style.arrow} {style.anchor}' />
-    </ui:msg>
-  </div>
-  </g:HTMLPanel>
-</ui:UiBinder>
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipManager.java
deleted file mode 100644
index 533ba1f..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipManager.java
+++ /dev/null
@@ -1,149 +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.client.diff;
-
-import com.google.gerrit.client.diff.DiffInfo.Region;
-import com.google.gerrit.client.patches.SkippedLine;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo;
-import com.google.gwt.core.client.JsArray;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import net.codemirror.lib.CodeMirror;
-
-/** Collapses common regions with {@link SkipBar} for {@link SideBySide} and {@link Unified}. */
-class SkipManager {
-  private final Set<SkipBar> skipBars;
-  private final DiffScreen host;
-  private SkipBar line0;
-
-  SkipManager(DiffScreen host) {
-    this.host = host;
-    this.skipBars = new HashSet<>();
-  }
-
-  void render(int context, DiffInfo diff) {
-    if (context == DiffPreferencesInfo.WHOLE_FILE_CONTEXT) {
-      return;
-    }
-
-    List<SkippedLine> skips = new ArrayList<>();
-    int lineA = 0;
-    int lineB = 0;
-    JsArray<Region> regions = diff.content();
-    for (int i = 0; i < regions.length(); i++) {
-      Region current = regions.get(i);
-      if (current.ab() != null || current.common() || current.skip() > 0) {
-        int len =
-            current.skip() > 0
-                ? current.skip()
-                : (current.ab() != null ? current.ab() : current.b()).length();
-        if (i == 0 && len > context + 1) {
-          skips.add(new SkippedLine(0, 0, len - context));
-        } else if (i == regions.length() - 1 && len > context + 1) {
-          skips.add(new SkippedLine(lineA + context, lineB + context, len - context));
-        } else if (len > 2 * context + 1) {
-          skips.add(new SkippedLine(lineA + context, lineB + context, len - 2 * context));
-        }
-        lineA += len;
-        lineB += len;
-      } else {
-        lineA += current.a() != null ? current.a().length() : 0;
-        lineB += current.b() != null ? current.b().length() : 0;
-      }
-    }
-    skips = host.getCommentManager().splitSkips(context, skips);
-    renderSkips(skips, lineA, lineB);
-  }
-
-  private void renderSkips(List<SkippedLine> skips, int lineA, int lineB) {
-    if (!skips.isEmpty()) {
-      boolean isSideBySide = host.isSideBySide();
-      CodeMirror cmA = null;
-      if (isSideBySide) {
-        cmA = host.getCmFromSide(DisplaySide.A);
-      }
-      CodeMirror cmB = host.getCmFromSide(DisplaySide.B);
-
-      for (SkippedLine skip : skips) {
-        SkipBar barA = null;
-        SkipBar barB = newSkipBar(cmB, DisplaySide.B, skip);
-        skipBars.add(barB);
-        if (isSideBySide) {
-          barA = newSkipBar(cmA, DisplaySide.A, skip);
-          SkipBar.link(barA, barB);
-          skipBars.add(barA);
-        }
-
-        if (skip.getStartA() == 0 || skip.getStartB() == 0) {
-          if (isSideBySide) {
-            barA.upArrow.setVisible(false);
-          }
-          barB.upArrow.setVisible(false);
-          setLine0(barB);
-        } else if (skip.getStartA() + skip.getSize() == lineA
-            || skip.getStartB() + skip.getSize() == lineB) {
-          if (isSideBySide) {
-            barA.downArrow.setVisible(false);
-          }
-          barB.downArrow.setVisible(false);
-        }
-      }
-    }
-  }
-
-  private SkipBar newSkipBar(CodeMirror cm, DisplaySide side, SkippedLine skip) {
-    int start = host.getCmLine(side == DisplaySide.A ? skip.getStartA() : skip.getStartB(), side);
-    int end = start + skip.getSize() - 1;
-
-    SkipBar bar = new SkipBar(this, cm);
-    host.getDiffTable().add(bar);
-    bar.collapse(start, end, true);
-    return bar;
-  }
-
-  void ensureFirstLineIsVisible() {
-    if (line0 != null) {
-      line0.expandBefore(1);
-      line0 = null;
-    }
-  }
-
-  void removeAll() {
-    if (!skipBars.isEmpty()) {
-      for (SkipBar bar : skipBars) {
-        bar.expandSideAll();
-      }
-      line0 = null;
-    }
-  }
-
-  void remove(SkipBar a, SkipBar b) {
-    skipBars.remove(a);
-    skipBars.remove(b);
-    if (getLine0() == a || getLine0() == b) {
-      setLine0(null);
-    }
-  }
-
-  SkipBar getLine0() {
-    return line0;
-  }
-
-  void setLine0(SkipBar bar) {
-    line0 = bar;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.java
deleted file mode 100644
index 7bd9804..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.java
+++ /dev/null
@@ -1,383 +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.client.diff;
-
-import static java.lang.Double.POSITIVE_INFINITY;
-
-import com.google.gerrit.client.DiffObject;
-import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.diff.UnifiedChunkManager.LineRegionInfo;
-import com.google.gerrit.client.patches.PatchUtil;
-import com.google.gerrit.client.projects.ConfigInfoCache;
-import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.client.ui.InlineHyperlink;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DiffView;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArrayString;
-import com.google.gwt.core.client.Scheduler;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.event.dom.client.FocusEvent;
-import com.google.gwt.event.dom.client.FocusHandler;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.user.client.Window;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.ImageResourceRenderer;
-import com.google.gwt.user.client.ui.InlineHTML;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-import com.google.gwtexpui.safehtml.client.SafeHtml;
-import java.util.Collections;
-import java.util.List;
-import net.codemirror.lib.CodeMirror;
-import net.codemirror.lib.CodeMirror.LineHandle;
-import net.codemirror.lib.Configuration;
-import net.codemirror.lib.Pos;
-import net.codemirror.lib.ScrollInfo;
-
-public class Unified extends DiffScreen {
-  interface Binder extends UiBinder<FlowPanel, Unified> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  @UiField(provided = true)
-  UnifiedTable diffTable;
-
-  private CodeMirror cm;
-
-  private UnifiedChunkManager chunkManager;
-  private UnifiedCommentManager commentManager;
-
-  private boolean autoHideDiffTableHeader;
-
-  public Unified(
-      @Nullable Project.NameKey project,
-      DiffObject base,
-      DiffObject revision,
-      String path,
-      DisplaySide startSide,
-      int startLine) {
-    super(project, base, revision, path, startSide, startLine, DiffView.UNIFIED_DIFF);
-
-    diffTable = new UnifiedTable(this, base, revision, path);
-    add(uiBinder.createAndBindUi(this));
-    addDomHandler(GlobalKey.STOP_PROPAGATION, KeyPressEvent.getType());
-  }
-
-  @Override
-  ScreenLoadCallback<ConfigInfoCache.Entry> getScreenLoadCallback(
-      final CommentsCollections comments) {
-    return new ScreenLoadCallback<ConfigInfoCache.Entry>(Unified.this) {
-      @Override
-      protected void preDisplay(ConfigInfoCache.Entry result) {
-        commentManager =
-            new UnifiedCommentManager(
-                Unified.this,
-                getProject(),
-                base,
-                revision,
-                path,
-                result.getCommentLinkProcessor(),
-                getChangeStatus().isOpen());
-        setTheme(result.getTheme());
-        display(comments);
-        header.setupPrevNextFiles(comments);
-      }
-    };
-  }
-
-  @Override
-  public void onShowView() {
-    super.onShowView();
-
-    operation(
-        () -> {
-          resizeCodeMirror();
-          cm.refresh();
-        });
-    setLineLength(Patch.COMMIT_MSG.equals(path) ? 72 : prefs.lineLength());
-    diffTable.refresh();
-
-    if (getStartLine() == 0) {
-      DiffChunkInfo d = chunkManager.getFirst();
-      if (d != null) {
-        if (d.isEdit() && d.getSide() == DisplaySide.A) {
-          setStartSide(DisplaySide.B);
-        } else {
-          setStartSide(d.getSide());
-        }
-        setStartLine(chunkManager.getCmLine(d.getStart(), d.getSide()) + 1);
-      }
-    }
-    if (getStartSide() != null && getStartLine() > 0) {
-      cm.scrollToLine(chunkManager.getCmLine(getStartLine() - 1, getStartSide()));
-      cm.focus();
-    } else {
-      cm.setCursor(Pos.create(0));
-      cm.focus();
-    }
-    if (Gerrit.isSignedIn() && prefs.autoReview()) {
-      header.autoReview();
-    }
-    prefetchNextFile();
-  }
-
-  @Override
-  void registerCmEvents(CodeMirror cm) {
-    super.registerCmEvents(cm);
-
-    cm.on(
-        "scroll",
-        () -> {
-          ScrollInfo si = cm.getScrollInfo();
-          if (autoHideDiffTableHeader) {
-            updateDiffTableHeader(si);
-          }
-        });
-    maybeRegisterRenderEntireFileKeyMap(cm);
-  }
-
-  @Override
-  public void registerKeys() {
-    super.registerKeys();
-
-    registerHandlers();
-  }
-
-  @Override
-  FocusHandler getFocusHandler() {
-    return new FocusHandler() {
-      @Override
-      public void onFocus(FocusEvent event) {
-        cm.focus();
-      }
-    };
-  }
-
-  private void display(CommentsCollections comments) {
-    DiffInfo diff = getDiff();
-    setThemeStyles(prefs.theme().isDark());
-    setShowIntraline(prefs.intralineDifference());
-    if (prefs.showLineNumbers()) {
-      diffTable.addStyleName(Resources.I.diffTableStyle().showLineNumbers());
-    }
-
-    cm =
-        newCm(diff.metaA() == null ? diff.metaB() : diff.metaA(), diff.textUnified(), diffTable.cm);
-    setShowTabs(prefs.showTabs());
-
-    chunkManager = new UnifiedChunkManager(this, cm, diffTable.scrollbar);
-
-    operation(
-        () -> {
-          // Estimate initial CodeMirror height, fixed up in onShowView.
-          int height = Window.getClientHeight() - (Gerrit.getHeaderFooterHeight() + 18);
-          cm.setHeight(height);
-
-          render(diff);
-          commentManager.render(comments, prefs.expandAllComments());
-          skipManager.render(prefs.context(), diff);
-        });
-
-    registerCmEvents(cm);
-
-    setPrefsAction(new PreferencesAction(this, prefs));
-    header.init(getPrefsAction(), getSideBySideDiffLink(), diff.unifiedWebLinks());
-    setAutoHideDiffHeader(prefs.autoHideDiffTableHeader());
-
-    setupSyntaxHighlighting();
-  }
-
-  private List<InlineHyperlink> getSideBySideDiffLink() {
-    InlineHyperlink toSideBySideDiffLink = new InlineHyperlink();
-    toSideBySideDiffLink.setHTML(
-        new ImageResourceRenderer().render(Gerrit.RESOURCES.sideBySideDiff()));
-    toSideBySideDiffLink.setTargetHistoryToken(
-        Dispatcher.toSideBySide(getProject(), base, revision, path));
-    toSideBySideDiffLink.setTitle(PatchUtil.C.sideBySideDiff());
-    return Collections.singletonList(toSideBySideDiffLink);
-  }
-
-  @Override
-  CodeMirror newCm(DiffInfo.FileMeta meta, String contents, Element parent) {
-    JsArrayString gutters = JavaScriptObject.createArray().cast();
-    gutters.push(UnifiedTable.style.lineNumbersLeft());
-    gutters.push(UnifiedTable.style.lineNumbersRight());
-
-    return CodeMirror.create(
-        parent,
-        Configuration.create()
-            .set("cursorBlinkRate", prefs.cursorBlinkRate())
-            .set("cursorHeight", 0.85)
-            .set("gutters", gutters)
-            .set("inputStyle", "textarea")
-            .set("keyMap", "vim_ro")
-            .set("lineNumbers", false)
-            .set("lineWrapping", prefs.lineWrapping())
-            .set("matchBrackets", prefs.matchBrackets())
-            .set("mode", getFileSize() == FileSize.SMALL ? getContentType(meta) : null)
-            .set("readOnly", true)
-            .set("scrollbarStyle", "overlay")
-            .set("styleSelectedText", true)
-            .set("showTrailingSpace", prefs.showWhitespaceErrors())
-            .set("tabSize", prefs.tabSize())
-            .set("theme", prefs.theme().name().toLowerCase())
-            .set("value", meta != null ? contents : "")
-            .set("viewportMargin", renderEntireFile() ? POSITIVE_INFINITY : 10));
-  }
-
-  @Override
-  void setShowLineNumbers(boolean b) {
-    super.setShowLineNumbers(b);
-
-    cm.refresh();
-  }
-
-  private void setLineNumber(DisplaySide side, int cmLine, Integer line, String styleName) {
-    SafeHtml html = SafeHtml.asis(line != null ? line.toString() : "&nbsp;");
-    InlineHTML gutter = new InlineHTML(html);
-    diffTable.add(gutter);
-    gutter.setStyleName(styleName);
-    cm.setGutterMarker(
-        cmLine,
-        side == DisplaySide.A
-            ? UnifiedTable.style.lineNumbersLeft()
-            : UnifiedTable.style.lineNumbersRight(),
-        gutter.getElement());
-  }
-
-  void setLineNumber(DisplaySide side, int cmLine, int line) {
-    setLineNumber(side, cmLine, line, UnifiedTable.style.unifiedLineNumber());
-  }
-
-  void setLineNumberEmpty(DisplaySide side, int cmLine) {
-    setLineNumber(side, cmLine, null, UnifiedTable.style.unifiedLineNumberEmpty());
-  }
-
-  @Override
-  void setSyntaxHighlighting(boolean b) {
-    final DiffInfo diff = getDiff();
-    if (b) {
-      injectMode(
-          diff,
-          new AsyncCallback<Void>() {
-            @Override
-            public void onSuccess(Void result) {
-              if (prefs.syntaxHighlighting()) {
-                cm.setOption(
-                    "mode", getContentType(diff.metaA() == null ? diff.metaB() : diff.metaA()));
-              }
-            }
-
-            @Override
-            public void onFailure(Throwable caught) {
-              prefs.syntaxHighlighting(false);
-            }
-          });
-    } else {
-      cm.setOption("mode", (String) null);
-    }
-  }
-
-  @Override
-  void setAutoHideDiffHeader(boolean autoHide) {
-    if (autoHide) {
-      updateDiffTableHeader(cm.getScrollInfo());
-    } else {
-      diffTable.setHeaderVisible(true);
-    }
-    autoHideDiffTableHeader = autoHide;
-  }
-
-  private void updateDiffTableHeader(ScrollInfo si) {
-    if (si.top() == 0) {
-      diffTable.setHeaderVisible(true);
-    } else if (si.top() > 0.5 * si.clientHeight()) {
-      diffTable.setHeaderVisible(false);
-    }
-  }
-
-  @Override
-  Runnable updateActiveLine(CodeMirror cm) {
-    return () -> {
-      // The rendering of active lines has to be deferred. Reflow
-      // caused by adding and removing styles chokes Firefox when arrow
-      // key (or j/k) is held down. Performance on Chrome is fine
-      // without the deferral.
-      //
-      Scheduler.get()
-          .scheduleDeferred(
-              () -> {
-                LineHandle handle = cm.getLineHandleVisualStart(cm.getCursor("end").line());
-                cm.extras().activeLine(handle);
-              });
-    };
-  }
-
-  @Override
-  CodeMirror getCmFromSide(DisplaySide side) {
-    return cm;
-  }
-
-  @Override
-  int getCmLine(int line, DisplaySide side) {
-    return chunkManager.getCmLine(line, side);
-  }
-
-  LineRegionInfo getLineRegionInfoFromCmLine(int cmLine) {
-    return chunkManager.getLineRegionInfoFromCmLine(cmLine);
-  }
-
-  @Override
-  void operation(Runnable apply) {
-    cm.operation(apply::run);
-  }
-
-  @Override
-  CodeMirror[] getCms() {
-    return new CodeMirror[] {cm};
-  }
-
-  @Override
-  UnifiedTable getDiffTable() {
-    return diffTable;
-  }
-
-  @Override
-  UnifiedChunkManager getChunkManager() {
-    return chunkManager;
-  }
-
-  @Override
-  UnifiedCommentManager getCommentManager() {
-    return commentManager;
-  }
-
-  @Override
-  boolean isSideBySide() {
-    return false;
-  }
-
-  @Override
-  String getLineNumberClassName() {
-    return UnifiedTable.style.unifiedLineNumber();
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.ui.xml
deleted file mode 100644
index 85f46a6..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.ui.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'
-    xmlns:d='urn:import:com.google.gerrit.client.diff'>
-  <ui:style>
-    .unified {
-      margin-left: -5px;
-      margin-right: -5px;
-    }
-  </ui:style>
-  <g:FlowPanel styleName='{style.unified}'>
-    <d:Header ui:field='header'/>
-    <d:UnifiedTable ui:field='diffTable'/>
-  </g:FlowPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedChunkManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedChunkManager.java
deleted file mode 100644
index 98ad023..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedChunkManager.java
+++ /dev/null
@@ -1,330 +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.client.diff;
-
-import static java.util.Comparator.comparing;
-
-import com.google.gerrit.client.diff.DiffInfo.Region;
-import com.google.gerrit.client.diff.DiffInfo.Span;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.core.client.JsArrayString;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.NativeEvent;
-import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.EventListener;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-import net.codemirror.lib.CodeMirror;
-import net.codemirror.lib.CodeMirror.LineClassWhere;
-import net.codemirror.lib.Configuration;
-import net.codemirror.lib.Pos;
-
-/** Colors modified regions for {@link Unified}. */
-class UnifiedChunkManager extends ChunkManager {
-  private static final JavaScriptObject focus = initOnClick();
-
-  private static native JavaScriptObject initOnClick() /*-{
-    return $entry(function(e){
-      @com.google.gerrit.client.diff.UnifiedChunkManager::focus(
-        Lcom/google/gwt/dom/client/NativeEvent;)(e)
-    });
-  }-*/;
-
-  private List<UnifiedDiffChunkInfo> chunks;
-
-  @Override
-  DiffChunkInfo getFirst() {
-    return !chunks.isEmpty() ? chunks.get(0) : null;
-  }
-
-  private static void focus(NativeEvent event) {
-    Element e = Element.as(event.getEventTarget());
-    for (e = DOM.getParent(e); e != null; e = DOM.getParent(e)) {
-      EventListener l = DOM.getEventListener(e);
-      if (l instanceof Unified) {
-        ((Unified) l).getCmFromSide(DisplaySide.A).focus();
-        event.stopPropagation();
-      }
-    }
-  }
-
-  static void focusOnClick(Element e) {
-    onClick(e, focus);
-  }
-
-  private final Unified host;
-  private final CodeMirror cm;
-
-  UnifiedChunkManager(Unified host, CodeMirror cm, Scrollbar scrollbar) {
-    super(scrollbar);
-
-    this.host = host;
-    this.cm = cm;
-  }
-
-  @Override
-  void render(DiffInfo diff) {
-    super.render();
-
-    chunks = new ArrayList<>();
-
-    int cmLine = 0;
-    boolean useIntralineBg = diff.metaA() == null || diff.metaB() == null;
-
-    for (Region current : Natives.asList(diff.content())) {
-      int origLineA = lineMapper.getLineA();
-      int origLineB = lineMapper.getLineB();
-      if (current.ab() != null) {
-        int length = current.ab().length();
-        lineMapper.appendCommon(length);
-        for (int i = 0; i < length; i++) {
-          host.setLineNumber(DisplaySide.A, cmLine + i, origLineA + i + 1);
-          host.setLineNumber(DisplaySide.B, cmLine + i, origLineB + i + 1);
-        }
-        cmLine += length;
-      } else if (current.skip() > 0) {
-        lineMapper.appendCommon(current.skip());
-        cmLine += current.skip(); // Maybe current.ab().length();
-      } else if (current.common()) {
-        lineMapper.appendCommon(current.b().length());
-        cmLine += current.b().length();
-      } else {
-        cmLine += render(current, cmLine, useIntralineBg);
-      }
-    }
-    host.setLineNumber(DisplaySide.A, cmLine, lineMapper.getLineA() + 1);
-    host.setLineNumber(DisplaySide.B, cmLine, lineMapper.getLineB() + 1);
-  }
-
-  private int render(Region region, int cmLine, boolean useIntralineBg) {
-    int startA = lineMapper.getLineA();
-    int startB = lineMapper.getLineB();
-
-    JsArrayString a = region.a();
-    JsArrayString b = region.b();
-    int aLen = a != null ? a.length() : 0;
-    int bLen = b != null ? b.length() : 0;
-    boolean insertOrDelete = a == null || b == null;
-
-    colorLines(
-        cm,
-        insertOrDelete && !useIntralineBg
-            ? UnifiedTable.style.diffDelete()
-            : UnifiedTable.style.intralineDelete(),
-        cmLine,
-        aLen);
-    colorLines(
-        cm,
-        insertOrDelete && !useIntralineBg
-            ? UnifiedTable.style.diffInsert()
-            : UnifiedTable.style.intralineInsert(),
-        cmLine + aLen,
-        bLen);
-    markEdit(DisplaySide.A, cmLine, a, region.editA());
-    markEdit(DisplaySide.B, cmLine + aLen, b, region.editB());
-    addGutterTag(region, cmLine); // TODO: verify addGutterTag
-    lineMapper.appendReplace(aLen, bLen);
-
-    int endA = lineMapper.getLineA() - 1;
-    int endB = lineMapper.getLineB() - 1;
-    if (aLen > 0) {
-      addDiffChunk(DisplaySide.A, endA, aLen, cmLine, bLen > 0);
-      for (int j = 0; j < aLen; j++) {
-        host.setLineNumber(DisplaySide.A, cmLine + j, startA + j + 1);
-        host.setLineNumberEmpty(DisplaySide.B, cmLine + j);
-      }
-    }
-    if (bLen > 0) {
-      addDiffChunk(DisplaySide.B, endB, bLen, cmLine + aLen, aLen > 0);
-      for (int j = 0; j < bLen; j++) {
-        host.setLineNumberEmpty(DisplaySide.A, cmLine + aLen + j);
-        host.setLineNumber(DisplaySide.B, cmLine + aLen + j, startB + j + 1);
-      }
-    }
-    return aLen + bLen;
-  }
-
-  private void addGutterTag(Region region, int cmLine) {
-    if (region.a() == null) {
-      scrollbar.insert(cm, cmLine, region.b().length());
-    } else if (region.b() == null) {
-      scrollbar.delete(cm, cm, cmLine, region.a().length());
-    } else {
-      scrollbar.edit(cm, cmLine, region.b().length());
-    }
-  }
-
-  private void markEdit(DisplaySide side, int startLine, JsArrayString lines, JsArray<Span> edits) {
-    if (lines == null || edits == null) {
-      return;
-    }
-
-    EditIterator iter = new EditIterator(lines, startLine);
-    Configuration bg =
-        Configuration.create().set("className", getIntralineBgFromSide(side)).set("readOnly", true);
-
-    Configuration diff =
-        Configuration.create().set("className", getDiffColorFromSide(side)).set("readOnly", true);
-
-    Pos last = Pos.create(0, 0);
-    for (Span span : Natives.asList(edits)) {
-      Pos from = iter.advance(span.skip());
-      Pos to = iter.advance(span.mark());
-      if (from.line() == last.line()) {
-        getMarkers().add(cm.markText(last, from, bg));
-      } else {
-        getMarkers().add(cm.markText(Pos.create(from.line(), 0), from, bg));
-      }
-      getMarkers().add(cm.markText(from, to, diff));
-      last = to;
-      colorLines(cm, LineClassWhere.BACKGROUND, getDiffColorFromSide(side), from.line(), to.line());
-    }
-  }
-
-  private String getIntralineBgFromSide(DisplaySide side) {
-    return side == DisplaySide.A
-        ? UnifiedTable.style.intralineDelete()
-        : UnifiedTable.style.intralineInsert();
-  }
-
-  private String getDiffColorFromSide(DisplaySide side) {
-    return side == DisplaySide.A
-        ? UnifiedTable.style.diffDelete()
-        : UnifiedTable.style.diffInsert();
-  }
-
-  private void addDiffChunk(
-      DisplaySide side, int chunkEnd, int chunkSize, int cmLine, boolean edit) {
-    chunks.add(new UnifiedDiffChunkInfo(side, chunkEnd - chunkSize + 1, chunkEnd, cmLine, edit));
-  }
-
-  @Override
-  Runnable diffChunkNav(CodeMirror cm, Direction dir) {
-    return () -> {
-      int line = cm.extras().hasActiveLine() ? cm.getLineNumber(cm.extras().activeLine()) : 0;
-      int res =
-          Collections.binarySearch(
-              chunks,
-              new UnifiedDiffChunkInfo(cm.side(), 0, 0, line, false),
-              getDiffChunkComparatorCmLine());
-      diffChunkNavHelper(chunks, host, res, dir);
-    };
-  }
-
-  /** Diff chunks are ordered by their starting lines in CodeMirror */
-  private Comparator<UnifiedDiffChunkInfo> getDiffChunkComparatorCmLine() {
-    return comparing(UnifiedDiffChunkInfo::getCmLine);
-  }
-
-  @Override
-  int getCmLine(int line, DisplaySide side) {
-    int res =
-        Collections.binarySearch(
-            chunks,
-            new UnifiedDiffChunkInfo(side, line, 0, 0, false), // Dummy DiffChunkInfo
-            getDiffChunkComparator());
-    if (res >= 0) {
-      return chunks.get(res).getCmLine();
-    }
-    // The line might be within a DiffChunk
-    res = -res - 1;
-    if (res > 0) {
-      UnifiedDiffChunkInfo info = chunks.get(res - 1);
-      if (side == DisplaySide.A && info.isEdit() && info.getSide() == DisplaySide.B) {
-        // Need to use the start and cmLine of the deletion chunk
-        UnifiedDiffChunkInfo delete = chunks.get(res - 2);
-        if (line <= delete.getEnd()) {
-          return delete.getCmLine() + line - delete.getStart();
-        }
-        // Need to add the length of the insertion chunk
-        return delete.getCmLine() + line - delete.getStart() + info.getEnd() - info.getStart() + 1;
-      } else if (side == info.getSide()) {
-        return info.getCmLine() + line - info.getStart();
-      } else {
-        return info.getCmLine() + lineMapper.lineOnOther(side, line).getLine() - info.getStart();
-      }
-    }
-    return line;
-  }
-
-  LineRegionInfo getLineRegionInfoFromCmLine(int cmLine) {
-    int res =
-        Collections.binarySearch(
-            chunks,
-            new UnifiedDiffChunkInfo(DisplaySide.A, 0, 0, cmLine, false), // Dummy DiffChunkInfo
-            getDiffChunkComparatorCmLine());
-    if (res >= 0) { // The line is right at the start of a diff chunk.
-      UnifiedDiffChunkInfo info = chunks.get(res);
-      return new LineRegionInfo(info.getStart(), displaySideToRegionType(info.getSide()));
-    }
-    // The line might be within or after a diff chunk.
-    res = -res - 1;
-    if (res > 0) {
-      UnifiedDiffChunkInfo info = chunks.get(res - 1);
-      int lineOnInfoSide = info.getStart() + cmLine - info.getCmLine();
-      if (lineOnInfoSide > info.getEnd()) { // After a diff chunk
-        if (info.getSide() == DisplaySide.A) {
-          // For the common region after a deletion chunk, associate the line
-          // on side B with a common region.
-          return new LineRegionInfo(
-              lineMapper.lineOnOther(DisplaySide.A, lineOnInfoSide).getLine(), RegionType.COMMON);
-        }
-        return new LineRegionInfo(lineOnInfoSide, RegionType.COMMON);
-      }
-      // Within a diff chunk
-      return new LineRegionInfo(lineOnInfoSide, displaySideToRegionType(info.getSide()));
-    }
-    // The line is before any diff chunk, so it always equals cmLine and
-    // belongs to a common region.
-    return new LineRegionInfo(cmLine, RegionType.COMMON);
-  }
-
-  enum RegionType {
-    INSERT,
-    DELETE,
-    COMMON,
-  }
-
-  private static RegionType displaySideToRegionType(DisplaySide side) {
-    return side == DisplaySide.A ? RegionType.DELETE : RegionType.INSERT;
-  }
-
-  /**
-   * Helper class to associate a line in the original file with the type of the region it belongs
-   * to.
-   *
-   * @field line The 0-based line number in the original file. Note that this might be different
-   *     from the line number shown in CodeMirror.
-   * @field type The type of the region the line belongs to. Can be INSERT, DELETE or COMMON.
-   */
-  static class LineRegionInfo {
-    final int line;
-    final RegionType type;
-
-    LineRegionInfo(int line, RegionType type) {
-      this.line = line;
-      this.type = type;
-    }
-
-    DisplaySide getSide() {
-      // Always return DisplaySide.B for INSERT or COMMON
-      return type == RegionType.DELETE ? DisplaySide.A : DisplaySide.B;
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedCommentGroup.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedCommentGroup.java
deleted file mode 100644
index 6d5fba3..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedCommentGroup.java
+++ /dev/null
@@ -1,88 +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.client.diff;
-
-import com.google.gwt.user.client.Timer;
-import net.codemirror.lib.CodeMirror;
-
-/**
- * LineWidget attached to a CodeMirror container.
- *
- * <p>When a comment is placed on a line a CommentWidget is created. The group tracks all comment
- * boxes on a line in unified diff view.
- */
-class UnifiedCommentGroup extends CommentGroup {
-  UnifiedCommentGroup(UnifiedCommentManager manager, CodeMirror cm, DisplaySide side, int line) {
-    super(manager, cm, side, line);
-  }
-
-  @Override
-  void remove(DraftBox box) {
-    super.remove(box);
-
-    if (0 < getBoxCount()) {
-      resize();
-    } else {
-      detach();
-    }
-  }
-
-  @Override
-  void init(DiffTable parent) {
-    if (getLineWidget() == null) {
-      attach(parent);
-    }
-  }
-
-  @Override
-  void handleRedraw() {
-    getLineWidget()
-        .onRedraw(
-            () -> {
-              if (canComputeHeight()) {
-                if (getResizeTimer() != null) {
-                  getResizeTimer().cancel();
-                  setResizeTimer(null);
-                }
-                reportHeightChange();
-              } else if (getResizeTimer() == null) {
-                setResizeTimer(
-                    new Timer() {
-                      @Override
-                      public void run() {
-                        if (canComputeHeight()) {
-                          cancel();
-                          setResizeTimer(null);
-                          reportHeightChange();
-                        }
-                      }
-                    });
-                getResizeTimer().scheduleRepeating(5);
-              }
-            });
-  }
-
-  @Override
-  void resize() {
-    if (getLineWidget() != null) {
-      reportHeightChange();
-    }
-  }
-
-  private void reportHeightChange() {
-    getLineWidget().changed();
-    updateSelection();
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedCommentManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedCommentManager.java
deleted file mode 100644
index c92075f..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedCommentManager.java
+++ /dev/null
@@ -1,203 +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.client.diff;
-
-import com.google.gerrit.client.DiffObject;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.CommentInfo;
-import com.google.gerrit.client.diff.LineMapper.LineOnOtherInfo;
-import com.google.gerrit.client.diff.UnifiedChunkManager.LineRegionInfo;
-import com.google.gerrit.client.diff.UnifiedChunkManager.RegionType;
-import com.google.gerrit.client.ui.CommentLinkProcessor;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.SortedMap;
-import java.util.TreeMap;
-import net.codemirror.lib.CodeMirror;
-import net.codemirror.lib.Pos;
-import net.codemirror.lib.TextMarker.FromTo;
-
-/** Tracks comment widgets for {@link Unified}. */
-class UnifiedCommentManager extends CommentManager {
-
-  private final SortedMap<Integer, CommentGroup> mergedMap;
-
-  // In Unified, a CodeMirror line can have up to two CommentGroups - one for
-  // the base side and one for the revision, so we need to keep track of the
-  // duplicates and replace the entries in mergedMap on draft removal.
-  private final Map<Integer, CommentGroup> duplicates;
-
-  UnifiedCommentManager(
-      Unified host,
-      @Nullable Project.NameKey project,
-      DiffObject base,
-      PatchSet.Id revision,
-      String path,
-      CommentLinkProcessor clp,
-      boolean open) {
-    super(host, project, base, revision, path, clp, open);
-    mergedMap = new TreeMap<>();
-    duplicates = new HashMap<>();
-  }
-
-  @Override
-  SortedMap<Integer, CommentGroup> getMapForNav(DisplaySide side) {
-    return mergedMap;
-  }
-
-  @Override
-  void clearLine(DisplaySide side, int line, CommentGroup group) {
-    super.clearLine(side, line, group);
-
-    if (mergedMap.get(line) == group) {
-      mergedMap.remove(line);
-      if (duplicates.containsKey(line)) {
-        mergedMap.put(line, duplicates.remove(line));
-      }
-    }
-  }
-
-  @Override
-  void newDraftOnGutterClick(CodeMirror cm, String gutterClass, int cmLinePlusOne) {
-    if (!Gerrit.isSignedIn()) {
-      signInCallback(cm).run();
-    } else {
-      LineRegionInfo info = ((Unified) host).getLineRegionInfoFromCmLine(cmLinePlusOne - 1);
-      DisplaySide side =
-          gutterClass.equals(UnifiedTable.style.lineNumbersLeft()) ? DisplaySide.A : DisplaySide.B;
-      int line = info.line;
-      if (info.getSide() != side) {
-        line = host.lineOnOther(info.getSide(), line).getLine();
-      }
-      insertNewDraft(side, line + 1);
-    }
-  }
-
-  @Override
-  CommentGroup getCommentGroupOnActiveLine(CodeMirror cm) {
-    CommentGroup group = null;
-    if (cm.extras().hasActiveLine()) {
-      int cmLinePlusOne = cm.getLineNumber(cm.extras().activeLine()) + 1;
-      LineRegionInfo info = ((Unified) host).getLineRegionInfoFromCmLine(cmLinePlusOne - 1);
-      CommentGroup forSide = map(info.getSide()).get(cmLinePlusOne);
-      group = forSide == null ? map(info.getSide().otherSide()).get(cmLinePlusOne) : forSide;
-    }
-    return group;
-  }
-
-  @Override
-  Collection<Integer> getLinesWithCommentGroups() {
-    return mergedMap.tailMap(1).keySet();
-  }
-
-  @Override
-  String getTokenSuffixForActiveLine(CodeMirror cm) {
-    int cmLinePlusOne = cm.getLineNumber(cm.extras().activeLine()) + 1;
-    LineRegionInfo info = ((Unified) host).getLineRegionInfoFromCmLine(cmLinePlusOne - 1);
-    return (info.getSide() == DisplaySide.A ? "a" : "") + cmLinePlusOne;
-  }
-
-  @Override
-  void newDraft(CodeMirror cm) {
-    if (cm.somethingSelected()) {
-      FromTo fromTo = adjustSelection(cm);
-      Pos from = fromTo.from();
-      Pos to = fromTo.to();
-      Unified unified = (Unified) host;
-      UnifiedChunkManager manager = unified.getChunkManager();
-      LineRegionInfo fromInfo = unified.getLineRegionInfoFromCmLine(from.line());
-      LineRegionInfo toInfo = unified.getLineRegionInfoFromCmLine(to.line());
-      DisplaySide side = toInfo.getSide();
-
-      // Handle special cases in selections that span multiple regions. Force
-      // start line to be on the same side as the end line.
-      if ((fromInfo.type == RegionType.INSERT || fromInfo.type == RegionType.COMMON)
-          && toInfo.type == RegionType.DELETE) {
-        LineOnOtherInfo infoOnSideA = manager.lineMapper.lineOnOther(DisplaySide.B, fromInfo.line);
-        int startLineOnSideA = infoOnSideA.getLine();
-        if (infoOnSideA.isAligned()) {
-          from.line(startLineOnSideA);
-        } else {
-          from.line(startLineOnSideA + 1);
-        }
-        from.ch(0);
-        to.line(toInfo.line);
-      } else if (fromInfo.type == RegionType.DELETE && toInfo.type == RegionType.INSERT) {
-        LineOnOtherInfo infoOnSideB = manager.lineMapper.lineOnOther(DisplaySide.A, fromInfo.line);
-        int startLineOnSideB = infoOnSideB.getLine();
-        if (infoOnSideB.isAligned()) {
-          from.line(startLineOnSideB);
-        } else {
-          from.line(startLineOnSideB + 1);
-        }
-        from.ch(0);
-        to.line(toInfo.line);
-      } else if (fromInfo.type == RegionType.DELETE && toInfo.type == RegionType.COMMON) {
-        int toLineOnSideA = manager.lineMapper.lineOnOther(DisplaySide.B, toInfo.line).getLine();
-        from.line(fromInfo.line);
-        // Force the end line to be on the same side as the start line.
-        to.line(toLineOnSideA);
-        side = DisplaySide.A;
-      } else { // Common case
-        from.line(fromInfo.line);
-        to.line(toInfo.line);
-      }
-
-      addDraftBox(
-              side,
-              CommentInfo.create(
-                  getPath(),
-                  getStoredSideFromDisplaySide(side),
-                  to.line() + 1,
-                  CommentRange.create(fromTo),
-                  false))
-          .setEdit(true);
-      cm.setCursor(Pos.create(host.getCmLine(to.line(), side), to.ch()));
-      cm.setSelection(cm.getCursor());
-    } else {
-      int cmLine = cm.getLineNumber(cm.extras().activeLine());
-      LineRegionInfo info = ((Unified) host).getLineRegionInfoFromCmLine(cmLine);
-      insertNewDraft(info.getSide(), cmLine + 1);
-    }
-  }
-
-  @Override
-  CommentGroup group(DisplaySide side, int cmLinePlusOne) {
-    Map<Integer, CommentGroup> map = map(side);
-    CommentGroup existing = map.get(cmLinePlusOne);
-    if (existing != null) {
-      return existing;
-    }
-
-    UnifiedCommentGroup g =
-        new UnifiedCommentGroup(this, host.getCmFromSide(side), side, cmLinePlusOne);
-    map.put(cmLinePlusOne, g);
-    if (mergedMap.containsKey(cmLinePlusOne)) {
-      duplicates.put(cmLinePlusOne, mergedMap.remove(cmLinePlusOne));
-    }
-    mergedMap.put(cmLinePlusOne, g);
-
-    if (isAttached()) {
-      g.init(host.getDiffTable());
-      g.handleRedraw();
-    }
-
-    return g;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedDiffChunkInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedDiffChunkInfo.java
deleted file mode 100644
index dc827cb..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedDiffChunkInfo.java
+++ /dev/null
@@ -1,29 +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.client.diff;
-
-public class UnifiedDiffChunkInfo extends DiffChunkInfo {
-
-  private int cmLine;
-
-  UnifiedDiffChunkInfo(DisplaySide side, int start, int end, int cmLine, boolean edit) {
-    super(side, start, end, edit);
-    this.cmLine = cmLine;
-  }
-
-  int getCmLine() {
-    return cmLine;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedTable.java
deleted file mode 100644
index 2d5df63..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedTable.java
+++ /dev/null
@@ -1,84 +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.client.diff;
-
-import com.google.gerrit.client.DiffObject;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.resources.client.CssResource;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.user.client.ui.HTMLPanel;
-
-/**
- * A table with one row and one column to hold a unified CodeMirror displaying the files to be
- * compared.
- */
-class UnifiedTable extends DiffTable {
-  interface Binder extends UiBinder<HTMLPanel, UnifiedTable> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  interface DiffTableStyle extends CssResource {
-    String intralineInsert();
-
-    String intralineDelete();
-
-    String diffInsert();
-
-    String diffDelete();
-
-    String unifiedLineNumber();
-
-    String unifiedLineNumberEmpty();
-
-    String lineNumbersLeft();
-
-    String lineNumbersRight();
-  }
-
-  private Unified parent;
-  @UiField Element cm;
-  @UiField static DiffTableStyle style;
-
-  UnifiedTable(Unified parent, DiffObject base, DiffObject revision, String path) {
-    super(parent, base, revision, path);
-
-    initWidget(uiBinder.createAndBindUi(this));
-    this.parent = parent;
-  }
-
-  @Override
-  void setHideEmptyPane(boolean hide) {}
-
-  @Override
-  boolean isVisibleA() {
-    return true;
-  }
-
-  @Override
-  Unified getDiffScreen() {
-    return parent;
-  }
-
-  @Override
-  int getHeaderHeight() {
-    int h = patchSetSelectBoxA.getOffsetHeight() + patchSetSelectBoxB.getOffsetHeight();
-    if (hasHeader()) {
-      h += diffHeaderRow.getOffsetHeight();
-    }
-    return h;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedTable.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedTable.ui.xml
deleted file mode 100644
index c2cefe4..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedTable.ui.xml
+++ /dev/null
@@ -1,152 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'
-    xmlns:d='urn:import:com.google.gerrit.client.diff'>
-  <ui:with field='res' type='com.google.gerrit.client.diff.Resources'/>
-  <ui:style gss='false' type='com.google.gerrit.client.diff.UnifiedTable.DiffTableStyle'>
-    @external .CodeMirror, .CodeMirror-selectedtext;
-    @external .CodeMirror-vscrollbar .CodeMirror-scroll;
-    @external .CodeMirror-dialog-bottom;
-    @external .CodeMirror-cursor;
-
-    @external .dark, .unifiedLineNumber, .noIntraline, .showLineNumbers;
-
-    .difftable .patchSetNav,
-    .difftable .CodeMirror {
-      -webkit-touch-callout: none;
-      -webkit-user-select: none;
-      -khtml-user-select: none;
-      -moz-user-select: none;
-      -ms-user-select: none;
-    }
-
-    .difftable .CodeMirror pre {
-      overflow: visible;
-      border-right: 0;
-      width: auto;
-    }
-
-    /* Preserve space for underscores. If this changes
-     * see ChunkManager.addPadding() and adjust there.
-     */
-    .difftable .CodeMirror pre,
-    .difftable .CodeMirror pre span {
-      padding-bottom: 1px;
-    }
-    .table {
-      width: 100%;
-      table-layout: fixed;
-      border-spacing: 0;
-    }
-    .table td { padding: 0 }
-
-    /* Hide scrollbars. */
-    .difftable .CodeMirror-scroll { padding-right: 0; }
-    .difftable .CodeMirror-vscrollbar { display: none !important; }
-
-    .diffDelete { background-color: #faa; }
-    .diffInsert { background-color: #9f9; }
-    .intralineDelete { background-color: #fee; }
-    .intralineInsert { background-color: #dfd; }
-    .noIntraline .intralineDelete { background-color: #faa; }
-    .noIntraline .intralineInsert { background-color: #9f9; }
-
-    .dark .diffDelete { background-color: #400; }
-    .dark .diffInsert { background-color: #444; }
-    .dark .intralineDelete { background-color: #888; }
-    .dark .intralineInsert { background-color: #bbb; }
-    .dark .noIntraline .intralineDelete { background-color: #400; }
-    .dark .noIntraline .intralineInsert { background-color: #444; }
-
-    .patchSetNav, .diff_header {
-      background-color: #f7f7f7;
-      line-height: 1;
-    }
-
-    .difftable .CodeMirror-selectedtext {
-      background-color: inherit !important;
-    }
-    .difftable .CodeMirror div.CodeMirror-cursor {
-      border-left: 2px solid black;
-    }
-    .difftable .CodeMirror-dialog-bottom {
-      border-top: 0;
-      border-left: 1px solid #000;
-      border-bottom: 1px solid #000;
-      background-color: #f7f7f7;
-      top: 0;
-      right: 0;
-      bottom: auto;
-      left: auto;
-    }
-    .showLineNumbers .lineNumbersLeft, .showLineNumbers .lineNumbersRight {
-      min-width: 20px;
-      width: 3em; /* TODO: This needs to be set based on number of lines */
-    }
-    .showLineNumbers .lineNumbersLeft {
-      border-right: 1px solid #ddd;
-    }
-    .unifiedLineNumber {
-      display: none;
-    }
-    .showLineNumbers .unifiedLineNumber {
-      display: block;
-      cursor: pointer;
-      padding: 0 3px 0 5px;
-      min-width: 20px;
-      text-align: right;
-      color: #999;
-    }
-    .unifiedLineNumberEmpty {
-      display: none;
-    }
-    .showLineNumbers .unifiedLineNumberEmpty {
-      display: block;
-      margin-left: 28px;
-      border-left: 2px solid #d64040;
-      padding-bottom: 1px;
-    }
-  </ui:style>
-  <g:HTMLPanel styleName='{style.difftable}'>
-    <table class='{style.table}'>
-      <tr ui:field='patchSetNavRow' class='{style.patchSetNav}'>
-        <td>
-          <table class='{style.table}'>
-            <tr>
-              <td ui:field='patchSetNavCellA'>
-                <d:PatchSetSelectBox ui:field='patchSetSelectBoxA' />
-              </td>
-            </tr>
-            <tr>
-              <td ui:field='patchSetNavCellB'>
-                <d:PatchSetSelectBox ui:field='patchSetSelectBoxB' />
-              </td>
-            </tr>
-          </table>
-        </td>
-      </tr>
-      <tr ui:field='diffHeaderRow' class='{res.diffTableStyle.diffHeader}'>
-        <td><pre ui:field='diffHeaderText' /></td>
-      </tr>
-      <tr>
-        <td ui:field='cm'/>
-      </tr>
-    </table>
-    <g:FlowPanel ui:field='widgets' visible='false'/>
-  </g:HTMLPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UpToChangeCommand.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UpToChangeCommand.java
deleted file mode 100644
index 50ef0d7..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UpToChangeCommand.java
+++ /dev/null
@@ -1,40 +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.client.diff;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.patches.PatchUtil;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwtexpui.globalkey.client.KeyCommand;
-
-class UpToChangeCommand extends KeyCommand {
-  private final PatchSet.Id revision;
-  @Nullable private final Project.NameKey project;
-
-  UpToChangeCommand(@Nullable Project.NameKey project, PatchSet.Id revision, int mask, int key) {
-    super(mask, key, PatchUtil.C.upToChange());
-    this.revision = revision;
-    this.project = project;
-  }
-
-  @Override
-  public void onKeyPress(KeyPressEvent event) {
-    Gerrit.display(PageLinks.toChange(project, revision.getParentKey(), revision.getId()));
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/goNext.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/goNext.png
deleted file mode 100644
index 872c197..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/goNext.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/goPrev.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/goPrev.png
deleted file mode 100644
index d68f29b..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/goPrev.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/goUp.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/goUp.png
deleted file mode 100644
index f75bed4..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/goUp.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/DocConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/DocConstants.java
deleted file mode 100644
index 2958783..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/DocConstants.java
+++ /dev/null
@@ -1,27 +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.client.documentation;
-
-import com.google.gwt.i18n.client.Constants;
-
-public interface DocConstants extends Constants {
-  String keyReloadSearch();
-
-  String docItemHelp();
-
-  String docTableColumnTitle();
-
-  String docTableNone();
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/DocConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/DocConstants.properties
deleted file mode 100644
index b48c507..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/DocConstants.properties
+++ /dev/null
@@ -1,5 +0,0 @@
-keyReloadSearch = Reload documentation list
-
-docItemHelp = documentation
-docTableColumnTitle = Title
-docTableNone = (None)
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/DocInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/DocInfo.java
deleted file mode 100644
index 5fcb6b0..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/DocInfo.java
+++ /dev/null
@@ -1,35 +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.client.documentation;
-
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.core.client.JavaScriptObject;
-
-public class DocInfo extends JavaScriptObject {
-
-  public final native String title() /*-{ return this.title; }-*/;
-
-  public final native String url() /*-{ return this.url; }-*/;
-
-  public static DocInfo create() {
-    return (DocInfo) createObject();
-  }
-
-  protected DocInfo() {}
-
-  public final String getFullUrl() {
-    return GWT.getHostPageBaseURL() + url();
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/DocMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/DocMessages.java
deleted file mode 100644
index 7d76f7b..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/DocMessages.java
+++ /dev/null
@@ -1,23 +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.client.documentation;
-
-import com.google.gwt.i18n.client.Messages;
-
-public interface DocMessages extends Messages {
-  String docQueryWindowTitle(String query);
-
-  String docQueryPageTitle(String query);
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/DocMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/DocMessages.properties
deleted file mode 100644
index 8810a4a..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/DocMessages.properties
+++ /dev/null
@@ -1,2 +0,0 @@
-docQueryWindowTitle = {0}
-docQueryPageTitle = Search for {0} in documentation
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/DocScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/DocScreen.java
deleted file mode 100644
index 0a87d29..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/DocScreen.java
+++ /dev/null
@@ -1,78 +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.client.documentation;
-
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.client.ui.Screen;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwtorm.client.KeyUtil;
-
-public class DocScreen extends Screen {
-  private static final String URI = "/Documentation/";
-
-  private DocTable table;
-  private final String query;
-
-  public DocScreen(String query) {
-    this.query = KeyUtil.decode(query);
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-
-    table = new DocTable();
-    table.setSavePointerId(query);
-    add(table);
-
-    setWindowTitle(Util.M.docQueryWindowTitle(query));
-    setPageTitle(Util.M.docQueryPageTitle(query));
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    doQuery();
-  }
-
-  @Override
-  public void registerKeys() {
-    super.registerKeys();
-    table.setRegisterKeys(true);
-  }
-
-  private AsyncCallback<JsArray<DocInfo>> loadCallback() {
-    return new GerritCallback<JsArray<DocInfo>>() {
-      @Override
-      public void onSuccess(JsArray<DocInfo> result) {
-        displayResults(result);
-        display();
-      }
-    };
-  }
-
-  private void displayResults(JsArray<DocInfo> result) {
-    table.display(result);
-    table.finishDisplay();
-  }
-
-  private void doQuery() {
-    RestApi call = new RestApi(URI);
-    call.addParameterRaw("q", KeyUtil.encode(query));
-    call.get(loadCallback());
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/DocTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/DocTable.java
deleted file mode 100644
index 677c2bf..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/DocTable.java
+++ /dev/null
@@ -1,126 +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.client.documentation;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.ui.NavigationTable;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.user.client.Window;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
-import com.google.gwt.user.client.ui.HTMLTable.Cell;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
-
-class DocTable extends NavigationTable<DocInfo> {
-  private static final int C_TITLE = 1;
-
-  private int rows;
-  private int dataBeginRow;
-
-  DocTable() {
-    super(Util.C.docItemHelp());
-
-    table.setText(0, C_TITLE, Util.C.docTableColumnTitle());
-
-    FlexCellFormatter fmt = table.getFlexCellFormatter();
-    fmt.addStyleName(0, C_TITLE, Gerrit.RESOURCES.css().dataHeader());
-
-    table.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            Cell cell = table.getCellForEvent(event);
-            if (cell == null) {
-              return;
-            }
-            if (getRowItem(cell.getRowIndex()) != null) {
-              movePointerTo(cell.getRowIndex());
-            }
-          }
-        });
-  }
-
-  @Override
-  protected Object getRowItemKey(DocInfo item) {
-    return item.url();
-  }
-
-  @Override
-  protected void onOpenRow(int row) {
-    DocInfo d = getRowItem(row);
-    Window.Location.assign(d.getFullUrl());
-  }
-
-  private void insertNoneRow(int row) {
-    table.insertRow(row);
-    table.setText(row, 0, Util.C.docTableNone());
-    FlexCellFormatter fmt = table.getFlexCellFormatter();
-    fmt.setStyleName(row, 0, Gerrit.RESOURCES.css().emptySection());
-  }
-
-  private void insertDocRow(int row) {
-    table.insertRow(row);
-    applyDataRowStyle(row);
-  }
-
-  @Override
-  protected void applyDataRowStyle(int row) {
-    super.applyDataRowStyle(row);
-    CellFormatter fmt = table.getCellFormatter();
-    fmt.addStyleName(row, C_TITLE, Gerrit.RESOURCES.css().dataCell());
-    fmt.addStyleName(row, C_TITLE, Gerrit.RESOURCES.css().cSUBJECT());
-  }
-
-  private void populateDocRow(int row, DocInfo d) {
-    table.setWidget(row, C_TITLE, new DocLink(d));
-    setRowItem(row, d);
-  }
-
-  public void display(JsArray<DocInfo> docList) {
-    int sz = docList != null ? docList.length() : 0;
-    boolean hadData = rows > 0;
-
-    if (hadData) {
-      while (sz < rows) {
-        table.removeRow(dataBeginRow);
-        rows--;
-      }
-    } else {
-      table.removeRow(dataBeginRow);
-    }
-
-    if (sz == 0) {
-      insertNoneRow(dataBeginRow);
-      return;
-    }
-
-    while (rows < sz) {
-      insertDocRow(dataBeginRow + rows);
-      rows++;
-    }
-    for (int i = 0; i < sz; i++) {
-      populateDocRow(dataBeginRow + i, docList.get(i));
-    }
-  }
-
-  public static class DocLink extends Anchor {
-    DocLink(DocInfo d) {
-      super(com.google.gerrit.client.changes.Util.cropSubject(d.title()));
-      setHref(d.getFullUrl());
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/Util.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/Util.java
deleted file mode 100644
index 273ead8..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/Util.java
+++ /dev/null
@@ -1,22 +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.client.documentation;
-
-import com.google.gwt.core.client.GWT;
-
-public class Util {
-  public static final DocConstants C = GWT.create(DocConstants.class);
-  public static final DocMessages M = GWT.create(DocMessages.class);
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadCommandLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadCommandLink.java
deleted file mode 100644
index 2b09175..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadCommandLink.java
+++ /dev/null
@@ -1,60 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT 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.client.download;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.info.DownloadInfo.DownloadCommandInfo;
-import com.google.gwt.aria.client.Roles;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtexpui.clippy.client.CopyableLabel;
-
-public class DownloadCommandLink extends Anchor implements ClickHandler {
-  private final CopyableLabel copyLabel;
-  private final String command;
-
-  public DownloadCommandLink(CopyableLabel copyLabel, DownloadCommandInfo commandInfo) {
-    super(commandInfo.name());
-    this.copyLabel = copyLabel;
-    this.command = commandInfo.command();
-
-    setStyleName(Gerrit.RESOURCES.css().downloadLink());
-    Roles.getTabRole().set(getElement());
-    addClickHandler(this);
-  }
-
-  @Override
-  public void onClick(ClickEvent event) {
-    event.preventDefault();
-    event.stopPropagation();
-
-    select();
-  }
-
-  void select() {
-    copyLabel.setText(command);
-
-    DownloadCommandPanel parent = (DownloadCommandPanel) getParent();
-    for (Widget w : parent) {
-      if (w != this && w instanceof DownloadCommandLink) {
-        w.removeStyleName(Gerrit.RESOURCES.css().downloadLink_Active());
-      }
-    }
-    parent.setCurrentCommand(this);
-    addStyleName(Gerrit.RESOURCES.css().downloadLink_Active());
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadCommandPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadCommandPanel.java
deleted file mode 100644
index 20cf3f3..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadCommandPanel.java
+++ /dev/null
@@ -1,60 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT 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.client.download;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gwt.aria.client.Roles;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.Widget;
-
-public class DownloadCommandPanel extends FlowPanel {
-  private DownloadCommandLink currentCommand;
-
-  public DownloadCommandPanel() {
-    setStyleName(Gerrit.RESOURCES.css().downloadLinkList());
-    Roles.getTablistRole().set(getElement());
-  }
-
-  public boolean isEmpty() {
-    return getWidgetCount() == 0;
-  }
-
-  public void select() {
-    DownloadCommandLink first = null;
-
-    for (Widget w : this) {
-      if (w instanceof DownloadCommandLink) {
-        DownloadCommandLink d = (DownloadCommandLink) w;
-        if (currentCommand != null && d.getText().equals(currentCommand.getText())) {
-          d.select();
-          return;
-        }
-        if (first == null) {
-          first = d;
-        }
-      }
-    }
-
-    // If none matched the requested type, select the first in the
-    // group as that will at least give us an initial baseline.
-    if (first != null) {
-      first.select();
-    }
-  }
-
-  void setCurrentCommand(DownloadCommandLink cmd) {
-    currentCommand = cmd;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadPanel.java
deleted file mode 100644
index b881505..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadPanel.java
+++ /dev/null
@@ -1,66 +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.client.download;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.info.DownloadInfo.DownloadCommandInfo;
-import com.google.gerrit.client.info.DownloadInfo.DownloadSchemeInfo;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.InlineLabel;
-import com.google.gwtexpui.clippy.client.CopyableLabel;
-import java.util.List;
-
-public abstract class DownloadPanel extends FlowPanel {
-  protected final String project;
-
-  private final DownloadCommandPanel commands = new DownloadCommandPanel();
-  private final DownloadUrlPanel urls = new DownloadUrlPanel();
-  private final CopyableLabel copyLabel = new CopyableLabel("");
-
-  public DownloadPanel(String project, boolean allowAnonymous) {
-    this.project = project;
-    copyLabel.setStyleName(Gerrit.RESOURCES.css().downloadLinkCopyLabel());
-    urls.add(DownloadUrlLink.createDownloadUrlLinks(allowAnonymous, this));
-
-    setupWidgets();
-  }
-
-  private void setupWidgets() {
-    if (!urls.isEmpty()) {
-      urls.select(Gerrit.getUserPreferences().downloadScheme());
-
-      FlowPanel p = new FlowPanel();
-      p.setStyleName(Gerrit.RESOURCES.css().downloadLinkHeader());
-      p.add(commands);
-      final InlineLabel glue = new InlineLabel();
-      glue.setStyleName(Gerrit.RESOURCES.css().downloadLinkHeaderGap());
-      p.add(glue);
-      p.add(urls);
-
-      add(p);
-      add(copyLabel);
-    }
-  }
-
-  void populateDownloadCommandLinks(DownloadSchemeInfo schemeInfo) {
-    commands.clear();
-    for (DownloadCommandInfo cmd : getCommands(schemeInfo)) {
-      commands.add(new DownloadCommandLink(copyLabel, cmd));
-    }
-    commands.select();
-  }
-
-  protected abstract List<DownloadCommandInfo> getCommands(DownloadSchemeInfo schemeInfo);
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadUrlLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadUrlLink.java
deleted file mode 100644
index 76e7d7c..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadUrlLink.java
+++ /dev/null
@@ -1,102 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT 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.client.download;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.account.AccountApi;
-import com.google.gerrit.client.info.DownloadInfo.DownloadSchemeInfo;
-import com.google.gerrit.client.info.GeneralPreferences;
-import com.google.gwt.aria.client.Roles;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.Widget;
-import java.util.ArrayList;
-import java.util.List;
-
-public class DownloadUrlLink extends Anchor implements ClickHandler {
-  public static List<DownloadUrlLink> createDownloadUrlLinks(
-      boolean allowAnonymous, DownloadPanel downloadPanel) {
-    List<DownloadUrlLink> urls = new ArrayList<>();
-    for (String s : Gerrit.info().download().schemes()) {
-      DownloadSchemeInfo scheme = Gerrit.info().download().scheme(s);
-      if (scheme.isAuthRequired() && !allowAnonymous) {
-        continue;
-      }
-      urls.add(new DownloadUrlLink(downloadPanel, scheme, s));
-    }
-    return urls;
-  }
-
-  private final DownloadPanel downloadPanel;
-  private final DownloadSchemeInfo schemeInfo;
-  private final String schemeName;
-
-  public DownloadUrlLink(
-      DownloadPanel downloadPanel, DownloadSchemeInfo schemeInfo, String schemeName) {
-    super(schemeName);
-    setStyleName(Gerrit.RESOURCES.css().downloadLink());
-    Roles.getTabRole().set(getElement());
-    addClickHandler(this);
-
-    this.downloadPanel = downloadPanel;
-    this.schemeInfo = schemeInfo;
-    this.schemeName = schemeName;
-  }
-
-  public String getSchemeName() {
-    return schemeName;
-  }
-
-  @Override
-  public void onClick(ClickEvent event) {
-    event.preventDefault();
-    event.stopPropagation();
-
-    select();
-
-    GeneralPreferences prefs = Gerrit.getUserPreferences();
-    if (Gerrit.isSignedIn() && !schemeName.equals(prefs.downloadScheme())) {
-      prefs.downloadScheme(schemeName);
-      GeneralPreferences in = GeneralPreferences.create();
-      in.downloadScheme(schemeName);
-      AccountApi.self()
-          .view("preferences")
-          .put(
-              in,
-              new AsyncCallback<JavaScriptObject>() {
-                @Override
-                public void onSuccess(JavaScriptObject result) {}
-
-                @Override
-                public void onFailure(Throwable caught) {}
-              });
-    }
-  }
-
-  void select() {
-    downloadPanel.populateDownloadCommandLinks(schemeInfo);
-
-    DownloadUrlPanel parent = (DownloadUrlPanel) getParent();
-    for (Widget w : parent) {
-      if (w != this && w instanceof DownloadUrlLink) {
-        w.removeStyleName(Gerrit.RESOURCES.css().downloadLink_Active());
-      }
-    }
-    addStyleName(Gerrit.RESOURCES.css().downloadLink_Active());
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadUrlPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadUrlPanel.java
deleted file mode 100644
index 6a5fbe4..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadUrlPanel.java
+++ /dev/null
@@ -1,62 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT 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.client.download;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gwt.aria.client.Roles;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.Widget;
-import java.util.Collection;
-
-public class DownloadUrlPanel extends FlowPanel {
-
-  public DownloadUrlPanel() {
-    setStyleName(Gerrit.RESOURCES.css().downloadLinkList());
-    Roles.getTablistRole().set(getElement());
-  }
-
-  public boolean isEmpty() {
-    return getWidgetCount() == 0;
-  }
-
-  public void select(String schemeName) {
-    DownloadUrlLink first = null;
-
-    for (Widget w : this) {
-      if (w instanceof DownloadUrlLink) {
-        final DownloadUrlLink d = (DownloadUrlLink) w;
-        if (first == null) {
-          first = d;
-        }
-        if (d.getSchemeName().equals(schemeName)) {
-          d.select();
-          return;
-        }
-      }
-    }
-
-    // If none matched the requested type, select the first in the
-    // group as that will at least give us an initial baseline.
-    if (first != null) {
-      first.select();
-    }
-  }
-
-  public void add(Collection<DownloadUrlLink> links) {
-    for (Widget link : links) {
-      add(link);
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditConstants.java
deleted file mode 100644
index b70c209..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditConstants.java
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.editor;
-
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.i18n.client.Constants;
-
-interface EditConstants extends Constants {
-  EditConstants I = GWT.create(EditConstants.class);
-
-  String closeUnsavedChanges();
-
-  String cancelUnsavedChanges();
-
-  String gotoLineNumber();
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditConstants.properties
deleted file mode 100644
index 2e8a087..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditConstants.properties
+++ /dev/null
@@ -1,7 +0,0 @@
-closeUnsavedChanges = Unsaved changes were made to this file.
-
-cancelUnsavedChanges = Unsaved changes were made to this file.\n\
-  \n\
-  Discard unsaved changes?
-
-gotoLineNumber = Go to Line:
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditFileInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditFileInfo.java
deleted file mode 100644
index 3f9d732..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditFileInfo.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.editor;
-
-import com.google.gerrit.client.DiffWebLinkInfo;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
-
-public class EditFileInfo extends JavaScriptObject {
-  public final native JsArray<DiffWebLinkInfo> webLinks() /*-{ return this.web_links; }-*/;
-
-  protected EditFileInfo() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesAction.java
deleted file mode 100644
index e11ded0..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesAction.java
+++ /dev/null
@@ -1,71 +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.client.editor;
-
-import com.google.gerrit.client.account.EditPreferences;
-import com.google.gwt.event.logical.shared.CloseEvent;
-import com.google.gwt.event.logical.shared.CloseHandler;
-import com.google.gwt.user.client.ui.PopupPanel;
-import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;
-
-class EditPreferencesAction {
-  private final EditScreen view;
-  private final EditPreferences prefs;
-  private PopupPanel popup;
-  private EditPreferencesBox current;
-
-  EditPreferencesAction(EditScreen view, EditPreferences prefs) {
-    this.view = view;
-    this.prefs = prefs;
-  }
-
-  void show() {
-    if (popup != null) {
-      hide();
-      return;
-    }
-
-    current = new EditPreferencesBox(view);
-    current.set(prefs);
-
-    popup = new PopupPanel(true, false);
-    popup.setStyleName(current.style.dialog());
-    popup.add(current);
-    popup.addCloseHandler(
-        new CloseHandler<PopupPanel>() {
-          @Override
-          public void onClose(CloseEvent<PopupPanel> event) {
-            view.getEditor().focus();
-            popup = null;
-            current = null;
-          }
-        });
-    popup.setPopupPositionAndShow(
-        new PositionCallback() {
-          @Override
-          public void setPosition(int offsetWidth, int offsetHeight) {
-            popup.setPopupPosition(300, 120);
-          }
-        });
-  }
-
-  void hide() {
-    if (popup != null) {
-      popup.hide();
-      popup = null;
-      current = null;
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesBox.java
deleted file mode 100644
index 5157123..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesBox.java
+++ /dev/null
@@ -1,333 +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.client.editor;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.account.AccountApi;
-import com.google.gerrit.client.account.EditPreferences;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.NpIntTextBox;
-import com.google.gerrit.extensions.client.EditPreferencesInfo;
-import com.google.gerrit.extensions.client.KeyMapType;
-import com.google.gerrit.extensions.client.Theme;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.Style.Visibility;
-import com.google.gwt.event.dom.client.ChangeEvent;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.resources.client.CssResource;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.uibinder.client.UiHandler;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwt.user.client.ui.ListBox;
-import com.google.gwt.user.client.ui.PopupPanel;
-import com.google.gwt.user.client.ui.ToggleButton;
-import com.google.gwt.user.client.ui.UIObject;
-import net.codemirror.theme.ThemeLoader;
-
-/** Displays current edit preferences. */
-public class EditPreferencesBox extends Composite {
-  interface Binder extends UiBinder<HTMLPanel, EditPreferencesBox> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  public interface Style extends CssResource {
-    String dialog();
-  }
-
-  private final EditScreen view;
-  private EditPreferences prefs;
-
-  @UiField Style style;
-  @UiField Element header;
-  @UiField Anchor close;
-  @UiField NpIntTextBox tabWidth;
-  @UiField NpIntTextBox lineLength;
-  @UiField NpIntTextBox indentUnit;
-  @UiField NpIntTextBox cursorBlinkRate;
-  @UiField ToggleButton topMenu;
-  @UiField ToggleButton syntaxHighlighting;
-  @UiField ToggleButton showTabs;
-  @UiField ToggleButton whitespaceErrors;
-  @UiField ToggleButton lineNumbers;
-  @UiField ToggleButton matchBrackets;
-  @UiField ToggleButton lineWrapping;
-  @UiField ToggleButton indentWithTabs;
-  @UiField ToggleButton autoCloseBrackets;
-  @UiField ToggleButton showBase;
-  @UiField ListBox theme;
-  @UiField ListBox keyMap;
-  @UiField Button apply;
-  @UiField Button save;
-
-  public EditPreferencesBox(EditScreen view) {
-    this.view = view;
-    initWidget(uiBinder.createAndBindUi(this));
-    initTheme();
-    initKeyMapType();
-
-    if (view == null) {
-      UIObject.setVisible(header, false);
-      apply.getElement().getStyle().setVisibility(Visibility.HIDDEN);
-    }
-  }
-
-  public Style getStyle() {
-    return style;
-  }
-
-  public void set(EditPreferences prefs) {
-    this.prefs = prefs;
-
-    tabWidth.setIntValue(prefs.tabSize());
-    lineLength.setIntValue(prefs.lineLength());
-    indentUnit.setIntValue(prefs.indentUnit());
-    cursorBlinkRate.setIntValue(prefs.cursorBlinkRate());
-    topMenu.setValue(!prefs.hideTopMenu());
-    syntaxHighlighting.setValue(prefs.syntaxHighlighting());
-    showTabs.setValue(prefs.showTabs());
-    whitespaceErrors.setValue(prefs.showWhitespaceErrors());
-    lineNumbers.setValue(prefs.hideLineNumbers());
-    matchBrackets.setValue(prefs.matchBrackets());
-    lineWrapping.setValue(prefs.lineWrapping());
-    indentWithTabs.setValue(prefs.indentWithTabs());
-    autoCloseBrackets.setValue(prefs.autoCloseBrackets());
-    showBase.setValue(prefs.showBase());
-    setTheme(prefs.theme());
-    setKeyMapType(prefs.keyMapType());
-  }
-
-  @UiHandler("tabWidth")
-  void onTabWidth(ValueChangeEvent<String> e) {
-    String v = e.getValue();
-    if (v != null && v.length() > 0) {
-      prefs.tabSize(Math.max(1, Integer.parseInt(v)));
-      if (view != null) {
-        view.setOption("tabSize", v);
-      }
-    }
-  }
-
-  @UiHandler("lineLength")
-  void onLineLength(ValueChangeEvent<String> e) {
-    String v = e.getValue();
-    if (v != null && v.length() > 0) {
-      prefs.lineLength(Math.max(1, Integer.parseInt(v)));
-      if (view != null) {
-        view.setLineLength(prefs.lineLength());
-      }
-    }
-  }
-
-  @UiHandler("indentUnit")
-  void onIndentUnit(ValueChangeEvent<String> e) {
-    String v = e.getValue();
-    if (v != null && v.length() > 0) {
-      prefs.indentUnit(Math.max(0, Integer.parseInt(v)));
-      if (view != null) {
-        view.setIndentUnit(prefs.indentUnit());
-      }
-    }
-  }
-
-  @UiHandler("cursorBlinkRate")
-  void onCursoBlinkRate(ValueChangeEvent<String> e) {
-    String v = e.getValue();
-    if (v != null && v.length() > 0) {
-      // A negative value hides the cursor entirely:
-      // don't let user shoot himself in the foot.
-      prefs.cursorBlinkRate(Math.max(0, Integer.parseInt(v)));
-      if (view != null) {
-        view.setOption("cursorBlinkRate", prefs.cursorBlinkRate());
-      }
-    }
-  }
-
-  @UiHandler("topMenu")
-  void onTopMenu(ValueChangeEvent<Boolean> e) {
-    prefs.hideTopMenu(!e.getValue());
-    if (view != null) {
-      Gerrit.setHeaderVisible(!prefs.hideTopMenu());
-      view.adjustHeight();
-    }
-  }
-
-  @UiHandler("showTabs")
-  void onShowTabs(ValueChangeEvent<Boolean> e) {
-    prefs.showTabs(e.getValue());
-    if (view != null) {
-      view.setShowTabs(prefs.showTabs());
-    }
-  }
-
-  @UiHandler("whitespaceErrors")
-  void onshowTrailingSpace(ValueChangeEvent<Boolean> e) {
-    prefs.showWhitespaceErrors(e.getValue());
-    if (view != null) {
-      view.setShowWhitespaceErrors(prefs.showWhitespaceErrors());
-    }
-  }
-
-  @UiHandler("lineNumbers")
-  void onLineNumbers(ValueChangeEvent<Boolean> e) {
-    prefs.hideLineNumbers(e.getValue());
-    if (view != null) {
-      view.setShowLineNumbers(prefs.hideLineNumbers());
-    }
-  }
-
-  @UiHandler("syntaxHighlighting")
-  void onSyntaxHighlighting(ValueChangeEvent<Boolean> e) {
-    prefs.syntaxHighlighting(e.getValue());
-    if (view != null) {
-      view.setSyntaxHighlighting(prefs.syntaxHighlighting());
-    }
-  }
-
-  @UiHandler("matchBrackets")
-  void onMatchBrackets(ValueChangeEvent<Boolean> e) {
-    prefs.matchBrackets(e.getValue());
-    if (view != null) {
-      view.setOption("matchBrackets", prefs.matchBrackets());
-    }
-  }
-
-  @UiHandler("lineWrapping")
-  void onLineWrapping(ValueChangeEvent<Boolean> e) {
-    prefs.lineWrapping(e.getValue());
-    if (view != null) {
-      view.getEditor().setOption("lineWrapping", prefs.lineWrapping());
-    }
-  }
-
-  @UiHandler("indentWithTabs")
-  void onIndentWithTabs(ValueChangeEvent<Boolean> e) {
-    prefs.indentWithTabs(e.getValue());
-    if (view != null) {
-      view.getEditor().setOption("indentWithTabs", prefs.indentWithTabs());
-    }
-  }
-
-  @UiHandler("autoCloseBrackets")
-  void onCloseBrackets(ValueChangeEvent<Boolean> e) {
-    prefs.autoCloseBrackets(e.getValue());
-    if (view != null) {
-      view.getEditor().setOption("autoCloseBrackets", prefs.autoCloseBrackets());
-    }
-  }
-
-  @UiHandler("showBase")
-  void onShowBase(ValueChangeEvent<Boolean> e) {
-    Boolean value = e.getValue();
-    prefs.showBase(value);
-    if (view != null) {
-      view.showBase.setValue(value, true);
-    }
-  }
-
-  @UiHandler("theme")
-  void onTheme(@SuppressWarnings("unused") ChangeEvent e) {
-    final Theme newTheme = Theme.valueOf(theme.getValue(theme.getSelectedIndex()));
-    prefs.theme(newTheme);
-    if (view != null) {
-      ThemeLoader.loadTheme(
-          newTheme,
-          new GerritCallback<Void>() {
-            @Override
-            public void onSuccess(Void result) {
-              view.setTheme(newTheme);
-            }
-          });
-    }
-  }
-
-  @UiHandler("keyMap")
-  void onKeyMap(@SuppressWarnings("unused") ChangeEvent e) {
-    KeyMapType keyMapType = KeyMapType.valueOf(keyMap.getValue(keyMap.getSelectedIndex()));
-    prefs.keyMapType(keyMapType);
-    if (view != null) {
-      view.setOption("keyMap", keyMapType.name().toLowerCase());
-    }
-  }
-
-  @UiHandler("apply")
-  void onApply(@SuppressWarnings("unused") ClickEvent e) {
-    close();
-  }
-
-  @UiHandler("save")
-  void onSave(@SuppressWarnings("unused") ClickEvent e) {
-    AccountApi.putEditPreferences(
-        prefs,
-        new GerritCallback<EditPreferences>() {
-          @Override
-          public void onSuccess(EditPreferences p) {
-            Gerrit.setEditPreferences(p.copyTo(new EditPreferencesInfo()));
-          }
-        });
-    if (view != null) {
-      close();
-    }
-  }
-
-  @UiHandler("close")
-  void onClose(ClickEvent e) {
-    e.preventDefault();
-    close();
-  }
-
-  private void close() {
-    ((PopupPanel) getParent()).hide();
-  }
-
-  private void setTheme(Theme v) {
-    String name = v != null ? v.name() : Theme.DEFAULT.name();
-    for (int i = 0; i < theme.getItemCount(); i++) {
-      if (theme.getValue(i).equals(name)) {
-        theme.setSelectedIndex(i);
-        return;
-      }
-    }
-    theme.setSelectedIndex(0);
-  }
-
-  private void initTheme() {
-    for (Theme t : Theme.values()) {
-      theme.addItem(t.name().toLowerCase(), t.name());
-    }
-  }
-
-  private void setKeyMapType(KeyMapType v) {
-    String name = v != null ? v.name() : KeyMapType.DEFAULT.name();
-    for (int i = 0; i < keyMap.getItemCount(); i++) {
-      if (keyMap.getValue(i).equals(name)) {
-        keyMap.setSelectedIndex(i);
-        return;
-      }
-    }
-    keyMap.setSelectedIndex(0);
-  }
-
-  private void initKeyMapType() {
-    for (KeyMapType t : KeyMapType.values()) {
-      keyMap.addItem(t.name().toLowerCase(), t.name());
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesBox.ui.xml
deleted file mode 100644
index f5ec71e..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesBox.ui.xml
+++ /dev/null
@@ -1,282 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'
-    xmlns:x='urn:import:com.google.gerrit.client.ui'>
-  <ui:style type='com.google.gerrit.client.editor.EditPreferencesBox.Style'>
-    @external .gwt-TextBox;
-    @external .gwt-ToggleButton .html-face;
-    @external .gwt-ToggleButton-up;
-    @external .gwt-ToggleButton-up-hovering;
-    @external .gwt-ToggleButton-up-disabled;
-    @external .gwt-ToggleButton-down;
-    @external .gwt-ToggleButton-down-hovering;
-    @external .gwt-ToggleButton-down-disabled;
-
-    .dialog {
-      background: rgba(0, 0, 0, 0.85) none repeat scroll 0 50%;
-      color: #ffffff;
-      font-family: arial,sans-serif;
-      font-weight: bold;
-      overflow: auto !important;
-      bottom: 0;
-      text-align: left;
-      text-shadow: 1px 1px 7px #000000;
-      min-width: 300px;
-      z-index: 200;
-      border-radius: 10px;
-    }
-
-    @if user.agent safari {
-      .dialog {
-        \-webkit-border-radius: 10px;
-      }
-    }
-
-    @if user.agent gecko1_8 {
-      .dialog {
-        \-moz-border-radius: 10px;
-      }
-    }
-
-    .box { margin: 10px; }
-    .box .gwt-TextBox { padding: 0; }
-    .context { vertical-align: bottom; }
-
-    .table tr { min-height: 23px; }
-    .table th,
-    .table td {
-      white-space: nowrap;
-      color: #ffffff;
-    }
-    .table th {
-      padding-right: 8px;
-      text-align: right;
-    }
-
-    .box a,
-    .box a:visited,
-    .box a:hover {
-      color: #dddd00;
-    }
-
-    .box .gwt-ToggleButton {
-      position: relative;
-      height: 19px;
-      width: 140px;
-      background: #fff;
-      color: #000;
-      text-shadow: none;
-    }
-    .box .gwt-ToggleButton .html-face {
-      position: absolute;
-      top: 0;
-      width: 68px;
-      height: 17px;
-      line-height: 17px;
-      text-align: center;
-      border-width: 1px;
-    }
-
-    .box .gwt-ToggleButton-up,
-    .box .gwt-ToggleButton-up-hovering,
-    .box .gwt-ToggleButton-up-disabled,
-    .box .gwt-ToggleButton-down,
-    .box .gwt-ToggleButton-down-hovering,
-    .box .gwt-ToggleButton-down-disabled {
-      padding: 0;
-      border: 0;
-    }
-    .box .gwt-ToggleButton-up .html-face,
-    .box .gwt-ToggleButton-up-hovering .html-face {
-      left: 0;
-      background: #cacaca;
-      border-style: outset;
-    }
-    .box .gwt-ToggleButton-down .html-face,
-    .box .gwt-ToggleButton-down-hovering .html-face {
-      right: 0;
-      background: #bcf;
-      border-style: inset;
-    }
-
-    .box button {
-      margin: 6px 3px 0 0;
-      border-color: rgba(0, 0, 0, 0.1);
-      text-align: center;
-      font-size: 8pt;
-      font-weight: bold;
-      border: 1px solid;
-      cursor: pointer;
-      color: #444;
-      background-color: #f5f5f5;
-      background-image: -webkit-linear-gradient(top, #f5f5f5, #f1f1f1);
-      -webkit-border-radius: 2px;
-      -webkit-box-sizing: content-box;
-    }
-    .box button div {
-      color: #444;
-      height: 10px;
-      min-width: 54px;
-      line-height: 10px;
-      white-space: nowrap;
-    }
-
-    button.apply {
-      background-color: #4d90fe;
-      background-image: -webkit-linear-gradient(top, #4d90fe, #4d90fe);
-    }
-    button.apply div { color: #fff; }
-
-    button.save {
-      margin-left: 10px;
-      color: #d14836;
-      background-color: #d14836;
-      background-image: -webkit-linear-gradient(top, #d14836, #d14836);
-    }
-    button.save div { color: #fff; }
-  </ui:style>
-
-  <g:HTMLPanel styleName='{style.box}'>
-    <div ui:field='header'>
-      <table style='width: 100%'>
-        <tr>
-          <td><ui:msg>Edit Preferences</ui:msg></td>
-          <td style='text-align: right'>
-            <g:Anchor ui:field='close' href='javascript:;'><ui:msg>Close</ui:msg></g:Anchor>
-          </td>
-        </tr>
-      </table>
-      <hr/>
-    </div>
-    <table class='{style.table}'>
-      <tr>
-        <th><ui:msg>Theme</ui:msg></th>
-        <td><g:ListBox ui:field='theme'/></td>
-      </tr>
-      <tr>
-        <th><ui:msg>Key Map</ui:msg></th>
-        <td><g:ListBox ui:field='keyMap'/></td>
-      </tr>
-      <tr>
-        <th><ui:msg>Tab Width</ui:msg></th>
-        <td><x:NpIntTextBox ui:field='tabWidth'
-            visibleLength='4'
-            alignment='RIGHT'/></td>
-      </tr>
-      <tr>
-        <th><ui:msg>Columns</ui:msg></th>
-        <td><x:NpIntTextBox ui:field='lineLength'
-            visibleLength='4'
-            alignment='RIGHT'/></td>
-      </tr>
-      <tr>
-        <th><ui:msg>Indent Unit</ui:msg></th>
-        <td><x:NpIntTextBox ui:field='indentUnit'
-            visibleLength='4'
-            alignment='RIGHT'/></td>
-      </tr>
-      <tr>
-        <th><ui:msg>Cursor Blink Rate</ui:msg></th>
-        <td><x:NpIntTextBox ui:field='cursorBlinkRate'
-            visibleLength='4'
-            alignment='RIGHT'/></td>
-      </tr>
-      <tr>
-        <th><ui:msg>Top Menu</ui:msg></th>
-        <td><g:ToggleButton ui:field='topMenu'>
-          <g:upFace><ui:msg>Hide</ui:msg></g:upFace>
-          <g:downFace><ui:msg>Show</ui:msg></g:downFace>
-        </g:ToggleButton></td>
-      </tr>
-      <tr>
-        <th><ui:msg>Syntax Highlighting</ui:msg></th>
-        <td><g:ToggleButton ui:field='syntaxHighlighting'>
-          <g:upFace><ui:msg>Hide</ui:msg></g:upFace>
-          <g:downFace><ui:msg>Show</ui:msg></g:downFace>
-        </g:ToggleButton></td>
-      </tr>
-      <tr>
-        <th><ui:msg>Show Tabs</ui:msg></th>
-        <td><g:ToggleButton ui:field='showTabs'>
-          <g:upFace><ui:msg>Hide</ui:msg></g:upFace>
-          <g:downFace><ui:msg>Show</ui:msg></g:downFace>
-        </g:ToggleButton></td>
-      </tr>
-      <tr>
-      <th><ui:msg>Whitespace Errors</ui:msg></th>
-        <td><g:ToggleButton ui:field='whitespaceErrors'>
-          <g:upFace><ui:msg>Hide</ui:msg></g:upFace>
-          <g:downFace><ui:msg>Show</ui:msg></g:downFace>
-        </g:ToggleButton></td>
-      </tr>
-      <tr>
-        <th><ui:msg>Line Numbers</ui:msg></th>
-        <td><g:ToggleButton ui:field='lineNumbers'>
-          <g:upFace><ui:msg>Hide</ui:msg></g:upFace>
-          <g:downFace><ui:msg>Show</ui:msg></g:downFace>
-        </g:ToggleButton></td>
-      </tr>
-      <tr>
-        <th><ui:msg>Match Brackets</ui:msg></th>
-        <td><g:ToggleButton ui:field='matchBrackets'>
-          <g:upFace><ui:msg>Off</ui:msg></g:upFace>
-          <g:downFace><ui:msg>On</ui:msg></g:downFace>
-        </g:ToggleButton></td>
-      </tr>
-      <tr>
-        <th><ui:msg>Line Wrapping</ui:msg></th>
-        <td><g:ToggleButton ui:field='lineWrapping'>
-          <g:upFace><ui:msg>Off</ui:msg></g:upFace>
-          <g:downFace><ui:msg>On</ui:msg></g:downFace>
-        </g:ToggleButton></td>
-      </tr>
-      <tr>
-        <th><ui:msg>Indent With Tabs</ui:msg></th>
-        <td><g:ToggleButton ui:field='indentWithTabs'>
-          <g:upFace><ui:msg>Off</ui:msg></g:upFace>
-          <g:downFace><ui:msg>On</ui:msg></g:downFace>
-        </g:ToggleButton></td>
-      </tr>
-      <tr>
-        <th><ui:msg>Auto Close Brackets</ui:msg></th>
-        <td><g:ToggleButton ui:field='autoCloseBrackets'>
-          <g:upFace><ui:msg>Off</ui:msg></g:upFace>
-          <g:downFace><ui:msg>On</ui:msg></g:downFace>
-        </g:ToggleButton></td>
-      </tr>
-      <tr>
-        <th><ui:msg>Show Base Version</ui:msg></th>
-        <td><g:ToggleButton ui:field='showBase'>
-          <g:upFace><ui:msg>Hide</ui:msg></g:upFace>
-          <g:downFace><ui:msg>Show</ui:msg></g:downFace>
-        </g:ToggleButton></td>
-      </tr>
-      <tr>
-        <td></td>
-        <td>
-          <g:Button ui:field='apply' styleName='{style.apply}'>
-            <div><ui:msg>Apply</ui:msg></div>
-          </g:Button>
-          <g:Button ui:field='save' styleName='{style.save}'>
-            <div><ui:msg>Save</ui:msg></div>
-          </g:Button>
-        </td>
-      </tr>
-    </table>
-  </g:HTMLPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
deleted file mode 100644
index cbf12a3..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
+++ /dev/null
@@ -1,699 +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.client.editor;
-
-import static com.google.gwt.dom.client.Style.Visibility.HIDDEN;
-import static com.google.gwt.dom.client.Style.Visibility.VISIBLE;
-
-import com.google.gerrit.client.DiffWebLinkInfo;
-import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.JumpKeys;
-import com.google.gerrit.client.VoidResult;
-import com.google.gerrit.client.account.EditPreferences;
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeEditApi;
-import com.google.gerrit.client.diff.DiffApi;
-import com.google.gerrit.client.diff.DiffInfo;
-import com.google.gerrit.client.diff.Header;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.info.FileInfo;
-import com.google.gerrit.client.patches.PatchUtil;
-import com.google.gerrit.client.rpc.CallbackGroup;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.HttpCallback;
-import com.google.gerrit.client.rpc.HttpResponse;
-import com.google.gerrit.client.rpc.NativeString;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.client.ui.InlineHyperlink;
-import com.google.gerrit.client.ui.Screen;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.extensions.client.KeyMapType;
-import com.google.gerrit.extensions.client.Theme;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.core.client.Scheduler;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.Style.Unit;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.logical.shared.ResizeEvent;
-import com.google.gwt.event.logical.shared.ResizeHandler;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.resources.client.CssResource;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.uibinder.client.UiHandler;
-import com.google.gwt.user.client.Window;
-import com.google.gwt.user.client.Window.ClosingEvent;
-import com.google.gwt.user.client.Window.ClosingHandler;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwt.user.client.ui.ImageResourceRenderer;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-import com.google.gwtexpui.safehtml.client.SafeHtml;
-import java.util.List;
-import net.codemirror.addon.AddonInjector;
-import net.codemirror.addon.Addons;
-import net.codemirror.lib.CodeMirror;
-import net.codemirror.lib.CodeMirror.ChangesHandler;
-import net.codemirror.lib.CodeMirror.CommandRunner;
-import net.codemirror.lib.Configuration;
-import net.codemirror.lib.KeyMap;
-import net.codemirror.lib.MergeView;
-import net.codemirror.lib.Pos;
-import net.codemirror.mode.ModeInfo;
-import net.codemirror.mode.ModeInjector;
-import net.codemirror.theme.ThemeLoader;
-
-public class EditScreen extends Screen {
-  interface Binder extends UiBinder<HTMLPanel, EditScreen> {}
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  interface Style extends CssResource {
-    String fullWidth();
-
-    String base();
-
-    String hideBase();
-  }
-
-  @Nullable private Project.NameKey projectKey;
-  private final PatchSet.Id revision;
-  private final String path;
-  private final int startLine;
-  private EditPreferences prefs;
-  private EditPreferencesAction editPrefsAction;
-  private MergeView mv;
-  private CodeMirror cmBase;
-  private CodeMirror cmEdit;
-  private HttpResponse<NativeString> content;
-  private HttpResponse<NativeString> baseContent;
-  private EditFileInfo editFileInfo;
-  private JsArray<DiffWebLinkInfo> diffLinks;
-
-  @UiField Element header;
-  @UiField Element project;
-  @UiField Element filePath;
-  @UiField FlowPanel linkPanel;
-  @UiField Element cursLine;
-  @UiField Element cursCol;
-  @UiField Element dirty;
-  @UiField CheckBox showBase;
-  @UiField Button close;
-  @UiField Button save;
-  @UiField Element editor;
-  @UiField Style style;
-
-  private HandlerRegistration resizeHandler;
-  private HandlerRegistration closeHandler;
-  private int generation;
-
-  public EditScreen(@Nullable Project.NameKey projectKey, Patch.Key patch, int startLine) {
-    this.projectKey = projectKey;
-    this.revision = patch.getParentKey();
-    this.path = patch.get();
-    this.startLine = startLine - 1;
-    setRequiresSignIn(true);
-    add(uiBinder.createAndBindUi(this));
-    addDomHandler(GlobalKey.STOP_PROPAGATION, KeyPressEvent.getType());
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    setHeaderVisible(false);
-    setWindowTitle(FileInfo.getFileName(path));
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-
-    prefs = EditPreferences.create(Gerrit.getEditPreferences());
-
-    CallbackGroup group1 = new CallbackGroup();
-    final CallbackGroup group2 = new CallbackGroup();
-    final CallbackGroup group3 = new CallbackGroup();
-
-    CodeMirror.initLibrary(
-        group1.add(
-            new AsyncCallback<Void>() {
-              final AsyncCallback<Void> themeCallback = group3.addEmpty();
-
-              @Override
-              public void onSuccess(Void result) {
-                // Load theme after CM library to ensure theme can override CSS.
-                ThemeLoader.loadTheme(prefs.theme(), themeCallback);
-                group2.done();
-
-                new AddonInjector()
-                    .add(Addons.I.merge_bundled().getName())
-                    .inject(
-                        new AsyncCallback<Void>() {
-                          @Override
-                          public void onFailure(Throwable caught) {}
-
-                          @Override
-                          public void onSuccess(Void result) {
-                            if (!prefs.showBase() || revision.get() > 0) {
-                              group3.done();
-                            }
-                          }
-                        });
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {}
-            }));
-
-    ChangeApi.detail(
-        Project.NameKey.asStringOrNull(projectKey),
-        revision.getParentKey().get(),
-        group1.add(
-            new AsyncCallback<ChangeInfo>() {
-              @Override
-              public void onSuccess(ChangeInfo c) {
-                projectKey = c.projectNameKey();
-                project.setInnerText(c.project());
-                SafeHtml.setInnerHTML(filePath, Header.formatPath(path));
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {}
-            }));
-
-    if (revision.get() == 0) {
-      ChangeEditApi.getMeta(
-          Project.NameKey.asStringOrNull(projectKey),
-          revision,
-          path,
-          group1.add(
-              new AsyncCallback<EditFileInfo>() {
-                @Override
-                public void onSuccess(EditFileInfo editInfo) {
-                  editFileInfo = editInfo;
-                }
-
-                @Override
-                public void onFailure(Throwable e) {}
-              }));
-
-      if (prefs.showBase()) {
-        ChangeEditApi.get(
-            projectKey,
-            revision,
-            path,
-            true /* base */,
-            group1.addFinal(
-                new HttpCallback<NativeString>() {
-                  @Override
-                  public void onSuccess(HttpResponse<NativeString> fc) {
-                    baseContent = fc;
-                    group3.done();
-                  }
-
-                  @Override
-                  public void onFailure(Throwable e) {}
-                }));
-      } else {
-        group1.done();
-      }
-    } else {
-      // TODO(davido): We probably want to create dedicated GET EditScreenMeta
-      // REST endpoint. Abuse GET diff for now, as it retrieves links we need.
-      DiffApi.diff(Project.NameKey.asStringOrNull(projectKey), revision, path)
-          .webLinksOnly()
-          .get(
-              group1.addFinal(
-                  new AsyncCallback<DiffInfo>() {
-                    @Override
-                    public void onSuccess(DiffInfo diffInfo) {
-                      diffLinks = diffInfo.webLinks();
-                    }
-
-                    @Override
-                    public void onFailure(Throwable e) {}
-                  }));
-    }
-
-    ChangeEditApi.get(
-        projectKey,
-        revision,
-        path,
-        group2.add(
-            new HttpCallback<NativeString>() {
-              final AsyncCallback<Void> modeCallback = group3.addEmpty();
-
-              @Override
-              public void onSuccess(HttpResponse<NativeString> fc) {
-                content = fc;
-                if (revision.get() > 0) {
-                  baseContent = fc;
-                }
-
-                if (prefs.syntaxHighlighting()) {
-                  injectMode(fc.getContentType(), modeCallback);
-                } else {
-                  modeCallback.onSuccess(null);
-                }
-              }
-
-              @Override
-              public void onFailure(Throwable e) {
-                // "Not Found" means it's a new file.
-                if (RestApi.isNotFound(e)) {
-                  content = null;
-                  modeCallback.onSuccess(null);
-                } else {
-                  GerritCallback.showFailure(e);
-                }
-              }
-            }));
-
-    group3.addListener(
-        new ScreenLoadCallback<Void>(this) {
-          @Override
-          protected void preDisplay(Void result) {
-            initEditor();
-
-            renderLinks(editFileInfo, diffLinks);
-            editFileInfo = null;
-            diffLinks = null;
-
-            showBase.setValue(prefs.showBase(), true);
-            cmBase.refresh();
-          }
-        });
-  }
-
-  @Override
-  public void registerKeys() {
-    super.registerKeys();
-    KeyMap localKeyMap = KeyMap.create();
-    localKeyMap.on("Ctrl-L", gotoLine()).on("Cmd-L", gotoLine()).on("Cmd-S", save());
-
-    // TODO(davido): Find a better way to prevent key maps collisions
-    if (prefs.keyMapType() != KeyMapType.EMACS) {
-      localKeyMap.on("Ctrl-S", save());
-    }
-
-    cmBase.addKeyMap(localKeyMap);
-    cmEdit.addKeyMap(localKeyMap);
-  }
-
-  private Runnable gotoLine() {
-    return () -> cmEdit.execCommand("jumpToLine");
-  }
-
-  @Override
-  public void onShowView() {
-    super.onShowView();
-    Window.enableScrolling(false);
-    JumpKeys.enable(false);
-    if (prefs.hideTopMenu()) {
-      Gerrit.setHeaderVisible(false);
-    }
-    resizeHandler =
-        Window.addResizeHandler(
-            new ResizeHandler() {
-              @Override
-              public void onResize(ResizeEvent event) {
-                adjustHeight();
-              }
-            });
-    closeHandler =
-        Window.addWindowClosingHandler(
-            new ClosingHandler() {
-              @Override
-              public void onWindowClosing(ClosingEvent event) {
-                if (!cmEdit.isClean(generation)) {
-                  event.setMessage(EditConstants.I.closeUnsavedChanges());
-                }
-              }
-            });
-
-    generation = cmEdit.changeGeneration(true);
-    setClean(true);
-    cmEdit.on(
-        new ChangesHandler() {
-          @Override
-          public void handle(CodeMirror cm) {
-            setClean(cm.isClean(generation));
-          }
-        });
-
-    adjustHeight();
-    cmEdit.on("cursorActivity", updateCursorPosition());
-    setShowTabs(prefs.showTabs());
-    setLineLength(prefs.lineLength());
-    cmEdit.refresh();
-    cmEdit.focus();
-
-    if (startLine > 0) {
-      cmEdit.scrollToLine(startLine);
-    }
-    updateActiveLine();
-    editPrefsAction = new EditPreferencesAction(this, prefs);
-  }
-
-  @Override
-  protected void onUnload() {
-    super.onUnload();
-    if (cmBase != null) {
-      cmBase.getWrapperElement().removeFromParent();
-    }
-    if (cmEdit != null) {
-      cmEdit.getWrapperElement().removeFromParent();
-    }
-    if (resizeHandler != null) {
-      resizeHandler.removeHandler();
-    }
-    if (closeHandler != null) {
-      closeHandler.removeHandler();
-    }
-    Window.enableScrolling(true);
-    Gerrit.setHeaderVisible(true);
-    JumpKeys.enable(true);
-  }
-
-  CodeMirror getEditor() {
-    return cmEdit;
-  }
-
-  @UiHandler("editSettings")
-  void onEditSetting(@SuppressWarnings("unused") ClickEvent e) {
-    editPrefsAction.show();
-  }
-
-  @UiHandler("save")
-  void onSave(@SuppressWarnings("unused") ClickEvent e) {
-    save().run();
-  }
-
-  @UiHandler("close")
-  void onClose(@SuppressWarnings("unused") ClickEvent e) {
-    if (cmEdit.isClean(generation) || Window.confirm(EditConstants.I.cancelUnsavedChanges())) {
-      upToChange();
-    }
-  }
-
-  private void displayBase() {
-    cmBase.getWrapperElement().getParentElement().removeClassName(style.hideBase());
-    cmEdit.getWrapperElement().getParentElement().removeClassName(style.fullWidth());
-    mv.getGapElement().removeClassName(style.hideBase());
-    setCmBaseValue();
-    setLineLength(prefs.lineLength());
-    cmBase.refresh();
-  }
-
-  @UiHandler("showBase")
-  void onShowBase(ValueChangeEvent<Boolean> e) {
-    boolean shouldShow = e.getValue();
-    if (shouldShow) {
-      if (baseContent == null) {
-        ChangeEditApi.get(
-            projectKey,
-            revision,
-            path,
-            true /* base */,
-            new HttpCallback<NativeString>() {
-              @Override
-              public void onSuccess(HttpResponse<NativeString> fc) {
-                baseContent = fc;
-                displayBase();
-              }
-
-              @Override
-              public void onFailure(Throwable e) {}
-            });
-      } else {
-        displayBase();
-      }
-    } else {
-      cmBase.getWrapperElement().getParentElement().addClassName(style.hideBase());
-      cmEdit.getWrapperElement().getParentElement().addClassName(style.fullWidth());
-      mv.getGapElement().addClassName(style.hideBase());
-    }
-    mv.setShowDifferences(shouldShow);
-  }
-
-  void setOption(String option, String value) {
-    cmBase.setOption(option, value);
-    cmEdit.setOption(option, value);
-  }
-
-  void setOption(String option, boolean value) {
-    cmBase.setOption(option, value);
-    cmEdit.setOption(option, value);
-  }
-
-  void setOption(String option, double value) {
-    cmBase.setOption(option, value);
-    cmEdit.setOption(option, value);
-  }
-
-  void setTheme(Theme newTheme) {
-    cmBase.operation(() -> cmBase.setOption("theme", newTheme.name().toLowerCase()));
-    cmEdit.operation(() -> cmEdit.setOption("theme", newTheme.name().toLowerCase()));
-  }
-
-  void setLineLength(int length) {
-    int adjustedLength = Patch.COMMIT_MSG.equals(path) ? 72 : length;
-    cmBase.extras().lineLength(adjustedLength);
-    cmEdit.extras().lineLength(adjustedLength);
-  }
-
-  void setIndentUnit(int indent) {
-    cmEdit.setOption("indentUnit", Patch.COMMIT_MSG.equals(path) ? 2 : indent);
-  }
-
-  void setShowLineNumbers(boolean show) {
-    cmBase.setOption("lineNumbers", show);
-    cmEdit.setOption("lineNumbers", show);
-  }
-
-  void setShowWhitespaceErrors(boolean show) {
-    cmBase.operation(() -> cmBase.setOption("showTrailingSpace", show));
-    cmEdit.operation(() -> cmEdit.setOption("showTrailingSpace", show));
-  }
-
-  void setShowTabs(boolean show) {
-    cmBase.extras().showTabs(show);
-    cmEdit.extras().showTabs(show);
-  }
-
-  void adjustHeight() {
-    int height = header.getOffsetHeight();
-    int rest = Gerrit.getHeaderFooterHeight() + height + 5; // Estimate
-    mv.getGapElement().getStyle().setHeight(Window.getClientHeight() - rest, Unit.PX);
-    cmBase.adjustHeight(height);
-    cmEdit.adjustHeight(height);
-  }
-
-  void setSyntaxHighlighting(boolean b) {
-    ModeInfo modeInfo = ModeInfo.findMode(content.getContentType(), path);
-    final String mode = modeInfo != null ? modeInfo.mime() : null;
-    if (b && mode != null && !mode.isEmpty()) {
-      injectMode(
-          mode,
-          new AsyncCallback<Void>() {
-            @Override
-            public void onSuccess(Void result) {
-              cmBase.setOption("mode", mode);
-              cmEdit.setOption("mode", mode);
-            }
-
-            @Override
-            public void onFailure(Throwable caught) {
-              prefs.syntaxHighlighting(false);
-            }
-          });
-    } else {
-      cmBase.setOption("mode", (String) null);
-      cmEdit.setOption("mode", (String) null);
-    }
-  }
-
-  private void upToChange() {
-    Gerrit.display(PageLinks.toChangeInEditMode(projectKey, revision.getParentKey()));
-  }
-
-  private void initEditor() {
-    ModeInfo mode = null;
-    String editContent = "";
-    if (content != null && content.getResult() != null) {
-      editContent = content.getResult().asString();
-      if (prefs.syntaxHighlighting()) {
-        mode = ModeInfo.findMode(content.getContentType(), path);
-      }
-    }
-
-    Configuration cfg =
-        Configuration.create()
-            .set("autoCloseBrackets", prefs.autoCloseBrackets())
-            .set("cursorBlinkRate", prefs.cursorBlinkRate())
-            .set("cursorHeight", 0.85)
-            .set("indentUnit", prefs.indentUnit())
-            .set("keyMap", prefs.keyMapType().name().toLowerCase())
-            .set("lineNumbers", prefs.hideLineNumbers())
-            .set("lineWrapping", prefs.lineWrapping())
-            .set("indentWithTabs", prefs.indentWithTabs())
-            .set("matchBrackets", prefs.matchBrackets())
-            .set("mode", mode != null ? mode.mime() : null)
-            .set("origLeft", editContent)
-            .set("scrollbarStyle", "overlay")
-            .set("showTrailingSpace", prefs.showWhitespaceErrors())
-            .set("styleSelectedText", true)
-            .set("tabSize", prefs.tabSize())
-            .set("theme", prefs.theme().name().toLowerCase())
-            .set("value", "");
-
-    if (editContent.contains("\r\n")) {
-      cfg.set("lineSeparator", "\r\n");
-    }
-
-    mv = MergeView.create(editor, cfg);
-
-    cmBase = mv.leftOriginal();
-    cmBase.getWrapperElement().addClassName(style.base());
-    cmEdit = mv.editor();
-    setCmBaseValue();
-    cmEdit.setValue(editContent);
-
-    CodeMirror.addCommand(
-        "save",
-        new CommandRunner() {
-          @Override
-          public void run(CodeMirror instance) {
-            save().run();
-          }
-        });
-  }
-
-  private void renderLinks(EditFileInfo editInfo, JsArray<DiffWebLinkInfo> diffLinks) {
-    renderLinksToDiff();
-
-    if (editInfo != null) {
-      renderLinks(Natives.asList(editInfo.webLinks()));
-    } else if (diffLinks != null) {
-      renderLinks(Natives.asList(diffLinks));
-    }
-  }
-
-  private void renderLinks(List<DiffWebLinkInfo> links) {
-    if (links != null) {
-      for (DiffWebLinkInfo webLink : links) {
-        linkPanel.add(webLink.toAnchor());
-      }
-    }
-  }
-
-  private void renderLinksToDiff() {
-    InlineHyperlink sbs = new InlineHyperlink();
-    sbs.setHTML(new ImageResourceRenderer().render(Gerrit.RESOURCES.sideBySideDiff()));
-    sbs.setTargetHistoryToken(
-        Dispatcher.toPatch(projectKey, "sidebyside", null, new Patch.Key(revision, path)));
-    sbs.setTitle(PatchUtil.C.sideBySideDiff());
-    linkPanel.add(sbs);
-
-    InlineHyperlink unified = new InlineHyperlink();
-    unified.setHTML(new ImageResourceRenderer().render(Gerrit.RESOURCES.unifiedDiff()));
-    unified.setTargetHistoryToken(
-        Dispatcher.toPatch(projectKey, "unified", null, new Patch.Key(revision, path)));
-    unified.setTitle(PatchUtil.C.unifiedDiff());
-    linkPanel.add(unified);
-  }
-
-  private Runnable updateCursorPosition() {
-    return () -> {
-      // The rendering of active lines has to be deferred. Reflow
-      // caused by adding and removing styles chokes Firefox when arrow
-      // key (or j/k) is held down. Performance on Chrome is fine
-      // without the deferral.
-      //
-      Scheduler.get().scheduleDeferred(() -> cmEdit.operation(this::updateActiveLine));
-    };
-  }
-
-  private void updateActiveLine() {
-    Pos p = cmEdit.getCursor("end");
-    cursLine.setInnerText(Integer.toString(p.line() + 1));
-    cursCol.setInnerText(Integer.toString(p.ch() + 1));
-    cmEdit.extras().activeLine(cmEdit.getLineHandleVisualStart(p.line()));
-  }
-
-  private void setClean(boolean clean) {
-    save.setEnabled(!clean);
-    close.setEnabled(true);
-    dirty.getStyle().setVisibility(!clean ? VISIBLE : HIDDEN);
-  }
-
-  private Runnable save() {
-    return () -> {
-      if (!cmEdit.isClean(generation)) {
-        close.setEnabled(false);
-        String text = cmEdit.getValue();
-        if (Patch.COMMIT_MSG.equals(path)) {
-          String trimmed = text.trim() + "\r";
-          if (!trimmed.equals(text)) {
-            text = trimmed;
-            cmEdit.setValue(text);
-          }
-        }
-        final int g = cmEdit.changeGeneration(false);
-        ChangeEditApi.put(
-            Project.NameKey.asStringOrNull(projectKey),
-            revision.getParentKey().get(),
-            path,
-            text,
-            new GerritCallback<VoidResult>() {
-              @Override
-              public void onSuccess(VoidResult result) {
-                generation = g;
-                setClean(cmEdit.isClean(g));
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {
-                close.setEnabled(true);
-              }
-            });
-      }
-    };
-  }
-
-  private void injectMode(String type, AsyncCallback<Void> cb) {
-    new ModeInjector().add(type).inject(cb);
-  }
-
-  private void setCmBaseValue() {
-    cmBase.setValue(
-        baseContent != null && baseContent.getResult() != null
-            ? baseContent.getResult().asString()
-            : "");
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.ui.xml
deleted file mode 100644
index 34282c8..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.ui.xml
+++ /dev/null
@@ -1,185 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:with field='ico' type='com.google.gerrit.client.GerritResources'/>
-  <ui:style gss='false' type='com.google.gerrit.client.editor.EditScreen.Style'>
-    @external .CodeMirror, .CodeMirror-cursor;
-    @external .CodeMirror-merge-2pane, .CodeMirror-merge-pane;
-    @external .CodeMirror-merge-gap;
-    @external .CodeMirror-scroll, .CodeMirror-overlayscroll-vertical;
-
-    .header {
-      position: relative;
-      height: 16px;
-      line-height: 16px;
-    }
-
-    .header .CodeMirror div.CodeMirror-cursor {
-      border-left: 2px solid black;
-    }
-
-    .headerLine {
-      background-color: #f7f7f7;
-      border-bottom: 1px solid #ddd;
-      padding-left: 30px;
-    }
-
-    .headerButtons {
-      display: inline-block;
-      padding-right: 5px;
-      border-right: 1px inset #ddd;
-      margin-right: 5px;
-    }
-
-    .headerButtons button:disabled {
-      background-color: #ddd;
-      font-weight: normal;
-      cursor: default;
-    }
-
-    .headerButtons button {
-      margin: 2px 0 2px 0;
-      text-align: center;
-      font-size: 8pt;
-      cursor: pointer;
-      border: 1px solid;
-      color: rgba(0, 0, 0, 0.15);
-      background-color: #f5f5f5;
-      -webkit-border-radius: 1px;
-      -webkit-box-sizing: content-box;
-    }
-
-    .headerButtons button div {
-      color: #444;
-      min-width: 54px;
-      white-space: nowrap;
-      line-height: 8pt;
-    }
-
-    .save {
-      font-weight: bold;
-    }
-
-    .path {
-      white-space: nowrap;
-    }
-
-    .statusLine {
-      position: fixed;
-      bottom: 0;
-      left: 0;
-      width: 175px;
-      height: 19px;
-      background-color: #f7f7f7;
-      border-top: 1px solid #ddd;
-      border-right: 1px solid #ddd;
-    }
-    .statusLine div {
-      height: inherit;
-    }
-
-    .cursorPosition {
-      display: inline-block;
-      margin: 0 5px 0 35px;
-      white-space: nowrap;
-    }
-
-    .dirty {
-      display: inline-block;
-      margin: 0 5px 0 5px;
-      padding: 0 0 0 5px;
-      border-left: 1px solid #ddd;
-      font-weight: bold;
-    }
-
-    .navigation {
-      position: absolute;
-      top: 0;
-      right: 10px;
-    }
-    .linkPanel {
-      float: left;
-    }
-    .linkPanel img {
-      padding-top: 2px;
-      padding-right: 3px;
-    }
-
-    .preferences {
-      position: relative;
-      top: 2px;
-      cursor: pointer;
-      outline: none;
-    }
-
-    .hideBase.CodeMirror-merge-pane {
-      display: none;
-    }
-
-    .hideBase.CodeMirror-merge-gap {
-      display: none;
-    }
-
-    .CodeMirror-merge-2pane .fullWidth.CodeMirror-merge-pane {
-      width: 100%;
-    }
-
-    /* Hide the vertical scrollbar on the base side. The edit side controls
-       both views */
-    .base .CodeMirror-scroll { margin-right: -42px; }
-    .base .CodeMirror-overlayscroll-vertical { display: none !important; }
-  </ui:style>
-  <g:HTMLPanel styleName='{style.header}'>
-    <div class='{style.headerLine}' ui:field='header'>
-       <div class='{style.headerButtons}'>
-         <g:Button ui:field='close'
-             title='Close file and return to change'>
-           <ui:attribute name='title'/>
-           <div><ui:msg>Close</ui:msg></div>
-         </g:Button>
-         <g:Button ui:field='save'
-             styleName='{style.save}'
-             title='Save'>
-           <ui:attribute name='title'/>
-           <div><ui:msg>Save</ui:msg></div>
-         </g:Button>
-       </div>
-       <span class='{style.path}'><span ui:field='project'/> / <span ui:field='filePath'/></span>
-       <div class='{style.navigation}'>
-         <g:Label text='Show Base' styleName='{style.linkPanel}'></g:Label>
-         <g:CheckBox ui:field='showBase' checked='true' styleName='{style.linkPanel}'
-             title='Show Base Version'>
-           <ui:attribute name='title'/>
-         </g:CheckBox>
-         <g:FlowPanel ui:field='linkPanel' styleName='{style.linkPanel}'/>
-         <g:Image
-             ui:field='editSettings'
-             styleName='{style.preferences}'
-             resource='{ico.gear}'
-             title='Edit screen preferences'>
-            <ui:attribute name='title'/>
-         </g:Image>
-       </div>
-    </div>
-    <div ui:field='editor' />
-    <div class='{style.statusLine}'>
-      <div class='{style.cursorPosition}'><span ui:field='cursLine'/> : <span ui:field='cursCol'/></div>
-      <div class='{style.dirty}' ui:field='dirty'>Unsaved</div>
-    </div>
-  </g:HTMLPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
deleted file mode 100644
index 4076296..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
+++ /dev/null
@@ -1,1060 +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.
- */
-
-/**
- * Make every single class external so users can rely on their names
- */
-@external .*;
-
-@def black #000000;
-@def white #ffffff;
-@def norm-font  sans-serif;
-@def mono-font  monospace;
-
-@eval backgroundColor com.google.gerrit.client.Gerrit.getTheme().backgroundColor;
-@eval topMenuColor com.google.gerrit.client.Gerrit.getTheme().topMenuColor;
-@eval textColor com.google.gerrit.client.Gerrit.getTheme().textColor;
-@eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
-@eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
-@eval changeTableOutdatedColor com.google.gerrit.client.Gerrit.getTheme().changeTableOutdatedColor;
-@eval tableOddRowColor com.google.gerrit.client.Gerrit.getTheme().tableOddRowColor;
-@eval tableEvenRowColor com.google.gerrit.client.Gerrit.getTheme().tableEvenRowColor;
-
-@sprite .greenCheckClass {
-  gwt-image: "greenCheck";
-}
-
-/** Override various GWT defaults */
-.gerritTopMenu {
-  font-size: 9pt;
-  padding-left: 5px;
-  padding-right: 5px;
-  background: transparent;
-}
-
-body, table td, select {
-  font-family: norm-font;
-}
-
-button {
-  padding: 1px 6px;
-}
-
-.gerritBody {
-  font-size: small;
-  padding-left: 5px;
-  padding-right: 5px;
-}
-
-a,
-a:visited {
-  color: #0654ac;
-  text-decoration: none;
-}
-
-a:hover {
-  color: #0654ac;
-  text-decoration: underline;
-}
-
-#gerrit_btmmenu {
-  clear: both;
-  color: #a0adcc;
-  text-align: right;
-  padding-right: 10px;
-}
-
-.version a,
-.version a:visited,
-.version a:hover {
-  color: #2a5db0;
-}
-
-
-/** Widgets **/
-.gwt-Button {
-  color: black;
-}
-
-.accountLinkPanel {
-  display: inline;
-}
-
-.accountLinkPanel img {
-  margin-right: 0.2em;
-  position: relative;
-  top: 2px;
-  height: 16px !important;
-  width: 16px;
-}
-
-.accountLinkPanel a {
-  position: relative;
-  top: -1px;
-}
-
-.inputFieldTypeHint {
-  color: grey;
-}
-
-.smallHeading {
-  margin-top: 5px;
-  font-weight: bold;
-}
-
-.link {
-  cursor: pointer;
-}
-
-.extensionPanel {
-  padding-top: 10px;
-}
-
-/** MenuScreen **/
-.menuScreenMenuBar {
-  background: topMenuColor;
-  padding-top: 0.5em;
-  padding-bottom: 10em;
-  padding-left: 0.5em;
-  padding-right: 0.5em;
-  border-right: 1px solid black;
-  margin-right: 0.5em;
-}
-
-.menuScreenMenuBar .menuItem {
-  white-space: nowrap;
-  display: block;
-  border-right: none;
-  padding: 0.2em;
-}
-
-.menuScreenMenuBar .menuItem.activeRow {
-  background: selectionColor;
-}
-
-.menuItem.activeRow {
-  background: selectionColor;
-}
-
-/** Menu **/
-.linkMenuBar {
-  font-size: 9pt;
-  display: inline;
-  white-space: nowrap;
-  padding-left: 6px;
-}
-.menuItem {
-  padding-left: 5px;
-  padding-right: 5px;
-}
-.linkMenuItemNotLast {
-  border-right: 1px solid black;
-}
-
-.topmenu {
-  width: 100%;
-}
-.topmenuTDmenu {
-  vertical-align: top;
-}
-.topmenuTDglue {
-  width: 100%;
-}
-
-.topmenuMenuLeft {
-  width: 300px;
-  font-size: 9pt;
-  padding-top: 5px;
-  padding-left: 5px;
-  padding-right: 5px;
-  background: none;
-  position: relative;
-  top: 0;
-}
-.topmenuMenuLeft tbody tr td table {
-  border: 0;
-}
-.topmenuMenuLeft tbody tr td table.gwt-TabBar {
-  border-bottom: 1px solid #DDD;
-}
-.topmenuMenuLeft .gwt-TextBox {
-  width: 250px;
-}
-.topmenuMenuLeft .gwt-Button {
-  padding: 3px 6px;
-}
-.topmenuMenuLeft .gwt-TabBarFirst {
-  display: none;
-}
-.topmenuMenuLeft .gwt-TabBarItem {
-  margin: 0px;
-  background: transparent;
-  padding-top: 0px;
-  padding-bottom: 1px;
-  padding-left: 1em;
-  padding-right: 1em;
-}
-.topmenuMenuLeft .gwt-TabBarRest {
-  background: transparent;
-  padding-top: 0px;
-}
-.topmenuMenuLeft .gwt-TabPanelBottom {
-  background: transparent;
-  border-top: none;
-  border-left: none;
-  border-right: none;
-  border-bottom: none;
-  padding: 1px;
-}
-.topmenuMenuLeft .menuItem {
-  padding-left: 1em;
-  padding-right: 1em;
-  border-right: none;
-}
-
-.topmenuMenuRight {
-  float: right;
-  text-align: right;
-}
-.menuBarUserName {
-  padding-left: 5px;
-  padding-right: 5px;
-  white-space: nowrap;
-}
-.menuBarUserNameAvatar {
-  vertical-align: middle;
-}
-.menuBarUserNameFocusPanel {
-  display: inline;
-}
-.menuBarUserNamePanel {
-  display: inline;
-  cursor: pointer;
-  font-weight: bold;
-}
-.userInfoPopup {
-  border: 1px solid black;
-  background: white;
-  box-shadow: 3px 3px 5px #888;
-  z-index: 200;
-}
-.searchPanel {
-  white-space: nowrap;
-  display: inline;
-}
-.searchPanel .searchTextBox {
-  font-size: 9pt;
-  margin: 8.286px 3px 0 0;
-}
-.searchPanel .searchDropdown {
-  font-size: 8pt;
-  border: 2px solid;
-  border-color: rgba(0, 0, 0, 0.15);
-  height: 16px;
-  border-radius: 2px;
-  box-sizing: content-box;
-}
-.searchPanel .searchButton {
-  text-align: center;
-  font-size: 8pt;
-  font-weight: bold;
-  cursor: pointer;
-  border: 2px solid;
-  color: #FFF;
-  border-color: rgba(0, 0, 0, 0.15);
-  height: 14px;
-  background-color: #53A93F;
-  border-radius: 2px;
-  box-sizing: content-box;
-}
-.suggestBoxPopup {
-  z-index: 200;
-}
-
-/** RPC Status **/
-.rpcStatus {
-  position: fixed;
-  top: 6px;
-  left: 50%;
-  padding-top: 4px;
-  padding-bottom: 4px;
-  padding-left: 10px;
-  padding-right: 10px;
-  text-align: center;
-  font-weight: bold;
-  background: #FFF1A8;
-  z-index: 200;
-}
-
-
-/** Error Dialog **/
-.errorDialog {
-  background: none;
-  border: none;
-  padding: 10px;
-  width: 600px;
-  color: backgroundColor;
-  font-size: 15px;
-  font-family: verdana;
-  z-index: 200;
-}
-.errorDialogGlass {
-  opacity: 0.75;
-  z-index: 200;
-}
-@if user.agent safari {
-  .errorDialogGlass {
-    opacity: 0.80;
-  }
-}
-@if user.agent ie8 {
-  /* IE just doesn't do opacity the way we want, make our dialog
-   * stand out in a way that it can't be missed against the page
-   */
-  .errorDialog {
-    color: black;
-    background: darkgray;
-    border: 10px groove lightgrey;
-  }
-}
-.errorDialogTitle {
-  font-size: 30px;
-  font-weight: bold;
-  margin-bottom: 15px;
-}
-.errorDialogErrorType {
-  font-weight: bold;
-  white-space: nowrap;
-  margin-bottom: 15px;
-}
-.errorDialogButtons {
-  width: 100%;
-  margin-top: 15px;
-}
-.errorDialog a,
-.errorDialog a:visited,
-.errorDialog a:hover {
-  color: white;
-  font-weight: bold;
-  font-size: 15px;
-  font-family: verdana;
-}
-.loadingPluginsDialog {
-  background: #fff;
-  color: #000;
-  width: auto;
-}
-
-
-/** Screen **/
-.screen {
-}
-
-.screenHeader {
-  white-space: nowrap;
-  font-size: 16pt;
-  margin: 3px 0 8px;
-  text-overflow: ellipsis;
-  overflow: hidden;
-}
-
-/** ChangeTable **/
-.changeTable {
-  border-collapse: separate;
-  border-spacing: 0;
-}
-
-.changeTable tr:nth-child\(even\) {
-  background: tableEvenRowColor;
-}
-
-.changeTable tr:nth-child\(odd\) {
-  background: tableOddRowColor;
-}
-
-.changeTable .iconCell {
-  width: 1px;
-  padding: 0px;
-  vertical-align: middle;
-  border-bottom: 1px solid trimColor;
-}
-
-.changeTable .leftMostCell {
-  border-left: 1px solid trimColor;
-}
-
-.changeTable .dataCell {
-  padding-left: 5px;
-  padding-right: 5px;
-  border-right: 1px solid trimColor;
-  border-bottom: 1px solid trimColor;
-  vertical-align: middle;
-  height: 20px;
-}
-
-.changeTable .dataCellHidden {
-  display: none;
-}
-
-.changeTable a.gwt-InlineHyperlink,
-.changeTable a.gwt-Anchor {
-  color: #222 !important;
-}
-
-.changeTable .changeSize {
-  height: 10px;
-  display: inline-block;
-  opacity: 0.6;
-}
-
-.accountDashboard.changeTable tr {
-  color: #444444;
-}
-.accountDashboard.changeTable tr a {
-  color: #444444;
-  text-decoration: none;
-}
-.accountDashboard.changeTable .needsReview,
-.accountDashboard.changeTable .needsReview a {
-  font-weight: bold;
-  color: textColor;
-}
-
-.changeTable .activeRow,
-.accountDashboard.changeTable .activeRow,
-.accountDashboard.changeTable .activeRow a {
-  background: selectionColor !important;
-}
-
-.changeTable .cSIZE {
-  width: 70px;
-  text-align: right;
-}
-
-.changeTable .cSUBJECT div {
-  text-overflow: ellipsis;
-  overflow: hidden;
-  white-space: nowrap;
-}
-
-.changeTable .cASSIGNEDTOME {
-  background: #ffe9d6 !important;
-}
-
-.changeTable .cASSIGNEE,
-.changeTable .cOWNER,
-.changeTable .cSTATUS {
-  white-space: nowrap;
-}
-
-.changeTable .cLastUpdate {
-  white-space: nowrap;
-  text-align: right;
-  width: 1em;
-}
-
-.changeTable .groupName {
-  white-space: nowrap;
-}
-
-.changeTable .cAPPROVAL {
-  width: 0.5em;
-  text-align: center;
-}
-.changeTable .dataCell.negscore {
-  color: red;
-}
-.changeTable .dataCell.posscore {
-  color: #08a400;
-}
-.changeTable .dataCell.singleLine {
-  white-space: nowrap;
-}
-.changeTable .dataCell.labelNotApplicable {
- background: #F5F5F5;
-}
-.changeTable .iconHeader {
-  border-top: 1px solid backgroundColor;
-  border-bottom: 1px solid backgroundColor;
-  background-color: trimColor;
-}
-
-.changeTable .dataHeader {
-  border: 1px solid backgroundColor;
-  padding: 2px 6px 1px;
-  background-color: trimColor;
-  font-style: italic;
-  white-space: nowrap;
-  color: textColor;
-}
-
-.changeTable .dataHeaderHidden {
-  display: none;
-}
-
-.changeTable .sectionHeader {
-  border-top: 8px solid backgroundColor;
-  padding: 2px 6px 1px;
-  background-color: trimColor;
-  white-space: nowrap;
-  font-weight: bold;
-  color: textColor;
-}
-
-.changeTable .emptySection {
-  border-left: 1px solid trimColor;
-  border-right: 1px solid trimColor;
-  border-bottom: 1px solid trimColor;
-  font-style: italic;
-  padding-left: 25px;
-}
-
-.changeTablePrevNextLinks {
-  float: right;
-  padding-right: 5px;
-}
-.changeTablePrevNextLinks td {
-  width: 5em;
-  text-align: right;
-}
-.changeTablePrevNextLinks .gwt-Hyperlink {
-  font-size: 9pt;
-  color: #2a5db0;
-}
-
-/** Change **/
-.avatarInfoPanel {
-  margin-right: 10px;
-}
-.avatarInfoPanel td {
-  text-align: center;
-}
-
-.infoBlock {
-  border-collapse: collapse;
-  border-spacing: 0;
-}
-
-.infoBlock td {
-  padding: 2px 4px 2px 6px;
-  border-right: 1px solid trimColor;
-  border-bottom: 1px solid trimColor;
-  text-align: left;
-  white-space: nowrap;
-}
-
-.infoBlock td td {
-  padding-left: 0px;
-  border-right: 0px;
-}
-
-.infoBlock td.topmost {
-  border-top: 1px solid trimColor;
-}
-
-.infoBlock td.header {
-  background-color: trimColor;
-  font-style: italic;
-  text-align: right;
-}
-
-.infoBlock td.bottomheader {
-  border-bottom: 1px solid trimColor;
-}
-
-
-.patchSetActions {
-  margin-bottom: 10px;
-}
-.patchSetActions .gwt-Button {
-  margin-right: 30px;
-  font-size: 8pt;
-}
-
-.downloadBox {
-  min-width: 580px;
-  margin: 5px;
-  margin-right: 15px;
-}
-.downloadBoxTable {
-  border-spacing: 0;
-  width: 100%;
-}
-.downloadBoxTableCommandColumn {
-  text-align: left;
-  font-weight: normal;
-  white-space: nowrap;
-  max-height: 18px;
-  width: 80px;
-  padding-right: 5px;
-}
-.downloadBoxSpacer {
-  margin-left: 5px;
-  margin-right: 5px;
-}
-.downloadBoxScheme {
-  float: right;
-}
-.downloadBoxCopyLabel {
-  font-size: smaller;
-  font-family: monospace;
-}
-.downloadBoxCopyLabel span {
-  width: 500px;
-  white-space: nowrap;
-  display: inline-block;
-  overflow: hidden;
-  text-overflow: ellipsis;
-}
-.downloadBoxCopyLabel .gwt-TextBox {
-  padding: 0;
-  margin: 0;
-  border: 0;
-  max-height: 18px;
-  width: 500px;
-}
-.downloadBoxCopyLabel div {
-  float: right;
-}
-.downloadLinkHeader {
-  background: trimColor;
-  white-space: nowrap;
-  border-bottom: 1px solid black;
-}
-.downloadLinkHeaderGap {
-  margin-left: 5em;
-}
-.downloadLinkList {
-  display: inline;
-  white-space: nowrap;
-}
-.downloadLink {
-  color: black;
-  text-decoration: none;
-  white-space: nowrap;
-  background: trimColor;
-  border-right: 1px solid black;
-  padding-left: 0.5em;
-  padding-right: 0.5em;
-}
-a:hover.downloadLink {
-  color: black;
-}
-.downloadLink_Active {
-  background: selectionColor;
-}
-.downloadLinkCopyLabel {
-  white-space: pre;
-  font-family: mono-font;
-  font-size: 12px;
-  margin-left: 0.5em;
-  margin-right: 0.5em;
-}
-.downloadLinkCopyLabel .gwt-TextBox {
-  width: 40em;
-}
-.downloadLinkCopyLabel span {
-  width: 40em;
-  white-space: nowrap;
-  display: inline-block;
-  overflow: hidden;
-  text-overflow: ellipsis;
-}
-
-/** AccountSettings  **/
-.usernameField {
-  white-space: nowrap;
-}
-.accountUsername {
-  font-family: mono-font;
-  font-size: small;
-}
-.accountPassword {
-  font-family: mono-font;
-  font-size: small;
-}
-.sshKeyPanelEncodedKey {
-  white-space: nowrap;
-  text-overflow: ellipsis;
-  overflow: hidden;
-  font-family: mono-font;
-  font-size: small;
-}
-.sshKeyPanelInvalid {
-  white-space: nowrap;
-  color: red;
-  font-weight: bold;
-}
-.identityUntrustedExternalId {
-  white-space: nowrap;
-  color: red;
-  font-weight: bold;
-}
-
-.accountInfoBlock {
-  margin-bottom: 10px;
-}
-.accountInfoBlock .gwt-Button {
-  margin-left: 10px;
-}
-
-.addWatchPanel {
-  margin-top: 10px;
-  padding: 5px 5px 5px 5px;
-}
-.watchedProjectFilter {
-  margin-left: 1em;
-  color: grey;
-}
-
-.addBranch {
-  margin-top: 10px;
-  background-color: trimColor;
-  padding: 5px 5px 5px 5px;
-}
-
-.addSshKeyPanel {
-  margin-top: 10px;
-  background-color: trimColor;
-  padding: 5px 5px 5px 5px;
-}
-
-.addSshKeyPanel ol {
-  margin-top: 0px;
-  margin-bottom: 5px;
-}
-
-.addSshKeyPanel td {
-  width: 100%;
-}
-
-.sshKeyTable td.dataCell, .sshKeyTable td.iconCell {
-  vertical-align: top;
-}
-
-.createProjectPanel {
-  margin-bottom: 10px;
-  background-color: trimColor;
-  padding: 5px 5px 5px 5px;
-}
-
-.sshHostKeyPanel {
-  margin-top: 10px;
-  border: 1px solid trimColor;
-  padding: 5px 5px 5px 5px;
-}
-.sshHostKeyPanelHeading {
-  white-space: nowrap;
-  margin-top: 5px;
-  margin-left: 1em;
-}
-.sshHostKeyPanelFingerprintData {
-  margin-left: 2em;
-  white-space: nowrap;
-  font-family: mono-font;
-  font-size: small;
-}
-.sshHostKeyPanelKnownHostEntry {
-  margin-left: 2em;
-  white-space: nowrap;
-  font-family: mono-font;
-  font-size: small;
-  width: 80em;
-}
-
-.contributorAgreementButton {
-  font-weight: bold;
-}
-
-.contributorAgreementShortDescription {
-  margin-left: 20px;
-  margin-right: 20px;
-  margin-bottom: 10px;
-  padding: 5px 5px 5px 5px;
-  border: 1px solid #b0bdcc;
-}
-
-.contributorAgreementAlreadySubmitted {
-  margin-left: 20px;
-  margin-right: 20px;
-  padding: 5px 5px 5px 5px;
-  color: red;
-}
-
-.contributorAgreementLegal {
-  margin-left: 20px;
-  margin-right: 20px;
-  padding: 5px 5px 5px 5px;
-  border: 1px solid #b0bdcc;
-}
-
-.registerScreenSection {
-  margin-top: 2em;
-}
-.registerScreenExplain {
-  margin-left: 10px;
-  margin-top: 5px;
-  margin-bottom: 5px;
-  width: 45em;
-}
-.registerScreenNextLinks {
-  margin-top: 2em;
-}
-.registerScreenNextLinks .gwt-InlineHyperlink {
-  margin-left: 2em;
-  white-space: nowrap;
-}
-.registerScreenSection .changeTable {
-  width: 45em;
-}
-.registerScreenSection .addSshKeyPanel {
-  background: none;
-}
-.registerScreenSection .sshHostKeyPanel {
-  border: none;
-}
-.registerScreenSection .sshHostKeyPanel .sshHostKeyPanelKnownHostEntry {
-  width: 45em;
-}
-
-.projectActions {
-  margin-bottom: 10px;
-}
-
-.oauthInfoBlock {
-  margin-bottom: 10px;
-}
-.oauthToken {
-  font-family: monospace;
-  font-size: small;
-  width: 40em;
-}
-.oauthToken span {
-  white-space: nowrap;
-  display: inline-block;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  width: 38em;
-}
-.oauthExpires {
-  font-family: monospace;
-  font-size: small;
-  width: 40em;
-}
-.oauthPanel {
-  margin-top: 10px;
-  border: 1px solid trimColor;
-  padding: 5px 5px 5px 5px;
-}
-.oauthPanelNetRCHeading {
-  margin-top: 5px;
-  margin-left: 1em;
-  white-space: nowrap;
-}
-.oauthPanelNetRCEntry {
-  margin-top: 5px;
-  margin-left: 2em;
-  font-family: monospace;
-  font-size: small;
-  width: 80em;
-}
-.oauthPanelNetRCEntry span {
-  white-space: nowrap;
-  display: inline-block;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  width: 78em;
-}
-.oauthPanelCookieHeading {
-  margin-top: 15px;
-  margin-left: 1em;
-  white-space: nowrap;
-}
-.oauthPanelCookieEntry {
-  margin-top: 5px;
-  margin-left: 2em;
-  font-family: monospace;
-  font-size: small;
-  width: 80em;
-}
-.oauthPanelCookieEntry span {
-  white-space: nowrap;
-  display: inline-block;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  width: 78em;
-}
-
-
-/** CommentedActionDialog **/
-.commentedActionDialog .gwt-DisclosurePanel .header td {
-  font-weight: bold;
-  white-space: nowrap;
-}
-.commentedActionDialog .smallHeading {
-  font-size: small;
-  font-weight: bold;
-  white-space: nowrap;
-}
-.commentedActionDialog .commentedActionMessage {
-  margin-left: 10px;
-  background: trimColor;
-  padding: 5px 5px 5px 5px;
-}
-.commentedActionDialog .commentedActionMessage textarea {
-  font-size: small;
-}
-.commentedActionDialog .gwt-Hyperlink {
-  white-space: nowrap;
-  font-size: small;
-}
-.commentedActionDialog .rebaseContentPanel {
-  margin-left: 10px;
-  background: trimColor;
-  padding: 5px 5px 5px 5px;
-  width: 300px;
-}
-.commentedActionDialog .rebaseContentPanel .rebaseSuggestBox {
-  font-size: small;
-  width: 100%;
-}
-
-/** AccountGroupInfoScreen **/
-.groupUUIDPanel {
-  margin-bottom: 10px;
-}
-.groupDescriptionPanel {
-  margin-bottom: 3px;
-}
-.groupNamePanel {
-  margin-bottom: 3px;
-}
-.groupNameTextBox {
-  margin-bottom: 2px;
-}
-.groupOptionsPanel {
-  margin-bottom: 5px;
-}
-.groupOwnerPanel {
-  margin-bottom: 3px;
-}
-.groupOwnerTextBox {
-  margin-bottom: 2px;
-}
-
-
-/** AccountGroupMembersScreen **/
-.groupMembersTable {
-  margin-bottom: 2px;
-}
-.groupIncludesTable {
-  margin-bottom: 2px;
-}
-
-
-/** AddMemberBox **/
-.addMemberTextBox {
-  margin-right: 2px;
-  margin-bottom: 2px;
-}
-
-
-/** ProjectBranchesScreen **/
-.specialBranchIconCell {
-  background: #ECECEC;
-  border-bottom: 1px solid #FFFFFF;
-  border-top: 1px solid #FFFFFF;
-}
-.specialBranchDataCell {
-  background: #ECECEC;
-  border: 1px solid white;
-  font-style: italic;
-  padding: 2px 6px 1px;
-}
-
-.editHeadButton {
-  float: right;
-  cursor: pointer;
-}
-
-.branchTableDeleteButton {
-  margin-top: 5px;
-}
-
-.branchTablePrevNextLinks {
-  position: relative;
-}
-.branchTablePrevNextLinks td {
-  float: left;
-  width: 5em;
-  text-align: left;
-  padding-right: 10px;
-}
-.branchTablePrevNextLinks .gwt-Hyperlink {
-  font-size: 9pt;
-  color: #2a5db0;
-}
-
-/** PluginListScreen **/
-.pluginsTable {
-}
-
-/** ProjectListScreen **/
-.projectFilterPanel {
-  margin-bottom: 10px;
-}
-.projectFilterPanel input {
-  width: 200px;
-}
-.projectFilterLabel {
-  margin-right: 5px;
-}
-.projectNameColumn {
-  min-width: 300px;
-}
-
-.queryIcon {
-  position: relative;
-  top: 2px;
-  margin-right: 3px;
-}
-
-/** ProjectSettings */
-.maxObjectSizeLimitEffectiveLabel {
-  padding-top: 5px;
-  padding-left: 5px;
-}
-
-.pluginProjectConfigInheritedValue {
-  padding-top: 5px;
-  padding-left: 5px;
-}
-
-/* StringListPanel */
-.stringListPanelButtons {
-  margin-left: 0.5em;
-}
-.stringListPanelButtons .gwt-Button {
-  margin-right: 2em;
-  font-size: 7pt;
-  padding: 1px;
-}
-
-/* List Screens */
-.pagingLink {
-  font-size: 18px;
-  margin-top: 5px;
-  margin-bottom: 15px;
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupApi.java
deleted file mode 100644
index 01c4d26..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupApi.java
+++ /dev/null
@@ -1,270 +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.client.groups;
-
-import com.google.gerrit.client.VoidResult;
-import com.google.gerrit.client.info.AccountInfo;
-import com.google.gerrit.client.info.GroupInfo;
-import com.google.gerrit.client.rpc.NativeString;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import java.util.Set;
-
-/** A collection of static methods which work on the Gerrit REST API for specific groups. */
-public class GroupApi {
-  /** Create a new group */
-  public static void createGroup(String groupName, AsyncCallback<GroupInfo> cb) {
-    JavaScriptObject in = JavaScriptObject.createObject();
-    new RestApi("/groups/").id(groupName).ifNoneMatch().put(in, cb);
-  }
-
-  public static void getGroupDetail(String group, AsyncCallback<GroupInfo> cb) {
-    group(group).view("detail").get(cb);
-  }
-
-  /** Get the name of a group */
-  public static void getGroupName(AccountGroup.UUID group, AsyncCallback<NativeString> cb) {
-    group(group).view("name").get(cb);
-  }
-
-  /** Check if the current user is owner of a group */
-  public static void isGroupOwner(String groupName, AsyncCallback<Boolean> cb) {
-    GroupMap.myOwned(
-        groupName,
-        new AsyncCallback<GroupMap>() {
-          @Override
-          public void onSuccess(GroupMap result) {
-            cb.onSuccess(!result.isEmpty());
-          }
-
-          @Override
-          public void onFailure(Throwable caught) {
-            cb.onFailure(caught);
-          }
-        });
-  }
-
-  /** Rename a group */
-  public static void renameGroup(
-      AccountGroup.UUID group, String newName, AsyncCallback<VoidResult> cb) {
-    GroupInput in = GroupInput.create();
-    in.name(newName);
-    group(group).view("name").put(in, cb);
-  }
-
-  /** Set description for a group */
-  public static void setGroupDescription(
-      AccountGroup.UUID group, String description, AsyncCallback<VoidResult> cb) {
-    RestApi call = group(group).view("description");
-    if (description != null && !description.isEmpty()) {
-      GroupInput in = GroupInput.create();
-      in.description(description);
-      call.put(in, cb);
-    } else {
-      call.delete(cb);
-    }
-  }
-
-  /** Set owner for a group */
-  public static void setGroupOwner(
-      AccountGroup.UUID group, String owner, AsyncCallback<GroupInfo> cb) {
-    GroupInput in = GroupInput.create();
-    in.owner(owner);
-    group(group).view("owner").put(in, cb);
-  }
-
-  /** Set the options for a group */
-  public static void setGroupOptions(
-      AccountGroup.UUID group, boolean isVisibleToAll, AsyncCallback<VoidResult> cb) {
-    GroupOptionsInput in = GroupOptionsInput.create();
-    in.visibleToAll(isVisibleToAll);
-    group(group).view("options").put(in, cb);
-  }
-
-  /** Add member to a group. */
-  public static void addMember(
-      AccountGroup.UUID group, String member, AsyncCallback<AccountInfo> cb) {
-    members(group).id(member).put(cb);
-  }
-
-  /** Add members to a group. */
-  public static void addMembers(
-      AccountGroup.UUID group, Set<String> members, AsyncCallback<JsArray<AccountInfo>> cb) {
-    if (members.size() == 1) {
-      addMember(
-          group,
-          members.iterator().next(),
-          new AsyncCallback<AccountInfo>() {
-            @Override
-            public void onSuccess(AccountInfo result) {
-              cb.onSuccess(Natives.arrayOf(result));
-            }
-
-            @Override
-            public void onFailure(Throwable caught) {
-              cb.onFailure(caught);
-            }
-          });
-    } else {
-      MemberInput input = MemberInput.create();
-      for (String member : members) {
-        input.addMember(member);
-      }
-      members(group).post(input, cb);
-    }
-  }
-
-  /** Remove members from a group. */
-  public static void removeMembers(
-      AccountGroup.UUID group, Set<Integer> ids, AsyncCallback<VoidResult> cb) {
-    if (ids.size() == 1) {
-      members(group).id(ids.iterator().next().toString()).delete(cb);
-    } else {
-      MemberInput in = MemberInput.create();
-      for (Integer id : ids) {
-        in.addMember(id.toString());
-      }
-      group(group).view("members.delete").post(in, cb);
-    }
-  }
-
-  /** Include a group into a group. */
-  public static void addIncludedGroup(
-      AccountGroup.UUID group, String include, AsyncCallback<GroupInfo> cb) {
-    groups(group).id(include).put(cb);
-  }
-
-  /** Include groups into a group. */
-  public static void addIncludedGroups(
-      AccountGroup.UUID group,
-      Set<String> includedGroups,
-      final AsyncCallback<JsArray<GroupInfo>> cb) {
-    if (includedGroups.size() == 1) {
-      addIncludedGroup(
-          group,
-          includedGroups.iterator().next(),
-          new AsyncCallback<GroupInfo>() {
-            @Override
-            public void onSuccess(GroupInfo result) {
-              cb.onSuccess(Natives.arrayOf(result));
-            }
-
-            @Override
-            public void onFailure(Throwable caught) {
-              cb.onFailure(caught);
-            }
-          });
-    } else {
-      IncludedGroupInput input = IncludedGroupInput.create();
-      for (String includedGroup : includedGroups) {
-        input.addGroup(includedGroup);
-      }
-      groups(group).post(input, cb);
-    }
-  }
-
-  /** Remove included groups from a group. */
-  public static void removeIncludedGroups(
-      AccountGroup.UUID group, Set<AccountGroup.UUID> ids, AsyncCallback<VoidResult> cb) {
-    if (ids.size() == 1) {
-      AccountGroup.UUID g = ids.iterator().next();
-      groups(group).id(g.get()).delete(cb);
-    } else {
-      IncludedGroupInput in = IncludedGroupInput.create();
-      for (AccountGroup.UUID g : ids) {
-        in.addGroup(g.get());
-      }
-      group(group).view("groups.delete").post(in, cb);
-    }
-  }
-
-  /** Get audit log of a group. */
-  public static void getAuditLog(
-      AccountGroup.UUID group, AsyncCallback<JsArray<GroupAuditEventInfo>> cb) {
-    group(group).view("log.audit").get(cb);
-  }
-
-  private static RestApi members(AccountGroup.UUID group) {
-    return group(group).view("members");
-  }
-
-  private static RestApi groups(AccountGroup.UUID group) {
-    return group(group).view("groups");
-  }
-
-  private static RestApi group(AccountGroup.UUID group) {
-    return group(group.get());
-  }
-
-  private static RestApi group(String group) {
-    return new RestApi("/groups/").id(group);
-  }
-
-  private static class GroupInput extends JavaScriptObject {
-    final native void description(String d) /*-{ if(d)this.description=d; }-*/;
-
-    final native void name(String n) /*-{ if(n)this.name=n; }-*/;
-
-    final native void owner(String o) /*-{ if(o)this.owner=o; }-*/;
-
-    static GroupInput create() {
-      return (GroupInput) createObject();
-    }
-
-    protected GroupInput() {}
-  }
-
-  private static class GroupOptionsInput extends JavaScriptObject {
-    final native void visibleToAll(boolean v) /*-{ if(v)this.visible_to_all=v; }-*/;
-
-    static GroupOptionsInput create() {
-      return (GroupOptionsInput) createObject();
-    }
-
-    protected GroupOptionsInput() {}
-  }
-
-  private static class MemberInput extends JavaScriptObject {
-    final native void init() /*-{ this.members = []; }-*/;
-
-    final native void addMember(String n) /*-{ this.members.push(n); }-*/;
-
-    static MemberInput create() {
-      MemberInput m = (MemberInput) createObject();
-      m.init();
-      return m;
-    }
-
-    protected MemberInput() {}
-  }
-
-  private static class IncludedGroupInput extends JavaScriptObject {
-    final native void init() /*-{ this.groups = []; }-*/;
-
-    final native void addGroup(String n) /*-{ this.groups.push(n); }-*/;
-
-    static IncludedGroupInput create() {
-      IncludedGroupInput g = (IncludedGroupInput) createObject();
-      g.init();
-      return g;
-    }
-
-    protected IncludedGroupInput() {}
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupAuditEventInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupAuditEventInfo.java
deleted file mode 100644
index 255c6e8..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupAuditEventInfo.java
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.groups;
-
-import com.google.gerrit.client.info.AccountInfo;
-import com.google.gerrit.client.info.GroupInfo;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwtjsonrpc.client.impl.ser.JavaSqlTimestamp_JsonSerializer;
-import java.sql.Timestamp;
-
-public class GroupAuditEventInfo extends JavaScriptObject {
-  public enum Type {
-    ADD_USER,
-    REMOVE_USER,
-    ADD_GROUP,
-    REMOVE_GROUP
-  }
-
-  public final Timestamp date() {
-    return JavaSqlTimestamp_JsonSerializer.parseTimestamp(dateRaw());
-  }
-
-  public final Type type() {
-    return Type.valueOf(typeRaw());
-  }
-
-  public final native AccountInfo user() /*-{ return this.user; }-*/;
-
-  public final native AccountInfo memberAsUser() /*-{ return this.member; }-*/;
-
-  public final native GroupInfo memberAsGroup() /*-{ return this.member; }-*/;
-
-  private native String dateRaw() /*-{ return this.date; }-*/;
-
-  private native String typeRaw() /*-{ return this.type; }-*/;
-
-  protected GroupAuditEventInfo() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupList.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupList.java
deleted file mode 100644
index db966b1..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupList.java
+++ /dev/null
@@ -1,34 +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.client.groups;
-
-import com.google.gerrit.client.info.GroupInfo;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-
-/** Groups available from {@code /groups/} or {@code /accounts/[id]/groups}. */
-public class GroupList extends JsArray<GroupInfo> {
-  public static void my(AsyncCallback<GroupList> callback) {
-    new RestApi("/accounts/self/groups").get(callback);
-  }
-
-  public static void included(AccountGroup.UUID group, AsyncCallback<GroupList> callback) {
-    new RestApi("/groups/").id(group.get()).view("groups").get(callback);
-  }
-
-  protected GroupList() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupMap.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupMap.java
deleted file mode 100644
index 73ac183..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupMap.java
+++ /dev/null
@@ -1,78 +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.client.groups;
-
-import com.google.gerrit.client.info.GroupInfo;
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-
-/** Groups available from {@code /groups/}. */
-public class GroupMap extends NativeMap<GroupInfo> {
-  public static void all(AsyncCallback<GroupMap> callback) {
-    groups().get(NativeMap.copyKeysIntoChildren(callback));
-  }
-
-  public static void match(String match, int limit, int start, AsyncCallback<GroupMap> cb) {
-    RestApi call = groups();
-    if (match != null) {
-      if (match.startsWith("^")) {
-        call.addParameter("r", match);
-      } else {
-        call.addParameter("m", match);
-      }
-    }
-    if (limit > 0) {
-      call.addParameter("n", limit);
-    }
-    if (start > 0) {
-      call.addParameter("S", start);
-    }
-    call.get(NativeMap.copyKeysIntoChildren(cb));
-  }
-
-  public static void suggestAccountGroupForProject(
-      String project, String query, int limit, AsyncCallback<GroupMap> cb) {
-    RestApi call = groups();
-    if (project != null) {
-      call.addParameter("p", project);
-    }
-    if (query != null) {
-      call.addParameter("s", query);
-    }
-    if (limit > 0) {
-      call.addParameter("n", limit);
-    }
-    call.get(NativeMap.copyKeysIntoChildren(cb));
-  }
-
-  public static void myOwned(AsyncCallback<GroupMap> cb) {
-    myOwnedGroups().get(NativeMap.copyKeysIntoChildren(cb));
-  }
-
-  public static void myOwned(String groupName, AsyncCallback<GroupMap> cb) {
-    myOwnedGroups().addParameter("g", groupName).get(NativeMap.copyKeysIntoChildren(cb));
-  }
-
-  private static RestApi myOwnedGroups() {
-    return groups().addParameterTrue("owned");
-  }
-
-  private static RestApi groups() {
-    return new RestApi("/groups/");
-  }
-
-  protected GroupMap() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gwt_override.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gwt_override.css
deleted file mode 100644
index c92117c..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gwt_override.css
+++ /dev/null
@@ -1,88 +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.
- */
-
-@external .gwt-Button;
-@external .gwt-DialogBox .dialogMiddleCenter;
-@external .gwt-TabBar;
-@external .gwt-TabBarFirst;
-@external .gwt-TabBarItem;
-@external .gwt-TabBarItem-selected;
-@external .gwt-TabBarRest;
-@external .gwt-TabPanel;
-@external .gwt-TabPanelBottom;
-
-@eval backgroundColor com.google.gerrit.client.Gerrit.getTheme().backgroundColor;
-@eval textColor com.google.gerrit.client.Gerrit.getTheme().textColor;
-@eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
-@eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
-
-body {
-  background: backgroundColor;
-  color: textColor;
-}
-
-.gwt-DialogBox .dialogMiddleCenter {
-  background: backgroundColor;
-  color: textColor;
-}
-
-.gwt-Button {
-  white-space: nowrap;
-}
-
-.gwt-TabBar .gwt-TabBarItem,
-.gwt-TabBar .gwt-TabBarRest,
-.gwt-TabPanelBottom {
-  background: transparent;
-}
-
-.gwt-TabBar {
-  border-bottom: 1px solid black;
-}
-.gwt-TabBar .gwt-TabBarFirst {
-  display: none;
-}
-.gwt-TabBar .gwt-TabBarItem {
-  color: #353535;
-  margin: 0;
-  background: trimColor;
-  padding-top: 0.5em;
-  padding-bottom: 1px;
-  padding-left: 1em;
-  padding-right: 1em;
-  border-bottom: 3px solid transparent;
-  border-right: 0;
-}
-.gwt-TabBar .gwt-TabBarItem-selected {
-  color: #990000;
-  background: selectionColor;
-  border-bottom-color: #990000;
-}
-.gwt-TabBar .gwt-TabBarRest {
-  background: trimColor;
-  padding-top: 0.5em;
-  padding-bottom: 1px;
-}
-.gwt-TabBar .gwt-TabPanelBottom {
-  background: trimColor;
-  border-top: 1px solid black;
-  border-left: none;
-  border-right: none;
-  border-bottom: none;
-  padding: 1px;
-}
-.gwt-TabPanel .gwt-TabPanelBottom {
-  border: none;
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java
deleted file mode 100644
index 4c4c8da..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java
+++ /dev/null
@@ -1,91 +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.client.patches;
-
-import com.google.gwt.i18n.client.Constants;
-
-public interface PatchConstants extends Constants {
-  String patchBase();
-
-  String patchSet();
-
-  String upToChange();
-
-  String openReply();
-
-  String linePrev();
-
-  String lineNext();
-
-  String chunkPrev();
-
-  String chunkNext();
-
-  String commentPrev();
-
-  String commentNext();
-
-  String focusSideA();
-
-  String focusSideB();
-
-  String expandComment();
-
-  String expandAllCommentsOnCurrentLine();
-
-  String toggleSideA();
-
-  String toggleIntraline();
-
-  String showPreferences();
-
-  String toggleReviewed();
-
-  String markAsReviewedAndGoToNext();
-
-  String commentEditorSet();
-
-  String commentInsert();
-
-  String commentSaveDraft();
-
-  String commentCancelEdit();
-
-  String whitespaceIGNORE_NONE();
-
-  String whitespaceIGNORE_TRAILING();
-
-  String whitespaceIGNORE_LEADING_AND_TRAILING();
-
-  String whitespaceIGNORE_ALL();
-
-  String previousFileHelp();
-
-  String nextFileHelp();
-
-  String download();
-
-  String edit();
-
-  String blame();
-
-  String addFileCommentToolTip();
-
-  String cannedReplyDone();
-
-  String sideBySideDiff();
-
-  String unifiedDiff();
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties
deleted file mode 100644
index 13f0afa..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties
+++ /dev/null
@@ -1,44 +0,0 @@
-cannedReplyDone = Done
-
-patchBase = Base
-patchSet = Patch Set
-
-upToChange = Up to change
-openReply = Reply and score
-linePrev = Previous line
-lineNext = Next line
-chunkPrev = Previous diff chunk
-chunkNext = Next diff chunk or search result
-commentPrev = Previous comment
-commentNext = Next comment
-focusSideA = Focus left side
-focusSideB = Focus right side
-expandComment = Expand or collapse comment
-expandAllCommentsOnCurrentLine = Expand or collapse all comments on current line
-toggleSideA = Toggle left side
-toggleIntraline = Toggle intraline difference
-showPreferences = Show diff preferences
-
-toggleReviewed = Toggle the reviewed flag
-markAsReviewedAndGoToNext = Mark patch as reviewed and go to next unreviewed patch
-
-commentEditorSet = Comment Editing
-commentInsert = Create a new inline comment
-commentSaveDraft = Save draft comment
-commentCancelEdit = Cancel comment edit
-
-whitespaceIGNORE_NONE=None
-whitespaceIGNORE_TRAILING=At Line End
-whitespaceIGNORE_LEADING_AND_TRAILING=Leading, At Line End
-whitespaceIGNORE_ALL=All
-
-previousFileHelp = Previous file
-nextFileHelp = Next file
-
-download = Download
-edit = Edit
-blame = Blame
-addFileCommentToolTip = Click to add file comment
-
-sideBySideDiff = Side-by-side diff
-unifiedDiff = Unified diff
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages.java
deleted file mode 100644
index 358ccd3..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages.java
+++ /dev/null
@@ -1,27 +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.client.patches;
-
-import com.google.gwt.i18n.client.Messages;
-
-public interface PatchMessages extends Messages {
-  String expandBefore(int cnt);
-
-  String expandAfter(int cnt);
-
-  String patchSkipRegion(String lineNumber);
-
-  String fileNameWithShortcutKey(String file, String key);
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages.properties
deleted file mode 100644
index 8dcebdc..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages.properties
+++ /dev/null
@@ -1,4 +0,0 @@
-expandBefore = +{0}&#x21e7;
-expandAfter = +{0}&#x21e9;
-patchSkipRegion = ... skipped {0} common lines ...
-fileNameWithShortcutKey = {0} (Shortcut: {1})
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages_en.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages_en.properties
deleted file mode 100644
index 8dcebdc..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages_en.properties
+++ /dev/null
@@ -1,4 +0,0 @@
-expandBefore = +{0}&#x21e7;
-expandAfter = +{0}&#x21e9;
-patchSkipRegion = ... skipped {0} common lines ...
-fileNameWithShortcutKey = {0} (Shortcut: {1})
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchUtil.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchUtil.java
deleted file mode 100644
index d599756..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchUtil.java
+++ /dev/null
@@ -1,22 +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.client.patches;
-
-import com.google.gwt.core.client.GWT;
-
-public class PatchUtil {
-  public static final PatchConstants C = GWT.create(PatchConstants.class);
-  public static final PatchMessages M = GWT.create(PatchMessages.class);
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SkippedLine.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SkippedLine.java
deleted file mode 100644
index 486e7b8..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SkippedLine.java
+++ /dev/null
@@ -1,50 +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.client.patches;
-
-public class SkippedLine {
-
-  private int a;
-  private int b;
-  private int sz;
-
-  public SkippedLine(int startA, int startB, int size) {
-    a = startA;
-    b = startB;
-    sz = size;
-  }
-
-  public int getStartA() {
-    return a;
-  }
-
-  public int getStartB() {
-    return b;
-  }
-
-  public int getSize() {
-    return sz;
-  }
-
-  public void incrementStart(int n) {
-    a += n;
-    b += n;
-    reduceSize(n);
-  }
-
-  public void reduceSize(int n) {
-    sz -= n;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/plugins/PluginInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/plugins/PluginInfo.java
deleted file mode 100644
index ac073de..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/plugins/PluginInfo.java
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.plugins;
-
-import com.google.gwt.core.client.JavaScriptObject;
-
-public class PluginInfo extends JavaScriptObject {
-  public final native String name() /*-{ return this.name }-*/;
-
-  public final native String version() /*-{ return this.version }-*/;
-
-  public final native String indexUrl() /*-{ return this.index_url }-*/;
-
-  public final native boolean disabled() /*-{ return this.disabled || false }-*/;
-
-  protected PluginInfo() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/plugins/PluginMap.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/plugins/PluginMap.java
deleted file mode 100644
index cea27b9..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/plugins/PluginMap.java
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.plugins;
-
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-
-/** Plugins available from {@code /plugins/}. */
-public class PluginMap extends NativeMap<PluginInfo> {
-  public static void all(AsyncCallback<PluginMap> callback) {
-    new RestApi("/plugins/").addParameterTrue("all").get(NativeMap.copyKeysIntoChildren(callback));
-  }
-
-  protected PluginMap() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/BranchInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/BranchInfo.java
deleted file mode 100644
index 097f26a..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/BranchInfo.java
+++ /dev/null
@@ -1,30 +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.client.projects;
-
-import com.google.gerrit.client.info.ActionInfo;
-import com.google.gerrit.client.info.WebLinkInfo;
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gwt.core.client.JsArray;
-
-public class BranchInfo extends RefInfo {
-  public final native boolean canDelete() /*-{ return this['can_delete'] ? true : false; }-*/;
-
-  public final native NativeMap<ActionInfo> actions() /*-{ return this.actions }-*/;
-
-  public final native JsArray<WebLinkInfo> webLinks() /*-{ return this.web_links; }-*/;
-
-  protected BranchInfo() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
deleted file mode 100644
index 684f8e6..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
+++ /dev/null
@@ -1,262 +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.client.projects;
-
-import com.google.gerrit.client.ErrorDialog;
-import com.google.gerrit.client.info.ActionInfo;
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.extensions.client.InheritableBoolean;
-import com.google.gerrit.extensions.client.ProjectState;
-import com.google.gerrit.extensions.client.SubmitType;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.core.client.JsArrayString;
-import com.google.gwtexpui.safehtml.client.FindReplace;
-import com.google.gwtexpui.safehtml.client.LinkFindReplace;
-import com.google.gwtexpui.safehtml.client.RawFindReplace;
-import java.util.ArrayList;
-import java.util.List;
-
-public class ConfigInfo extends JavaScriptObject {
-
-  public final native String description() /*-{ return this.description }-*/;
-
-  public final native InheritedBooleanInfo requireChangeId()
-      /*-{ return this.require_change_id; }-*/ ;
-
-  public final native InheritedBooleanInfo useContentMerge()
-      /*-{ return this.use_content_merge; }-*/ ;
-
-  public final native InheritedBooleanInfo useContributorAgreements()
-      /*-{ return this.use_contributor_agreements; }-*/ ;
-
-  public final native InheritedBooleanInfo createNewChangeForAllNotInTarget()
-      /*-{ return this.create_new_change_for_all_not_in_target; }-*/ ;
-
-  public final native InheritedBooleanInfo useSignedOffBy()
-      /*-{ return this.use_signed_off_by; }-*/ ;
-
-  public final native InheritedBooleanInfo enableSignedPush()
-      /*-{ return this.enable_signed_push; }-*/ ;
-
-  public final native InheritedBooleanInfo requireSignedPush()
-      /*-{ return this.require_signed_push; }-*/ ;
-
-  public final native InheritedBooleanInfo rejectImplicitMerges()
-      /*-{ return this.reject_implicit_merges; }-*/ ;
-
-  public final native InheritedBooleanInfo privateByDefault()
-      /*-{ return this.private_by_default; }-*/ ;
-
-  public final native InheritedBooleanInfo workInProgressByDefault()
-      /*-{ return this.work_in_progress_by_default; }-*/ ;
-
-  public final native InheritedBooleanInfo enableReviewerByEmail()
-      /*-{ return this.enable_reviewer_by_email; }-*/ ;
-
-  public final native InheritedBooleanInfo matchAuthorToCommitterDate()
-      /*-{ return this.match_author_to_committer_date; }-*/ ;
-
-  public final SubmitType submitType() {
-    return SubmitType.valueOf(submitTypeRaw());
-  }
-
-  public final native SubmitTypeInfo defaultSubmitType() /*-{ return this.default_submit_type; }-*/;
-
-  public final native NativeMap<NativeMap<ConfigParameterInfo>> pluginConfig()
-      /*-{ return this.plugin_config || {}; }-*/ ;
-
-  public final native NativeMap<ConfigParameterInfo> pluginConfig(String p)
-      /*-{ return this.plugin_config[p]; }-*/ ;
-
-  public final native NativeMap<ActionInfo> actions() /*-{ return this.actions; }-*/;
-
-  private native String submitTypeRaw() /*-{ return this.submit_type }-*/;
-
-  public final ProjectState state() {
-    if (stateRaw() == null) {
-      return ProjectState.ACTIVE;
-    }
-    return ProjectState.valueOf(stateRaw());
-  }
-
-  private native String stateRaw() /*-{ return this.state }-*/;
-
-  public final native MaxObjectSizeLimitInfo maxObjectSizeLimit()
-      /*-{ return this.max_object_size_limit; }-*/ ;
-
-  private native NativeMap<CommentLinkInfo> commentlinks0() /*-{ return this.commentlinks; }-*/;
-
-  final List<FindReplace> commentlinks() {
-    JsArray<CommentLinkInfo> cls = commentlinks0().values();
-    List<FindReplace> commentLinks = new ArrayList<>(cls.length());
-    for (int i = 0; i < cls.length(); i++) {
-      CommentLinkInfo cl = cls.get(i);
-      if (!cl.enabled()) {
-        continue;
-      }
-      if (cl.link() != null) {
-        commentLinks.add(new LinkFindReplace(cl.match(), cl.link()));
-      } else {
-        try {
-          FindReplace fr = new RawFindReplace(cl.match(), cl.html());
-          commentLinks.add(fr);
-        } catch (RuntimeException e) {
-          int index = e.getMessage().indexOf("at Object");
-          new ErrorDialog(
-                  "Invalid commentlink configuration: "
-                      + (index == -1 ? e.getMessage() : e.getMessage().substring(0, index)))
-              .center();
-        }
-      }
-    }
-    return commentLinks;
-  }
-
-  final native ThemeInfo theme() /*-{ return this.theme; }-*/;
-
-  final native NativeMap<JsArrayString>
-      extensionPanelNames() /*-{ return this.extension_panel_names; }-*/;
-
-  protected ConfigInfo() {}
-
-  static class CommentLinkInfo extends JavaScriptObject {
-    final native String match() /*-{ return this.match; }-*/;
-
-    final native String link() /*-{ return this.link; }-*/;
-
-    final native String html() /*-{ return this.html; }-*/;
-
-    final native boolean enabled() /*-{
-      return !this.hasOwnProperty('enabled') || this.enabled;
-    }-*/;
-
-    protected CommentLinkInfo() {}
-  }
-
-  public static class InheritedBooleanInfo extends JavaScriptObject {
-    public static InheritedBooleanInfo create() {
-      return (InheritedBooleanInfo) createObject();
-    }
-
-    public final native boolean value() /*-{ return this.value ? true : false; }-*/;
-
-    public final native boolean inheritedValue()
-        /*-{ return this.inherited_value ? true : false; }-*/ ;
-
-    public final InheritableBoolean configuredValue() {
-      return InheritableBoolean.valueOf(configuredValueRaw());
-    }
-
-    private native String configuredValueRaw() /*-{ return this.configured_value }-*/;
-
-    public final void setConfiguredValue(InheritableBoolean v) {
-      setConfiguredValueRaw(v.name());
-    }
-
-    public final native void setConfiguredValueRaw(String v)
-        /*-{ if(v)this.configured_value=v; }-*/ ;
-
-    protected InheritedBooleanInfo() {}
-  }
-
-  public static class MaxObjectSizeLimitInfo extends JavaScriptObject {
-    public final native String value() /*-{ return this.value; }-*/;
-
-    public final native String configuredValue() /*-{ return this.configured_value }-*/;
-
-    public final native String summary() /*-{ return this.summary; }-*/;
-
-    protected MaxObjectSizeLimitInfo() {}
-  }
-
-  public static class ConfigParameterInfo extends JavaScriptObject {
-    public final native String name() /*-{ return this.name; }-*/;
-
-    public final native String displayName() /*-{ return this.display_name; }-*/;
-
-    public final native String description() /*-{ return this.description; }-*/;
-
-    public final native String warning() /*-{ return this.warning; }-*/;
-
-    public final native String type() /*-{ return this.type; }-*/;
-
-    public final native String value() /*-{ return this.value; }-*/;
-
-    public final native boolean editable() /*-{ return this.editable ? true : false; }-*/;
-
-    public final native boolean inheritable() /*-{ return this.inheritable ? true : false; }-*/;
-
-    public final native String configuredValue() /*-{ return this.configured_value; }-*/;
-
-    public final native String inheritedValue() /*-{ return this.inherited_value; }-*/;
-
-    public final native JsArrayString permittedValues() /*-{ return this.permitted_values; }-*/;
-
-    public final native JsArrayString values() /*-{ return this.values; }-*/;
-
-    protected ConfigParameterInfo() {}
-  }
-
-  public static class ConfigParameterValue extends JavaScriptObject {
-    final native void init() /*-{ this.values = []; }-*/;
-
-    final native void addValue(String v) /*-{ this.values.push(v); }-*/;
-
-    final native void setValue(String v) /*-{ if(v)this.value = v; }-*/;
-
-    public static ConfigParameterValue create() {
-      ConfigParameterValue v = createObject().cast();
-      return v;
-    }
-
-    public final ConfigParameterValue values(String[] values) {
-      init();
-      for (String v : values) {
-        addValue(v);
-      }
-      return this;
-    }
-
-    public final ConfigParameterValue value(String v) {
-      setValue(v);
-      return this;
-    }
-
-    protected ConfigParameterValue() {}
-  }
-
-  public static class SubmitTypeInfo extends JavaScriptObject {
-    public final SubmitType value() {
-      return SubmitType.valueOf(valueRaw());
-    }
-
-    public final SubmitType configuredValue() {
-      return SubmitType.valueOf(configuredValueRaw());
-    }
-
-    public final SubmitType inheritedValue() {
-      return SubmitType.valueOf(inheritedValueRaw());
-    }
-
-    private final native String valueRaw() /*-{ return this.value; }-*/;
-
-    private final native String configuredValueRaw() /*-{ return this.configured_value; }-*/;
-
-    private final native String inheritedValueRaw() /*-{ return this.inherited_value; }-*/;
-
-    protected SubmitTypeInfo() {}
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfoCache.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfoCache.java
deleted file mode 100644
index 7262b3a..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfoCache.java
+++ /dev/null
@@ -1,141 +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.client.projects;
-
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.ui.CommentLinkProcessor;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
-/** Cache of {@link ConfigInfo} objects by project name. */
-public class ConfigInfoCache {
-  private static final int PROJECT_LIMIT = 25;
-  private static final int CHANGE_LIMIT = 100;
-  private static final ConfigInfoCache instance = GWT.create(ConfigInfoCache.class);
-
-  public static class Entry {
-    private final ConfigInfo info;
-    private CommentLinkProcessor commentLinkProcessor;
-
-    private Entry(ConfigInfo info) {
-      this.info = info;
-    }
-
-    public CommentLinkProcessor getCommentLinkProcessor() {
-      if (commentLinkProcessor == null) {
-        commentLinkProcessor = new CommentLinkProcessor(info.commentlinks());
-      }
-      return commentLinkProcessor;
-    }
-
-    public ThemeInfo getTheme() {
-      return info.theme();
-    }
-
-    public List<String> getExtensionPanelNames(String extensionPoint) {
-      return Natives.asList(info.extensionPanelNames().get(extensionPoint));
-    }
-  }
-
-  public static void get(Project.NameKey name, AsyncCallback<Entry> cb) {
-    instance.getImpl(name.get(), cb);
-  }
-
-  public static void get(Change.Id changeId, AsyncCallback<Entry> cb) {
-    instance.getImpl(changeId.get(), cb);
-  }
-
-  public static void add(ChangeInfo info) {
-    instance.changeToProject.put(info.legacyId().get(), info.project());
-  }
-
-  private final LinkedHashMap<String, Entry> cache;
-  private final LinkedHashMap<Integer, String> changeToProject;
-
-  protected ConfigInfoCache() {
-    cache =
-        new LinkedHashMap<String, Entry>(PROJECT_LIMIT) {
-          private static final long serialVersionUID = 1L;
-
-          @Override
-          protected boolean removeEldestEntry(Map.Entry<String, ConfigInfoCache.Entry> e) {
-            return size() > PROJECT_LIMIT;
-          }
-        };
-
-    changeToProject =
-        new LinkedHashMap<Integer, String>(CHANGE_LIMIT) {
-          private static final long serialVersionUID = 1L;
-
-          @Override
-          protected boolean removeEldestEntry(Map.Entry<Integer, String> e) {
-            return size() > CHANGE_LIMIT;
-          }
-        };
-  }
-
-  private void getImpl(String name, AsyncCallback<Entry> cb) {
-    Entry e = cache.get(name);
-    if (e != null) {
-      cb.onSuccess(e);
-      return;
-    }
-    ProjectApi.getConfig(
-        new Project.NameKey(name),
-        new AsyncCallback<ConfigInfo>() {
-          @Override
-          public void onSuccess(ConfigInfo result) {
-            Entry e = new Entry(result);
-            cache.put(name, e);
-            cb.onSuccess(e);
-          }
-
-          @Override
-          public void onFailure(Throwable caught) {
-            cb.onFailure(caught);
-          }
-        });
-  }
-
-  private void getImpl(Integer id, AsyncCallback<Entry> cb) {
-    String name = changeToProject.get(id);
-    if (name != null) {
-      getImpl(name, cb);
-      return;
-    }
-    // TODO(hiesel) Make a preflight request to get project before we deprecate the numeric changeId
-    ChangeApi.change(null, id)
-        .get(
-            new AsyncCallback<ChangeInfo>() {
-              @Override
-              public void onSuccess(ChangeInfo result) {
-                changeToProject.put(id, result.project());
-                getImpl(result.project(), cb);
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {
-                cb.onFailure(caught);
-              }
-            });
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
deleted file mode 100644
index 3be52e6..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
+++ /dev/null
@@ -1,461 +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.client.projects;
-
-import com.google.gerrit.client.VoidResult;
-import com.google.gerrit.client.projects.ConfigInfo.ConfigParameterValue;
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.NativeString;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.extensions.client.InheritableBoolean;
-import com.google.gerrit.extensions.client.ProjectState;
-import com.google.gerrit.extensions.client.SubmitType;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-
-public class ProjectApi {
-  /** Create a new project */
-  public static void createProject(
-      String projectName,
-      String parent,
-      Boolean createEmptyCcommit,
-      Boolean permissionsOnly,
-      AsyncCallback<VoidResult> cb) {
-    ProjectInput input = ProjectInput.create();
-    input.setName(projectName);
-    input.setParent(parent);
-    input.setPermissionsOnly(permissionsOnly);
-    input.setCreateEmptyCommit(createEmptyCcommit);
-    new RestApi("/projects/").id(projectName).ifNoneMatch().put(input, cb);
-  }
-
-  private static RestApi getRestApi(
-      Project.NameKey name, String viewName, int limit, int start, String match) {
-    RestApi call = project(name).view(viewName);
-    call.addParameter("n", limit);
-    call.addParameter("S", start);
-    if (match != null) {
-      if (match.startsWith("^")) {
-        call.addParameter("r", match);
-      } else {
-        call.addParameter("m", match);
-      }
-    }
-    return call;
-  }
-
-  /** Create a new tag */
-  public static void createTag(
-      Project.NameKey name,
-      String ref,
-      String revision,
-      String annotation,
-      AsyncCallback<TagInfo> cb) {
-    TagInput input = TagInput.create();
-    input.setRevision(revision);
-    input.setMessage(annotation);
-    project(name).view("tags").id(ref).ifNoneMatch().put(input, cb);
-  }
-
-  /** Retrieve all visible tags of the project */
-  public static void getTags(Project.NameKey name, AsyncCallback<JsArray<TagInfo>> cb) {
-    project(name).view("tags").get(cb);
-  }
-
-  public static void getTags(
-      Project.NameKey name,
-      int limit,
-      int start,
-      String match,
-      AsyncCallback<JsArray<TagInfo>> cb) {
-    getRestApi(name, "tags", limit, start, match).get(cb);
-  }
-
-  /** Delete tags. One call is fired to the server to delete all the tags. */
-  public static void deleteTags(
-      Project.NameKey name, Set<String> refs, AsyncCallback<VoidResult> cb) {
-    if (refs.size() == 1) {
-      project(name).view("tags").id(refs.iterator().next()).delete(cb);
-    } else {
-      DeleteTagsInput d = DeleteTagsInput.create();
-      for (String ref : refs) {
-        d.addTag(ref);
-      }
-      project(name).view("tags:delete").post(d, cb);
-    }
-  }
-
-  /** Create a new branch */
-  public static void createBranch(
-      Project.NameKey name, String ref, String revision, AsyncCallback<BranchInfo> cb) {
-    BranchInput input = BranchInput.create();
-    input.setRevision(revision);
-    project(name).view("branches").id(ref).ifNoneMatch().put(input, cb);
-  }
-
-  /** Retrieve all visible branches of the project */
-  public static void getBranches(Project.NameKey name, AsyncCallback<JsArray<BranchInfo>> cb) {
-    project(name).view("branches").get(cb);
-  }
-
-  public static void getBranches(
-      Project.NameKey name,
-      int limit,
-      int start,
-      String match,
-      AsyncCallback<JsArray<BranchInfo>> cb) {
-    getRestApi(name, "branches", limit, start, match).get(cb);
-  }
-
-  /** Delete branches. One call is fired to the server to delete all the branches. */
-  public static void deleteBranches(
-      Project.NameKey name, Set<String> refs, AsyncCallback<VoidResult> cb) {
-    if (refs.size() == 1) {
-      project(name).view("branches").id(refs.iterator().next()).delete(cb);
-    } else {
-      DeleteBranchesInput d = DeleteBranchesInput.create();
-      for (String ref : refs) {
-        d.addBranch(ref);
-      }
-      project(name).view("branches:delete").post(d, cb);
-    }
-  }
-
-  public static void getConfig(Project.NameKey name, AsyncCallback<ConfigInfo> cb) {
-    project(name).view("config").get(cb);
-  }
-
-  public static void setConfig(
-      Project.NameKey name,
-      String description,
-      InheritableBoolean useContributorAgreements,
-      InheritableBoolean useContentMerge,
-      InheritableBoolean useSignedOffBy,
-      InheritableBoolean createNewChangeForAllNotInTarget,
-      InheritableBoolean requireChangeId,
-      InheritableBoolean enableSignedPush,
-      InheritableBoolean requireSignedPush,
-      InheritableBoolean rejectImplicitMerges,
-      InheritableBoolean privateByDefault,
-      InheritableBoolean workInProgressByDefault,
-      InheritableBoolean enableReviewerByEmail,
-      InheritableBoolean matchAuthorToCommitterDate,
-      String maxObjectSizeLimit,
-      SubmitType submitType,
-      ProjectState state,
-      Map<String, Map<String, ConfigParameterValue>> pluginConfigValues,
-      AsyncCallback<ConfigInfo> cb) {
-    ConfigInput in = ConfigInput.create();
-    in.setDescription(description);
-    in.setUseContributorAgreements(useContributorAgreements);
-    in.setUseContentMerge(useContentMerge);
-    in.setUseSignedOffBy(useSignedOffBy);
-    in.setRequireChangeId(requireChangeId);
-    in.setCreateNewChangeForAllNotInTarget(createNewChangeForAllNotInTarget);
-    if (enableSignedPush != null) {
-      in.setEnableSignedPush(enableSignedPush);
-    }
-    if (requireSignedPush != null) {
-      in.setRequireSignedPush(requireSignedPush);
-    }
-    in.setRejectImplicitMerges(rejectImplicitMerges);
-    in.setPrivateByDefault(privateByDefault);
-    in.setWorkInProgressByDefault(workInProgressByDefault);
-    in.setMaxObjectSizeLimit(maxObjectSizeLimit);
-    if (submitType != null) {
-      in.setSubmitType(submitType);
-    }
-    in.setState(state);
-    in.setPluginConfigValues(pluginConfigValues);
-    in.setEnableReviewerByEmail(enableReviewerByEmail);
-    in.setMatchAuthorToCommitterDate(matchAuthorToCommitterDate);
-
-    project(name).view("config").put(in, cb);
-  }
-
-  public static void getParent(Project.NameKey name, AsyncCallback<Project.NameKey> cb) {
-    project(name)
-        .view("parent")
-        .get(
-            new AsyncCallback<NativeString>() {
-              @Override
-              public void onSuccess(NativeString result) {
-                cb.onSuccess(new Project.NameKey(result.asString()));
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {
-                cb.onFailure(caught);
-              }
-            });
-  }
-
-  public static void getChildren(
-      Project.NameKey name, boolean recursive, AsyncCallback<JsArray<ProjectInfo>> cb) {
-    RestApi view = project(name).view("children");
-    if (recursive) {
-      view.addParameterTrue("recursive");
-    }
-    view.get(cb);
-  }
-
-  public static void getDescription(Project.NameKey name, AsyncCallback<NativeString> cb) {
-    project(name).view("description").get(cb);
-  }
-
-  public static void setDescription(
-      Project.NameKey name, String description, AsyncCallback<NativeString> cb) {
-    RestApi call = project(name).view("description");
-    if (description != null && !description.isEmpty()) {
-      DescriptionInput input = DescriptionInput.create();
-      input.setDescription(description);
-      call.put(input, cb);
-    } else {
-      call.delete(cb);
-    }
-  }
-
-  public static void setHead(Project.NameKey name, String ref, AsyncCallback<NativeString> cb) {
-    RestApi call = project(name).view("HEAD");
-    HeadInput input = HeadInput.create();
-    input.setRef(ref);
-    call.put(input, cb);
-  }
-
-  public static RestApi project(Project.NameKey name) {
-    return new RestApi("/projects/").id(name.get());
-  }
-
-  private static class ProjectInput extends JavaScriptObject {
-    static ProjectInput create() {
-      return (ProjectInput) createObject();
-    }
-
-    protected ProjectInput() {}
-
-    final native void setName(String n) /*-{ if(n)this.name=n; }-*/;
-
-    final native void setParent(String p) /*-{ if(p)this.parent=p; }-*/;
-
-    final native void setPermissionsOnly(boolean po) /*-{ if(po)this.permissions_only=po; }-*/;
-
-    final native void setCreateEmptyCommit(boolean cc) /*-{ if(cc)this.create_empty_commit=cc; }-*/;
-  }
-
-  private static class ConfigInput extends JavaScriptObject {
-    static ConfigInput create() {
-      return (ConfigInput) createObject();
-    }
-
-    protected ConfigInput() {}
-
-    final native void setDescription(String d) /*-{ if(d)this.description=d; }-*/;
-
-    final void setUseContributorAgreements(InheritableBoolean v) {
-      setUseContributorAgreementsRaw(v.name());
-    }
-
-    private native void setUseContributorAgreementsRaw(String v)
-        /*-{ if(v)this.use_contributor_agreements=v; }-*/ ;
-
-    final void setUseContentMerge(InheritableBoolean v) {
-      setUseContentMergeRaw(v.name());
-    }
-
-    private native void setUseContentMergeRaw(String v) /*-{ if(v)this.use_content_merge=v; }-*/;
-
-    final void setUseSignedOffBy(InheritableBoolean v) {
-      setUseSignedOffByRaw(v.name());
-    }
-
-    private native void setUseSignedOffByRaw(String v) /*-{ if(v)this.use_signed_off_by=v; }-*/;
-
-    final void setRequireChangeId(InheritableBoolean v) {
-      setRequireChangeIdRaw(v.name());
-    }
-
-    private native void setRequireChangeIdRaw(String v) /*-{ if(v)this.require_change_id=v; }-*/;
-
-    final void setCreateNewChangeForAllNotInTarget(InheritableBoolean v) {
-      setCreateNewChangeForAllNotInTargetRaw(v.name());
-    }
-
-    private native void setCreateNewChangeForAllNotInTargetRaw(String v)
-        /*-{ if(v)this.create_new_change_for_all_not_in_target=v; }-*/ ;
-
-    final void setEnableSignedPush(InheritableBoolean v) {
-      setEnableSignedPushRaw(v.name());
-    }
-
-    private native void setEnableSignedPushRaw(String v) /*-{ if(v)this.enable_signed_push=v; }-*/;
-
-    final void setRequireSignedPush(InheritableBoolean v) {
-      setRequireSignedPushRaw(v.name());
-    }
-
-    final void setPrivateByDefault(InheritableBoolean v) {
-      setPrivateByDefault(v.name());
-    }
-
-    private native void setPrivateByDefault(String v) /*-{ if(v)this.private_by_default=v; }-*/;
-
-    final void setWorkInProgressByDefault(InheritableBoolean v) {
-      setWorkInProgressByDefault(v.name());
-    }
-
-    private native void setWorkInProgressByDefault(
-        String v) /*-{ if(v)this.work_in_progress_by_default=v; }-*/;
-
-    final void setEnableReviewerByEmail(InheritableBoolean v) {
-      setEnableReviewerByEmailRaw(v.name());
-    }
-
-    final void setMatchAuthorToCommitterDate(InheritableBoolean v) {
-      setMatchAuthorToCommitterDateRaw(v.name());
-    }
-
-    private native void setMatchAuthorToCommitterDateRaw(String v)
-        /*-{ if(v)this.match_author_to_committer_date=v; }-*/ ;
-
-    private native void setEnableReviewerByEmailRaw(String v)
-        /*-{ if(v)this.enable_reviewer_by_email=v; }-*/ ;
-
-    private native void setRequireSignedPushRaw(String v)
-        /*-{ if(v)this.require_signed_push=v; }-*/ ;
-
-    final void setRejectImplicitMerges(InheritableBoolean v) {
-      setRejectImplicitMergesRaw(v.name());
-    }
-
-    private native void setRejectImplicitMergesRaw(String v)
-        /*-{ if(v)this.reject_implicit_merges=v; }-*/ ;
-
-    final native void setMaxObjectSizeLimit(String l) /*-{ if(l)this.max_object_size_limit=l; }-*/;
-
-    final void setSubmitType(SubmitType t) {
-      setSubmitTypeRaw(t.name());
-    }
-
-    private native void setSubmitTypeRaw(String t) /*-{ if(t)this.submit_type=t; }-*/;
-
-    final void setState(ProjectState s) {
-      setStateRaw(s.name());
-    }
-
-    private native void setStateRaw(String s) /*-{ if(s)this.state=s; }-*/;
-
-    final void setPluginConfigValues(
-        Map<String, Map<String, ConfigParameterValue>> pluginConfigValues) {
-      if (!pluginConfigValues.isEmpty()) {
-        NativeMap<ConfigParameterValueMap> configValues = NativeMap.create().cast();
-        for (Entry<String, Map<String, ConfigParameterValue>> e : pluginConfigValues.entrySet()) {
-          ConfigParameterValueMap values = ConfigParameterValueMap.create();
-          configValues.put(e.getKey(), values);
-          for (Entry<String, ConfigParameterValue> e2 : e.getValue().entrySet()) {
-            values.put(e2.getKey(), e2.getValue());
-          }
-        }
-        setPluginConfigValuesRaw(configValues);
-      }
-    }
-
-    private native void setPluginConfigValuesRaw(NativeMap<ConfigParameterValueMap> v)
-        /*-{ this.plugin_config_values=v; }-*/ ;
-  }
-
-  private static class ConfigParameterValueMap extends JavaScriptObject {
-    static ConfigParameterValueMap create() {
-      return createObject().cast();
-    }
-
-    protected ConfigParameterValueMap() {}
-
-    public final native void put(String n, ConfigParameterValue v) /*-{ this[n] = v; }-*/;
-  }
-
-  private static class TagInput extends JavaScriptObject {
-    static TagInput create() {
-      return (TagInput) createObject();
-    }
-
-    protected TagInput() {}
-
-    final native void setRevision(String r) /*-{ if(r)this.revision=r; }-*/;
-
-    final native void setMessage(String m) /*-{ if(m)this.message=m; }-*/;
-  }
-
-  private static class BranchInput extends JavaScriptObject {
-    static BranchInput create() {
-      return (BranchInput) createObject();
-    }
-
-    protected BranchInput() {}
-
-    final native void setRevision(String r) /*-{ if(r)this.revision=r; }-*/;
-  }
-
-  private static class DescriptionInput extends JavaScriptObject {
-    static DescriptionInput create() {
-      return (DescriptionInput) createObject();
-    }
-
-    protected DescriptionInput() {}
-
-    final native void setDescription(String d) /*-{ if(d)this.description=d; }-*/;
-  }
-
-  private static class HeadInput extends JavaScriptObject {
-    static HeadInput create() {
-      return createObject().cast();
-    }
-
-    protected HeadInput() {}
-
-    final native void setRef(String r) /*-{ if(r)this.ref=r; }-*/;
-  }
-
-  private static class DeleteTagsInput extends JavaScriptObject {
-    static DeleteTagsInput create() {
-      DeleteTagsInput d = createObject().cast();
-      d.init();
-      return d;
-    }
-
-    protected DeleteTagsInput() {}
-
-    final native void init() /*-{ this.tags = []; }-*/;
-
-    final native void addTag(String b) /*-{ this.tags.push(b); }-*/;
-  }
-
-  private static class DeleteBranchesInput extends JavaScriptObject {
-    static DeleteBranchesInput create() {
-      DeleteBranchesInput d = createObject().cast();
-      d.init();
-      return d;
-    }
-
-    protected DeleteBranchesInput() {}
-
-    final native void init() /*-{ this.branches = []; }-*/;
-
-    final native void addBranch(String b) /*-{ this.branches.push(b); }-*/;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectInfo.java
deleted file mode 100644
index 1ff568f..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectInfo.java
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.projects;
-
-import com.google.gerrit.client.info.WebLinkInfo;
-import com.google.gerrit.extensions.client.ProjectState;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.user.client.ui.SuggestOracle;
-
-public class ProjectInfo extends JavaScriptObject implements SuggestOracle.Suggestion {
-  public final Project.NameKey name_key() {
-    return new Project.NameKey(name());
-  }
-
-  public final native String name() /*-{ return this.name; }-*/;
-
-  public final native String description() /*-{ return this.description; }-*/;
-
-  public final native JsArray<WebLinkInfo> webLinks() /*-{ return this.web_links; }-*/;
-
-  public final ProjectState state() {
-    return ProjectState.valueOf(getStringState());
-  }
-
-  private native String getStringState() /*-{ return this.state; }-*/;
-
-  @Override
-  public final String getDisplayString() {
-    if (description() != null) {
-      return name() + " (" + description() + ")";
-    }
-    return name();
-  }
-
-  @Override
-  public final String getReplacementString() {
-    return name();
-  }
-
-  protected ProjectInfo() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectMap.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectMap.java
deleted file mode 100644
index 5ff300d..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectMap.java
+++ /dev/null
@@ -1,82 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.projects;
-
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-
-/** Projects available from {@code /projects/}. */
-public class ProjectMap extends NativeMap<ProjectInfo> {
-  public static void all(AsyncCallback<ProjectMap> callback) {
-    new RestApi("/projects/")
-        .addParameterRaw("type", "ALL")
-        .addParameterTrue("all")
-        .addParameterTrue("d") // description
-        .get(NativeMap.copyKeysIntoChildren(callback));
-  }
-
-  public static void permissions(AsyncCallback<ProjectMap> callback) {
-    new RestApi("/projects/")
-        .addParameterRaw("type", "PERMISSIONS")
-        .addParameterTrue("all")
-        .addParameterTrue("d") // description
-        .get(NativeMap.copyKeysIntoChildren(callback));
-  }
-
-  public static void parentCandidates(AsyncCallback<ProjectMap> callback) {
-    new RestApi("/projects/")
-        .addParameterRaw("type", "PARENT_CANDIDATES")
-        .addParameterTrue("all")
-        .addParameterTrue("d") // description
-        .get(NativeMap.copyKeysIntoChildren(callback));
-  }
-
-  public static void suggest(String match, int limit, AsyncCallback<ProjectMap> cb) {
-    new RestApi("/projects/")
-        .addParameter("m", match)
-        .addParameter("n", limit)
-        .addParameterRaw("type", "ALL")
-        .addParameterTrue("d") // description
-        .background()
-        .get(NativeMap.copyKeysIntoChildren(cb));
-  }
-
-  public static void match(String match, int limit, int start, AsyncCallback<ProjectMap> cb) {
-    RestApi call = new RestApi("/projects/");
-    if (match != null) {
-      if (match.startsWith("^")) {
-        call.addParameter("r", match);
-      } else {
-        call.addParameter("m", match);
-      }
-    }
-    if (limit > 0) {
-      call.addParameter("n", limit);
-    }
-    if (start > 0) {
-      call.addParameter("S", start);
-    }
-    call.addParameterRaw("type", "ALL");
-    call.addParameterTrue("d"); // description
-    call.get(NativeMap.copyKeysIntoChildren(cb));
-  }
-
-  public static void match(String match, AsyncCallback<ProjectMap> cb) {
-    match(match, 0, 0, cb);
-  }
-
-  protected ProjectMap() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/RefInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/RefInfo.java
deleted file mode 100644
index 90c862f..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/RefInfo.java
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.projects;
-
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gwt.core.client.JavaScriptObject;
-
-public class RefInfo extends JavaScriptObject {
-  public final String getShortName() {
-    return RefNames.shortName(ref());
-  }
-
-  public final native String ref() /*-{ return this.ref; }-*/;
-
-  public final native String revision() /*-{ return this.revision; }-*/;
-
-  protected RefInfo() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/TagInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/TagInfo.java
deleted file mode 100644
index fc13fe1..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/TagInfo.java
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.projects;
-
-import com.google.gerrit.client.info.WebLinkInfo;
-import com.google.gwt.core.client.JsArray;
-
-public class TagInfo extends RefInfo {
-  public final native boolean canDelete() /*-{ return this['can_delete'] ? true : false; }-*/;
-
-  public final native JsArray<WebLinkInfo> webLinks() /*-{ return this.web_links; }-*/;
-
-  // TODO(dpursehouse) add extra tag-related fields (message, tagger, etc)
-  protected TagInfo() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ThemeInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ThemeInfo.java
deleted file mode 100644
index 7584e14..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ThemeInfo.java
+++ /dev/null
@@ -1,27 +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.client.projects;
-
-import com.google.gwt.core.client.JavaScriptObject;
-
-public class ThemeInfo extends JavaScriptObject {
-  public final native String css() /*-{ return this.css; }-*/;
-
-  public final native String header() /*-{ return this.header; }-*/;
-
-  public final native String footer() /*-{ return this.footer; }-*/;
-
-  protected ThemeInfo() {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/CallbackGroup.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/CallbackGroup.java
deleted file mode 100644
index af32d01..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/CallbackGroup.java
+++ /dev/null
@@ -1,260 +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.client.rpc;
-
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-/**
- * Class for grouping together callbacks and calling them in order.
- *
- * <p>Callbacks are added to the group with {@link #add(AsyncCallback)}, which returns a wrapped
- * callback suitable for passing to an asynchronous RPC call. The last callback must be added using
- * {@link #addFinal(AsyncCallback)} or {@link #done()} must be invoked.
- *
- * <p>The enclosing group buffers returned results and ensures that {@code onSuccess} is called
- * exactly once for each callback in the group, in the same order that callbacks were added. This
- * allows callers to, for example, use a {@link ScreenLoadCallback} as the last callback in the list
- * and only display the screen once all callbacks have succeeded.
- *
- * <p>In the event of a failure, the <em>first</em> caught exception is sent to <em>all</em>
- * callbacks' {@code onFailure} methods, in order; subsequent successes or failures are all ignored.
- * Note that this means {@code onFailure} may be called with an exception unrelated to the callback
- * processing it.
- */
-public class CallbackGroup {
-  private final List<CallbackGlue> callbacks;
-  private final Set<CallbackGlue> remaining;
-  private boolean finalAdded;
-
-  private boolean failed;
-  private Throwable failedThrowable;
-
-  public static <T> Callback<T> emptyCallback() {
-    return new Callback<T>() {
-      @Override
-      public void onSuccess(T result) {}
-
-      @Override
-      public void onFailure(Throwable err) {}
-    };
-  }
-
-  public CallbackGroup() {
-    callbacks = new ArrayList<>();
-    remaining = new HashSet<>();
-  }
-
-  public <T> Callback<T> addEmpty() {
-    Callback<T> cb = emptyCallback();
-    return add(cb);
-  }
-
-  public <T> Callback<T> add(AsyncCallback<T> cb) {
-    checkFinalAdded();
-    return handleAdd(cb);
-  }
-
-  public <T> HttpCallback<T> add(HttpCallback<T> cb) {
-    checkFinalAdded();
-    return handleAdd(cb);
-  }
-
-  public <T> Callback<T> addFinal(AsyncCallback<T> cb) {
-    checkFinalAdded();
-    finalAdded = true;
-    return handleAdd(cb);
-  }
-
-  public <T> HttpCallback<T> addFinal(HttpCallback<T> cb) {
-    checkFinalAdded();
-    finalAdded = true;
-    return handleAdd(cb);
-  }
-
-  public void done() {
-    finalAdded = true;
-    apply();
-  }
-
-  public void addListener(AsyncCallback<Void> cb) {
-    if (!failed && finalAdded && remaining.isEmpty()) {
-      cb.onSuccess(null);
-    } else {
-      handleAdd(cb).onSuccess(null);
-    }
-  }
-
-  public void addListener(CallbackGroup group) {
-    addListener(group.<Void>addEmpty());
-  }
-
-  private void success(CallbackGlue cb) {
-    remaining.remove(cb);
-    apply();
-  }
-
-  private <T> void failure(CallbackGlue w, Throwable caught) {
-    if (!failed) {
-      failed = true;
-      failedThrowable = caught;
-    }
-    remaining.remove(w);
-    apply();
-  }
-
-  private void apply() {
-    if (finalAdded && remaining.isEmpty()) {
-      if (failed) {
-        for (CallbackGlue cb : callbacks) {
-          cb.applyFailed();
-        }
-      } else {
-        for (CallbackGlue cb : callbacks) {
-          cb.applySuccess();
-        }
-      }
-      callbacks.clear();
-    }
-  }
-
-  private <T> Callback<T> handleAdd(AsyncCallback<T> cb) {
-    if (failed) {
-      cb.onFailure(failedThrowable);
-      return emptyCallback();
-    }
-
-    CallbackImpl<T> wrapper = new CallbackImpl<>(cb);
-    callbacks.add(wrapper);
-    remaining.add(wrapper);
-    return wrapper;
-  }
-
-  private <T> HttpCallback<T> handleAdd(HttpCallback<T> cb) {
-    if (failed) {
-      cb.onFailure(failedThrowable);
-      return new HttpCallback<T>() {
-        @Override
-        public void onSuccess(HttpResponse<T> result) {}
-
-        @Override
-        public void onFailure(Throwable caught) {}
-      };
-    }
-
-    HttpCallbackImpl<T> w = new HttpCallbackImpl<>(cb);
-    callbacks.add(w);
-    remaining.add(w);
-    return w;
-  }
-
-  private void checkFinalAdded() {
-    if (finalAdded) {
-      throw new IllegalStateException("final callback already added");
-    }
-  }
-
-  public interface Callback<T>
-      extends AsyncCallback<T>, com.google.gwtjsonrpc.common.AsyncCallback<T> {}
-
-  private interface CallbackGlue {
-    void applySuccess();
-
-    void applyFailed();
-  }
-
-  private class CallbackImpl<T> implements Callback<T>, CallbackGlue {
-    AsyncCallback<T> delegate;
-    T result;
-
-    CallbackImpl(AsyncCallback<T> delegate) {
-      this.delegate = delegate;
-    }
-
-    @Override
-    public void onSuccess(T value) {
-      this.result = value;
-      success(this);
-    }
-
-    @Override
-    public void onFailure(Throwable caught) {
-      failure(this, caught);
-    }
-
-    @Override
-    public void applySuccess() {
-      AsyncCallback<T> cb = delegate;
-      if (cb != null) {
-        delegate = null;
-        cb.onSuccess(result);
-        result = null;
-      }
-    }
-
-    @Override
-    public void applyFailed() {
-      AsyncCallback<T> cb = delegate;
-      if (cb != null) {
-        delegate = null;
-        result = null;
-        cb.onFailure(failedThrowable);
-      }
-    }
-  }
-
-  private class HttpCallbackImpl<T> implements HttpCallback<T>, CallbackGlue {
-    private HttpCallback<T> delegate;
-    private HttpResponse<T> result;
-
-    HttpCallbackImpl(HttpCallback<T> delegate) {
-      this.delegate = delegate;
-    }
-
-    @Override
-    public void onSuccess(HttpResponse<T> result) {
-      this.result = result;
-      success(this);
-    }
-
-    @Override
-    public void onFailure(Throwable caught) {
-      failure(this, caught);
-    }
-
-    @Override
-    public void applySuccess() {
-      HttpCallback<T> cb = delegate;
-      if (cb != null) {
-        delegate = null;
-        cb.onSuccess(result);
-        result = null;
-      }
-    }
-
-    @Override
-    public void applyFailed() {
-      HttpCallback<T> cb = delegate;
-      if (cb != null) {
-        delegate = null;
-        result = null;
-        cb.onFailure(failedThrowable);
-      }
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java
deleted file mode 100644
index 2d6723a..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java
+++ /dev/null
@@ -1,111 +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.client.rpc;
-
-import com.google.gerrit.client.ErrorDialog;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.NotSignedInDialog;
-import com.google.gerrit.common.errors.NameAlreadyUsedException;
-import com.google.gerrit.common.errors.NoSuchAccountException;
-import com.google.gerrit.common.errors.NoSuchEntityException;
-import com.google.gerrit.common.errors.NoSuchGroupException;
-import com.google.gerrit.common.errors.NotSignedInException;
-import com.google.gwt.user.client.rpc.InvocationException;
-import com.google.gwtjsonrpc.client.RemoteJsonException;
-import com.google.gwtjsonrpc.client.ServerUnavailableException;
-import com.google.gwtjsonrpc.common.JsonConstants;
-
-/** Abstract callback handling generic error conditions automatically */
-public abstract class GerritCallback<T>
-    implements com.google.gwtjsonrpc.common.AsyncCallback<T>,
-        com.google.gwt.user.client.rpc.AsyncCallback<T> {
-  @Override
-  public void onFailure(Throwable caught) {
-    showFailure(caught);
-  }
-
-  public static void showFailure(Throwable caught) {
-    if (isSigninFailure(caught)) {
-      new NotSignedInDialog().center();
-    } else if (isNoSuchEntity(caught)) {
-      new ErrorDialog(Gerrit.C.notFoundBody()).center();
-    } else if (isNoSuchAccount(caught)) {
-      final String msg = caught.getMessage();
-      final String who = msg.substring(NoSuchAccountException.MESSAGE.length());
-      final ErrorDialog d = new ErrorDialog(Gerrit.M.noSuchAccountMessage(who));
-      d.setText(Gerrit.C.noSuchAccountTitle());
-      d.center();
-
-    } else if (isNameAlreadyUsed(caught)) {
-      final String msg = caught.getMessage();
-      final String alreadyUsedName = msg.substring(NameAlreadyUsedException.MESSAGE.length());
-      new ErrorDialog(Gerrit.M.nameAlreadyUsedBody(alreadyUsedName)).center();
-
-    } else if (isNoSuchGroup(caught)) {
-      final String msg = caught.getMessage();
-      final String group = msg.substring(NoSuchGroupException.MESSAGE.length());
-      final ErrorDialog d = new ErrorDialog(Gerrit.M.noSuchGroupMessage(group));
-      d.setText(Gerrit.C.noSuchGroupTitle());
-      d.center();
-
-    } else if (caught instanceof ServerUnavailableException) {
-      new ErrorDialog(RpcConstants.C.errorServerUnavailable()).center();
-
-    } else {
-      new ErrorDialog(caught).center();
-    }
-  }
-
-  public static boolean isSigninFailure(Throwable caught) {
-    if (isNotSignedIn(caught)
-        || isInvalidXSRF(caught)
-        || (isNoSuchEntity(caught) && !Gerrit.isSignedIn())) {
-      return true;
-    }
-    return false;
-  }
-
-  protected static boolean isInvalidXSRF(Throwable caught) {
-    return caught instanceof InvocationException
-        && caught.getMessage().equals(JsonConstants.ERROR_INVALID_XSRF);
-  }
-
-  protected static boolean isNotSignedIn(Throwable caught) {
-    return RestApi.isNotSignedIn(caught)
-        || (caught instanceof RemoteJsonException
-            && caught.getMessage().equals(NotSignedInException.MESSAGE));
-  }
-
-  protected static boolean isNoSuchEntity(Throwable caught) {
-    return RestApi.isNotFound(caught)
-        || (caught instanceof RemoteJsonException
-            && caught.getMessage().equals(NoSuchEntityException.MESSAGE));
-  }
-
-  protected static boolean isNoSuchAccount(Throwable caught) {
-    return caught instanceof RemoteJsonException
-        && caught.getMessage().startsWith(NoSuchAccountException.MESSAGE);
-  }
-
-  protected static boolean isNameAlreadyUsed(Throwable caught) {
-    return caught instanceof RemoteJsonException
-        && caught.getMessage().startsWith(NameAlreadyUsedException.MESSAGE);
-  }
-
-  protected static boolean isNoSuchGroup(Throwable caught) {
-    return caught instanceof RemoteJsonException
-        && caught.getMessage().startsWith(NoSuchGroupException.MESSAGE);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/HttpCallback.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/HttpCallback.java
deleted file mode 100644
index 2de2980..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/HttpCallback.java
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.rpc;
-
-/** AsyncCallback supplied with HTTP response headers. */
-public interface HttpCallback<T> {
-  void onSuccess(HttpResponse<T> result);
-
-  void onFailure(Throwable caught);
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/HttpResponse.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/HttpResponse.java
deleted file mode 100644
index 22d62fb..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/HttpResponse.java
+++ /dev/null
@@ -1,56 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.rpc;
-
-import com.google.gwt.http.client.Response;
-
-/** Wraps decoded server reply with HTTP headers. */
-public class HttpResponse<T> {
-  private final Response httpResponse;
-  private final String contentType;
-  private final T result;
-
-  HttpResponse(Response httpResponse, String contentType, T result) {
-    this.httpResponse = httpResponse;
-    this.contentType = contentType;
-    this.result = result;
-  }
-
-  /** HTTP status code, always in the 2xx family. */
-  public int getStatusCode() {
-    return httpResponse.getStatusCode();
-  }
-
-  /**
-   * Content type supplied by the server.
-   *
-   * <p>This helper simplifies the common {@code getHeader("Content-Type")} case.
-   */
-  public String getContentType() {
-    return contentType;
-  }
-
-  /** Lookup an arbitrary reply header. */
-  public String getHeader(String header) {
-    if ("Content-Type".equals(header)) {
-      return contentType;
-    }
-    return httpResponse.getHeader(header);
-  }
-
-  public T getResult() {
-    return result;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java
deleted file mode 100644
index e2a9ffb..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java
+++ /dev/null
@@ -1,519 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.rpc;
-
-import static com.google.gwt.http.client.RequestBuilder.DELETE;
-import static com.google.gwt.http.client.RequestBuilder.GET;
-import static com.google.gwt.http.client.RequestBuilder.POST;
-import static com.google.gwt.http.client.RequestBuilder.PUT;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.RpcStatus;
-import com.google.gerrit.common.data.HostPageData;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.Scheduler;
-import com.google.gwt.http.client.Request;
-import com.google.gwt.http.client.RequestBuilder;
-import com.google.gwt.http.client.RequestBuilder.Method;
-import com.google.gwt.http.client.RequestCallback;
-import com.google.gwt.http.client.RequestException;
-import com.google.gwt.http.client.Response;
-import com.google.gwt.http.client.URL;
-import com.google.gwt.json.client.JSONException;
-import com.google.gwt.json.client.JSONParser;
-import com.google.gwt.json.client.JSONValue;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.rpc.StatusCodeException;
-
-/** Makes a REST API call to the server. */
-public class RestApi {
-  private static final int SC_UNAVAILABLE = 2;
-  private static final int SC_BAD_TRANSPORT = 3;
-  private static final int SC_BAD_RESPONSE = 4;
-  private static final String JSON_TYPE = "application/json";
-  private static final String JSON_UTF8 = JSON_TYPE + "; charset=utf-8";
-  private static final String TEXT_TYPE = "text/plain";
-  private static final String TEXT_UTF8 = TEXT_TYPE + "; charset=utf-8";
-
-  /**
-   * Expected JSON content body prefix that prevents XSSI.
-   *
-   * <p>The server always includes this line as the first line of the response content body when the
-   * response body is formatted as JSON. It gets inserted by the server to prevent the resource from
-   * being imported into another domain's page using a &lt;script&gt; tag. This line must be removed
-   * before the JSON can be parsed.
-   */
-  private static final String JSON_MAGIC = ")]}'\n";
-
-  /** True if err is a StatusCodeException reporting Not Found. */
-  public static boolean isNotFound(Throwable err) {
-    return isStatus(err, Response.SC_NOT_FOUND);
-  }
-
-  /** True if err is describing a user that is currently anonymous. */
-  public static boolean isNotSignedIn(Throwable err) {
-    if (err instanceof StatusCodeException) {
-      StatusCodeException sce = (StatusCodeException) err;
-      if (sce.getStatusCode() == Response.SC_UNAUTHORIZED) {
-        return true;
-      }
-      return sce.getStatusCode() == Response.SC_FORBIDDEN
-          && (sce.getEncodedResponse().equals("Authentication required")
-              || sce.getEncodedResponse().startsWith("Must be signed-in")
-              || sce.getEncodedResponse().startsWith("Invalid authentication"));
-    }
-    return false;
-  }
-
-  /** True if err is a StatusCodeException with a specific HTTP code. */
-  public static boolean isStatus(Throwable err, int status) {
-    return err instanceof StatusCodeException
-        && ((StatusCodeException) err).getStatusCode() == status;
-  }
-
-  /** Is the Gerrit Code Review server likely to return this status? */
-  public static boolean isExpected(int statusCode) {
-    switch (statusCode) {
-      case SC_UNAVAILABLE:
-      case Response.SC_BAD_REQUEST:
-      case Response.SC_UNAUTHORIZED:
-      case Response.SC_FORBIDDEN:
-      case Response.SC_NOT_FOUND:
-      case Response.SC_METHOD_NOT_ALLOWED:
-      case Response.SC_CONFLICT:
-      case Response.SC_PRECONDITION_FAILED:
-      case 422: // Unprocessable Entity
-      case 429: // Too Many Requests (RFC 6585)
-        return true;
-
-      default:
-        // Assume any other code is not expected. These may be
-        // local proxy server errors outside of our control.
-        return false;
-    }
-  }
-
-  private static class HttpImpl<T extends JavaScriptObject> implements RequestCallback {
-    private final boolean background;
-    private final HttpCallback<T> cb;
-
-    HttpImpl(boolean bg, HttpCallback<T> cb) {
-      this.background = bg;
-      this.cb = cb;
-    }
-
-    @Override
-    public void onResponseReceived(Request req, Response res) {
-      int status = res.getStatusCode();
-      if (status == Response.SC_NO_CONTENT) {
-        cb.onSuccess(new HttpResponse<T>(res, null, null));
-        if (!background) {
-          RpcStatus.INSTANCE.onRpcComplete();
-        }
-
-      } else if (200 <= status && status < 300) {
-        long start = System.currentTimeMillis();
-        final T data;
-        final String type;
-        if (isJsonBody(res)) {
-          try {
-            JSONValue val = parseJson(res);
-            if (isJsonEncoded(res) && val.isString() != null) {
-              data = NativeString.wrap(val.isString().stringValue()).cast();
-              type = simpleType(res.getHeader("X-FYI-Content-Type"));
-            } else {
-              data = RestApi.<T>cast(val);
-              type = JSON_TYPE;
-            }
-          } catch (JSONException e) {
-            if (!background) {
-              RpcStatus.INSTANCE.onRpcComplete();
-            }
-            cb.onFailure(
-                new StatusCodeException(SC_BAD_RESPONSE, "Invalid JSON: " + e.getMessage()));
-            return;
-          }
-        } else if (isTextBody(res)) {
-          data = NativeString.wrap(res.getText()).cast();
-          type = TEXT_TYPE;
-        } else {
-          if (!background) {
-            RpcStatus.INSTANCE.onRpcComplete();
-          }
-          cb.onFailure(
-              new StatusCodeException(
-                  SC_BAD_RESPONSE,
-                  "Expected "
-                      + JSON_TYPE
-                      + " or "
-                      + TEXT_TYPE
-                      + "; received Content-Type: "
-                      + res.getHeader("Content-Type")));
-          return;
-        }
-
-        Scheduler.ScheduledCommand cmd =
-            new Scheduler.ScheduledCommand() {
-              @Override
-              public void execute() {
-                try {
-                  cb.onSuccess(new HttpResponse<>(res, type, data));
-                } finally {
-                  if (!background) {
-                    RpcStatus.INSTANCE.onRpcComplete();
-                  }
-                }
-              }
-            };
-
-        // Defer handling the response if the create took a while.
-        if ((System.currentTimeMillis() - start) > 75) {
-          Scheduler.get().scheduleDeferred(cmd);
-        } else {
-          cmd.execute();
-        }
-      } else {
-        String msg;
-        if (isTextBody(res)) {
-          msg = res.getText().trim();
-        } else if (isJsonBody(res)) {
-          JSONValue v;
-          try {
-            v = parseJson(res);
-          } catch (JSONException e) {
-            v = null;
-          }
-          if (v != null && v.isString() != null) {
-            msg = v.isString().stringValue();
-          } else {
-            msg = trimJsonMagic(res.getText()).trim();
-          }
-        } else {
-          msg = res.getStatusText();
-        }
-
-        if (!background) {
-          RpcStatus.INSTANCE.onRpcComplete();
-        }
-        cb.onFailure(new StatusCodeException(status, msg));
-      }
-    }
-
-    @Override
-    public void onError(Request req, Throwable err) {
-      if (!background) {
-        RpcStatus.INSTANCE.onRpcComplete();
-      }
-      if (err.getMessage().contains("XmlHttpRequest.status")) {
-        cb.onFailure(
-            new StatusCodeException(SC_UNAVAILABLE, RpcConstants.C.errorServerUnavailable()));
-      } else {
-        cb.onFailure(new StatusCodeException(SC_BAD_TRANSPORT, err.getMessage()));
-      }
-    }
-  }
-
-  private StringBuilder url;
-  private boolean hasQueryParams;
-  private boolean background;
-  private String ifNoneMatch;
-
-  /**
-   * Initialize a new API call.
-   *
-   * <p>By default the JSON format will be selected by including an HTTP Accept header in the
-   * request.
-   *
-   * @param name URL of the REST resource to access, e.g. {@code "/projects/"} to list accessible
-   *     projects from the server.
-   */
-  public RestApi(String name) {
-    if (name.startsWith("/")) {
-      name = name.substring(1);
-    }
-
-    url = new StringBuilder();
-    url.append(GWT.getHostPageBaseURL());
-    url.append(name);
-  }
-
-  public RestApi view(String name) {
-    return idRaw(name);
-  }
-
-  public RestApi id(String id) {
-    return idRaw(URL.encodePathSegment(id));
-  }
-
-  public RestApi id(String project, int id) {
-    return idRaw(URL.encodePathSegment(project) + "~" + id);
-  }
-
-  public RestApi id(int id) {
-    return idRaw(Integer.toString(id));
-  }
-
-  public RestApi idRaw(String name) {
-    if (hasQueryParams) {
-      throw new IllegalStateException();
-    }
-    if (url.charAt(url.length() - 1) != '/') {
-      url.append('/');
-    }
-    url.append(name);
-    return this;
-  }
-
-  public RestApi addParameter(String name, String value) {
-    return addParameterRaw(name, URL.encodeQueryString(value));
-  }
-
-  public RestApi addParameter(String name, String... value) {
-    for (String val : value) {
-      addParameter(name, val);
-    }
-    return this;
-  }
-
-  public RestApi addParameterTrue(String name) {
-    return addParameterRaw(name, null);
-  }
-
-  public RestApi addParameter(String name, boolean value) {
-    return addParameterRaw(name, value ? "t" : "f");
-  }
-
-  public RestApi addParameter(String name, int value) {
-    return addParameterRaw(name, String.valueOf(value));
-  }
-
-  public RestApi addParameter(String name, Enum<?> value) {
-    return addParameterRaw(name, value.name());
-  }
-
-  public RestApi addParameterRaw(String name, String value) {
-    if (hasQueryParams) {
-      url.append("&");
-    } else {
-      url.append("?");
-      hasQueryParams = true;
-    }
-    url.append(name);
-    if (value != null) {
-      url.append("=").append(value);
-    }
-    return this;
-  }
-
-  public RestApi ifNoneMatch() {
-    return ifNoneMatch("*");
-  }
-
-  public RestApi ifNoneMatch(String etag) {
-    ifNoneMatch = etag;
-    return this;
-  }
-
-  public RestApi background() {
-    background = true;
-    return this;
-  }
-
-  public String url() {
-    return url.toString();
-  }
-
-  public <T extends JavaScriptObject> void get(AsyncCallback<T> cb) {
-    get(wrap(cb));
-  }
-
-  public <T extends JavaScriptObject> void get(HttpCallback<T> cb) {
-    send(GET, cb);
-  }
-
-  public <T extends JavaScriptObject> void delete(AsyncCallback<T> cb) {
-    delete(wrap(cb));
-  }
-
-  public <T extends JavaScriptObject> void delete(HttpCallback<T> cb) {
-    send(DELETE, cb);
-  }
-
-  private <T extends JavaScriptObject> void send(Method method, HttpCallback<T> cb) {
-    HttpImpl<T> httpCallback = new HttpImpl<>(background, cb);
-    try {
-      if (!background) {
-        RpcStatus.INSTANCE.onRpcStart();
-      }
-      request(method).sendRequest(null, httpCallback);
-    } catch (RequestException e) {
-      httpCallback.onError(null, e);
-    }
-  }
-
-  public <T extends JavaScriptObject> void post(JavaScriptObject content, AsyncCallback<T> cb) {
-    post(content, wrap(cb));
-  }
-
-  public <T extends JavaScriptObject> void post(JavaScriptObject content, HttpCallback<T> cb) {
-    sendJSON(POST, content, cb);
-  }
-
-  public <T extends JavaScriptObject> void post(String content, AsyncCallback<T> cb) {
-    post(content, wrap(cb));
-  }
-
-  public <T extends JavaScriptObject> void post(String content, HttpCallback<T> cb) {
-    sendText(POST, content, cb);
-  }
-
-  public <T extends JavaScriptObject> void put(AsyncCallback<T> cb) {
-    put(wrap(cb));
-  }
-
-  public <T extends JavaScriptObject> void put(HttpCallback<T> cb) {
-    send(PUT, cb);
-  }
-
-  public <T extends JavaScriptObject> void put(String content, AsyncCallback<T> cb) {
-    put(content, wrap(cb));
-  }
-
-  public <T extends JavaScriptObject> void put(String content, HttpCallback<T> cb) {
-    sendText(PUT, content, cb);
-  }
-
-  public <T extends JavaScriptObject> void put(JavaScriptObject content, AsyncCallback<T> cb) {
-    put(content, wrap(cb));
-  }
-
-  public <T extends JavaScriptObject> void put(JavaScriptObject content, HttpCallback<T> cb) {
-    sendJSON(PUT, content, cb);
-  }
-
-  private <T extends JavaScriptObject> void sendJSON(
-      Method method, JavaScriptObject content, HttpCallback<T> cb) {
-    HttpImpl<T> httpCallback = new HttpImpl<>(background, cb);
-    try {
-      if (!background) {
-        RpcStatus.INSTANCE.onRpcStart();
-      }
-      RequestBuilder req = request(method);
-      req.setHeader("Content-Type", JSON_UTF8);
-      req.sendRequest(str(content), httpCallback);
-    } catch (RequestException e) {
-      httpCallback.onError(null, e);
-    }
-  }
-
-  private static native String str(JavaScriptObject jso) /*-{ return JSON.stringify(jso) }-*/;
-
-  private <T extends JavaScriptObject> void sendText(
-      Method method, String body, HttpCallback<T> cb) {
-    HttpImpl<T> httpCallback = new HttpImpl<>(background, cb);
-    try {
-      if (!background) {
-        RpcStatus.INSTANCE.onRpcStart();
-      }
-      RequestBuilder req = request(method);
-      req.setHeader("Content-Type", TEXT_UTF8);
-      req.sendRequest(body, httpCallback);
-    } catch (RequestException e) {
-      httpCallback.onError(null, e);
-    }
-  }
-
-  private RequestBuilder request(Method method) {
-    RequestBuilder req = new RequestBuilder(method, url());
-    if (ifNoneMatch != null) {
-      req.setHeader("If-None-Match", ifNoneMatch);
-    }
-    req.setHeader("Accept", JSON_TYPE);
-    if (Gerrit.getXGerritAuth() != null) {
-      req.setHeader(HostPageData.XSRF_HEADER_NAME, Gerrit.getXGerritAuth());
-    }
-    return req;
-  }
-
-  private static boolean isJsonBody(Response res) {
-    return isContentType(res, JSON_TYPE);
-  }
-
-  private static boolean isTextBody(Response res) {
-    return isContentType(res, TEXT_TYPE);
-  }
-
-  private static boolean isJsonEncoded(Response res) {
-    return "json".equals(res.getHeader("X-FYI-Content-Encoding"));
-  }
-
-  private static boolean isContentType(Response res, String want) {
-    String type = res.getHeader("Content-Type");
-    return type != null && want.equals(simpleType(type));
-  }
-
-  private static String simpleType(String type) {
-    int semi = type.indexOf(';');
-    if (semi >= 0) {
-      return type.substring(0, semi).trim();
-    }
-    return type;
-  }
-
-  private static JSONValue parseJson(Response res) throws JSONException {
-    String json = trimJsonMagic(res.getText());
-    if (json.isEmpty()) {
-      throw new JSONException("response was empty");
-    }
-    return JSONParser.parseStrict(json);
-  }
-
-  private static String trimJsonMagic(String json) {
-    if (json.startsWith(JSON_MAGIC)) {
-      json = json.substring(JSON_MAGIC.length());
-    }
-    return json;
-  }
-
-  @SuppressWarnings("unchecked")
-  private static <T extends JavaScriptObject> T cast(JSONValue val) {
-    if (val.isObject() != null) {
-      return (T) val.isObject().getJavaScriptObject();
-    } else if (val.isArray() != null) {
-      return (T) val.isArray().getJavaScriptObject();
-    } else if (val.isString() != null) {
-      return (T) NativeString.wrap(val.isString().stringValue());
-    } else if (val.isNull() != null) {
-      return null;
-    } else {
-      throw new JSONException("unsupported JSON type");
-    }
-  }
-
-  private static <T extends JavaScriptObject> HttpCallback<T> wrap(AsyncCallback<T> cb) {
-    return new HttpCallback<T>() {
-      @Override
-      public void onSuccess(HttpResponse<T> r) {
-        cb.onSuccess(r.getResult());
-      }
-
-      @Override
-      public void onFailure(Throwable e) {
-        cb.onFailure(e);
-      }
-    };
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RpcConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RpcConstants.java
deleted file mode 100644
index 56d536d..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RpcConstants.java
+++ /dev/null
@@ -1,26 +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.client.rpc;
-
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.i18n.client.Constants;
-
-public interface RpcConstants extends Constants {
-  RpcConstants C = GWT.create(RpcConstants.class);
-
-  String errorServerUnavailable();
-
-  String errorRemoteJsonException();
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RpcConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RpcConstants.properties
deleted file mode 100644
index e8695b1..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RpcConstants.properties
+++ /dev/null
@@ -1,2 +0,0 @@
-errorServerUnavailable = Server Unavailable
-errorRemoteJsonException = Server Error
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/ScreenLoadCallback.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/ScreenLoadCallback.java
deleted file mode 100644
index 3aae04a..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/ScreenLoadCallback.java
+++ /dev/null
@@ -1,54 +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.client.rpc;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.NotFoundScreen;
-import com.google.gerrit.client.NotSignedInDialog;
-import com.google.gerrit.client.ui.Screen;
-import com.google.gerrit.common.errors.NoSuchEntityException;
-
-/** Callback switching {@link NoSuchEntityException} to {@link NotFoundScreen} */
-public abstract class ScreenLoadCallback<T> extends GerritCallback<T> {
-  private final Screen screen;
-
-  public ScreenLoadCallback(Screen s) {
-    screen = s;
-  }
-
-  @Override
-  public final void onSuccess(T result) {
-    if (screen.isAttached()) {
-      preDisplay(result);
-      screen.display();
-      postDisplay();
-    }
-  }
-
-  protected abstract void preDisplay(T result);
-
-  protected void postDisplay() {}
-
-  @Override
-  public void onFailure(Throwable caught) {
-    if (isSigninFailure(caught)) {
-      new NotSignedInDialog().center();
-    } else if (isNoSuchEntity(caught)) {
-      Gerrit.display(screen.getToken(), new NotFoundScreen());
-    } else {
-      super.onFailure(caught);
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
deleted file mode 100644
index bdebd68..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
+++ /dev/null
@@ -1,80 +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.client.ui;
-
-import com.google.gerrit.client.groups.GroupMap;
-import com.google.gerrit.client.info.GroupInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.user.client.ui.SuggestOracle;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Map;
-
-/** Suggestion Oracle for AccountGroup entities. */
-public class AccountGroupSuggestOracle extends SuggestAfterTypingNCharsOracle {
-  private Map<String, AccountGroup.UUID> priorResults = new HashMap<>();
-
-  private Project.NameKey projectName;
-
-  @Override
-  public void _onRequestSuggestions(Request req, Callback callback) {
-    GroupMap.suggestAccountGroupForProject(
-        projectName == null ? null : projectName.get(),
-        req.getQuery(),
-        req.getLimit(),
-        new GerritCallback<GroupMap>() {
-          @Override
-          public void onSuccess(GroupMap result) {
-            priorResults.clear();
-            ArrayList<AccountGroupSuggestion> r = new ArrayList<>(result.size());
-            for (GroupInfo group : Natives.asList(result.values())) {
-              r.add(new AccountGroupSuggestion(group));
-              priorResults.put(group.name(), group.getGroupUUID());
-            }
-            callback.onSuggestionsReady(req, new Response(r));
-          }
-        });
-  }
-
-  public void setProject(Project.NameKey projectName) {
-    this.projectName = projectName;
-  }
-
-  private static class AccountGroupSuggestion implements SuggestOracle.Suggestion {
-    private final GroupInfo info;
-
-    AccountGroupSuggestion(GroupInfo k) {
-      info = k;
-    }
-
-    @Override
-    public String getDisplayString() {
-      return info.name();
-    }
-
-    @Override
-    public String getReplacementString() {
-      return info.name();
-    }
-  }
-
-  /** @return the group UUID, or null if it cannot be found. */
-  public AccountGroup.UUID getUUID(String name) {
-    return priorResults.get(name);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLinkPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLinkPanel.java
deleted file mode 100644
index c44f357..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLinkPanel.java
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.ui;
-
-import com.google.gerrit.client.AvatarImage;
-import com.google.gerrit.client.FormatUtil;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.info.AccountInfo;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gwt.user.client.ui.FlowPanel;
-import java.util.function.Function;
-
-/** Link to any user's account dashboard. */
-public class AccountLinkPanel extends FlowPanel {
-  public static AccountLinkPanel create(AccountInfo ai) {
-    return withStatus(ai, Change.Status.NEW);
-  }
-
-  public static AccountLinkPanel withStatus(AccountInfo ai, Change.Status status) {
-    return new AccountLinkPanel(ai, name -> PageLinks.toAccountQuery(name, status));
-  }
-
-  public static AccountLinkPanel forAssignee(AccountInfo ai) {
-    return new AccountLinkPanel(ai, PageLinks::toAssigneeQuery);
-  }
-
-  private AccountLinkPanel(AccountInfo ai, Function<String, String> nameToQuery) {
-    addStyleName(Gerrit.RESOURCES.css().accountLinkPanel());
-
-    InlineHyperlink l =
-        new InlineHyperlink(FormatUtil.name(ai), nameToQuery.apply(name(ai))) {
-          @Override
-          public void go() {
-            Gerrit.display(getTargetHistoryToken());
-          }
-        };
-    l.setTitle(FormatUtil.nameEmail(ai));
-
-    add(new AvatarImage(ai));
-    add(l);
-  }
-
-  private static String name(AccountInfo ai) {
-    if (ai.email() != null) {
-      return ai.email();
-    } else if (ai.name() != null) {
-      return ai.name();
-    } else if (ai._accountId() != 0) {
-      return "" + ai._accountId();
-    } else {
-      return "";
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountScreen.java
deleted file mode 100644
index 59ff9fe3..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountScreen.java
+++ /dev/null
@@ -1,22 +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.client.ui;
-
-/** A screen that requires the user to be signed-into their account. */
-public abstract class AccountScreen extends Screen {
-  protected AccountScreen() {
-    setRequiresSignIn(true);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java
deleted file mode 100644
index 5038ad9..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java
+++ /dev/null
@@ -1,88 +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.client.ui;
-
-import com.google.gerrit.client.FormatUtil;
-import com.google.gerrit.client.account.AccountApi;
-import com.google.gerrit.client.info.AccountInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.user.client.ui.SuggestOracle;
-import java.util.ArrayList;
-import java.util.List;
-
-/** Suggestion Oracle for Account entities. */
-public class AccountSuggestOracle extends SuggestAfterTypingNCharsOracle {
-  @Override
-  public void _onRequestSuggestions(Request req, Callback cb) {
-    AccountApi.suggest(
-        req.getQuery(),
-        req.getLimit(),
-        new GerritCallback<JsArray<AccountInfo>>() {
-          @Override
-          public void onSuccess(JsArray<AccountInfo> in) {
-            List<AccountSuggestion> r = new ArrayList<>(in.length());
-            for (AccountInfo p : Natives.asList(in)) {
-              r.add(new AccountSuggestion(p, req.getQuery()));
-            }
-            cb.onSuggestionsReady(req, new Response(r));
-          }
-        });
-  }
-
-  public static class AccountSuggestion implements SuggestOracle.Suggestion {
-    private final String suggestion;
-
-    public AccountSuggestion(AccountInfo info, String query) {
-      this.suggestion = format(info, query);
-    }
-
-    @Override
-    public String getDisplayString() {
-      return suggestion;
-    }
-
-    @Override
-    public String getReplacementString() {
-      return suggestion;
-    }
-
-    public static String format(AccountInfo info, String query) {
-      String s = FormatUtil.nameEmail(info);
-      if (query != null && !containsQuery(s, query) && info.secondaryEmails() != null) {
-        for (String email : Natives.asList(info.secondaryEmails())) {
-          AccountInfo info2 =
-              AccountInfo.create(info._accountId(), info.name(), email, info.username());
-          String s2 = FormatUtil.nameEmail(info2);
-          if (containsQuery(s2, query)) {
-            s = s2;
-            break;
-          }
-        }
-      }
-      return s;
-    }
-
-    private static boolean containsQuery(String s, String query) {
-      for (String qterm : query.split("\\s+")) {
-        if (!s.toLowerCase().contains(qterm.toLowerCase())) {
-          return false;
-        }
-      }
-      return true;
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AddMemberBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AddMemberBox.java
deleted file mode 100644
index a1d2229..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AddMemberBox.java
+++ /dev/null
@@ -1,70 +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.client.ui;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.logical.shared.SelectionEvent;
-import com.google.gwt.event.logical.shared.SelectionHandler;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.SuggestOracle;
-
-public class AddMemberBox extends Composite {
-  private final FlowPanel addPanel;
-  private final Button addMember;
-  private final RemoteSuggestBox suggestBox;
-
-  public AddMemberBox(final String buttonLabel, String hint, SuggestOracle suggestOracle) {
-    addPanel = new FlowPanel();
-    addMember = new Button(buttonLabel);
-
-    suggestBox = new RemoteSuggestBox(suggestOracle);
-    suggestBox.setStyleName(Gerrit.RESOURCES.css().addMemberTextBox());
-    suggestBox.setVisibleLength(50);
-    suggestBox.setHintText(hint);
-    suggestBox.addSelectionHandler(
-        new SelectionHandler<String>() {
-          @Override
-          public void onSelection(SelectionEvent<String> event) {
-            addMember.fireEvent(new ClickEvent() {});
-          }
-        });
-
-    addPanel.add(suggestBox);
-    addPanel.add(addMember);
-
-    initWidget(addPanel);
-  }
-
-  public void addClickHandler(ClickHandler handler) {
-    addMember.addClickHandler(handler);
-  }
-
-  public String getText() {
-    return suggestBox.getText();
-  }
-
-  public void setEnabled(boolean enabled) {
-    addMember.setEnabled(enabled);
-    suggestBox.setEnabled(enabled);
-  }
-
-  public void setText(String text) {
-    suggestBox.setText(text);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/BranchLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/BranchLink.java
deleted file mode 100644
index 6e05f83..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/BranchLink.java
+++ /dev/null
@@ -1,85 +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.client.ui;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.QueryScreen;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
-
-/** Link to the open changes of a project. */
-public class BranchLink extends InlineHyperlink {
-  private final String query;
-
-  public BranchLink(Project.NameKey project, Change.Status status, String branch, String topic) {
-    this(text(branch, topic), query(project, status, branch, topic));
-  }
-
-  public BranchLink(
-      String text, Project.NameKey project, Change.Status status, String branch, String topic) {
-    this(text, query(project, status, branch, topic));
-  }
-
-  private BranchLink(String text, String query) {
-    super(text, PageLinks.toChangeQuery(query));
-    this.query = query;
-  }
-
-  @Override
-  public void go() {
-    Gerrit.display(getTargetHistoryToken(), createScreen());
-  }
-
-  private Screen createScreen() {
-    return QueryScreen.forQuery(query);
-  }
-
-  private static String text(String branch, String topic) {
-    if (topic != null && !topic.isEmpty()) {
-      return branch + " (" + topic + ")";
-    }
-    return branch;
-  }
-
-  public static String query(
-      Project.NameKey project, Change.Status status, String branch, String topic) {
-    String query = PageLinks.projectQuery(project, status);
-
-    if (branch.startsWith(RefNames.REFS)) {
-      if (branch.startsWith(RefNames.REFS_HEADS)) {
-        query +=
-            " "
-                + PageLinks.op(
-                    "branch", //
-                    branch.substring(RefNames.REFS_HEADS.length()));
-      } else {
-        query += " " + PageLinks.op("ref", branch);
-      }
-    } else {
-      // Assume it was clipped already by the caller.  This
-      // happens for example inside of the ChangeInfo object.
-      //
-      query += " " + PageLinks.op("branch", branch);
-    }
-
-    if (topic != null && !topic.isEmpty()) {
-      query += " " + PageLinks.op("topic", topic);
-    }
-
-    return query;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ChangeLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ChangeLink.java
deleted file mode 100644
index b54d752..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ChangeLink.java
+++ /dev/null
@@ -1,40 +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.client.ui;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.GWT;
-
-public class ChangeLink extends InlineHyperlink {
-  public static String permalink(Change.Id c) {
-    return GWT.getHostPageBaseURL() + c.get();
-  }
-
-  protected Change.Id cid;
-
-  public ChangeLink(Project.NameKey project, Change.Id c, String text) {
-    super(text, PageLinks.toChange(project, c));
-    getElement().setPropertyString("href", permalink(c));
-    cid = c;
-  }
-
-  @Override
-  public void go() {
-    Gerrit.display(getTargetHistoryToken());
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CherryPickDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CherryPickDialog.java
deleted file mode 100644
index 0a0c14a..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CherryPickDialog.java
+++ /dev/null
@@ -1,107 +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.client.ui;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.Util;
-import com.google.gerrit.client.projects.BranchInfo;
-import com.google.gerrit.client.projects.ProjectApi;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.SuggestBox;
-import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
-import java.util.ArrayList;
-import java.util.List;
-
-public abstract class CherryPickDialog extends TextAreaActionDialog {
-  private SuggestBox newBranch;
-  private List<BranchInfo> branches;
-
-  public CherryPickDialog(Project.NameKey project) {
-    super(Util.C.cherryPickTitle(), Util.C.cherryPickCommitMessage());
-    ProjectApi.getBranches(
-        project,
-        new GerritCallback<JsArray<BranchInfo>>() {
-          @Override
-          public void onSuccess(JsArray<BranchInfo> result) {
-            branches = Natives.asList(result);
-          }
-        });
-
-    newBranch =
-        new SuggestBox(
-            new HighlightSuggestOracle() {
-              @Override
-              protected void onRequestSuggestions(Request request, Callback done) {
-                List<BranchSuggestion> suggestions = new ArrayList<>();
-                for (BranchInfo b : branches) {
-                  if (b.ref().contains(request.getQuery())) {
-                    suggestions.add(new BranchSuggestion(b));
-                  }
-                }
-                done.onSuggestionsReady(request, new Response(suggestions));
-              }
-            });
-
-    newBranch.setWidth("100%");
-    newBranch.getElement().getStyle().setProperty("boxSizing", "border-box");
-    message.setCharacterWidth(70);
-
-    final FlowPanel mwrap = new FlowPanel();
-    mwrap.setStyleName(Gerrit.RESOURCES.css().commentedActionMessage());
-    mwrap.add(newBranch);
-
-    panel.insert(mwrap, 0);
-    panel.insert(new SmallHeading(Util.C.headingCherryPickBranch()), 0);
-  }
-
-  @Override
-  public void center() {
-    super.center();
-    GlobalKey.dialog(this);
-    newBranch.setFocus(true);
-  }
-
-  public String getDestinationBranch() {
-    return newBranch.getText();
-  }
-
-  static class BranchSuggestion implements Suggestion {
-    private BranchInfo branch;
-
-    BranchSuggestion(BranchInfo branch) {
-      this.branch = branch;
-    }
-
-    @Override
-    public String getDisplayString() {
-      final String refsHeads = "refs/heads/";
-      if (branch.ref().startsWith(refsHeads)) {
-        return branch.ref().substring(refsHeads.length());
-      }
-      return branch.ref();
-    }
-
-    @Override
-    public String getReplacementString() {
-      return branch.getShortName();
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommandMenuItem.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommandMenuItem.java
deleted file mode 100644
index c5ee34f..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommandMenuItem.java
+++ /dev/null
@@ -1,40 +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.client.ui;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gwt.aria.client.Roles;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.user.client.Command;
-import com.google.gwt.user.client.ui.Anchor;
-
-public class CommandMenuItem extends Anchor implements ClickHandler {
-  private final Command command;
-
-  public CommandMenuItem(String text, Command cmd) {
-    super(text);
-    setStyleName(Gerrit.RESOURCES.css().menuItem());
-    Roles.getMenuitemRole().set(getElement());
-    addClickHandler(this);
-    command = cmd;
-  }
-
-  @Override
-  public void onClick(ClickEvent event) {
-    setFocus(false);
-    command.execute();
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentLinkProcessor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentLinkProcessor.java
deleted file mode 100644
index 1753ade..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentLinkProcessor.java
+++ /dev/null
@@ -1,95 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT 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.client.ui;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gwtexpui.safehtml.client.FindReplace;
-import com.google.gwtexpui.safehtml.client.SafeHtml;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.gwtjsonrpc.common.VoidResult;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-public class CommentLinkProcessor {
-  private List<FindReplace> commentLinks;
-
-  public CommentLinkProcessor(List<FindReplace> commentLinks) {
-    this.commentLinks = commentLinks;
-  }
-
-  public SafeHtml apply(SafeHtml buf) {
-    try {
-      return buf.replaceAll(commentLinks);
-    } catch (RuntimeException err) {
-      // One or more of the patterns isn't valid on this browser.
-      // Try to filter the list down and remove the invalid ones.
-
-      List<FindReplace> safe = new ArrayList<>(commentLinks.size());
-
-      List<PatternError> bad = new ArrayList<>();
-      for (FindReplace r : commentLinks) {
-        try {
-          buf.replaceAll(Collections.singletonList(r));
-          safe.add(r);
-        } catch (RuntimeException why) {
-          bad.add(new PatternError(r, why.getMessage()));
-        }
-      }
-
-      if (!bad.isEmpty()) {
-        StringBuilder msg = new StringBuilder();
-        msg.append("Invalid commentlink pattern(s):");
-        for (PatternError e : bad) {
-          msg.append("\n");
-          msg.append("\"");
-          msg.append(e.pattern.pattern().getSource());
-          msg.append("\": ");
-          msg.append(e.errorMessage);
-        }
-        Gerrit.SYSTEM_SVC.clientError(
-            msg.toString(),
-            new AsyncCallback<VoidResult>() {
-              @Override
-              public void onFailure(Throwable caught) {}
-
-              @Override
-              public void onSuccess(VoidResult result) {}
-            });
-      }
-
-      try {
-        commentLinks = safe;
-        return buf.replaceAll(safe);
-      } catch (RuntimeException err2) {
-        // To heck with it. The patterns passed individually above but
-        // failed as a group? Just render without.
-        //
-        commentLinks = null;
-        return buf;
-      }
-    }
-  }
-
-  private static class PatternError {
-    FindReplace pattern;
-    String errorMessage;
-
-    PatternError(FindReplace r, String w) {
-      pattern = r;
-      errorMessage = w;
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentedActionDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentedActionDialog.java
deleted file mode 100644
index b68f329..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentedActionDialog.java
+++ /dev/null
@@ -1,110 +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.client.ui;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.logical.shared.CloseEvent;
-import com.google.gwt.event.logical.shared.CloseHandler;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.FocusWidget;
-import com.google.gwt.user.client.ui.PopupPanel;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-import com.google.gwtexpui.user.client.AutoCenterDialogBox;
-
-public abstract class CommentedActionDialog extends AutoCenterDialogBox
-    implements CloseHandler<PopupPanel> {
-  protected final FlowPanel panel;
-  protected final Button sendButton;
-  protected final Button cancelButton;
-  protected final FlowPanel buttonPanel;
-  protected final FlowPanel contentPanel;
-  protected FocusWidget focusOn;
-
-  protected boolean sent;
-
-  public CommentedActionDialog(String title, String heading) {
-    super(/* auto hide */ false, /* modal */ true);
-    setGlassEnabled(true);
-    setText(title);
-
-    addStyleName(Gerrit.RESOURCES.css().commentedActionDialog());
-
-    sendButton = new Button(Util.C.commentedActionButtonSend());
-    sendButton.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            enableButtons(false);
-            onSend();
-          }
-        });
-
-    cancelButton = new Button(Util.C.commentedActionButtonCancel());
-    cancelButton.getElement().getStyle().setProperty("float", "right");
-    cancelButton.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            hide();
-          }
-        });
-
-    contentPanel = new FlowPanel();
-    contentPanel.setStyleName(Gerrit.RESOURCES.css().commentedActionMessage());
-
-    buttonPanel = new FlowPanel();
-    buttonPanel.add(sendButton);
-    buttonPanel.add(cancelButton);
-    buttonPanel.getElement().getStyle().setProperty("marginTop", "4px");
-
-    panel = new FlowPanel();
-    if (heading != null) {
-      panel.add(new SmallHeading(heading));
-    }
-    panel.add(contentPanel);
-    panel.add(buttonPanel);
-    add(panel);
-
-    addCloseHandler(this);
-  }
-
-  public void setFocusOn(FocusWidget focusWidget) {
-    focusOn = focusWidget;
-  }
-
-  public void enableButtons(boolean enable) {
-    sendButton.setEnabled(enable);
-    cancelButton.setEnabled(enable);
-  }
-
-  @Override
-  public void center() {
-    super.center();
-    GlobalKey.dialog(this);
-    if (focusOn != null) {
-      focusOn.setFocus(true);
-    }
-  }
-
-  @Override
-  public void onClose(CloseEvent<PopupPanel> event) {
-    sent = false;
-  }
-
-  public abstract void onSend();
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ComplexDisclosurePanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ComplexDisclosurePanel.java
deleted file mode 100644
index c0b662a..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ComplexDisclosurePanel.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.client.ui;
-
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.event.logical.shared.CloseHandler;
-import com.google.gwt.event.logical.shared.HasCloseHandlers;
-import com.google.gwt.event.logical.shared.HasOpenHandlers;
-import com.google.gwt.event.logical.shared.OpenHandler;
-import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.ui.ComplexPanel;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.DisclosurePanel;
-import com.google.gwt.user.client.ui.Panel;
-import com.google.gwt.user.client.ui.Widget;
-
-public class ComplexDisclosurePanel extends Composite
-    implements HasOpenHandlers<DisclosurePanel>, HasCloseHandlers<DisclosurePanel> {
-  private final DisclosurePanel main;
-  private final Panel header;
-
-  public ComplexDisclosurePanel(String text, boolean isOpen) {
-    // Ick. GWT's DisclosurePanel won't let us subclass it, or do any
-    // other modification of its header. We're stuck with injecting
-    // into the DOM directly.
-    //
-    main = new DisclosurePanel(text);
-    main.setOpen(isOpen);
-    final Element headerParent;
-    {
-      final Element table = main.getElement();
-      final Element tbody = DOM.getFirstChild(table);
-      final Element tr1 = DOM.getChild(tbody, 0);
-      final Element tr2 = DOM.getChild(tbody, 1);
-
-      DOM.getChild(tr1, 0).setPropertyString("width", "20px");
-      DOM.getChild(tr2, 0).setPropertyInt("colSpan", 2);
-      headerParent = tr1;
-    }
-
-    header =
-        new ComplexPanel() {
-          {
-            setElement((Element) (DOM.createTD()));
-            getElement().setInnerHTML("&nbsp;");
-          }
-
-          @Override
-          public void add(Widget w) {
-            add(w, (Element) getElement());
-          }
-        };
-
-    initWidget(
-        new ComplexPanel() {
-          {
-            final DisclosurePanel main = ComplexDisclosurePanel.this.main;
-            setElement((Element) (main.getElement()));
-            getChildren().add(main);
-            adopt(main);
-
-            add(ComplexDisclosurePanel.this.header, headerParent);
-          }
-        });
-  }
-
-  public Panel getHeader() {
-    return header;
-  }
-
-  public void setContent(Widget w) {
-    main.setContent(w);
-  }
-
-  public Widget getContent() {
-    return main.getContent();
-  }
-
-  @Override
-  public HandlerRegistration addOpenHandler(OpenHandler<DisclosurePanel> h) {
-    return main.addOpenHandler(h);
-  }
-
-  @Override
-  public HandlerRegistration addCloseHandler(CloseHandler<DisclosurePanel> h) {
-    return main.addCloseHandler(h);
-  }
-
-  /** @return true if the panel's content is visible. */
-  public boolean isOpen() {
-    return main.isOpen();
-  }
-
-  /**
-   * Changes the visible state of this panel's content.
-   *
-   * @param isOpen {@code true} to open, {@code false} to close
-   */
-  public void setOpen(boolean isOpen) {
-    main.setOpen(isOpen);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CreateChangeDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CreateChangeDialog.java
deleted file mode 100644
index 2d00281..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CreateChangeDialog.java
+++ /dev/null
@@ -1,116 +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.client.ui;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.projects.BranchInfo;
-import com.google.gerrit.client.projects.ProjectApi;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.SuggestBox;
-import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
-import com.google.gwt.user.client.ui.TextBox;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
-import java.util.ArrayList;
-import java.util.List;
-
-public abstract class CreateChangeDialog extends TextAreaActionDialog {
-  private SuggestBox newChange;
-  private List<BranchInfo> branches;
-  private TextBox topic;
-
-  public CreateChangeDialog(Project.NameKey project) {
-    super(Util.C.dialogCreateChangeTitle(), Util.C.dialogCreateChangeHeading());
-    ProjectApi.getBranches(
-        project,
-        new GerritCallback<JsArray<BranchInfo>>() {
-          @Override
-          public void onSuccess(JsArray<BranchInfo> result) {
-            branches = Natives.asList(result);
-          }
-        });
-
-    topic = new TextBox();
-    topic.setWidth("100%");
-    topic.getElement().getStyle().setProperty("boxSizing", "border-box");
-    FlowPanel newTopicPanel = new FlowPanel();
-    newTopicPanel.setStyleName(Gerrit.RESOURCES.css().commentedActionMessage());
-    newTopicPanel.add(topic);
-    panel.insert(newTopicPanel, 0);
-    panel.insert(new SmallHeading(Util.C.newChangeTopicSuggestion()), 0);
-
-    newChange =
-        new SuggestBox(
-            new HighlightSuggestOracle() {
-              @Override
-              protected void onRequestSuggestions(Request request, Callback done) {
-                List<BranchSuggestion> suggestions = new ArrayList<>();
-                for (BranchInfo b : branches) {
-                  if (b.ref().contains(request.getQuery())) {
-                    suggestions.add(new BranchSuggestion(b));
-                  }
-                }
-                done.onSuggestionsReady(request, new Response(suggestions));
-              }
-            });
-
-    newChange.setWidth("100%");
-    newChange.getElement().getStyle().setProperty("boxSizing", "border-box");
-    FlowPanel newChangePanel = new FlowPanel();
-    newChangePanel.setStyleName(Gerrit.RESOURCES.css().commentedActionMessage());
-    newChangePanel.add(newChange);
-    panel.insert(newChangePanel, 0);
-    panel.insert(new SmallHeading(Util.C.newChangeBranchSuggestion()), 0);
-
-    message.setCharacterWidth(70);
-  }
-
-  @Override
-  public void center() {
-    super.center();
-    GlobalKey.dialog(this);
-    newChange.setFocus(true);
-  }
-
-  public String getDestinationBranch() {
-    return newChange.getText();
-  }
-
-  public String getDestinationTopic() {
-    return topic.getText();
-  }
-
-  static class BranchSuggestion implements Suggestion {
-    private BranchInfo branch;
-
-    BranchSuggestion(BranchInfo branch) {
-      this.branch = branch;
-    }
-
-    @Override
-    public String getDisplayString() {
-      return branch.getShortName();
-    }
-
-    @Override
-    public String getReplacementString() {
-      return branch.getShortName();
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTable.java
deleted file mode 100644
index 045e0ae..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTable.java
+++ /dev/null
@@ -1,222 +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.client.ui;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.dom.client.Document;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlexTable;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtexpui.safehtml.client.SafeHtml;
-import java.util.Comparator;
-import java.util.Iterator;
-
-public abstract class FancyFlexTable<RowItem> extends Composite {
-  private static final FancyFlexTableImpl impl = GWT.create(FancyFlexTableImpl.class);
-
-  protected static final int C_ARROW = 0;
-
-  protected final MyFlexTable table;
-
-  protected FancyFlexTable() {
-    table = createFlexTable();
-    table.addStyleName(Gerrit.RESOURCES.css().changeTable());
-    table.setWidth("100%");
-    initWidget(table);
-
-    table.setText(0, C_ARROW, "");
-    table.getCellFormatter().addStyleName(0, C_ARROW, Gerrit.RESOURCES.css().iconHeader());
-  }
-
-  protected MyFlexTable createFlexTable() {
-    return new MyFlexTable();
-  }
-
-  protected RowItem getRowItem(int row) {
-    return FancyFlexTable.<RowItem>getRowItem(table.getCellFormatter().getElement(row, 0));
-  }
-
-  protected void setRowItem(int row, RowItem item) {
-    setRowItem(table.getCellFormatter().getElement(row, 0), item);
-  }
-
-  /**
-   * Finds an item in the table.
-   *
-   * @param comparator comparator by which the items in the table are sorted
-   * @param item the item that should be found
-   * @return if the item is found the number of the row that contains the item; if the item is not
-   *     found {@code -1}
-   */
-  protected int findRowItem(Comparator<RowItem> comparator, RowItem item) {
-    int row = lookupRowItem(comparator, item);
-    if (row < table.getRowCount() && comparator.compare(item, getRowItem(row)) == 0) {
-      return row;
-    }
-    return -1;
-  }
-
-  /**
-   * Finds the number of the row where a new item should be inserted into the table.
-   *
-   * @param comparator comparator by which the items in the table are sorted
-   * @param item the new item that should be inserted
-   * @return if the item is not yet contained in the table, the number of the row where the new item
-   *     should be inserted; if the item is already contained in the table {@code -1}
-   */
-  protected int getInsertRow(Comparator<RowItem> comparator, RowItem item) {
-    int row = lookupRowItem(comparator, item);
-    if (row >= table.getRowCount() || comparator.compare(item, getRowItem(row)) != 0) {
-      return row;
-    }
-    return -1;
-  }
-
-  /**
-   * Makes a binary search for the given row item over the table.
-   *
-   * @param comparator comparator by which the items in the table are sorted
-   * @param item the item that should be looked up
-   * @return if the item is found the number of the row that contains the item; if the item is not
-   *     found the number of the row where the item should be inserted according to the given
-   *     comparator.
-   */
-  private int lookupRowItem(Comparator<RowItem> comparator, RowItem item) {
-    int left = 1;
-    int right = table.getRowCount() - 1;
-    while (left <= right) {
-      int middle = (left + right) >>> 1; // (left+right)/2
-      RowItem i = getRowItem(middle);
-      int cmp = comparator.compare(i, item);
-
-      if (cmp < 0) {
-        left = middle + 1;
-      } else if (cmp > 0) {
-        right = middle - 1;
-      } else {
-        // item is already contained in the table
-        return middle;
-      }
-    }
-    return left;
-  }
-
-  protected void resetHtml(SafeHtml body) {
-    for (Iterator<Widget> i = table.iterator(); i.hasNext(); ) {
-      i.next();
-      i.remove();
-    }
-    impl.resetHtml(table, body);
-  }
-
-  protected void scrollIntoView(int topRow, int endRow) {
-    final CellFormatter fmt = table.getCellFormatter();
-    final Element top = fmt.getElement(topRow, C_ARROW).getParentElement();
-    final Element end = fmt.getElement(endRow, C_ARROW).getParentElement();
-
-    final int rTop = top.getAbsoluteTop();
-    final int rEnd = end.getAbsoluteTop() + end.getOffsetHeight();
-    final int rHeight = rEnd - rTop;
-
-    final int sTop = Document.get().getScrollTop();
-    final int sHeight = Document.get().getClientHeight();
-    final int sEnd = sTop + sHeight;
-
-    final int nTop;
-    if (sHeight <= rHeight) {
-      // The region is larger than the visible area, make the top
-      // exactly the top of the region, its the most visible area.
-      //
-      nTop = rTop;
-    } else if (sTop <= rTop && rTop <= sEnd) {
-      // At least part of the region is already visible.
-      //
-      if (rEnd <= sEnd) {
-        // ... actually its all visible. Don't scroll.
-        //
-        return;
-      }
-
-      // Move only enough to make the end visible.
-      //
-      nTop = sTop + (rHeight - (sEnd - rTop));
-    } else {
-      // None of the region is visible. Make it visible.
-      //
-      nTop = rTop;
-    }
-    Document.get().setScrollTop(nTop);
-  }
-
-  protected void applyDataRowStyle(int newRow) {
-    table.getCellFormatter().addStyleName(newRow, C_ARROW, Gerrit.RESOURCES.css().iconCell());
-    table.getCellFormatter().addStyleName(newRow, C_ARROW, Gerrit.RESOURCES.css().leftMostCell());
-  }
-
-  /**
-   * Get the td element that contains another element.
-   *
-   * @param target the child element whose parent td is required.
-   * @return the td containing element {@code target}; null if {@code target} is not a member of
-   *     this table.
-   */
-  protected Element getParentCell(Element target) {
-    final Element body = FancyFlexTableImpl.getBodyElement(table);
-    for (Element td = target; td != null && td != body; td = DOM.getParent(td)) {
-      // If it's a TD, it might be the one we're looking for.
-      if ("td".equalsIgnoreCase(td.getTagName())) {
-        // Make sure it's directly a part of this table.
-        Element tr = DOM.getParent(td);
-        if (DOM.getParent(tr) == body) {
-          return td;
-        }
-      }
-    }
-    return null;
-  }
-
-  /** @return the row of the child element; -1 if the child is not in the table. */
-  protected int rowOf(Element target) {
-    final Element td = getParentCell(target);
-    if (td == null) {
-      return -1;
-    }
-    final Element tr = DOM.getParent(td);
-    final Element body = DOM.getParent(tr);
-    return DOM.getChildIndex(body, tr);
-  }
-
-  /** @return the cell of the child element; -1 if the child is not in the table. */
-  protected int columnOf(Element target) {
-    final Element td = getParentCell(target);
-    if (td == null) {
-      return -1;
-    }
-    final Element tr = DOM.getParent(td);
-    return DOM.getChildIndex(tr, td);
-  }
-
-  protected static class MyFlexTable extends FlexTable {}
-
-  private static native <ItemType> void setRowItem(Element td, ItemType c)
-      /*-{ td['__gerritRowItem'] = c; }-*/ ;
-
-  private static native <ItemType> ItemType getRowItem(Element td)
-      /*-{ return td['__gerritRowItem']; }-*/ ;
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImpl.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImpl.java
deleted file mode 100644
index a3a2a7a..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImpl.java
+++ /dev/null
@@ -1,29 +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.client.ui;
-
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.user.client.ui.FlexTable;
-import com.google.gwt.user.client.ui.HTMLTable;
-import com.google.gwtexpui.safehtml.client.SafeHtml;
-
-public class FancyFlexTableImpl {
-  public void resetHtml(FlexTable myTable, SafeHtml body) {
-    SafeHtml.setInnerHTML(getBodyElement(myTable), body);
-  }
-
-  protected static native Element getBodyElement(HTMLTable myTable)
-      /*-{ return myTable.@com.google.gwt.user.client.ui.HTMLTable::bodyElem; }-*/ ;
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImplIE8.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImplIE8.java
deleted file mode 100644
index 3eae0f8..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImplIE8.java
+++ /dev/null
@@ -1,54 +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.client.ui;
-
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.ui.FlexTable;
-import com.google.gwt.user.client.ui.HTMLTable;
-import com.google.gwtexpui.safehtml.client.SafeHtml;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-
-public class FancyFlexTableImplIE8 extends FancyFlexTableImpl {
-  @Override
-  public void resetHtml(FlexTable myTable, SafeHtml bodyHtml) {
-    final Element oldBody = getBodyElement(myTable);
-    final Element newBody = parseBody(bodyHtml);
-    assert newBody != null;
-
-    final Element tableElem = DOM.getParent(oldBody);
-    tableElem.removeChild(oldBody);
-    setBodyElement(myTable, newBody);
-    DOM.appendChild(tableElem, newBody);
-  }
-
-  private static Element parseBody(SafeHtml body) {
-    final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    b.openElement("table");
-    b.append(body);
-    b.closeElement("table");
-
-    final Element newTable = SafeHtml.parse(b);
-    for (Element e = DOM.getFirstChild(newTable); e != null; e = DOM.getNextSibling(e)) {
-      if ("tbody".equals(e.getTagName().toLowerCase())) {
-        return e;
-      }
-    }
-    return null;
-  }
-
-  private static native void setBodyElement(HTMLTable myTable, Element newBody)
-      /*-{ myTable.@com.google.gwt.user.client.ui.HTMLTable::bodyElem = newBody; }-*/ ;
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HighlightingInlineHyperlink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HighlightingInlineHyperlink.java
deleted file mode 100644
index f8e382a..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HighlightingInlineHyperlink.java
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.ui;
-
-public class HighlightingInlineHyperlink extends InlineHyperlink {
-
-  private String toHighlight;
-
-  public HighlightingInlineHyperlink(final String text, String token, String toHighlight) {
-    super(text, token);
-    this.toHighlight = toHighlight;
-    highlight(text, toHighlight);
-  }
-
-  @Override
-  public void setText(String text) {
-    super.setText(text);
-    highlight(text, toHighlight);
-  }
-
-  private void highlight(String text, String toHighlight) {
-    setHTML(Util.highlight(text, toHighlight));
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HighlightingProjectsTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HighlightingProjectsTable.java
deleted file mode 100644
index 1e3be3f..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HighlightingProjectsTable.java
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.ui;
-
-import com.google.gerrit.client.projects.ProjectInfo;
-import com.google.gerrit.client.projects.ProjectMap;
-import com.google.gwt.user.client.ui.InlineHTML;
-
-public class HighlightingProjectsTable extends ProjectsTable {
-  private String toHighlight;
-
-  public void display(ProjectMap projects, String toHighlight) {
-    this.toHighlight = toHighlight;
-    super.display(projects);
-  }
-
-  @Override
-  protected void populate(int row, ProjectInfo k) {
-    populateState(row, k);
-    table.setWidget(
-        row, ProjectsTable.C_NAME, new InlineHTML(Util.highlight(k.name(), toHighlight)));
-    table.setText(row, ProjectsTable.C_DESCRIPTION, k.description());
-
-    setRowItem(row, k);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HintTextBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HintTextBox.java
deleted file mode 100644
index 4ccfe9d..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HintTextBox.java
+++ /dev/null
@@ -1,218 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT 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.client.ui;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gwt.event.dom.client.BlurEvent;
-import com.google.gwt.event.dom.client.BlurHandler;
-import com.google.gwt.event.dom.client.FocusEvent;
-import com.google.gwt.event.dom.client.FocusHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyDownEvent;
-import com.google.gwt.event.dom.client.KeyDownHandler;
-import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.user.client.ui.SuggestBox;
-import com.google.gwt.user.client.ui.SuggestBox.DefaultSuggestionDisplay;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtexpui.globalkey.client.NpTextBox;
-
-public class HintTextBox extends NpTextBox {
-  private HandlerRegistration hintFocusHandler;
-  private HandlerRegistration hintBlurHandler;
-  private HandlerRegistration keyDownHandler;
-
-  private String hintText;
-  private String hintStyleName = Gerrit.RESOURCES.css().inputFieldTypeHint();
-
-  private String prevText;
-
-  private boolean hintOn;
-  private boolean isFocused;
-
-  @Override
-  public String getText() {
-    if (hintOn) {
-      return "";
-    }
-    return super.getText();
-  }
-
-  @Override
-  public void setText(String text) {
-    focusHint();
-
-    super.setText(text);
-    prevText = text;
-
-    if (!isFocused) {
-      blurHint();
-    }
-  }
-
-  public String getHintText() {
-    return hintText;
-  }
-
-  public void setHintText(String text) {
-    if (text == null) {
-      if (hintText == null) { // was not set, still not set, no change.
-        return;
-      }
-
-      // Clearing a previously set Hint
-      hintFocusHandler.removeHandler();
-      hintFocusHandler = null;
-      hintBlurHandler.removeHandler();
-      hintBlurHandler = null;
-      keyDownHandler.removeHandler();
-      keyDownHandler = null;
-      hintText = null;
-      focusHint();
-
-      return;
-    }
-
-    // Setting Hints
-
-    if (hintText == null) { // first time (was not already set)
-      hintText = text;
-
-      hintFocusHandler =
-          addFocusHandler(
-              new FocusHandler() {
-                @Override
-                public void onFocus(FocusEvent event) {
-                  focusHint();
-                  prevText = getText();
-                  isFocused = true;
-                }
-              });
-
-      hintBlurHandler =
-          addBlurHandler(
-              new BlurHandler() {
-                @Override
-                public void onBlur(BlurEvent event) {
-                  blurHint();
-                  isFocused = false;
-                }
-              });
-
-      /*
-       * There seems to be a strange bug (at least on firefox 3.5.9 ubuntu) with
-       * the textbox under the following circumstances:
-       *  1) The field is not focused with BText in it.
-       *  2) The field receives focus and a focus listener changes the text to FText
-       *  3) The ESC key is pressed and the value of the field has not changed
-       *     (ever) from FText
-       *  4) BUG: The text value gets reset to BText!
-       *
-       *  A counter to this bug seems to be to force setFocus(false) on ESC.
-       */
-
-      /* Chrome does not create a KeyPressEvent on ESC, so use KeyDownEvents */
-      keyDownHandler =
-          addKeyDownHandler(
-              new KeyDownHandler() {
-                @Override
-                public void onKeyDown(KeyDownEvent event) {
-                  onKey(event.getNativeKeyCode());
-                }
-              });
-
-    } else { // Changing an already set Hint
-
-      focusHint();
-      hintText = text;
-    }
-
-    if (!isFocused) {
-      blurHint();
-    }
-  }
-
-  private void onKey(int key) {
-    if (key == KeyCodes.KEY_ESCAPE) {
-      setText(prevText);
-
-      Widget p = getParent();
-      if (p instanceof SuggestBox) {
-        // Since the text was changed, ensure that the SuggestBox is
-        // aware of this change so that it will refresh properly on
-        // the next keystroke.  Without this, if the first keystroke
-        // recreates the same string as before ESC was pressed, the
-        // SuggestBox will think that the string has not changed, and
-        // it will not yet provide any Suggestions.
-        ((SuggestBox) p).showSuggestionList();
-
-        // The suggestion list lingers if we don't hide it.
-        ((DefaultSuggestionDisplay) ((SuggestBox) p).getSuggestionDisplay()).hideSuggestions();
-      }
-
-      setFocus(false);
-    }
-  }
-
-  public void setHintStyleName(String styleName) {
-    if (hintStyleName != null && hintOn) {
-      removeStyleName(hintStyleName);
-    }
-
-    hintStyleName = styleName;
-
-    if (styleName != null && hintOn) {
-      addStyleName(styleName);
-    }
-  }
-
-  public String getHintStyleName() {
-    return hintStyleName;
-  }
-
-  protected void blurHint() {
-    if (!hintOn && getHintText() != null && "".equals(super.getText())) {
-      hintOn = true;
-      super.setText(getHintText());
-      if (getHintStyleName() != null) {
-        addStyleName(getHintStyleName());
-      }
-    }
-  }
-
-  protected void focusHint() {
-    if (hintOn) {
-      super.setText("");
-      if (getHintStyleName() != null) {
-        removeStyleName(getHintStyleName());
-      }
-      hintOn = false;
-    }
-  }
-
-  @Override
-  public void setFocus(boolean focus) {
-    super.setFocus(focus);
-
-    if (focus != isFocused) {
-      if (focus) {
-        focusHint();
-      } else {
-        blurHint();
-      }
-    }
-
-    isFocused = focus;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Hyperlink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Hyperlink.java
deleted file mode 100644
index c35d097..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Hyperlink.java
+++ /dev/null
@@ -1,68 +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.client.ui;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.Event;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.impl.HyperlinkImpl;
-
-/** Standard GWT hyperlink with late updating of the token. */
-public class Hyperlink extends com.google.gwt.user.client.ui.Hyperlink {
-  public static final HyperlinkImpl impl = GWT.create(HyperlinkImpl.class);
-
-  /** Initialize a default hyperlink with no target and no text. */
-  public Hyperlink() {}
-
-  /**
-   * Creates a hyperlink with its text and target history token specified.
-   *
-   * @param text the hyperlink's text
-   * @param token the history token to which it will link, which may not be null (use {@link Anchor}
-   *     instead if you don't need history processing)
-   */
-  public Hyperlink(String text, String token) {
-    super(text, token);
-  }
-
-  /**
-   * Creates a hyperlink with its text and target history token specified.
-   *
-   * @param text the hyperlink's text
-   * @param asHTML {@code true} to treat the specified text as html
-   * @param token the history token to which it will link
-   * @see #setTargetHistoryToken
-   */
-  public Hyperlink(String text, boolean asHTML, String token) {
-    super(text, asHTML, token);
-  }
-
-  @Override
-  public void onBrowserEvent(Event event) {
-    if (DOM.eventGetType(event) == Event.ONCLICK && impl.handleAsClick(event)) {
-      event.preventDefault();
-      go();
-    } else {
-      super.onBrowserEvent(event);
-    }
-  }
-
-  /** Create the screen and start rendering, updating the browser history. */
-  public void go() {
-    Gerrit.display(getTargetHistoryToken());
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/InlineHyperlink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/InlineHyperlink.java
deleted file mode 100644
index a4edb5b..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/InlineHyperlink.java
+++ /dev/null
@@ -1,52 +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.client.ui;
-
-import static com.google.gerrit.client.ui.Hyperlink.impl;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.Event;
-
-/** Standard GWT hyperlink with late updating of the token. */
-public class InlineHyperlink extends com.google.gwt.user.client.ui.InlineHyperlink {
-  /**
-   * Creates a link with its text and target history token specified.
-   *
-   * @param text the hyperlink's text
-   * @param token the history token to which it will link
-   */
-  public InlineHyperlink(String text, String token) {
-    super(text, token);
-  }
-
-  /** Creates an empty link. */
-  public InlineHyperlink() {}
-
-  @Override
-  public void onBrowserEvent(Event event) {
-    if (DOM.eventGetType(event) == Event.ONCLICK && impl.handleAsClick(event)) {
-      event.preventDefault();
-      go();
-    } else {
-      super.onBrowserEvent(event);
-    }
-  }
-
-  /** Create the screen and start rendering, updating the browser history. */
-  public void go() {
-    Gerrit.display(getTargetHistoryToken());
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuBar.java
deleted file mode 100644
index d3db098..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuBar.java
+++ /dev/null
@@ -1,91 +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.client.ui;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gwt.aria.client.Roles;
-import com.google.gwt.user.client.Command;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.Widget;
-
-public class LinkMenuBar extends Composite implements ScreenLoadHandler {
-  private final FlowPanel body;
-
-  public LinkMenuBar() {
-    body = new FlowPanel();
-    initWidget(body);
-    setStyleName(Gerrit.RESOURCES.css().linkMenuBar());
-    Roles.getMenubarRole().set(getElement());
-    Gerrit.EVENT_BUS.addHandler(ScreenLoadEvent.TYPE, this);
-  }
-
-  public void addItem(String text, Command imp) {
-    add(new CommandMenuItem(text, imp));
-  }
-
-  public void addItem(CommandMenuItem i) {
-    add(i);
-  }
-
-  public void addItem(LinkMenuItem i) {
-    i.setMenuBar(this);
-    add(i);
-  }
-
-  public void insertItem(LinkMenuItem i, int beforeIndex) {
-    i.setMenuBar(this);
-    insert(i, beforeIndex);
-  }
-
-  public void clear() {
-    body.clear();
-  }
-
-  public LinkMenuItem find(String targetToken) {
-    for (Widget w : body) {
-      if (w instanceof LinkMenuItem) {
-        LinkMenuItem m = (LinkMenuItem) w;
-        if (targetToken.equals(m.getTargetHistoryToken())) {
-          return m;
-        }
-      }
-    }
-    return null;
-  }
-
-  public void add(Widget i) {
-    if (body.getWidgetCount() > 0) {
-      final Widget p = body.getWidget(body.getWidgetCount() - 1);
-      p.addStyleName(Gerrit.RESOURCES.css().linkMenuItemNotLast());
-    }
-    body.add(i);
-  }
-
-  public void insert(Widget i, int beforeIndex) {
-    if (body.getWidgetCount() == 0 || body.getWidgetCount() <= beforeIndex) {
-      add(i);
-      return;
-    }
-    body.insert(i, beforeIndex);
-  }
-
-  public int getWidgetIndex(Widget i) {
-    return body.getWidgetIndex(i);
-  }
-
-  @Override
-  public void onScreenLoad(ScreenLoadEvent event) {}
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuItem.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuItem.java
deleted file mode 100644
index 8a8ab25..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuItem.java
+++ /dev/null
@@ -1,54 +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.client.ui;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gwt.aria.client.Roles;
-import com.google.gwt.dom.client.AnchorElement;
-
-public class LinkMenuItem extends InlineHyperlink implements ScreenLoadHandler {
-  private LinkMenuBar bar;
-
-  public LinkMenuItem(String text, String targetHistoryToken) {
-    super(text, targetHistoryToken);
-    setStyleName(Gerrit.RESOURCES.css().menuItem());
-    Roles.getMenuitemRole().set(getElement());
-    Gerrit.EVENT_BUS.addHandler(ScreenLoadEvent.TYPE, this);
-  }
-
-  @Override
-  public void go() {
-    super.go();
-    AnchorElement.as(getElement()).blur();
-  }
-
-  public void setMenuBar(LinkMenuBar bar) {
-    this.bar = bar;
-  }
-
-  @Override
-  public void onScreenLoad(ScreenLoadEvent event) {
-    if (match(event.getScreen().getToken())) {
-      Gerrit.selectMenu(bar);
-      addStyleName(Gerrit.RESOURCES.css().activeRow());
-    } else {
-      removeStyleName(Gerrit.RESOURCES.css().activeRow());
-    }
-  }
-
-  protected boolean match(String token) {
-    return token.equals(getTargetHistoryToken());
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MenuScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MenuScreen.java
deleted file mode 100644
index 0f28ddc..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MenuScreen.java
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT 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.client.ui;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.HorizontalPanel;
-import com.google.gwt.user.client.ui.Widget;
-
-public abstract class MenuScreen extends Screen {
-  private final LinkMenuBar menu;
-  private final FlowPanel body;
-
-  public MenuScreen() {
-    menu = new LinkMenuBar();
-    menu.setStyleName(Gerrit.RESOURCES.css().menuScreenMenuBar());
-    body = new FlowPanel();
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-
-    HorizontalPanel hp = new HorizontalPanel();
-    hp.add(menu);
-    hp.add(body);
-    super.add(hp);
-  }
-
-  @Override
-  public void setToken(String token) {
-    LinkMenuItem self = menu.find(token);
-    if (self != null) {
-      self.addStyleName(Gerrit.RESOURCES.css().activeRow());
-    }
-    super.setToken(token);
-  }
-
-  @Override
-  protected FlowPanel getBody() {
-    return body;
-  }
-
-  @Override
-  protected void add(Widget w) {
-    body.add(w);
-  }
-
-  protected void link(String text, String target) {
-    link(text, target, true);
-  }
-
-  protected void link(String text, String target, boolean visible) {
-    final LinkMenuItem item = new LinkMenuItem(text, target);
-    item.setStyleName(Gerrit.RESOURCES.css().menuItem());
-    item.setVisible(visible);
-    menu.add(item);
-  }
-
-  protected void setLinkVisible(String token, boolean visible) {
-    final LinkMenuItem item = menu.find(token);
-    item.setVisible(visible);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MorphingTabPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MorphingTabPanel.java
deleted file mode 100644
index 7fd6432..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MorphingTabPanel.java
+++ /dev/null
@@ -1,101 +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.client.ui;
-
-import com.google.gwt.event.logical.shared.SelectionEvent;
-import com.google.gwt.event.logical.shared.SelectionHandler;
-import com.google.gwt.user.client.ui.TabPanel;
-import com.google.gwt.user.client.ui.Widget;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A TabPanel which allows entries to be hidden. This class is not yet designed to handle removes or
- * any other add methods than the one overridden here. It is also not designed to handle anything
- * other than text for the tab.
- */
-public class MorphingTabPanel extends TabPanel {
-  // Keep track of the order the widgets/texts should be in when not hidden.
-  private List<Widget> widgets = new ArrayList<>();
-  private List<String> texts = new ArrayList<>();
-
-  // currently visible widgets
-  private List<Widget> visibles = new ArrayList<>();
-
-  private int selection;
-
-  public MorphingTabPanel() {
-    addSelectionHandler(
-        new SelectionHandler<Integer>() {
-          @Override
-          public void onSelection(SelectionEvent<Integer> ev) {
-            selection = ev.getSelectedItem();
-          }
-        });
-  }
-
-  public int getSelectedIndex() {
-    return selection;
-  }
-
-  public Widget getSelectedWidget() {
-    return getWidget(getSelectedIndex());
-  }
-
-  @Override
-  public void clear() {
-    super.clear();
-    widgets.clear();
-    texts.clear();
-    visibles.clear();
-  }
-
-  @Override
-  public void add(Widget w, String tabText) {
-    addInvisible(w, tabText);
-    visibles.add(w);
-    super.add(w, tabText);
-  }
-
-  public void addInvisible(Widget w, String tabText) {
-    widgets.add(w);
-    texts.add(tabText);
-  }
-
-  public void setVisible(Widget w, boolean visible) {
-    if (visible) {
-      if (!visibles.contains(w)) {
-        int origPos = widgets.indexOf(w);
-
-        /* Re-insert the widget right after the first visible widget found
-        when scanning backwards from the current widget */
-        for (int pos = origPos - 1; pos >= 0; pos--) {
-          int visiblePos = visibles.indexOf(widgets.get(pos));
-          if (visiblePos != -1) {
-            visibles.add(visiblePos + 1, w);
-            insert(w, texts.get(origPos), visiblePos + 1);
-            break;
-          }
-        }
-      }
-    } else {
-      int i = visibles.indexOf(w);
-      if (i != -1) {
-        visibles.remove(i);
-        remove(i);
-      }
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MoveDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MoveDialog.java
deleted file mode 100644
index 3821e93..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MoveDialog.java
+++ /dev/null
@@ -1,107 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.ui;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.Util;
-import com.google.gerrit.client.projects.BranchInfo;
-import com.google.gerrit.client.projects.ProjectApi;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.SuggestBox;
-import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
-import java.util.ArrayList;
-import java.util.List;
-
-public abstract class MoveDialog extends TextAreaActionDialog {
-  private SuggestBox newBranch;
-  private List<BranchInfo> branches;
-
-  public MoveDialog(Project.NameKey project) {
-    super(Util.C.moveTitle(), Util.C.moveChangeMessage());
-    ProjectApi.getBranches(
-        project,
-        new GerritCallback<JsArray<BranchInfo>>() {
-          @Override
-          public void onSuccess(JsArray<BranchInfo> result) {
-            branches = Natives.asList(result);
-          }
-        });
-
-    newBranch =
-        new SuggestBox(
-            new HighlightSuggestOracle() {
-              @Override
-              protected void onRequestSuggestions(Request request, Callback done) {
-                List<BranchSuggestion> suggestions = new ArrayList<>();
-                for (BranchInfo b : branches) {
-                  if (b.ref().contains(request.getQuery())) {
-                    suggestions.add(new BranchSuggestion(b));
-                  }
-                }
-                done.onSuggestionsReady(request, new Response(suggestions));
-              }
-            });
-
-    newBranch.setWidth("100%");
-    newBranch.getElement().getStyle().setProperty("boxSizing", "border-box");
-    message.setCharacterWidth(70);
-
-    FlowPanel mwrap = new FlowPanel();
-    mwrap.setStyleName(Gerrit.RESOURCES.css().commentedActionMessage());
-    mwrap.add(newBranch);
-
-    panel.insert(mwrap, 0);
-    panel.insert(new SmallHeading(Util.C.headingMoveBranch()), 0);
-  }
-
-  @Override
-  public void center() {
-    super.center();
-    GlobalKey.dialog(this);
-    newBranch.setFocus(true);
-  }
-
-  public String getDestinationBranch() {
-    return newBranch.getText();
-  }
-
-  static class BranchSuggestion implements Suggestion {
-    private BranchInfo branch;
-
-    BranchSuggestion(BranchInfo branch) {
-      this.branch = branch;
-    }
-
-    @Override
-    public String getDisplayString() {
-      String refsHeads = "refs/heads/";
-      if (branch.ref().startsWith(refsHeads)) {
-        return branch.ref().substring(refsHeads.length());
-      }
-      return branch.ref();
-    }
-
-    @Override
-    public String getReplacementString() {
-      return branch.getShortName();
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NavigationTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NavigationTable.java
deleted file mode 100644
index b2306a6..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NavigationTable.java
+++ /dev/null
@@ -1,408 +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.client.ui;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gwt.dom.client.Document;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.Event;
-import com.google.gwt.user.client.Window;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwt.user.client.ui.ScrollPanel;
-import com.google.gwt.user.client.ui.UIObject;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-import com.google.gwtexpui.globalkey.client.KeyCommand;
-import com.google.gwtexpui.globalkey.client.KeyCommandSet;
-import com.google.gwtexpui.safehtml.client.SafeHtml;
-import java.util.LinkedHashMap;
-import java.util.Map.Entry;
-
-public abstract class NavigationTable<RowItem> extends FancyFlexTable<RowItem> {
-  protected class MyFlexTable extends FancyFlexTable.MyFlexTable {
-    public MyFlexTable() {
-      sinkEvents(Event.ONDBLCLICK | Event.ONCLICK);
-    }
-
-    @Override
-    public void onBrowserEvent(Event event) {
-      switch (DOM.eventGetType(event)) {
-        case Event.ONCLICK:
-          {
-            // Find out which cell was actually clicked.
-            final Element td = getEventTargetCell(event);
-            if (td == null) {
-              break;
-            }
-            final int row = rowOf(td);
-            if (getRowItem(row) != null) {
-              onCellSingleClick(event, rowOf(td), columnOf(td));
-              return;
-            }
-            break;
-          }
-        case Event.ONDBLCLICK:
-          {
-            // Find out which cell was actually clicked.
-            Element td = getEventTargetCell(event);
-            if (td == null) {
-              return;
-            }
-            onCellDoubleClick(rowOf(td), columnOf(td));
-            return;
-          }
-      }
-      super.onBrowserEvent(event);
-    }
-  }
-
-  private static final LinkedHashMap<String, Object> savedPositions =
-      new LinkedHashMap<String, Object>(10, 0.75f, true) {
-        private static final long serialVersionUID = 1L;
-
-        @Override
-        protected boolean removeEldestEntry(Entry<String, Object> eldest) {
-          return size() >= 20;
-        }
-      };
-
-  private final Image pointer;
-  protected final KeyCommandSet keysNavigation;
-  protected final KeyCommandSet keysAction;
-  private HandlerRegistration regNavigation;
-  private HandlerRegistration regAction;
-  private int currentRow = -1;
-  private String saveId;
-
-  private boolean computedScrollType;
-  private ScrollPanel parentScrollPanel;
-
-  protected NavigationTable(String itemHelpName) {
-    this();
-    keysNavigation.add(
-        new PrevKeyCommand(0, 'k', Util.M.helpListPrev(itemHelpName)),
-        new NextKeyCommand(0, 'j', Util.M.helpListNext(itemHelpName)));
-    keysNavigation.add(new OpenKeyCommand(0, 'o', Util.M.helpListOpen(itemHelpName)));
-    keysNavigation.add(
-        new OpenKeyCommand(0, KeyCodes.KEY_ENTER, Util.M.helpListOpen(itemHelpName)));
-  }
-
-  protected NavigationTable() {
-    pointer = new Image(Gerrit.RESOURCES.arrowRight());
-    keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
-    keysAction = new KeyCommandSet(Gerrit.C.sectionActions());
-  }
-
-  protected abstract void onOpenRow(int row);
-
-  protected abstract Object getRowItemKey(RowItem item);
-
-  private void onUp() {
-    for (int row = currentRow - 1; row >= 0; row--) {
-      if (getRowItem(row) != null) {
-        movePointerTo(row);
-        break;
-      }
-    }
-  }
-
-  private void onDown() {
-    final int max = table.getRowCount();
-    for (int row = currentRow + 1; row < max; row++) {
-      if (getRowItem(row) != null) {
-        movePointerTo(row);
-        break;
-      }
-    }
-  }
-
-  private void onOpen() {
-    if (0 <= currentRow && currentRow < table.getRowCount()) {
-      if (getRowItem(currentRow) != null) {
-        onOpenRow(currentRow);
-      }
-    }
-  }
-
-  /**
-   * Invoked when the user double clicks on a table cell.
-   *
-   * @param row row number.
-   * @param column column number.
-   */
-  protected void onCellDoubleClick(int row, int column) {
-    onOpenRow(row);
-  }
-
-  /**
-   * Invoked when the user clicks on a table cell.
-   *
-   * @param event click event.
-   * @param row row number.
-   * @param column column number.
-   */
-  protected void onCellSingleClick(Event event, int row, int column) {
-    movePointerTo(row);
-  }
-
-  protected int getCurrentRow() {
-    return currentRow;
-  }
-
-  protected void ensurePointerVisible() {
-    final int max = table.getRowCount();
-    int row = currentRow;
-    final int init = row;
-    if (row < 0) {
-      row = 0;
-    } else if (max <= row) {
-      row = max - 1;
-    }
-
-    final CellFormatter fmt = table.getCellFormatter();
-    final int sTop = Document.get().getScrollTop();
-    final int sEnd = sTop + Document.get().getClientHeight();
-
-    while (0 <= row && row < max) {
-      final Element cur = fmt.getElement(row, C_ARROW).getParentElement();
-      final int cTop = cur.getAbsoluteTop();
-      final int cEnd = cTop + cur.getOffsetHeight();
-
-      if (cEnd < sTop) {
-        row++;
-      } else if (sEnd < cTop) {
-        row--;
-      } else {
-        break;
-      }
-    }
-
-    if (init != row) {
-      movePointerTo(row, false);
-    }
-  }
-
-  protected void movePointerTo(int newRow) {
-    movePointerTo(newRow, true);
-  }
-
-  protected void movePointerTo(int newRow, boolean scroll) {
-    final CellFormatter fmt = table.getCellFormatter();
-    final boolean clear = 0 <= currentRow && currentRow < table.getRowCount();
-    if (clear) {
-      final Element tr = fmt.getElement(currentRow, C_ARROW).getParentElement();
-      UIObject.setStyleName(tr, Gerrit.RESOURCES.css().activeRow(), false);
-    }
-    if (0 <= newRow && newRow < table.getRowCount() && getRowItem(newRow) != null) {
-      table.setWidget(newRow, C_ARROW, pointer);
-      final Element tr = fmt.getElement(newRow, C_ARROW).getParentElement();
-      UIObject.setStyleName(tr, Gerrit.RESOURCES.css().activeRow(), true);
-      if (scroll && isAttached()) {
-        scrollIntoView(tr);
-      }
-    } else if (clear) {
-      table.setWidget(currentRow, C_ARROW, null);
-      pointer.removeFromParent();
-    }
-    currentRow = newRow;
-  }
-
-  protected void scrollIntoView(Element tr) {
-    if (!computedScrollType) {
-      parentScrollPanel = null;
-      Widget w = getParent();
-      while (w != null) {
-        if (w instanceof ScrollPanel) {
-          parentScrollPanel = (ScrollPanel) w;
-          break;
-        }
-        w = w.getParent();
-      }
-      computedScrollType = true;
-    }
-
-    if (parentScrollPanel != null) {
-      parentScrollPanel.ensureVisible(
-          new UIObject() {
-            {
-              setElement(tr);
-            }
-          });
-    } else {
-      int rt = tr.getAbsoluteTop();
-      int rl = tr.getAbsoluteLeft();
-      int rb = tr.getAbsoluteBottom();
-
-      int wt = Window.getScrollTop();
-      int wl = Window.getScrollLeft();
-
-      int wh = Window.getClientHeight();
-      int ww = Window.getClientWidth();
-      int wb = wt + wh;
-
-      // If the row is partially or fully obscured, scroll:
-      //
-      // rl < wl: Row left edge is off screen to left.
-      // rt < wt: Row top is above top of window.
-      // wb < rt: Row top is below bottom of window.
-      // wb < rb: Row bottom is below bottom of window.
-      if (rl < wl || rt < wt || wb < rt || wb < rb) {
-        if (rl < wl) {
-          // Left edge needs to move to make it visible.
-          // If the row fully fits in the window, set 0.
-          if (tr.getAbsoluteRight() < ww) {
-            wl = 0;
-          } else {
-            wl = Math.max(tr.getAbsoluteLeft() - 5, 0);
-          }
-        }
-
-        // Vertically center the row in the window.
-        int h = (wh - (rb - rt)) / 2;
-        Window.scrollTo(wl, Math.max(rt - h, 0));
-      }
-    }
-  }
-
-  protected void movePointerTo(Object oldId) {
-    final int row = findRow(oldId);
-    if (0 <= row) {
-      movePointerTo(row);
-    }
-  }
-
-  protected int findRow(Object oldId) {
-    if (oldId != null) {
-      final int max = table.getRowCount();
-      for (int row = 0; row < max; row++) {
-        final RowItem c = getRowItem(row);
-        if (c != null && oldId.equals(getRowItemKey(c))) {
-          return row;
-        }
-      }
-    }
-    return -1;
-  }
-
-  @Override
-  public void resetHtml(SafeHtml body) {
-    currentRow = -1;
-    super.resetHtml(body);
-  }
-
-  public void finishDisplay() {
-    if (currentRow >= table.getRowCount()) {
-      currentRow = -1;
-    }
-    if (saveId != null) {
-      movePointerTo(savedPositions.get(saveId));
-    }
-    if (currentRow < 0) {
-      onDown();
-    }
-  }
-
-  public void setSavePointerId(String id) {
-    saveId = id;
-  }
-
-  public void setRegisterKeys(boolean on) {
-    if (on && isAttached()) {
-      if (regNavigation == null) {
-        regNavigation = GlobalKey.add(this, keysNavigation);
-      }
-      if (regAction == null) {
-        regAction = GlobalKey.add(this, keysAction);
-      }
-    } else {
-      if (regNavigation != null) {
-        regNavigation.removeHandler();
-        regNavigation = null;
-      }
-      if (regAction != null) {
-        regAction.removeHandler();
-        regAction = null;
-      }
-    }
-  }
-
-  @Override
-  protected void onLoad() {
-    computedScrollType = false;
-    parentScrollPanel = null;
-  }
-
-  @Override
-  protected void onUnload() {
-    setRegisterKeys(false);
-
-    if (saveId != null && currentRow >= 0) {
-      final RowItem c = getRowItem(currentRow);
-      if (c != null) {
-        savedPositions.put(saveId, getRowItemKey(c));
-      }
-    }
-
-    computedScrollType = false;
-    parentScrollPanel = null;
-    super.onUnload();
-  }
-
-  @Override
-  protected MyFlexTable createFlexTable() {
-    return new MyFlexTable();
-  }
-
-  public class PrevKeyCommand extends KeyCommand {
-    public PrevKeyCommand(int mask, char key, String help) {
-      super(mask, key, help);
-    }
-
-    @Override
-    public void onKeyPress(KeyPressEvent event) {
-      ensurePointerVisible();
-      onUp();
-    }
-  }
-
-  public class NextKeyCommand extends KeyCommand {
-    public NextKeyCommand(int mask, char key, String help) {
-      super(mask, key, help);
-    }
-
-    @Override
-    public void onKeyPress(KeyPressEvent event) {
-      ensurePointerVisible();
-      onDown();
-    }
-  }
-
-  public class OpenKeyCommand extends KeyCommand {
-    public OpenKeyCommand(int mask, int key, String help) {
-      super(mask, key, help);
-    }
-
-    @Override
-    public void onKeyPress(KeyPressEvent event) {
-      ensurePointerVisible();
-      onOpen();
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NeedsSignInKeyCommand.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NeedsSignInKeyCommand.java
deleted file mode 100644
index 4420762..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NeedsSignInKeyCommand.java
+++ /dev/null
@@ -1,27 +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.client.ui;
-
-import com.google.gwtexpui.globalkey.client.KeyCommand;
-
-public abstract class NeedsSignInKeyCommand extends KeyCommand {
-  public NeedsSignInKeyCommand(int mask, int key, String help) {
-    super(mask, key, help);
-  }
-
-  public NeedsSignInKeyCommand(int mask, char key, String help) {
-    super(mask, key, help);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NpIntTextBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NpIntTextBox.java
deleted file mode 100644
index 8be6647..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NpIntTextBox.java
+++ /dev/null
@@ -1,96 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT 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.client.ui;
-
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyDownEvent;
-import com.google.gwt.event.dom.client.KeyDownHandler;
-import com.google.gwt.event.dom.client.KeyEvent;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwtexpui.globalkey.client.NpTextBox;
-
-/** Text box that accepts only integer values. */
-public class NpIntTextBox extends NpTextBox {
-  private int intValue;
-
-  public NpIntTextBox() {
-    init();
-  }
-
-  private void init() {
-    addKeyDownHandler(
-        new KeyDownHandler() {
-          @Override
-          public void onKeyDown(KeyDownEvent event) {
-            int code = event.getNativeKeyCode();
-            onKey(event, code, code);
-          }
-        });
-    addKeyPressHandler(
-        new KeyPressHandler() {
-          @Override
-          public void onKeyPress(KeyPressEvent event) {
-            int charCode = event.getCharCode();
-            int keyCode = event.getNativeEvent().getKeyCode();
-            onKey(event, charCode, keyCode);
-          }
-        });
-  }
-
-  private void onKey(KeyEvent<?> event, int charCode, int keyCode) {
-    if ('0' <= charCode && charCode <= '9') {
-      if (event.isAnyModifierKeyDown()) {
-        event.preventDefault();
-      }
-    } else {
-      switch (keyCode) {
-        case KeyCodes.KEY_BACKSPACE:
-        case KeyCodes.KEY_LEFT:
-        case KeyCodes.KEY_RIGHT:
-        case KeyCodes.KEY_HOME:
-        case KeyCodes.KEY_END:
-        case KeyCodes.KEY_TAB:
-        case KeyCodes.KEY_DELETE:
-          break;
-
-        default:
-          // Allow copy and paste using ctl-c/ctrl-v,
-          // or whatever the platform's convention is.
-          if (!(event.isControlKeyDown() || event.isMetaKeyDown() || event.isAltKeyDown())) {
-            event.preventDefault();
-          }
-          break;
-      }
-    }
-  }
-
-  public int getIntValue() {
-    String txt = getText().trim();
-    if (!txt.isEmpty()) {
-      try {
-        intValue = Integer.parseInt(getText());
-      } catch (NumberFormatException e) {
-        // Ignored
-      }
-    }
-    return intValue;
-  }
-
-  public void setIntValue(int v) {
-    intValue = v;
-    setText(Integer.toString(v));
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java
deleted file mode 100644
index 2c7fcd4..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java
+++ /dev/null
@@ -1,192 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT 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.client.ui;
-
-import com.google.gwt.core.client.Scheduler;
-import com.google.gwt.core.client.Scheduler.ScheduledCommand;
-import com.google.gwt.event.dom.client.ChangeEvent;
-import com.google.gwt.event.dom.client.ChangeHandler;
-import com.google.gwt.event.dom.client.FocusEvent;
-import com.google.gwt.event.dom.client.FocusHandler;
-import com.google.gwt.event.dom.client.KeyDownEvent;
-import com.google.gwt.event.dom.client.KeyDownHandler;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwt.event.dom.client.MouseUpEvent;
-import com.google.gwt.event.dom.client.MouseUpHandler;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.logical.shared.ValueChangeHandler;
-import com.google.gwt.event.shared.GwtEvent;
-import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.FocusWidget;
-import com.google.gwt.user.client.ui.ListBox;
-import com.google.gwt.user.client.ui.TextBoxBase;
-import com.google.gwt.user.client.ui.ValueBoxBase;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * Enables a FocusWidget (e.g. a Button) if an edit is detected from any registered input widget.
- */
-public class OnEditEnabler
-    implements KeyPressHandler,
-        KeyDownHandler,
-        MouseUpHandler,
-        ChangeHandler,
-        ValueChangeHandler<Object> {
-
-  private final FocusWidget widget;
-  private Map<TextBoxBase, String> strings = new HashMap<>();
-  private String originalValue;
-
-  // The first parameter to the contructors must be the FocusWidget to enable,
-  // subsequent parameters are widgets to listenTo.
-
-  public OnEditEnabler(FocusWidget w, TextBoxBase tb) {
-    this(w);
-    originalValue = tb.getValue().trim();
-    listenTo(tb);
-  }
-
-  public OnEditEnabler(FocusWidget w, ListBox lb) {
-    this(w);
-    listenTo(lb);
-  }
-
-  public OnEditEnabler(FocusWidget w, CheckBox cb) {
-    this(w);
-    listenTo(cb);
-  }
-
-  public OnEditEnabler(FocusWidget w) {
-    widget = w;
-  }
-
-  public void updateOriginalValue(TextBoxBase tb) {
-    originalValue = tb.getValue().trim();
-  }
-
-  // Register input widgets to be listened to
-
-  public void listenTo(TextBoxBase tb) {
-    strings.put(tb, tb.getText().trim());
-    tb.addKeyPressHandler(this);
-
-    // Is there another way to capture middle button X11 pastes in browsers
-    // which do not yet support ONPASTE events (Firefox)?
-    tb.addMouseUpHandler(this);
-
-    // Resetting the "original text" on focus ensures that we are
-    // up to date with non-user updates of the text (calls to
-    // setText()...) and also up to date with user changes which
-    // occurred after enabling "widget".
-    tb.addFocusHandler(
-        new FocusHandler() {
-          @Override
-          public void onFocus(FocusEvent event) {
-            strings.put(tb, tb.getText().trim());
-          }
-        });
-
-    // CTRL-V Pastes in Chrome seem only detectable via BrowserEvents or
-    // KeyDownEvents, the latter is better.
-    tb.addKeyDownHandler(this);
-  }
-
-  public void listenTo(ListBox lb) {
-    lb.addChangeHandler(this);
-  }
-
-  @SuppressWarnings({"unchecked", "rawtypes"})
-  public void listenTo(CheckBox cb) {
-    cb.addValueChangeHandler((ValueChangeHandler) this);
-  }
-
-  // Handlers
-
-  @Override
-  public void onKeyPress(KeyPressEvent e) {
-    on(e);
-  }
-
-  @Override
-  public void onKeyDown(KeyDownEvent e) {
-    on(e);
-  }
-
-  @Override
-  public void onMouseUp(MouseUpEvent e) {
-    on(e);
-  }
-
-  @Override
-  public void onChange(ChangeEvent e) {
-    on(e);
-  }
-
-  @SuppressWarnings("rawtypes")
-  @Override
-  public void onValueChange(ValueChangeEvent e) {
-    on(e);
-  }
-
-  private void on(GwtEvent<?> e) {
-    if (widget.isEnabled()
-        || !(e.getSource() instanceof FocusWidget)
-        || !((FocusWidget) e.getSource()).isEnabled()) {
-      if (e.getSource() instanceof ValueBoxBase) {
-        final TextBoxBase box = ((TextBoxBase) e.getSource());
-        Scheduler.get()
-            .scheduleDeferred(
-                new ScheduledCommand() {
-                  @Override
-                  public void execute() {
-                    if (box.getValue().trim().equals(originalValue)) {
-                      widget.setEnabled(false);
-                    }
-                  }
-                });
-      }
-      return;
-    }
-
-    if (e.getSource() instanceof TextBoxBase) {
-      onTextBoxBase((TextBoxBase) e.getSource());
-    } else {
-      // For many widgets, we can assume that a change is an edit. If
-      // a widget does not work that way, it should be special cased
-      // above.
-      widget.setEnabled(true);
-    }
-  }
-
-  private void onTextBoxBase(TextBoxBase tb) {
-    // The text appears to not get updated until the handlers complete.
-    Scheduler.get()
-        .scheduleDeferred(
-            new ScheduledCommand() {
-              @Override
-              public void execute() {
-                String orig = strings.get(tb);
-                if (orig == null) {
-                  orig = "";
-                }
-                if (!orig.equals(tb.getText().trim())) {
-                  widget.setEnabled(true);
-                }
-              }
-            });
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/PagingHyperlink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/PagingHyperlink.java
deleted file mode 100644
index 7f0ef68..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/PagingHyperlink.java
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.ui;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.admin.AdminConstants;
-
-public class PagingHyperlink extends Hyperlink {
-
-  public static PagingHyperlink createPrev() {
-    return new PagingHyperlink(AdminConstants.I.pagedListPrev());
-  }
-
-  public static PagingHyperlink createNext() {
-    return new PagingHyperlink(AdminConstants.I.pagedListNext());
-  }
-
-  private PagingHyperlink(String text) {
-    super(text, true, "");
-    setStyleName(Gerrit.RESOURCES.css().pagingLink());
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ParentProjectBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ParentProjectBox.java
deleted file mode 100644
index 7c45a20..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ParentProjectBox.java
+++ /dev/null
@@ -1,100 +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.client.ui;
-
-import com.google.gerrit.client.projects.ProjectApi;
-import com.google.gerrit.client.projects.ProjectInfo;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.Composite;
-import java.util.HashSet;
-import java.util.Set;
-
-public class ParentProjectBox extends Composite {
-  private final RemoteSuggestBox suggestBox;
-  private final ParentProjectNameSuggestOracle suggestOracle;
-
-  public ParentProjectBox() {
-    suggestOracle = new ParentProjectNameSuggestOracle();
-    suggestBox = new RemoteSuggestBox(suggestOracle);
-    initWidget(suggestBox);
-  }
-
-  public void setVisibleLength(int len) {
-    suggestBox.setVisibleLength(len);
-  }
-
-  public void setProject(Project.NameKey project) {
-    suggestOracle.setProject(project);
-  }
-
-  public void setParentProject(Project.NameKey parent) {
-    suggestBox.setText(parent != null ? parent.get() : "");
-  }
-
-  public Project.NameKey getParentProjectName() {
-    final String projectName = suggestBox.getText().trim();
-    if (projectName.isEmpty()) {
-      return null;
-    }
-    return new Project.NameKey(projectName);
-  }
-
-  private static class ParentProjectNameSuggestOracle extends ProjectNameSuggestOracle {
-    private Set<String> exclude = new HashSet<>();
-
-    public void setProject(Project.NameKey project) {
-      exclude.clear();
-      exclude.add(project.get());
-      ProjectApi.getChildren(
-          project,
-          true,
-          new AsyncCallback<JsArray<ProjectInfo>>() {
-            @Override
-            public void onSuccess(JsArray<ProjectInfo> result) {
-              for (ProjectInfo p : Natives.asList(result)) {
-                exclude.add(p.name());
-              }
-            }
-
-            @Override
-            public void onFailure(Throwable caught) {}
-          });
-    }
-
-    @Override
-    public void _onRequestSuggestions(Request req, Callback callback) {
-      super._onRequestSuggestions(
-          req,
-          new Callback() {
-            @Override
-            public void onSuggestionsReady(Request request, Response response) {
-              if (exclude.size() > 0) {
-                Set<Suggestion> filteredSuggestions = new HashSet<>(response.getSuggestions());
-                for (Suggestion s : response.getSuggestions()) {
-                  if (exclude.contains(s.getReplacementString())) {
-                    filteredSuggestions.remove(s);
-                  }
-                }
-                response.setSuggestions(filteredSuggestions);
-              }
-              callback.onSuggestionsReady(request, response);
-            }
-          });
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectLink.java
deleted file mode 100644
index c5b609a..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectLink.java
+++ /dev/null
@@ -1,29 +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.client.ui;
-
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Project;
-
-/** Link to the default dashboard of a project. */
-public class ProjectLink extends InlineHyperlink {
-  public ProjectLink(Project.NameKey proj) {
-    this(proj.get(), proj);
-  }
-
-  public ProjectLink(String text, Project.NameKey proj) {
-    super(text, PageLinks.toProjectDefaultDashboard(proj));
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectLinkMenuItem.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectLinkMenuItem.java
deleted file mode 100644
index 114f794..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectLinkMenuItem.java
+++ /dev/null
@@ -1,51 +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.client.ui;
-
-import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.client.admin.ProjectScreen;
-import com.google.gerrit.reviewdb.client.Project;
-
-public class ProjectLinkMenuItem extends LinkMenuItem {
-  protected final String panel;
-
-  public ProjectLinkMenuItem(String text, String panel) {
-    super(text, "");
-    this.panel = panel;
-  }
-
-  @Override
-  public void onScreenLoad(ScreenLoadEvent event) {
-    Screen screen = event.getScreen();
-    Project.NameKey projectKey;
-    if (screen instanceof ProjectScreen) {
-      projectKey = ((ProjectScreen) screen).getProjectKey();
-    } else {
-      projectKey = ProjectScreen.getSavedKey();
-    }
-
-    if (projectKey != null) {
-      setVisible(true);
-      onScreenLoad(projectKey);
-    } else {
-      setVisible(false);
-    }
-    super.onScreenLoad(event);
-  }
-
-  protected void onScreenLoad(Project.NameKey project) {
-    setTargetHistoryToken(Dispatcher.toProjectAdmin(project, panel));
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectListPopup.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectListPopup.java
deleted file mode 100644
index 89bff71..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectListPopup.java
+++ /dev/null
@@ -1,233 +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.client.ui;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.account.Util;
-import com.google.gerrit.client.projects.ProjectMap;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyUpEvent;
-import com.google.gwt.event.dom.client.KeyUpHandler;
-import com.google.gwt.user.client.Window;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.DialogBox;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.HorizontalPanel;
-import com.google.gwt.user.client.ui.Label;
-import com.google.gwt.user.client.ui.PopupPanel;
-import com.google.gwt.user.client.ui.ScrollPanel;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-import com.google.gwtexpui.globalkey.client.HidePopupPanelCommand;
-import com.google.gwtexpui.globalkey.client.NpTextBox;
-
-/** A popup containing all projects. */
-public class ProjectListPopup {
-  private HighlightingProjectsTable projectsTab;
-  private DialogBox popup;
-  private NpTextBox filterTxt;
-  private HorizontalPanel filterPanel;
-  private String match;
-  private Query query;
-  private Button closeTop;
-  private Button closeBottom;
-  private ScrollPanel sp;
-  private PopupPanel.PositionCallback popupPosition;
-  private int preferredTop;
-  private int preferredLeft;
-  private boolean poppingUp;
-  private boolean firstPopupLoad = true;
-
-  public void initPopup(String popupText, String currentPageLink) {
-    createWidgets(popupText, currentPageLink);
-    final FlowPanel pfp = new FlowPanel();
-    pfp.add(filterPanel);
-    pfp.add(closeTop);
-    sp = new ScrollPanel(projectsTab);
-    sp.setSize("100%", "100%");
-    pfp.add(sp);
-    pfp.add(closeBottom);
-    popup.setWidget(pfp);
-    popup.setHeight("100%");
-    popupPosition = getPositionCallback();
-  }
-
-  protected PopupPanel.PositionCallback getPositionCallback() {
-    return new PopupPanel.PositionCallback() {
-      @Override
-      public void setPosition(int offsetWidth, int offsetHeight) {
-        if (preferredTop + offsetHeight > Window.getClientWidth()) {
-          preferredTop = Window.getClientWidth() - offsetHeight;
-        }
-        if (preferredLeft + offsetWidth > Window.getClientWidth()) {
-          preferredLeft = Window.getClientWidth() - offsetWidth;
-        }
-
-        if (preferredTop < 0) {
-          sp.setHeight((sp.getOffsetHeight() + preferredTop) + "px");
-          preferredTop = 0;
-        }
-        if (preferredLeft < 0) {
-          sp.setWidth((sp.getOffsetWidth() + preferredLeft) + "px");
-          preferredLeft = 0;
-        }
-
-        popup.setPopupPosition(preferredLeft, preferredTop);
-      }
-    };
-  }
-
-  /**
-   * Invoked after moving pointer to a project.
-   *
-   * @param projectName project name.
-   */
-  protected void onMovePointerTo(String projectName) {}
-
-  /**
-   * Invoked after opening a project row.
-   *
-   * @param projectName project name.
-   */
-  protected void openRow(String projectName) {}
-
-  public boolean isPoppingUp() {
-    return poppingUp;
-  }
-
-  private void createWidgets(String popupText, String currentPageLink) {
-    filterPanel = new HorizontalPanel();
-    filterPanel.setStyleName(Gerrit.RESOURCES.css().projectFilterPanel());
-    final Label filterLabel =
-        new Label(com.google.gerrit.client.admin.AdminConstants.I.projectFilter());
-    filterLabel.setStyleName(Gerrit.RESOURCES.css().projectFilterLabel());
-    filterPanel.add(filterLabel);
-    filterTxt = new NpTextBox();
-    filterTxt.addKeyUpHandler(
-        new KeyUpHandler() {
-          @Override
-          public void onKeyUp(KeyUpEvent event) {
-            Query q = new Query(filterTxt.getValue());
-            if (!match.equals(q.qMatch)) {
-              if (query == null) {
-                q.run();
-              }
-              query = q;
-            }
-          }
-        });
-    filterPanel.add(filterTxt);
-
-    projectsTab =
-        new HighlightingProjectsTable() {
-          @Override
-          protected void movePointerTo(int row, boolean scroll) {
-            super.movePointerTo(row, scroll);
-            onMovePointerTo(getRowItem(row).name());
-          }
-
-          @Override
-          protected void onOpenRow(int row) {
-            super.onOpenRow(row);
-            openRow(getRowItem(row).name());
-          }
-        };
-    projectsTab.setSavePointerId(currentPageLink);
-
-    closeTop = createCloseButton();
-    closeBottom = createCloseButton();
-
-    popup = new DialogBox();
-    popup.setModal(false);
-    popup.setText(popupText);
-  }
-
-  private Button createCloseButton() {
-    Button close = new Button(Util.C.projectsClose());
-    close.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            closePopup();
-          }
-        });
-    return close;
-  }
-
-  public void displayPopup() {
-    poppingUp = true;
-    if (firstPopupLoad) { // For sizing/positioning, delay display until loaded
-      match = "";
-      query = new Query(match).run();
-    } else {
-      popup.setPopupPositionAndShow(popupPosition);
-      GlobalKey.dialog(popup);
-      GlobalKey.addApplication(popup, new HidePopupPanelCommand(0, KeyCodes.KEY_ESCAPE, popup));
-      projectsTab.setRegisterKeys(true);
-      projectsTab.finishDisplay();
-      filterTxt.setFocus(true);
-      poppingUp = false;
-    }
-  }
-
-  public void closePopup() {
-    popup.hide();
-  }
-
-  public void setPreferredCoordinates(int top, int left) {
-    this.preferredTop = top;
-    this.preferredLeft = left;
-  }
-
-  private class Query {
-    private final String qMatch;
-
-    Query(String match) {
-      this.qMatch = match;
-    }
-
-    Query run() {
-      ProjectMap.match(
-          qMatch,
-          new GerritCallback<ProjectMap>() {
-            @Override
-            public void onSuccess(ProjectMap result) {
-              if (!firstPopupLoad && !popup.isShowing()) {
-                query = null;
-              } else if (query == Query.this) {
-                query = null;
-                showMap(result);
-              } else {
-                query.run();
-              }
-            }
-          });
-      return this;
-    }
-
-    private void showMap(ProjectMap result) {
-      ProjectListPopup.this.match = qMatch;
-      projectsTab.display(result, qMatch);
-
-      if (firstPopupLoad) {
-        // Display was delayed until table was loaded
-        firstPopupLoad = false;
-        displayPopup();
-      }
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectNameSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectNameSuggestOracle.java
deleted file mode 100644
index f2ebf81..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectNameSuggestOracle.java
+++ /dev/null
@@ -1,35 +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.client.ui;
-
-import com.google.gerrit.client.projects.ProjectMap;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.Natives;
-
-/** Suggestion Oracle for Project.NameKey entities. */
-public class ProjectNameSuggestOracle extends SuggestAfterTypingNCharsOracle {
-  @Override
-  public void _onRequestSuggestions(Request req, Callback callback) {
-    ProjectMap.suggest(
-        req.getQuery(),
-        req.getLimit(),
-        new GerritCallback<ProjectMap>() {
-          @Override
-          public void onSuccess(ProjectMap map) {
-            callback.onSuggestionsReady(req, new Response(Natives.asList(map.values())));
-          }
-        });
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectSearchLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectSearchLink.java
deleted file mode 100644
index f0e06a0..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectSearchLink.java
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.ui;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.admin.AdminConstants;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.ui.Image;
-
-public class ProjectSearchLink extends InlineHyperlink {
-
-  public ProjectSearchLink(Project.NameKey projectName) {
-    super(" ", PageLinks.toProjectDefaultDashboard(projectName));
-    setTitle(AdminConstants.I.projectListQueryLink());
-    final Image image = new Image(Gerrit.RESOURCES.queryIcon());
-    image.setStyleName(Gerrit.RESOURCES.css().queryIcon());
-    DOM.insertBefore(getElement(), image.getElement(), DOM.getFirstChild(getElement()));
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java
deleted file mode 100644
index 3576b12..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java
+++ /dev/null
@@ -1,121 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT 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.client.ui;
-
-import static java.util.Comparator.comparing;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.projects.ProjectInfo;
-import com.google.gerrit.client.projects.ProjectMap;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
-import com.google.gwt.user.client.ui.Image;
-import java.util.List;
-
-public class ProjectsTable extends NavigationTable<ProjectInfo> {
-  public static final int C_STATE = 1;
-  public static final int C_NAME = 2;
-  public static final int C_DESCRIPTION = 3;
-  public static final int C_REPO_BROWSER = 4;
-
-  public ProjectsTable() {
-    super(Util.C.projectItemHelp());
-    initColumnHeaders();
-  }
-
-  protected void initColumnHeaders() {
-    table.setText(0, C_STATE, Util.C.projectStateAbbrev());
-    table.getCellFormatter().getElement(0, C_STATE).setTitle(Util.C.projectStateHelp());
-    table.setText(0, C_NAME, Util.C.projectName());
-    table.setText(0, C_DESCRIPTION, Util.C.projectDescription());
-
-    final FlexCellFormatter fmt = table.getFlexCellFormatter();
-    fmt.addStyleName(0, C_STATE, Gerrit.RESOURCES.css().iconHeader());
-    fmt.addStyleName(0, C_NAME, Gerrit.RESOURCES.css().dataHeader());
-    fmt.addStyleName(0, C_DESCRIPTION, Gerrit.RESOURCES.css().dataHeader());
-  }
-
-  @Override
-  protected Object getRowItemKey(ProjectInfo item) {
-    return item.name();
-  }
-
-  @Override
-  protected void onOpenRow(int row) {
-    if (row > 0) {
-      movePointerTo(row);
-    }
-  }
-
-  public void display(ProjectMap projects) {
-    displaySubset(projects, 0, projects.size());
-  }
-
-  public void displaySubset(ProjectMap projects, int fromIndex, int toIndex) {
-    while (1 < table.getRowCount()) {
-      table.removeRow(table.getRowCount() - 1);
-    }
-
-    List<ProjectInfo> list = Natives.asList(projects.values());
-    list.sort(comparing(ProjectInfo::name));
-    for (ProjectInfo p : list.subList(fromIndex, toIndex)) {
-      insert(table.getRowCount(), p);
-    }
-
-    finishDisplay();
-  }
-
-  protected void insert(int row, ProjectInfo k) {
-    table.insertRow(row);
-
-    applyDataRowStyle(row);
-
-    final FlexCellFormatter fmt = table.getFlexCellFormatter();
-    fmt.addStyleName(row, C_STATE, Gerrit.RESOURCES.css().iconCell());
-    fmt.addStyleName(row, C_NAME, Gerrit.RESOURCES.css().dataCell());
-    fmt.addStyleName(row, C_NAME, Gerrit.RESOURCES.css().projectNameColumn());
-    fmt.addStyleName(row, C_DESCRIPTION, Gerrit.RESOURCES.css().dataCell());
-
-    populate(row, k);
-  }
-
-  protected void populate(int row, ProjectInfo k) {
-    populateState(row, k);
-    table.setText(row, C_NAME, k.name());
-    table.setText(row, C_DESCRIPTION, k.description());
-
-    setRowItem(row, k);
-  }
-
-  protected void populateState(int row, ProjectInfo k) {
-    Image state = new Image();
-    switch (k.state()) {
-      case HIDDEN:
-        state.setResource(Gerrit.RESOURCES.redNot());
-        state.setTitle(com.google.gerrit.client.admin.Util.toLongString(k.state()));
-        table.setWidget(row, ProjectsTable.C_STATE, state);
-        break;
-      case READ_ONLY:
-        state.setResource(Gerrit.RESOURCES.readOnly());
-        state.setTitle(com.google.gerrit.client.admin.Util.toLongString(k.state()));
-        table.setWidget(row, ProjectsTable.C_STATE, state);
-        break;
-      case ACTIVE:
-      default:
-        // Intentionally left blank, do not show an icon when active.
-        break;
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RebaseDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RebaseDialog.java
deleted file mode 100644
index e03ac46..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RebaseDialog.java
+++ /dev/null
@@ -1,169 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.ui;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeList;
-import com.google.gerrit.client.changes.Util;
-import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.extensions.client.ListChangesOption;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.SuggestBox;
-import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-public abstract class RebaseDialog extends CommentedActionDialog {
-  private final SuggestBox base;
-  private final CheckBox changeParent;
-  private List<ChangeInfo> candidateChanges;
-  private final boolean sendEnabled;
-
-  public RebaseDialog(
-      final Project.NameKey project,
-      final String branch,
-      final Change.Id changeId,
-      final boolean sendEnabled) {
-    super(Util.C.rebaseTitle(), null);
-    this.sendEnabled = sendEnabled;
-    sendButton.setText(Util.C.buttonRebaseChangeSend());
-
-    // Create the suggestion box to filter over a list of recent changes
-    // open on the same branch. The list of candidates is primed by the
-    // changeParent CheckBox (below) getting enabled by the user.
-    base =
-        new SuggestBox(
-            new HighlightSuggestOracle() {
-              @Override
-              protected void onRequestSuggestions(Request request, Callback done) {
-                String query = request.getQuery().toLowerCase();
-                List<ChangeSuggestion> suggestions = new ArrayList<>();
-                for (ChangeInfo ci : candidateChanges) {
-                  if (changeId.equals(ci.legacyId())) {
-                    continue; // do not suggest current change
-                  }
-                  String id = String.valueOf(ci.legacyId().get());
-                  if (id.contains(query) || ci.subject().toLowerCase().contains(query)) {
-                    suggestions.add(new ChangeSuggestion(ci));
-                    if (suggestions.size() >= 50) { // limit to 50 suggestions
-                      break;
-                    }
-                  }
-                }
-                done.onSuggestionsReady(request, new Response(suggestions));
-              }
-            });
-    base.getElement().setAttribute("placeholder", Util.C.rebasePlaceholderMessage());
-    base.setStyleName(Gerrit.RESOURCES.css().rebaseSuggestBox());
-
-    // The changeParent checkbox must be clicked to load into browser memory
-    // a list of open changes from the same project and same branch that this
-    // change may rebase onto.
-    changeParent = new CheckBox(Util.C.rebaseConfirmMessage());
-    changeParent.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            if (changeParent.getValue()) {
-              ChangeList.query(
-                  PageLinks.projectQuery(project)
-                      + " "
-                      + PageLinks.op("branch", branch)
-                      + " is:open -age:90d",
-                  Collections.<ListChangesOption>emptySet(),
-                  new GerritCallback<ChangeList>() {
-                    @Override
-                    public void onSuccess(ChangeList result) {
-                      candidateChanges = Natives.asList(result);
-                      updateControls(true);
-                    }
-
-                    @Override
-                    public void onFailure(Throwable err) {
-                      updateControls(false);
-                      changeParent.setValue(false);
-                      super.onFailure(err);
-                    }
-                  });
-            } else {
-              updateControls(false);
-            }
-          }
-        });
-
-    // add the checkbox and suggestbox widgets to the content panel
-    contentPanel.add(changeParent);
-    contentPanel.add(base);
-    contentPanel.setStyleName(Gerrit.RESOURCES.css().rebaseContentPanel());
-  }
-
-  @Override
-  public void center() {
-    super.center();
-    GlobalKey.dialog(this);
-    updateControls(false);
-  }
-
-  private void updateControls(boolean changeParentEnabled) {
-    if (changeParentEnabled) {
-      sendButton.setTitle(null);
-      sendButton.setEnabled(true);
-      base.setEnabled(true);
-      base.setFocus(true);
-    } else {
-      base.setEnabled(false);
-      sendButton.setEnabled(sendEnabled);
-      if (sendEnabled) {
-        sendButton.setTitle(null);
-        sendButton.setFocus(true);
-      } else {
-        sendButton.setTitle(Util.C.rebaseNotPossibleMessage());
-        cancelButton.setFocus(true);
-      }
-    }
-  }
-
-  public String getBase() {
-    return changeParent.getValue() ? base.getText() : null;
-  }
-
-  private static class ChangeSuggestion implements Suggestion {
-    private ChangeInfo change;
-
-    ChangeSuggestion(ChangeInfo change) {
-      this.change = change;
-    }
-
-    @Override
-    public String getDisplayString() {
-      return String.valueOf(change.legacyId().get()) + ": " + change.subject();
-    }
-
-    @Override
-    public String getReplacementString() {
-      return String.valueOf(change.legacyId().get());
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RemoteSuggestBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RemoteSuggestBox.java
deleted file mode 100644
index 5d741cf..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RemoteSuggestBox.java
+++ /dev/null
@@ -1,165 +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.client.ui;
-
-import com.google.gwt.event.dom.client.FocusEvent;
-import com.google.gwt.event.dom.client.FocusHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyDownEvent;
-import com.google.gwt.event.dom.client.KeyDownHandler;
-import com.google.gwt.event.logical.shared.CloseEvent;
-import com.google.gwt.event.logical.shared.CloseHandler;
-import com.google.gwt.event.logical.shared.HasCloseHandlers;
-import com.google.gwt.event.logical.shared.HasSelectionHandlers;
-import com.google.gwt.event.logical.shared.SelectionEvent;
-import com.google.gwt.event.logical.shared.SelectionHandler;
-import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.Focusable;
-import com.google.gwt.user.client.ui.HasText;
-import com.google.gwt.user.client.ui.SuggestBox;
-import com.google.gwt.user.client.ui.SuggestBox.DefaultSuggestionDisplay;
-import com.google.gwt.user.client.ui.SuggestOracle;
-import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
-import com.google.gwt.user.client.ui.TextBoxBase;
-
-public class RemoteSuggestBox extends Composite
-    implements Focusable,
-        HasText,
-        HasSelectionHandlers<String>,
-        HasCloseHandlers<RemoteSuggestBox> {
-  private final RemoteSuggestOracle remoteSuggestOracle;
-  private final DefaultSuggestionDisplay display;
-  private final HintTextBox textBox;
-  private final SuggestBox suggestBox;
-  private boolean submitOnSelection;
-
-  public RemoteSuggestBox(SuggestOracle oracle) {
-    remoteSuggestOracle = new RemoteSuggestOracle(oracle);
-    remoteSuggestOracle.setServeSuggestions(true);
-    display = new DefaultSuggestionDisplay();
-
-    textBox = new HintTextBox();
-    textBox.addKeyDownHandler(
-        new KeyDownHandler() {
-          @Override
-          public void onKeyDown(KeyDownEvent e) {
-            submitOnSelection = false;
-            if (e.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) {
-              CloseEvent.fire(RemoteSuggestBox.this, RemoteSuggestBox.this);
-            } else if (e.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
-              if (display.isSuggestionListShowing()) {
-                if (textBox.getValue().equals(remoteSuggestOracle.getLast())) {
-                  submitOnSelection = true;
-                } else {
-                  display.hideSuggestions();
-                }
-              } else {
-                SelectionEvent.fire(RemoteSuggestBox.this, getText());
-              }
-            }
-          }
-        });
-
-    suggestBox = new SuggestBox(remoteSuggestOracle, textBox, display);
-    suggestBox.addSelectionHandler(
-        new SelectionHandler<Suggestion>() {
-          @Override
-          public void onSelection(SelectionEvent<Suggestion> event) {
-            if (submitOnSelection) {
-              SelectionEvent.fire(RemoteSuggestBox.this, getText());
-            }
-            remoteSuggestOracle.cancelOutstandingRequest();
-            display.hideSuggestions();
-          }
-        });
-    initWidget(suggestBox);
-  }
-
-  public void setHintText(String hint) {
-    textBox.setHintText(hint);
-  }
-
-  public void setVisibleLength(int len) {
-    textBox.setVisibleLength(len);
-  }
-
-  public void setEnabled(boolean enabled) {
-    suggestBox.setEnabled(enabled);
-  }
-
-  public TextBoxBase getTextBox() {
-    return textBox;
-  }
-
-  @Override
-  public String getText() {
-    return suggestBox.getText();
-  }
-
-  @Override
-  public void setText(String value) {
-    suggestBox.setText(value);
-  }
-
-  @Override
-  public void setFocus(boolean focus) {
-    suggestBox.setFocus(focus);
-  }
-
-  @Override
-  public int getTabIndex() {
-    return suggestBox.getTabIndex();
-  }
-
-  @Override
-  public void setAccessKey(char key) {
-    suggestBox.setAccessKey(key);
-  }
-
-  @Override
-  public void setTabIndex(int index) {
-    suggestBox.setTabIndex(index);
-  }
-
-  @Override
-  public HandlerRegistration addSelectionHandler(SelectionHandler<String> h) {
-    return addHandler(h, SelectionEvent.getType());
-  }
-
-  @Override
-  public HandlerRegistration addCloseHandler(CloseHandler<RemoteSuggestBox> h) {
-    return addHandler(h, CloseEvent.getType());
-  }
-
-  public void selectAll() {
-    suggestBox.getValueBox().selectAll();
-  }
-
-  public void enableDefaultSuggestions() {
-    textBox.addFocusHandler(
-        new FocusHandler() {
-          @Override
-          public void onFocus(FocusEvent focusEvent) {
-            if (textBox.getText().equals("")) {
-              suggestBox.showSuggestionList();
-            }
-          }
-        });
-  }
-
-  public void setServeSuggestionsOnOracle(boolean serveSuggestions) {
-    remoteSuggestOracle.setServeSuggestions(serveSuggestions);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java
deleted file mode 100644
index 03ed899..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java
+++ /dev/null
@@ -1,199 +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.client.ui;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.projects.ThemeInfo;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.HasHorizontalAlignment;
-import com.google.gwt.user.client.ui.InlineLabel;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtexpui.user.client.View;
-
-/**
- * A Screen layout with a header and a body.
- *
- * <p>The header is mainly a text title, but it can be decorated in the West, the East, and the
- * FarEast by any Widget. The West and East decorations will surround the text on the left and right
- * respectively, and the FarEast will be right justified to the right edge of the screen. The East
- * decoration will expand to take up any extra space.
- */
-public abstract class Screen extends View {
-  private Grid header;
-  private InlineLabel headerText;
-  private FlowPanel body;
-  private String token;
-  private boolean requiresSignIn;
-  private String windowTitle;
-  private Widget titleWidget;
-
-  private ThemeInfo theme;
-  private boolean setTheme;
-
-  protected Screen() {
-    initWidget(new FlowPanel());
-    setStyleName(Gerrit.RESOURCES.css().screen());
-    body = new FlowPanel();
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    if (header == null) {
-      onInitUI();
-    }
-  }
-
-  @Override
-  protected void onUnload() {
-    super.onUnload();
-    if (setTheme) {
-      Gerrit.THEMER.clear();
-    }
-  }
-
-  public void registerKeys() {}
-
-  private enum Cols {
-    West,
-    Title,
-    East,
-    FarEast
-  }
-
-  protected void onInitUI() {
-    final FlowPanel me = (FlowPanel) getWidget();
-    me.add(header = new Grid(1, Cols.values().length));
-    me.add(body);
-
-    headerText = new InlineLabel();
-    if (titleWidget == null) {
-      titleWidget = headerText;
-    }
-    FlowPanel title = new FlowPanel();
-    title.add(titleWidget);
-    title.setStyleName(Gerrit.RESOURCES.css().screenHeader());
-    header.setWidget(0, Cols.Title.ordinal(), title);
-
-    header.setStyleName(Gerrit.RESOURCES.css().screenHeader());
-    header
-        .getCellFormatter()
-        .setHorizontalAlignment(0, Cols.FarEast.ordinal(), HasHorizontalAlignment.ALIGN_RIGHT);
-    // force FarEast all the way to the right
-    header.getCellFormatter().setWidth(0, Cols.FarEast.ordinal(), "100%");
-  }
-
-  protected void setWindowTitle(String text) {
-    windowTitle = text;
-    Gerrit.setWindowTitle(this, text);
-  }
-
-  protected void setPageTitle(String text) {
-    final String old = headerText.getText();
-    if (text.isEmpty()) {
-      header.setVisible(false);
-    } else {
-      headerText.setText(text);
-      header.setVisible(true);
-    }
-    if (windowTitle == null || windowTitle.equals(old)) {
-      setWindowTitle(text);
-    }
-  }
-
-  protected void setHeaderVisible(boolean value) {
-    header.setVisible(value);
-  }
-
-  public void setTitle(Widget w) {
-    titleWidget = w;
-  }
-
-  protected void setTitleEast(Widget w) {
-    header.setWidget(0, Cols.East.ordinal(), w);
-  }
-
-  protected void setTitleFarEast(Widget w) {
-    header.setWidget(0, Cols.FarEast.ordinal(), w);
-  }
-
-  protected void setTitleWest(Widget w) {
-    header.setWidget(0, Cols.West.ordinal(), w);
-  }
-
-  protected void add(Widget w) {
-    body.add(w);
-  }
-
-  protected FlowPanel getBody() {
-    return body;
-  }
-
-  protected void setTheme(ThemeInfo t) {
-    theme = t;
-  }
-
-  /** Get the history token for this screen. */
-  public String getToken() {
-    return token;
-  }
-
-  /** Set the history token for this screen. */
-  public void setToken(String t) {
-    assert t != null && !t.isEmpty();
-    token = t;
-
-    if (isCurrentView()) {
-      Gerrit.updateImpl(token);
-    }
-  }
-
-  /**
-   * If this view can display the given token, update it.
-   *
-   * @param newToken token the UI wants to show.
-   * @return true if this view can show the token immediately, false if not.
-   */
-  public boolean displayToken(String newToken) {
-    return false;
-  }
-
-  /** Set whether or not {@link Gerrit#isSignedIn()} must be true. */
-  public final void setRequiresSignIn(boolean b) {
-    requiresSignIn = b;
-  }
-
-  /** Does {@link Gerrit#isSignedIn()} have to be true to be on this screen? */
-  public final boolean isRequiresSignIn() {
-    return requiresSignIn;
-  }
-
-  public void onShowView() {
-    if (windowTitle != null) {
-      Gerrit.setWindowTitle(this, windowTitle);
-    }
-    Gerrit.EVENT_BUS.fireEvent(new ScreenLoadEvent(this));
-    Gerrit.setQueryString(null);
-    registerKeys();
-
-    if (theme != null) {
-      Gerrit.THEMER.set(theme);
-      setTheme = true;
-    } else {
-      Gerrit.THEMER.clear();
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ScreenLoadEvent.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ScreenLoadEvent.java
deleted file mode 100644
index debd9a6..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ScreenLoadEvent.java
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.ui;
-
-import com.google.gwt.event.shared.GwtEvent;
-
-public class ScreenLoadEvent extends GwtEvent<ScreenLoadHandler> {
-  private final Screen screen;
-
-  public ScreenLoadEvent(Screen screen) {
-    super();
-    this.screen = screen;
-  }
-
-  public static final Type<ScreenLoadHandler> TYPE = new Type<>();
-
-  @Override
-  protected void dispatch(ScreenLoadHandler handler) {
-    handler.onScreenLoad(this);
-  }
-
-  @Override
-  public GwtEvent.Type<ScreenLoadHandler> getAssociatedType() {
-    return TYPE;
-  }
-
-  public Screen getScreen() {
-    return screen;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ScreenLoadHandler.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ScreenLoadHandler.java
deleted file mode 100644
index 9a5eb03..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ScreenLoadHandler.java
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.ui;
-
-import com.google.gwt.event.shared.EventHandler;
-
-public interface ScreenLoadHandler extends EventHandler {
-  void onScreenLoad(ScreenLoadEvent event);
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/SmallHeading.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/SmallHeading.java
deleted file mode 100644
index ea18d62..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/SmallHeading.java
+++ /dev/null
@@ -1,29 +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.client.ui;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gwt.user.client.ui.Label;
-
-public class SmallHeading extends Label {
-  public SmallHeading() {
-    setStyleName(Gerrit.RESOURCES.css().smallHeading());
-  }
-
-  public SmallHeading(String text) {
-    this();
-    setText(text);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/SuggestAfterTypingNCharsOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/SuggestAfterTypingNCharsOracle.java
deleted file mode 100644
index e24f347..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/SuggestAfterTypingNCharsOracle.java
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.ui;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * Suggest oracle that only provides suggestions if the user has typed at least as many characters
- * as configured by 'suggest.from'. If 'suggest.from' is set to 0, suggestions will always be
- * provided.
- */
-public abstract class SuggestAfterTypingNCharsOracle extends HighlightSuggestOracle {
-
-  @Override
-  protected void onRequestSuggestions(Request req, Callback cb) {
-    if (req.getQuery() != null && req.getQuery().length() >= Gerrit.info().suggest().from()) {
-      _onRequestSuggestions(req, cb);
-    } else {
-      List<Suggestion> none = Collections.emptyList();
-      cb.onSuggestionsReady(req, new Response(none));
-    }
-  }
-
-  protected abstract void _onRequestSuggestions(Request request, Callback done);
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/TextAreaActionDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/TextAreaActionDialog.java
deleted file mode 100644
index d7d5d6a..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/TextAreaActionDialog.java
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.ui;
-
-import com.google.gwt.event.logical.shared.CloseHandler;
-import com.google.gwt.user.client.ui.PopupPanel;
-import com.google.gwtexpui.globalkey.client.NpTextArea;
-
-public abstract class TextAreaActionDialog extends CommentedActionDialog
-    implements CloseHandler<PopupPanel> {
-  protected final NpTextArea message;
-
-  public TextAreaActionDialog(String title, String heading) {
-    super(title, heading);
-
-    message = new NpTextArea();
-    message.setCharacterWidth(60);
-    message.setVisibleLines(10);
-    message.getElement().setPropertyBoolean("spellcheck", true);
-    setFocusOn(message);
-
-    contentPanel.add(message);
-  }
-
-  public String getMessageText() {
-    return message.getText().trim();
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.java
deleted file mode 100644
index 32bb796..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.java
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT 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.client.ui;
-
-import com.google.gwt.i18n.client.Constants;
-
-public interface UIConstants extends Constants {
-  String commentedActionButtonSend();
-
-  String commentedActionButtonCancel();
-
-  String projectName();
-
-  String projectDescription();
-
-  String projectItemHelp();
-
-  String projectStateAbbrev();
-
-  String projectStateHelp();
-
-  String dialogCreateChangeTitle();
-
-  String dialogCreateChangeHeading();
-
-  String newChangeBranchSuggestion();
-
-  String newChangeTopicSuggestion();
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.properties
deleted file mode 100644
index 736e210..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.properties
+++ /dev/null
@@ -1,13 +0,0 @@
-commentedActionButtonSend = Submit
-commentedActionButtonCancel = Cancel
-
-projectName = Project Name
-projectDescription = Project Description
-projectItemHelp = project
-projectStateAbbrev = S
-projectStateHelp = State
-
-dialogCreateChangeTitle = Create Change
-dialogCreateChangeHeading = Description
-newChangeBranchSuggestion = Select branch for new change
-newChangeTopicSuggestion = Enter topic for new change
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIMessages.java
deleted file mode 100644
index af17390..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIMessages.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.ui;
-
-import com.google.gwt.i18n.client.Messages;
-
-public interface UIMessages extends Messages {
-  String helpListOpen(String item);
-
-  String helpListPrev(String item);
-
-  String helpListNext(String item);
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIMessages.properties
deleted file mode 100644
index 1439245..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIMessages.properties
+++ /dev/null
@@ -1,3 +0,0 @@
-helpListOpen = Select {0}
-helpListPrev = Previous {0}
-helpListNext = Next {0}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UserActivityMonitor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UserActivityMonitor.java
deleted file mode 100644
index 2b76b9b..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UserActivityMonitor.java
+++ /dev/null
@@ -1,113 +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.client.ui;
-
-import com.google.gwt.core.client.Scheduler;
-import com.google.gwt.core.client.Scheduler.RepeatingCommand;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwt.event.dom.client.MouseMoveEvent;
-import com.google.gwt.event.dom.client.MouseMoveHandler;
-import com.google.gwt.event.logical.shared.HasValueChangeHandlers;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.logical.shared.ValueChangeHandler;
-import com.google.gwt.event.shared.EventBus;
-import com.google.gwt.event.shared.GwtEvent;
-import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.event.shared.SimpleEventBus;
-import com.google.gwt.user.client.History;
-import com.google.gwtexpui.globalkey.client.DocWidget;
-
-/** Checks for user keyboard and mouse activity. */
-public class UserActivityMonitor {
-  private static final long TIMEOUT = 10 * 60 * 1000;
-  private static final MonitorImpl impl;
-
-  /**
-   * @return true if there has been keyboard and/or mouse activity in recent enough history to
-   *     believe a user is still controlling this session.
-   */
-  public static boolean isActive() {
-    return impl.active || impl.recent;
-  }
-
-  public static HandlerRegistration addValueChangeHandler(ValueChangeHandler<Boolean> handler) {
-    return impl.addValueChangeHandler(handler);
-  }
-
-  static {
-    impl = new MonitorImpl();
-    DocWidget.get().addKeyPressHandler(impl);
-    DocWidget.get().addMouseMoveHandler(impl);
-    History.addValueChangeHandler(impl);
-    Scheduler.get().scheduleFixedDelay(impl, 60 * 1000);
-  }
-
-  private UserActivityMonitor() {}
-
-  private static class MonitorImpl
-      implements RepeatingCommand,
-          KeyPressHandler,
-          MouseMoveHandler,
-          ValueChangeHandler<String>,
-          HasValueChangeHandlers<Boolean> {
-    private final EventBus bus = new SimpleEventBus();
-    private boolean recent = true;
-    private boolean active = true;
-    private long last = System.currentTimeMillis();
-
-    @Override
-    public void onKeyPress(KeyPressEvent event) {
-      recent = true;
-    }
-
-    @Override
-    public void onMouseMove(MouseMoveEvent event) {
-      recent = true;
-    }
-
-    @Override
-    public void onValueChange(ValueChangeEvent<String> event) {
-      recent = true;
-    }
-
-    @Override
-    public boolean execute() {
-      long now = System.currentTimeMillis();
-      if (recent) {
-        if (!active) {
-          ValueChangeEvent.fire(this, active);
-        }
-        recent = false;
-        active = true;
-        last = now;
-      } else if (active && (now - last) > TIMEOUT) {
-        active = false;
-        ValueChangeEvent.fire(this, false);
-      }
-      return true;
-    }
-
-    @Override
-    public HandlerRegistration addValueChangeHandler(ValueChangeHandler<Boolean> handler) {
-      return bus.addHandler(ValueChangeEvent.getType(), handler);
-    }
-
-    @Override
-    public void fireEvent(GwtEvent<?> event) {
-      bus.fireEvent(event);
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Util.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Util.java
deleted file mode 100644
index 41e3573..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Util.java
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT 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.client.ui;
-
-import com.google.gwt.core.client.GWT;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-
-public class Util {
-  public static final UIConstants C = GWT.create(UIConstants.class);
-  public static final UIMessages M = GWT.create(UIMessages.class);
-
-  public static String highlight(String text, String toHighlight) {
-    final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    if (toHighlight == null || "".equals(toHighlight)) {
-      b.append(text);
-      return b.toSafeHtml().asString();
-    }
-
-    int pos = 0;
-    int endPos = 0;
-    while ((pos = text.toLowerCase().indexOf(toHighlight.toLowerCase(), pos)) > -1) {
-      if (pos > endPos) {
-        b.append(text.substring(endPos, pos));
-      }
-      endPos = pos + toHighlight.length();
-      b.openElement("b");
-      b.append(text.substring(pos, endPos));
-      b.closeElement("b");
-      pos = endPos;
-    }
-    if (endPos < text.length()) {
-      b.append(text.substring(endPos));
-    }
-    return b.toSafeHtml().asString();
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/CodeMirror.gwt.xml b/gerrit-gwtui/src/main/java/net/codemirror/CodeMirror.gwt.xml
deleted file mode 100644
index add033f..0000000
--- a/gerrit-gwtui/src/main/java/net/codemirror/CodeMirror.gwt.xml
+++ /dev/null
@@ -1,24 +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.
--->
-<module>
-  <inherits name='com.google.gwt.logging.Logging'/>
-  <inherits name='com.google.gwt.resources.Resources'/>
-  <source path='addon'/>
-  <source path='lib'/>
-  <source path='keymap'/>
-  <source path='mode'/>
-  <source path='theme'/>
-</module>
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/addon/AddonInjector.java b/gerrit-gwtui/src/main/java/net/codemirror/addon/AddonInjector.java
deleted file mode 100644
index cb1891e..0000000
--- a/gerrit-gwtui/src/main/java/net/codemirror/addon/AddonInjector.java
+++ /dev/null
@@ -1,92 +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 net.codemirror.addon;
-
-import com.google.gwt.safehtml.shared.SafeUri;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import net.codemirror.lib.Loader;
-
-public class AddonInjector {
-  private static final Map<String, SafeUri> addonUris = new HashMap<>();
-
-  static {
-    addonUris.put(Addons.I.merge_bundled().getName(), Addons.I.merge_bundled().getSafeUri());
-  }
-
-  public static SafeUri getAddonScriptUri(String addon) {
-    return addonUris.get(addon);
-  }
-
-  private static boolean canLoad(String addon) {
-    return getAddonScriptUri(addon) != null;
-  }
-
-  private final Set<String> loading = new HashSet<>();
-  private int pending;
-  private AsyncCallback<Void> appCallback;
-
-  public AddonInjector add(String name) {
-    if (name == null) {
-      return this;
-    }
-
-    if (!canLoad(name)) {
-      Logger.getLogger("net.codemirror")
-          .log(Level.WARNING, "CodeMirror addon " + name + " not configured.");
-      return this;
-    }
-
-    loading.add(name);
-    return this;
-  }
-
-  public void inject(AsyncCallback<Void> appCallback) {
-    this.appCallback = appCallback;
-    for (String addon : loading) {
-      beginLoading(addon);
-    }
-    if (pending == 0) {
-      appCallback.onSuccess(null);
-    }
-  }
-
-  private void beginLoading(String addon) {
-    pending++;
-    Loader.injectScript(
-        getAddonScriptUri(addon),
-        new AsyncCallback<Void>() {
-          @Override
-          public void onSuccess(Void result) {
-            pending--;
-            if (pending == 0) {
-              appCallback.onSuccess(null);
-            }
-          }
-
-          @Override
-          public void onFailure(Throwable caught) {
-            if (--pending == 0) {
-              appCallback.onFailure(caught);
-            }
-          }
-        });
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/addon/Addons.java b/gerrit-gwtui/src/main/java/net/codemirror/addon/Addons.java
deleted file mode 100644
index 19a681c..0000000
--- a/gerrit-gwtui/src/main/java/net/codemirror/addon/Addons.java
+++ /dev/null
@@ -1,28 +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 net.codemirror.addon;
-
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.resources.client.ClientBundle;
-import com.google.gwt.resources.client.DataResource;
-import com.google.gwt.resources.client.DataResource.DoNotEmbed;
-
-public interface Addons extends ClientBundle {
-  Addons I = GWT.create(Addons.class);
-
-  @Source("merge_bundled.js")
-  @DoNotEmbed
-  DataResource merge_bundled();
-}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/BlameConfig.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/BlameConfig.java
deleted file mode 100644
index 1e81f83a..0000000
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/BlameConfig.java
+++ /dev/null
@@ -1,23 +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 net.codemirror.lib;
-
-import com.google.gwt.i18n.client.Messages;
-
-public interface BlameConfig extends Messages {
-  String shortBlameMsg(String commitId, String date, String author);
-
-  String detailedBlameMsg(String commitId, String author, String time, String msg);
-}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/BlameConfig.properties b/gerrit-gwtui/src/main/java/net/codemirror/lib/BlameConfig.properties
deleted file mode 100644
index 658b50f..0000000
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/BlameConfig.properties
+++ /dev/null
@@ -1,2 +0,0 @@
-shortBlameMsg={0} {1} {2}
-detailedBlameMsg=commit {0}\nAuthor: {1}\nDate: {2}\n\n{3}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
deleted file mode 100644
index a84c464..0000000
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
+++ /dev/null
@@ -1,448 +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 net.codemirror.lib;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.diff.DisplaySide;
-import com.google.gerrit.client.rpc.CallbackGroup;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.NativeEvent;
-import com.google.gwt.resources.client.CssResource;
-import com.google.gwt.user.client.Window;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import net.codemirror.lib.TextMarker.FromTo;
-
-/**
- * Glue to connect CodeMirror to be callable from GWT.
- *
- * @see <a href="http://codemirror.net/doc/manual.html#api">CodeMirror API</a>
- */
-public class CodeMirror extends JavaScriptObject {
-  public static void preload() {
-    initLibrary(CallbackGroup.<Void>emptyCallback());
-  }
-
-  public static void initLibrary(AsyncCallback<Void> cb) {
-    Loader.initLibrary(cb);
-  }
-
-  interface Style extends CssResource {
-    String activeLine();
-
-    String showTabs();
-
-    String margin();
-  }
-
-  static Style style() {
-    return Lib.I.style();
-  }
-
-  public static CodeMirror create(Element p, Configuration cfg) {
-    CodeMirror cm = newCM(p, cfg);
-    Extras.attach(cm);
-    return cm;
-  }
-
-  private static native CodeMirror newCM(Element p, Configuration cfg) /*-{
-    return $wnd.CodeMirror(p, cfg);
-  }-*/;
-
-  public final native void setOption(String option, boolean value) /*-{
-    this.setOption(option, value)
-  }-*/;
-
-  public final native void setOption(String option, double value) /*-{
-    this.setOption(option, value)
-  }-*/;
-
-  public final native void setOption(String option, String value) /*-{
-    this.setOption(option, value)
-  }-*/;
-
-  public final native void setOption(String option, JavaScriptObject val) /*-{
-    this.setOption(option, val)
-  }-*/;
-
-  public final native String getStringOption(String o) /*-{ return this.getOption(o) }-*/;
-
-  public final native String getValue() /*-{ return this.getValue() }-*/;
-
-  public final native void setValue(String v) /*-{ this.setValue(v) }-*/;
-
-  public final native int changeGeneration(boolean closeEvent)
-      /*-{ return this.changeGeneration(closeEvent) }-*/ ;
-
-  public final native boolean isClean(int generation) /*-{ return this.isClean(generation) }-*/;
-
-  public final native void setWidth(double w) /*-{ this.setSize(w, null) }-*/;
-
-  public final native void setHeight(double h) /*-{ this.setSize(null, h) }-*/;
-
-  public final int getHeight() {
-    return getWrapperElement().getClientHeight();
-  }
-
-  public final void adjustHeight(int localHeader) {
-    int rest = Gerrit.getHeaderFooterHeight() + localHeader + 5; // Estimate
-    setHeight(Window.getClientHeight() - rest);
-  }
-
-  public final native String getLine(int n) /*-{ return this.getLine(n) }-*/;
-
-  public final native double barHeight() /*-{ return this.display.barHeight }-*/;
-
-  public final native double barWidth() /*-{ return this.display.barWidth }-*/;
-
-  public final native int lastLine() /*-{ return this.lastLine() }-*/;
-
-  public final native void refresh() /*-{ this.refresh() }-*/;
-
-  public final native TextMarker markText(Pos from, Pos to, Configuration options) /*-{
-    return this.markText(from, to, options)
-  }-*/;
-
-  public enum LineClassWhere {
-    TEXT {
-      @Override
-      String value() {
-        return "text";
-      }
-    },
-    BACKGROUND {
-      @Override
-      String value() {
-        return "background";
-      }
-    },
-    WRAP {
-      @Override
-      String value() {
-        return "wrap";
-      }
-    };
-
-    abstract String value();
-  }
-
-  public final void addLineClass(int line, LineClassWhere where, String className) {
-    addLineClassNative(line, where.value(), className);
-  }
-
-  private native void addLineClassNative(int line, String where, String lineClass) /*-{
-    this.addLineClass(line, where, lineClass)
-  }-*/;
-
-  public final void addLineClass(LineHandle line, LineClassWhere where, String className) {
-    addLineClassNative(line, where.value(), className);
-  }
-
-  private native void addLineClassNative(LineHandle line, String where, String lineClass) /*-{
-    this.addLineClass(line, where, lineClass)
-  }-*/;
-
-  public final void removeLineClass(int line, LineClassWhere where, String className) {
-    removeLineClassNative(line, where.value(), className);
-  }
-
-  private native void removeLineClassNative(int line, String where, String lineClass) /*-{
-    this.removeLineClass(line, where, lineClass)
-  }-*/;
-
-  public final void removeLineClass(LineHandle line, LineClassWhere where, String className) {
-    removeLineClassNative(line, where.value(), className);
-  }
-
-  private native void removeLineClassNative(LineHandle line, String where, String lineClass) /*-{
-    this.removeLineClass(line, where, lineClass)
-  }-*/;
-
-  public final native void addWidget(Pos pos, Element node) /*-{
-    this.addWidget(pos, node, false)
-  }-*/;
-
-  public final native LineWidget addLineWidget(int line, Element node, Configuration options) /*-{
-    return this.addLineWidget(line, node, options)
-  }-*/;
-
-  public final native int lineAtHeight(double height) /*-{
-    return this.lineAtHeight(height)
-  }-*/;
-
-  public final native int lineAtHeight(double height, String mode) /*-{
-    return this.lineAtHeight(height, mode)
-  }-*/;
-
-  public final native double heightAtLine(int line) /*-{
-    return this.heightAtLine(line)
-  }-*/;
-
-  public final native double heightAtLine(int line, String mode) /*-{
-    return this.heightAtLine(line, mode)
-  }-*/;
-
-  public final native Rect charCoords(Pos pos, String mode) /*-{
-    return this.charCoords(pos, mode)
-  }-*/;
-
-  public final native CodeMirrorDoc getDoc() /*-{
-    return this.getDoc()
-  }-*/;
-
-  public final native void scrollTo(double x, double y) /*-{
-    this.scrollTo(x, y)
-  }-*/;
-
-  public final native void scrollToY(double y) /*-{
-    this.scrollTo(null, y)
-  }-*/;
-
-  public final void scrollToLine(int line) {
-    int height = getHeight();
-    if (lineAtHeight(height - 20) < line) {
-      scrollToY(heightAtLine(line, "local") - 0.5 * height);
-    }
-    setCursor(Pos.create(line, 0));
-  }
-
-  public final native ScrollInfo getScrollInfo() /*-{
-    return this.getScrollInfo()
-  }-*/;
-
-  public final native Viewport getViewport() /*-{
-    return this.getViewport()
-  }-*/;
-
-  public final native void operation(Runnable thunk) /*-{
-    this.operation(function() {
-      thunk.@java.lang.Runnable::run()();
-    })
-  }-*/;
-
-  public final native void off(String event, RegisteredHandler h) /*-{
-    this.off(event, h)
-  }-*/;
-
-  public final native RegisteredHandler on(String event, Runnable thunk) /*-{
-    var h = $entry(function() { thunk.@java.lang.Runnable::run()() });
-    this.on(event, h);
-    return h;
-  }-*/;
-
-  public final native void on(String event, EventHandler handler) /*-{
-    this.on(event, $entry(function(cm, e) {
-      handler.@net.codemirror.lib.CodeMirror.EventHandler::handle(
-        Lnet/codemirror/lib/CodeMirror;
-        Lcom/google/gwt/dom/client/NativeEvent;)(cm, e);
-    }))
-  }-*/;
-
-  public final native void on(String event, RenderLineHandler handler) /*-{
-    this.on(event, $entry(function(cm, h, e) {
-      handler.@net.codemirror.lib.CodeMirror.RenderLineHandler::handle(
-        Lnet/codemirror/lib/CodeMirror;
-        Lnet/codemirror/lib/CodeMirror$LineHandle;
-        Lcom/google/gwt/dom/client/Element;)(cm, h, e);
-    }))
-  }-*/;
-
-  public final native void on(String event, GutterClickHandler handler) /*-{
-    this.on(event, $entry(function(cm, l, g, e) {
-      handler.@net.codemirror.lib.CodeMirror.GutterClickHandler::handle(
-        Lnet/codemirror/lib/CodeMirror;
-        I
-        Ljava/lang/String;
-        Lcom/google/gwt/dom/client/NativeEvent;)(cm, l, g, e);
-    }))
-  }-*/;
-
-  public final native void on(String event, BeforeSelectionChangeHandler handler) /*-{
-    this.on(event, $entry(function(cm, o) {
-      var e = o.ranges[o.ranges.length-1];
-      handler.@net.codemirror.lib.CodeMirror.BeforeSelectionChangeHandler::handle(
-        Lnet/codemirror/lib/CodeMirror;
-        Lnet/codemirror/lib/Pos;
-        Lnet/codemirror/lib/Pos;)(cm, e.anchor, e.head);
-    }))
-  }-*/;
-
-  public final native void on(ChangesHandler handler) /*-{
-    this.on('changes', $entry(function(cm, o) {
-      handler.@net.codemirror.lib.CodeMirror.ChangesHandler::handle(
-        Lnet/codemirror/lib/CodeMirror;)(cm);
-    }))
-  }-*/;
-
-  public final native void setCursor(Pos p) /*-{ this.setCursor(p) }-*/;
-
-  public final native Pos getCursor() /*-{ return this.getCursor() }-*/;
-
-  public final native Pos getCursor(String start) /*-{
-    return this.getCursor(start)
-  }-*/;
-
-  public final FromTo getSelectedRange() {
-    return FromTo.create(getCursor("start"), getCursor("end"));
-  }
-
-  public final native void setSelection(Pos p) /*-{ this.setSelection(p) }-*/;
-
-  public final native void setSelection(Pos anchor, Pos head) /*-{
-    this.setSelection(anchor, head)
-  }-*/;
-
-  public final native boolean somethingSelected() /*-{
-    return this.somethingSelected()
-  }-*/;
-
-  public final native void addKeyMap(KeyMap map) /*-{ this.addKeyMap(map) }-*/;
-
-  public final native void removeKeyMap(KeyMap map) /*-{ this.removeKeyMap(map) }-*/;
-
-  public final native LineHandle getLineHandle(int line) /*-{
-    return this.getLineHandle(line)
-  }-*/;
-
-  public final native LineHandle getLineHandleVisualStart(int line) /*-{
-    return this.getLineHandleVisualStart(line)
-  }-*/;
-
-  public final native int getLineNumber(LineHandle handle) /*-{
-    return this.getLineNumber(handle)
-  }-*/;
-
-  public final native void focus() /*-{
-    this.focus()
-  }-*/;
-
-  public final native Element getWrapperElement() /*-{
-    return this.getWrapperElement()
-  }-*/;
-
-  public final native Element getGutterElement() /*-{
-    return this.getGutterElement()
-  }-*/;
-
-  public final native Element sizer() /*-{
-    return this.display.sizer
-  }-*/;
-
-  public final native Element mover() /*-{
-    return this.display.mover
-  }-*/;
-
-  public final native Element measure() /*-{
-    return this.display.measure
-  }-*/;
-
-  public final native Element scrollbarV() /*-{
-    return this.display.scrollbars.vert.node;
-  }-*/;
-
-  public final native void execCommand(String cmd) /*-{
-    this.execCommand(cmd)
-  }-*/;
-
-  public static final native KeyMap getKeyMap(String name) /*-{
-    return $wnd.CodeMirror.keyMap[name];
-  }-*/;
-
-  public static final native void addKeyMap(String name, KeyMap km) /*-{
-    $wnd.CodeMirror.keyMap[name] = km
-  }-*/;
-
-  public static final native void normalizeKeyMap(KeyMap km) /*-{
-    $wnd.CodeMirror.normalizeKeyMap(km);
-  }-*/;
-
-  public static final native void addCommand(String name, CommandRunner runner) /*-{
-    $wnd.CodeMirror.commands[name] = function(cm) {
-      runner.@net.codemirror.lib.CodeMirror.CommandRunner::run(
-        Lnet/codemirror/lib/CodeMirror;)(cm);
-    };
-  }-*/;
-
-  public final native Vim vim() /*-{
-    return this;
-  }-*/;
-
-  public final DisplaySide side() {
-    return extras().side();
-  }
-
-  public final Extras extras() {
-    return Extras.get(this);
-  }
-
-  public final native LineHandle setGutterMarker(int line, String gutterId, Element value) /*-{
-    return this.setGutterMarker(line, gutterId, value);
-  }-*/;
-
-  public final native LineHandle setGutterMarker(
-      LineHandle line, String gutterId, Element value) /*-{
-    return this.setGutterMarker(line, gutterId, value);
-  }-*/;
-
-  public final native boolean hasSearchHighlight() /*-{
-    return this.state.search && !!this.state.search.query;
-  }-*/;
-
-  protected CodeMirror() {}
-
-  public static class Viewport extends JavaScriptObject {
-    public final native int from() /*-{ return this.from }-*/;
-
-    public final native int to() /*-{ return this.to }-*/;
-
-    public final boolean contains(int line) {
-      return from() <= line && line < to();
-    }
-
-    protected Viewport() {}
-  }
-
-  public static class LineHandle extends JavaScriptObject {
-    protected LineHandle() {}
-  }
-
-  public static class RegisteredHandler extends JavaScriptObject {
-    protected RegisteredHandler() {}
-  }
-
-  public interface EventHandler {
-    void handle(CodeMirror instance, NativeEvent event);
-  }
-
-  public interface RenderLineHandler {
-    void handle(CodeMirror instance, LineHandle handle, Element element);
-  }
-
-  public interface GutterClickHandler {
-    void handle(CodeMirror instance, int line, String gutter, NativeEvent clickEvent);
-  }
-
-  public interface BeforeSelectionChangeHandler {
-    void handle(CodeMirror instance, Pos anchor, Pos head);
-  }
-
-  public interface ChangesHandler {
-    void handle(CodeMirror instance);
-  }
-
-  public interface CommandRunner {
-    void run(CodeMirror instance);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirrorDoc.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirrorDoc.java
deleted file mode 100644
index be1af05..0000000
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirrorDoc.java
+++ /dev/null
@@ -1,31 +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 net.codemirror.lib;
-
-import com.google.gwt.core.client.JavaScriptObject;
-
-/** The Doc object representing the content in a CodeMirror */
-public class CodeMirrorDoc extends JavaScriptObject {
-
-  public final native void replaceRange(String replacement, Pos from, Pos to) /*-{
-    this.replaceRange(replacement, from, to);
-  }-*/;
-
-  public final native void insertText(String insertion, Pos at) /*-{
-    this.replaceRange(insertion, at);
-  }-*/;
-
-  protected CodeMirrorDoc() {}
-}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/Configuration.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/Configuration.java
deleted file mode 100644
index d37b70b..0000000
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/Configuration.java
+++ /dev/null
@@ -1,46 +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 net.codemirror.lib;
-
-import com.google.gwt.core.client.JavaScriptObject;
-
-/**
- * Simple map-like structure to pass configuration to CodeMirror.
- *
- * @see <a href="http://codemirror.net/doc/manual.html#config">CodeMirror config</a>
- * @see CodeMirror#create(com.google.gwt.dom.client.Element, Configuration)
- */
-public class Configuration extends JavaScriptObject {
-  public static Configuration create() {
-    return createObject().cast();
-  }
-
-  public final native Configuration set(String name, String val)
-      /*-{ this[name] = val; return this; }-*/ ;
-
-  public final native Configuration set(String name, int val)
-      /*-{ this[name] = val; return this; }-*/ ;
-
-  public final native Configuration set(String name, double val)
-      /*-{ this[name] = val; return this; }-*/ ;
-
-  public final native Configuration set(String name, boolean val)
-      /*-{ this[name] = val; return this; }-*/ ;
-
-  public final native Configuration set(String name, JavaScriptObject val)
-      /*-{ this[name] = val; return this; }-*/ ;
-
-  protected Configuration() {}
-}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/Extras.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/Extras.java
deleted file mode 100644
index a5af703..0000000
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/Extras.java
+++ /dev/null
@@ -1,221 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package net.codemirror.lib;
-
-import static com.google.gwt.dom.client.Style.Display.INLINE_BLOCK;
-import static com.google.gwt.dom.client.Style.Unit.PX;
-import static net.codemirror.lib.CodeMirror.LineClassWhere.WRAP;
-import static net.codemirror.lib.CodeMirror.style;
-
-import com.google.gerrit.client.FormatUtil;
-import com.google.gerrit.client.RangeInfo;
-import com.google.gerrit.client.blame.BlameInfo;
-import com.google.gerrit.client.diff.DisplaySide;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.core.client.JsArrayString;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.i18n.client.DateTimeFormat;
-import com.google.gwt.user.client.DOM;
-import java.util.Date;
-import java.util.Objects;
-import net.codemirror.lib.CodeMirror.LineHandle;
-
-/** Additional features added to CodeMirror by Gerrit Code Review. */
-public class Extras {
-  private static final String ANNOTATION_GUTTER_ID = "CodeMirror-lint-markers";
-  private static final BlameConfig C = GWT.create(BlameConfig.class);
-
-  static final native Extras get(CodeMirror c) /*-{ return c.gerritExtras }-*/;
-
-  private static native void set(CodeMirror c, Extras e) /*-{ c.gerritExtras = e }-*/;
-
-  static void attach(CodeMirror c) {
-    set(c, new Extras(c));
-  }
-
-  private final CodeMirror cm;
-  private Element margin;
-  private DisplaySide side;
-  private double charWidthPx;
-  private double lineHeightPx;
-  private LineHandle activeLine;
-  private boolean annotated;
-
-  private Extras(CodeMirror cm) {
-    this.cm = cm;
-  }
-
-  public DisplaySide side() {
-    return side;
-  }
-
-  public void side(DisplaySide s) {
-    side = s;
-  }
-
-  public double charWidthPx() {
-    if (charWidthPx <= 1) {
-      int len = 100;
-      StringBuilder s = new StringBuilder();
-      for (int i = 0; i < len; i++) {
-        s.append('m');
-      }
-
-      Element e = DOM.createSpan();
-      e.getStyle().setDisplay(INLINE_BLOCK);
-      e.setInnerText(s.toString());
-
-      cm.measure().appendChild(e);
-      charWidthPx = ((double) e.getOffsetWidth()) / len;
-      e.removeFromParent();
-    }
-    return charWidthPx;
-  }
-
-  public double lineHeightPx() {
-    if (lineHeightPx <= 1) {
-      Element p = DOM.createDiv();
-      int lines = 1;
-      for (int i = 0; i < lines; i++) {
-        Element e = DOM.createDiv();
-        p.appendChild(e);
-
-        Element pre = DOM.createElement("pre");
-        pre.setInnerText("gqyŚŻŹŃ");
-        e.appendChild(pre);
-      }
-
-      cm.measure().appendChild(p);
-      lineHeightPx = ((double) p.getOffsetHeight()) / lines;
-      p.removeFromParent();
-    }
-    return lineHeightPx;
-  }
-
-  public void lineLength(int columns) {
-    if (margin == null) {
-      margin = DOM.createDiv();
-      margin.setClassName(style().margin());
-      cm.mover().appendChild(margin);
-    }
-    margin.getStyle().setMarginLeft(columns * charWidthPx(), PX);
-  }
-
-  public void showTabs(boolean show) {
-    Element e = cm.getWrapperElement();
-    if (show) {
-      e.addClassName(style().showTabs());
-    } else {
-      e.removeClassName(style().showTabs());
-    }
-  }
-
-  public final boolean hasActiveLine() {
-    return activeLine != null;
-  }
-
-  public final LineHandle activeLine() {
-    return activeLine;
-  }
-
-  public final boolean activeLine(LineHandle line) {
-    if (Objects.equals(activeLine, line)) {
-      return false;
-    }
-
-    if (activeLine != null) {
-      cm.removeLineClass(activeLine, WRAP, style().activeLine());
-    }
-    activeLine = line;
-    cm.addLineClass(activeLine, WRAP, style().activeLine());
-    return true;
-  }
-
-  public final void clearActiveLine() {
-    if (activeLine != null) {
-      cm.removeLineClass(activeLine, WRAP, style().activeLine());
-      activeLine = null;
-    }
-  }
-
-  public boolean isAnnotated() {
-    return annotated;
-  }
-
-  public final void clearAnnotations() {
-    JsArrayString gutters = ((JsArrayString) JsArrayString.createArray());
-    cm.setOption("gutters", gutters);
-    annotated = false;
-  }
-
-  public final void setAnnotations(JsArray<BlameInfo> blameInfos) {
-    if (blameInfos.length() > 0) {
-      setBlameInfo(blameInfos);
-      JsArrayString gutters = ((JsArrayString) JsArrayString.createArray());
-      gutters.push(ANNOTATION_GUTTER_ID);
-      cm.setOption("gutters", gutters);
-      annotated = true;
-      DateTimeFormat format = DateTimeFormat.getFormat(DateTimeFormat.PredefinedFormat.DATE_SHORT);
-      JsArray<LintLine> annotations = JsArray.createArray().cast();
-      for (BlameInfo blameInfo : Natives.asList(blameInfos)) {
-        for (RangeInfo range : Natives.asList(blameInfo.ranges())) {
-          Date commitTime = new Date(blameInfo.time() * 1000L);
-          String shortId = blameInfo.id().substring(0, 8);
-          String shortBlame =
-              C.shortBlameMsg(shortId, format.format(commitTime), blameInfo.author());
-          String detailedBlame =
-              C.detailedBlameMsg(
-                  blameInfo.id(),
-                  blameInfo.author(),
-                  FormatUtil.mediumFormat(commitTime),
-                  blameInfo.commitMsg());
-
-          annotations.push(
-              LintLine.create(shortBlame, detailedBlame, shortId, Pos.create(range.start() - 1)));
-        }
-      }
-      cm.setOption("lint", getAnnotation(annotations));
-    }
-  }
-
-  private native JavaScriptObject getAnnotation(JsArray<LintLine> annotations) /*-{
-     return {
-        getAnnotations: function(text, options, cm) { return annotations; }
-     };
-  }-*/;
-
-  public final native JsArray<BlameInfo> getBlameInfo() /*-{
-    return this.blameInfos;
-  }-*/;
-
-  public final native void setBlameInfo(JsArray<BlameInfo> blameInfos) /*-{
-    this['blameInfos'] = blameInfos;
-  }-*/;
-
-  public final void toggleAnnotation() {
-    toggleAnnotation(getBlameInfo());
-  }
-
-  public final void toggleAnnotation(JsArray<BlameInfo> blameInfos) {
-    if (isAnnotated()) {
-      clearAnnotations();
-    } else {
-      setAnnotations(blameInfos);
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/KeyMap.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/KeyMap.java
deleted file mode 100644
index be1852f..0000000
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/KeyMap.java
+++ /dev/null
@@ -1,43 +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 net.codemirror.lib;
-
-import com.google.gwt.core.client.JavaScriptObject;
-
-/** Object that associates a key or key combination with a handler. */
-public class KeyMap extends JavaScriptObject {
-  public static KeyMap create() {
-    return createObject().cast();
-  }
-
-  public final native KeyMap on(String key, Runnable thunk) /*-{
-    this[key] = function() { $entry(thunk.@java.lang.Runnable::run()()); };
-    return this;
-  }-*/;
-
-  /** Do not handle inside of CodeMirror; instead push up the DOM tree. */
-  public final native KeyMap propagate(String key) /*-{
-    this[key] = false;
-    return this;
-  }-*/;
-
-  /** Delegate undefined keys to another KeyMap implementation. */
-  public final native KeyMap fallthrough(KeyMap m) /*-{
-    this.fallthrough = m;
-    return this;
-  }-*/;
-
-  protected KeyMap() {}
-}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/Lib.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/Lib.java
deleted file mode 100644
index f205ef9..0000000
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/Lib.java
+++ /dev/null
@@ -1,35 +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 net.codemirror.lib;
-
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.resources.client.ClientBundle;
-import com.google.gwt.resources.client.DataResource;
-import com.google.gwt.resources.client.DataResource.DoNotEmbed;
-import com.google.gwt.resources.client.ExternalTextResource;
-
-interface Lib extends ClientBundle {
-  Lib I = GWT.create(Lib.class);
-
-  @Source("cm.css")
-  ExternalTextResource css();
-
-  @Source("cm.js")
-  @DoNotEmbed
-  DataResource js();
-
-  @Source("style.css")
-  CodeMirror.Style style();
-}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/LineWidget.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/LineWidget.java
deleted file mode 100644
index 4a15fd3..0000000
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/LineWidget.java
+++ /dev/null
@@ -1,41 +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 net.codemirror.lib;
-
-import com.google.gwt.core.client.JavaScriptObject;
-
-/** LineWidget objects used within CodeMirror. */
-public class LineWidget extends JavaScriptObject {
-  public final native void clear() /*-{ this.clear() }-*/;
-
-  public final native void changed() /*-{ this.changed() }-*/;
-
-  public final native void onRedraw(Runnable thunk) /*-{
-    this.on("redraw", $entry(function() {
-      thunk.@java.lang.Runnable::run()();
-    }))
-  }-*/;
-
-  public final native void onFirstRedraw(Runnable thunk) /*-{
-    var w = this;
-    var h = $entry(function() {
-      thunk.@java.lang.Runnable::run()();
-      w.off("redraw", h);
-    });
-    w.on("redraw", h);
-  }-*/;
-
-  protected LineWidget() {}
-}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/LintLine.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/LintLine.java
deleted file mode 100644
index b0b2ae0..0000000
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/LintLine.java
+++ /dev/null
@@ -1,63 +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 net.codemirror.lib;
-
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.dom.client.StyleInjector;
-
-public class LintLine extends JavaScriptObject {
-  public static LintLine create(String shortMsg, String msg, String sev, Pos line) {
-    StyleInjector.inject(
-        ".CodeMirror-lint-marker-"
-            + sev
-            + " {\n"
-            + "  visibility: hidden;\n"
-            + "  text-overflow: ellipsis;\n"
-            + "  white-space: nowrap;\n"
-            + "  overflow: hidden;\n"
-            + "  position: relative;\n"
-            + "}\n"
-            + ".CodeMirror-lint-marker-"
-            + sev
-            + ":after {\n"
-            + "  content:'"
-            + shortMsg
-            + "';\n"
-            + "  visibility: visible;\n"
-            + "}");
-    return create(msg, sev, line, null);
-  }
-
-  public static native LintLine create(String msg, String sev, Pos f, Pos t) /*-{
-    return {
-      message : msg,
-      severity : sev,
-      from : f,
-      to : t
-    };
-  }-*/;
-
-  public final native String message() /*-{ return this.message; }-*/;
-
-  public final native String detailedMessage() /*-{ return this.message; }-*/;
-
-  public final native String severity() /*-{ return this.severity; }-*/;
-
-  public final native Pos from() /*-{ return this.from; }-*/;
-
-  public final native Pos to() /*-{ return this.to; }-*/;
-
-  protected LintLine() {}
-}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/Loader.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/Loader.java
deleted file mode 100644
index 01bc7e2..0000000
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/Loader.java
+++ /dev/null
@@ -1,100 +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 net.codemirror.lib;
-
-import com.google.gerrit.client.rpc.CallbackGroup;
-import com.google.gwt.core.client.Callback;
-import com.google.gwt.core.client.ScriptInjector;
-import com.google.gwt.dom.client.ScriptElement;
-import com.google.gwt.dom.client.StyleInjector;
-import com.google.gwt.resources.client.ExternalTextResource;
-import com.google.gwt.resources.client.ResourceCallback;
-import com.google.gwt.resources.client.ResourceException;
-import com.google.gwt.resources.client.TextResource;
-import com.google.gwt.safehtml.shared.SafeUri;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-
-public class Loader {
-  private static native boolean isLibLoaded() /*-{ return $wnd.hasOwnProperty('CodeMirror'); }-*/;
-
-  static void initLibrary(AsyncCallback<Void> cb) {
-    if (isLibLoaded()) {
-      cb.onSuccess(null);
-      return;
-    }
-
-    CallbackGroup group = new CallbackGroup();
-    injectCss(Lib.I.css(), group.<Void>addEmpty());
-    injectScript(
-        Lib.I.js().getSafeUri(),
-        group.add(
-            new AsyncCallback<Void>() {
-              @Override
-              public void onSuccess(Void result) {
-                Vim.initKeyMap();
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {}
-            }));
-    group.addListener(cb);
-    group.done();
-  }
-
-  private static void injectCss(ExternalTextResource css, AsyncCallback<Void> cb) {
-    try {
-      css.getText(
-          new ResourceCallback<TextResource>() {
-            @Override
-            public void onSuccess(TextResource resource) {
-              StyleInjector.inject(resource.getText());
-              Lib.I.style().ensureInjected();
-              cb.onSuccess(null);
-            }
-
-            @Override
-            public void onError(ResourceException e) {
-              cb.onFailure(e);
-            }
-          });
-    } catch (ResourceException e) {
-      cb.onFailure(e);
-    }
-  }
-
-  public static void injectScript(SafeUri js, AsyncCallback<Void> callback) {
-    final ScriptElement[] script = new ScriptElement[1];
-    script[0] =
-        ScriptInjector.fromUrl(js.asString())
-            .setWindow(ScriptInjector.TOP_WINDOW)
-            .setCallback(
-                new Callback<Void, Exception>() {
-                  @Override
-                  public void onSuccess(Void result) {
-                    script[0].removeFromParent();
-                    callback.onSuccess(result);
-                  }
-
-                  @Override
-                  public void onFailure(Exception reason) {
-                    callback.onFailure(reason);
-                  }
-                })
-            .inject()
-            .cast();
-  }
-
-  private Loader() {}
-}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/MergeView.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/MergeView.java
deleted file mode 100644
index 38c3906..0000000
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/MergeView.java
+++ /dev/null
@@ -1,50 +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 net.codemirror.lib;
-
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.dom.client.Element;
-
-/** Object that represents a text marker within CodeMirror */
-public class MergeView extends JavaScriptObject {
-  public static MergeView create(Element p, Configuration cfg) {
-    MergeView mv = newMergeView(p, cfg);
-    Extras.attach(mv.leftOriginal());
-    Extras.attach(mv.editor());
-    return mv;
-  }
-
-  private static native MergeView newMergeView(Element p, Configuration cfg) /*-{
-    return $wnd.CodeMirror.MergeView(p, cfg);
-  }-*/;
-
-  public final native CodeMirror leftOriginal() /*-{
-    return this.leftOriginal();
-  }-*/;
-
-  public final native CodeMirror editor() /*-{
-    return this.editor();
-  }-*/;
-
-  public final native void setShowDifferences(boolean b) /*-{
-    this.setShowDifferences(b);
-  }-*/;
-
-  public final native Element getGapElement() /*-{
-    return $doc.getElementsByClassName("CodeMirror-merge-gap")[0];
-  }-*/;
-
-  protected MergeView() {}
-}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/Pos.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/Pos.java
deleted file mode 100644
index 7f83b75..0000000
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/Pos.java
+++ /dev/null
@@ -1,42 +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 net.codemirror.lib;
-
-import com.google.gwt.core.client.JavaScriptObject;
-
-/** Pos (or {line, ch}) objects used within CodeMirror. */
-public class Pos extends JavaScriptObject {
-  public static final native Pos create(int line) /*-{
-    return $wnd.CodeMirror.Pos(line)
-  }-*/;
-
-  public static final native Pos create(int line, int ch) /*-{
-    return $wnd.CodeMirror.Pos(line, ch)
-  }-*/;
-
-  public final native void line(int l) /*-{ this.line = l }-*/;
-
-  public final native void ch(int c) /*-{ this.ch = c }-*/;
-
-  public final native int line() /*-{ return this.line }-*/;
-
-  public final native int ch() /*-{ return this.ch || 0 }-*/;
-
-  public final boolean equals(Pos o) {
-    return this == o || (line() == o.line() && ch() == o.ch());
-  }
-
-  protected Pos() {}
-}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/Rect.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/Rect.java
deleted file mode 100644
index 1114403..0000000
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/Rect.java
+++ /dev/null
@@ -1,30 +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 net.codemirror.lib;
-
-import com.google.gwt.core.client.JavaScriptObject;
-
-/** {left, right, top, bottom} objects used within CodeMirror. */
-public class Rect extends JavaScriptObject {
-  public final native double left() /*-{ return this.left }-*/;
-
-  public final native double right() /*-{ return this.right }-*/;
-
-  public final native double top() /*-{ return this.top }-*/;
-
-  public final native double bottom() /*-{ return this.bottom }-*/;
-
-  protected Rect() {}
-}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/ScrollInfo.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/ScrollInfo.java
deleted file mode 100644
index 432a60f..0000000
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/ScrollInfo.java
+++ /dev/null
@@ -1,40 +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 net.codemirror.lib;
-
-import com.google.gwt.core.client.JavaScriptObject;
-
-/** Returned by {@link CodeMirror#getScrollInfo()}. */
-public class ScrollInfo extends JavaScriptObject {
-  public final native double left() /*-{ return this.left }-*/;
-
-  public final native double top() /*-{ return this.top }-*/;
-
-  /**
-   * Pixel height of the full content being scrolled. This may only be an estimate given by
-   * CodeMirror. Line widgets further down in the document may not be measured, so line heights can
-   * be incorrect until drawn.
-   */
-  public final native double height() /*-{ return this.height }-*/;
-
-  public final native double width() /*-{ return this.width }-*/;
-
-  /** Visible height of the viewport, excluding scrollbars. */
-  public final native double clientHeight() /*-{ return this.clientHeight }-*/;
-
-  public final native double clientWidth() /*-{ return this.clientWidth }-*/;
-
-  protected ScrollInfo() {}
-}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/TextMarker.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/TextMarker.java
deleted file mode 100644
index c3d248a..0000000
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/TextMarker.java
+++ /dev/null
@@ -1,54 +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 net.codemirror.lib;
-
-import com.google.gerrit.client.diff.CommentRange;
-import com.google.gwt.core.client.JavaScriptObject;
-
-/** Object that represents a text marker within CodeMirror */
-public class TextMarker extends JavaScriptObject {
-  public final native void clear() /*-{ this.clear(); }-*/;
-
-  public final native void changed() /*-{ this.changed(); }-*/;
-
-  public final native FromTo find() /*-{ return this.find(); }-*/;
-
-  public final native void on(String event, Runnable thunk)
-      /*-{ this.on(event, function(){$entry(thunk.@java.lang.Runnable::run()())}) }-*/ ;
-
-  protected TextMarker() {}
-
-  public static class FromTo extends JavaScriptObject {
-    public static final native FromTo create(Pos f, Pos t) /*-{
-      return {from: f, to: t}
-    }-*/;
-
-    public static FromTo create(CommentRange range) {
-      return create(
-          Pos.create(range.startLine() - 1, range.startCharacter()),
-          Pos.create(range.endLine() - 1, range.endCharacter()));
-    }
-
-    public final native Pos from() /*-{ return this.from }-*/;
-
-    public final native Pos to() /*-{ return this.to }-*/;
-
-    public final native void from(Pos f) /*-{ this.from = f }-*/;
-
-    public final native void to(Pos t) /*-{ this.to = t }-*/;
-
-    protected FromTo() {}
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/Vim.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/Vim.java
deleted file mode 100644
index 06016ef..0000000
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/Vim.java
+++ /dev/null
@@ -1,70 +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 net.codemirror.lib;
-
-import com.google.gwt.core.client.JavaScriptObject;
-
-/**
- * Glue around the Vim emulation for {@link CodeMirror}.
- *
- * <p>As an instance {@code this} is actually the {@link CodeMirror} object. Class Vim is providing
- * a new namespace for Vim related methods that are associated with an editor.
- */
-public class Vim extends JavaScriptObject {
-  static void initKeyMap() {
-    KeyMap km = KeyMap.create();
-    for (String key : new String[] {"A", "C", "I", "O", "R", "U"}) {
-      km.propagate(key);
-      km.propagate("'" + key.toLowerCase() + "'");
-    }
-    for (String key :
-        new String[] {
-          "Ctrl-C", "Ctrl-O", "Ctrl-P", "Ctrl-S", "Ctrl-F", "Ctrl-B", "Ctrl-R",
-        }) {
-      km.propagate(key);
-    }
-    for (int i = 0; i <= 9; i++) {
-      km.propagate("Ctrl-" + i);
-    }
-    km.fallthrough(CodeMirror.getKeyMap("vim"));
-    CodeMirror.addKeyMap("vim_ro", km);
-
-    mapKey("j", "gj");
-    mapKey("k", "gk");
-    mapKey("Down", "gj");
-    mapKey("Up", "gk");
-    mapKey("<PageUp>", "<C-u>");
-    mapKey("<PageDown>", "<C-d>");
-  }
-
-  public static final native void mapKey(String alias, String actual) /*-{
-    $wnd.CodeMirror.Vim.map(alias, actual)
-  }-*/;
-
-  public final native void handleKey(String key) /*-{
-    $wnd.CodeMirror.Vim.handleKey(this, key)
-  }-*/;
-
-  public final native void handleEx(String exCommand) /*-{
-    $wnd.CodeMirror.Vim.handleEx(this, exCommand);
-  }-*/;
-
-  public final native boolean hasSearchHighlight() /*-{
-    var v = this.state.vim;
-    return v && v.searchState_ && !!v.searchState_.getOverlay();
-  }-*/;
-
-  protected Vim() {}
-}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/style.css b/gerrit-gwtui/src/main/java/net/codemirror/lib/style.css
deleted file mode 100644
index 022a800..0000000
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/style.css
+++ /dev/null
@@ -1,124 +0,0 @@
-/* Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@external .CodeMirror;
-@external .CodeMirror-lines;
-@external .CodeMirror-linenumber;
-@external .CodeMirror-lint-markers;
-@external .CodeMirror-lint-tooltip;
-@external .CodeMirror-overlayscroll-horizontal;
-@external .CodeMirror-overlayscroll-vertical;
-@external .CodeMirror-scrollbar-filler;
-@external .cm-tab;
-@external .cm-searching;
-@external .cm-trailingspace;
-@external .unifiedLineNumber;
-
-/* Reduce margins around CodeMirror to save space. */
-.CodeMirror-lines {
-  padding: 0;
-}
-.CodeMirror pre {
-  padding: 0;
-  line-height: normal;
-}
-
-/* Minimum scrollbar bubble size even on large files. */
-.CodeMirror-overlayscroll-horizontal div {
-  min-width: 25px;
-}
-.CodeMirror-overlayscroll-vertical div {
-  min-height: 25px;
-}
-/* Ensure the scrollbars are not too narrow */
-.CodeMirror-overlayscroll-horizontal {
-  min-height: 12px;
-}
-.CodeMirror-overlayscroll-vertical {
-  min-width: 12px;
-}
-.CodeMirror-scrollbar-filler {
-  min-height: 12px;
-  min-width: 12px;
-}
-/* Stack the scrollbar so annotations can receive clicks. */
-.CodeMirror-overlayscroll-vertical {
-  z-index: inherit;
-}
-.CodeMirror-overlayscroll-horizontal div,
-.CodeMirror-overlayscroll-vertical div {
-  background-color: rgba(128, 128, 128, 0.50);
-  z-index: 8;
-}
-
-/* Highlight current line number in the line gutter. */
-.activeLine .CodeMirror-linenumber,
-.activeLine .unifiedLineNumber {
-  background-color: #bcf !important;
-  color: #000;
-}
-
-.showTabs .cm-tab:before {
-  position: absolute;
-  content: "\00bb";
-  color: #f00;
-}
-
-.cm-searching {
-  background-color: #ffa !important;
-}
-
-.cm-trailingspace {
-  background-color: red !important;
-}
-
-/* Line length margin displayed at NN columns to provide
- * a visual guide for length of any single line of code.
- */
-.margin {
-  position: absolute;
-  top: 0;
-  bottom: 0;
-  width: 0;
-  border-right: 1px dashed #ffa500;
-  z-index: 2;
-  cursor: text;
-}
-
-.CodeMirror-lint-markers {
-  width: 250px;
-}
-
-.CodeMirror-lint-tooltip {
-  background-color: infobackground;
-  border: 1px solid black;
-  border-radius: 4px 4px 4px 4px;
-  color: infotext;
-  font-family: monospace;
-  font-size: 10pt;
-  overflow: hidden;
-  padding: 2px 5px;
-  position: fixed;
-  white-space: pre;
-  white-space: pre-wrap;
-  z-index: 100;
-  max-width: 600px;
-  opacity: 0;
-  transition: opacity .4s;
-  -moz-transition: opacity .4s;
-  -webkit-transition: opacity .4s;
-  -o-transition: opacity .4s;
-  -ms-transition: opacity .4s;
-}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java b/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java
deleted file mode 100644
index 9df066d..0000000
--- a/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java
+++ /dev/null
@@ -1,270 +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 net.codemirror.mode;
-
-import static java.util.Comparator.comparing;
-
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.core.client.JsArrayString;
-import com.google.gwt.resources.client.DataResource;
-import com.google.gwt.safehtml.shared.SafeUri;
-import java.util.HashMap;
-import java.util.Map;
-
-/** Description of a CodeMirror language mode. */
-public class ModeInfo extends JavaScriptObject {
-  private static NativeMap<ModeInfo> byMime;
-  private static NativeMap<ModeInfo> byExt;
-
-  /** Map of names such as "clike" to URI for code download. */
-  private static final Map<String, SafeUri> modeUris = new HashMap<>();
-
-  static {
-    indexModes(
-        new DataResource[] {
-          Modes.I.apl(),
-          Modes.I.asciiarmor(),
-          Modes.I.asn_1(),
-          Modes.I.asterisk(),
-          Modes.I.brainfuck(),
-          Modes.I.clike(),
-          Modes.I.clojure(),
-          Modes.I.cmake(),
-          Modes.I.cobol(),
-          Modes.I.coffeescript(),
-          Modes.I.commonlisp(),
-          Modes.I.crystal(),
-          Modes.I.css(),
-          Modes.I.cypher(),
-          Modes.I.d(),
-          Modes.I.dart(),
-          Modes.I.diff(),
-          Modes.I.django(),
-          Modes.I.dockerfile(),
-          Modes.I.dtd(),
-          Modes.I.dylan(),
-          Modes.I.ebnf(),
-          Modes.I.ecl(),
-          Modes.I.eiffel(),
-          Modes.I.elm(),
-          Modes.I.erlang(),
-          Modes.I.factor(),
-          Modes.I.fcl(),
-          Modes.I.forth(),
-          Modes.I.fortran(),
-          Modes.I.gas(),
-          Modes.I.gerrit_commit(),
-          Modes.I.gfm(),
-          Modes.I.gherkin(),
-          Modes.I.go(),
-          Modes.I.groovy(),
-          Modes.I.haml(),
-          Modes.I.handlebars(),
-          Modes.I.haskell_literate(),
-          Modes.I.haskell(),
-          Modes.I.haxe(),
-          Modes.I.htmlembedded(),
-          Modes.I.htmlmixed(),
-          Modes.I.http(),
-          Modes.I.idl(),
-          Modes.I.javascript(),
-          Modes.I.jinja2(),
-          Modes.I.jsx(),
-          Modes.I.julia(),
-          Modes.I.livescript(),
-          Modes.I.lua(),
-          Modes.I.markdown(),
-          Modes.I.mathematica(),
-          Modes.I.mbox(),
-          Modes.I.mirc(),
-          Modes.I.mllike(),
-          Modes.I.modelica(),
-          Modes.I.mscgen(),
-          Modes.I.mumps(),
-          Modes.I.nginx(),
-          Modes.I.nsis(),
-          Modes.I.ntriples(),
-          Modes.I.octave(),
-          Modes.I.oz(),
-          Modes.I.pascal(),
-          Modes.I.pegjs(),
-          Modes.I.perl(),
-          Modes.I.php(),
-          Modes.I.pig(),
-          Modes.I.powershell(),
-          Modes.I.properties(),
-          Modes.I.protobuf(),
-          Modes.I.pug(),
-          Modes.I.puppet(),
-          Modes.I.python(),
-          Modes.I.q(),
-          Modes.I.r(),
-          Modes.I.rpm(),
-          Modes.I.rst(),
-          Modes.I.ruby(),
-          Modes.I.rust(),
-          Modes.I.sas(),
-          Modes.I.sass(),
-          Modes.I.scheme(),
-          Modes.I.shell(),
-          Modes.I.smalltalk(),
-          Modes.I.smarty(),
-          Modes.I.solr(),
-          Modes.I.soy(),
-          Modes.I.sparql(),
-          Modes.I.spreadsheet(),
-          Modes.I.sql(),
-          Modes.I.stex(),
-          Modes.I.stylus(),
-          Modes.I.swift(),
-          Modes.I.tcl(),
-          Modes.I.textile(),
-          Modes.I.tiddlywiki(),
-          Modes.I.tiki(),
-          Modes.I.toml(),
-          Modes.I.tornado(),
-          Modes.I.troff(),
-          Modes.I.ttcn_cfg(),
-          Modes.I.ttcn(),
-          Modes.I.turtle(),
-          Modes.I.twig(),
-          Modes.I.vb(),
-          Modes.I.vbscript(),
-          Modes.I.velocity(),
-          Modes.I.verilog(),
-          Modes.I.vhdl(),
-          Modes.I.vue(),
-          Modes.I.webidl(),
-          Modes.I.xml(),
-          Modes.I.xquery(),
-          Modes.I.yacas(),
-          Modes.I.yaml_frontmatter(),
-          Modes.I.yaml(),
-          Modes.I.z80(),
-        });
-
-    alias("application/x-httpd-php-open", "application/x-httpd-php");
-    alias("application/x-javascript", "application/javascript");
-    alias("application/x-shellscript", "text/x-sh");
-    alias("application/x-tcl", "text/x-tcl");
-    alias("text/typescript", "application/typescript");
-    alias("text/x-c", "text/x-csrc");
-    alias("text/x-c++hdr", "text/x-c++src");
-    alias("text/x-chdr", "text/x-csrc");
-    alias("text/x-h", "text/x-csrc");
-    alias("text/x-ini", "text/x-properties");
-    alias("text/x-java-source", "text/x-java");
-    alias("text/x-php", "application/x-httpd-php");
-    alias("text/x-scripttcl", "text/x-tcl");
-  }
-
-  /** All supported modes. */
-  public static native JsArray<ModeInfo> all() /*-{
-    return $wnd.CodeMirror.modeInfo
-  }-*/;
-
-  private static native void setAll(JsArray<ModeInfo> m) /*-{
-    $wnd.CodeMirror.modeInfo = m
-  }-*/;
-
-  /** Look up mode by primary or alternate MIME types. */
-  public static ModeInfo findModeByMIME(String mime) {
-    return byMime.get(mime);
-  }
-
-  public static SafeUri getModeScriptUri(String mode) {
-    return modeUris.get(mode);
-  }
-
-  /** Look up mode by MIME type or file extension from a path. */
-  public static ModeInfo findMode(String mime, String path) {
-    ModeInfo m = byMime.get(mime);
-    if (m != null) {
-      return m;
-    }
-
-    int s = path.lastIndexOf('/');
-    int d = path.lastIndexOf('.');
-    if (d == -1 || s > d) {
-      return null; // punt on "foo.src/bar" type paths.
-    }
-
-    if (byExt == null) {
-      byExt = NativeMap.create();
-      for (ModeInfo mode : Natives.asList(all())) {
-        for (String ext : Natives.asList(mode.ext())) {
-          byExt.put(ext, mode);
-        }
-      }
-    }
-    return byExt.get(path.substring(d + 1));
-  }
-
-  private static void alias(String serverMime, String toMime) {
-    ModeInfo mode = byMime.get(toMime);
-    if (mode != null) {
-      byMime.put(serverMime, mode);
-    }
-  }
-
-  private static void indexModes(DataResource[] all) {
-    for (DataResource r : all) {
-      modeUris.put(r.getName(), r.getSafeUri());
-    }
-
-    JsArray<ModeInfo> modeList = all();
-    modeList.push(gerrit_commit());
-
-    byMime = NativeMap.create();
-    JsArray<ModeInfo> filtered = JsArray.createArray().cast();
-    for (ModeInfo m : Natives.asList(modeList)) {
-      if (modeUris.containsKey(m.mode())) {
-        filtered.push(m);
-
-        for (String mimeType : Natives.asList(m.mimes())) {
-          byMime.put(mimeType, m);
-        }
-        byMime.put(m.mode(), m);
-      }
-    }
-    Natives.asList(filtered).sort(comparing(m -> m.name().toLowerCase()));
-    setAll(filtered);
-  }
-
-  /** Human readable name of the mode, such as "C++". */
-  public final native String name() /*-{ return this.name }-*/;
-
-  /** Internal CodeMirror name for {@code mode.js} file to load. */
-  public final native String mode() /*-{ return this.mode }-*/;
-
-  /** Primary MIME type to activate this mode. */
-  public final native String mime() /*-{ return this.mime }-*/;
-
-  /** Primary and additional MIME types that activate this mode. */
-  public final native JsArrayString mimes() /*-{ return this.mimes || [this.mime] }-*/;
-
-  private native JsArrayString ext() /*-{ return this.ext || [] }-*/;
-
-  protected ModeInfo() {}
-
-  private static native ModeInfo gerrit_commit() /*-{
-    return {name: "Git Commit Message",
-            mime: "text/x-gerrit-commit-message",
-            mode: "gerrit_commit"}
-  }-*/;
-}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInjector.java b/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInjector.java
deleted file mode 100644
index 5fda608..0000000
--- a/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInjector.java
+++ /dev/null
@@ -1,114 +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 net.codemirror.mode;
-
-import com.google.gwt.core.client.JsArrayString;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import java.util.HashSet;
-import java.util.Set;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import net.codemirror.lib.Loader;
-
-public class ModeInjector {
-  private static boolean canLoad(String mode) {
-    return ModeInfo.getModeScriptUri(mode) != null;
-  }
-
-  private static native boolean isModeLoaded(String n)
-      /*-{ return $wnd.CodeMirror.modes.hasOwnProperty(n); }-*/ ;
-
-  private static native boolean isMimeLoaded(String n)
-      /*-{ return $wnd.CodeMirror.mimeModes.hasOwnProperty(n); }-*/ ;
-
-  private static native JsArrayString getDependencies(String n)
-      /*-{ return $wnd.CodeMirror.modes[n].dependencies || []; }-*/ ;
-
-  private final Set<String> loading = new HashSet<>(4);
-  private int pending;
-  private AsyncCallback<Void> appCallback;
-
-  public ModeInjector add(String name) {
-    if (name == null || isModeLoaded(name) || isMimeLoaded(name)) {
-      return this;
-    }
-
-    ModeInfo m = ModeInfo.findModeByMIME(name);
-    if (m != null) {
-      name = m.mode();
-    }
-
-    if (!canLoad(name)) {
-      Logger.getLogger("net.codemirror")
-          .log(Level.WARNING, "CodeMirror mode " + name + " not configured.");
-      return this;
-    }
-
-    loading.add(name);
-    return this;
-  }
-
-  public void inject(AsyncCallback<Void> appCallback) {
-    this.appCallback = appCallback;
-    for (String mode : loading) {
-      beginLoading(mode);
-    }
-    if (pending == 0) {
-      appCallback.onSuccess(null);
-    }
-  }
-
-  private void beginLoading(String mode) {
-    pending++;
-    Loader.injectScript(
-        ModeInfo.getModeScriptUri(mode),
-        new AsyncCallback<Void>() {
-          @Override
-          public void onSuccess(Void result) {
-            pending--;
-            ensureDependenciesAreLoaded(mode);
-            if (pending == 0) {
-              appCallback.onSuccess(null);
-            }
-          }
-
-          @Override
-          public void onFailure(Throwable caught) {
-            if (--pending == 0) {
-              appCallback.onFailure(caught);
-            }
-          }
-        });
-  }
-
-  private void ensureDependenciesAreLoaded(String mode) {
-    JsArrayString deps = getDependencies(mode);
-    for (int i = 0; i < deps.length(); i++) {
-      String d = deps.get(i);
-      if (loading.contains(d) || isModeLoaded(d)) {
-        continue;
-      }
-
-      if (!canLoad(d)) {
-        Logger.getLogger("net.codemirror")
-            .log(Level.SEVERE, "CodeMirror mode " + d + " needs " + d);
-        continue;
-      }
-
-      loading.add(d);
-      beginLoading(d);
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java b/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java
deleted file mode 100644
index 2b87a34..0000000
--- a/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java
+++ /dev/null
@@ -1,510 +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 net.codemirror.mode;
-
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.resources.client.ClientBundle;
-import com.google.gwt.resources.client.DataResource;
-import com.google.gwt.resources.client.DataResource.DoNotEmbed;
-
-public interface Modes extends ClientBundle {
-  Modes I = GWT.create(Modes.class);
-
-  @Source("apl.js")
-  @DoNotEmbed
-  DataResource apl();
-
-  @Source("asciiarmor.js")
-  @DoNotEmbed
-  DataResource asciiarmor();
-
-  @Source("asn.1.js")
-  @DoNotEmbed
-  DataResource asn_1();
-
-  @Source("asterisk.js")
-  @DoNotEmbed
-  DataResource asterisk();
-
-  @Source("brainfuck.js")
-  @DoNotEmbed
-  DataResource brainfuck();
-
-  @Source("clike.js")
-  @DoNotEmbed
-  DataResource clike();
-
-  @Source("clojure.js")
-  @DoNotEmbed
-  DataResource clojure();
-
-  @Source("cmake.js")
-  @DoNotEmbed
-  DataResource cmake();
-
-  @Source("cobol.js")
-  @DoNotEmbed
-  DataResource cobol();
-
-  @Source("coffeescript.js")
-  @DoNotEmbed
-  DataResource coffeescript();
-
-  @Source("commonlisp.js")
-  @DoNotEmbed
-  DataResource commonlisp();
-
-  @Source("crystal.js")
-  @DoNotEmbed
-  DataResource crystal();
-
-  @Source("css.js")
-  @DoNotEmbed
-  DataResource css();
-
-  @Source("cypher.js")
-  @DoNotEmbed
-  DataResource cypher();
-
-  @Source("d.js")
-  @DoNotEmbed
-  DataResource d();
-
-  @Source("dart.js")
-  @DoNotEmbed
-  DataResource dart();
-
-  @Source("diff.js")
-  @DoNotEmbed
-  DataResource diff();
-
-  @Source("django.js")
-  @DoNotEmbed
-  DataResource django();
-
-  @Source("dockerfile.js")
-  @DoNotEmbed
-  DataResource dockerfile();
-
-  @Source("dtd.js")
-  @DoNotEmbed
-  DataResource dtd();
-
-  @Source("dylan.js")
-  @DoNotEmbed
-  DataResource dylan();
-
-  @Source("ebnf.js")
-  @DoNotEmbed
-  DataResource ebnf();
-
-  @Source("ecl.js")
-  @DoNotEmbed
-  DataResource ecl();
-
-  @Source("eiffel.js")
-  @DoNotEmbed
-  DataResource eiffel();
-
-  @Source("elm.js")
-  @DoNotEmbed
-  DataResource elm();
-
-  @Source("erlang.js")
-  @DoNotEmbed
-  DataResource erlang();
-
-  @Source("factor.js")
-  @DoNotEmbed
-  DataResource factor();
-
-  @Source("fcl.js")
-  @DoNotEmbed
-  DataResource fcl();
-
-  @Source("forth.js")
-  @DoNotEmbed
-  DataResource forth();
-
-  @Source("fortran.js")
-  @DoNotEmbed
-  DataResource fortran();
-
-  @Source("gas.js")
-  @DoNotEmbed
-  DataResource gas();
-
-  @Source("gerrit/commit.js")
-  @DoNotEmbed
-  DataResource gerrit_commit();
-
-  @Source("gfm.js")
-  @DoNotEmbed
-  DataResource gfm();
-
-  @Source("gherkin.js")
-  @DoNotEmbed
-  DataResource gherkin();
-
-  @Source("go.js")
-  @DoNotEmbed
-  DataResource go();
-
-  @Source("groovy.js")
-  @DoNotEmbed
-  DataResource groovy();
-
-  @Source("haml.js")
-  @DoNotEmbed
-  DataResource haml();
-
-  @Source("handlebars.js")
-  @DoNotEmbed
-  DataResource handlebars();
-
-  @Source("haskell-literate.js")
-  @DoNotEmbed
-  DataResource haskell_literate();
-
-  @Source("haskell.js")
-  @DoNotEmbed
-  DataResource haskell();
-
-  @Source("haxe.js")
-  @DoNotEmbed
-  DataResource haxe();
-
-  @Source("htmlembedded.js")
-  @DoNotEmbed
-  DataResource htmlembedded();
-
-  @Source("htmlmixed.js")
-  @DoNotEmbed
-  DataResource htmlmixed();
-
-  @Source("http.js")
-  @DoNotEmbed
-  DataResource http();
-
-  @Source("idl.js")
-  @DoNotEmbed
-  DataResource idl();
-
-  @Source("javascript.js")
-  @DoNotEmbed
-  DataResource javascript();
-
-  @Source("jinja2.js")
-  @DoNotEmbed
-  DataResource jinja2();
-
-  @Source("jsx.js")
-  @DoNotEmbed
-  DataResource jsx();
-
-  @Source("julia.js")
-  @DoNotEmbed
-  DataResource julia();
-
-  @Source("livescript.js")
-  @DoNotEmbed
-  DataResource livescript();
-
-  @Source("lua.js")
-  @DoNotEmbed
-  DataResource lua();
-
-  @Source("markdown.js")
-  @DoNotEmbed
-  DataResource markdown();
-
-  @Source("mathematica.js")
-  @DoNotEmbed
-  DataResource mathematica();
-
-  @Source("mbox.js")
-  @DoNotEmbed
-  DataResource mbox();
-
-  @Source("mirc.js")
-  @DoNotEmbed
-  DataResource mirc();
-
-  @Source("mllike.js")
-  @DoNotEmbed
-  DataResource mllike();
-
-  @Source("modelica.js")
-  @DoNotEmbed
-  DataResource modelica();
-
-  @Source("mscgen.js")
-  @DoNotEmbed
-  DataResource mscgen();
-
-  @Source("mumps.js")
-  @DoNotEmbed
-  DataResource mumps();
-
-  @Source("nginx.js")
-  @DoNotEmbed
-  DataResource nginx();
-
-  @Source("nsis.js")
-  @DoNotEmbed
-  DataResource nsis();
-
-  @Source("ntriples.js")
-  @DoNotEmbed
-  DataResource ntriples();
-
-  @Source("octave.js")
-  @DoNotEmbed
-  DataResource octave();
-
-  @Source("oz.js")
-  @DoNotEmbed
-  DataResource oz();
-
-  @Source("pascal.js")
-  @DoNotEmbed
-  DataResource pascal();
-
-  @Source("pegjs.js")
-  @DoNotEmbed
-  DataResource pegjs();
-
-  @Source("perl.js")
-  @DoNotEmbed
-  DataResource perl();
-
-  @Source("php.js")
-  @DoNotEmbed
-  DataResource php();
-
-  @Source("pig.js")
-  @DoNotEmbed
-  DataResource pig();
-
-  @Source("powershell.js")
-  @DoNotEmbed
-  DataResource powershell();
-
-  @Source("properties.js")
-  @DoNotEmbed
-  DataResource properties();
-
-  @Source("protobuf.js")
-  @DoNotEmbed
-  DataResource protobuf();
-
-  @Source("pug.js")
-  @DoNotEmbed
-  DataResource pug();
-
-  @Source("puppet.js")
-  @DoNotEmbed
-  DataResource puppet();
-
-  @Source("python.js")
-  @DoNotEmbed
-  DataResource python();
-
-  @Source("q.js")
-  @DoNotEmbed
-  DataResource q();
-
-  @Source("r.js")
-  @DoNotEmbed
-  DataResource r();
-
-  @Source("rpm.js")
-  @DoNotEmbed
-  DataResource rpm();
-
-  @Source("rst.js")
-  @DoNotEmbed
-  DataResource rst();
-
-  @Source("ruby.js")
-  @DoNotEmbed
-  DataResource ruby();
-
-  @Source("rust.js")
-  @DoNotEmbed
-  DataResource rust();
-
-  @Source("sas.js")
-  @DoNotEmbed
-  DataResource sas();
-
-  @Source("sass.js")
-  @DoNotEmbed
-  DataResource sass();
-
-  @Source("scheme.js")
-  @DoNotEmbed
-  DataResource scheme();
-
-  @Source("shell.js")
-  @DoNotEmbed
-  DataResource shell();
-
-  @Source("sieve.js")
-  @DoNotEmbed
-  DataResource sieve();
-
-  @Source("slim.js")
-  @DoNotEmbed
-  DataResource slim();
-
-  @Source("smalltalk.js")
-  @DoNotEmbed
-  DataResource smalltalk();
-
-  @Source("smarty.js")
-  @DoNotEmbed
-  DataResource smarty();
-
-  @Source("solr.js")
-  @DoNotEmbed
-  DataResource solr();
-
-  @Source("soy.js")
-  @DoNotEmbed
-  DataResource soy();
-
-  @Source("sparql.js")
-  @DoNotEmbed
-  DataResource sparql();
-
-  @Source("spreadsheet.js")
-  @DoNotEmbed
-  DataResource spreadsheet();
-
-  @Source("sql.js")
-  @DoNotEmbed
-  DataResource sql();
-
-  @Source("stex.js")
-  @DoNotEmbed
-  DataResource stex();
-
-  @Source("stylus.js")
-  @DoNotEmbed
-  DataResource stylus();
-
-  @Source("swift.js")
-  @DoNotEmbed
-  DataResource swift();
-
-  @Source("tcl.js")
-  @DoNotEmbed
-  DataResource tcl();
-
-  @Source("textile.js")
-  @DoNotEmbed
-  DataResource textile();
-
-  @Source("tiddlywiki.js")
-  @DoNotEmbed
-  DataResource tiddlywiki();
-
-  @Source("tiki.js")
-  @DoNotEmbed
-  DataResource tiki();
-
-  @Source("toml.js")
-  @DoNotEmbed
-  DataResource toml();
-
-  @Source("tornado.js")
-  @DoNotEmbed
-  DataResource tornado();
-
-  @Source("troff.js")
-  @DoNotEmbed
-  DataResource troff();
-
-  @Source("ttcn-cfg.js")
-  @DoNotEmbed
-  DataResource ttcn_cfg();
-
-  @Source("ttcn.js")
-  @DoNotEmbed
-  DataResource ttcn();
-
-  @Source("turtle.js")
-  @DoNotEmbed
-  DataResource turtle();
-
-  @Source("twig.js")
-  @DoNotEmbed
-  DataResource twig();
-
-  @Source("vb.js")
-  @DoNotEmbed
-  DataResource vb();
-
-  @Source("vbscript.js")
-  @DoNotEmbed
-  DataResource vbscript();
-
-  @Source("velocity.js")
-  @DoNotEmbed
-  DataResource velocity();
-
-  @Source("verilog.js")
-  @DoNotEmbed
-  DataResource verilog();
-
-  @Source("vhdl.js")
-  @DoNotEmbed
-  DataResource vhdl();
-
-  @Source("vue.js")
-  @DoNotEmbed
-  DataResource vue();
-
-  @Source("webidl.js")
-  @DoNotEmbed
-  DataResource webidl();
-
-  @Source("xml.js")
-  @DoNotEmbed
-  DataResource xml();
-
-  @Source("xquery.js")
-  @DoNotEmbed
-  DataResource xquery();
-
-  @Source("yacas.js")
-  @DoNotEmbed
-  DataResource yacas();
-
-  @Source("yaml-frontmatter.js")
-  @DoNotEmbed
-  DataResource yaml_frontmatter();
-
-  @Source("yaml.js")
-  @DoNotEmbed
-  DataResource yaml();
-
-  @Source("z80.js")
-  @DoNotEmbed
-  DataResource z80();
-
-  // When adding a resource, update static initializer in ModeInfo.
-}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/mode/gerrit/commit.js b/gerrit-gwtui/src/main/java/net/codemirror/mode/gerrit/commit.js
deleted file mode 100644
index e1fe898..0000000
--- a/gerrit-gwtui/src/main/java/net/codemirror/mode/gerrit/commit.js
+++ /dev/null
@@ -1,26 +0,0 @@
-CodeMirror.defineMode('gerrit_commit', function() {
-  var header = /^(Parent|Author|AuthorDate|Commit|CommitDate):/;
-  var id = /^Change-Id: I[0-9a-f]{40}/;
-  var footer = /^[A-Z][A-Za-z0-9-]+:/;
-  var sha1 = /\b[0-9a-f]{6,40}/;
-
-  return {
-    token: function(stream) {
-      if (stream.sol()) {
-        if (stream.match(header))
-          return 'keyword';
-        if (stream.match(id) || stream.match(footer))
-          return 'builtin';
-      }
-
-      stream.eatSpace();
-      if (stream.match(sha1))
-        return 'variable-2';
-      if (stream.match(/".*"/))
-        return 'string';
-      stream.next();
-      return null;
-    }
-  };
-});
-CodeMirror.defineMIME('text/x-gerrit-commit-message', 'gerrit_commit');
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/theme/ThemeLoader.java b/gerrit-gwtui/src/main/java/net/codemirror/theme/ThemeLoader.java
deleted file mode 100644
index 23039d4..0000000
--- a/gerrit-gwtui/src/main/java/net/codemirror/theme/ThemeLoader.java
+++ /dev/null
@@ -1,119 +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 net.codemirror.theme;
-
-import com.google.gerrit.extensions.client.Theme;
-import com.google.gwt.dom.client.StyleInjector;
-import com.google.gwt.resources.client.ExternalTextResource;
-import com.google.gwt.resources.client.ResourceCallback;
-import com.google.gwt.resources.client.ResourceException;
-import com.google.gwt.resources.client.TextResource;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import java.util.EnumSet;
-
-/** Dynamically loads a known CodeMirror theme's CSS */
-public class ThemeLoader {
-  private static final ExternalTextResource[] THEMES = {
-    Themes.I.day_3024(),
-    Themes.I.night_3024(),
-    Themes.I.abcdef(),
-    Themes.I.ambiance(),
-    Themes.I.base16_dark(),
-    Themes.I.base16_light(),
-    Themes.I.bespin(),
-    Themes.I.blackboard(),
-    Themes.I.cobalt(),
-    Themes.I.colorforth(),
-    Themes.I.dracula(),
-    Themes.I.eclipse(),
-    Themes.I.elegant(),
-    Themes.I.erlang_dark(),
-    Themes.I.hopscotch(),
-    Themes.I.icecoder(),
-    Themes.I.isotope(),
-    Themes.I.lesser_dark(),
-    Themes.I.liquibyte(),
-    Themes.I.material(),
-    Themes.I.mbo(),
-    Themes.I.mdn_like(),
-    Themes.I.midnight(),
-    Themes.I.monokai(),
-    Themes.I.neat(),
-    Themes.I.neo(),
-    Themes.I.night(),
-    Themes.I.paraiso_dark(),
-    Themes.I.paraiso_light(),
-    Themes.I.pastel_on_dark(),
-    Themes.I.railscasts(),
-    Themes.I.rubyblue(),
-    Themes.I.seti(),
-    Themes.I.solarized(),
-    Themes.I.the_matrix(),
-    Themes.I.tomorrow_night_bright(),
-    Themes.I.tomorrow_night_eighties(),
-    Themes.I.ttcn(),
-    Themes.I.twilight(),
-    Themes.I.vibrant_ink(),
-    Themes.I.xq_dark(),
-    Themes.I.xq_light(),
-    Themes.I.yeti(),
-    Themes.I.zenburn(),
-  };
-
-  private static final EnumSet<Theme> loaded = EnumSet.of(Theme.DEFAULT);
-
-  public static final void loadTheme(Theme theme, AsyncCallback<Void> cb) {
-    if (loaded.contains(theme)) {
-      cb.onSuccess(null);
-      return;
-    }
-
-    ExternalTextResource resource = findTheme(theme);
-    if (resource == null) {
-      cb.onFailure(new Exception("unknown theme " + theme));
-      return;
-    }
-
-    try {
-      resource.getText(
-          new ResourceCallback<TextResource>() {
-            @Override
-            public void onSuccess(TextResource resource) {
-              StyleInjector.inject(resource.getText());
-              loaded.add(theme);
-              cb.onSuccess(null);
-            }
-
-            @Override
-            public void onError(ResourceException e) {
-              cb.onFailure(e);
-            }
-          });
-    } catch (ResourceException e) {
-      cb.onFailure(e);
-    }
-  }
-
-  private static ExternalTextResource findTheme(Theme theme) {
-    for (ExternalTextResource r : THEMES) {
-      if (theme.name().toLowerCase().equals(r.getName())) {
-        return r;
-      }
-    }
-    return null;
-  }
-
-  private ThemeLoader() {}
-}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/theme/Themes.java b/gerrit-gwtui/src/main/java/net/codemirror/theme/Themes.java
deleted file mode 100644
index cfe853f..0000000
--- a/gerrit-gwtui/src/main/java/net/codemirror/theme/Themes.java
+++ /dev/null
@@ -1,165 +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 net.codemirror.theme;
-
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.resources.client.ClientBundle;
-import com.google.gwt.resources.client.ExternalTextResource;
-
-public interface Themes extends ClientBundle {
-  Themes I = GWT.create(Themes.class);
-
-  @Source("3024-day.css")
-  ExternalTextResource day_3024();
-
-  @Source("3024-night.css")
-  ExternalTextResource night_3024();
-
-  @Source("abcdef.css")
-  ExternalTextResource abcdef();
-
-  @Source("ambiance.css")
-  ExternalTextResource ambiance();
-
-  @Source("base16-dark.css")
-  ExternalTextResource base16_dark();
-
-  @Source("base16-light.css")
-  ExternalTextResource base16_light();
-
-  @Source("bespin.css")
-  ExternalTextResource bespin();
-
-  @Source("blackboard.css")
-  ExternalTextResource blackboard();
-
-  @Source("cobalt.css")
-  ExternalTextResource cobalt();
-
-  @Source("colorforth.css")
-  ExternalTextResource colorforth();
-
-  @Source("dracula.css")
-  ExternalTextResource dracula();
-
-  @Source("duotone-dark.css")
-  ExternalTextResource duotone_dark();
-
-  @Source("duotone-light.css")
-  ExternalTextResource duotone_light();
-
-  @Source("eclipse.css")
-  ExternalTextResource eclipse();
-
-  @Source("elegant.css")
-  ExternalTextResource elegant();
-
-  @Source("erlang-dark.css")
-  ExternalTextResource erlang_dark();
-
-  @Source("hopscotch.css")
-  ExternalTextResource hopscotch();
-
-  @Source("icecoder.css")
-  ExternalTextResource icecoder();
-
-  @Source("isotope.css")
-  ExternalTextResource isotope();
-
-  @Source("lesser-dark.css")
-  ExternalTextResource lesser_dark();
-
-  @Source("liquibyte.css")
-  ExternalTextResource liquibyte();
-
-  @Source("material.css")
-  ExternalTextResource material();
-
-  @Source("mbo.css")
-  ExternalTextResource mbo();
-
-  @Source("mdn-like.css")
-  ExternalTextResource mdn_like();
-
-  @Source("midnight.css")
-  ExternalTextResource midnight();
-
-  @Source("monokai.css")
-  ExternalTextResource monokai();
-
-  @Source("neat.css")
-  ExternalTextResource neat();
-
-  @Source("neo.css")
-  ExternalTextResource neo();
-
-  @Source("night.css")
-  ExternalTextResource night();
-
-  @Source("paraiso-dark.css")
-  ExternalTextResource paraiso_dark();
-
-  @Source("paraiso-light.css")
-  ExternalTextResource paraiso_light();
-
-  @Source("pastel-on-dark.css")
-  ExternalTextResource pastel_on_dark();
-
-  @Source("railscasts.css")
-  ExternalTextResource railscasts();
-
-  @Source("rubyblue.css")
-  ExternalTextResource rubyblue();
-
-  @Source("seti.css")
-  ExternalTextResource seti();
-
-  @Source("solarized.css")
-  ExternalTextResource solarized();
-
-  @Source("the-matrix.css")
-  ExternalTextResource the_matrix();
-
-  @Source("tomorrow-night-bright.css")
-  ExternalTextResource tomorrow_night_bright();
-
-  @Source("tomorrow-night-eighties.css")
-  ExternalTextResource tomorrow_night_eighties();
-
-  @Source("ttcn.css")
-  ExternalTextResource ttcn();
-
-  @Source("twilight.css")
-  ExternalTextResource twilight();
-
-  @Source("vibrant-ink.css")
-  ExternalTextResource vibrant_ink();
-
-  @Source("xq-dark.css")
-  ExternalTextResource xq_dark();
-
-  @Source("xq-light.css")
-  ExternalTextResource xq_light();
-
-  @Source("yeti.css")
-  ExternalTextResource yeti();
-
-  @Source("zenburn.css")
-  ExternalTextResource zenburn();
-
-  // When adding a resource, update:
-  // - static initializer in ThemeLoader
-  // - enum value in com.google.gerrit.extensions.common.Theme
-}
diff --git a/gerrit-gwtui/src/test/java/com/google/gerrit/client/change/ProjectChangeIdTest.java b/gerrit-gwtui/src/test/java/com/google/gerrit/client/change/ProjectChangeIdTest.java
deleted file mode 100644
index 1d47a82..0000000
--- a/gerrit-gwtui/src/test/java/com/google/gerrit/client/change/ProjectChangeIdTest.java
+++ /dev/null
@@ -1,86 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.change;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-
-public class ProjectChangeIdTest {
-
-  @Rule public ExpectedException exception = ExpectedException.none();
-
-  @Test
-  public void emptyStringThrowsException() {
-    exception.expect(IllegalArgumentException.class);
-    exception.expectMessage(" is not a valid change identifier");
-    ProjectChangeId.create("");
-  }
-
-  @Test
-  public void noChangeIdThrowsException() {
-    exception.expect(IllegalArgumentException.class);
-    exception.expectMessage("some/path is not a valid change identifier");
-    ProjectChangeId.create("some/path");
-  }
-
-  @Test
-  public void noChangeButProjectIdThrowsException() {
-    exception.expect(IllegalArgumentException.class);
-    exception.expectMessage("some/+/path is not a valid change identifier");
-    ProjectChangeId.create("some/+/path");
-  }
-
-  @Test
-  public void project() {
-    assertThat(ProjectChangeId.create("test/+/123/some/path")).isEqualTo(result("test", 123));
-    assertThat(ProjectChangeId.create("test/+/123/some/path/")).isEqualTo(result("test", 123));
-    assertThat(ProjectChangeId.create("test/+/123/")).isEqualTo(result("test", 123));
-    assertThat(ProjectChangeId.create("test/+/123")).isEqualTo(result("test", 123));
-    // Numeric Project.NameKey
-    assertThat(ProjectChangeId.create("123/+/123")).isEqualTo(result("123", 123));
-    // Numeric Project.NameKey with ,edit as part of the name
-    assertThat(ProjectChangeId.create("123,edit/+/123")).isEqualTo(result("123,edit", 123));
-  }
-
-  @Test
-  public void noProject() {
-    assertThat(ProjectChangeId.create("123/some/path")).isEqualTo(result(null, 123));
-    assertThat(ProjectChangeId.create("123/some/path/")).isEqualTo(result(null, 123));
-    assertThat(ProjectChangeId.create("123/")).isEqualTo(result(null, 123));
-    assertThat(ProjectChangeId.create("123")).isEqualTo(result(null, 123));
-  }
-
-  @Test
-  public void editSuffix() {
-    assertThat(ProjectChangeId.create("123,edit/some/path")).isEqualTo(result(null, 123));
-    assertThat(ProjectChangeId.create("123,edit/")).isEqualTo(result(null, 123));
-    assertThat(ProjectChangeId.create("123,edit")).isEqualTo(result(null, 123));
-
-    assertThat(ProjectChangeId.create("test/+/123,edit/some/path")).isEqualTo(result("test", 123));
-    assertThat(ProjectChangeId.create("test/+/123,edit/")).isEqualTo(result("test", 123));
-    assertThat(ProjectChangeId.create("test/+/123,edit")).isEqualTo(result("test", 123));
-  }
-
-  private static ProjectChangeId result(@Nullable String project, int id) {
-    return new ProjectChangeId(
-        project == null ? null : new Project.NameKey(project), new Change.Id(id));
-  }
-}
diff --git a/gerrit-gwtui/src/test/java/com/google/gerrit/client/diff/LineMapperTest.java b/gerrit-gwtui/src/test/java/com/google/gerrit/client/diff/LineMapperTest.java
deleted file mode 100644
index fcc214e..0000000
--- a/gerrit-gwtui/src/test/java/com/google/gerrit/client/diff/LineMapperTest.java
+++ /dev/null
@@ -1,117 +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.client.diff;
-
-import static org.junit.Assert.assertEquals;
-
-import com.google.gerrit.client.diff.LineMapper.LineOnOtherInfo;
-import org.junit.Test;
-
-/** Unit tests for LineMapper */
-public class LineMapperTest {
-
-  @Test
-  public void appendCommon() {
-    LineMapper mapper = new LineMapper();
-    mapper.appendCommon(10);
-    assertEquals(10, mapper.getLineA());
-    assertEquals(10, mapper.getLineB());
-  }
-
-  @Test
-  public void appendInsert() {
-    LineMapper mapper = new LineMapper();
-    mapper.appendInsert(10);
-    assertEquals(0, mapper.getLineA());
-    assertEquals(10, mapper.getLineB());
-  }
-
-  @Test
-  public void appendDelete() {
-    LineMapper mapper = new LineMapper();
-    mapper.appendDelete(10);
-    assertEquals(10, mapper.getLineA());
-    assertEquals(0, mapper.getLineB());
-  }
-
-  @Test
-  public void findInCommon() {
-    LineMapper mapper = new LineMapper();
-    mapper.appendCommon(10);
-    assertEquals(new LineOnOtherInfo(9, true), mapper.lineOnOther(DisplaySide.A, 9));
-    assertEquals(new LineOnOtherInfo(9, true), mapper.lineOnOther(DisplaySide.B, 9));
-  }
-
-  @Test
-  public void findAfterCommon() {
-    LineMapper mapper = new LineMapper();
-    mapper.appendCommon(10);
-    assertEquals(new LineOnOtherInfo(10, true), mapper.lineOnOther(DisplaySide.A, 10));
-    assertEquals(new LineOnOtherInfo(10, true), mapper.lineOnOther(DisplaySide.B, 10));
-  }
-
-  @Test
-  public void findInInsertGap() {
-    LineMapper mapper = new LineMapper();
-    mapper.appendInsert(10);
-    assertEquals(new LineOnOtherInfo(-1, false), mapper.lineOnOther(DisplaySide.B, 9));
-  }
-
-  @Test
-  public void findAfterInsertGap() {
-    LineMapper mapper = new LineMapper();
-    mapper.appendInsert(10);
-    assertEquals(new LineOnOtherInfo(0, true), mapper.lineOnOther(DisplaySide.B, 10));
-    assertEquals(new LineOnOtherInfo(10, true), mapper.lineOnOther(DisplaySide.A, 0));
-  }
-
-  @Test
-  public void findInDeleteGap() {
-    LineMapper mapper = new LineMapper();
-    mapper.appendDelete(10);
-    assertEquals(new LineOnOtherInfo(-1, false), mapper.lineOnOther(DisplaySide.A, 9));
-  }
-
-  @Test
-  public void findAfterDeleteGap() {
-    LineMapper mapper = new LineMapper();
-    mapper.appendDelete(10);
-    assertEquals(new LineOnOtherInfo(0, true), mapper.lineOnOther(DisplaySide.A, 10));
-    assertEquals(new LineOnOtherInfo(10, true), mapper.lineOnOther(DisplaySide.B, 0));
-  }
-
-  @Test
-  public void replaceWithInsertInB() {
-    // 0 c c
-    // 1 a b
-    // 2 a b
-    // 3 - b
-    // 4 - b
-    // 5 c c
-    LineMapper mapper = new LineMapper();
-    mapper.appendCommon(1);
-    mapper.appendReplace(2, 4);
-    mapper.appendCommon(1);
-
-    assertEquals(4, mapper.getLineA());
-    assertEquals(6, mapper.getLineB());
-
-    assertEquals(new LineOnOtherInfo(1, true), mapper.lineOnOther(DisplaySide.B, 1));
-    assertEquals(new LineOnOtherInfo(3, true), mapper.lineOnOther(DisplaySide.B, 5));
-
-    assertEquals(new LineOnOtherInfo(2, true), mapper.lineOnOther(DisplaySide.B, 2));
-    assertEquals(new LineOnOtherInfo(2, false), mapper.lineOnOther(DisplaySide.B, 3));
-  }
-}
diff --git a/gerrit-plugin-gwtui/BUILD b/gerrit-plugin-gwtui/BUILD
deleted file mode 100644
index d9281c8..0000000
--- a/gerrit-plugin-gwtui/BUILD
+++ /dev/null
@@ -1,87 +0,0 @@
-load("@rules_java//java:defs.bzl", "java_binary")
-load("//tools/bzl:java.bzl", "java_library2")
-load("//tools/bzl:javadoc.bzl", "java_doc")
-
-package(default_visibility = ["//visibility:public"])
-
-SRCS = glob(["src/main/java/com/google/gerrit/**/*.java"])
-
-DEPS = ["//lib/gwt:user-neverlink"]
-
-java_binary(
-    name = "gwtui-api",
-    main_class = "Dummy",
-    runtime_deps = [
-        ":gwtui-api-lib",
-        "//gerrit-gwtui-common:client-lib",
-    ],
-)
-
-java_library2(
-    name = "gwtui-api-lib",
-    srcs = SRCS,
-    exported_deps = [
-        "//gerrit-gwtui:client-lib",
-        "//gerrit-gwtui-common:client-lib",
-    ],
-    resources = glob(["src/main/**/*"]),
-    deps = DEPS + [
-        "//java/org/eclipse/jgit:libclient-src.jar",
-        "//java/org/eclipse/jgit:libEdit-src.jar",
-        "//java/com/google/gerrit/common:libclient-src.jar",
-        "//java/com/google/gerrit/extensions:libapi-src.jar",
-        "//java/com/google/gwtexpui/clippy:libclippy-src.jar",
-        "//java/com/google/gwtexpui/globalkey:libglobalkey-src.jar",
-        "//java/com/google/gwtexpui/progress:libprogress-src.jar",
-        "//java/com/google/gwtexpui/safehtml:libsafehtml-src.jar",
-        "//java/com/google/gwtexpui/user:libagent-src.jar",
-        "//gerrit-gwtui-common:libclient-src.jar",
-        "//java/com/google/gerrit/prettify:libclient-src.jar",
-        "//java/com/google/gerrit/reviewdb:libclient-src.jar",
-        "//lib/gwt:dev-neverlink",
-    ],
-)
-
-java_library2(
-    name = "gwtui-api-lib-neverlink",
-    srcs = SRCS,
-    exported_deps = ["//gerrit-gwtui-common:client-lib"],
-    neverlink = 1,  # we want this to be exported deps
-    resources = glob(["src/main/**/*"]),
-    deps = DEPS + ["//lib/gwt:dev"],
-)
-
-java_binary(
-    name = "gwtui-api-source",
-    main_class = "Dummy",
-    runtime_deps = [
-        ":libgwtui-api-lib-src.jar",
-        "//gerrit-gwtui-common:libclient-lib-src.jar",
-        "//java/com/google/gwtexpui/clippy:libclippy-src.jar",
-        "//java/com/google/gwtexpui/globalkey:libglobalkey-src.jar",
-        "//java/com/google/gwtexpui/progress:libprogress-src.jar",
-        "//java/com/google/gwtexpui/safehtml:libsafehtml-src.jar",
-        "//java/com/google/gwtexpui/user:libagent-src.jar",
-    ],
-)
-
-java_doc(
-    name = "gwtui-api-javadoc",
-    libs = DEPS + [
-        ":gwtui-api-lib",
-        "//lib:gwtjsonrpc",
-        "//lib:gwtorm-client",
-        "//lib/gwt:dev",
-        "//gerrit-gwtui-common:client-lib",
-        "//java/com/google/gerrit/common:client",
-        "//java/com/google/gerrit/reviewdb:client",
-    ],
-    pkgs = [
-        "com.google.gerrit.plugin",
-        "com.google.gwtexpui.clippy",
-        "com.google.gwtexpui.globalkey",
-        "com.google.gwtexpui.safehtml",
-        "com.google.gwtexpui.user",
-    ],
-    title = "Gerrit Review GWT Extension API Documentation",
-)
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/Plugin.gwt.xml b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/Plugin.gwt.xml
deleted file mode 100644
index e0b0833..0000000
--- a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/Plugin.gwt.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<!--
- Copyright (C) 2012 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<module>
-  <inherits name="com.google.gwt.json.JSON"/>
-  <inherits name='com.google.gerrit.GerritGwtUICommon'/>
-
-  <define-linker name="gerrit_plugin" class="com.google.gerrit.plugin.linker.GerritPluginLinker"/>
-  <add-linker name="gerrit_plugin"/>
-  <generate-with class="com.google.gerrit.plugin.rebind.PluginGenerator">
-    <when-type-assignable class="com.google.gerrit.plugin.client.Plugin"/>
-  </generate-with>
-  <source path="plugin/client"/>
-</module>
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/FormatUtil.java b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/FormatUtil.java
deleted file mode 100644
index 85bc9c3..0000000
--- a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/FormatUtil.java
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.plugin.client;
-
-import com.google.gerrit.client.AccountFormatter;
-import com.google.gerrit.client.DateFormatter;
-import com.google.gerrit.client.RelativeDateFormatter;
-import com.google.gerrit.client.info.AccountInfo;
-import java.util.Date;
-
-public class FormatUtil {
-  private static final AccountFormatter accountFormatter =
-      new AccountFormatter(Plugin.get().getServerInfo().user().anonymousCowardName());
-
-  /** Format a date using a really short format. */
-  public static String shortFormat(Date dt) {
-    return createDateFormatter().shortFormat(dt);
-  }
-
-  /** Format a date using a really short format. */
-  public static String shortFormatDayTime(Date dt) {
-    return createDateFormatter().shortFormatDayTime(dt);
-  }
-
-  /** Format a date using the locale's medium length format. */
-  public static String mediumFormat(Date dt) {
-    return createDateFormatter().mediumFormat(dt);
-  }
-
-  private static DateFormatter createDateFormatter() {
-    return new DateFormatter(Plugin.get().getUserPreferences());
-  }
-
-  /** Format a date using git log's relative date format. */
-  public static String relativeFormat(Date dt) {
-    return RelativeDateFormatter.format(dt);
-  }
-
-  /**
-   * Formats an account as a name and an email address.
-   *
-   * <p>Example output:
-   *
-   * <ul>
-   *   <li>{@code A U. Thor &lt;author@example.com&gt;}: full populated
-   *   <li>{@code A U. Thor (12)}: missing email address
-   *   <li>{@code Anonymous Coward &lt;author@example.com&gt;}: missing name
-   *   <li>{@code Anonymous Coward (12)}: missing name and email address
-   * </ul>
-   */
-  public static String nameEmail(AccountInfo info) {
-    return accountFormatter.nameEmail(info);
-  }
-
-  /**
-   * Formats an account name.
-   *
-   * <p>If the account has a full name, it returns only the full name. Otherwise it returns a longer
-   * form that includes the email address.
-   */
-  public static String name(AccountInfo info) {
-    return accountFormatter.name(info);
-  }
-}
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/Plugin.java b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/Plugin.java
deleted file mode 100644
index bfcb3d6..0000000
--- a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/Plugin.java
+++ /dev/null
@@ -1,155 +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.plugin.client;
-
-import com.google.gerrit.client.GerritUiExtensionPoint;
-import com.google.gerrit.client.info.AccountInfo;
-import com.google.gerrit.client.info.GeneralPreferences;
-import com.google.gerrit.client.info.ServerInfo;
-import com.google.gerrit.plugin.client.extension.Panel;
-import com.google.gerrit.plugin.client.screen.Screen;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.core.client.JavaScriptObject;
-
-/**
- * Wrapper around the plugin instance exposed by Gerrit.
- *
- * <p>Listeners for events generated by the main UI must be registered through this instance.
- */
-public final class Plugin extends JavaScriptObject {
-  private static final Plugin self =
-      install(GWT.getModuleBaseURL() + GWT.getModuleName() + ".nocache.js");
-
-  /** Obtain the plugin instance wrapper. */
-  public static Plugin get() {
-    return self;
-  }
-
-  /** Installed name of the plugin. */
-  public String getName() {
-    return getPluginName();
-  }
-
-  /** Installed name of the plugin. */
-  public native String getPluginName() /*-{ return this.getPluginName() }-*/;
-
-  /** Navigate the UI to the screen identified by the token. */
-  public native void go(String token) /*-{ return this.go(token) }-*/;
-
-  /** Refresh the current UI. */
-  public native void refresh() /*-{ return this.refresh() }-*/;
-
-  /** Refresh Gerrit's menu bar. */
-  public native void refreshMenuBar() /*-{ return this.refreshMenuBar() }-*/;
-
-  /**
-   * @return the preferences of the currently signed in user, the default preferences if not signed
-   *     in
-   */
-  public native GeneralPreferences getUserPreferences() /*-{ return this.getUserPreferences() }-*/;
-
-  /** Refresh the user preferences of the current user. */
-  public native void refreshUserPreferences() /*-{ return this.refreshUserPreferences() }-*/;
-
-  /** @return the server info */
-  public native ServerInfo getServerInfo() /*-{ return this.getServerInfo() }-*/;
-
-  /** @return the current user */
-  public native AccountInfo getCurrentUser() /*-{ return this.getCurrentUser() }-*/;
-
-  /** Check if user is signed in. */
-  public native boolean isSignedIn() /*-{ return this.isSignedIn() }-*/;
-
-  /** Show message in Gerrit's ErrorDialog. */
-  public native void showError(String message) /*-{ return this.showError(message) }-*/;
-
-  /**
-   * Register a screen displayed at {@code /#/x/plugin/token}.
-   *
-   * @param token literal anchor token appearing after the plugin name. For regular expression
-   *     matching use {@code screenRegex()} .
-   * @param entry callback function invoked to create the screen widgets.
-   */
-  public void screen(String token, Screen.EntryPoint entry) {
-    screen(token, wrap(entry));
-  }
-
-  private native void screen(String t, JavaScriptObject e) /*-{ this.screen(t, e) }-*/;
-
-  /**
-   * Register a screen displayed at {@code /#/x/plugin/regex}.
-   *
-   * @param regex JavaScript {@code RegExp} expression to match the anchor token after the plugin
-   *     name. Matching groups are exposed through the {@code Screen} object passed into the {@code
-   *     Screen.EntryPoint}.
-   * @param entry callback function invoked to create the screen widgets.
-   */
-  public void screenRegex(String regex, Screen.EntryPoint entry) {
-    screenRegex(regex, wrap(entry));
-  }
-
-  private native void screenRegex(String p, JavaScriptObject e)
-      /*-{ this.screen(new $wnd.RegExp(p), e) }-*/ ;
-
-  /**
-   * Register a settings screen displayed at {@code /#/settings/x/plugin/token}.
-   *
-   * @param token literal anchor token appearing after the plugin name.
-   * @param entry callback function invoked to create the settings screen widgets.
-   */
-  public void settingsScreen(String token, String menu, Screen.EntryPoint entry) {
-    settingsScreen(token, menu, wrap(entry));
-  }
-
-  private native void settingsScreen(String t, String m, JavaScriptObject e)
-      /*-{ this.settingsScreen(t, m, e) }-*/ ;
-
-  /**
-   * Register a panel for a UI extension point.
-   *
-   * @param extensionPoint the UI extension point for which the panel should be registered.
-   * @param entry callback function invoked to create the panel widgets.
-   * @param name the name of the panel which can be used to specify panel ordering via project
-   *     config
-   */
-  public final void panel(
-      GerritUiExtensionPoint extensionPoint, Panel.EntryPoint entry, String name) {
-    panel(extensionPoint.name(), wrap(entry), name);
-  }
-
-  private native void panel(String i, JavaScriptObject e, String n) /*-{ this.panel(i, e, n) }-*/;
-
-  protected Plugin() {}
-
-  native void _initialized() /*-{ this._success = true }-*/;
-
-  native void _loaded() /*-{ this._loadedGwt() }-*/;
-
-  private static native Plugin install(String u) /*-{ return $wnd.Gerrit.installGwt(u) }-*/;
-
-  private static native JavaScriptObject wrap(Screen.EntryPoint b) /*-{
-    return $entry(function(c){
-      b.@com.google.gerrit.plugin.client.screen.Screen.EntryPoint::onLoad(Lcom/google/gerrit/plugin/client/screen/Screen;)(
-        @com.google.gerrit.plugin.client.screen.Screen::new(Lcom/google/gerrit/plugin/client/screen/Screen$Context;)(c));
-    });
-  }-*/;
-
-  private static native JavaScriptObject wrap(Panel.EntryPoint b) /*-{
-    return $entry(function(c){
-      b.@com.google.gerrit.plugin.client.extension.Panel.EntryPoint::onLoad(Lcom/google/gerrit/plugin/client/extension/Panel;)(
-        @com.google.gerrit.plugin.client.extension.Panel::new(Lcom/google/gerrit/plugin/client/extension/Panel$Context;)(c));
-    });
-  }-*/;
-}
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/PluginEntryPoint.java b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/PluginEntryPoint.java
deleted file mode 100644
index 808cda3..0000000
--- a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/PluginEntryPoint.java
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.plugin.client;
-
-import com.google.gwt.core.client.EntryPoint;
-
-/**
- * Base class for writing Gerrit Web UI plugins
- *
- * <p>Writing a plugin:
- *
- * <ol>
- *   <li>Declare subtype of Plugin
- *   <li>Bind WebUiPlugin to GwtPlugin implementation in Gerrit-Module
- * </ol>
- */
-public abstract class PluginEntryPoint implements EntryPoint {
-  /**
-   * The plugin entry point method, called automatically by loading a module that declares an
-   * implementing class as an entry point.
-   */
-  public abstract void onPluginLoad();
-
-  @Override
-  public final void onModuleLoad() {
-    Plugin self = Plugin.get();
-    try {
-      onPluginLoad();
-      self._initialized();
-    } finally {
-      self._loaded();
-    }
-  }
-}
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/extension/Panel.java b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/extension/Panel.java
deleted file mode 100644
index 8ee6d0e..0000000
--- a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/extension/Panel.java
+++ /dev/null
@@ -1,109 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.plugin.client.extension;
-
-import com.google.gerrit.client.GerritUiExtensionPoint;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.user.client.ui.SimplePanel;
-
-/**
- * Panel that extends a Gerrit core screen contributed by this plugin.
- *
- * <p>Panel should be registered early at module load:
- *
- * <pre>
- * &#064;Override
- * public void onModuleLoad() {
- *   Plugin.get().panel(GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK,
- *       new Panel.EntryPoint() {
- *         &#064;Override
- *         public void onLoad(Panel panel) {
- *           panel.setWidget(new Label(&quot;World&quot;));
- *         }
- *       });
- * }
- * </pre>
- */
-public class Panel extends SimplePanel {
-  /** Initializes a panel for display. */
-  public interface EntryPoint {
-    /**
-     * Invoked when the panel has been created.
-     *
-     * <p>The implementation should create a single widget to define the content of this panel and
-     * add it to the passed panel instance.
-     *
-     * <p>To use multiple widgets, compose them in panels such as {@code FlowPanel} and add only the
-     * top level widget to the panel.
-     *
-     * <p>The panel is already attached to the browser DOM. Any widgets added to the screen will
-     * immediately receive {@code onLoad()}. GWT will fire {@code onUnload()} when the panel is
-     * removed from the UI, generally caused by the user navigating to another screen.
-     *
-     * @param panel panel that will contain the panel widget.
-     */
-    void onLoad(Panel panel);
-  }
-
-  static final class Context extends JavaScriptObject {
-    native Element body() /*-{ return this.body }-*/;
-
-    native String get(String k) /*-{ return this.p[k]; }-*/;
-
-    native int getInt(String k, int d) /*-{
-      return this.p.hasOwnProperty(k) ? this.p[k] : d
-    }-*/;
-
-    native int getBoolean(String k, boolean d) /*-{
-      return this.p.hasOwnProperty(k) ? this.p[k] : d
-    }-*/;
-
-    native JavaScriptObject getObject(String k) /*-{ return this.p[k]; }-*/;
-
-    native void detach(Panel p) /*-{
-      this.onUnload($entry(function(){
-        p.@com.google.gwt.user.client.ui.Widget::onDetach()();
-      }));
-    }-*/;
-
-    protected Context() {}
-  }
-
-  private final Context ctx;
-
-  Panel(Context ctx) {
-    super(ctx.body());
-    this.ctx = ctx;
-    onAttach();
-    ctx.detach(this);
-  }
-
-  public String get(GerritUiExtensionPoint.Key key) {
-    return ctx.get(key.name());
-  }
-
-  public int getInt(GerritUiExtensionPoint.Key key, int defaultValue) {
-    return ctx.getInt(key.name(), defaultValue);
-  }
-
-  public int getBoolean(GerritUiExtensionPoint.Key key, boolean defaultValue) {
-    return ctx.getBoolean(key.name(), defaultValue);
-  }
-
-  public JavaScriptObject getObject(GerritUiExtensionPoint.Key key) {
-    return ctx.getObject(key.name());
-  }
-}
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/rpc/NoContent.java b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/rpc/NoContent.java
deleted file mode 100644
index 9ff4fed..0000000
--- a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/rpc/NoContent.java
+++ /dev/null
@@ -1,21 +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.plugin.client.rpc;
-
-import com.google.gwt.core.client.JavaScriptObject;
-
-public class NoContent extends JavaScriptObject {
-  protected NoContent() {}
-}
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/rpc/RestApi.java b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/rpc/RestApi.java
deleted file mode 100644
index 86791f8..0000000
--- a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/rpc/RestApi.java
+++ /dev/null
@@ -1,160 +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.plugin.client.rpc;
-
-import com.google.gerrit.client.rpc.NativeString;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.http.client.URL;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-
-public class RestApi {
-  private final StringBuilder path;
-  private boolean hasQueryParams;
-
-  public RestApi(String name) {
-    path = new StringBuilder();
-    path.append(name);
-  }
-
-  public RestApi view(String name) {
-    return idRaw(name);
-  }
-
-  public RestApi view(String pluginName, String name) {
-    return idRaw(pluginName + "~" + name);
-  }
-
-  public RestApi id(String id) {
-    return idRaw(URL.encodePathSegment(id));
-  }
-
-  public RestApi id(int id) {
-    return idRaw(Integer.toString(id));
-  }
-
-  public RestApi idRaw(String name) {
-    if (hasQueryParams) {
-      throw new IllegalStateException();
-    }
-    if (path.charAt(path.length() - 1) != '/') {
-      path.append('/');
-    }
-    path.append(name);
-    return this;
-  }
-
-  public RestApi addParameter(String name, String value) {
-    return addParameterRaw(name, URL.encodeQueryString(value));
-  }
-
-  public RestApi addParameter(String name, String... value) {
-    for (String val : value) {
-      addParameter(name, val);
-    }
-    return this;
-  }
-
-  public RestApi addParameterTrue(String name) {
-    return addParameterRaw(name, null);
-  }
-
-  public RestApi addParameter(String name, boolean value) {
-    return addParameterRaw(name, value ? "t" : "f");
-  }
-
-  public RestApi addParameter(String name, int value) {
-    return addParameterRaw(name, String.valueOf(value));
-  }
-
-  public RestApi addParameter(String name, Enum<?> value) {
-    return addParameterRaw(name, value.name());
-  }
-
-  public RestApi addParameterRaw(String name, String value) {
-    if (hasQueryParams) {
-      path.append("&");
-    } else {
-      path.append("?");
-      hasQueryParams = true;
-    }
-    path.append(name);
-    if (value != null) {
-      path.append("=").append(value);
-    }
-    return this;
-  }
-
-  public String path() {
-    return path.toString();
-  }
-
-  public <T extends JavaScriptObject> void get(AsyncCallback<T> cb) {
-    get(path(), wrap(cb));
-  }
-
-  public void getString(AsyncCallback<String> cb) {
-    get(NativeString.unwrap(cb));
-  }
-
-  private static native void get(String p, JavaScriptObject r) /*-{ $wnd.Gerrit.get_raw(p, r) }-*/;
-
-  public <T extends JavaScriptObject> void put(AsyncCallback<T> cb) {
-    put(path(), wrap(cb));
-  }
-
-  private static native void put(String p, JavaScriptObject r) /*-{ $wnd.Gerrit.put_raw(p, r) }-*/;
-
-  public <T extends JavaScriptObject> void put(String content, AsyncCallback<T> cb) {
-    put(path(), content, wrap(cb));
-  }
-
-  private static native void put(String p, String c, JavaScriptObject r)
-      /*-{ $wnd.Gerrit.put_raw(p, c, r) }-*/ ;
-
-  public <T extends JavaScriptObject> void put(JavaScriptObject content, AsyncCallback<T> cb) {
-    put(path(), content, wrap(cb));
-  }
-
-  private static native void put(String p, JavaScriptObject c, JavaScriptObject r)
-      /*-{ $wnd.Gerrit.put_raw(p, c, r) }-*/ ;
-
-  public <T extends JavaScriptObject> void post(String content, AsyncCallback<T> cb) {
-    post(path(), content, wrap(cb));
-  }
-
-  private static native void post(String p, String c, JavaScriptObject r)
-      /*-{ $wnd.Gerrit.post_raw(p, c, r) }-*/ ;
-
-  public <T extends JavaScriptObject> void post(JavaScriptObject content, AsyncCallback<T> cb) {
-    post(path(), content, wrap(cb));
-  }
-
-  private static native void post(String p, JavaScriptObject c, JavaScriptObject r)
-      /*-{ $wnd.Gerrit.post_raw(p, c, r) }-*/ ;
-
-  public void delete(AsyncCallback<NoContent> cb) {
-    delete(path(), wrap(cb));
-  }
-
-  private static native void delete(String p, JavaScriptObject r)
-      /*-{ $wnd.Gerrit.del_raw(p, r) }-*/ ;
-
-  private static native <T extends JavaScriptObject> JavaScriptObject wrap(
-      AsyncCallback<T> b) /*-{
-    return function(r) {
-      b.@com.google.gwt.user.client.rpc.AsyncCallback::onSuccess(Ljava/lang/Object;)(r)
-    }
-  }-*/;
-}
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/screen/Screen.java b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/screen/Screen.java
deleted file mode 100644
index 226ac48..0000000
--- a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/screen/Screen.java
+++ /dev/null
@@ -1,144 +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.plugin.client.screen;
-
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArrayString;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.user.client.ui.SimplePanel;
-import com.google.gwt.user.client.ui.Widget;
-
-/**
- * Screen contributed by this plugin.
- *
- * <p>Screens should be registered early at module load:
- *
- * <pre>
- * &#064;Override
- * public void onModuleLoad() {
- *   Plugin.get().screen(&quot;hi&quot;, new Screen.EntryPoint() {
- *     &#064;Override
- *     public void onLoad(Screen screen) {
- *       screen.setPageTitle(&quot;Hi&quot;);
- *       screen.show(new Label(&quot;World&quot;));
- *     }
- *   });
- * }
- * </pre>
- */
-public final class Screen extends SimplePanel {
-  /** Initializes a screen for display. */
-  public interface EntryPoint {
-    /**
-     * Invoked when the screen has been created, but not yet displayed.
-     *
-     * <p>The implementation should create a single widget to define the content of this screen and
-     * added it to the passed screen instance. When the screen is ready to be displayed, call {@link
-     * Screen#show()}.
-     *
-     * <p>To use multiple widgets, compose them in panels such as {@code FlowPanel} and add only the
-     * top level widget to the screen.
-     *
-     * <p>The screen is already attached to the browser DOM in an invisible area. Any widgets added
-     * to the screen will immediately receive {@code onLoad()}. GWT will fire {@code onUnload()}
-     * when the screen is removed from the UI, generally caused by the user navigating to another
-     * screen.
-     *
-     * @param screen panel that will contain the screen widget.
-     */
-    void onLoad(Screen screen);
-  }
-
-  static final class Context extends JavaScriptObject {
-    native Element body() /*-{ return this.body }-*/;
-
-    native JsArrayString token_match() /*-{ return this.token_match }-*/;
-
-    native void show() /*-{ this.show() }-*/;
-
-    native void setTitle(String t) /*-{ this.setTitle(t) }-*/;
-
-    native void setWindowTitle(String t) /*-{ this.setWindowTitle(t) }-*/;
-
-    native void detach(Screen s) /*-{
-      this.onUnload($entry(function(){
-        s.@com.google.gwt.user.client.ui.Widget::onDetach()();
-      }));
-    }-*/;
-
-    protected Context() {}
-  }
-
-  private final Context ctx;
-
-  Screen(Context ctx) {
-    super(ctx.body());
-    this.ctx = ctx;
-    onAttach();
-    ctx.detach(this);
-  }
-
-  /** @return the token suffix after {@code "/#/x/plugin-name/"}. */
-  public String getToken() {
-    return getToken(0);
-  }
-
-  /**
-   * @param group groups range from 1 to {@code getTokenGroups() - 1}. Token group 0 is the entire
-   *     token, see {@link #getToken()}.
-   * @return the token from the regex match group.
-   */
-  public String getToken(int group) {
-    return ctx.token_match().get(group);
-  }
-
-  /** @return total number of token groups. */
-  public int getTokenGroups() {
-    return ctx.token_match().length();
-  }
-
-  /**
-   * Set the page title text; appears above the widget.
-   *
-   * @param titleText text to display above the widget.
-   */
-  public void setPageTitle(String titleText) {
-    ctx.setTitle(titleText);
-  }
-
-  /**
-   * Set the window title text; appears in the browser window title bar.
-   *
-   * @param titleText text to display in the window title bar.
-   */
-  public void setWindowTitle(String titleText) {
-    ctx.setWindowTitle(titleText);
-  }
-
-  /**
-   * Add the widget and immediately show the screen.
-   *
-   * @param w child containing the content.
-   */
-  public void show(Widget w) {
-    setWidget(w);
-    ctx.show();
-  }
-
-  /** Show this screen in the web interface. */
-  public void show() {
-    ctx.show();
-  }
-}
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/ui/GroupSuggestOracle.java b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/ui/GroupSuggestOracle.java
deleted file mode 100644
index df5be2c..0000000
--- a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/ui/GroupSuggestOracle.java
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.plugin.client.ui;
-
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.ui.HighlightSuggestion;
-import com.google.gerrit.plugin.client.rpc.RestApi;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.SuggestOracle;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/** A {@code SuggestOracle} for groups. */
-public class GroupSuggestOracle extends SuggestOracle {
-
-  private final int chars;
-
-  /** @param chars minimum chars to start suggesting. */
-  public GroupSuggestOracle(int chars) {
-    this.chars = chars;
-  }
-
-  @Override
-  public boolean isDisplayStringHTML() {
-    return true;
-  }
-
-  @Override
-  public void requestSuggestions(Request req, Callback done) {
-    if (req.getQuery().length() < chars) {
-      responseEmptySuggestion(req, done);
-      return;
-    }
-    RestApi rest = new RestApi("/groups/").addParameter("suggest", req.getQuery());
-    if (req.getLimit() > 0) {
-      rest.addParameter("n", req.getLimit());
-    }
-    rest.get(
-        new AsyncCallback<NativeMap<JavaScriptObject>>() {
-          @Override
-          public void onSuccess(NativeMap<JavaScriptObject> result) {
-            List<String> keys = result.sortedKeys();
-            List<Suggestion> suggestions = new ArrayList<>(keys.size());
-            for (String g : keys) {
-              suggestions.add(new HighlightSuggestion(req.getQuery(), g));
-            }
-            done.onSuggestionsReady(req, new Response(suggestions));
-          }
-
-          @Override
-          public void onFailure(Throwable caught) {
-            responseEmptySuggestion(req, done);
-          }
-        });
-  }
-
-  private static void responseEmptySuggestion(Request req, Callback done) {
-    List<Suggestion> empty = Collections.emptyList();
-    done.onSuggestionsReady(req, new Response(empty));
-  }
-}
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/linker/GerritPluginLinker.java b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/linker/GerritPluginLinker.java
deleted file mode 100644
index 18f6e54..0000000
--- a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/linker/GerritPluginLinker.java
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.plugin.linker;
-
-import com.google.gwt.core.ext.LinkerContext;
-import com.google.gwt.core.linker.CrossSiteIframeLinker;
-
-/** Finalizes the module manifest file with the selection script. */
-public final class GerritPluginLinker extends CrossSiteIframeLinker {
-  @Override
-  public String getDescription() {
-    return "Gerrit GWT UI plugin";
-  }
-
-  @Override
-  protected String getJsComputeUrlForResource(LinkerContext context) {
-    return "com/google/gerrit/linker/computeUrlForPluginResource.js";
-  }
-}
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/rebind/PluginGenerator.java b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/rebind/PluginGenerator.java
deleted file mode 100644
index ba14556..0000000
--- a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/rebind/PluginGenerator.java
+++ /dev/null
@@ -1,86 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-// 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.gerrit.plugin.rebind;
-
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.core.ext.Generator;
-import com.google.gwt.core.ext.GeneratorContext;
-import com.google.gwt.core.ext.TreeLogger;
-import com.google.gwt.core.ext.UnableToCompleteException;
-import com.google.gwt.core.ext.typeinfo.JClassType;
-import com.google.gwt.core.ext.typeinfo.TypeOracle;
-import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
-import com.google.gwt.user.rebind.SourceWriter;
-import java.io.PrintWriter;
-
-/**
- * Write the top layer in the Gadget bootstrap sandwich and generate a stub manifest that will be
- * completed by the linker.
- *
- * <p>Based on gwt-gadgets GadgetGenerator class
- */
-public class PluginGenerator extends Generator {
-  @Override
-  public String generate(TreeLogger logger, GeneratorContext context, String typeName)
-      throws UnableToCompleteException {
-
-    // The TypeOracle knows about all types in the type system
-    TypeOracle typeOracle = context.getTypeOracle();
-
-    // Get a reference to the type that the generator should implement
-    JClassType sourceType = typeOracle.findType(typeName);
-
-    // Ensure that the requested type exists
-    if (sourceType == null) {
-      logger.log(TreeLogger.ERROR, "Could not find requested typeName", null);
-      throw new UnableToCompleteException();
-    }
-
-    // Make sure the Gadget type is correctly defined
-    validateType(logger, sourceType);
-
-    // Pick a name for the generated class to not conflict.
-    String generatedSimpleSourceName = sourceType.getSimpleSourceName() + "PluginImpl";
-
-    // Begin writing the generated source.
-    ClassSourceFileComposerFactory f =
-        new ClassSourceFileComposerFactory(
-            sourceType.getPackage().getName(), generatedSimpleSourceName);
-    f.addImport(GWT.class.getName());
-    f.setSuperclass(typeName);
-
-    // All source gets written through this Writer
-    PrintWriter out =
-        context.tryCreate(logger, sourceType.getPackage().getName(), generatedSimpleSourceName);
-
-    // If an implementation already exists, we don't need to do any work
-    if (out != null) {
-
-      // We really use a SourceWriter since it's convenient
-      SourceWriter sw = f.createSourceWriter(context, out);
-      sw.commit(logger);
-    }
-
-    return f.getCreatedClassName();
-  }
-
-  protected void validateType(TreeLogger logger, JClassType type) throws UnableToCompleteException {
-    if (!type.isDefaultInstantiable()) {
-      logger.log(TreeLogger.ERROR, "Plugin types must be default instantiable", null);
-      throw new UnableToCompleteException();
-    }
-  }
-}
diff --git a/gerrit-plugin-gwtui/src/main/resources/com/google/gerrit/linker/computeUrlForPluginResource.js b/gerrit-plugin-gwtui/src/main/resources/com/google/gerrit/linker/computeUrlForPluginResource.js
deleted file mode 100644
index 3e20e94..0000000
--- a/gerrit-plugin-gwtui/src/main/resources/com/google/gerrit/linker/computeUrlForPluginResource.js
+++ /dev/null
@@ -1,3 +0,0 @@
-function computeUrlForResource(resource) {
-  return __MODULE_FUNC__.__moduleBase + resource;
-}
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index d163395..bb6a4b4 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.acceptance;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assert_;
 import static com.google.common.truth.Truth8.assertThat;
@@ -25,6 +26,7 @@
 import static com.google.gerrit.server.project.testing.Util.category;
 import static com.google.gerrit.server.project.testing.Util.value;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Objects.requireNonNull;
 import static java.util.stream.Collectors.toList;
 import static org.eclipse.jgit.lib.Constants.HEAD;
 
@@ -38,23 +40,23 @@
 import com.google.common.primitives.Chars;
 import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
 import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
+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.ContributorAgreement;
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.LabelFunction;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelValue;
 import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.data.PermissionRule.Action;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
 import com.google.gerrit.extensions.api.changes.SubmittedTogetherInfo;
-import com.google.gerrit.extensions.api.groups.GroupApi;
-import com.google.gerrit.extensions.api.groups.GroupInput;
 import com.google.gerrit.extensions.api.projects.BranchApi;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
@@ -72,6 +74,7 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.index.project.ProjectIndex;
 import com.google.gerrit.index.project.ProjectIndexCollection;
+import com.google.gerrit.json.OutputFormat;
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.mail.EmailHeader;
 import com.google.gerrit.reviewdb.client.Account;
@@ -82,12 +85,10 @@
 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.server.ReviewDb;
-import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.OutputFormat;
 import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.PluginUser;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.Accounts;
@@ -103,6 +104,7 @@
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.group.InternalGroup;
@@ -113,9 +115,11 @@
 import com.google.gerrit.server.index.change.ChangeIndex;
 import com.google.gerrit.server.index.change.ChangeIndexCollection;
 import com.google.gerrit.server.index.change.ChangeIndexer;
+import com.google.gerrit.server.notedb.AbstractChangeNotes;
 import com.google.gerrit.server.notedb.ChangeNoteUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.notedb.MutableNotesMigration;
+import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
+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;
@@ -126,13 +130,8 @@
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.FakeEmailSender;
 import com.google.gerrit.testing.FakeEmailSender.Message;
-import com.google.gerrit.testing.FakeGroupAuditService;
-import com.google.gerrit.testing.NoteDbMode;
 import com.google.gerrit.testing.SshMode;
-import com.google.gerrit.testing.TempFileUtil;
 import com.google.gson.Gson;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.Provider;
@@ -141,6 +140,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.lang.reflect.Modifier;
 import java.nio.file.DirectoryStream;
 import java.nio.file.FileSystem;
 import java.nio.file.FileSystems;
@@ -163,7 +163,6 @@
 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.Constants;
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -181,8 +180,10 @@
 import org.junit.After;
 import org.junit.AfterClass;
 import org.junit.Before;
+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;
 import org.junit.runner.RunWith;
@@ -193,6 +194,8 @@
   private static GerritServer commonServer;
   private static Description firstTest;
 
+  @ClassRule public static TemporaryFolder temporaryFolder = new TemporaryFolder();
+
   @ConfigSuite.Parameter public Config baseConfig;
   @ConfigSuite.Name private String configName;
 
@@ -210,10 +213,7 @@
                 firstTest = description;
               }
               beforeTest(description);
-              ProjectResetter.Config input = resetProjects();
-              if (input == null) {
-                input = defaultResetProjects();
-              }
+              ProjectResetter.Config input = requireNonNull(resetProjects());
 
               try (ProjectResetter resetter = projectResetter.builder().build(input)) {
                 AbstractDaemonTest.this.resetter = resetter;
@@ -243,7 +243,6 @@
   @Inject protected ChangeNoteUtil changeNoteUtil;
   @Inject protected ChangeResource.Factory changeResourceFactory;
   @Inject protected FakeEmailSender sender;
-  @Inject protected FakeGroupAuditService auditService;
   @Inject protected GerritApi gApi;
   @Inject protected GitRepositoryManager repoManager;
   @Inject protected GroupBackend groupBackend;
@@ -252,13 +251,13 @@
   @Inject protected MetaDataUpdate.Server metaDataUpdateFactory;
   @Inject protected PatchSetUtil psUtil;
   @Inject protected ProjectCache projectCache;
+  @Inject protected ProjectConfig.Factory projectConfigFactory;
   @Inject protected ProjectResetter.Builder.Factory projectResetter;
   @Inject protected Provider<InternalChangeQuery> queryProvider;
   @Inject protected PushOneCommit.Factory pushFactory;
   @Inject protected PluginConfigFactory pluginConfig;
   @Inject protected Revisions revisions;
   @Inject protected SystemGroupBackend systemGroupBackend;
-  @Inject protected MutableNotesMigration notesMigration;
   @Inject protected ChangeNotes.Factory notesFactory;
   @Inject protected BatchAbandon batchAbandon;
   @Inject protected TestSshKeys sshKeys;
@@ -268,7 +267,7 @@
   protected Project.NameKey project;
   protected RestSession adminRestSession;
   protected RestSession userRestSession;
-  protected ReviewDb db;
+  protected RestSession anonymousRestSession;
   protected SshSession adminSshSession;
   protected SshSession userSshSession;
   protected TestAccount admin;
@@ -279,14 +278,18 @@
   protected boolean testRequiresSsh;
   protected BlockStrategy noSleepBlockStrategy = t -> {}; // Don't sleep in tests.
 
-  @Inject private ChangeIndexCollection changeIndexes;
+  @Inject private AbstractChangeNotes.Args changeNotesArgs;
   @Inject private AccountIndexCollection accountIndexes;
-  @Inject private ProjectIndexCollection projectIndexes;
+  @Inject private AccountIndexer accountIndexer;
+  @Inject private ChangeIndexCollection changeIndexes;
   @Inject private EventRecorder.Factory eventRecorderFactory;
   @Inject private InProcessProtocol inProcessProtocol;
-  @Inject private Provider<AnonymousUser> anonymousUser;
-  @Inject private SchemaFactory<ReviewDb> reviewDbProvider;
-  @Inject private AccountIndexer accountIndexer;
+  @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;
 
   private ProjectResetter resetter;
   private List<Repository> toClose;
@@ -330,15 +333,10 @@
         commonServer = null;
       }
     }
-    TempFileUtil.cleanup();
   }
 
   /** Controls which project and branches should be reset after each test case. */
   protected ProjectResetter.Config resetProjects() {
-    return null;
-  }
-
-  private ProjectResetter.Config defaultResetProjects() {
     return new ProjectResetter.Config()
         // Don't reset all refs so that refs/sequences/changes is not touched and change IDs are
         // not reused.
@@ -365,7 +363,7 @@
     initSsh();
   }
 
-  protected void evictAndReindexAccount(Account.Id accountId) throws IOException {
+  protected void evictAndReindexAccount(Account.Id accountId) {
     accountCache.evict(accountId);
     accountIndexer.index(accountId);
   }
@@ -402,28 +400,27 @@
     Module module = createModule();
     if (classDesc.equals(methodDesc) && !classDesc.sandboxed() && !methodDesc.sandboxed()) {
       if (commonServer == null) {
-        commonServer = GerritServer.initAndStart(classDesc, baseConfig, module);
+        commonServer = GerritServer.initAndStart(temporaryFolder, classDesc, baseConfig, module);
       }
       server = commonServer;
     } else {
-      server = GerritServer.initAndStart(methodDesc, baseConfig, module);
+      server = GerritServer.initAndStart(temporaryFolder, methodDesc, baseConfig, module);
     }
 
     server.getTestInjector().injectMembers(this);
     Transport.register(inProcessProtocol);
-    toClose = Collections.synchronizedList(new ArrayList<Repository>());
-
-    db = reviewDbProvider.open();
+    toClose = Collections.synchronizedList(new ArrayList<>());
 
     admin = accountCreator.admin();
     user = accountCreator.user();
 
     // Evict and reindex accounts in case tests modify them.
-    evictAndReindexAccount(admin.getId());
-    evictAndReindexAccount(user.getId());
+    evictAndReindexAccount(admin.id());
+    evictAndReindexAccount(user.id());
 
     adminRestSession = new RestSession(server, admin);
     userRestSession = new RestSession(server, user);
+    anonymousRestSession = new RestSession(server, null);
 
     initSsh();
 
@@ -437,7 +434,9 @@
     ProjectInput in = projectInput(description);
     gApi.projects().create(in);
     project = new Project.NameKey(in.name);
-    testRepo = cloneProject(project, getCloneAsAccount(description));
+    if (!classDesc.skipProjectClone()) {
+      testRepo = cloneProject(project, getCloneAsAccount(description));
+    }
   }
 
   /** Override to bind an additional Guice module */
@@ -524,24 +523,7 @@
     return resourcePrefix + name;
   }
 
-  protected Project.NameKey createProject(String nameSuffix) throws RestApiException {
-    return createProject(nameSuffix, null);
-  }
-
-  protected Project.NameKey createProject(String nameSuffix, Project.NameKey parent)
-      throws RestApiException {
-    // Default for createEmptyCommit should match TestProjectConfig.
-    return createProject(nameSuffix, parent, true, null);
-  }
-
-  protected Project.NameKey createProject(
-      String nameSuffix, Project.NameKey parent, boolean createEmptyCommit)
-      throws RestApiException {
-    // Default for createEmptyCommit should match TestProjectConfig.
-    return createProject(nameSuffix, parent, createEmptyCommit, null);
-  }
-
-  protected Project.NameKey createProject(
+  protected Project.NameKey createProjectOverAPI(
       String nameSuffix, Project.NameKey parent, boolean createEmptyCommit, SubmitType submitType)
       throws RestApiException {
     ProjectInput in = new ProjectInput();
@@ -570,8 +552,7 @@
   protected String registerRepoConnection(Project.NameKey p, TestAccount testAccount)
       throws Exception {
     InProcessProtocol.Context ctx =
-        new InProcessProtocol.Context(
-            reviewDbProvider, identifiedUserFactory, testAccount.getId(), p);
+        new InProcessProtocol.Context(identifiedUserFactory, testAccount.id(), p);
     Repository repo = repoManager.openRepository(p);
     toClose.add(repo);
     return inProcessProtocol.register(ctx, repo).toString();
@@ -582,13 +563,11 @@
     for (Repository repo : toClose) {
       repo.close();
     }
-    db.close();
     closeSsh();
     if (server != commonServer) {
       server.close();
       server = null;
     }
-    NoteDbMode.resetFromEnv(notesMigration);
   }
 
   protected void closeSsh() {
@@ -625,7 +604,7 @@
   }
 
   protected PushOneCommit.Result createChange(String ref) throws Exception {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result result = push.to(ref);
     result.assertOkStatus();
     return result;
@@ -641,8 +620,7 @@
     PushOneCommit.Result p1 =
         pushFactory
             .create(
-                db,
-                admin.getIdent(),
+                admin.newIdent(),
                 testRepo,
                 "parent 1",
                 ImmutableMap.of(file, "foo-1", "bar", "bar-1"))
@@ -654,8 +632,7 @@
     PushOneCommit.Result p2 =
         pushFactory
             .create(
-                db,
-                admin.getIdent(),
+                admin.newIdent(),
                 testRepo,
                 "parent 2",
                 ImmutableMap.of(file, "foo-2", "bar", "bar-2"))
@@ -663,11 +640,7 @@
 
     PushOneCommit m =
         pushFactory.create(
-            db,
-            admin.getIdent(),
-            testRepo,
-            "merge",
-            ImmutableMap.of(file, "foo-1", "bar", "bar-2"));
+            admin.newIdent(), testRepo, "merge", ImmutableMap.of(file, "foo-1", "bar", "bar-2"));
     m.setParents(ImmutableList.of(p1.getCommit(), p2.getCommit()));
     PushOneCommit.Result result = m.to(ref);
     result.assertOkStatus();
@@ -682,7 +655,7 @@
       String content)
       throws Exception {
     PushOneCommit.Result result =
-        pushFactory.create(db, admin.getIdent(), repo, commitMsg, fileName, content).to(ref);
+        pushFactory.create(admin.newIdent(), repo, commitMsg, fileName, content).to(ref);
     result.assertOkStatus();
     return result;
   }
@@ -701,8 +674,7 @@
 
   protected PushOneCommit.Result createChange(String subject, String fileName, String content)
       throws Exception {
-    PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), testRepo, subject, fileName, content);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo, subject, fileName, content);
     return push.to("refs/for/master");
   }
 
@@ -714,7 +686,7 @@
       String content,
       String topic)
       throws Exception {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), repo, subject, fileName, content);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), repo, subject, fileName, content);
     return push.to("refs/for/" + branch + "%topic=" + name(topic));
   }
 
@@ -733,7 +705,7 @@
   }
 
   private static final List<Character> RANDOM =
-      Chars.asList(new char[] {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'});
+      Chars.asList('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h');
 
   protected PushOneCommit.Result amendChange(String changeId) throws Exception {
     return amendChange(changeId, "refs/for/master", admin, testRepo);
@@ -768,7 +740,7 @@
       String content)
       throws Exception {
     PushOneCommit push =
-        pushFactory.create(db, testAccount.getIdent(), repo, subject, fileName, content, changeId);
+        pushFactory.create(testAccount.newIdent(), repo, subject, fileName, content, changeId);
     return push.to(ref);
   }
 
@@ -794,28 +766,8 @@
   }
 
   private Context newRequestContext(TestAccount account) {
-    return atrScope.newContext(
-        reviewDbProvider,
-        new SshSession(sshKeys, server, account),
-        identifiedUserFactory.create(account.getId()));
-  }
-
-  /**
-   * Enforce a new request context for the current API user.
-   *
-   * <p>This recreates the IdentifiedUser, hence everything which is cached in the IdentifiedUser is
-   * reloaded (e.g. the email addresses of the user).
-   */
-  protected Context resetCurrentApiUser() {
-    return atrScope.set(newRequestContext(atrScope.get().getSession().getAccount()));
-  }
-
-  protected Context setApiUser(TestAccount account) {
-    return atrScope.set(newRequestContext(account));
-  }
-
-  protected Context setApiUserAnonymous() {
-    return atrScope.set(atrScope.newContext(reviewDbProvider, null, anonymousUser.get()));
+    requestScopeOperations.setApiUser(account.id());
+    return atrScope.get();
   }
 
   protected Account getAccount(Account.Id accountId) {
@@ -828,14 +780,13 @@
     return accountState.get();
   }
 
-  protected Context disableDb() {
-    notesMigration.setFailOnLoadForTest(true);
-    return atrScope.disableDb();
-  }
-
-  protected void enableDb(Context preDisableContext) {
-    notesMigration.setFailOnLoadForTest(false);
-    atrScope.set(preDisableContext);
+  protected AutoCloseable disableNoteDb() {
+    changeNotesArgs.failOnLoadForTest.set(true);
+    Context oldContext = atrScope.disableNoteDb();
+    return () -> {
+      changeNotesArgs.failOnLoadForTest.set(false);
+      atrScope.set(oldContext);
+    };
   }
 
   protected void disableChangeIndexWrites() {
@@ -856,55 +807,49 @@
 
   protected AutoCloseable disableChangeIndex() {
     disableChangeIndexWrites();
-    ChangeIndex searchIndex = changeIndexes.getSearchIndex();
-    if (!(searchIndex instanceof DisabledChangeIndex)) {
-      changeIndexes.setSearchIndex(new DisabledChangeIndex(searchIndex), false);
+    ChangeIndex maybeDisabledSearchIndex = changeIndexes.getSearchIndex();
+    if (!(maybeDisabledSearchIndex instanceof DisabledChangeIndex)) {
+      changeIndexes.setSearchIndex(new DisabledChangeIndex(maybeDisabledSearchIndex), false);
     }
 
-    return new AutoCloseable() {
-      @Override
-      public void close() throws Exception {
-        enableChangeIndexWrites();
-        ChangeIndex searchIndex = changeIndexes.getSearchIndex();
-        if (searchIndex instanceof DisabledChangeIndex) {
-          changeIndexes.setSearchIndex(((DisabledChangeIndex) searchIndex).unwrap(), false);
-        }
+    return () -> {
+      enableChangeIndexWrites();
+      ChangeIndex maybeEnabledSearchIndex = changeIndexes.getSearchIndex();
+      if (maybeEnabledSearchIndex instanceof DisabledChangeIndex) {
+        changeIndexes.setSearchIndex(
+            ((DisabledChangeIndex) maybeEnabledSearchIndex).unwrap(), false);
       }
     };
   }
 
   protected AutoCloseable disableAccountIndex() {
-    AccountIndex searchIndex = accountIndexes.getSearchIndex();
-    if (!(searchIndex instanceof DisabledAccountIndex)) {
-      accountIndexes.setSearchIndex(new DisabledAccountIndex(searchIndex), false);
+    AccountIndex maybeDisabledSearchIndex = accountIndexes.getSearchIndex();
+    if (!(maybeDisabledSearchIndex instanceof DisabledAccountIndex)) {
+      accountIndexes.setSearchIndex(new DisabledAccountIndex(maybeDisabledSearchIndex), false);
     }
 
-    return new AutoCloseable() {
-      @Override
-      public void close() {
-        AccountIndex searchIndex = accountIndexes.getSearchIndex();
-        if (searchIndex instanceof DisabledAccountIndex) {
-          accountIndexes.setSearchIndex(((DisabledAccountIndex) searchIndex).unwrap(), false);
-        }
+    return () -> {
+      AccountIndex maybeEnabledSearchIndex = accountIndexes.getSearchIndex();
+      if (maybeEnabledSearchIndex instanceof DisabledAccountIndex) {
+        accountIndexes.setSearchIndex(
+            ((DisabledAccountIndex) maybeEnabledSearchIndex).unwrap(), false);
       }
     };
   }
 
   protected AutoCloseable disableProjectIndex() {
     disableProjectIndexWrites();
-    ProjectIndex searchIndex = projectIndexes.getSearchIndex();
-    if (!(searchIndex instanceof DisabledProjectIndex)) {
-      projectIndexes.setSearchIndex(new DisabledProjectIndex(searchIndex), false);
+    ProjectIndex maybeDisabledSearchIndex = projectIndexes.getSearchIndex();
+    if (!(maybeDisabledSearchIndex instanceof DisabledProjectIndex)) {
+      projectIndexes.setSearchIndex(new DisabledProjectIndex(maybeDisabledSearchIndex), false);
     }
 
-    return new AutoCloseable() {
-      @Override
-      public void close() {
-        enableProjectIndexWrites();
-        ProjectIndex searchIndex = projectIndexes.getSearchIndex();
-        if (searchIndex instanceof DisabledProjectIndex) {
-          projectIndexes.setSearchIndex(((DisabledProjectIndex) searchIndex).unwrap(), false);
-        }
+    return () -> {
+      enableProjectIndexWrites();
+      ProjectIndex maybeEnabledSearchIndex = projectIndexes.getSearchIndex();
+      if (maybeEnabledSearchIndex instanceof DisabledProjectIndex) {
+        projectIndexes.setSearchIndex(
+            ((DisabledProjectIndex) maybeEnabledSearchIndex).unwrap(), false);
       }
     };
   }
@@ -945,6 +890,17 @@
     }
   }
 
+  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));
@@ -975,18 +931,9 @@
     }
   }
 
-  protected void setUseContributorAgreements(InheritableBoolean value) throws Exception {
-    try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
-      ProjectConfig config = ProjectConfig.read(md);
-      config.getProject().setBooleanConfig(BooleanProjectConfig.USE_CONTRIBUTOR_AGREEMENTS, value);
-      config.commit(md);
-      projectCache.evict(config.getProject());
-    }
-  }
-
   protected void setUseSignedOffBy(InheritableBoolean value) throws Exception {
     try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
-      ProjectConfig config = ProjectConfig.read(md);
+      ProjectConfig config = projectConfigFactory.read(md);
       config.getProject().setBooleanConfig(BooleanProjectConfig.USE_SIGNED_OFF_BY, value);
       config.commit(md);
       projectCache.evict(config.getProject());
@@ -995,7 +942,7 @@
 
   protected void setRequireChangeId(InheritableBoolean value) throws Exception {
     try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
-      ProjectConfig config = ProjectConfig.read(md);
+      ProjectConfig config = projectConfigFactory.read(md);
       config.getProject().setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, value);
       config.commit(md);
       projectCache.evict(config.getProject());
@@ -1057,7 +1004,7 @@
       throws RepositoryNotFoundException, IOException, ConfigInvalidException {
     try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
       md.setMessage(String.format("Grant %s on %s", permission, ref));
-      ProjectConfig config = ProjectConfig.read(md);
+      ProjectConfig config = projectConfigFactory.read(md);
       AccessSection s = config.getAccessSection(ref, true);
       Permission p = s.getPermission(permission, true);
       PermissionRule rule = Util.newRule(config, groupUUID);
@@ -1081,7 +1028,7 @@
     String permission = Permission.LABEL + label;
     try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
       md.setMessage(String.format("Grant %s on %s", permission, ref));
-      ProjectConfig config = ProjectConfig.read(md);
+      ProjectConfig config = projectConfigFactory.read(md);
       AccessSection s = config.getAccessSection(ref, true);
       Permission p = s.getPermission(permission, true);
       p.setExclusiveGroup(exclusive);
@@ -1099,7 +1046,7 @@
       throws IOException, ConfigInvalidException {
     try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
       md.setMessage(String.format("Remove %s on %s", permission, ref));
-      ProjectConfig config = ProjectConfig.read(md);
+      ProjectConfig config = projectConfigFactory.read(md);
       AccessSection s = config.getAccessSection(ref, true);
       Permission p = s.getPermission(permission, true);
       p.clearRules();
@@ -1113,7 +1060,7 @@
   }
 
   protected PushOneCommit.Result pushTo(String ref) throws Exception {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     return push.to(ref);
   }
 
@@ -1139,12 +1086,12 @@
         .inOrder();
   }
 
-  protected PatchSet getPatchSet(PatchSet.Id psId) throws OrmException {
-    return changeDataFactory.create(db, project, psId.getParentKey()).patchSet(psId);
+  protected PatchSet getPatchSet(PatchSet.Id psId) {
+    return changeDataFactory.create(project, psId.getParentKey()).patchSet(psId);
   }
 
   protected IdentifiedUser user(TestAccount testAccount) {
-    return identifiedUserFactory.create(testAccount.getId());
+    return identifiedUserFactory.create(testAccount.id());
   }
 
   protected RevisionResource parseCurrentRevisionResource(String changeId) throws Exception {
@@ -1169,46 +1116,16 @@
     return changeResourceFactory.create(notes.get(0), atrScope.get().getUser());
   }
 
-  protected String createGroup(String name) throws Exception {
-    return createGroup(name, "Administrators");
-  }
-
-  protected String createGroupWithRealName(String name) throws Exception {
-    GroupInput in = new GroupInput();
-    in.name = name;
-    in.ownerId = "Administrators";
-    gApi.groups().create(in);
-    return name;
-  }
-
-  protected String createGroup(String name, String owner) throws Exception {
-    name = name(name);
-    GroupInput in = new GroupInput();
-    in.name = name;
-    in.ownerId = owner;
-    gApi.groups().create(in);
-    return name;
-  }
-
   protected RevCommit getHead(Repository repo, String name) throws Exception {
     try (RevWalk rw = new RevWalk(repo)) {
       Ref r = repo.exactRef(name);
-      return r != null ? rw.parseCommit(r.getObjectId()) : null;
+      return rw.parseCommit(r.getObjectId());
     }
   }
 
-  protected RevCommit getHead(Repository repo) throws Exception {
-    return getHead(repo, "HEAD");
-  }
-
+  // TODO(hanwen): push this down.
   protected RevCommit getRemoteHead(Project.NameKey project, String branch) throws Exception {
-    try (Repository repo = repoManager.openRepository(project)) {
-      return getHead(repo, branch.startsWith(Constants.R_REFS) ? branch : "refs/heads/" + branch);
-    }
-  }
-
-  protected RevCommit getRemoteHead(String project, String branch) throws Exception {
-    return getRemoteHead(new Project.NameKey(project), branch);
+    return projectOperations.project(project).getHead(branch);
   }
 
   protected RevCommit getRemoteHead() throws Exception {
@@ -1221,33 +1138,6 @@
     assertThat(replyTo.getString()).contains(email);
   }
 
-  protected ContributorAgreement configureContributorAgreement(boolean autoVerify)
-      throws Exception {
-    ContributorAgreement ca;
-    if (autoVerify) {
-      String g = createGroup("cla-test-group");
-      GroupApi groupApi = gApi.groups().id(g);
-      groupApi.description("CLA test group");
-      InternalGroup caGroup = group(new AccountGroup.UUID(groupApi.detail().id));
-      GroupReference groupRef = new GroupReference(caGroup.getGroupUUID(), caGroup.getName());
-      PermissionRule rule = new PermissionRule(groupRef);
-      rule.setAction(PermissionRule.Action.ALLOW);
-      ca = new ContributorAgreement("cla-test");
-      ca.setAutoVerify(groupRef);
-      ca.setAccepted(ImmutableList.of(rule));
-    } else {
-      ca = new ContributorAgreement("cla-test-no-auto-verify");
-    }
-    ca.setDescription("description");
-    ca.setAgreementUrl("agreement-url");
-
-    try (ProjectConfigUpdate u = updateProject(allProjects)) {
-      u.getConfig().replace(ca);
-      u.save();
-      return ca;
-    }
-  }
-
   protected Map<Branch.NameKey, ObjectId> fetchFromSubmitPreview(String changeId) throws Exception {
     try (BinaryResult result = gApi.changes().id(changeId).current().submitPreview()) {
       return fetchFromBundles(result);
@@ -1465,7 +1355,7 @@
   }
 
   protected void assertNotifyTo(TestAccount expected) {
-    assertNotifyTo(expected.email, expected.fullName);
+    assertNotifyTo(expected.email(), expected.fullName());
   }
 
   protected void assertNotifyTo(String expectedEmail, String expectedFullname) {
@@ -1479,7 +1369,7 @@
   }
 
   protected void assertNotifyCc(TestAccount expected) {
-    assertNotifyCc(expected.emailAddress);
+    assertNotifyCc(expected.getEmailAddress());
   }
 
   protected void assertNotifyCc(String expectedEmail, String expectedFullname) {
@@ -1499,7 +1389,7 @@
   protected void assertNotifyBcc(TestAccount expected) {
     assertThat(sender.getMessages()).hasSize(1);
     Message m = sender.getMessages().get(0);
-    assertThat(m.rcpt()).containsExactly(expected.emailAddress);
+    assertThat(m.rcpt()).containsExactly(expected.getEmailAddress());
     assertThat(m.headers().get("To").isEmpty()).isTrue();
     assertThat(m.headers().get("Cc").isEmpty()).isTrue();
   }
@@ -1525,7 +1415,7 @@
   }
 
   protected void watch(PushOneCommit.Result r, ProjectWatchInfoConfiguration config)
-      throws OrmException, RestApiException {
+      throws RestApiException {
     watch(r.getChange().project().get(), config);
   }
 
@@ -1646,7 +1536,7 @@
 
     private ProjectConfigUpdate(Project.NameKey projectName) throws Exception {
       metaDataUpdate = metaDataUpdateFactory.create(projectName);
-      projectConfig = ProjectConfig.read(metaDataUpdate);
+      projectConfig = projectConfigFactory.read(metaDataUpdate);
     }
 
     public ProjectConfig getConfig() {
@@ -1654,7 +1544,7 @@
     }
 
     public void save() throws Exception {
-      metaDataUpdate.setAuthor(identifiedUserFactory.create(admin.getId()));
+      metaDataUpdate.setAuthor(identifiedUserFactory.create(admin.id()));
       projectConfig.commit(metaDataUpdate);
       metaDataUpdate.close();
       metaDataUpdate = null;
@@ -1693,4 +1583,45 @@
     comments.sort(Comparator.comparing(c -> c.id));
     return comments;
   }
+
+  protected AutoCloseable installPlugin(String pluginName, Class<? extends Module> sysModuleClass)
+      throws Exception {
+    return installPlugin(pluginName, sysModuleClass, null, null);
+  }
+
+  protected AutoCloseable installPlugin(
+      String pluginName,
+      @Nullable Class<? extends Module> sysModuleClass,
+      @Nullable Class<? extends Module> httpModuleClass,
+      @Nullable Class<? extends Module> sshModuleClass)
+      throws Exception {
+    checkStatic(sysModuleClass);
+    checkStatic(httpModuleClass);
+    checkStatic(sshModuleClass);
+    TestServerPlugin plugin =
+        new TestServerPlugin(
+            pluginName,
+            "http://example.com/" + pluginName,
+            pluginUserFactory.create(pluginName),
+            getClass().getClassLoader(),
+            sysModuleClass != null ? sysModuleClass.getName() : null,
+            httpModuleClass != null ? httpModuleClass.getName() : null,
+            sshModuleClass != null ? sshModuleClass.getName() : null,
+            sitePaths.data_dir.resolve(pluginName));
+    plugin.start(pluginGuiceEnvironment);
+    pluginGuiceEnvironment.onStartPlugin(plugin);
+    return () -> {
+      plugin.stop(pluginGuiceEnvironment);
+      pluginGuiceEnvironment.onStopPlugin(plugin);
+    };
+  }
+
+  private static void checkStatic(@Nullable Class<? extends Module> moduleClass) {
+    if (moduleClass != null) {
+      checkArgument(
+          (moduleClass.getModifiers() & Modifier.STATIC) != 0,
+          "module must be static: %s",
+          moduleClass.getName());
+    }
+  }
 }
diff --git a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
index c33bb7a..af2f17e 100644
--- a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
@@ -26,6 +26,7 @@
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.Subject;
 import com.google.common.truth.Truth;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -41,7 +42,7 @@
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.testing.FakeEmailSender;
 import com.google.gerrit.testing.FakeEmailSender.Message;
-import java.io.IOException;
+import com.google.inject.Inject;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -54,9 +55,11 @@
 import org.junit.Before;
 
 public abstract class AbstractNotificationTest extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @Before
   public void enableReviewerByEmail() throws Exception {
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     ConfigInput conf = new ConfigInput();
     conf.enableReviewerByEmail = InheritableBoolean.TRUE;
     gApi.projects().name(project.get()).config(conf);
@@ -82,7 +85,7 @@
     if (record) {
       accountsModifyingEmailStrategy.add(account);
     }
-    setApiUser(account);
+    requestScopeOperations.setApiUser(account.id());
     GeneralPreferencesInfo prefs = gApi.accounts().self().getPreferences();
     prefs.emailStrategy = strategy;
     gApi.accounts().self().setPreferences(prefs);
@@ -99,9 +102,10 @@
       super(failureMetadata, target);
     }
 
-    public FakeEmailSenderSubject notSent() {
-      if (actual().peekMessage() != null) {
-        failWithoutActual(fact("expected message", "sent"));
+    public FakeEmailSenderSubject didNotSend() {
+      Message message = actual().peekMessage();
+      if (message != null) {
+        failWithoutActual(fact("expected no message", message));
       }
       return this;
     }
@@ -127,7 +131,13 @@
       }
       EmailHeader header = message.headers().get("X-Gerrit-MessageType");
       if (!header.equals(new EmailHeader.String(messageType))) {
-        failWithoutActual(fact("expected message of type", messageType));
+        failWithoutActual(
+            fact("expected message of type", messageType),
+            fact(
+                "actual",
+                header instanceof EmailHeader.String
+                    ? ((EmailHeader.String) header).getString()
+                    : header));
       }
 
       // Return a named subject that displays a human-readable table of
@@ -203,7 +213,7 @@
 
     public FakeEmailSenderSubject noOneElse() {
       for (Map.Entry<NotifyType, TestAccount> watchEntry : users.watchers.entrySet()) {
-        if (!accountedFor.contains(watchEntry.getValue().email)) {
+        if (!accountedFor.contains(watchEntry.getValue().email())) {
           notTo(watchEntry.getKey());
         }
       }
@@ -256,7 +266,7 @@
     }
 
     private void rcpt(@Nullable RecipientType type, TestAccount account) {
-      rcpt(type, account.email);
+      rcpt(type, account.email());
     }
 
     public FakeEmailSenderSubject to(NotifyType... watches) {
@@ -324,8 +334,8 @@
       return description.getClassName();
     }
 
-    private TestAccount evictAndCopy(TestAccount account) throws IOException {
-      evictAndReindexAccount(account.id);
+    private TestAccount evictAndCopy(TestAccount account) {
+      evictAndReindexAccount(account.id());
       return account;
     }
 
@@ -354,7 +364,7 @@
         assignee = testAccount("assignee");
 
         watchingProjectOwner = testAccount("watchingProjectOwner", "Administrators");
-        setApiUser(watchingProjectOwner);
+        requestScopeOperations.setApiUser(watchingProjectOwner.id());
         watch(allProjects.get(), pwi -> pwi.notifyNewChanges = true);
 
         for (NotifyType watch : NotifyType.values()) {
@@ -362,7 +372,7 @@
             continue;
           }
           TestAccount watcher = testAccount(watch.toString());
-          setApiUser(watcher);
+          requestScopeOperations.setApiUser(watcher.id());
           watch(
               allProjects.get(),
               pwi -> {
@@ -393,20 +403,20 @@
     public TestAccount testAccount(String name) throws Exception {
       String username = name(name);
       TestAccount account = accountCreator.create(username, email(username), name);
-      accountsByEmail.put(account.email, account);
+      accountsByEmail.put(account.email(), account);
       return account;
     }
 
     public TestAccount testAccount(String name, String groupName) throws Exception {
       String username = name(name);
       TestAccount account = accountCreator.create(username, email(username), name, groupName);
-      accountsByEmail.put(account.email, account);
+      accountsByEmail.put(account.email(), account);
       return account;
     }
 
     String emailToName(String email) {
       if (accountsByEmail.containsKey(email)) {
-        return accountsByEmail.get(email).fullName;
+        return accountsByEmail.get(email).fullName();
       }
       return email;
     }
@@ -414,9 +424,9 @@
     protected void addReviewers(PushOneCommit.Result r) throws Exception {
       ReviewInput in =
           ReviewInput.noScore()
-              .reviewer(reviewer.email)
+              .reviewer(reviewer.email())
               .reviewer(reviewerByEmail)
-              .reviewer(ccer.email, ReviewerState.CC, false)
+              .reviewer(ccer.email(), ReviewerState.CC, false)
               .reviewer(ccerByEmail, ReviewerState.CC, false);
       ReviewResult result = gApi.changes().id(r.getChangeId()).revision("current").review(in);
       supportReviewersByEmail = true;
@@ -424,8 +434,8 @@
         supportReviewersByEmail = false;
         in =
             ReviewInput.noScore()
-                .reviewer(reviewer.email)
-                .reviewer(ccer.email, ReviewerState.CC, false);
+                .reviewer(reviewer.email())
+                .reviewer(ccer.email(), ReviewerState.CC, false);
         result = gApi.changes().id(r.getChangeId()).revision("current").review(in);
       }
       Truth.assertThat(result.reviewers.values().stream().allMatch(v -> v.error == null)).isTrue();
@@ -455,9 +465,9 @@
       if (pushOptions != null) {
         ref = ref + '%' + Joiner.on(',').join(pushOptions);
       }
-      setApiUser(owner);
+      requestScopeOperations.setApiUser(owner.id());
       repo = cloneProject(project, owner);
-      PushOneCommit push = pushFactory.create(db, owner.getIdent(), repo);
+      PushOneCommit push = pushFactory.create(owner.newIdent(), repo);
       result = push.to(ref);
       result.assertOkStatus();
       changeId = result.getChangeId();
@@ -477,33 +487,38 @@
     StagedChange(String ref) throws Exception {
       super(ref);
 
-      setApiUser(starrer);
+      requestScopeOperations.setApiUser(starrer.id());
       gApi.accounts().self().starChange(result.getChangeId());
 
-      setApiUser(owner);
+      requestScopeOperations.setApiUser(owner.id());
       addReviewers(result);
       sender.clear();
     }
   }
 
   protected StagedChange stageReviewableChange() throws Exception {
-    return new StagedChange("refs/for/master");
+    StagedChange sc = new StagedChange("refs/for/master");
+    sender.clear();
+    return sc;
   }
 
   protected StagedChange stageWipChange() throws Exception {
-    return new StagedChange("refs/for/master%wip");
+    StagedChange sc = new StagedChange("refs/for/master%wip");
+    sender.clear();
+    return sc;
   }
 
   protected StagedChange stageReviewableWipChange() throws Exception {
     StagedChange sc = stageReviewableChange();
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.id());
     gApi.changes().id(sc.changeId).setWorkInProgress();
+    sender.clear();
     return sc;
   }
 
   protected StagedChange stageAbandonedReviewableChange() throws Exception {
     StagedChange sc = stageReviewableChange();
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.id());
     gApi.changes().id(sc.changeId).abandon();
     sender.clear();
     return sc;
@@ -511,7 +526,7 @@
 
   protected StagedChange stageAbandonedReviewableWipChange() throws Exception {
     StagedChange sc = stageReviewableWipChange();
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.id());
     gApi.changes().id(sc.changeId).abandon();
     sender.clear();
     return sc;
@@ -519,7 +534,7 @@
 
   protected StagedChange stageAbandonedWipChange() throws Exception {
     StagedChange sc = stageWipChange();
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.id());
     gApi.changes().id(sc.changeId).abandon();
     sender.clear();
     return sc;
diff --git a/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java b/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java
new file mode 100644
index 0000000..ccd30ab
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java
@@ -0,0 +1,201 @@
+// 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 com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.truth.Truth.assertThat;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.gerrit.common.Nullable;
+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;
+import com.google.gerrit.server.restapi.change.QueryChanges;
+import com.google.gerrit.sshd.commands.Query;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.google.inject.AbstractModule;
+import com.google.inject.Module;
+import java.util.List;
+import java.util.Objects;
+import org.kohsuke.args4j.Option;
+
+public class AbstractPluginFieldsTest extends AbstractDaemonTest {
+  protected static class MyInfo extends PluginDefinedInfo {
+    @Nullable String theAttribute;
+
+    public MyInfo(@Nullable String theAttribute) {
+      this.theAttribute = theAttribute;
+    }
+
+    MyInfo(String name, @Nullable String theAttribute) {
+      this.name = requireNonNull(name);
+      this.theAttribute = theAttribute;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (!(o instanceof MyInfo)) {
+        return false;
+      }
+      MyInfo i = (MyInfo) o;
+      return Objects.equals(name, i.name) && Objects.equals(theAttribute, i.theAttribute);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(name, theAttribute);
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this)
+          .add("name", name)
+          .add("theAttribute", theAttribute)
+          .toString();
+    }
+  }
+
+  protected static class NullAttributeModule extends AbstractModule {
+    @Override
+    public void configure() {
+      DynamicSet.bind(binder(), ChangeAttributeFactory.class).toInstance((cd, bp, p) -> null);
+    }
+  }
+
+  protected static class SimpleAttributeModule extends AbstractModule {
+    @Override
+    public void configure() {
+      DynamicSet.bind(binder(), ChangeAttributeFactory.class)
+          .toInstance((cd, bp, p) -> new MyInfo("change " + cd.getId()));
+    }
+  }
+
+  private static class MyOptions implements DynamicBean {
+    @Option(name = "--opt")
+    private String opt;
+  }
+
+  protected static class OptionAttributeModule extends AbstractModule {
+    @Override
+    public void configure() {
+      DynamicSet.bind(binder(), ChangeAttributeFactory.class)
+          .toInstance(
+              (cd, bp, p) -> {
+                MyOptions opts = (MyOptions) bp.getDynamicBean(p);
+                return opts != null ? new MyInfo("opt " + opts.opt) : null;
+              });
+      bind(DynamicBean.class).annotatedWith(Exports.named(Query.class)).to(MyOptions.class);
+      bind(DynamicBean.class).annotatedWith(Exports.named(QueryChanges.class)).to(MyOptions.class);
+      bind(DynamicBean.class).annotatedWith(Exports.named(GetChange.class)).to(MyOptions.class);
+    }
+  }
+
+  protected void getChangeWithNullAttribute(PluginInfoGetter getter) throws Exception {
+    Change.Id id = createChange().getChange().getId();
+    assertThat(getter.call(id)).isNull();
+
+    try (AutoCloseable ignored = installPlugin("my-plugin", NullAttributeModule.class)) {
+      assertThat(getter.call(id)).isNull();
+    }
+
+    assertThat(getter.call(id)).isNull();
+  }
+
+  protected void getChangeWithSimpleAttribute(PluginInfoGetter getter) throws Exception {
+    getChangeWithSimpleAttribute(getter, SimpleAttributeModule.class);
+  }
+
+  protected void getChangeWithSimpleAttribute(
+      PluginInfoGetter getter, Class<? extends Module> moduleClass) throws Exception {
+    Change.Id id = createChange().getChange().getId();
+    assertThat(getter.call(id)).isNull();
+
+    try (AutoCloseable ignored = installPlugin("my-plugin", moduleClass)) {
+      assertThat(getter.call(id)).containsExactly(new MyInfo("my-plugin", "change " + id));
+    }
+
+    assertThat(getter.call(id)).isNull();
+  }
+
+  protected void getChangeWithOption(
+      PluginInfoGetter getterWithoutOptions, PluginInfoGetterWithOptions getterWithOptions)
+      throws Exception {
+    Change.Id id = createChange().getChange().getId();
+    assertThat(getterWithoutOptions.call(id)).isNull();
+
+    try (AutoCloseable ignored = installPlugin("my-plugin", OptionAttributeModule.class)) {
+      assertThat(getterWithoutOptions.call(id))
+          .containsExactly(new MyInfo("my-plugin", "opt null"));
+      assertThat(getterWithOptions.call(id, ImmutableListMultimap.of("my-plugin--opt", "foo")))
+          .containsExactly(new MyInfo("my-plugin", "opt foo"));
+    }
+
+    assertThat(getterWithoutOptions.call(id)).isNull();
+  }
+
+  protected static List<MyInfo> pluginInfoFromSingletonList(List<ChangeInfo> changeInfos) {
+    assertThat(changeInfos).hasSize(1);
+    return pluginInfoFromChangeInfo(changeInfos.get(0));
+  }
+
+  protected static List<MyInfo> pluginInfoFromChangeInfo(ChangeInfo changeInfo) {
+    List<PluginDefinedInfo> pluginInfo = changeInfo.plugins;
+    if (pluginInfo == null) {
+      return null;
+    }
+    return pluginInfo.stream().map(MyInfo.class::cast).collect(toImmutableList());
+  }
+
+  /**
+   * Decode {@code MyInfo}s from a raw list of maps returned from Gson.
+   *
+   * <p>This method is used instead of decoding {@code ChangeInfo} or {@code ChangAttribute}, since
+   * Gson would decode the {@code plugins} field as a {@code List<PluginDefinedInfo>}, which would
+   * return the base type and silently ignore any fields that are defined only in the subclass.
+   * Instead, decode the enclosing {@code ChangeInfo} or {@code ChangeAttribute} as a raw {@code
+   * Map<String, Object>}, and pass the {@code "plugins"} value to this method.
+   *
+   * @param gson Gson converter.
+   * @param plugins list of {@code MyInfo} objects, each as a raw map returned from Gson.
+   * @return decoded list of {@code MyInfo}s.
+   */
+  protected static List<MyInfo> decodeRawPluginsList(Gson gson, @Nullable Object plugins) {
+    if (plugins == null) {
+      return null;
+    }
+    checkArgument(plugins instanceof List, "not a list: %s", plugins);
+    return gson.fromJson(gson.toJson(plugins), new TypeToken<List<MyInfo>>() {}.getType());
+  }
+
+  @FunctionalInterface
+  protected interface PluginInfoGetter {
+    List<MyInfo> call(Change.Id id) throws Exception;
+  }
+
+  @FunctionalInterface
+  protected interface PluginInfoGetterWithOptions {
+    List<MyInfo> call(Change.Id id, ImmutableListMultimap<String, String> pluginOptions)
+        throws Exception;
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java b/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
index 9a3e811..50536d8 100644
--- a/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
+++ b/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
@@ -14,22 +14,17 @@
 
 package com.google.gerrit.acceptance;
 
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.RequestCleanup;
-import com.google.gerrit.server.config.RequestScopedReviewDbProvider;
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestScopePropagator;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gerrit.testing.DisabledReviewDb;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Key;
 import com.google.inject.OutOfScopeException;
 import com.google.inject.Provider;
 import com.google.inject.Scope;
-import com.google.inject.util.Providers;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -37,13 +32,9 @@
 public class AcceptanceTestRequestScope {
   private static final Key<RequestCleanup> RC_KEY = Key.get(RequestCleanup.class);
 
-  private static final Key<RequestScopedReviewDbProvider> DB_KEY =
-      Key.get(RequestScopedReviewDbProvider.class);
-
   public static class Context implements RequestContext {
     private final RequestCleanup cleanup = new RequestCleanup();
     private final Map<Key<?>, Object> map = new HashMap<>();
-    private final SchemaFactory<ReviewDb> schemaFactory;
     private final SshSession session;
     private final CurrentUser user;
 
@@ -51,22 +42,20 @@
     volatile long started;
     volatile long finished;
 
-    private Context(SchemaFactory<ReviewDb> sf, SshSession s, CurrentUser u, long at) {
-      schemaFactory = sf;
+    private Context(SshSession s, CurrentUser u, long at) {
       session = s;
       user = u;
       created = started = finished = at;
       map.put(RC_KEY, cleanup);
-      map.put(DB_KEY, new RequestScopedReviewDbProvider(schemaFactory, Providers.of(cleanup)));
     }
 
     private Context(Context p, SshSession s, CurrentUser c) {
-      this(p.schemaFactory, s, c, p.created);
+      this(s, c, p.created);
       started = p.started;
       finished = p.finished;
     }
 
-    SshSession getSession() {
+    public SshSession getSession() {
       return session;
     }
 
@@ -78,11 +67,6 @@
       return user;
     }
 
-    @Override
-    public Provider<ReviewDb> getReviewDbProvider() {
-      return (RequestScopedReviewDbProvider) map.get(DB_KEY);
-    }
-
     synchronized <T> T get(Key<T> key, Provider<T> creator) {
       @SuppressWarnings("unchecked")
       T t = (T) map.get(key);
@@ -112,11 +96,8 @@
     private final AcceptanceTestRequestScope atrScope;
 
     @Inject
-    Propagator(
-        AcceptanceTestRequestScope atrScope,
-        ThreadLocalRequestContext local,
-        Provider<RequestScopedReviewDbProvider> dbProviderProvider) {
-      super(REQUEST, current, local, dbProviderProvider);
+    Propagator(AcceptanceTestRequestScope atrScope, ThreadLocalRequestContext local) {
+      super(REQUEST, current, local);
       this.atrScope = atrScope;
     }
 
@@ -145,8 +126,8 @@
     this.local = local;
   }
 
-  public Context newContext(SchemaFactory<ReviewDb> sf, SshSession s, CurrentUser user) {
-    return new Context(sf, s, user, TimeUtil.nowMs());
+  public Context newContext(SshSession s, CurrentUser user) {
+    return new Context(s, user, TimeUtil.nowMs());
   }
 
   private Context newContinuingContext(Context ctx) {
@@ -164,23 +145,18 @@
     return current.get();
   }
 
-  public Context disableDb() {
+  /**
+   * Disables read and write access to NoteDb and returns the context prior to that modification.
+   */
+  public Context disableNoteDb() {
     Context old = current.get();
-    SchemaFactory<ReviewDb> sf = DisabledReviewDb::new;
-    Context ctx = new Context(sf, old.session, old.user, old.created);
+    Context ctx = new Context(old.session, old.user, old.created);
 
     current.set(ctx);
     local.setContext(ctx);
     return old;
   }
 
-  public Context reopenDb() {
-    // Setting a new context with the same fields is enough to get the ReviewDb
-    // provider to reopen the database.
-    Context old = current.get();
-    return set(new Context(old.schemaFactory, old.session, old.user, old.created));
-  }
-
   /** Returns exactly one instance per command executed. */
   static final Scope REQUEST =
       new Scope() {
diff --git a/java/com/google/gerrit/acceptance/AccountCreator.java b/java/com/google/gerrit/acceptance/AccountCreator.java
index b8a8776..aeae2c2 100644
--- a/java/com/google/gerrit/acceptance/AccountCreator.java
+++ b/java/com/google/gerrit/acceptance/AccountCreator.java
@@ -20,10 +20,9 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+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.Sequences;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.GroupCache;
@@ -31,7 +30,7 @@
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.group.db.GroupsUpdate;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.server.notedb.Sequences;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -108,7 +107,7 @@
       }
     }
 
-    account = new TestAccount(id, username, email, fullName, httpPass);
+    account = TestAccount.create(id, username, email, fullName, httpPass);
     if (username != null) {
       accounts.put(username, account);
     }
@@ -149,7 +148,7 @@
   }
 
   public void evict(Collection<Account.Id> ids) {
-    accounts.values().removeIf(a -> ids.contains(a.id));
+    accounts.values().removeIf(a -> ids.contains(a.id()));
   }
 
   public ImmutableList<TestAccount> getAll() {
@@ -157,7 +156,7 @@
   }
 
   private void addGroupMember(AccountGroup.UUID groupUuid, Account.Id accountId)
-      throws OrmException, IOException, NoSuchGroupException, ConfigInvalidException {
+      throws IOException, NoSuchGroupException, ConfigInvalidException {
     InternalGroupUpdate groupUpdate =
         InternalGroupUpdate.builder()
             .setMemberModification(memberIds -> Sets.union(memberIds, ImmutableSet.of(accountId)))
diff --git a/java/com/google/gerrit/acceptance/BUILD b/java/com/google/gerrit/acceptance/BUILD
index e41249b5..02f218a 100644
--- a/java/com/google/gerrit/acceptance/BUILD
+++ b/java/com/google/gerrit/acceptance/BUILD
@@ -19,6 +19,7 @@
         "//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",
@@ -28,6 +29,7 @@
         "//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",
@@ -36,8 +38,6 @@
         "//lib:args4j",
         "//lib:gson",
         "//lib:guava-retrying",
-        "//lib:gwtjsonrpc",
-        "//lib:gwtorm",
         "//lib:h2",
         "//lib:jimfs",
         "//lib:jsch",
@@ -68,6 +68,7 @@
     testonly = True,
     srcs = glob(["**/*.java"]),
     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",
@@ -90,6 +91,7 @@
         "//lib/jgit/org.eclipse.jgit.junit:junit",
         "//lib/log:impl-log4j",
         "//lib/log:log4j",
+        "//lib/mockito",
         "//lib/truth",
         "//lib/truth:truth-java8-extension",
         "//prolog:gerrit-prolog-common",
@@ -102,21 +104,25 @@
         "//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/pgm/init",
         "//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:gwtorm",
         "//lib:jsch",
         "//lib:servlet-api-3_1",
+        "//lib/commons:lang",
         "//lib/greenmail",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
diff --git a/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java b/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java
index 0aa56cf..0a1d765 100644
--- a/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java
+++ b/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.acceptance;
 
+import com.google.auto.value.AutoAnnotation;
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
-import java.lang.annotation.Annotation;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -45,32 +45,13 @@
     return cfg;
   }
 
-  static class GlobalPluginConfigToGerritConfig implements GerritConfig {
-    private final GlobalPluginConfig delegate;
+  private static GerritConfig toGerritConfig(GlobalPluginConfig annotation) {
+    return newGerritConfig(annotation.name(), annotation.value(), annotation.values());
+  }
 
-    GlobalPluginConfigToGerritConfig(GlobalPluginConfig delegate) {
-      this.delegate = delegate;
-    }
-
-    @Override
-    public Class<? extends Annotation> annotationType() {
-      return delegate.annotationType();
-    }
-
-    @Override
-    public String name() {
-      return delegate.name();
-    }
-
-    @Override
-    public String value() {
-      return delegate.value();
-    }
-
-    @Override
-    public String[] values() {
-      return delegate.values();
-    }
+  @AutoAnnotation
+  private static GerritConfig newGerritConfig(String name, String value, String[] values) {
+    return new AutoAnnotation_ConfigAnnotationParser_newGerritConfig(name, value, values);
   }
 
   static Map<String, Config> parse(GlobalPluginConfig annotation) {
@@ -79,7 +60,7 @@
     }
     Map<String, Config> result = new HashMap<>();
     Config cfg = new Config();
-    parseAnnotation(cfg, new GlobalPluginConfigToGerritConfig(annotation));
+    parseAnnotation(cfg, toGerritConfig(annotation));
     result.put(annotation.pluginName(), cfg);
     return result;
   }
@@ -100,7 +81,7 @@
         config = new Config();
         result.put(pluginName, config);
       }
-      parseAnnotation(config, new GlobalPluginConfigToGerritConfig(c));
+      parseAnnotation(config, toGerritConfig(c));
     }
 
     return result;
diff --git a/java/com/google/gerrit/acceptance/DisabledChangeIndex.java b/java/com/google/gerrit/acceptance/DisabledChangeIndex.java
index 0d473af..a32c6d1 100644
--- a/java/com/google/gerrit/acceptance/DisabledChangeIndex.java
+++ b/java/com/google/gerrit/acceptance/DisabledChangeIndex.java
@@ -20,10 +20,8 @@
 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.Id;
 import com.google.gerrit.server.index.change.ChangeIndex;
 import com.google.gerrit.server.query.change.ChangeData;
-import java.io.IOException;
 import java.util.Optional;
 
 /**
@@ -54,17 +52,17 @@
   }
 
   @Override
-  public void replace(ChangeData obj) throws IOException {
+  public void replace(ChangeData obj) {
     throw new UnsupportedOperationException("ChangeIndex is disabled");
   }
 
   @Override
-  public void delete(Id key) throws IOException {
+  public void delete(Change.Id key) {
     throw new UnsupportedOperationException("ChangeIndex is disabled");
   }
 
   @Override
-  public void deleteAll() throws IOException {
+  public void deleteAll() {
     throw new UnsupportedOperationException("ChangeIndex is disabled");
   }
 
@@ -75,12 +73,12 @@
   }
 
   @Override
-  public void markReady(boolean ready) throws IOException {
+  public void markReady(boolean ready) {
     throw new UnsupportedOperationException("ChangeIndex is disabled");
   }
 
   @Override
-  public Optional<ChangeData> get(Change.Id key, QueryOptions opts) throws IOException {
+  public Optional<ChangeData> get(Change.Id key, QueryOptions opts) {
     throw new UnsupportedOperationException("ChangeIndex is disabled");
   }
 }
diff --git a/java/com/google/gerrit/acceptance/EventRecorder.java b/java/com/google/gerrit/acceptance/EventRecorder.java
index fda77d2..cab6b58 100644
--- a/java/com/google/gerrit/acceptance/EventRecorder.java
+++ b/java/com/google/gerrit/acceptance/EventRecorder.java
@@ -56,7 +56,7 @@
     }
 
     public EventRecorder create(TestAccount user) {
-      return new EventRecorder(eventListeners, userFactory.create(user.id));
+      return new EventRecorder(eventListeners, userFactory.create(user.id()));
     }
   }
 
diff --git a/java/com/google/gerrit/acceptance/FakeGroupAuditService.java b/java/com/google/gerrit/acceptance/FakeGroupAuditService.java
new file mode 100644
index 0000000..48dc408
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/FakeGroupAuditService.java
@@ -0,0 +1,95 @@
+// 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;
+
+import static java.util.Comparator.comparing;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.primitives.Ints;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.httpd.GitOverHttpServlet;
+import com.google.gerrit.server.AuditEvent;
+import com.google.gerrit.server.audit.AuditListener;
+import com.google.gerrit.server.audit.AuditService;
+import com.google.gerrit.server.audit.HttpAuditEvent;
+import com.google.gerrit.server.audit.group.GroupAuditListener;
+import com.google.gerrit.server.group.GroupAuditService;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicLong;
+
+@Singleton
+public class FakeGroupAuditService extends AuditService {
+  public static class Module extends AbstractModule {
+    @Override
+    public void configure() {
+      DynamicSet.setOf(binder(), GroupAuditListener.class);
+      DynamicSet.setOf(binder(), AuditListener.class);
+
+      // Use this fake service at the Guice level rather than depending on tests binding their own
+      // audit listeners. If we used per-test listeners, then there would be a race between
+      // dispatching the audit events from HTTP requests performed during test setup in
+      // AbstractDaemonTest, and the later test setup binding the audit listener. Using a separate
+      // audit service implementation ensures all events get recorded.
+      bind(GroupAuditService.class).to(FakeGroupAuditService.class);
+    }
+  }
+
+  private final GitOverHttpServlet.Metrics httpMetrics;
+  private final BlockingQueue<HttpAuditEvent> httpEvents;
+  private final AtomicLong drainedSoFar;
+
+  @Inject
+  FakeGroupAuditService(
+      PluginSetContext<AuditListener> auditListeners,
+      PluginSetContext<GroupAuditListener> groupAuditListeners,
+      GitOverHttpServlet.Metrics httpMetrics) {
+    super(auditListeners, groupAuditListeners);
+    this.httpMetrics = httpMetrics;
+    this.httpEvents = new LinkedBlockingQueue<>();
+    this.drainedSoFar = new AtomicLong();
+  }
+
+  @Override
+  public void dispatch(AuditEvent action) {
+    super.dispatch(action);
+    if (action instanceof HttpAuditEvent) {
+      httpEvents.add((HttpAuditEvent) action);
+    }
+  }
+
+  public ImmutableList<HttpAuditEvent> drainHttpAuditEvents() throws Exception {
+    // Assumes that all HttpAuditEvents are produced by GitOverHttpServlet.
+    int expectedSize = Ints.checkedCast(httpMetrics.getRequestsStarted() - drainedSoFar.get());
+    List<HttpAuditEvent> result = new ArrayList<>();
+    for (int i = 0; i < expectedSize; i++) {
+      HttpAuditEvent e = httpEvents.poll(30, SECONDS);
+      if (e == null) {
+        throw new AssertionError(
+            String.format("Timeout after receiving %d/%d audit events", i, expectedSize));
+      }
+      drainedSoFar.incrementAndGet();
+      result.add(e);
+    }
+    return ImmutableList.sortedCopyOf(comparing(e -> e.when), result);
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/GcAssert.java b/java/com/google/gerrit/acceptance/GcAssert.java
index 7f90c3a..b9ef629 100644
--- a/java/com/google/gerrit/acceptance/GcAssert.java
+++ b/java/com/google/gerrit/acceptance/GcAssert.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
 import java.io.File;
-import java.io.FilenameFilter;
 import java.io.IOException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Repository;
@@ -53,13 +52,7 @@
   private String[] getPackFiles(Project.NameKey p) throws RepositoryNotFoundException, IOException {
     try (Repository repo = repoManager.openRepository(p)) {
       File packDir = new File(repo.getDirectory(), "objects/pack");
-      return packDir.list(
-          new FilenameFilter() {
-            @Override
-            public boolean accept(File dir, String name) {
-              return name.endsWith(".pack");
-            }
-          });
+      return packDir.list((dir, name) -> name.endsWith(".pack"));
     }
   }
 }
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index a88d09c..68f3175 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
 import static java.util.Objects.requireNonNull;
 import static org.apache.log4j.Logger.getLogger;
 
@@ -28,6 +29,10 @@
 import com.google.gerrit.acceptance.testsuite.account.AccountOperationsImpl;
 import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
 import com.google.gerrit.acceptance.testsuite.group.GroupOperationsImpl;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperationsImpl;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperationsImpl;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.lucene.LuceneIndexModule;
@@ -37,24 +42,22 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.git.receive.AsyncReceiveCommits;
+import com.google.gerrit.server.schema.JdbcAccountPatchReviewStore;
 import com.google.gerrit.server.ssh.NoSshModule;
-import com.google.gerrit.server.util.ManualRequestContext;
-import com.google.gerrit.server.util.OneOffRequestContext;
 import com.google.gerrit.server.util.SocketUtil;
 import com.google.gerrit.server.util.SystemLog;
 import com.google.gerrit.testing.FakeEmailSender;
-import com.google.gerrit.testing.FakeGroupAuditService;
-import com.google.gerrit.testing.InMemoryDatabase;
 import com.google.gerrit.testing.InMemoryRepositoryManager;
-import com.google.gerrit.testing.NoteDbChecker;
-import com.google.gerrit.testing.NoteDbMode;
 import com.google.gerrit.testing.SshMode;
-import com.google.gerrit.testing.TempFileUtil;
 import com.google.inject.AbstractModule;
+import com.google.inject.BindingAnnotation;
 import com.google.inject.Injector;
 import com.google.inject.Key;
 import com.google.inject.Module;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
 import java.lang.annotation.Annotation;
+import java.lang.annotation.Retention;
 import java.lang.reflect.Field;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
@@ -79,6 +82,7 @@
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.RepositoryCache;
 import org.eclipse.jgit.util.FS;
+import org.junit.rules.TemporaryFolder;
 
 public class GerritServer implements AutoCloseable {
   public static class StartupException extends Exception {
@@ -89,6 +93,11 @@
     }
   }
 
+  /** Marker on {@link InetSocketAddress} for test SSH server. */
+  @Retention(RUNTIME)
+  @BindingAnnotation
+  public @interface TestSshServerAddress {}
+
   @AutoValue
   public abstract static class Description {
     public static Description forTestClass(
@@ -99,6 +108,7 @@
           !has(UseLocalDisk.class, testDesc.getTestClass()) && !forceLocalDisk(),
           !has(NoHttpd.class, testDesc.getTestClass()),
           has(Sandboxed.class, testDesc.getTestClass()),
+          has(SkipProjectClone.class, testDesc.getTestClass()),
           has(UseSsh.class, testDesc.getTestClass()),
           null, // @GerritConfig is only valid on methods.
           null, // @GerritConfigs is only valid on methods.
@@ -119,6 +129,8 @@
               && !has(NoHttpd.class, testDesc.getTestClass()),
           testDesc.getAnnotation(Sandboxed.class) != null
               || has(Sandboxed.class, testDesc.getTestClass()),
+          testDesc.getAnnotation(SkipProjectClone.class) != null
+              || has(SkipProjectClone.class, testDesc.getTestClass()),
           testDesc.getAnnotation(UseSsh.class) != null
               || has(UseSsh.class, testDesc.getTestClass()),
           testDesc.getAnnotation(GerritConfig.class),
@@ -156,6 +168,8 @@
 
     abstract boolean sandboxed();
 
+    abstract boolean skipProjectClone();
+
     abstract boolean useSshAnnotation();
 
     boolean useSsh() {
@@ -309,11 +323,11 @@
   /**
    * Initializes new Gerrit site and returns started server.
    *
-   * <p>A new temporary directory for the site will be created with {@link TempFileUtil}, even in
+   * <p>A new temporary directory for the site will be created with {@code temporaryFolder}, even in
    * the server is otherwise configured in-memory. Closing the server stops the daemon but does not
-   * delete the temporary directory. Callers may either get the directory with {@link
-   * #getSitePath()} and delete it manually, or call {@link TempFileUtil#cleanup()}.
+   * delete the temporary directory..
    *
+   * @param temporaryFolder helper rule for creating site directories.
    * @param desc server description.
    * @param baseConfig default config values; merged with config from {@code desc}.
    * @param testSysModule additional Guice module to use.
@@ -321,15 +335,18 @@
    * @throws Exception
    */
   public static GerritServer initAndStart(
-      Description desc, Config baseConfig, @Nullable Module testSysModule) throws Exception {
-    Path site = TempFileUtil.createTempDirectory().toPath();
+      TemporaryFolder temporaryFolder,
+      Description desc,
+      Config baseConfig,
+      @Nullable Module testSysModule)
+      throws Exception {
+    Path site = temporaryFolder.newFolder().toPath();
     try {
       if (!desc.memory()) {
         init(desc, baseConfig, site);
       }
-      return start(desc, baseConfig, site, testSysModule, null, null);
+      return start(desc, baseConfig, site, testSysModule, null);
     } catch (Exception e) {
-      TempFileUtil.recursivelyDelete(site.toFile());
       throw e;
     }
   }
@@ -346,8 +363,6 @@
    * @param testSysModule optional additional module to add to the system injector.
    * @param inMemoryRepoManager {@link InMemoryRepositoryManager} that should be used if the site is
    *     started in memory
-   * @param inMemoryDatabaseInstance {@link com.google.gerrit.testing.InMemoryDatabase.Instance}
-   *     that should be used if the site is started in memory
    * @param additionalArgs additional command-line arguments for the daemon program; only allowed if
    *     the test is not in-memory.
    * @return started server.
@@ -359,7 +374,6 @@
       Path site,
       @Nullable Module testSysModule,
       @Nullable InMemoryRepositoryManager inMemoryRepoManager,
-      @Nullable InMemoryDatabase.Instance inMemoryDatabaseInstance,
       String... additionalArgs)
       throws Exception {
     checkArgument(site != null, "site is required (even for in-memory server");
@@ -385,8 +399,7 @@
 
     if (desc.memory()) {
       checkArgument(additionalArgs.length == 0, "cannot pass args to in-memory server");
-      return startInMemory(
-          desc, site, baseConfig, daemon, inMemoryRepoManager, inMemoryDatabaseInstance);
+      return startInMemory(desc, site, baseConfig, daemon, inMemoryRepoManager);
     }
     return startOnDisk(desc, site, daemon, serverStarted, additionalArgs);
   }
@@ -396,8 +409,7 @@
       Path site,
       Config baseConfig,
       Daemon daemon,
-      @Nullable InMemoryRepositoryManager inMemoryRepoManager,
-      @Nullable InMemoryDatabase.Instance inMemoryDatabaseInstance)
+      @Nullable InMemoryRepositoryManager inMemoryRepoManager)
       throws Exception {
     Config cfg = desc.buildConfig(baseConfig);
     daemon.setSlave(isSlave(baseConfig) || cfg.getBoolean("container", "slave", false));
@@ -410,12 +422,13 @@
     cfg.setBoolean("index", "lucene", "testInmemory", true);
     cfg.setBoolean("index", null, "onlineUpgrade", false);
     cfg.setString("gitweb", null, "cgi", "");
+    cfg.setString(
+        "accountPatchReviewDb", null, "url", JdbcAccountPatchReviewStore.TEST_IN_MEMORY_URL);
     daemon.setEnableHttpd(desc.httpd());
     daemon.setLuceneModule(LuceneIndexModule.singleVersionAllLatest(0, isSlave(baseConfig)));
     daemon.setDatabaseForTesting(
-        ImmutableList.<Module>of(
-            new InMemoryTestingDatabaseModule(
-                cfg, site, inMemoryRepoManager, inMemoryDatabaseInstance),
+        ImmutableList.of(
+            new InMemoryTestingDatabaseModule(cfg, site, inMemoryRepoManager),
             new AbstractModule() {
               @Override
               protected void configure() {
@@ -510,8 +523,6 @@
     cfg.setInt("receive", null, "threadPoolSize", 1);
     cfg.setInt("index", null, "threads", 1);
     cfg.setBoolean("index", null, "reindexAfterRefUpdate", false);
-
-    NoteDbMode.newNotesMigrationFromEnv().setConfigValues(cfg);
   }
 
   private static Injector createTestInjector(Daemon daemon) throws Exception {
@@ -524,12 +535,26 @@
             bind(AccountCreator.class);
             bind(AccountOperations.class).to(AccountOperationsImpl.class);
             bind(GroupOperations.class).to(GroupOperationsImpl.class);
+            bind(ProjectOperations.class).to(ProjectOperationsImpl.class);
+            bind(RequestScopeOperations.class).to(RequestScopeOperationsImpl.class);
             factory(PushOneCommit.Factory.class);
             install(InProcessProtocol.module());
             install(new NoSshModule());
             install(new AsyncReceiveCommits.Module());
             factory(ProjectResetter.Builder.Factory.class);
           }
+
+          @Provides
+          @Singleton
+          @Nullable
+          @TestSshServerAddress
+          InetSocketAddress getSshAddress(@GerritServerConfig Config cfg) {
+            String addr = cfg.getString("sshd", null, "listenAddress");
+            // We do not use InitSshd.isOff to avoid coupling GerritServer to the SSH code.
+            return !"off".equalsIgnoreCase(addr)
+                ? SocketUtil.resolve(cfg.getString("sshd", null, "listenAddress"), 0)
+                : null;
+          }
         };
     return sysInjector.createChildInjector(module);
   }
@@ -554,7 +579,6 @@
   private ExecutorService daemonService;
   private Injector testInjector;
   private String url;
-  private InetSocketAddress sshdAddress;
   private InetSocketAddress httpAddress;
 
   private GerritServer(
@@ -572,12 +596,6 @@
     Config cfg = testInjector.getInstance(Key.get(Config.class, GerritServerConfig.class));
     url = cfg.getString("gerrit", null, "canonicalWebUrl");
     URI uri = URI.create(url);
-
-    String addr = cfg.getString("sshd", null, "listenAddress");
-    // We do not use InitSshd.isOff to avoid coupling GerritServer to the SSH code.
-    if (!"off".equalsIgnoreCase(addr)) {
-      sshdAddress = SocketUtil.resolve(cfg.getString("sshd", null, "listenAddress"), 0);
-    }
     httpAddress = new InetSocketAddress(uri.getHost(), uri.getPort());
   }
 
@@ -585,10 +603,6 @@
     return url;
   }
 
-  InetSocketAddress getSshdAddress() {
-    return sshdAddress;
-  }
-
   InetSocketAddress getHttpAddress() {
     return httpAddress;
   }
@@ -614,21 +628,9 @@
       inMemoryRepoManager = server.testInjector.getInstance(InMemoryRepositoryManager.class);
     }
 
-    InMemoryDatabase.Instance dbInstance = null;
-    if (hasBinding(server.testInjector, InMemoryDatabase.class)) {
-      InMemoryDatabase inMemoryDatabase = server.testInjector.getInstance(InMemoryDatabase.class);
-      dbInstance = inMemoryDatabase.getDbInstance();
-      dbInstance.setKeepOpen(true);
-    }
-    try {
-      server.close();
-      server.daemon.stop();
-      return start(server.desc, cfg, site, null, inMemoryRepoManager, dbInstance);
-    } finally {
-      if (dbInstance != null) {
-        dbInstance.setKeepOpen(false);
-      }
-    }
+    server.close();
+    server.daemon.stop();
+    return start(server.desc, cfg, site, null, inMemoryRepoManager);
   }
 
   private static boolean hasBinding(Injector injector, Class<?> clazz) {
@@ -637,40 +639,19 @@
 
   @Override
   public void close() throws Exception {
-    try {
-      checkNoteDbState();
-    } finally {
-      daemon.getLifecycleManager().stop();
-      if (daemonService != null) {
-        System.out.println("Gerrit Server Shutdown");
-        daemonService.shutdownNow();
-        daemonService.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
-      }
-      RepositoryCache.clear();
+    daemon.getLifecycleManager().stop();
+    if (daemonService != null) {
+      System.out.println("Gerrit Server Shutdown");
+      daemonService.shutdownNow();
+      daemonService.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
     }
+    RepositoryCache.clear();
   }
 
   public Path getSitePath() {
     return sitePath;
   }
 
-  private void checkNoteDbState() throws Exception {
-    NoteDbMode mode = NoteDbMode.get();
-    if (mode != NoteDbMode.CHECK && mode != NoteDbMode.PRIMARY) {
-      return;
-    }
-    NoteDbChecker checker = testInjector.getInstance(NoteDbChecker.class);
-    OneOffRequestContext oneOffRequestContext =
-        testInjector.getInstance(OneOffRequestContext.class);
-    try (ManualRequestContext ctx = oneOffRequestContext.open()) {
-      if (mode == NoteDbMode.CHECK) {
-        checker.rebuildAndCheckAllChanges();
-      } else if (mode == NoteDbMode.PRIMARY) {
-        checker.assertNoReviewDbChanges(desc.testDescription());
-      }
-    }
-  }
-
   @Override
   public String toString() {
     return MoreObjects.toStringHelper(this).addValue(desc).toString();
diff --git a/java/com/google/gerrit/acceptance/HttpSession.java b/java/com/google/gerrit/acceptance/HttpSession.java
index fe446f4..833c53b 100644
--- a/java/com/google/gerrit/acceptance/HttpSession.java
+++ b/java/com/google/gerrit/acceptance/HttpSession.java
@@ -19,8 +19,10 @@
 import java.io.IOException;
 import java.net.URI;
 import org.apache.http.HttpHost;
+import org.apache.http.client.HttpClient;
 import org.apache.http.client.fluent.Executor;
 import org.apache.http.client.fluent.Request;
+import org.apache.http.impl.client.HttpClientBuilder;
 
 public class HttpSession {
   protected TestAccount account;
@@ -30,11 +32,12 @@
   public HttpSession(GerritServer server, @Nullable TestAccount account) {
     this.url = CharMatcher.is('/').trimTrailingFrom(server.getUrl());
     URI uri = URI.create(url);
-    this.executor = Executor.newInstance();
+    HttpClient noRedirectClient = HttpClientBuilder.create().disableRedirectHandling().build();
+    this.executor = Executor.newInstance(noRedirectClient);
     this.account = account;
     if (account != null) {
       executor.auth(
-          new HttpHost(uri.getHost(), uri.getPort()), account.username, account.httpPassword);
+          new HttpHost(uri.getHost(), uri.getPort()), account.username(), account.httpPassword());
     }
   }
 
diff --git a/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java b/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
index 6ee5c09..a704d2f 100644
--- a/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
+++ b/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
@@ -17,56 +17,38 @@
 import static com.google.inject.Scopes.SINGLETON;
 
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.metrics.DisabledMetricMaker;
 import com.google.gerrit.metrics.MetricMaker;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gerrit.server.config.TrackingFootersProvider;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.notedb.ChangeBundleReader;
-import com.google.gerrit.server.notedb.GwtormChangeBundleReader;
-import com.google.gerrit.server.notedb.NotesMigration;
-import com.google.gerrit.server.schema.DataSourceType;
-import com.google.gerrit.server.schema.NotesMigrationSchemaFactory;
-import com.google.gerrit.server.schema.ReviewDbFactory;
+import com.google.gerrit.server.schema.SchemaCreator;
 import com.google.gerrit.server.schema.SchemaModule;
-import com.google.gerrit.server.schema.SchemaVersion;
-import com.google.gerrit.testing.InMemoryDatabase;
-import com.google.gerrit.testing.InMemoryH2Type;
 import com.google.gerrit.testing.InMemoryRepositoryManager;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.OrmRuntimeException;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
-import com.google.inject.Key;
 import com.google.inject.ProvisionException;
-import com.google.inject.TypeLiteral;
-import com.google.inject.util.Providers;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
 
 class InMemoryTestingDatabaseModule extends LifecycleModule {
   private final Config cfg;
   private final Path sitePath;
   @Nullable private final InMemoryRepositoryManager repoManager;
-  @Nullable private final InMemoryDatabase.Instance inMemoryDatabaseInstance;
 
   InMemoryTestingDatabaseModule(
-      Config cfg,
-      Path sitePath,
-      @Nullable InMemoryRepositoryManager repoManager,
-      @Nullable InMemoryDatabase.Instance inMemoryDatabaseInstance) {
+      Config cfg, Path sitePath, @Nullable InMemoryRepositoryManager repoManager) {
     this.cfg = cfg;
     this.sitePath = sitePath;
     this.repoManager = repoManager;
-    this.inMemoryDatabaseInstance = inMemoryDatabaseInstance;
     makeSiteDirs(sitePath);
   }
 
@@ -85,49 +67,36 @@
     }
 
     bind(MetricMaker.class).to(DisabledMetricMaker.class);
-    bind(DataSourceType.class).to(InMemoryH2Type.class);
 
-    install(new NotesMigration.Module());
-    TypeLiteral<SchemaFactory<ReviewDb>> schemaFactory =
-        new TypeLiteral<SchemaFactory<ReviewDb>>() {};
-    bind(schemaFactory).to(NotesMigrationSchemaFactory.class);
-    bind(InMemoryDatabase.Instance.class).toProvider(Providers.of(inMemoryDatabaseInstance));
-    bind(Key.get(schemaFactory, ReviewDbFactory.class)).to(InMemoryDatabase.class);
-    bind(InMemoryDatabase.class).in(SINGLETON);
-    bind(ChangeBundleReader.class).to(GwtormChangeBundleReader.class);
-
-    listener().to(CreateDatabase.class);
+    listener().to(CreateSchema.class);
 
     bind(SitePaths.class);
     bind(TrackingFooters.class).toProvider(TrackingFootersProvider.class).in(SINGLETON);
 
     install(new SchemaModule());
-    bind(SchemaVersion.class).to(SchemaVersion.C);
 
     install(new SshdModule());
   }
 
-  static class CreateDatabase implements LifecycleListener {
-    private final InMemoryDatabase mem;
+  static class CreateSchema implements LifecycleListener {
+    private final SchemaCreator schemaCreator;
 
     @Inject
-    CreateDatabase(InMemoryDatabase mem) {
-      this.mem = mem;
+    CreateSchema(SchemaCreator schemaCreator) {
+      this.schemaCreator = schemaCreator;
     }
 
     @Override
     public void start() {
       try {
-        mem.create();
-      } catch (OrmException e) {
-        throw new OrmRuntimeException(e);
+        schemaCreator.ensureCreated();
+      } catch (IOException | ConfigInvalidException e) {
+        throw new StorageException(e);
       }
     }
 
     @Override
-    public void stop() {
-      mem.getDbInstance().drop();
-    }
+    public void stop() {}
   }
 
   private static void makeSiteDirs(Path p) {
diff --git a/java/com/google/gerrit/acceptance/InProcessProtocol.java b/java/com/google/gerrit/acceptance/InProcessProtocol.java
index 94c4635..69715a9 100644
--- a/java/com/google/gerrit/acceptance/InProcessProtocol.java
+++ b/java/com/google/gerrit/acceptance/InProcessProtocol.java
@@ -14,6 +14,10 @@
 
 package com.google.gerrit.acceptance;
 
+import static com.google.gerrit.server.git.receive.LazyPostReceiveHookChain.affectsSize;
+import static com.google.gerrit.server.quota.QuotaGroupDefinitions.REPOSITORY_SIZE_GROUP;
+
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.gerrit.acceptance.InProcessProtocol.Context;
 import com.google.gerrit.common.data.Capable;
@@ -21,14 +25,12 @@
 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.server.ReviewDb;
 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.config.RequestScopedReviewDbProvider;
 import com.google.gerrit.server.git.DefaultAdvertiseRefsHook;
 import com.google.gerrit.server.git.ReceivePackInitializer;
 import com.google.gerrit.server.git.TransferConfig;
@@ -39,13 +41,16 @@
 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.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.quota.QuotaBackend;
+import com.google.gerrit.server.quota.QuotaException;
+import com.google.gerrit.server.quota.QuotaResponse;
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.RequestScopePropagator;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestScopePropagator;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Key;
@@ -55,7 +60,6 @@
 import com.google.inject.Provides;
 import com.google.inject.Scope;
 import com.google.inject.servlet.RequestScoped;
-import com.google.inject.util.Providers;
 import java.io.IOException;
 import java.net.SocketAddress;
 import java.util.HashMap;
@@ -122,10 +126,8 @@
 
   private static class Propagator extends ThreadLocalRequestScopePropagator<Context> {
     @Inject
-    Propagator(
-        ThreadLocalRequestContext local,
-        Provider<RequestScopedReviewDbProvider> dbProviderProvider) {
-      super(REQUEST, current, local, dbProviderProvider);
+    Propagator(ThreadLocalRequestContext local) {
+      super(REQUEST, current, local);
     }
 
     @Override
@@ -150,12 +152,9 @@
    * request.
    */
   static class Context implements RequestContext {
-    private static final Key<RequestScopedReviewDbProvider> DB_KEY =
-        Key.get(RequestScopedReviewDbProvider.class);
     private static final Key<RequestCleanup> RC_KEY = Key.get(RequestCleanup.class);
     private static final Key<CurrentUser> USER_KEY = Key.get(CurrentUser.class);
 
-    private final SchemaFactory<ReviewDb> schemaFactory;
     private final IdentifiedUser.GenericFactory userFactory;
     private final Account.Id accountId;
     private final Project.NameKey project;
@@ -163,17 +162,12 @@
     private final Map<Key<?>, Object> map;
 
     Context(
-        SchemaFactory<ReviewDb> schemaFactory,
-        IdentifiedUser.GenericFactory userFactory,
-        Account.Id accountId,
-        Project.NameKey project) {
-      this.schemaFactory = schemaFactory;
+        IdentifiedUser.GenericFactory userFactory, Account.Id accountId, Project.NameKey project) {
       this.userFactory = userFactory;
       this.accountId = accountId;
       this.project = project;
       map = new HashMap<>();
       cleanup = new RequestCleanup();
-      map.put(DB_KEY, new RequestScopedReviewDbProvider(schemaFactory, Providers.of(cleanup)));
       map.put(RC_KEY, cleanup);
 
       IdentifiedUser user = userFactory.create(accountId);
@@ -182,7 +176,7 @@
     }
 
     private Context newContinuingContext() {
-      return new Context(schemaFactory, userFactory, accountId, project);
+      return new Context(userFactory, accountId, project);
     }
 
     @Override
@@ -190,11 +184,6 @@
       return get(USER_KEY, null);
     }
 
-    @Override
-    public Provider<ReviewDb> getReviewDbProvider() {
-      return get(DB_KEY, null);
-    }
-
     private synchronized <T> T get(Key<T> key, Provider<T> creator) {
       @SuppressWarnings("unchecked")
       T t = (T) map.get(key);
@@ -208,7 +197,7 @@
 
   private static class Upload implements UploadPackFactory<Context> {
     private final TransferConfig transferConfig;
-    private final DynamicSet<UploadPackInitializer> uploadPackInitializers;
+    private final PluginSetContext<UploadPackInitializer> uploadPackInitializers;
     private final DynamicSet<PreUploadHook> preUploadHooks;
     private final UploadValidators.Factory uploadValidatorsFactory;
     private final ThreadLocalRequestContext threadContext;
@@ -218,7 +207,7 @@
     @Inject
     Upload(
         TransferConfig transferConfig,
-        DynamicSet<UploadPackInitializer> uploadPackInitializers,
+        PluginSetContext<UploadPackInitializer> uploadPackInitializers,
         DynamicSet<PreUploadHook> preUploadHooks,
         UploadValidators.Factory uploadValidatorsFactory,
         ThreadLocalRequestContext threadContext,
@@ -267,9 +256,7 @@
       List<PreUploadHook> hooks = Lists.newArrayList(preUploadHooks);
       hooks.add(uploadValidatorsFactory.create(projectState.getProject(), repo, "localhost-test"));
       up.setPreUploadHook(PreUploadHookChain.newChain(hooks));
-      for (UploadPackInitializer initializer : uploadPackInitializers) {
-        initializer.init(req.project, up);
-      }
+      uploadPackInitializers.runEach(initializer -> initializer.init(req.project, up));
       return up;
     }
   }
@@ -279,10 +266,11 @@
     private final ProjectCache projectCache;
     private final AsyncReceiveCommits.Factory factory;
     private final TransferConfig config;
-    private final DynamicSet<ReceivePackInitializer> receivePackInitializers;
+    private final PluginSetContext<ReceivePackInitializer> receivePackInitializers;
     private final DynamicSet<PostReceiveHook> postReceiveHooks;
     private final ThreadLocalRequestContext threadContext;
     private final PermissionBackend permissionBackend;
+    private final QuotaBackend quotaBackend;
 
     @Inject
     Receive(
@@ -290,10 +278,11 @@
         ProjectCache projectCache,
         AsyncReceiveCommits.Factory factory,
         TransferConfig config,
-        DynamicSet<ReceivePackInitializer> receivePackInitializers,
+        PluginSetContext<ReceivePackInitializer> receivePackInitializers,
         DynamicSet<PostReceiveHook> postReceiveHooks,
         ThreadLocalRequestContext threadContext,
-        PermissionBackend permissionBackend) {
+        PermissionBackend permissionBackend,
+        QuotaBackend quotaBackend) {
       this.userProvider = userProvider;
       this.projectCache = projectCache;
       this.factory = factory;
@@ -302,6 +291,7 @@
       this.postReceiveHooks = postReceiveHooks;
       this.threadContext = threadContext;
       this.permissionBackend = permissionBackend;
+      this.quotaBackend = quotaBackend;
     }
 
     @Override
@@ -339,13 +329,37 @@
         rp.setTimeout(config.getTimeout());
         rp.setMaxObjectSizeLimit(config.getMaxObjectSizeLimit());
 
-        for (ReceivePackInitializer initializer : receivePackInitializers) {
-          initializer.init(projectState.getNameKey(), rp);
-        }
+        receivePackInitializers.runEach(
+            initializer -> initializer.init(projectState.getNameKey(), rp));
+        QuotaResponse.Aggregated availableTokens =
+            quotaBackend
+                .user(identifiedUser)
+                .project(req.project)
+                .availableTokens(REPOSITORY_SIZE_GROUP);
+        availableTokens.throwOnError();
+        availableTokens.availableTokens().ifPresent(v -> rp.setMaxObjectSizeLimit(v));
 
-        rp.setPostReceiveHook(PostReceiveHookChain.newChain(Lists.newArrayList(postReceiveHooks)));
+        ImmutableList<PostReceiveHook> hooks =
+            ImmutableList.<PostReceiveHook>builder()
+                .add(
+                    (pack, commands) -> {
+                      if (affectsSize(pack, commands)) {
+                        try {
+                          quotaBackend
+                              .user(identifiedUser)
+                              .project(req.project)
+                              .requestTokens(REPOSITORY_SIZE_GROUP, pack.getPackSize())
+                              .throwOnError();
+                        } catch (QuotaException e) {
+                          throw new RuntimeException(e);
+                        }
+                      }
+                    })
+                .addAll(postReceiveHooks)
+                .build();
+        rp.setPostReceiveHook(PostReceiveHookChain.newChain(hooks));
         return rp;
-      } catch (IOException | PermissionBackendException e) {
+      } catch (IOException | PermissionBackendException | QuotaException e) {
         throw new RuntimeException(e);
       }
     }
diff --git a/java/com/google/gerrit/acceptance/ProjectResetter.java b/java/com/google/gerrit/acceptance/ProjectResetter.java
index cc263c6..ae397a9 100644
--- a/java/com/google/gerrit/acceptance/ProjectResetter.java
+++ b/java/com/google/gerrit/acceptance/ProjectResetter.java
@@ -353,7 +353,7 @@
   }
 
   /** Evict groups that were modified. */
-  private void evictAndReindexGroups() throws IOException {
+  private void evictAndReindexGroups() {
     if (groupCache != null || groupIndexer != null) {
       Set<AccountGroup.UUID> modifiedGroups =
           new HashSet<>(groupUUIDs(restoredRefsByProject.get(allUsersName)));
@@ -367,7 +367,7 @@
     }
   }
 
-  private void evictAndReindexAccount(Account.Id accountId) throws IOException {
+  private void evictAndReindexAccount(Account.Id accountId) {
     if (accountCache != null) {
       accountCache.evict(accountId);
     }
@@ -379,7 +379,7 @@
     }
   }
 
-  private void evictAndReindexGroup(AccountGroup.UUID uuid) throws IOException {
+  private void evictAndReindexGroup(AccountGroup.UUID uuid) {
     if (groupCache != null) {
       groupCache.evict(uuid);
     }
diff --git a/java/com/google/gerrit/acceptance/PushOneCommit.java b/java/com/google/gerrit/acceptance/PushOneCommit.java
index 5e45df2..e15dd40 100644
--- a/java/com/google/gerrit/acceptance/PushOneCommit.java
+++ b/java/com/google/gerrit/acceptance/PushOneCommit.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
-import static java.util.stream.Collectors.toList;
 import static org.junit.Assert.assertEquals;
 
 import com.google.common.base.Strings;
@@ -28,14 +27,11 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
@@ -43,7 +39,6 @@
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicInteger;
-import java.util.stream.Stream;
 import org.eclipse.jgit.api.TagCommand;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -77,16 +72,12 @@
           + PATCH_FILE_ONLY;
 
   public interface Factory {
-    PushOneCommit create(ReviewDb db, PersonIdent i, TestRepository<?> testRepo);
+    PushOneCommit create(PersonIdent i, TestRepository<?> testRepo);
 
     PushOneCommit create(
-        ReviewDb db,
-        PersonIdent i,
-        TestRepository<?> testRepo,
-        @Assisted("changeId") String changeId);
+        PersonIdent i, TestRepository<?> testRepo, @Assisted("changeId") String changeId);
 
     PushOneCommit create(
-        ReviewDb db,
         PersonIdent i,
         TestRepository<?> testRepo,
         @Assisted("subject") String subject,
@@ -94,14 +85,12 @@
         @Assisted("content") String content);
 
     PushOneCommit create(
-        ReviewDb db,
         PersonIdent i,
         TestRepository<?> testRepo,
         @Assisted String subject,
         @Assisted Map<String, String> files);
 
     PushOneCommit create(
-        ReviewDb db,
         PersonIdent i,
         TestRepository<?> testRepo,
         @Assisted("subject") String subject,
@@ -144,8 +133,6 @@
   private final ChangeNotes.Factory notesFactory;
   private final ApprovalsUtil approvalsUtil;
   private final Provider<InternalChangeQuery> queryProvider;
-  private final NotesMigration notesMigration;
-  private final ReviewDb db;
   private final TestRepository<?> testRepo;
 
   private final String subject;
@@ -162,22 +149,10 @@
       ChangeNotes.Factory notesFactory,
       ApprovalsUtil approvalsUtil,
       Provider<InternalChangeQuery> queryProvider,
-      NotesMigration notesMigration,
-      @Assisted ReviewDb db,
       @Assisted PersonIdent i,
       @Assisted TestRepository<?> testRepo)
       throws Exception {
-    this(
-        notesFactory,
-        approvalsUtil,
-        queryProvider,
-        notesMigration,
-        db,
-        i,
-        testRepo,
-        SUBJECT,
-        FILE_NAME,
-        FILE_CONTENT);
+    this(notesFactory, approvalsUtil, queryProvider, i, testRepo, SUBJECT, FILE_NAME, FILE_CONTENT);
   }
 
   @AssistedInject
@@ -185,8 +160,6 @@
       ChangeNotes.Factory notesFactory,
       ApprovalsUtil approvalsUtil,
       Provider<InternalChangeQuery> queryProvider,
-      NotesMigration notesMigration,
-      @Assisted ReviewDb db,
       @Assisted PersonIdent i,
       @Assisted TestRepository<?> testRepo,
       @Assisted("changeId") String changeId)
@@ -195,8 +168,6 @@
         notesFactory,
         approvalsUtil,
         queryProvider,
-        notesMigration,
-        db,
         i,
         testRepo,
         SUBJECT,
@@ -210,26 +181,13 @@
       ChangeNotes.Factory notesFactory,
       ApprovalsUtil approvalsUtil,
       Provider<InternalChangeQuery> queryProvider,
-      NotesMigration notesMigration,
-      @Assisted ReviewDb db,
       @Assisted PersonIdent i,
       @Assisted TestRepository<?> testRepo,
       @Assisted("subject") String subject,
       @Assisted("fileName") String fileName,
       @Assisted("content") String content)
       throws Exception {
-    this(
-        notesFactory,
-        approvalsUtil,
-        queryProvider,
-        notesMigration,
-        db,
-        i,
-        testRepo,
-        subject,
-        fileName,
-        content,
-        null);
+    this(notesFactory, approvalsUtil, queryProvider, i, testRepo, subject, fileName, content, null);
   }
 
   @AssistedInject
@@ -237,24 +195,12 @@
       ChangeNotes.Factory notesFactory,
       ApprovalsUtil approvalsUtil,
       Provider<InternalChangeQuery> queryProvider,
-      NotesMigration notesMigration,
-      @Assisted ReviewDb db,
       @Assisted PersonIdent i,
       @Assisted TestRepository<?> testRepo,
       @Assisted String subject,
       @Assisted Map<String, String> files)
       throws Exception {
-    this(
-        notesFactory,
-        approvalsUtil,
-        queryProvider,
-        notesMigration,
-        db,
-        i,
-        testRepo,
-        subject,
-        files,
-        null);
+    this(notesFactory, approvalsUtil, queryProvider, i, testRepo, subject, files, null);
   }
 
   @AssistedInject
@@ -262,8 +208,6 @@
       ChangeNotes.Factory notesFactory,
       ApprovalsUtil approvalsUtil,
       Provider<InternalChangeQuery> queryProvider,
-      NotesMigration notesMigration,
-      @Assisted ReviewDb db,
       @Assisted PersonIdent i,
       @Assisted TestRepository<?> testRepo,
       @Assisted("subject") String subject,
@@ -275,8 +219,6 @@
         notesFactory,
         approvalsUtil,
         queryProvider,
-        notesMigration,
-        db,
         i,
         testRepo,
         subject,
@@ -288,20 +230,16 @@
       ChangeNotes.Factory notesFactory,
       ApprovalsUtil approvalsUtil,
       Provider<InternalChangeQuery> queryProvider,
-      NotesMigration notesMigration,
-      ReviewDb db,
       PersonIdent i,
       TestRepository<?> testRepo,
       String subject,
       Map<String, String> files,
       String changeId)
       throws Exception {
-    this.db = db;
     this.testRepo = testRepo;
     this.notesFactory = notesFactory;
     this.approvalsUtil = approvalsUtil;
     this.queryProvider = queryProvider;
-    this.notesMigration = notesMigration;
     this.subject = subject;
     this.files = files;
     this.changeId = changeId;
@@ -395,15 +333,15 @@
       this.resSubj = subject;
     }
 
-    public ChangeData getChange() throws OrmException {
+    public ChangeData getChange() {
       return Iterables.getOnlyElement(queryProvider.get().byKeyPrefix(changeId));
     }
 
-    public PatchSet getPatchSet() throws OrmException {
+    public PatchSet getPatchSet() {
       return getChange().currentPatchSet();
     }
 
-    public PatchSet.Id getPatchSetId() throws OrmException {
+    public PatchSet.Id getPatchSetId() {
       return getChange().change().currentPatchSetId();
     }
 
@@ -420,8 +358,7 @@
     }
 
     public void assertChange(
-        Change.Status expectedStatus, String expectedTopic, TestAccount... expectedReviewers)
-        throws OrmException {
+        Change.Status expectedStatus, String expectedTopic, TestAccount... expectedReviewers) {
       assertChange(
           expectedStatus, expectedTopic, Arrays.asList(expectedReviewers), ImmutableList.of());
     }
@@ -430,28 +367,19 @@
         Change.Status expectedStatus,
         String expectedTopic,
         List<TestAccount> expectedReviewers,
-        List<TestAccount> expectedCcs)
-        throws OrmException {
+        List<TestAccount> expectedCcs) {
       Change c = getChange().change();
       assertThat(c.getSubject()).isEqualTo(resSubj);
       assertThat(c.getStatus()).isEqualTo(expectedStatus);
       assertThat(Strings.emptyToNull(c.getTopic())).isEqualTo(expectedTopic);
-      if (notesMigration.readChanges()) {
-        assertReviewers(c, ReviewerStateInternal.REVIEWER, expectedReviewers);
-        assertReviewers(c, ReviewerStateInternal.CC, expectedCcs);
-      } else {
-        assertReviewers(
-            c,
-            ReviewerStateInternal.REVIEWER,
-            Stream.concat(expectedReviewers.stream(), expectedCcs.stream()).collect(toList()));
-      }
+      assertReviewers(c, ReviewerStateInternal.REVIEWER, expectedReviewers);
+      assertReviewers(c, ReviewerStateInternal.CC, expectedCcs);
     }
 
     private void assertReviewers(
-        Change c, ReviewerStateInternal state, List<TestAccount> expectedReviewers)
-        throws OrmException {
+        Change c, ReviewerStateInternal state, List<TestAccount> expectedReviewers) {
       Iterable<Account.Id> actualIds =
-          approvalsUtil.getReviewers(db, notesFactory.createChecked(db, c)).byState(state);
+          approvalsUtil.getReviewers(notesFactory.createChecked(c)).byState(state);
       assertThat(actualIds)
           .containsExactlyElementsIn(Sets.newHashSet(TestAccount.ids(expectedReviewers)));
     }
diff --git a/java/com/google/gerrit/acceptance/ReadOnlyChangeIndex.java b/java/com/google/gerrit/acceptance/ReadOnlyChangeIndex.java
index 5209f90..19910db 100644
--- a/java/com/google/gerrit/acceptance/ReadOnlyChangeIndex.java
+++ b/java/com/google/gerrit/acceptance/ReadOnlyChangeIndex.java
@@ -19,10 +19,9 @@
 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.Id;
+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.io.IOException;
 
 class ReadOnlyChangeIndex implements ChangeIndex {
   private final ChangeIndex index;
@@ -46,17 +45,17 @@
   }
 
   @Override
-  public void replace(ChangeData obj) throws IOException {
+  public void replace(ChangeData obj) {
     // do nothing
   }
 
   @Override
-  public void delete(Id key) throws IOException {
+  public void delete(Change.Id key) {
     // do nothing
   }
 
   @Override
-  public void deleteAll() throws IOException {
+  public void deleteAll() {
     // do nothing
   }
 
@@ -67,7 +66,7 @@
   }
 
   @Override
-  public void markReady(boolean ready) throws IOException {
+  public void markReady(boolean ready) {
     // do nothing
   }
 }
diff --git a/java/com/google/gerrit/acceptance/ReindexGroupsAtStartup.java b/java/com/google/gerrit/acceptance/ReindexGroupsAtStartup.java
index 84e798c..bd8a926 100644
--- a/java/com/google/gerrit/acceptance/ReindexGroupsAtStartup.java
+++ b/java/com/google/gerrit/acceptance/ReindexGroupsAtStartup.java
@@ -63,15 +63,7 @@
       throw new IllegalStateException("Unable to reindex groups, tests may fail", e);
     }
 
-    allGroupReferences.forEach(
-        group -> {
-          try {
-            groupIndexer.index(group.getUUID());
-          } catch (IOException e) {
-            throw new IllegalStateException(
-                String.format("Unable to index %s, tests may fail", group), e);
-          }
-        });
+    allGroupReferences.forEach(group -> groupIndexer.index(group.getUUID()));
   }
 
   @Override
diff --git a/java/com/google/gerrit/acceptance/ReindexProjectsAtStartup.java b/java/com/google/gerrit/acceptance/ReindexProjectsAtStartup.java
index f585f81..2f0ffcb 100644
--- a/java/com/google/gerrit/acceptance/ReindexProjectsAtStartup.java
+++ b/java/com/google/gerrit/acceptance/ReindexProjectsAtStartup.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
 import com.google.inject.Scopes;
-import java.io.IOException;
 
 /** Reindex all projects at Gerrit daemon startup. */
 public class ReindexProjectsAtStartup implements LifecycleListener {
@@ -42,16 +41,7 @@
 
   @Override
   public void start() {
-    repoMgr.list().stream()
-        .forEach(
-            projectName -> {
-              try {
-                projectIndexer.index(projectName);
-              } catch (IOException e) {
-                throw new IllegalStateException(
-                    String.format("Unable to index %s, tests may fail", projectName), e);
-              }
-            });
+    repoMgr.list().stream().forEach(projectIndexer::index);
   }
 
   @Override
diff --git a/java/com/google/gerrit/acceptance/RestResponse.java b/java/com/google/gerrit/acceptance/RestResponse.java
index da08215..e8de5c6 100644
--- a/java/com/google/gerrit/acceptance/RestResponse.java
+++ b/java/com/google/gerrit/acceptance/RestResponse.java
@@ -14,6 +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 com.google.gerrit.httpd.restapi.RestApiServlet.JSON_MAGIC;
 import static java.nio.charset.StandardCharsets.UTF_8;
@@ -21,6 +22,7 @@
 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 {
@@ -83,4 +85,9 @@
   public void assertPreconditionFailed() throws Exception {
     assertStatus(HttpStatus.SC_PRECONDITION_FAILED);
   }
+
+  public void assertTemporaryRedirect(String path) throws Exception {
+    assertStatus(HttpStatus.SC_MOVED_TEMPORARILY);
+    assertThat(URI.create(getHeader("Location")).getPath()).isEqualTo(path);
+  }
 }
diff --git a/java/com/google/gerrit/acceptance/RestSession.java b/java/com/google/gerrit/acceptance/RestSession.java
index e77e527..0e7ad4b 100644
--- a/java/com/google/gerrit/acceptance/RestSession.java
+++ b/java/com/google/gerrit/acceptance/RestSession.java
@@ -20,7 +20,7 @@
 import com.google.common.net.HttpHeaders;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.restapi.RawInput;
-import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.json.OutputFormat;
 import java.io.IOException;
 import org.apache.http.Header;
 import org.apache.http.client.fluent.Request;
diff --git a/java/com/google/gerrit/acceptance/SkipProjectClone.java b/java/com/google/gerrit/acceptance/SkipProjectClone.java
new file mode 100644
index 0000000..9a326d8
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/SkipProjectClone.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.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;
+
+@Target({TYPE, METHOD})
+@Retention(RUNTIME)
+public @interface SkipProjectClone {}
diff --git a/java/com/google/gerrit/acceptance/SshSession.java b/java/com/google/gerrit/acceptance/SshSession.java
index 52d7f28..fa0bc90 100644
--- a/java/com/google/gerrit/acceptance/SshSession.java
+++ b/java/com/google/gerrit/acceptance/SshSession.java
@@ -19,6 +19,7 @@
 import static com.google.common.truth.Truth.assertWithMessage;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.gerrit.acceptance.testsuite.account.TestAccount;
 import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
 import com.jcraft.jsch.ChannelExec;
 import com.jcraft.jsch.JSch;
@@ -35,9 +36,9 @@
   private Session session;
   private String error;
 
-  public SshSession(TestSshKeys sshKeys, GerritServer server, TestAccount account) {
+  public SshSession(TestSshKeys sshKeys, InetSocketAddress addr, TestAccount account) {
     this.sshKeys = sshKeys;
-    this.addr = server.getSshdAddress();
+    this.addr = addr;
     this.account = account;
   }
 
@@ -112,8 +113,14 @@
       JSch jsch = new JSch();
       jsch.addIdentity(
           "KeyPair", TestSshKeys.privateKey(keyPair), keyPair.getPublicKeyBlob(), null);
-      session =
-          jsch.getSession(account.username, addr.getAddress().getHostAddress(), addr.getPort());
+      String username =
+          account
+              .username()
+              .orElseThrow(
+                  () ->
+                      new IllegalStateException(
+                          "account " + account.accountId() + " must have a username to use SSH"));
+      session = jsch.getSession(username, addr.getAddress().getHostAddress(), addr.getPort());
       session.setConfig("StrictHostKeyChecking", "no");
       session.connect();
     }
diff --git a/java/com/google/gerrit/acceptance/StandaloneSiteTest.java b/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
index a50de1a..29d0b35 100644
--- a/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
+++ b/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
@@ -25,7 +25,6 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.launcher.GerritLauncher;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.util.ManualRequestContext;
@@ -34,7 +33,6 @@
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.inject.Injector;
 import com.google.inject.Module;
-import com.google.inject.Provider;
 import java.io.File;
 import java.util.Arrays;
 import java.util.Collections;
@@ -61,7 +59,7 @@
       this.server = server;
       Injector i = server.getTestInjector();
       if (adminId == null) {
-        adminId = i.getInstance(AccountCreator.class).admin().getId();
+        adminId = i.getInstance(AccountCreator.class).admin().id();
       }
       ctx = i.getInstance(OneOffRequestContext.class).openAs(adminId);
       GerritApi gApi = i.getInstance(GerritApi.class);
@@ -82,11 +80,6 @@
       return ctx.getUser();
     }
 
-    @Override
-    public Provider<ReviewDb> getReviewDbProvider() {
-      return ctx.getReviewDbProvider();
-    }
-
     public Injector getInjector() {
       return server.getTestInjector();
     }
@@ -107,10 +100,8 @@
   private final TemporaryFolder tempSiteDir = new TemporaryFolder();
 
   private final TestRule testRunner =
-      new TestRule() {
-        @Override
-        public Statement apply(Statement base, Description description) {
-          return new Statement() {
+      (base, description) ->
+          new Statement() {
             @Override
             public void evaluate() throws Throwable {
               try {
@@ -121,8 +112,6 @@
               }
             }
           };
-        }
-      };
 
   @Rule public RuleChain ruleChain = RuleChain.outerRule(tempSiteDir).around(testRunner);
 
@@ -209,7 +198,7 @@
   private GerritServer startImpl(@Nullable Module testSysModule, String... additionalArgs)
       throws Exception {
     return GerritServer.start(
-        serverDesc, baseConfig, sitePaths.site_path, testSysModule, null, null, additionalArgs);
+        serverDesc, baseConfig, sitePaths.site_path, testSysModule, null, additionalArgs);
   }
 
   protected static void runGerrit(String... args) throws Exception {
diff --git a/java/com/google/gerrit/acceptance/TestAccount.java b/java/com/google/gerrit/acceptance/TestAccount.java
index ac197bd..c937aed 100644
--- a/java/com/google/gerrit/acceptance/TestAccount.java
+++ b/java/com/google/gerrit/acceptance/TestAccount.java
@@ -14,67 +14,80 @@
 
 package com.google.gerrit.acceptance;
 
-import static java.util.stream.Collectors.toList;
+import static com.google.common.collect.ImmutableList.toImmutableList;
 
-import com.google.common.base.MoreObjects;
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Streams;
 import com.google.common.net.InetAddresses;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
 import java.net.InetSocketAddress;
 import java.util.Arrays;
-import java.util.List;
 import org.apache.http.client.utils.URIBuilder;
 import org.eclipse.jgit.lib.PersonIdent;
 
-public class TestAccount {
-  public static List<Account.Id> ids(List<TestAccount> accounts) {
-    return accounts.stream().map(a -> a.id).collect(toList());
+@AutoValue
+public abstract class TestAccount {
+  public static ImmutableList<Account.Id> ids(Iterable<TestAccount> accounts) {
+    return Streams.stream(accounts).map(TestAccount::id).collect(toImmutableList());
   }
 
-  public static List<String> names(List<TestAccount> accounts) {
-    return accounts.stream().map(a -> a.fullName).collect(toList());
+  public static ImmutableList<String> names(Iterable<TestAccount> accounts) {
+    return Streams.stream(accounts).map(TestAccount::fullName).collect(toImmutableList());
   }
 
-  public static List<String> names(TestAccount... accounts) {
+  public static ImmutableList<String> names(TestAccount... accounts) {
     return names(Arrays.asList(accounts));
   }
 
-  public final Account.Id id;
-  public final String username;
-  public final String email;
-  public final Address emailAddress;
-  public final String fullName;
-  public final String httpPassword;
-
-  TestAccount(Account.Id id, String username, String email, String fullName, String httpPassword) {
-    this.id = id;
-    this.username = username;
-    this.email = email;
-    this.emailAddress = new Address(fullName, email);
-    this.fullName = fullName;
-    this.httpPassword = httpPassword;
+  static TestAccount create(
+      Account.Id id,
+      @Nullable String username,
+      @Nullable String email,
+      @Nullable String fullName,
+      @Nullable String httpPassword) {
+    return new AutoValue_TestAccount(id, username, email, fullName, httpPassword);
   }
 
-  public PersonIdent getIdent() {
-    return new PersonIdent(fullName, email);
+  public abstract Account.Id id();
+
+  @Nullable
+  public abstract String username();
+
+  @Nullable
+  public abstract String email();
+
+  @Nullable
+  public abstract String fullName();
+
+  @Nullable
+  public abstract String httpPassword();
+
+  public PersonIdent newIdent() {
+    return new PersonIdent(fullName(), email());
   }
 
   public String getHttpUrl(GerritServer server) {
     InetSocketAddress addr = server.getHttpAddress();
     return new URIBuilder()
         .setScheme("http")
-        .setUserInfo(username, httpPassword)
+        .setUserInfo(username(), httpPassword())
         .setHost(InetAddresses.toUriString(addr.getAddress()))
         .setPort(addr.getPort())
         .toString();
   }
 
-  public Account.Id getId() {
-    return id;
-  }
-
-  @Override
-  public String toString() {
-    return MoreObjects.toStringHelper(this).add("id", id).add("username", username).toString();
+  public Address getEmailAddress() {
+    // Address is weird enough that it's safer and clearer to create a new instance in a
+    // non-abstract method rather than, say, having an abstract emailAddress() as part of this
+    // AutoValue class. Specifically:
+    //  * Email is not specified as @Nullable in Address, but it is nullable in this class. If this
+    //    is a problem, at least it's a problem only for users of TestAccount that actually call
+    //    emailAddress().
+    //  * Address#equals only considers email, not name, whereas TestAccount#equals should include
+    //    name.
+    return new Address(fullName(), email());
   }
 }
diff --git a/java/com/google/gerrit/acceptance/testsuite/ThrowingConsumer.java b/java/com/google/gerrit/acceptance/testsuite/ThrowingConsumer.java
index 5efdc81..8efb6ae 100644
--- a/java/com/google/gerrit/acceptance/testsuite/ThrowingConsumer.java
+++ b/java/com/google/gerrit/acceptance/testsuite/ThrowingConsumer.java
@@ -17,4 +17,12 @@
 @FunctionalInterface
 public interface ThrowingConsumer<T> {
   void accept(T t) throws Exception;
+
+  default void acceptAndThrowSilently(T t) {
+    try {
+      accept(t);
+    } catch (Exception e) {
+      throw new IllegalStateException(e);
+    }
+  }
 }
diff --git a/java/com/google/gerrit/acceptance/testsuite/ThrowingFunction.java b/java/com/google/gerrit/acceptance/testsuite/ThrowingFunction.java
index d41672a..2337331 100644
--- a/java/com/google/gerrit/acceptance/testsuite/ThrowingFunction.java
+++ b/java/com/google/gerrit/acceptance/testsuite/ThrowingFunction.java
@@ -18,4 +18,12 @@
 public interface ThrowingFunction<T, R> {
 
   R apply(T value) throws Exception;
+
+  default R applyAndThrowSilently(T t) {
+    try {
+      return apply(t);
+    } catch (Exception e) {
+      throw new IllegalStateException(e);
+    }
+  }
 }
diff --git a/java/com/google/gerrit/acceptance/testsuite/account/AccountOperations.java b/java/com/google/gerrit/acceptance/testsuite/account/AccountOperations.java
index 61b7599..61b828e 100644
--- a/java/com/google/gerrit/acceptance/testsuite/account/AccountOperations.java
+++ b/java/com/google/gerrit/acceptance/testsuite/account/AccountOperations.java
@@ -28,11 +28,11 @@
 
   /**
    * Starts the fluent chain for a querying or modifying an account. Please see the methods of
-   * {@link MoreAccountOperations} for details on possible operations.
+   * {@link PerAccountOperations} for details on possible operations.
    *
    * @return an aggregation of operations on a specific account
    */
-  MoreAccountOperations account(Account.Id accountId);
+  PerAccountOperations account(Account.Id accountId);
 
   /**
    * Starts the fluent chain to create an account. The returned builder can be used to specify the
@@ -58,14 +58,14 @@
   TestAccountCreation.Builder newAccount();
 
   /** An aggregation of methods on a specific account. */
-  interface MoreAccountOperations {
+  interface PerAccountOperations {
 
     /**
      * Checks whether the account exists.
      *
      * @return {@code true} if the account exists
      */
-    boolean exists() throws Exception;
+    boolean exists();
 
     /**
      * Retrieves the account.
@@ -76,7 +76,7 @@
      *
      * @return the corresponding {@code TestAccount}
      */
-    TestAccount get() throws Exception;
+    TestAccount get();
 
     /**
      * Starts the fluent chain to update an account. The returned builder can be used to specify how
diff --git a/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java
index ebbcfe4..7641e47 100644
--- a/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java
@@ -17,14 +17,13 @@
 import static com.google.common.base.Preconditions.checkState;
 
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.Accounts;
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.InternalAccountUpdate;
 import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.server.notedb.Sequences;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.Optional;
@@ -50,8 +49,8 @@
   }
 
   @Override
-  public MoreAccountOperations account(Account.Id accountId) {
-    return new MoreAccountOperationsImpl(accountId);
+  public PerAccountOperations account(Account.Id accountId) {
+    return new PerAccountOperationsImpl(accountId);
   }
 
   @Override
@@ -68,7 +67,7 @@
   }
 
   private AccountState createAccount(AccountsUpdate.AccountUpdater accountUpdater)
-      throws OrmException, IOException, ConfigInvalidException {
+      throws IOException, ConfigInvalidException {
     Account.Id accountId = new Account.Id(seq.nextAccountId());
     return accountsUpdate.insert("Create Test Account", accountId, accountUpdater);
   }
@@ -100,28 +99,35 @@
     return builder.addExternalId(ExternalId.createUsername(username, accountId, httpPassword));
   }
 
-  private class MoreAccountOperationsImpl implements MoreAccountOperations {
+  private class PerAccountOperationsImpl implements PerAccountOperations {
     private final Account.Id accountId;
 
-    MoreAccountOperationsImpl(Account.Id accountId) {
+    PerAccountOperationsImpl(Account.Id accountId) {
       this.accountId = accountId;
     }
 
     @Override
-    public boolean exists() throws Exception {
-      return accounts.get(accountId).isPresent();
+    public boolean exists() {
+      return getAccountState(accountId).isPresent();
     }
 
     @Override
-    public TestAccount get() throws Exception {
+    public TestAccount get() {
       AccountState account =
-          accounts
-              .get(accountId)
+          getAccountState(accountId)
               .orElseThrow(
                   () -> new IllegalStateException("Tried to get non-existing test account"));
       return toTestAccount(account);
     }
 
+    private Optional<AccountState> getAccountState(Account.Id accountId) {
+      try {
+        return accounts.get(accountId);
+      } catch (IOException | ConfigInvalidException e) {
+        throw new IllegalStateException(e);
+      }
+    }
+
     private TestAccount toTestAccount(AccountState accountState) {
       Account account = accountState.getAccount();
       return TestAccount.builder()
@@ -139,7 +145,7 @@
     }
 
     private void updateAccount(TestAccountUpdate accountUpdate)
-        throws OrmException, IOException, ConfigInvalidException {
+        throws IOException, ConfigInvalidException {
       AccountsUpdate.AccountUpdater accountUpdater =
           (account, updateBuilder) -> fillBuilder(updateBuilder, accountUpdate, accountId);
       Optional<AccountState> updatedAccount = updateAccount(accountUpdater);
@@ -147,7 +153,7 @@
     }
 
     private Optional<AccountState> updateAccount(AccountsUpdate.AccountUpdater accountUpdater)
-        throws OrmException, IOException, ConfigInvalidException {
+        throws IOException, ConfigInvalidException {
       return accountsUpdate.update("Update Test Account", accountId, accountUpdater);
     }
 
diff --git a/java/com/google/gerrit/acceptance/testsuite/account/TestAccountCreation.java b/java/com/google/gerrit/acceptance/testsuite/account/TestAccountCreation.java
index ab32409..f2414e0 100644
--- a/java/com/google/gerrit/acceptance/testsuite/account/TestAccountCreation.java
+++ b/java/com/google/gerrit/acceptance/testsuite/account/TestAccountCreation.java
@@ -88,9 +88,9 @@
 
     abstract TestAccountCreation autoBuild();
 
-    public Account.Id create() throws Exception {
+    public Account.Id create() {
       TestAccountCreation accountUpdate = autoBuild();
-      return accountUpdate.accountCreator().apply(accountUpdate);
+      return accountUpdate.accountCreator().applyAndThrowSilently(accountUpdate);
     }
   }
 }
diff --git a/java/com/google/gerrit/acceptance/testsuite/account/TestAccountUpdate.java b/java/com/google/gerrit/acceptance/testsuite/account/TestAccountUpdate.java
index 251f452..da599e7 100644
--- a/java/com/google/gerrit/acceptance/testsuite/account/TestAccountUpdate.java
+++ b/java/com/google/gerrit/acceptance/testsuite/account/TestAccountUpdate.java
@@ -86,9 +86,9 @@
 
     abstract TestAccountUpdate autoBuild();
 
-    public void update() throws Exception {
+    public void update() {
       TestAccountUpdate accountUpdate = autoBuild();
-      accountUpdate.accountUpdater().accept(accountUpdate);
+      accountUpdate.accountUpdater().acceptAndThrowSilently(accountUpdate);
     }
   }
 }
diff --git a/java/com/google/gerrit/acceptance/testsuite/account/TestSshKeys.java b/java/com/google/gerrit/acceptance/testsuite/account/TestSshKeys.java
index 0cb5cf3..4847fdb 100644
--- a/java/com/google/gerrit/acceptance/testsuite/account/TestSshKeys.java
+++ b/java/com/google/gerrit/acceptance/testsuite/account/TestSshKeys.java
@@ -55,14 +55,14 @@
   public KeyPair getKeyPair(com.google.gerrit.acceptance.TestAccount account) throws Exception {
     checkState(sshEnabled, "Requested SSH key pair, but SSH is disabled");
     checkState(
-        account.username != null,
+        account.username() != null,
         "Requested SSH key pair for account %s, but username is not set",
-        account.id);
+        account.id());
 
-    String username = account.username;
+    String username = account.username();
     KeyPair keyPair = sshKeyPairs.get(username);
     if (keyPair == null) {
-      keyPair = createKeyPair(account.id, username, account.email);
+      keyPair = createKeyPair(account.id(), username, account.email());
       sshKeyPairs.put(username, keyPair);
     }
     return keyPair;
diff --git a/java/com/google/gerrit/acceptance/testsuite/group/GroupOperations.java b/java/com/google/gerrit/acceptance/testsuite/group/GroupOperations.java
index f75ca2e..533d06b 100644
--- a/java/com/google/gerrit/acceptance/testsuite/group/GroupOperations.java
+++ b/java/com/google/gerrit/acceptance/testsuite/group/GroupOperations.java
@@ -27,11 +27,11 @@
 public interface GroupOperations {
   /**
    * Starts the fluent chain for querying or modifying a group. Please see the methods of {@link
-   * MoreGroupOperations} for details on possible operations.
+   * PerGroupOperations} for details on possible operations.
    *
    * @return an aggregation of operations on a specific group
    */
-  MoreGroupOperations group(AccountGroup.UUID groupUuid);
+  PerGroupOperations group(AccountGroup.UUID groupUuid);
 
   /**
    * Starts the fluent chain to create a group. The returned builder can be used to specify the
@@ -56,14 +56,14 @@
   TestGroupCreation.Builder newGroup();
 
   /** An aggregation of methods on a specific group. */
-  interface MoreGroupOperations {
+  interface PerGroupOperations {
 
     /**
      * Checks whether the group exists.
      *
      * @return {@code true} if the group exists
      */
-    boolean exists() throws Exception;
+    boolean exists();
 
     /**
      * Retrieves the group.
@@ -74,7 +74,7 @@
      *
      * @return the corresponding {@code TestGroup}
      */
-    TestGroup get() throws Exception;
+    TestGroup get();
 
     /**
      * Starts the fluent chain to update a group. The returned builder can be used to specify how
diff --git a/java/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImpl.java
index f9769c5..e0ddee5 100644
--- a/java/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImpl.java
@@ -16,10 +16,10 @@
 
 import static com.google.common.base.Preconditions.checkState;
 
-import com.google.gerrit.common.errors.NoSuchGroupException;
+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.Sequences;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.GroupUUID;
 import com.google.gerrit.server.group.InternalGroup;
@@ -27,8 +27,7 @@
 import com.google.gerrit.server.group.db.GroupsUpdate;
 import com.google.gerrit.server.group.db.InternalGroupCreation;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.server.notedb.Sequences;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.Optional;
@@ -60,8 +59,8 @@
   }
 
   @Override
-  public MoreGroupOperations group(AccountGroup.UUID groupUuid) {
-    return new MoreGroupOperationsImpl(groupUuid);
+  public PerGroupOperations group(AccountGroup.UUID groupUuid) {
+    return new PerGroupOperationsImpl(groupUuid);
   }
 
   @Override
@@ -70,7 +69,7 @@
   }
 
   private AccountGroup.UUID createNewGroup(TestGroupCreation groupCreation)
-      throws ConfigInvalidException, IOException, OrmException {
+      throws ConfigInvalidException, IOException {
     InternalGroupCreation internalGroupCreation = toInternalGroupCreation(groupCreation);
     InternalGroupUpdate internalGroupUpdate = toInternalGroupUpdate(groupCreation);
     InternalGroup internalGroup =
@@ -78,8 +77,7 @@
     return internalGroup.getGroupUUID();
   }
 
-  private InternalGroupCreation toInternalGroupCreation(TestGroupCreation groupCreation)
-      throws OrmException {
+  private InternalGroupCreation toInternalGroupCreation(TestGroupCreation groupCreation) {
     AccountGroup.Id groupId = new AccountGroup.Id(seq.nextGroupId());
     String groupName = groupCreation.name().orElse("group-with-id-" + groupId.get());
     AccountGroup.UUID groupUuid = GroupUUID.make(groupName, serverIdent);
@@ -101,25 +99,33 @@
     return builder.build();
   }
 
-  private class MoreGroupOperationsImpl implements MoreGroupOperations {
+  private class PerGroupOperationsImpl implements PerGroupOperations {
     private final AccountGroup.UUID groupUuid;
 
-    MoreGroupOperationsImpl(AccountGroup.UUID groupUuid) {
+    PerGroupOperationsImpl(AccountGroup.UUID groupUuid) {
       this.groupUuid = groupUuid;
     }
 
     @Override
-    public boolean exists() throws Exception {
-      return groups.getGroup(groupUuid).isPresent();
+    public boolean exists() {
+      return getGroup(groupUuid).isPresent();
     }
 
     @Override
-    public TestGroup get() throws Exception {
-      Optional<InternalGroup> group = groups.getGroup(groupUuid);
+    public TestGroup get() {
+      Optional<InternalGroup> group = getGroup(groupUuid);
       checkState(group.isPresent(), "Tried to get non-existing test group");
       return toTestGroup(group.get());
     }
 
+    private Optional<InternalGroup> getGroup(AccountGroup.UUID groupUuid) {
+      try {
+        return groups.getGroup(groupUuid);
+      } catch (IOException | ConfigInvalidException e) {
+        throw new IllegalStateException(e);
+      }
+    }
+
     private TestGroup toTestGroup(InternalGroup internalGroup) {
       return TestGroup.builder()
           .groupUuid(internalGroup.getGroupUUID())
@@ -140,7 +146,7 @@
     }
 
     private void updateGroup(TestGroupUpdate groupUpdate)
-        throws OrmDuplicateKeyException, NoSuchGroupException, ConfigInvalidException, IOException {
+        throws DuplicateKeyException, NoSuchGroupException, ConfigInvalidException, IOException {
       InternalGroupUpdate internalGroupUpdate = toInternalGroupUpdate(groupUpdate);
       groupsUpdate.updateGroup(groupUuid, internalGroupUpdate);
     }
diff --git a/java/com/google/gerrit/acceptance/testsuite/group/TestGroupCreation.java b/java/com/google/gerrit/acceptance/testsuite/group/TestGroupCreation.java
index efed720..612ce2a 100644
--- a/java/com/google/gerrit/acceptance/testsuite/group/TestGroupCreation.java
+++ b/java/com/google/gerrit/acceptance/testsuite/group/TestGroupCreation.java
@@ -104,9 +104,9 @@
      *
      * @return the UUID of the created group
      */
-    public AccountGroup.UUID create() throws Exception {
+    public AccountGroup.UUID create() {
       TestGroupCreation groupCreation = autoBuild();
-      return groupCreation.groupCreator().apply(groupCreation);
+      return groupCreation.groupCreator().applyAndThrowSilently(groupCreation);
     }
   }
 }
diff --git a/java/com/google/gerrit/acceptance/testsuite/group/TestGroupUpdate.java b/java/com/google/gerrit/acceptance/testsuite/group/TestGroupUpdate.java
index 095a270..bc9d569 100644
--- a/java/com/google/gerrit/acceptance/testsuite/group/TestGroupUpdate.java
+++ b/java/com/google/gerrit/acceptance/testsuite/group/TestGroupUpdate.java
@@ -126,9 +126,9 @@
     abstract TestGroupUpdate autoBuild();
 
     /** Executes the group update as specified. */
-    public void update() throws Exception {
+    public void update() {
       TestGroupUpdate groupUpdater = autoBuild();
-      groupUpdater.groupUpdater().accept(groupUpdater);
+      groupUpdater.groupUpdater().acceptAndThrowSilently(groupUpdater);
     }
   }
 }
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperations.java b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperations.java
new file mode 100644
index 0000000..029d161
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperations.java
@@ -0,0 +1,44 @@
+// 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.testsuite.project;
+
+import com.google.gerrit.reviewdb.client.Project;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+/**
+ * Operations for constructing projects in tests. This does not necessarily use the project REST
+ * API, so don't use it for testing that.
+ */
+public interface ProjectOperations {
+
+  /** Starts a fluent chain for creating a new project. */
+  TestProjectCreation.Builder newProject();
+
+  PerProjectOperations project(Project.NameKey key);
+
+  interface PerProjectOperations {
+    /**
+     * Returns the commit for this project. branchName can either be shortened ("HEAD", "master") or
+     * a fully qualified refname ("refs/heads/master"). The branch must exist.
+     */
+    RevCommit getHead(String branchName);
+
+    /**
+     * Returns true if a branch exists. branchName can either be shortened ("HEAD", "master") or a
+     * fully qualified refname ("refs/heads/master").
+     */
+    boolean hasHead(String branchName);
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
new file mode 100644
index 0000000..28be3f3
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
@@ -0,0 +1,101 @@
+// 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.testsuite.project;
+
+import com.google.common.base.Preconditions;
+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.server.git.GitRepositoryManager;
+import com.google.gerrit.server.project.CreateProjectArgs;
+import com.google.gerrit.server.project.ProjectCreator;
+import com.google.inject.Inject;
+import java.util.ArrayList;
+import java.util.Collections;
+import org.apache.commons.lang.RandomStringUtils;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+public class ProjectOperationsImpl implements ProjectOperations {
+  private final ProjectCreator projectCreator;
+  private final GitRepositoryManager repoManager;
+
+  @Inject
+  ProjectOperationsImpl(GitRepositoryManager repoManager, ProjectCreator projectCreator) {
+    this.repoManager = repoManager;
+    this.projectCreator = projectCreator;
+  }
+
+  @Override
+  public Builder newProject() {
+    return TestProjectCreation.builder(this::createNewProject);
+  }
+
+  private Project.NameKey createNewProject(TestProjectCreation projectCreation) throws Exception {
+    String name = projectCreation.name().orElse(RandomStringUtils.randomAlphabetic(8));
+
+    CreateProjectArgs args = new CreateProjectArgs();
+    args.setProjectName(name);
+    args.branch = Collections.singletonList(Constants.R_HEADS + Constants.MASTER);
+    args.createEmptyCommit = projectCreation.createEmptyCommit().orElse(true);
+    projectCreation.parent().ifPresent(p -> args.newParent = p);
+    // ProjectCreator wants non-null owner IDs.
+    args.ownerIds = new ArrayList<>();
+    projectCreation.submitType().ifPresent(st -> args.submitType = st);
+    projectCreator.createProject(args);
+    return new Project.NameKey(name);
+  }
+
+  @Override
+  public ProjectOperations.PerProjectOperations project(Project.NameKey key) {
+    return new PerProjectOperations(key);
+  }
+
+  private class PerProjectOperations implements ProjectOperations.PerProjectOperations {
+
+    Project.NameKey nameKey;
+
+    PerProjectOperations(Project.NameKey nameKey) {
+      this.nameKey = nameKey;
+    }
+
+    @Override
+    public RevCommit getHead(String branch) {
+      return Preconditions.checkNotNull(headOrNull(branch));
+    }
+
+    @Override
+    public boolean hasHead(String branch) {
+      return headOrNull(branch) != null;
+    }
+
+    private RevCommit headOrNull(String branch) {
+      if (!branch.startsWith(Constants.R_REFS)) {
+        branch = RefNames.REFS_HEADS + branch;
+      }
+
+      try (Repository repo = repoManager.openRepository(nameKey);
+          RevWalk rw = new RevWalk(repo)) {
+        Ref r = repo.exactRef(branch);
+        return r == null ? null : rw.parseCommit(r.getObjectId());
+      } catch (Exception e) {
+        throw new IllegalStateException(e);
+      }
+    }
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/TestProjectCreation.java b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectCreation.java
new file mode 100644
index 0000000..31af1d2
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectCreation.java
@@ -0,0 +1,71 @@
+// 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.testsuite.project;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.acceptance.testsuite.ThrowingFunction;
+import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.reviewdb.client.Project;
+import java.util.Optional;
+
+@AutoValue
+public abstract class TestProjectCreation {
+
+  public abstract Optional<String> name();
+
+  public abstract Optional<Project.NameKey> parent();
+
+  public abstract Optional<Boolean> createEmptyCommit();
+
+  public abstract Optional<SubmitType> submitType();
+
+  abstract ThrowingFunction<TestProjectCreation, Project.NameKey> projectCreator();
+
+  public static Builder builder(
+      ThrowingFunction<TestProjectCreation, Project.NameKey> projectCreator) {
+    return new AutoValue_TestProjectCreation.Builder().projectCreator(projectCreator);
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+    public abstract TestProjectCreation.Builder name(String name);
+
+    public abstract TestProjectCreation.Builder parent(Project.NameKey parent);
+
+    public abstract TestProjectCreation.Builder submitType(SubmitType submitType);
+
+    public abstract TestProjectCreation.Builder createEmptyCommit(boolean value);
+
+    /** Skips the empty commit on creation. This means that project's branches will not exist. */
+    public TestProjectCreation.Builder noEmptyCommit() {
+      return createEmptyCommit(false);
+    }
+
+    abstract TestProjectCreation.Builder projectCreator(
+        ThrowingFunction<TestProjectCreation, Project.NameKey> projectCreator);
+
+    abstract TestProjectCreation autoBuild();
+
+    /**
+     * Executes the project creation as specified.
+     *
+     * @return the name of the created project
+     */
+    public Project.NameKey create() {
+      TestProjectCreation creation = autoBuild();
+      return creation.projectCreator().applyAndThrowSilently(creation);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperations.java b/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperations.java
new file mode 100644
index 0000000..17d9294
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperations.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.acceptance.testsuite.request;
+
+import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
+import com.google.gerrit.acceptance.testsuite.account.TestAccount;
+import com.google.gerrit.reviewdb.client.Account;
+
+/**
+ * An aggregation of operations on Guice request scopes for test purposes.
+ *
+ * <p>To execute the operations, no Gerrit permissions are necessary.
+ */
+public interface RequestScopeOperations {
+  /**
+   * Sets the Guice request scope to the given account.
+   *
+   * <p>The resulting context has an SSH session attached. In order to use the SSH session returned
+   * by {@link com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context#getSession()}, SSH
+   * must be enabled in the test and the account must have a username set. However, these are not
+   * requirements simply to call this method.
+   *
+   * @param accountId account ID. Must exist; throws an unchecked exception otherwise.
+   * @return the previous request scope.
+   */
+  AcceptanceTestRequestScope.Context setApiUser(Account.Id accountId);
+
+  /**
+   * Sets the Guice request scope to the given account.
+   *
+   * <p>The resulting context has an SSH session attached. In order to use the SSH session returned
+   * by {@link com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context#getSession()}, SSH
+   * must be enabled in the test and the account must have a username set. However, these are not
+   * requirements simply to call this method.
+   *
+   * @param testAccount test account from {@code AccountOperations}.
+   * @return the previous request scope.
+   */
+  AcceptanceTestRequestScope.Context setApiUser(TestAccount testAccount);
+
+  /**
+   * Enforces a new request context for the current API user.
+   *
+   * <p>This recreates the {@code IdentifiedUser}, hence everything which is cached in the {@code
+   * IdentifiedUser} is reloaded (e.g. the email addresses of the user).
+   *
+   * <p>The current user must be an identified user.
+   *
+   * @return the previous request scope.
+   */
+  AcceptanceTestRequestScope.Context resetCurrentApiUser();
+
+  /**
+   * Sets the Guice request scope to the anonymous user.
+   *
+   * @return the previous request scope.
+   */
+  AcceptanceTestRequestScope.Context setApiUserAnonymous();
+
+  /**
+   * Sets the Guice request scope to the internal server user.
+   *
+   * @return the previous request scope.
+   */
+  AcceptanceTestRequestScope.Context setApiUserInternal();
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java
new file mode 100644
index 0000000..5546422
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java
@@ -0,0 +1,114 @@
+// 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.request;
+
+import static com.google.common.base.Preconditions.checkState;
+import static java.util.Objects.requireNonNull;
+
+import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
+import com.google.gerrit.acceptance.GerritServer.TestSshServerAddress;
+import com.google.gerrit.acceptance.SshSession;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+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.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.IdentifiedUser.GenericFactory;
+import com.google.gerrit.server.InternalUser;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.net.InetSocketAddress;
+
+/**
+ * The implementation of {@code RequestScopeOperations}.
+ *
+ * <p>There is only one implementation of {@code RequestScopeOperations}. Nevertheless, we keep the
+ * separation between interface and implementation to enhance clarity.
+ */
+@Singleton
+public class RequestScopeOperationsImpl implements RequestScopeOperations {
+  private final AcceptanceTestRequestScope atrScope;
+  private final AccountCache accountCache;
+  private final AccountOperations accountOperations;
+  private final IdentifiedUser.GenericFactory userFactory;
+  private final Provider<AnonymousUser> anonymousUserProvider;
+  private final InternalUser.Factory internalUserFactory;
+  private final InetSocketAddress sshAddress;
+  private final TestSshKeys testSshKeys;
+
+  @Inject
+  RequestScopeOperationsImpl(
+      AcceptanceTestRequestScope atrScope,
+      AccountCache accountCache,
+      AccountOperations accountOperations,
+      GenericFactory userFactory,
+      Provider<AnonymousUser> anonymousUserProvider,
+      InternalUser.Factory internalUserFactory,
+      @Nullable @TestSshServerAddress InetSocketAddress sshAddress,
+      TestSshKeys testSshKeys) {
+    this.atrScope = atrScope;
+    this.accountCache = accountCache;
+    this.accountOperations = accountOperations;
+    this.userFactory = userFactory;
+    this.anonymousUserProvider = anonymousUserProvider;
+    this.internalUserFactory = internalUserFactory;
+    this.sshAddress = sshAddress;
+    this.testSshKeys = testSshKeys;
+  }
+
+  @Override
+  public AcceptanceTestRequestScope.Context setApiUser(Account.Id accountId) {
+    return setApiUser(accountOperations.account(accountId).get());
+  }
+
+  @Override
+  public AcceptanceTestRequestScope.Context setApiUser(TestAccount testAccount) {
+    return atrScope.set(
+        atrScope.newContext(
+            new SshSession(testSshKeys, sshAddress, testAccount),
+            createIdentifiedUser(testAccount.accountId())));
+  }
+
+  @Override
+  public AcceptanceTestRequestScope.Context resetCurrentApiUser() {
+    CurrentUser user = atrScope.get().getUser();
+    // More special cases for anonymous users etc. can be added as needed.
+    checkState(user.isIdentifiedUser(), "can only reset IdentifiedUser, not %s", user);
+    return setApiUser(user.getAccountId());
+  }
+
+  @Override
+  public AcceptanceTestRequestScope.Context setApiUserAnonymous() {
+    return atrScope.set(atrScope.newContext(null, anonymousUserProvider.get()));
+  }
+
+  @Override
+  public AcceptanceTestRequestScope.Context setApiUserInternal() {
+    return atrScope.set(atrScope.newContext(null, internalUserFactory.create()));
+  }
+
+  private IdentifiedUser createIdentifiedUser(Account.Id accountId) {
+    return userFactory.create(
+        accountCache
+            .get(requireNonNull(accountId))
+            .orElseThrow(
+                () -> new IllegalArgumentException("account does not exist: " + accountId)));
+  }
+}
diff --git a/java/com/google/gerrit/asciidoctor/AsciiDoctor.java b/java/com/google/gerrit/asciidoctor/AsciiDoctor.java
index 5779070..9d0a28e 100644
--- a/java/com/google/gerrit/asciidoctor/AsciiDoctor.java
+++ b/java/com/google/gerrit/asciidoctor/AsciiDoctor.java
@@ -19,7 +19,6 @@
 import com.google.common.io.ByteStreams;
 import java.io.BufferedReader;
 import java.io.File;
-import java.io.FilenameFilter;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.file.Files;
@@ -127,7 +126,7 @@
       int equalsIndex = attribute.indexOf('=');
       if (equalsIndex > -1) {
         String name = attribute.substring(0, equalsIndex);
-        String value = attribute.substring(equalsIndex + 1, attribute.length());
+        String value = attribute.substring(equalsIndex + 1);
 
         attributeValues.put(name, value);
       } else {
@@ -168,14 +167,7 @@
       try (ZipOutputStream zip = new ZipOutputStream(Files.newOutputStream(Paths.get(zipFile)))) {
         renderFiles(inputFiles, zip);
 
-        File[] cssFiles =
-            tmpdir.listFiles(
-                new FilenameFilter() {
-                  @Override
-                  public boolean accept(File dir, String name) {
-                    return name.endsWith(".css");
-                  }
-                });
+        File[] cssFiles = tmpdir.listFiles((dir, name) -> name.endsWith(".css"));
         for (File css : cssFiles) {
           zipFile(css, css.getName(), zip);
         }
diff --git a/java/com/google/gerrit/common/BUILD b/java/com/google/gerrit/common/BUILD
index 8055ea7..717f585 100644
--- a/java/com/google/gerrit/common/BUILD
+++ b/java/com/google/gerrit/common/BUILD
@@ -1,34 +1,15 @@
 load("@rules_java//java:defs.bzl", "java_library")
-load("//tools/bzl:gwt.bzl", "gwt_module")
 
 ANNOTATIONS = [
     "Nullable.java",
-    "audit/Audit.java",
-    "auth/SignInRequired.java",
+    "UsedAt.java",
 ]
 
 java_library(
     name = "annotations",
     srcs = ANNOTATIONS,
     visibility = ["//visibility:public"],
-)
-
-gwt_module(
-    name = "client",
-    srcs = glob(["**/*.java"]),
-    exported_deps = [
-        "//java/com/google/gerrit/extensions:api",
-        "//java/com/google/gerrit/prettify:client",
-        "//lib:guava",
-        "//lib:gwtorm-client",
-        "//lib:servlet-api-3_1",
-        "//lib/auto:auto-value",
-        "//lib/auto:auto-value-annotations",
-        "//lib/flogger:api",
-        "//lib/jgit/org.eclipse.jgit:jgit",
-    ],
-    gwt_xml = "Common.gwt.xml",
-    visibility = ["//visibility:public"],
+    deps = ["//lib:guava"],
 )
 
 java_library(
@@ -43,10 +24,8 @@
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/prettify:server",
         "//java/com/google/gerrit/reviewdb:server",
-        "//java/org/eclipse/jgit:server",
+        "//java/com/google/gwtorm",
         "//lib:guava",
-        "//lib:gwtjsonrpc",
-        "//lib:gwtorm",
         "//lib:servlet-api-3_1",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
diff --git a/java/com/google/gerrit/common/Common.gwt.xml b/java/com/google/gerrit/common/Common.gwt.xml
deleted file mode 100644
index 56bbb84..0000000
--- a/java/com/google/gerrit/common/Common.gwt.xml
+++ /dev/null
@@ -1,24 +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.
--->
-<module>
-  <inherits name='com.google.gerrit.reviewdb.ReviewDB' />
-  <inherits name='com.google.gwtjsonrpc.GWTJSONRPC'/>
-  <inherits name="com.google.gwt.logging.Logging"/>
-  <source path="">
-    <exclude name='**/testing/**/*.java'/>
-    <include name='**/*.java'/>
-  </source>
-</module>
diff --git a/java/com/google/gerrit/common/FileUtil.java b/java/com/google/gerrit/common/FileUtil.java
index 24e3808..04288bc 100644
--- a/java/com/google/gerrit/common/FileUtil.java
+++ b/java/com/google/gerrit/common/FileUtil.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.common;
 
-import com.google.common.annotations.GwtIncompatible;
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
@@ -25,7 +24,6 @@
 import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.eclipse.jgit.util.IO;
 
-@GwtIncompatible("Unemulated classes in java.io, java.nio and JGit")
 public class FileUtil {
   public static boolean modified(FileBasedConfig cfg) throws IOException {
     byte[] curVers;
diff --git a/java/com/google/gerrit/common/FooterConstants.java b/java/com/google/gerrit/common/FooterConstants.java
index d76c92b..3ec809c 100644
--- a/java/com/google/gerrit/common/FooterConstants.java
+++ b/java/com/google/gerrit/common/FooterConstants.java
@@ -14,10 +14,8 @@
 
 package com.google.gerrit.common;
 
-import com.google.common.annotations.GwtIncompatible;
 import org.eclipse.jgit.revwalk.FooterKey;
 
-@GwtIncompatible("Unemulated com.google.gerrit.common.FooterConstants")
 public class FooterConstants {
   /** The change ID as used to track patch sets. */
   public static final FooterKey CHANGE_ID = new FooterKey("Change-Id");
diff --git a/java/com/google/gerrit/common/FormatUtil.java b/java/com/google/gerrit/common/FormatUtil.java
deleted file mode 100644
index 0f6b37a..0000000
--- a/java/com/google/gerrit/common/FormatUtil.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common;
-
-public class FormatUtil {
-  public static String elide(String s, int max) {
-    if (s == null || s.length() <= max) {
-      return s;
-    }
-    int len = (max - 3) / 2;
-    return s.substring(0, len) + "..." + s.substring(s.length() - len);
-  }
-}
diff --git a/java/com/google/gerrit/common/IoUtil.java b/java/com/google/gerrit/common/IoUtil.java
index 526e88b..37f6c2c 100644
--- a/java/com/google/gerrit/common/IoUtil.java
+++ b/java/com/google/gerrit/common/IoUtil.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.common;
 
-import com.google.common.annotations.GwtIncompatible;
 import com.google.common.collect.Sets;
 import java.io.IOException;
 import java.io.InputStream;
@@ -30,7 +29,6 @@
 import java.util.Collections;
 import java.util.Set;
 
-@GwtIncompatible("Unemulated methods in Class and OutputStream")
 public final class IoUtil {
   public static void copyWithThread(InputStream src, OutputStream dst) {
     new Thread("IoUtil-Copy") {
diff --git a/java/com/google/gerrit/common/PluginData.java b/java/com/google/gerrit/common/PluginData.java
index b14543d..c440de1 100644
--- a/java/com/google/gerrit/common/PluginData.java
+++ b/java/com/google/gerrit/common/PluginData.java
@@ -14,11 +14,9 @@
 
 package com.google.gerrit.common;
 
-import com.google.common.annotations.GwtIncompatible;
 import java.nio.file.Path;
 import java.util.Objects;
 
-@GwtIncompatible("Unemulated java.nio.file.Path")
 public class PluginData {
   public final String name;
   public final String version;
diff --git a/java/com/google/gerrit/common/ProjectAccessUtil.java b/java/com/google/gerrit/common/ProjectAccessUtil.java
deleted file mode 100644
index 0369bfe..0000000
--- a/java/com/google/gerrit/common/ProjectAccessUtil.java
+++ /dev/null
@@ -1,66 +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.common;
-
-import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.Permission;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-public class ProjectAccessUtil {
-  public static List<AccessSection> mergeSections(List<AccessSection> src) {
-    Map<String, AccessSection> map = new LinkedHashMap<>();
-    for (AccessSection section : src) {
-      if (section.getPermissions().isEmpty()) {
-        continue;
-      }
-
-      final AccessSection prior = map.get(section.getName());
-      if (prior != null) {
-        prior.mergeFrom(section);
-      } else {
-        map.put(section.getName(), section);
-      }
-    }
-    return new ArrayList<>(map.values());
-  }
-
-  public static List<AccessSection> removeEmptyPermissionsAndSections(
-      final List<AccessSection> src) {
-    final Set<AccessSection> sectionsToRemove = new HashSet<>();
-    for (AccessSection section : src) {
-      final Set<Permission> permissionsToRemove = new HashSet<>();
-      for (Permission permission : section.getPermissions()) {
-        if (permission.getRules().isEmpty()) {
-          permissionsToRemove.add(permission);
-        }
-      }
-      for (Permission permissionToRemove : permissionsToRemove) {
-        section.remove(permissionToRemove);
-      }
-      if (section.getPermissions().isEmpty()) {
-        sectionsToRemove.add(section);
-      }
-    }
-    for (AccessSection sectionToRemove : sectionsToRemove) {
-      src.remove(sectionToRemove);
-    }
-    return src;
-  }
-}
diff --git a/java/com/google/gerrit/common/ProjectUtil.java b/java/com/google/gerrit/common/ProjectUtil.java
deleted file mode 100644
index e9d3781..0000000
--- a/java/com/google/gerrit/common/ProjectUtil.java
+++ /dev/null
@@ -1,43 +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.common;
-
-public class ProjectUtil {
-  public static String sanitizeProjectName(String name) {
-    name = stripGitSuffix(name);
-    name = stripTrailingSlash(name);
-    return name;
-  }
-
-  public static String stripGitSuffix(String name) {
-    if (name.endsWith(".git")) {
-      // Be nice and drop the trailing ".git" suffix, which we never keep
-      // in our database, but clients might mistakenly provide anyway.
-      //
-      name = name.substring(0, name.length() - 4);
-      name = stripTrailingSlash(name);
-    }
-    return name;
-  }
-
-  private static String stripTrailingSlash(String name) {
-    while (name.endsWith("/")) {
-      name = name.substring(0, name.length() - 1);
-    }
-    return name;
-  }
-
-  private ProjectUtil() {}
-}
diff --git a/java/com/google/gerrit/common/RawInputUtil.java b/java/com/google/gerrit/common/RawInputUtil.java
index e102eab..4a676e6 100644
--- a/java/com/google/gerrit/common/RawInputUtil.java
+++ b/java/com/google/gerrit/common/RawInputUtil.java
@@ -18,14 +18,12 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Objects.requireNonNull;
 
-import com.google.common.annotations.GwtIncompatible;
 import com.google.gerrit.extensions.restapi.RawInput;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import javax.servlet.http.HttpServletRequest;
 
-@GwtIncompatible("Unemulated classes in java.io and javax.servlet")
 public class RawInputUtil {
   public static RawInput create(String content) {
     return create(content.getBytes(UTF_8));
diff --git a/java/com/google/gerrit/common/SiteLibraryLoaderUtil.java b/java/com/google/gerrit/common/SiteLibraryLoaderUtil.java
index cf86f74..fa9b139 100644
--- a/java/com/google/gerrit/common/SiteLibraryLoaderUtil.java
+++ b/java/com/google/gerrit/common/SiteLibraryLoaderUtil.java
@@ -18,7 +18,6 @@
 import static com.google.gerrit.common.FileUtil.lastModified;
 import static java.util.stream.Collectors.joining;
 
-import com.google.common.annotations.GwtIncompatible;
 import com.google.common.collect.ComparisonChain;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Ordering;
@@ -30,7 +29,6 @@
 import java.nio.file.Path;
 import java.util.List;
 
-@GwtIncompatible("Unemulated classes in java.nio and Guava")
 public final class SiteLibraryLoaderUtil {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
@@ -50,12 +48,9 @@
 
   public static List<Path> listJars(Path dir) throws IOException {
     DirectoryStream.Filter<Path> filter =
-        new DirectoryStream.Filter<Path>() {
-          @Override
-          public boolean accept(Path entry) throws IOException {
-            String name = entry.getFileName().toString();
-            return (name.endsWith(".jar") || name.endsWith(".zip")) && Files.isRegularFile(entry);
-          }
+        entry -> {
+          String name = entry.getFileName().toString();
+          return (name.endsWith(".jar") || name.endsWith(".zip")) && Files.isRegularFile(entry);
         };
     try (DirectoryStream<Path> jars = Files.newDirectoryStream(dir, filter)) {
       return new Ordering<Path>() {
diff --git a/java/com/google/gerrit/common/UsedAt.java b/java/com/google/gerrit/common/UsedAt.java
new file mode 100644
index 0000000..44ed92b
--- /dev/null
+++ b/java/com/google/gerrit/common/UsedAt.java
@@ -0,0 +1,43 @@
+// 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.common;
+
+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;
+
+/**
+ * A marker for a method that is public solely because it is called from inside a project or an
+ * organisation using Gerrit.
+ */
+@Target({METHOD, TYPE})
+@Retention(RUNTIME)
+public @interface UsedAt {
+  /** Enumeration of projects that call a method that would otherwise be private. */
+  enum Project {
+    GOOGLE,
+    COLLABNET,
+    PLUGIN_CHECKS,
+    PLUGIN_DELETE_PROJECT,
+    PLUGIN_SERVICEUSER,
+    PLUGINS_ALL, // Use this project if a method/type is generally made available to all plugins.
+  }
+
+  /** Reference to the project that uses the method annotated with this annotation. */
+  Project value();
+}
diff --git a/java/com/google/gerrit/common/Version.java b/java/com/google/gerrit/common/Version.java
index b8d3b67..6197be5 100644
--- a/java/com/google/gerrit/common/Version.java
+++ b/java/com/google/gerrit/common/Version.java
@@ -16,7 +16,6 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import com.google.common.annotations.GwtIncompatible;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.flogger.FluentLogger;
 import java.io.BufferedReader;
@@ -24,7 +23,6 @@
 import java.io.InputStream;
 import java.io.InputStreamReader;
 
-@GwtIncompatible("Unemulated com.google.gerrit.common.Version")
 public class Version {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
diff --git a/java/com/google/gerrit/common/audit/Audit.java b/java/com/google/gerrit/common/audit/Audit.java
deleted file mode 100644
index a791e97..0000000
--- a/java/com/google/gerrit/common/audit/Audit.java
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common.audit;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * Audit annotation for JSON/RPC interfaces.
- *
- * <p>Flag with @Audit all the JSON/RPC methods to be traced in audit-trail and submitted to the
- * GroupAuditService.
- */
-@Retention(RetentionPolicy.RUNTIME)
-@Target({ElementType.METHOD})
-public @interface Audit {
-  String action() default "";
-
-  /** List of positions of parameters to be obfuscated in audit-trail (i.e. passwords) */
-  int[] obfuscate() default {};
-}
diff --git a/java/com/google/gerrit/common/auth/SignInRequired.java b/java/com/google/gerrit/common/auth/SignInRequired.java
deleted file mode 100644
index bcebf5c..0000000
--- a/java/com/google/gerrit/common/auth/SignInRequired.java
+++ /dev/null
@@ -1,30 +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.common.auth;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * Annotation indicating a service method requires a current user.
- *
- * <p>If there is no current user then {@code com.google.gerrit.common.errors.NotSignedInException}
- * will be given to the callback's onFailure method.
- */
-@Retention(RetentionPolicy.RUNTIME)
-@Target(ElementType.METHOD)
-public @interface SignInRequired {}
diff --git a/java/com/google/gerrit/common/data/AccessSection.java b/java/com/google/gerrit/common/data/AccessSection.java
index 49948f8f..3670e96 100644
--- a/java/com/google/gerrit/common/data/AccessSection.java
+++ b/java/com/google/gerrit/common/data/AccessSection.java
@@ -16,6 +16,7 @@
 
 import static java.util.Objects.requireNonNull;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Project;
 import java.util.ArrayList;
@@ -24,24 +25,38 @@
 import java.util.Set;
 
 /** Portion of a {@link Project} describing access rules. */
-public class AccessSection extends RefConfigSection implements Comparable<AccessSection> {
+public final class AccessSection implements Comparable<AccessSection> {
   /** Special name given to the global capabilities; not a valid reference. */
   public static final String GLOBAL_CAPABILITIES = "GLOBAL_CAPABILITIES";
+  /** Pattern that matches all references in a project. */
+  public static final String ALL = "refs/*";
 
-  protected List<Permission> permissions;
+  /** Pattern that matches all branches in a project. */
+  public static final String HEADS = "refs/heads/*";
 
-  protected AccessSection() {}
+  /** Prefix that triggers a regular expression pattern. */
+  public static final String REGEX_PREFIX = "^";
 
-  public AccessSection(String refPattern) {
-    super(refPattern);
+  /** Name of the access section. It could be a ref pattern or something else. */
+  private String name;
+
+  private List<Permission> permissions;
+
+  public AccessSection(String name) {
+    this.name = name;
   }
 
-  // TODO(ekempin): Make this method return an ImmutableList once the GWT UI is gone.
-  public List<Permission> getPermissions() {
-    if (permissions == null) {
-      return new ArrayList<>();
-    }
-    return new ArrayList<>(permissions);
+  /** @return true if the name is likely to be a valid reference section name. */
+  public static boolean isValidRefSectionName(String name) {
+    return name.startsWith("refs/") || name.startsWith("^refs/");
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public ImmutableList<Permission> getPermissions() {
+    return permissions == null ? ImmutableList.of() : ImmutableList.copyOf(permissions);
   }
 
   public void setPermissions(List<Permission> list) {
@@ -148,7 +163,12 @@
 
   @Override
   public boolean equals(Object obj) {
-    if (!super.equals(obj) || !(obj instanceof AccessSection)) {
+    if (!(obj instanceof AccessSection)) {
+      return false;
+    }
+
+    AccessSection other = (AccessSection) obj;
+    if (!getName().equals(other.getName())) {
       return false;
     }
     return new HashSet<>(getPermissions())
@@ -163,6 +183,7 @@
         hashCode += permission.hashCode();
       }
     }
+    hashCode += getName().hashCode();
     return hashCode;
   }
 }
diff --git a/java/com/google/gerrit/common/data/CommentDetail.java b/java/com/google/gerrit/common/data/CommentDetail.java
index ed7c79b..1ae246f 100644
--- a/java/com/google/gerrit/common/data/CommentDetail.java
+++ b/java/com/google/gerrit/common/data/CommentDetail.java
@@ -84,7 +84,7 @@
 
   private static List<Comment> get(Map<Integer, List<Comment>> m, int i) {
     List<Comment> r = m.get(i);
-    return r != null ? orderComments(r) : Collections.<Comment>emptyList();
+    return r != null ? orderComments(r) : Collections.emptyList();
   }
 
   /**
diff --git a/java/com/google/gerrit/common/data/ContributorAgreement.java b/java/com/google/gerrit/common/data/ContributorAgreement.java
index b43dbfa..a6e8cdd 100644
--- a/java/com/google/gerrit/common/data/ContributorAgreement.java
+++ b/java/com/google/gerrit/common/data/ContributorAgreement.java
@@ -25,6 +25,8 @@
   protected List<PermissionRule> accepted;
   protected GroupReference autoVerify;
   protected String agreementUrl;
+  protected List<String> excludeProjectsRegexes;
+  protected List<String> matchProjectsRegexes;
 
   protected ContributorAgreement() {}
 
@@ -75,6 +77,28 @@
     this.agreementUrl = agreementUrl;
   }
 
+  public List<String> getExcludeProjectsRegexes() {
+    if (excludeProjectsRegexes == null) {
+      excludeProjectsRegexes = new ArrayList<>();
+    }
+    return excludeProjectsRegexes;
+  }
+
+  public void setExcludeProjectsRegexes(List<String> excludeProjectsRegexes) {
+    this.excludeProjectsRegexes = excludeProjectsRegexes;
+  }
+
+  public List<String> getMatchProjectsRegexes() {
+    if (matchProjectsRegexes == null) {
+      matchProjectsRegexes = new ArrayList<>();
+    }
+    return matchProjectsRegexes;
+  }
+
+  public void setMatchProjectsRegexes(List<String> matchProjectsRegexes) {
+    this.matchProjectsRegexes = matchProjectsRegexes;
+  }
+
   @Override
   public int compareTo(ContributorAgreement o) {
     return getName().compareTo(o.getName());
diff --git a/java/com/google/gerrit/common/data/GlobalCapability.java b/java/com/google/gerrit/common/data/GlobalCapability.java
index 3e11256..fbe1deb 100644
--- a/java/com/google/gerrit/common/data/GlobalCapability.java
+++ b/java/com/google/gerrit/common/data/GlobalCapability.java
@@ -22,7 +22,7 @@
 
 /** Server wide capabilities. Represented as {@link Permission} objects. */
 public class GlobalCapability {
-  /** Ability to access the database (with gsql). */
+  /** Ability to view code review metadata refs in repositories. */
   public static final String ACCESS_DATABASE = "accessDatabase";
 
   /**
diff --git a/java/com/google/gerrit/common/data/GroupDetail.java b/java/com/google/gerrit/common/data/GroupDetail.java
deleted file mode 100644
index 1ac06db..0000000
--- a/java/com/google/gerrit/common/data/GroupDetail.java
+++ /dev/null
@@ -1,37 +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.common.data;
-
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import java.util.Set;
-
-public class GroupDetail {
-  private Set<Account.Id> members;
-  private Set<AccountGroup.UUID> includes;
-
-  public GroupDetail(Set<Account.Id> members, Set<AccountGroup.UUID> includes) {
-    this.members = members;
-    this.includes = includes;
-  }
-
-  public Set<Account.Id> getMembers() {
-    return members;
-  }
-
-  public Set<AccountGroup.UUID> getIncludes() {
-    return includes;
-  }
-}
diff --git a/java/com/google/gerrit/common/data/GroupInfo.java b/java/com/google/gerrit/common/data/GroupInfo.java
deleted file mode 100644
index 2b5bf1b..0000000
--- a/java/com/google/gerrit/common/data/GroupInfo.java
+++ /dev/null
@@ -1,71 +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.common.data;
-
-import com.google.gerrit.reviewdb.client.AccountGroup;
-
-/** Summary information about an {@link AccountGroup}, for simple tabular displays. */
-public class GroupInfo {
-  protected AccountGroup.UUID uuid;
-  protected String name;
-  protected String description;
-  protected String url;
-
-  protected GroupInfo() {}
-
-  /**
-   * Create an anonymous group info, when only the id is known.
-   *
-   * <p>This constructor should only be a last-ditch effort, when the usual group lookup has failed
-   * and a stale group id has been discovered in the data store.
-   */
-  public GroupInfo(AccountGroup.UUID uuid) {
-    this.uuid = uuid;
-  }
-
-  /**
-   * Create a group description from a real data store record.
-   *
-   * @param a the data store record holding the specific group details.
-   */
-  public GroupInfo(GroupDescription.Basic a) {
-    uuid = a.getGroupUUID();
-    name = a.getName();
-    url = a.getUrl();
-
-    if (a instanceof GroupDescription.Internal) {
-      description = ((GroupDescription.Internal) a).getDescription();
-    }
-  }
-
-  /** @return the unique local id of the group */
-  public AccountGroup.UUID getId() {
-    return uuid;
-  }
-
-  /** @return the name of the group; null if not supplied */
-  public String getName() {
-    return name;
-  }
-
-  /** @return the description of the group; null if not supplied */
-  public String getDescription() {
-    return description;
-  }
-
-  public String getUrl() {
-    return url;
-  }
-}
diff --git a/java/com/google/gerrit/common/data/HostPageData.java b/java/com/google/gerrit/common/data/HostPageData.java
deleted file mode 100644
index 517c520..0000000
--- a/java/com/google/gerrit/common/data/HostPageData.java
+++ /dev/null
@@ -1,60 +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.common.data;
-
-import com.google.gerrit.extensions.client.DiffPreferencesInfo;
-import java.util.Date;
-import java.util.List;
-
-/** Data sent as part of the host page, to bootstrap the UI. */
-public class HostPageData {
-  /**
-   * Name of the cookie in which the XSRF token is sent from the server to the client during host
-   * page bootstrapping.
-   */
-  public static final String XSRF_COOKIE_NAME = "XSRF_TOKEN";
-
-  /**
-   * Name of the HTTP header in which the client must send the XSRF token to the server on each
-   * request.
-   */
-  public static final String XSRF_HEADER_NAME = "X-Gerrit-Auth";
-
-  public String version;
-  public DiffPreferencesInfo accountDiffPref;
-  public Theme theme;
-  public List<String> plugins;
-  public List<Message> messages;
-  public Integer pluginsLoadTimeout;
-  public boolean isNoteDbEnabled;
-  public boolean canLoadInIFrame;
-
-  public static class Theme {
-    public String backgroundColor;
-    public String topMenuColor;
-    public String textColor;
-    public String trimColor;
-    public String selectionColor;
-    public String changeTableOutdatedColor;
-    public String tableOddRowColor;
-    public String tableEvenRowColor;
-  }
-
-  public static class Message {
-    public String id;
-    public Date redisplay;
-    public String html;
-  }
-}
diff --git a/java/com/google/gerrit/common/data/LabelType.java b/java/com/google/gerrit/common/data/LabelType.java
index ff7d25b..be4c33c 100644
--- a/java/com/google/gerrit/common/data/LabelType.java
+++ b/java/com/google/gerrit/common/data/LabelType.java
@@ -94,8 +94,7 @@
 
   protected String name;
 
-  // String rather than LabelFunction for backwards compatibility with GWT JSON interface.
-  protected String functionName;
+  protected LabelFunction function;
 
   protected boolean copyMinScore;
   protected boolean copyMaxScore;
@@ -123,7 +122,7 @@
     values = sortValues(valueList);
     defaultValue = 0;
 
-    functionName = LabelFunction.MAX_WITH_BLOCK.getFunctionName();
+    function = LabelFunction.MAX_WITH_BLOCK;
 
     maxNegative = Short.MIN_VALUE;
     maxPositive = Short.MAX_VALUE;
@@ -160,15 +159,11 @@
   }
 
   public LabelFunction getFunction() {
-    if (functionName == null) {
-      return null;
-    }
-    return LabelFunction.parse(functionName)
-        .orElseThrow(() -> new IllegalStateException("Unsupported functionName: " + functionName));
+    return function;
   }
 
   public void setFunction(@Nullable LabelFunction function) {
-    this.functionName = function != null ? function.getFunctionName() : null;
+    this.function = function;
   }
 
   public boolean canOverride() {
diff --git a/java/com/google/gerrit/common/data/ParameterizedString.java b/java/com/google/gerrit/common/data/ParameterizedString.java
index 28e47ee..84bb535 100644
--- a/java/com/google/gerrit/common/data/ParameterizedString.java
+++ b/java/com/google/gerrit/common/data/ParameterizedString.java
@@ -40,7 +40,7 @@
   private ParameterizedString(Constant c) {
     pattern = c.text;
     rawPattern = c.text;
-    patternOps = Collections.<Format>singletonList(c);
+    patternOps = Collections.singletonList(c);
     parameters = Collections.emptyList();
   }
 
@@ -60,7 +60,7 @@
         break;
       }
 
-      raw.append(pattern.substring(i, b));
+      raw.append(pattern, i, b);
       ops.add(new Constant(pattern.substring(i, b)));
 
       // "${parameter[.functions...]}" -> "parameter[.functions...]"
diff --git a/java/com/google/gerrit/common/data/Permission.java b/java/com/google/gerrit/common/data/Permission.java
index de6108e..3ba0ba7 100644
--- a/java/com/google/gerrit/common/data/Permission.java
+++ b/java/com/google/gerrit/common/data/Permission.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.common.data;
 
+import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -45,6 +46,7 @@
   public static final String REMOVE_REVIEWER = "removeReviewer";
   public static final String SUBMIT = "submit";
   public static final String SUBMIT_AS = "submitAs";
+  public static final String TOGGLE_WORK_IN_PROGRESS_STATE = "toggleWipState";
   public static final String VIEW_PRIVATE_CHANGES = "viewPrivateChanges";
 
   private static final List<String> NAMES_LC;
@@ -77,6 +79,7 @@
     NAMES_LC.add(REMOVE_REVIEWER.toLowerCase());
     NAMES_LC.add(SUBMIT.toLowerCase());
     NAMES_LC.add(SUBMIT_AS.toLowerCase());
+    NAMES_LC.add(TOGGLE_WORK_IN_PROGRESS_STATE.toLowerCase());
     NAMES_LC.add(VIEW_PRIVATE_CHANGES.toLowerCase());
 
     LABEL_INDEX = NAMES_LC.indexOf(Permission.LABEL);
@@ -157,12 +160,8 @@
     exclusiveGroup = newExclusiveGroup;
   }
 
-  // TODO(ekempin): Make this method return an ImmutableList once the GWT UI is gone.
-  public List<PermissionRule> getRules() {
-    if (rules == null) {
-      return new ArrayList<>();
-    }
-    return new ArrayList<>(rules);
+  public ImmutableList<PermissionRule> getRules() {
+    return rules == null ? ImmutableList.of() : ImmutableList.copyOf(rules);
   }
 
   public void setRules(List<PermissionRule> list) {
diff --git a/java/com/google/gerrit/common/data/ProjectAccess.java b/java/com/google/gerrit/common/data/ProjectAccess.java
deleted file mode 100644
index ea17525..0000000
--- a/java/com/google/gerrit/common/data/ProjectAccess.java
+++ /dev/null
@@ -1,142 +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.common.data;
-
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-public class ProjectAccess {
-  protected Project.NameKey projectName;
-  protected String revision;
-  protected Project.NameKey inheritsFrom;
-  protected List<AccessSection> local;
-  protected Set<String> ownerOf;
-  protected boolean isConfigVisible;
-  protected boolean canUpload;
-  protected LabelTypes labelTypes;
-  protected Map<String, String> capabilities;
-  protected Map<AccountGroup.UUID, GroupInfo> groupInfo;
-  protected List<WebLinkInfoCommon> fileHistoryLinks;
-
-  public ProjectAccess() {}
-
-  public Project.NameKey getProjectName() {
-    return projectName;
-  }
-
-  public void setProjectName(Project.NameKey projectName) {
-    this.projectName = projectName;
-  }
-
-  public String getRevision() {
-    return revision;
-  }
-
-  public void setRevision(String name) {
-    revision = name;
-  }
-
-  public Project.NameKey getInheritsFrom() {
-    return inheritsFrom;
-  }
-
-  public void setInheritsFrom(Project.NameKey name) {
-    inheritsFrom = name;
-  }
-
-  public List<AccessSection> getLocal() {
-    return local;
-  }
-
-  public void setLocal(List<AccessSection> as) {
-    local = as;
-  }
-
-  public AccessSection getLocal(String name) {
-    for (AccessSection s : local) {
-      if (s.getName().equals(name)) {
-        return s;
-      }
-    }
-    return null;
-  }
-
-  public boolean isOwnerOf(AccessSection section) {
-    return isOwnerOf(section.getName());
-  }
-
-  public boolean isOwnerOf(String name) {
-    return ownerOf.contains(name);
-  }
-
-  public Set<String> getOwnerOf() {
-    return ownerOf;
-  }
-
-  public void setOwnerOf(Set<String> refs) {
-    ownerOf = refs;
-  }
-
-  public boolean isConfigVisible() {
-    return isConfigVisible;
-  }
-
-  public void setConfigVisible(boolean isConfigVisible) {
-    this.isConfigVisible = isConfigVisible;
-  }
-
-  public boolean canUpload() {
-    return canUpload;
-  }
-
-  public void setCanUpload(boolean canUpload) {
-    this.canUpload = canUpload;
-  }
-
-  public LabelTypes getLabelTypes() {
-    return labelTypes;
-  }
-
-  public void setLabelTypes(LabelTypes labelTypes) {
-    this.labelTypes = labelTypes;
-  }
-
-  public Map<String, String> getCapabilities() {
-    return capabilities;
-  }
-
-  public void setCapabilities(Map<String, String> capabilities) {
-    this.capabilities = capabilities;
-  }
-
-  public Map<AccountGroup.UUID, GroupInfo> getGroupInfo() {
-    return groupInfo;
-  }
-
-  public void setGroupInfo(Map<AccountGroup.UUID, GroupInfo> m) {
-    groupInfo = m;
-  }
-
-  public void setFileHistoryLinks(List<WebLinkInfoCommon> links) {
-    fileHistoryLinks = links;
-  }
-
-  public List<WebLinkInfoCommon> getFileHistoryLinks() {
-    return fileHistoryLinks;
-  }
-}
diff --git a/java/com/google/gerrit/common/data/ProjectAdminService.java b/java/com/google/gerrit/common/data/ProjectAdminService.java
deleted file mode 100644
index e9a7c15..0000000
--- a/java/com/google/gerrit/common/data/ProjectAdminService.java
+++ /dev/null
@@ -1,49 +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.common.data;
-
-import com.google.gerrit.common.audit.Audit;
-import com.google.gerrit.common.auth.SignInRequired;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.gwtjsonrpc.common.RemoteJsonService;
-import com.google.gwtjsonrpc.common.RpcImpl;
-import com.google.gwtjsonrpc.common.RpcImpl.Version;
-import java.util.List;
-
-@RpcImpl(version = Version.V2_0)
-public interface ProjectAdminService extends RemoteJsonService {
-  void projectAccess(Project.NameKey projectName, AsyncCallback<ProjectAccess> callback);
-
-  @Audit
-  @SignInRequired
-  void changeProjectAccess(
-      Project.NameKey projectName,
-      String baseRevision,
-      String message,
-      List<AccessSection> sections,
-      Project.NameKey parentProjectName,
-      AsyncCallback<ProjectAccess> callback);
-
-  @SignInRequired
-  void reviewProjectAccess(
-      Project.NameKey projectName,
-      String baseRevision,
-      String message,
-      List<AccessSection> sections,
-      Project.NameKey parentProjectName,
-      AsyncCallback<Change.Id> callback);
-}
diff --git a/java/com/google/gerrit/common/data/RefConfigSection.java b/java/com/google/gerrit/common/data/RefConfigSection.java
deleted file mode 100644
index 663379a..0000000
--- a/java/com/google/gerrit/common/data/RefConfigSection.java
+++ /dev/null
@@ -1,60 +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.common.data;
-
-public abstract class RefConfigSection {
-  /** Pattern that matches all references in a project. */
-  public static final String ALL = "refs/*";
-
-  /** Pattern that matches all branches in a project. */
-  public static final String HEADS = "refs/heads/*";
-
-  /** Prefix that triggers a regular expression pattern. */
-  public static final String REGEX_PREFIX = "^";
-
-  /** @return true if the name is likely to be a valid reference section name. */
-  public static boolean isValid(String name) {
-    return name.startsWith("refs/") || name.startsWith("^refs/");
-  }
-
-  protected String name;
-
-  public RefConfigSection() {}
-
-  public RefConfigSection(String name) {
-    setName(name);
-  }
-
-  public String getName() {
-    return name;
-  }
-
-  public void setName(String name) {
-    this.name = name;
-  }
-
-  @Override
-  public boolean equals(Object obj) {
-    if (!(obj instanceof RefConfigSection)) {
-      return false;
-    }
-    return name.equals(((RefConfigSection) obj).name);
-  }
-
-  @Override
-  public int hashCode() {
-    return name.hashCode();
-  }
-}
diff --git a/java/com/google/gerrit/common/data/SshHostKey.java b/java/com/google/gerrit/common/data/SshHostKey.java
deleted file mode 100644
index 05f1611..0000000
--- a/java/com/google/gerrit/common/data/SshHostKey.java
+++ /dev/null
@@ -1,45 +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.common.data;
-
-/** Description of the SSH daemon host key used by Gerrit. */
-public class SshHostKey {
-  protected String hostIdent;
-  protected String hostKey;
-  protected String fingerprint;
-
-  protected SshHostKey() {}
-
-  public SshHostKey(String hi, String hk, String fp) {
-    hostIdent = hi;
-    hostKey = hk;
-    fingerprint = fp;
-  }
-
-  /** @return host name string, to appear in a known_hosts file. */
-  public String getHostIdent() {
-    return hostIdent;
-  }
-
-  /** @return base 64 encoded host key string, starting with key type. */
-  public String getHostKey() {
-    return hostKey;
-  }
-
-  /** @return the key fingerprint, as displayed by a connecting client. */
-  public String getFingerprint() {
-    return fingerprint;
-  }
-}
diff --git a/java/com/google/gerrit/common/data/SubmitRecord.java b/java/com/google/gerrit/common/data/SubmitRecord.java
index 8638d6d..22861b2 100644
--- a/java/com/google/gerrit/common/data/SubmitRecord.java
+++ b/java/com/google/gerrit/common/data/SubmitRecord.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.common.annotations.GwtIncompatible;
 import com.google.gerrit.reviewdb.client.Account;
 import java.util.Collection;
 import java.util.List;
@@ -65,7 +64,7 @@
 
   public Status status;
   public List<Label> labels;
-  @GwtIncompatible public List<SubmitRequirement> requirements;
+  public List<SubmitRequirement> requirements;
   public String errorMessage;
 
   public static class Label {
@@ -136,7 +135,6 @@
     }
   }
 
-  @GwtIncompatible
   @Override
   public String toString() {
     StringBuilder sb = new StringBuilder();
@@ -164,7 +162,6 @@
     return sb.toString();
   }
 
-  @GwtIncompatible
   @Override
   public boolean equals(Object o) {
     if (o instanceof SubmitRecord) {
@@ -177,7 +174,6 @@
     return false;
   }
 
-  @GwtIncompatible
   @Override
   public int hashCode() {
     return Objects.hash(status, labels, errorMessage, requirements);
diff --git a/java/com/google/gerrit/common/data/SubmitRequirement.java b/java/com/google/gerrit/common/data/SubmitRequirement.java
index 0c978ca..66e647d 100644
--- a/java/com/google/gerrit/common/data/SubmitRequirement.java
+++ b/java/com/google/gerrit/common/data/SubmitRequirement.java
@@ -17,13 +17,11 @@
 import static com.google.common.base.Preconditions.checkState;
 
 import com.google.auto.value.AutoValue;
-import com.google.common.annotations.GwtIncompatible;
 import com.google.common.base.CharMatcher;
 import com.google.common.collect.ImmutableMap;
 import java.util.Map;
 
 /** Describes a requirement to submit a change. */
-@GwtIncompatible
 @AutoValue
 @AutoValue.CopyAnnotations
 public abstract class SubmitRequirement {
diff --git a/java/com/google/gerrit/common/data/SubscribeSection.java b/java/com/google/gerrit/common/data/SubscribeSection.java
index a3468d7..aaf0798 100644
--- a/java/com/google/gerrit/common/data/SubscribeSection.java
+++ b/java/com/google/gerrit/common/data/SubscribeSection.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.common.annotations.GwtIncompatible;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import java.util.ArrayList;
@@ -24,7 +23,6 @@
 import org.eclipse.jgit.transport.RefSpec;
 
 /** Portion of a {@link Project} describing superproject subscription rules. */
-@GwtIncompatible("Unemulated org.eclipse.jgit.transport.RefSpec")
 public class SubscribeSection {
 
   private final List<RefSpec> multiMatchRefSpecs;
diff --git a/java/com/google/gerrit/common/data/SystemInfoService.java b/java/com/google/gerrit/common/data/SystemInfoService.java
deleted file mode 100644
index d88b638..0000000
--- a/java/com/google/gerrit/common/data/SystemInfoService.java
+++ /dev/null
@@ -1,31 +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.common.data;
-
-import com.google.gwtjsonrpc.common.AllowCrossSiteRequest;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.gwtjsonrpc.common.RemoteJsonService;
-import com.google.gwtjsonrpc.common.RpcImpl;
-import com.google.gwtjsonrpc.common.RpcImpl.Version;
-import com.google.gwtjsonrpc.common.VoidResult;
-import java.util.List;
-
-@RpcImpl(version = Version.V2_0)
-public interface SystemInfoService extends RemoteJsonService {
-  @AllowCrossSiteRequest
-  void daemonHostKeys(AsyncCallback<List<SshHostKey>> callback);
-
-  void clientError(String message, AsyncCallback<VoidResult> callback);
-}
diff --git a/java/com/google/gerrit/common/data/WebLinkInfoCommon.java b/java/com/google/gerrit/common/data/WebLinkInfoCommon.java
deleted file mode 100644
index dd0a70a..0000000
--- a/java/com/google/gerrit/common/data/WebLinkInfoCommon.java
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common.data;
-
-public class WebLinkInfoCommon {
-  public WebLinkInfoCommon() {}
-
-  public String name;
-  public String imageUrl;
-  public String url;
-  public String target;
-}
diff --git a/java/com/google/gerrit/common/data/testing/GroupReferenceSubject.java b/java/com/google/gerrit/common/data/testing/GroupReferenceSubject.java
index 1988d66..265d590 100644
--- a/java/com/google/gerrit/common/data/testing/GroupReferenceSubject.java
+++ b/java/com/google/gerrit/common/data/testing/GroupReferenceSubject.java
@@ -20,14 +20,17 @@
 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.common.data.GroupReference;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 
 public class GroupReferenceSubject extends Subject<GroupReferenceSubject, GroupReference> {
 
   public static GroupReferenceSubject assertThat(GroupReference group) {
-    return assertAbout(GroupReferenceSubject::new).that(group);
+    return assertAbout(groupReferences()).that(group);
+  }
+
+  public static Subject.Factory<GroupReferenceSubject, GroupReference> groupReferences() {
+    return GroupReferenceSubject::new;
   }
 
   private GroupReferenceSubject(FailureMetadata metadata, GroupReference group) {
@@ -37,12 +40,12 @@
   public ComparableSubject<?, AccountGroup.UUID> groupUuid() {
     isNotNull();
     GroupReference group = actual();
-    return Truth.assertThat(group.getUUID()).named("groupUuid");
+    return check("groupUuid()").that(group.getUUID());
   }
 
   public StringSubject name() {
     isNotNull();
     GroupReference group = actual();
-    return Truth.assertThat(group.getName()).named("name");
+    return check("name()").that(group.getName());
   }
 }
diff --git a/java/com/google/gerrit/common/errors/EmailException.java b/java/com/google/gerrit/common/errors/EmailException.java
deleted file mode 100644
index 635335d..0000000
--- a/java/com/google/gerrit/common/errors/EmailException.java
+++ /dev/null
@@ -1,29 +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.common.errors;
-
-public class EmailException extends Exception {
-  private static final long serialVersionUID = 1L;
-
-  public static final String MESSAGE = "Mail Error: ";
-
-  public EmailException(String msg) {
-    super(MESSAGE + msg);
-  }
-
-  public EmailException(String msg, Throwable why) {
-    super(MESSAGE + msg, why);
-  }
-}
diff --git a/java/com/google/gerrit/common/errors/InvalidNameException.java b/java/com/google/gerrit/common/errors/InvalidNameException.java
deleted file mode 100644
index d975aef..0000000
--- a/java/com/google/gerrit/common/errors/InvalidNameException.java
+++ /dev/null
@@ -1,30 +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.common.errors;
-
-/** Error indicating the entity name is invalid as supplied. */
-public class InvalidNameException extends Exception {
-  private static final long serialVersionUID = 1L;
-
-  public static final String MESSAGE = "Invalid Name";
-
-  public InvalidNameException() {
-    super(MESSAGE);
-  }
-
-  public InvalidNameException(String invalidName) {
-    super(MESSAGE + ": " + invalidName);
-  }
-}
diff --git a/java/com/google/gerrit/common/errors/InvalidSshKeyException.java b/java/com/google/gerrit/common/errors/InvalidSshKeyException.java
deleted file mode 100644
index 0e5d80d..0000000
--- a/java/com/google/gerrit/common/errors/InvalidSshKeyException.java
+++ /dev/null
@@ -1,30 +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.common.errors;
-
-/** Error indicating the SSH key string is invalid as supplied. */
-public class InvalidSshKeyException extends Exception {
-  private static final long serialVersionUID = 1L;
-
-  public static final String MESSAGE = "Invalid SSH Key";
-
-  public InvalidSshKeyException() {
-    super(MESSAGE);
-  }
-
-  public InvalidSshKeyException(Throwable cause) {
-    super(MESSAGE, cause);
-  }
-}
diff --git a/java/com/google/gerrit/common/errors/NameAlreadyUsedException.java b/java/com/google/gerrit/common/errors/NameAlreadyUsedException.java
deleted file mode 100644
index ea20e2e..0000000
--- a/java/com/google/gerrit/common/errors/NameAlreadyUsedException.java
+++ /dev/null
@@ -1,26 +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.common.errors;
-
-/** Error indicating entity name is already taken by another entity. */
-public class NameAlreadyUsedException extends Exception {
-  private static final long serialVersionUID = 1L;
-
-  public static final String MESSAGE = "Name Already Used: ";
-
-  public NameAlreadyUsedException(String name) {
-    super(MESSAGE + name);
-  }
-}
diff --git a/java/com/google/gerrit/common/errors/NoSuchAccountException.java b/java/com/google/gerrit/common/errors/NoSuchAccountException.java
deleted file mode 100644
index 90bf624..0000000
--- a/java/com/google/gerrit/common/errors/NoSuchAccountException.java
+++ /dev/null
@@ -1,26 +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.common.errors;
-
-/** Error indicating the account requested doesn't exist. */
-public class NoSuchAccountException extends Exception {
-  private static final long serialVersionUID = 1L;
-
-  public static final String MESSAGE = "Not Found: ";
-
-  public NoSuchAccountException(String who) {
-    super(MESSAGE + who);
-  }
-}
diff --git a/java/com/google/gerrit/common/errors/NoSuchEntityException.java b/java/com/google/gerrit/common/errors/NoSuchEntityException.java
deleted file mode 100644
index 1829c8b..0000000
--- a/java/com/google/gerrit/common/errors/NoSuchEntityException.java
+++ /dev/null
@@ -1,30 +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.common.errors;
-
-/** Error indicating the entity requested doesn't exist. */
-public class NoSuchEntityException extends Exception {
-  private static final long serialVersionUID = 1L;
-
-  public static final String MESSAGE = "Not Found";
-
-  public NoSuchEntityException() {
-    super(MESSAGE);
-  }
-
-  public NoSuchEntityException(String message) {
-    super(message);
-  }
-}
diff --git a/java/com/google/gerrit/common/errors/NoSuchGroupException.java b/java/com/google/gerrit/common/errors/NoSuchGroupException.java
deleted file mode 100644
index 6e3db9e..0000000
--- a/java/com/google/gerrit/common/errors/NoSuchGroupException.java
+++ /dev/null
@@ -1,52 +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.common.errors;
-
-import com.google.gerrit.reviewdb.client.AccountGroup;
-
-/** Indicates the account group does not exist. */
-public class NoSuchGroupException extends Exception {
-  private static final long serialVersionUID = 1L;
-
-  public static final String MESSAGE = "Group Not Found: ";
-
-  public NoSuchGroupException(AccountGroup.Id key) {
-    this(key, null);
-  }
-
-  public NoSuchGroupException(AccountGroup.UUID key) {
-    this(key, null);
-  }
-
-  public NoSuchGroupException(AccountGroup.Id key, Throwable why) {
-    super(MESSAGE + key.toString(), why);
-  }
-
-  public NoSuchGroupException(AccountGroup.UUID key, Throwable why) {
-    super(MESSAGE + key.toString(), why);
-  }
-
-  public NoSuchGroupException(AccountGroup.NameKey k, Throwable why) {
-    super(MESSAGE + k.toString(), why);
-  }
-
-  public NoSuchGroupException(String who) {
-    this(who, null);
-  }
-
-  public NoSuchGroupException(String who, Throwable why) {
-    super(MESSAGE + who, why);
-  }
-}
diff --git a/java/com/google/gerrit/common/errors/NotSignedInException.java b/java/com/google/gerrit/common/errors/NotSignedInException.java
deleted file mode 100644
index 65caf02..0000000
--- a/java/com/google/gerrit/common/errors/NotSignedInException.java
+++ /dev/null
@@ -1,26 +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.common.errors;
-
-/** Error stating the user must be signed-in in order to perform this action. */
-public class NotSignedInException extends Exception {
-  private static final long serialVersionUID = 1L;
-
-  public static final String MESSAGE = "Not Signed In";
-
-  public NotSignedInException() {
-    super(MESSAGE);
-  }
-}
diff --git a/java/com/google/gerrit/common/errors/UpdateParentFailedException.java b/java/com/google/gerrit/common/errors/UpdateParentFailedException.java
deleted file mode 100644
index 16d5240..0000000
--- a/java/com/google/gerrit/common/errors/UpdateParentFailedException.java
+++ /dev/null
@@ -1,26 +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.common.errors;
-
-/** Error indicating that updating a parent project failed. */
-public class UpdateParentFailedException extends Exception {
-  private static final long serialVersionUID = 1L;
-
-  public static final String MESSAGE = "Update Parent Project Failed: ";
-
-  public UpdateParentFailedException(String message, Throwable why) {
-    super(MESSAGE + ": " + message, why);
-  }
-}
diff --git a/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java b/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
index d634e35..dd62f37 100644
--- a/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
+++ b/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
@@ -15,14 +15,16 @@
 package com.google.gerrit.elasticsearch;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.gson.FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.collect.ArrayListMultimap;
-import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
+import com.google.common.collect.Streams;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.io.CharStreams;
 import com.google.gerrit.common.Nullable;
@@ -30,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.exceptions.StorageException;
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.FieldType;
 import com.google.gerrit.index.Index;
@@ -37,8 +40,12 @@
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.query.DataSource;
 import com.google.gerrit.index.query.FieldBundle;
+import com.google.gerrit.index.query.ListResultSet;
 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.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;
@@ -47,9 +54,7 @@
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.google.gson.JsonParser;
-import com.google.gwtorm.protobuf.ProtobufCodec;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
+import com.google.protobuf.MessageLite;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
@@ -59,7 +64,6 @@
 import java.sql.Timestamp;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -88,14 +92,22 @@
   }
 
   protected static <T> List<T> decodeProtos(
-      JsonObject doc, String fieldName, ProtobufCodec<T> codec) {
+      JsonObject doc, String fieldName, ProtoConverter<?, T> converter) {
     JsonArray field = doc.getAsJsonArray(fieldName);
     if (field == null) {
       return null;
     }
-    return FluentIterable.from(field)
-        .transform(i -> codec.decode(decodeBase64(i.getAsString())))
-        .toList();
+    return Streams.stream(field)
+        .map(JsonElement::getAsString)
+        .map(AbstractElasticIndex::decodeBase64)
+        .map(bytes -> parseProtoFrom(bytes, converter))
+        .collect(toImmutableList());
+  }
+
+  protected static <P extends MessageLite, T> T parseProtoFrom(
+      byte[] bytes, ProtoConverter<P, T> converter) {
+    P message = Protos.parseUnchecked(converter.getParser(), bytes);
+    return converter.fromProto(message);
   }
 
   static String getContent(Response response) throws IOException {
@@ -159,23 +171,23 @@
   }
 
   @Override
-  public void markReady(boolean ready) throws IOException {
+  public void markReady(boolean ready) {
     IndexUtils.setReady(sitePaths, indexNameRaw, schema.getVersion(), ready);
   }
 
   @Override
-  public void delete(K id) throws IOException {
+  public void delete(K id) {
     String uri = getURI(type, BULK);
     Response response = postRequest(uri, getDeleteActions(id), getRefreshParam());
     int statusCode = response.getStatusLine().getStatusCode();
     if (statusCode != HttpStatus.SC_OK) {
-      throw new IOException(
+      throw new StorageException(
           String.format("Failed to delete %s from index %s: %s", id, indexName, statusCode));
     }
   }
 
   @Override
-  public void deleteAll() throws IOException {
+  public void deleteAll() {
     // Delete the index, if it exists.
     String endpoint = indexName + client.adapter().indicesExistParams();
     Response response = performRequest("HEAD", endpoint);
@@ -184,7 +196,7 @@
       response = performRequest("DELETE", indexName);
       statusCode = response.getStatusLine().getStatusCode();
       if (statusCode != HttpStatus.SC_OK) {
-        throw new IOException(
+        throw new StorageException(
             String.format("Failed to delete index %s: %s", indexName, statusCode));
       }
     }
@@ -197,7 +209,7 @@
     statusCode = response.getStatusLine().getStatusCode();
     if (statusCode != HttpStatus.SC_OK) {
       String error = String.format("Failed to create index %s: %s", indexName, statusCode);
-      throw new IOException(error);
+      throw new StorageException(error);
     }
   }
 
@@ -303,22 +315,25 @@
     return sortArray;
   }
 
-  protected String getURI(String type, String request) throws UnsupportedEncodingException {
-    String encodedIndexName = URLEncoder.encode(indexName, UTF_8.toString());
-    if (SEARCH.equals(request) && client.adapter().omitType()) {
-      return encodedIndexName + "/" + request;
+  protected String getURI(String type, 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;
+    } catch (UnsupportedEncodingException e) {
+      throw new StorageException(e);
     }
-    String encodedTypeIfAny =
-        client.adapter().omitType() ? "" : "/" + URLEncoder.encode(type, UTF_8.toString());
-    return encodedIndexName + encodedTypeIfAny + "/" + request;
   }
 
-  protected Response postRequest(String uri, Object payload) throws IOException {
+  protected Response postRequest(String uri, Object payload) {
     return performRequest("POST", uri, payload);
   }
 
-  protected Response postRequest(String uri, Object payload, Map<String, String> params)
-      throws IOException {
+  protected Response postRequest(String uri, Object payload, Map<String, String> params) {
     return performRequest("POST", uri, payload, params);
   }
 
@@ -326,18 +341,16 @@
     return target.substring(0, target.length() - 1) + "," + addition.substring(1);
   }
 
-  private Response performRequest(String method, String uri) throws IOException {
+  private Response performRequest(String method, String uri) {
     return performRequest(method, uri, null);
   }
 
-  private Response performRequest(String method, String uri, @Nullable Object payload)
-      throws IOException {
+  private Response performRequest(String method, String uri, @Nullable Object payload) {
     return performRequest(method, uri, payload, Collections.emptyMap());
   }
 
   private Response performRequest(
-      String method, String uri, @Nullable Object payload, Map<String, String> params)
-      throws IOException {
+      String method, String uri, @Nullable Object payload, Map<String, String> params) {
     Request request = new Request(method, uri.startsWith("/") ? uri : "/" + uri);
     if (payload != null) {
       String payloadStr = payload instanceof String ? (String) payload : payload.toString();
@@ -346,7 +359,11 @@
     for (Map.Entry<String, String> entry : params.entrySet()) {
       request.addParameter(entry.getKey(), entry.getValue());
     }
-    return client.get().performRequest(request);
+    try {
+      return client.get().performRequest(request);
+    } catch (IOException e) {
+      throw new StorageException(e);
+    }
   }
 
   protected class ElasticQuerySource implements DataSource<V> {
@@ -374,18 +391,17 @@
     }
 
     @Override
-    public ResultSet<V> read() throws OrmException {
+    public ResultSet<V> read() {
       return readImpl((doc) -> AbstractElasticIndex.this.fromDocument(doc, opts.fields()));
     }
 
     @Override
-    public ResultSet<FieldBundle> readRaw() throws OrmException {
+    public ResultSet<FieldBundle> readRaw() {
       return readImpl(AbstractElasticIndex.this::toFieldBundle);
     }
 
-    private <T> ResultSet<T> readImpl(Function<JsonObject, T> mapper) throws OrmException {
+    private <T> ResultSet<T> readImpl(Function<JsonObject, T> mapper) {
       try {
-        List<T> results = Collections.emptyList();
         String uri = getURI(index, SEARCH);
         Response response =
             performRequest(HttpPost.METHOD_NAME, uri, search, Collections.emptyMap());
@@ -396,36 +412,21 @@
               new JsonParser().parse(content).getAsJsonObject().getAsJsonObject("hits");
           if (obj.get("hits") != null) {
             JsonArray json = obj.getAsJsonArray("hits");
-            results = Lists.newArrayListWithCapacity(json.size());
+            ImmutableList.Builder<T> results = ImmutableList.builderWithExpectedSize(json.size());
             for (int i = 0; i < json.size(); i++) {
               T mapperResult = mapper.apply(json.get(i).getAsJsonObject());
               if (mapperResult != null) {
                 results.add(mapperResult);
               }
             }
+            return new ListResultSet<>(results.build());
           }
         } else {
           logger.atSevere().log(statusLine.getReasonPhrase());
         }
-        final List<T> r = Collections.unmodifiableList(results);
-        return new ResultSet<T>() {
-          @Override
-          public Iterator<T> iterator() {
-            return r.iterator();
-          }
-
-          @Override
-          public List<T> toList() {
-            return r;
-          }
-
-          @Override
-          public void close() {
-            // Do nothing.
-          }
-        };
+        return new ListResultSet<>(ImmutableList.of());
       } catch (IOException e) {
-        throw new OrmException(e);
+        throw new StorageException(e);
       }
     }
   }
diff --git a/java/com/google/gerrit/elasticsearch/BUILD b/java/com/google/gerrit/elasticsearch/BUILD
index c71d536..a9b145b 100644
--- a/java/com/google/gerrit/elasticsearch/BUILD
+++ b/java/com/google/gerrit/elasticsearch/BUILD
@@ -6,16 +6,17 @@
     visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/common:annotations",
+        "//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",
         "//lib:gson",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib:protobuf",
         "//lib/commons:codec",
         "//lib/commons:lang",
diff --git a/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java b/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
index d0b70ae..c25aa90 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
@@ -20,6 +20,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.exceptions.StorageException;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.query.DataSource;
@@ -38,7 +39,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
-import java.io.IOException;
 import java.util.Set;
 import org.apache.http.HttpStatus;
 import org.elasticsearch.client.Response;
@@ -73,7 +73,7 @@
   }
 
   @Override
-  public void replace(AccountState as) throws IOException {
+  public void replace(AccountState as) {
     BulkRequest bulk =
         new IndexRequest(getId(as), indexName, type, client.adapter())
             .add(new UpdateRequest<>(schema, as));
@@ -82,7 +82,7 @@
     Response response = postRequest(uri, bulk, getRefreshParam());
     int statusCode = response.getStatusLine().getStatusCode();
     if (statusCode != HttpStatus.SC_OK) {
-      throw new IOException(
+      throw new StorageException(
           String.format(
               "Failed to replace account %s in index %s: %s",
               as.getAccount().getId(), indexName, statusCode));
diff --git a/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java b/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
index 9466816..9e82e2b 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
@@ -14,9 +14,6 @@
 
 package com.google.gerrit.elasticsearch;
 
-import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.APPROVAL_CODEC;
-import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.CHANGE_CODEC;
-import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.PATCH_SET_CODEC;
 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;
@@ -35,6 +32,7 @@
 import com.google.gerrit.elasticsearch.bulk.DeleteRequest;
 import com.google.gerrit.elasticsearch.bulk.IndexRequest;
 import com.google.gerrit.elasticsearch.bulk.UpdateRequest;
+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;
@@ -42,9 +40,10 @@
 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.Change.Id;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
+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;
@@ -58,11 +57,8 @@
 import com.google.gson.JsonArray;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
-import java.io.IOException;
 import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
@@ -91,40 +87,33 @@
   private static final String CLOSED_CHANGES = "closed_" + CHANGES;
 
   private final ChangeMapping mapping;
-  private final Provider<ReviewDb> db;
   private final ChangeData.Factory changeDataFactory;
   private final Schema<ChangeData> schema;
 
   @Inject
   ElasticChangeIndex(
       ElasticConfiguration cfg,
-      Provider<ReviewDb> db,
       ChangeData.Factory changeDataFactory,
       SitePaths sitePaths,
       ElasticRestClientProvider clientBuilder,
       @Assisted Schema<ChangeData> schema) {
     super(cfg, sitePaths, schema, clientBuilder, CHANGES);
-    this.db = db;
     this.changeDataFactory = changeDataFactory;
     this.schema = schema;
     mapping = new ChangeMapping(schema, client.adapter());
   }
 
   @Override
-  public void replace(ChangeData cd) throws IOException {
+  public void replace(ChangeData cd) {
     String deleteIndex;
     String insertIndex;
 
-    try {
-      if (cd.change().getStatus().isOpen()) {
-        insertIndex = OPEN_CHANGES;
-        deleteIndex = CLOSED_CHANGES;
-      } else {
-        insertIndex = CLOSED_CHANGES;
-        deleteIndex = OPEN_CHANGES;
-      }
-    } catch (OrmException e) {
-      throw new IOException(e);
+    if (cd.change().isNew()) {
+      insertIndex = OPEN_CHANGES;
+      deleteIndex = CLOSED_CHANGES;
+    } else {
+      insertIndex = CLOSED_CHANGES;
+      deleteIndex = OPEN_CHANGES;
     }
 
     ElasticQueryAdapter adapter = client.adapter();
@@ -139,7 +128,7 @@
     Response response = postRequest(uri, bulk, getRefreshParam());
     int statusCode = response.getStatusLine().getStatusCode();
     if (statusCode != HttpStatus.SC_OK) {
-      throw new IOException(
+      throw new StorageException(
           String.format(
               "Failed to replace change %s in index %s: %s", cd.getId(), indexName, statusCode));
     }
@@ -185,7 +174,7 @@
   }
 
   @Override
-  protected String getDeleteActions(Id c) {
+  protected String getDeleteActions(Change.Id c) {
     if (!client.adapter().useV5Type()) {
       return delete(client.adapter().getType(), c);
     }
@@ -218,21 +207,24 @@
       int id = source.get(ChangeField.LEGACY_ID.getName()).getAsInt();
       // IndexUtils#changeFields ensures either CHANGE or PROJECT is always present.
       String projectName = requireNonNull(source.get(ChangeField.PROJECT.getName()).getAsString());
-      return changeDataFactory.create(
-          db.get(), new Project.NameKey(projectName), new Change.Id(id));
+      return changeDataFactory.create(new Project.NameKey(projectName), new Change.Id(id));
     }
 
     ChangeData cd =
-        changeDataFactory.create(db.get(), CHANGE_CODEC.decode(decodeBase64(c.getAsString())));
+        changeDataFactory.create(
+            parseProtoFrom(decodeBase64(c.getAsString()), ChangeProtoConverter.INSTANCE));
 
     // Any decoding that is done here must also be done in {@link LuceneChangeIndex}.
 
     // Patch sets.
-    cd.setPatchSets(decodeProtos(source, ChangeField.PATCH_SET.getName(), PATCH_SET_CODEC));
+    cd.setPatchSets(
+        decodeProtos(source, ChangeField.PATCH_SET.getName(), PatchSetProtoConverter.INSTANCE));
 
     // Approvals.
     if (source.get(ChangeField.APPROVAL.getName()) != null) {
-      cd.setCurrentApprovals(decodeProtos(source, ChangeField.APPROVAL.getName(), APPROVAL_CODEC));
+      cd.setCurrentApprovals(
+          decodeProtos(
+              source, ChangeField.APPROVAL.getName(), PatchSetApprovalProtoConverter.INSTANCE));
     } else if (fields.contains(ChangeField.APPROVAL.getName())) {
       cd.setCurrentApprovals(Collections.emptyList());
     }
diff --git a/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java b/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
index e74f208..ecda1ee 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.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.exceptions.StorageException;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.query.DataSource;
@@ -36,7 +37,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
-import java.io.IOException;
 import java.util.Set;
 import org.apache.http.HttpStatus;
 import org.elasticsearch.client.Response;
@@ -71,7 +71,7 @@
   }
 
   @Override
-  public void replace(InternalGroup group) throws IOException {
+  public void replace(InternalGroup group) {
     BulkRequest bulk =
         new IndexRequest(getId(group), indexName, type, client.adapter())
             .add(new UpdateRequest<>(schema, group));
@@ -80,7 +80,7 @@
     Response response = postRequest(uri, bulk, getRefreshParam());
     int statusCode = response.getStatusLine().getStatusCode();
     if (statusCode != HttpStatus.SC_OK) {
-      throw new IOException(
+      throw new StorageException(
           String.format(
               "Failed to replace group %s in index %s: %s",
               group.getGroupUUID().get(), indexName, statusCode));
diff --git a/java/com/google/gerrit/elasticsearch/ElasticIndexVersionManager.java b/java/com/google/gerrit/elasticsearch/ElasticIndexVersionManager.java
index 8011efa..b9d86d5 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticIndexVersionManager.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticIndexVersionManager.java
@@ -17,7 +17,6 @@
 import com.google.common.base.Strings;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
-import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.index.Index;
 import com.google.gerrit.index.IndexDefinition;
 import com.google.gerrit.index.Schema;
@@ -26,6 +25,7 @@
 import com.google.gerrit.server.index.GerritIndexStatus;
 import com.google.gerrit.server.index.OnlineUpgradeListener;
 import com.google.gerrit.server.index.VersionManager;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -45,7 +45,7 @@
   ElasticIndexVersionManager(
       @GerritServerConfig Config cfg,
       SitePaths sitePaths,
-      DynamicSet<OnlineUpgradeListener> listeners,
+      PluginSetContext<OnlineUpgradeListener> listeners,
       Collection<IndexDefinition<?, ?, ?>> defs,
       ElasticIndexVersionDiscovery versionDiscovery) {
     super(sitePaths, listeners, defs, VersionManager.getOnlineUpgrade(cfg));
diff --git a/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java b/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java
index 8510559..daf3702 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.exceptions.StorageException;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.project.ProjectData;
@@ -36,7 +37,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
-import java.io.IOException;
 import java.util.Set;
 import org.apache.http.HttpStatus;
 import org.elasticsearch.client.Response;
@@ -71,7 +71,7 @@
   }
 
   @Override
-  public void replace(ProjectData projectState) throws IOException {
+  public void replace(ProjectData projectState) {
     BulkRequest bulk =
         new IndexRequest(projectState.getProject().getName(), indexName, type, client.adapter())
             .add(new UpdateRequest<>(schema, projectState));
@@ -80,7 +80,7 @@
     Response response = postRequest(uri, bulk, getRefreshParam());
     int statusCode = response.getStatusLine().getStatusCode();
     if (statusCode != HttpStatus.SC_OK) {
-      throw new IOException(
+      throw new StorageException(
           String.format(
               "Failed to replace project %s in index %s: %s",
               projectState.getProject().getName(), indexName, statusCode));
diff --git a/java/com/google/gerrit/exceptions/BUILD b/java/com/google/gerrit/exceptions/BUILD
new file mode 100644
index 0000000..e08c3fd
--- /dev/null
+++ b/java/com/google/gerrit/exceptions/BUILD
@@ -0,0 +1,8 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
+java_library(
+    name = "exceptions",
+    srcs = glob(["*.java"]),
+    visibility = ["//visibility:public"],
+    deps = ["//java/com/google/gerrit/reviewdb:server"],
+)
diff --git a/java/com/google/gerrit/exceptions/DuplicateKeyException.java b/java/com/google/gerrit/exceptions/DuplicateKeyException.java
new file mode 100644
index 0000000..d052450
--- /dev/null
+++ b/java/com/google/gerrit/exceptions/DuplicateKeyException.java
@@ -0,0 +1,28 @@
+// Copyright 2009 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.gerrit.exceptions;
+
+/** Indicates one or more entities were concurrently inserted with the same key. */
+public class DuplicateKeyException extends StorageException {
+  private static final long serialVersionUID = 1L;
+
+  public DuplicateKeyException(String msg) {
+    super(msg);
+  }
+
+  public DuplicateKeyException(String msg, Throwable why) {
+    super(msg, why);
+  }
+}
diff --git a/java/com/google/gerrit/exceptions/EmailException.java b/java/com/google/gerrit/exceptions/EmailException.java
new file mode 100644
index 0000000..a278eed
--- /dev/null
+++ b/java/com/google/gerrit/exceptions/EmailException.java
@@ -0,0 +1,29 @@
+// 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.exceptions;
+
+public class EmailException extends Exception {
+  private static final long serialVersionUID = 1L;
+
+  public static final String MESSAGE = "Mail Error: ";
+
+  public EmailException(String msg) {
+    super(MESSAGE + msg);
+  }
+
+  public EmailException(String msg, Throwable why) {
+    super(MESSAGE + msg, why);
+  }
+}
diff --git a/java/com/google/gerrit/exceptions/InvalidNameException.java b/java/com/google/gerrit/exceptions/InvalidNameException.java
new file mode 100644
index 0000000..4539500
--- /dev/null
+++ b/java/com/google/gerrit/exceptions/InvalidNameException.java
@@ -0,0 +1,30 @@
+// 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.exceptions;
+
+/** Error indicating the entity name is invalid as supplied. */
+public class InvalidNameException extends Exception {
+  private static final long serialVersionUID = 1L;
+
+  public static final String MESSAGE = "Invalid Name";
+
+  public InvalidNameException() {
+    super(MESSAGE);
+  }
+
+  public InvalidNameException(String invalidName) {
+    super(MESSAGE + ": " + invalidName);
+  }
+}
diff --git a/java/com/google/gerrit/exceptions/InvalidSshKeyException.java b/java/com/google/gerrit/exceptions/InvalidSshKeyException.java
new file mode 100644
index 0000000..dda80cf
--- /dev/null
+++ b/java/com/google/gerrit/exceptions/InvalidSshKeyException.java
@@ -0,0 +1,30 @@
+// 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.exceptions;
+
+/** Error indicating the SSH key string is invalid as supplied. */
+public class InvalidSshKeyException extends Exception {
+  private static final long serialVersionUID = 1L;
+
+  public static final String MESSAGE = "Invalid SSH Key";
+
+  public InvalidSshKeyException() {
+    super(MESSAGE);
+  }
+
+  public InvalidSshKeyException(Throwable cause) {
+    super(MESSAGE, cause);
+  }
+}
diff --git a/java/com/google/gerrit/exceptions/NameAlreadyUsedException.java b/java/com/google/gerrit/exceptions/NameAlreadyUsedException.java
new file mode 100644
index 0000000..df2631b
--- /dev/null
+++ b/java/com/google/gerrit/exceptions/NameAlreadyUsedException.java
@@ -0,0 +1,26 @@
+// 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.exceptions;
+
+/** Error indicating entity name is already taken by another entity. */
+public class NameAlreadyUsedException extends Exception {
+  private static final long serialVersionUID = 1L;
+
+  public static final String MESSAGE = "Name Already Used: ";
+
+  public NameAlreadyUsedException(String name) {
+    super(MESSAGE + name);
+  }
+}
diff --git a/java/com/google/gerrit/exceptions/NoSuchAccountException.java b/java/com/google/gerrit/exceptions/NoSuchAccountException.java
new file mode 100644
index 0000000..d753128
--- /dev/null
+++ b/java/com/google/gerrit/exceptions/NoSuchAccountException.java
@@ -0,0 +1,26 @@
+// 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.exceptions;
+
+/** Error indicating the account requested doesn't exist. */
+public class NoSuchAccountException extends Exception {
+  private static final long serialVersionUID = 1L;
+
+  public static final String MESSAGE = "Not Found: ";
+
+  public NoSuchAccountException(String who) {
+    super(MESSAGE + who);
+  }
+}
diff --git a/java/com/google/gerrit/exceptions/NoSuchEntityException.java b/java/com/google/gerrit/exceptions/NoSuchEntityException.java
new file mode 100644
index 0000000..c812a38
--- /dev/null
+++ b/java/com/google/gerrit/exceptions/NoSuchEntityException.java
@@ -0,0 +1,30 @@
+// 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.exceptions;
+
+/** Error indicating the entity requested doesn't exist. */
+public class NoSuchEntityException extends Exception {
+  private static final long serialVersionUID = 1L;
+
+  public static final String MESSAGE = "Not Found";
+
+  public NoSuchEntityException() {
+    super(MESSAGE);
+  }
+
+  public NoSuchEntityException(String message) {
+    super(message);
+  }
+}
diff --git a/java/com/google/gerrit/exceptions/NoSuchGroupException.java b/java/com/google/gerrit/exceptions/NoSuchGroupException.java
new file mode 100644
index 0000000..dca28cb
--- /dev/null
+++ b/java/com/google/gerrit/exceptions/NoSuchGroupException.java
@@ -0,0 +1,52 @@
+// 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.exceptions;
+
+import com.google.gerrit.reviewdb.client.AccountGroup;
+
+/** Indicates the account group does not exist. */
+public class NoSuchGroupException extends Exception {
+  private static final long serialVersionUID = 1L;
+
+  public static final String MESSAGE = "Group Not Found: ";
+
+  public NoSuchGroupException(AccountGroup.Id key) {
+    this(key, null);
+  }
+
+  public NoSuchGroupException(AccountGroup.UUID key) {
+    this(key, null);
+  }
+
+  public NoSuchGroupException(AccountGroup.Id key, Throwable why) {
+    super(MESSAGE + key.toString(), why);
+  }
+
+  public NoSuchGroupException(AccountGroup.UUID key, Throwable why) {
+    super(MESSAGE + key.toString(), why);
+  }
+
+  public NoSuchGroupException(AccountGroup.NameKey k, Throwable why) {
+    super(MESSAGE + k.toString(), why);
+  }
+
+  public NoSuchGroupException(String who) {
+    this(who, null);
+  }
+
+  public NoSuchGroupException(String who, Throwable why) {
+    super(MESSAGE + who, why);
+  }
+}
diff --git a/java/com/google/gerrit/exceptions/NotSignedInException.java b/java/com/google/gerrit/exceptions/NotSignedInException.java
new file mode 100644
index 0000000..1919dc39
--- /dev/null
+++ b/java/com/google/gerrit/exceptions/NotSignedInException.java
@@ -0,0 +1,26 @@
+// 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.exceptions;
+
+/** Error stating the user must be signed-in in order to perform this action. */
+public class NotSignedInException extends Exception {
+  private static final long serialVersionUID = 1L;
+
+  public static final String MESSAGE = "Not Signed In";
+
+  public NotSignedInException() {
+    super(MESSAGE);
+  }
+}
diff --git a/java/com/google/gerrit/exceptions/StorageException.java b/java/com/google/gerrit/exceptions/StorageException.java
new file mode 100644
index 0000000..a788fff
--- /dev/null
+++ b/java/com/google/gerrit/exceptions/StorageException.java
@@ -0,0 +1,44 @@
+// 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.gerrit.exceptions;
+
+/**
+ * Any read/write error in a storage layer.
+ *
+ * <p>This includes but is not limited to:
+ *
+ * <ul>
+ *   <li>NoteDb exceptions
+ *   <li>Secondary index exceptions
+ *   <li>{@code AccountPatchReviewStore} exceptions
+ *   <li>Wrapped JGit exceptions
+ *   <li>Other wrapped {@code IOException}s
+ * </ul>
+ */
+public class StorageException extends RuntimeException {
+  private static final long serialVersionUID = 1L;
+
+  public StorageException(String message) {
+    super(message);
+  }
+
+  public StorageException(String message, Throwable why) {
+    super(message, why);
+  }
+
+  public StorageException(Throwable why) {
+    super(why);
+  }
+}
diff --git a/java/com/google/gerrit/extensions/BUILD b/java/com/google/gerrit/extensions/BUILD
index 5ba510c..0022584 100644
--- a/java/com/google/gerrit/extensions/BUILD
+++ b/java/com/google/gerrit/extensions/BUILD
@@ -1,18 +1,8 @@
 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:gwt.bzl", "gwt_module")
 load("//tools/bzl:javadoc.bzl", "java_doc")
 
-EXT_API_SRCS = glob(["client/*.java"])
-
-gwt_module(
-    name = "client",
-    srcs = EXT_API_SRCS,
-    gwt_xml = "Extensions.gwt.xml",
-    visibility = ["//visibility:public"],
-)
-
 java_binary(
     name = "extension-api",
     main_class = "Dummy",
@@ -41,6 +31,7 @@
     deps = [
         "//java/com/google/gerrit/common:annotations",
         "//lib:guava",
+        "//lib/auto:auto-value-annotations",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
     ],
diff --git a/java/com/google/gerrit/extensions/Extensions.gwt.xml b/java/com/google/gerrit/extensions/Extensions.gwt.xml
deleted file mode 100644
index c857b60..0000000
--- a/java/com/google/gerrit/extensions/Extensions.gwt.xml
+++ /dev/null
@@ -1,18 +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.
--->
-<module>
-  <source path='client' />
-</module>
diff --git a/java/com/google/gerrit/extensions/annotations/ExportImpl.java b/java/com/google/gerrit/extensions/annotations/ExportImpl.java
deleted file mode 100644
index a3e72bc..0000000
--- a/java/com/google/gerrit/extensions/annotations/ExportImpl.java
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.extensions.annotations;
-
-import java.io.Serializable;
-import java.lang.annotation.Annotation;
-
-final class ExportImpl implements Export, Serializable {
-  private static final long serialVersionUID = 0;
-  private final String value;
-
-  ExportImpl(String value) {
-    this.value = value;
-  }
-
-  @Override
-  public Class<? extends Annotation> annotationType() {
-    return Export.class;
-  }
-
-  @Override
-  public String value() {
-    return value;
-  }
-
-  @Override
-  public int hashCode() {
-    return (127 * "value".hashCode()) ^ value.hashCode();
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    return o instanceof Export && value.equals(((Export) o).value());
-  }
-
-  @Override
-  public String toString() {
-    return "@" + Export.class.getName() + "(value=" + value + ")";
-  }
-}
diff --git a/java/com/google/gerrit/extensions/annotations/Exports.java b/java/com/google/gerrit/extensions/annotations/Exports.java
index 1295ea0..9b196b6 100644
--- a/java/com/google/gerrit/extensions/annotations/Exports.java
+++ b/java/com/google/gerrit/extensions/annotations/Exports.java
@@ -14,11 +14,14 @@
 
 package com.google.gerrit.extensions.annotations;
 
+import com.google.auto.value.AutoAnnotation;
+
 /** Static constructors for {@link Export} annotations. */
 public final class Exports {
   /** Create an annotation to export under a specific name. */
-  public static Export named(String name) {
-    return new ExportImpl(name);
+  @AutoAnnotation
+  public static Export named(String value) {
+    return new AutoAnnotation_Exports_named(value);
   }
 
   /** Create an annotation to export based on a cannonical class name. */
diff --git a/java/com/google/gerrit/extensions/api/access/CoreOrPluginProjectPermission.java b/java/com/google/gerrit/extensions/api/access/CoreOrPluginProjectPermission.java
new file mode 100644
index 0000000..de68987
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/access/CoreOrPluginProjectPermission.java
@@ -0,0 +1,18 @@
+// 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.api.access;
+
+/** A repository permission either defined in Gerrit core or a plugin. */
+public interface CoreOrPluginProjectPermission extends GerritPermission {}
diff --git a/java/com/google/gerrit/extensions/api/access/PluginProjectPermission.java b/java/com/google/gerrit/extensions/api/access/PluginProjectPermission.java
new file mode 100644
index 0000000..a62fc63
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/access/PluginProjectPermission.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.extensions.api.access;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.base.MoreObjects;
+import java.util.Objects;
+import java.util.regex.Pattern;
+
+/** Repository permissions defined by plugins. */
+public final class PluginProjectPermission implements CoreOrPluginProjectPermission {
+  public static final String PLUGIN_PERMISSION_NAME_PATTERN_STRING = "[a-zA-Z]+";
+  private static final Pattern PLUGIN_PERMISSION_PATTERN =
+      Pattern.compile("^" + PLUGIN_PERMISSION_NAME_PATTERN_STRING + "$");
+
+  private final String pluginName;
+  private final String permission;
+
+  public PluginProjectPermission(String pluginName, String permission) {
+    requireNonNull(pluginName, "pluginName");
+    requireNonNull(permission, "permission");
+    checkArgument(
+        isValidPluginPermissionName(permission), "invalid plugin permission name: ", permission);
+
+    this.pluginName = pluginName;
+    this.permission = permission;
+  }
+
+  public String pluginName() {
+    return pluginName;
+  }
+
+  public String permission() {
+    return permission;
+  }
+
+  @Override
+  public String describeForException() {
+    return permission + " for plugin " + pluginName;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(pluginName, permission);
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (other instanceof PluginProjectPermission) {
+      PluginProjectPermission b = (PluginProjectPermission) other;
+      return pluginName.equals(b.pluginName) && permission.equals(b.permission);
+    }
+    return false;
+  }
+
+  @Override
+  public String toString() {
+    return MoreObjects.toStringHelper(this)
+        .add("pluginName", pluginName)
+        .add("permission", permission)
+        .toString();
+  }
+
+  /**
+   * Checks if a given name is valid to be used for plugin permissions.
+   *
+   * @param name a name string.
+   * @return whether the name is valid as a plugin permission.
+   */
+  private static boolean isValidPluginPermissionName(String name) {
+    return PLUGIN_PERMISSION_PATTERN.matcher(name).matches();
+  }
+}
diff --git a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index 7a7e6ea..09d8068 100644
--- a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.api.changes;
 
+import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.ListChangesOption;
@@ -49,17 +50,21 @@
    * @return API for accessing the revision.
    * @throws RestApiException if an error occurred.
    */
-  RevisionApi current() throws RestApiException;
+  default RevisionApi current() throws RestApiException {
+    return revision("current");
+  }
 
   /**
    * Look up a revision of a change by number.
    *
    * @see #current()
    */
-  RevisionApi revision(int id) throws RestApiException;
+  default RevisionApi revision(int id) throws RestApiException {
+    return revision(Integer.toString(id));
+  }
 
   /**
-   * Look up a revision of a change by commit SHA-1.
+   * Look up a revision of a change by commit SHA-1 or other supported revision string.
    *
    * @see #current()
    */
@@ -79,15 +84,23 @@
    */
   ReviewerApi reviewer(String id) throws RestApiException;
 
-  void abandon() throws RestApiException;
+  default void abandon() throws RestApiException {
+    abandon(new AbandonInput());
+  }
 
   void abandon(AbandonInput in) throws RestApiException;
 
-  void restore() throws RestApiException;
+  default void restore() throws RestApiException {
+    restore(new RestoreInput());
+  }
 
   void restore(RestoreInput in) throws RestApiException;
 
-  void move(String destination) throws RestApiException;
+  default void move(String destination) throws RestApiException {
+    MoveInput in = new MoveInput();
+    in.destinationBranch = destination;
+    move(in);
+  }
 
   void move(MoveInput in) throws RestApiException;
 
@@ -136,7 +149,9 @@
    *
    * @see Changes#id(int)
    */
-  ChangeApi revert() throws RestApiException;
+  default ChangeApi revert() throws RestApiException {
+    return revert(new RevertInput());
+  }
 
   /**
    * Create a new change that reverts this change.
@@ -148,10 +163,17 @@
   /** Create a merge patch set for the change. */
   ChangeInfo createMergePatchSet(MergePatchSetInput in) throws RestApiException;
 
-  List<ChangeInfo> submittedTogether() throws RestApiException;
+  default List<ChangeInfo> submittedTogether() throws RestApiException {
+    SubmittedTogetherInfo info =
+        submittedTogether(
+            EnumSet.noneOf(ListChangesOption.class), EnumSet.noneOf(SubmittedTogetherOption.class));
+    return info.changes;
+  }
 
-  SubmittedTogetherInfo submittedTogether(EnumSet<SubmittedTogetherOption> options)
-      throws RestApiException;
+  default SubmittedTogetherInfo submittedTogether(EnumSet<SubmittedTogetherOption> options)
+      throws RestApiException {
+    return submittedTogether(EnumSet.noneOf(ListChangesOption.class), options);
+  }
 
   SubmittedTogetherInfo submittedTogether(
       EnumSet<ListChangesOption> listOptions, EnumSet<SubmittedTogetherOption> submitOptions)
@@ -159,10 +181,14 @@
 
   /** Publishes a draft change. */
   @Deprecated
-  void publish() throws RestApiException;
+  default void publish() {
+    throw new UnsupportedOperationException("draft workflow is discontinued");
+  }
 
   /** Rebase the current revision of a change using default options. */
-  void rebase() throws RestApiException;
+  default void rebase() throws RestApiException {
+    rebase(new RebaseInput());
+  }
 
   /** Rebase the current revision of a change. */
   void rebase(RebaseInput in) throws RestApiException;
@@ -176,20 +202,37 @@
 
   IncludedInInfo includedIn() throws RestApiException;
 
-  AddReviewerResult addReviewer(AddReviewerInput in) throws RestApiException;
+  default AddReviewerResult addReviewer(String reviewer) throws RestApiException {
+    AddReviewerInput in = new AddReviewerInput();
+    in.reviewer = reviewer;
+    return addReviewer(in);
+  }
 
-  AddReviewerResult addReviewer(String in) throws RestApiException;
+  AddReviewerResult addReviewer(AddReviewerInput in) throws RestApiException;
 
   SuggestedReviewersRequest suggestReviewers() throws RestApiException;
 
-  SuggestedReviewersRequest suggestReviewers(String query) throws RestApiException;
+  default SuggestedReviewersRequest suggestReviewers(String query) throws RestApiException {
+    return suggestReviewers().withQuery(query);
+  }
 
   /**
    * Retrieve reviewers ({@code ReviewerState.REVIEWER} and {@code ReviewerState.CC}) on the change.
    */
   List<ReviewerInfo> reviewers() throws RestApiException;
 
-  ChangeInfo get(EnumSet<ListChangesOption> options) throws RestApiException;
+  ChangeInfo get(
+      EnumSet<ListChangesOption> options, ImmutableListMultimap<String, String> pluginOptions)
+      throws RestApiException;
+
+  default ChangeInfo get(ImmutableListMultimap<String, String> pluginOptions)
+      throws RestApiException {
+    return get(EnumSet.noneOf(ListChangesOption.class), pluginOptions);
+  }
+
+  default ChangeInfo get(EnumSet<ListChangesOption> options) throws RestApiException {
+    return get(options, ImmutableListMultimap.of());
+  }
 
   default ChangeInfo get(Iterable<ListChangesOption> options) throws RestApiException {
     return get(Sets.newEnumSet(options, ListChangesOption.class));
@@ -207,10 +250,16 @@
    *   <li>{@code SKIP_MERGEABLE} is omitted, so the {@code mergeable} bit <em>is</em> set.
    * </ul>
    */
-  ChangeInfo get() throws RestApiException;
+  default ChangeInfo get() throws RestApiException {
+    return get(
+        EnumSet.complementOf(
+            EnumSet.of(ListChangesOption.CHECK, ListChangesOption.SKIP_MERGEABLE)));
+  }
 
   /** {@link #get(ListChangesOption...)} with no options included. */
-  ChangeInfo info() throws RestApiException;
+  default ChangeInfo info() throws RestApiException {
+    return get(EnumSet.noneOf(ListChangesOption.class));
+  }
 
   /**
    * Retrieve change edit when exists.
@@ -219,7 +268,9 @@
    *     ChangeEditApi#get()}.
    */
   @Deprecated
-  EditInfo getEdit() throws RestApiException;
+  default EditInfo getEdit() throws RestApiException {
+    return edit().get().orElse(null);
+  }
 
   /**
    * Provides access to an API regarding the change edit of this change.
@@ -230,7 +281,11 @@
   ChangeEditApi edit() throws RestApiException;
 
   /** Create a new patch set with a new commit message. */
-  void setMessage(String message) throws RestApiException;
+  default void setMessage(String message) throws RestApiException {
+    CommitMessageInput in = new CommitMessageInput();
+    in.message = message;
+    setMessage(in);
+  }
 
   /** Create a new patch set with a new commit message. */
   void setMessage(CommitMessageInput in) throws RestApiException;
@@ -330,9 +385,8 @@
   /**
    * Look up a change message of a change by its id.
    *
-   * @param id the id of the change message. Note that in NoteDb, this id is the {@code ObjectId} of
-   *     a commit on the change meta branch. In ReviewDb, it's a UUID generated randomly. That means
-   *     a change message id could be different between NoteDb and ReviewDb.
+   * @param id the id of the change message. In NoteDb, this id is the {@code ObjectId} of a commit
+   *     on the change meta branch.
    * @return API for accessing a change message.
    * @throws RestApiException if the id is invalid.
    */
@@ -374,16 +428,6 @@
     }
 
     @Override
-    public RevisionApi current() throws RestApiException {
-      throw new NotImplementedException();
-    }
-
-    @Override
-    public RevisionApi revision(int id) throws RestApiException {
-      throw new NotImplementedException();
-    }
-
-    @Override
     public ReviewerApi reviewer(String id) throws RestApiException {
       throw new NotImplementedException();
     }
@@ -394,31 +438,16 @@
     }
 
     @Override
-    public void abandon() throws RestApiException {
-      throw new NotImplementedException();
-    }
-
-    @Override
     public void abandon(AbandonInput in) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void restore() throws RestApiException {
-      throw new NotImplementedException();
-    }
-
-    @Override
     public void restore(RestoreInput in) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void move(String destination) throws RestApiException {
-      throw new NotImplementedException();
-    }
-
-    @Override
     public void move(MoveInput in) throws RestApiException {
       throw new NotImplementedException();
     }
@@ -439,27 +468,11 @@
     }
 
     @Override
-    public ChangeApi revert() throws RestApiException {
-      throw new NotImplementedException();
-    }
-
-    @Override
     public ChangeApi revert(RevertInput in) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void publish() throws RestApiException {
-      throw new NotImplementedException();
-    }
-
-    @Deprecated
-    @Override
-    public void rebase() throws RestApiException {
-      throw new NotImplementedException();
-    }
-
-    @Override
     public void rebase(RebaseInput in) throws RestApiException {
       throw new NotImplementedException();
     }
@@ -490,11 +503,6 @@
     }
 
     @Override
-    public AddReviewerResult addReviewer(String in) throws RestApiException {
-      throw new NotImplementedException();
-    }
-
-    @Override
     public SuggestedReviewersRequest suggestReviewers() throws RestApiException {
       throw new NotImplementedException();
     }
@@ -510,22 +518,9 @@
     }
 
     @Override
-    public ChangeInfo get(EnumSet<ListChangesOption> options) throws RestApiException {
-      throw new NotImplementedException();
-    }
-
-    @Override
-    public ChangeInfo get() throws RestApiException {
-      throw new NotImplementedException();
-    }
-
-    @Override
-    public ChangeInfo info() throws RestApiException {
-      throw new NotImplementedException();
-    }
-
-    @Override
-    public void setMessage(String message) throws RestApiException {
+    public ChangeInfo get(
+        EnumSet<ListChangesOption> options, ImmutableListMultimap<String, String> pluginOptions)
+        throws RestApiException {
       throw new NotImplementedException();
     }
 
@@ -535,11 +530,6 @@
     }
 
     @Override
-    public EditInfo getEdit() throws RestApiException {
-      throw new NotImplementedException();
-    }
-
-    @Override
     public ChangeEditApi edit() throws RestApiException {
       throw new NotImplementedException();
     }
diff --git a/java/com/google/gerrit/extensions/api/changes/Changes.java b/java/com/google/gerrit/extensions/api/changes/Changes.java
index dc0a250..bcb49de1 100644
--- a/java/com/google/gerrit/extensions/api/changes/Changes.java
+++ b/java/com/google/gerrit/extensions/api/changes/Changes.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.extensions.api.changes;
 
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeInput;
@@ -75,7 +77,9 @@
     private String query;
     private int limit;
     private int start;
+    private boolean isNoLimit;
     private EnumSet<ListChangesOption> options = EnumSet.noneOf(ListChangesOption.class);
+    private ListMultimap<String, String> pluginOptions = ArrayListMultimap.create();
 
     public abstract List<ChangeInfo> get() throws RestApiException;
 
@@ -89,6 +93,11 @@
       return this;
     }
 
+    public QueryRequest withNoLimit() {
+      this.isNoLimit = true;
+      return this;
+    }
+
     public QueryRequest withStart(int start) {
       this.start = start;
       return this;
@@ -112,6 +121,18 @@
       return this;
     }
 
+    /** Set a plugin option on the request, appending to existing options. */
+    public QueryRequest withPluginOption(String name, String value) {
+      this.pluginOptions.put(name, value);
+      return this;
+    }
+
+    /** Set a plugin option on the request, replacing existing options. */
+    public QueryRequest withPluginOptions(ListMultimap<String, String> options) {
+      this.pluginOptions = ArrayListMultimap.create(options);
+      return this;
+    }
+
     public String getQuery() {
       return query;
     }
@@ -120,6 +141,10 @@
       return limit;
     }
 
+    public boolean getNoLimit() {
+      return isNoLimit;
+    }
+
     public int getStart() {
       return start;
     }
@@ -128,6 +153,10 @@
       return options;
     }
 
+    public ListMultimap<String, String> getPluginOptions() {
+      return pluginOptions;
+    }
+
     @Override
     public String toString() {
       StringBuilder sb = new StringBuilder(getClass().getSimpleName()).append('{').append(query);
@@ -140,7 +169,11 @@
       if (!options.isEmpty()) {
         sb.append("options=").append(options);
       }
-      return sb.append('}').toString();
+      sb.append('}');
+      if (isNoLimit == true) {
+        sb.append(" --no-limit");
+      }
+      return sb.toString();
     }
   }
 
diff --git a/java/com/google/gerrit/extensions/api/changes/FileApi.java b/java/com/google/gerrit/extensions/api/changes/FileApi.java
index 39cf2b7..8d9b2d5 100644
--- a/java/com/google/gerrit/extensions/api/changes/FileApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/FileApi.java
@@ -15,10 +15,12 @@
 package com.google.gerrit.extensions.api.changes;
 
 import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
+import com.google.gerrit.extensions.common.BlameInfo;
 import com.google.gerrit.extensions.common.DiffInfo;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import java.util.List;
 import java.util.OptionalInt;
 
 public interface FileApi {
@@ -42,6 +44,12 @@
   /** Set the file reviewed or not reviewed */
   void setReviewed(boolean reviewed) throws RestApiException;
 
+  /**
+   * Creates a request to retrieve the blame information. On the returned request formatting options
+   * for the blame request can be set.
+   */
+  BlameRequest blameRequest() throws RestApiException;
+
   abstract class DiffRequest {
     private String base;
     private Integer context;
@@ -97,6 +105,21 @@
     }
   }
 
+  abstract class BlameRequest {
+    private boolean forBase;
+
+    public abstract List<BlameInfo> get() throws RestApiException;
+
+    public BlameRequest forBase(boolean forBase) {
+      this.forBase = forBase;
+      return this;
+    }
+
+    public boolean isForBase() {
+      return forBase;
+    }
+  }
+
   /**
    * A default implementation which allows source compatibility when adding new methods to the
    * interface.
@@ -131,5 +154,10 @@
     public void setReviewed(boolean reviewed) throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public BlameRequest blameRequest() throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
index 0eaa75e..7d356bf 100644
--- a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.extensions.api.changes;
 
 import com.google.common.collect.ListMultimap;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ActionInfo;
 import com.google.gerrit.extensions.common.ApprovalInfo;
@@ -36,7 +37,9 @@
 
 public interface RevisionApi {
   @Deprecated
-  void delete() throws RestApiException;
+  default void delete() {
+    throw new UnsupportedOperationException("draft workflow is discontinued");
+  }
 
   String description() throws RestApiException;
 
@@ -44,22 +47,32 @@
 
   ReviewResult review(ReviewInput in) throws RestApiException;
 
-  void submit() throws RestApiException;
+  default void submit() throws RestApiException {
+    SubmitInput in = new SubmitInput();
+    submit(in);
+  }
 
   void submit(SubmitInput in) throws RestApiException;
 
-  BinaryResult submitPreview() throws RestApiException;
+  default BinaryResult submitPreview() throws RestApiException {
+    return submitPreview("zip");
+  }
 
   BinaryResult submitPreview(String format) throws RestApiException;
 
   @Deprecated
-  void publish() throws RestApiException;
+  default void publish() {
+    throw new UnsupportedOperationException("draft workflow is discontinued");
+  }
 
   ChangeApi cherryPick(CherryPickInput in) throws RestApiException;
 
   CherryPickChangeInfo cherryPickAsInfo(CherryPickInput in) throws RestApiException;
 
-  ChangeApi rebase() throws RestApiException;
+  default ChangeApi rebase() throws RestApiException {
+    RebaseInput in = new RebaseInput();
+    return rebase(in);
+  }
 
   ChangeApi rebase(RebaseInput in) throws RestApiException;
 
@@ -71,9 +84,11 @@
 
   Set<String> reviewed() throws RestApiException;
 
-  Map<String, FileInfo> files() throws RestApiException;
+  default Map<String, FileInfo> files() throws RestApiException {
+    return files(null);
+  }
 
-  Map<String, FileInfo> files(String base) throws RestApiException;
+  Map<String, FileInfo> files(@Nullable String base) throws RestApiException;
 
   Map<String, FileInfo> files(int parentNum) throws RestApiException;
 
@@ -170,33 +185,16 @@
    * interface.
    */
   class NotImplemented implements RevisionApi {
-    @Deprecated
-    @Override
-    public void delete() throws RestApiException {
-      throw new NotImplementedException();
-    }
-
     @Override
     public ReviewResult review(ReviewInput in) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void submit() throws RestApiException {
-      throw new NotImplementedException();
-    }
-
-    @Override
     public void submit(SubmitInput in) throws RestApiException {
       throw new NotImplementedException();
     }
 
-    @Deprecated
-    @Override
-    public void publish() throws RestApiException {
-      throw new NotImplementedException();
-    }
-
     @Override
     public ChangeApi cherryPick(CherryPickInput in) throws RestApiException {
       throw new NotImplementedException();
@@ -208,11 +206,6 @@
     }
 
     @Override
-    public ChangeApi rebase() throws RestApiException {
-      throw new NotImplementedException();
-    }
-
-    @Override
     public ChangeApi rebase(RebaseInput in) throws RestApiException {
       throw new NotImplementedException();
     }
@@ -258,11 +251,6 @@
     }
 
     @Override
-    public Map<String, FileInfo> files() throws RestApiException {
-      throw new NotImplementedException();
-    }
-
-    @Override
     public List<String> queryFiles(String query) throws RestApiException {
       throw new NotImplementedException();
     }
@@ -353,11 +341,6 @@
     }
 
     @Override
-    public BinaryResult submitPreview() throws RestApiException {
-      throw new NotImplementedException();
-    }
-
-    @Override
     public BinaryResult submitPreview(String format) throws RestApiException {
       throw new NotImplementedException();
     }
diff --git a/java/com/google/gerrit/extensions/api/config/Server.java b/java/com/google/gerrit/extensions/api/config/Server.java
index 5ec63af..70d1bff 100644
--- a/java/com/google/gerrit/extensions/api/config/Server.java
+++ b/java/com/google/gerrit/extensions/api/config/Server.java
@@ -20,6 +20,8 @@
 import com.google.gerrit.extensions.common.ServerInfo;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.webui.TopMenu;
+import java.util.List;
 
 public interface Server {
   /** @return Version of server. */
@@ -41,6 +43,8 @@
 
   ConsistencyCheckInfo checkConsistency(ConsistencyCheckInput in) throws RestApiException;
 
+  List<TopMenu.MenuEntry> topMenus() throws RestApiException;
+
   /**
    * A default implementation which allows source compatibility when adding new methods to the
    * interface.
@@ -93,5 +97,10 @@
     public ConsistencyCheckInfo checkConsistency(ConsistencyCheckInput in) throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public List<TopMenu.MenuEntry> topMenus() throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/java/com/google/gerrit/extensions/api/groups/GroupApi.java b/java/com/google/gerrit/extensions/api/groups/GroupApi.java
index fe85eaa..067f120 100644
--- a/java/com/google/gerrit/extensions/api/groups/GroupApi.java
+++ b/java/com/google/gerrit/extensions/api/groups/GroupApi.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.extensions.common.GroupOptionsInfo;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import java.util.Arrays;
 import java.util.List;
 
 public interface GroupApi {
@@ -97,7 +98,18 @@
    *     com.google.gerrit.extensions.api.accounts.Accounts#id(String)}
    * @throws RestApiException
    */
-  void addMembers(String... members) throws RestApiException;
+  void addMembers(List<String> members) throws RestApiException;
+
+  /**
+   * Add members to a group.
+   *
+   * @param members list of member identifiers, in any format accepted by {@link
+   *     com.google.gerrit.extensions.api.accounts.Accounts#id(String)}
+   * @throws RestApiException
+   */
+  default void addMembers(String... members) throws RestApiException {
+    addMembers(Arrays.asList(members));
+  }
 
   /**
    * Remove members from a group.
@@ -106,7 +118,18 @@
    *     com.google.gerrit.extensions.api.accounts.Accounts#id(String)}
    * @throws RestApiException
    */
-  void removeMembers(String... members) throws RestApiException;
+  void removeMembers(List<String> members) throws RestApiException;
+
+  /**
+   * Remove members from a group.
+   *
+   * @param members list of member identifiers, in any format accepted by {@link
+   *     com.google.gerrit.extensions.api.accounts.Accounts#id(String)}
+   * @throws RestApiException
+   */
+  default void removeMembers(String... members) throws RestApiException {
+    removeMembers(Arrays.asList(members));
+  }
 
   /**
    * Lists the subgroups of this group.
@@ -122,7 +145,17 @@
    * @param groups list of group identifiers, in any format accepted by {@link Groups#id(String)}
    * @throws RestApiException
    */
-  void addGroups(String... groups) throws RestApiException;
+  void addGroups(List<String> groups) throws RestApiException;
+
+  /**
+   * Adds subgroups to this group.
+   *
+   * @param groups list of group identifiers, in any format accepted by {@link Groups#id(String)}
+   * @throws RestApiException
+   */
+  default void addGroups(String... groups) throws RestApiException {
+    addGroups(Arrays.asList(groups));
+  }
 
   /**
    * Removes subgroups from this group.
@@ -130,7 +163,17 @@
    * @param groups list of group identifiers, in any format accepted by {@link Groups#id(String)}
    * @throws RestApiException
    */
-  void removeGroups(String... groups) throws RestApiException;
+  void removeGroups(List<String> groups) throws RestApiException;
+
+  /**
+   * Removes subgroups from this group.
+   *
+   * @param groups list of group identifiers, in any format accepted by {@link Groups#id(String)}
+   * @throws RestApiException
+   */
+  default void removeGroups(String... groups) throws RestApiException {
+    removeGroups(Arrays.asList(groups));
+  }
 
   /**
    * Returns the audit log of the group.
@@ -215,12 +258,12 @@
     }
 
     @Override
-    public void addMembers(String... members) throws RestApiException {
+    public void addMembers(List<String> members) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void removeMembers(String... members) throws RestApiException {
+    public void removeMembers(List<String> members) throws RestApiException {
       throw new NotImplementedException();
     }
 
@@ -230,12 +273,12 @@
     }
 
     @Override
-    public void addGroups(String... groups) throws RestApiException {
+    public void addGroups(List<String> groups) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void removeGroups(String... groups) throws RestApiException {
+    public void removeGroups(List<String> groups) throws RestApiException {
       throw new NotImplementedException();
     }
 
diff --git a/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java b/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
index 08ba486..fb2a0fe 100644
--- a/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
+++ b/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
@@ -48,7 +48,6 @@
   public Map<String, ActionInfo> actions;
 
   public Map<String, CommentLinkInfo> commentlinks;
-  public ThemeInfo theme;
 
   public Map<String, List<String>> extensionPanelNames;
 
diff --git a/java/com/google/gerrit/extensions/api/projects/Projects.java b/java/com/google/gerrit/extensions/api/projects/Projects.java
index 85ec26f..34ca7d4 100644
--- a/java/com/google/gerrit/extensions/api/projects/Projects.java
+++ b/java/com/google/gerrit/extensions/api/projects/Projects.java
@@ -80,7 +80,6 @@
   abstract class ListRequest {
     public enum FilterType {
       CODE,
-      PARENT_CANDIDATES,
       PERMISSIONS,
       ALL
     }
diff --git a/java/com/google/gerrit/extensions/api/projects/ThemeInfo.java b/java/com/google/gerrit/extensions/api/projects/ThemeInfo.java
deleted file mode 100644
index d5d520f..0000000
--- a/java/com/google/gerrit/extensions/api/projects/ThemeInfo.java
+++ /dev/null
@@ -1,29 +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.extensions.api.projects;
-
-public class ThemeInfo {
-  public static final ThemeInfo INHERIT = new ThemeInfo(null, null, null);
-
-  public final String css;
-  public final String header;
-  public final String footer;
-
-  public ThemeInfo(String css, String header, String footer) {
-    this.css = css;
-    this.header = header;
-    this.footer = footer;
-  }
-}
diff --git a/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java b/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java
index 0d5bdfa..652abcc 100644
--- a/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java
+++ b/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java
@@ -60,7 +60,6 @@
   public Boolean hideEmptyPane;
   public Boolean matchBrackets;
   public Boolean lineWrapping;
-  public Theme theme;
   public Whitespace ignoreWhitespace;
   public Boolean retainHeader;
   public Boolean skipDeleted;
@@ -75,7 +74,6 @@
     i.lineLength = DEFAULT_LINE_LENGTH;
     i.cursorBlinkRate = 0;
     i.ignoreWhitespace = Whitespace.IGNORE_NONE;
-    i.theme = Theme.DEFAULT;
     i.expandAllComments = false;
     i.intralineDifference = true;
     i.manualReview = false;
diff --git a/java/com/google/gerrit/extensions/client/EditPreferencesInfo.java b/java/com/google/gerrit/extensions/client/EditPreferencesInfo.java
index 7ab22e1..6672cb1 100644
--- a/java/com/google/gerrit/extensions/client/EditPreferencesInfo.java
+++ b/java/com/google/gerrit/extensions/client/EditPreferencesInfo.java
@@ -30,8 +30,6 @@
   public Boolean indentWithTabs;
   public Boolean autoCloseBrackets;
   public Boolean showBase;
-  public Theme theme;
-  public KeyMapType keyMapType;
 
   public static EditPreferencesInfo defaults() {
     EditPreferencesInfo i = new EditPreferencesInfo();
@@ -49,8 +47,6 @@
     i.indentWithTabs = false;
     i.autoCloseBrackets = false;
     i.showBase = false;
-    i.theme = Theme.DEFAULT;
-    i.keyMapType = KeyMapType.DEFAULT;
     return i;
   }
 }
diff --git a/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java b/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
index 1f16d8d..8eb54e1 100644
--- a/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
+++ b/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.extensions.client;
 
 import java.util.List;
-import java.util.Map;
 
 /** Preferences about a single user. */
 public class GeneralPreferencesInfo {
@@ -137,8 +136,6 @@
   public Boolean useFlashClipboard;
   /** Type of download URL the user prefers to use. */
   public String downloadScheme;
-  /** Type of download command the user prefers to use. */
-  public DownloadCommand downloadCommand;
 
   public DateFormat dateFormat;
   public TimeFormat timeFormat;
@@ -153,7 +150,6 @@
   public Boolean signedOffBy;
   public List<MenuItem> my;
   public List<String> changeTable;
-  public Map<String, String> urlAliases;
   public EmailStrategy emailStrategy;
   public EmailFormat emailFormat;
   public DefaultBase defaultBaseForMerges;
@@ -215,7 +211,6 @@
     p.emailFormat = EmailFormat.HTML_PLAINTEXT;
     p.reviewCategoryStrategy = ReviewCategoryStrategy.NONE;
     p.downloadScheme = null;
-    p.downloadCommand = DownloadCommand.CHECKOUT;
     p.dateFormat = DateFormat.STD;
     p.timeFormat = TimeFormat.HHMM_12;
     p.expandInlineDiffs = false;
diff --git a/java/com/google/gerrit/extensions/client/KeyMapType.java b/java/com/google/gerrit/extensions/client/KeyMapType.java
deleted file mode 100644
index 9c85ac7..0000000
--- a/java/com/google/gerrit/extensions/client/KeyMapType.java
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.extensions.client;
-
-public enum KeyMapType {
-  DEFAULT,
-  EMACS,
-  SUBLIME,
-  VIM
-}
diff --git a/java/com/google/gerrit/extensions/client/ListAccountsOption.java b/java/com/google/gerrit/extensions/client/ListAccountsOption.java
index b5e9004..2274d5d 100644
--- a/java/com/google/gerrit/extensions/client/ListAccountsOption.java
+++ b/java/com/google/gerrit/extensions/client/ListAccountsOption.java
@@ -14,11 +14,8 @@
 
 package com.google.gerrit.extensions.client;
 
-import java.util.EnumSet;
-import java.util.Set;
-
 /** Output options available for retrieval of account details. */
-public enum ListAccountsOption {
+public enum ListAccountsOption implements ListOption {
   /** Return detailed account properties. */
   DETAILS(0),
 
@@ -31,32 +28,8 @@
     this.value = v;
   }
 
+  @Override
   public int getValue() {
     return value;
   }
-
-  public static EnumSet<ListAccountsOption> fromBits(int v) {
-    EnumSet<ListAccountsOption> r = EnumSet.noneOf(ListAccountsOption.class);
-    for (ListAccountsOption o : ListAccountsOption.values()) {
-      if ((v & (1 << o.value)) != 0) {
-        r.add(o);
-        v &= ~(1 << o.value);
-      }
-      if (v == 0) {
-        return r;
-      }
-    }
-    if (v != 0) {
-      throw new IllegalArgumentException("unknown " + Integer.toHexString(v));
-    }
-    return r;
-  }
-
-  public static int toBits(Set<ListAccountsOption> set) {
-    int r = 0;
-    for (ListAccountsOption o : set) {
-      r |= 1 << o.value;
-    }
-    return r;
-  }
 }
diff --git a/java/com/google/gerrit/extensions/client/ListChangesOption.java b/java/com/google/gerrit/extensions/client/ListChangesOption.java
index 5e4a3a7..c842adc 100644
--- a/java/com/google/gerrit/extensions/client/ListChangesOption.java
+++ b/java/com/google/gerrit/extensions/client/ListChangesOption.java
@@ -14,11 +14,8 @@
 
 package com.google.gerrit.extensions.client;
 
-import java.util.EnumSet;
-import java.util.Set;
-
 /** Output options available for retrieval of change details. */
-public enum ListChangesOption {
+public enum ListChangesOption implements ListOption {
   LABELS(0),
   DETAILED_LABELS(8),
 
@@ -86,32 +83,8 @@
     this.value = v;
   }
 
+  @Override
   public int getValue() {
     return value;
   }
-
-  public static EnumSet<ListChangesOption> fromBits(int v) {
-    EnumSet<ListChangesOption> r = EnumSet.noneOf(ListChangesOption.class);
-    for (ListChangesOption o : ListChangesOption.values()) {
-      if ((v & (1 << o.value)) != 0) {
-        r.add(o);
-        v &= ~(1 << o.value);
-      }
-      if (v == 0) {
-        return r;
-      }
-    }
-    if (v != 0) {
-      throw new IllegalArgumentException("unknown " + Integer.toHexString(v));
-    }
-    return r;
-  }
-
-  public static int toBits(Set<ListChangesOption> set) {
-    int r = 0;
-    for (ListChangesOption o : set) {
-      r |= 1 << o.value;
-    }
-    return r;
-  }
 }
diff --git a/java/com/google/gerrit/extensions/client/ListGroupsOption.java b/java/com/google/gerrit/extensions/client/ListGroupsOption.java
index e95570f..a971226 100644
--- a/java/com/google/gerrit/extensions/client/ListGroupsOption.java
+++ b/java/com/google/gerrit/extensions/client/ListGroupsOption.java
@@ -14,10 +14,8 @@
 
 package com.google.gerrit.extensions.client;
 
-import java.util.EnumSet;
-
 /** Output options available when using {@code /groups/} RPCs. */
-public enum ListGroupsOption {
+public enum ListGroupsOption implements ListOption {
   /** Return information on the direct group members. */
   MEMBERS(0),
 
@@ -30,32 +28,8 @@
     this.value = v;
   }
 
+  @Override
   public int getValue() {
     return value;
   }
-
-  public static EnumSet<ListGroupsOption> fromBits(int v) {
-    EnumSet<ListGroupsOption> r = EnumSet.noneOf(ListGroupsOption.class);
-    for (ListGroupsOption o : ListGroupsOption.values()) {
-      if ((v & (1 << o.value)) != 0) {
-        r.add(o);
-        v &= ~(1 << o.value);
-      }
-      if (v == 0) {
-        return r;
-      }
-    }
-    if (v != 0) {
-      throw new IllegalArgumentException("unknown " + Integer.toHexString(v));
-    }
-    return r;
-  }
-
-  public static int toBits(EnumSet<ListGroupsOption> set) {
-    int r = 0;
-    for (ListGroupsOption o : set) {
-      r |= 1 << o.value;
-    }
-    return r;
-  }
 }
diff --git a/java/com/google/gerrit/extensions/client/ListOption.java b/java/com/google/gerrit/extensions/client/ListOption.java
new file mode 100644
index 0000000..e694c0e
--- /dev/null
+++ b/java/com/google/gerrit/extensions/client/ListOption.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.extensions.client;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.EnumSet;
+
+/** Enum that can be expressed as a bitset in query parameters. */
+public interface ListOption {
+  int getValue();
+
+  static <T extends Enum<T> & ListOption> EnumSet<T> fromBits(Class<T> clazz, int v) {
+    EnumSet<T> r = EnumSet.noneOf(clazz);
+    T[] values;
+    try {
+      @SuppressWarnings("unchecked")
+      T[] tmp = (T[]) clazz.getMethod("values").invoke(null);
+      values = tmp;
+    } catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
+      throw new IllegalStateException(e);
+    }
+    for (T o : values) {
+      if ((v & (1 << o.getValue())) != 0) {
+        r.add(o);
+        v &= ~(1 << o.getValue());
+      }
+      if (v == 0) {
+        return r;
+      }
+    }
+    if (v != 0) {
+      throw new IllegalArgumentException(
+          "unknown " + clazz.getName() + ": " + Integer.toHexString(v));
+    }
+    return r;
+  }
+}
diff --git a/java/com/google/gerrit/extensions/client/MenuItem.java b/java/com/google/gerrit/extensions/client/MenuItem.java
index 8375bba..0c7dd88 100644
--- a/java/com/google/gerrit/extensions/client/MenuItem.java
+++ b/java/com/google/gerrit/extensions/client/MenuItem.java
@@ -22,11 +22,6 @@
   public final String target;
   public final String id;
 
-  // Needed for GWT
-  public MenuItem() {
-    this(null, null, null, null);
-  }
-
   public MenuItem(String name, String url) {
     this(name, url, "_blank");
   }
diff --git a/java/com/google/gerrit/extensions/client/Theme.java b/java/com/google/gerrit/extensions/client/Theme.java
deleted file mode 100644
index d7a5b80..0000000
--- a/java/com/google/gerrit/extensions/client/Theme.java
+++ /dev/null
@@ -1,123 +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.extensions.client;
-
-public enum Theme {
-  // Light themes
-  DEFAULT,
-  DAY_3024,
-  DUOTONE_LIGHT,
-  BASE16_LIGHT,
-  ECLIPSE,
-  ELEGANT,
-  MDN_LIKE,
-  NEAT,
-  NEO,
-  PARAISO_LIGHT,
-  SOLARIZED,
-  TTCN,
-  XQ_LIGHT,
-  YETI,
-
-  // Dark themes
-  NIGHT_3024,
-  ABCDEF,
-  AMBIANCE,
-  BASE16_DARK,
-  BESPIN,
-  BLACKBOARD,
-  COBALT,
-  COLORFORTH,
-  DRACULA,
-  DUOTONE_DARK,
-  ERLANG_DARK,
-  HOPSCOTCH,
-  ICECODER,
-  ISOTOPE,
-  LESSER_DARK,
-  LIQUIBYTE,
-  MATERIAL,
-  MBO,
-  MIDNIGHT,
-  MONOKAI,
-  NIGHT,
-  PARAISO_DARK,
-  PASTEL_ON_DARK,
-  RAILSCASTS,
-  RUBYBLUE,
-  SETI,
-  THE_MATRIX,
-  TOMORROW_NIGHT_BRIGHT,
-  TOMORROW_NIGHT_EIGHTIES,
-  TWILIGHT,
-  VIBRANT_INK,
-  XQ_DARK,
-  ZENBURN;
-
-  public boolean isDark() {
-    switch (this) {
-      case ABCDEF:
-      case AMBIANCE:
-      case BASE16_DARK:
-      case BESPIN:
-      case BLACKBOARD:
-      case COBALT:
-      case COLORFORTH:
-      case DRACULA:
-      case DUOTONE_DARK:
-      case ERLANG_DARK:
-      case HOPSCOTCH:
-      case ICECODER:
-      case ISOTOPE:
-      case LESSER_DARK:
-      case LIQUIBYTE:
-      case MATERIAL:
-      case MBO:
-      case MIDNIGHT:
-      case MONOKAI:
-      case NIGHT:
-      case NIGHT_3024:
-      case PARAISO_DARK:
-      case PASTEL_ON_DARK:
-      case RAILSCASTS:
-      case RUBYBLUE:
-      case SETI:
-      case THE_MATRIX:
-      case TOMORROW_NIGHT_BRIGHT:
-      case TOMORROW_NIGHT_EIGHTIES:
-      case TWILIGHT:
-      case VIBRANT_INK:
-      case XQ_DARK:
-      case ZENBURN:
-        return true;
-      case BASE16_LIGHT:
-      case DEFAULT:
-      case DAY_3024:
-      case DUOTONE_LIGHT:
-      case ECLIPSE:
-      case ELEGANT:
-      case MDN_LIKE:
-      case NEAT:
-      case NEO:
-      case PARAISO_LIGHT:
-      case SOLARIZED:
-      case TTCN:
-      case XQ_LIGHT:
-      case YETI:
-      default:
-        return false;
-    }
-  }
-}
diff --git a/java/com/google/gerrit/extensions/client/UiType.java b/java/com/google/gerrit/extensions/client/UiType.java
deleted file mode 100644
index 0d9df39..0000000
--- a/java/com/google/gerrit/extensions/client/UiType.java
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.extensions.client;
-
-public enum UiType {
-  NONE,
-  GWT,
-  POLYGERRIT;
-
-  public static UiType parse(String str) {
-    if (str != null) {
-      for (UiType type : UiType.values()) {
-        if (type.name().equalsIgnoreCase(str)) {
-          return type;
-        }
-      }
-    }
-    return null;
-  }
-}
diff --git a/java/com/google/gerrit/extensions/common/ChangeInfo.java b/java/com/google/gerrit/extensions/common/ChangeInfo.java
index 945c239..9a739ef 100644
--- a/java/com/google/gerrit/extensions/common/ChangeInfo.java
+++ b/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -46,6 +46,7 @@
   public Boolean submittable;
   public Integer insertions;
   public Integer deletions;
+  public Integer totalCommentCount;
   public Integer unresolvedCommentCount;
   public Boolean isPrivate;
   public Boolean workInProgress;
diff --git a/java/com/google/gerrit/extensions/common/GerritInfo.java b/java/com/google/gerrit/extensions/common/GerritInfo.java
index ef1d679..5c462d9 100644
--- a/java/com/google/gerrit/extensions/common/GerritInfo.java
+++ b/java/com/google/gerrit/extensions/common/GerritInfo.java
@@ -14,9 +14,6 @@
 
 package com.google.gerrit.extensions.common;
 
-import com.google.gerrit.extensions.client.UiType;
-import java.util.Set;
-
 public class GerritInfo {
   public String allProjects;
   public String allUsers;
@@ -24,7 +21,5 @@
   public String docUrl;
   public Boolean editGpgKeys;
   public String reportBugUrl;
-  public String reportBugText;
-  public Set<UiType> webUis;
   public String primaryWeblinkName;
 }
diff --git a/java/com/google/gerrit/extensions/common/ServerInfo.java b/java/com/google/gerrit/extensions/common/ServerInfo.java
index 8904f0a..82d5bc8 100644
--- a/java/com/google/gerrit/extensions/common/ServerInfo.java
+++ b/java/com/google/gerrit/extensions/common/ServerInfo.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.extensions.common;
 
-import java.util.Map;
-
 public class ServerInfo {
   public AccountsInfo accounts;
   public AuthInfo auth;
@@ -26,7 +24,6 @@
   public PluginConfigInfo plugin;
   public SshdInfo sshd;
   public SuggestInfo suggest;
-  public Map<String, String> urlAliases;
   public UserConfigInfo user;
   public ReceiveInfo receive;
   public String defaultTheme;
diff --git a/java/com/google/gerrit/extensions/common/testing/BUILD b/java/com/google/gerrit/extensions/common/testing/BUILD
index 1841152..9cecb66 100644
--- a/java/com/google/gerrit/extensions/common/testing/BUILD
+++ b/java/com/google/gerrit/extensions/common/testing/BUILD
@@ -8,6 +8,7 @@
     deps = [
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/truth",
+        "//lib:guava",
         "//lib/jgit/org.eclipse.jgit: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 6dd5ce4..f0f5516 100644
--- a/java/com/google/gerrit/extensions/common/testing/CommitInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/CommitInfoSubject.java
@@ -15,18 +15,23 @@
 package com.google.gerrit.extensions.common.testing;
 
 import static com.google.common.truth.Truth.assertAbout;
+import static com.google.gerrit.extensions.common.testing.GitPersonSubject.gitPersons;
+import static com.google.gerrit.truth.ListSubject.elements;
 
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.truth.ListSubject;
 
 public class CommitInfoSubject extends Subject<CommitInfoSubject, CommitInfo> {
 
   public static CommitInfoSubject assertThat(CommitInfo commitInfo) {
-    return assertAbout(CommitInfoSubject::new).that(commitInfo);
+    return assertAbout(commits()).that(commitInfo);
+  }
+
+  public static Subject.Factory<CommitInfoSubject, CommitInfo> commits() {
+    return CommitInfoSubject::new;
   }
 
   private CommitInfoSubject(FailureMetadata failureMetadata, CommitInfo commitInfo) {
@@ -36,31 +41,30 @@
   public StringSubject commit() {
     isNotNull();
     CommitInfo commitInfo = actual();
-    return Truth.assertThat(commitInfo.commit).named("commit");
+    return check("commit()").that(commitInfo.commit);
   }
 
   public ListSubject<CommitInfoSubject, CommitInfo> parents() {
     isNotNull();
     CommitInfo commitInfo = actual();
-    return ListSubject.assertThat(commitInfo.parents, CommitInfoSubject::assertThat)
-        .named("parents");
+    return check("parents()").about(elements()).thatCustom(commitInfo.parents, commits());
   }
 
   public GitPersonSubject committer() {
     isNotNull();
     CommitInfo commitInfo = actual();
-    return GitPersonSubject.assertThat(commitInfo.committer).named("committer");
+    return check("committer()").about(gitPersons()).that(commitInfo.committer);
   }
 
   public GitPersonSubject author() {
     isNotNull();
     CommitInfo commitInfo = actual();
-    return GitPersonSubject.assertThat(commitInfo.author).named("author");
+    return check("author()").about(gitPersons()).that(commitInfo.author);
   }
 
   public StringSubject message() {
     isNotNull();
     CommitInfo commitInfo = actual();
-    return Truth.assertThat(commitInfo.message).named("message");
+    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 5fc8ba6..25750c1 100644
--- a/java/com/google/gerrit/extensions/common/testing/ContentEntrySubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/ContentEntrySubject.java
@@ -14,21 +14,27 @@
 
 package com.google.gerrit.extensions.common.testing;
 
+import static com.google.common.truth.Fact.simpleFact;
 import static com.google.common.truth.Truth.assertAbout;
+import static com.google.gerrit.truth.ListSubject.elements;
 
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.IntegerSubject;
 import com.google.common.truth.IterableSubject;
+import com.google.common.truth.StandardSubjectBuilder;
 import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.extensions.common.DiffInfo.ContentEntry;
 import com.google.gerrit.truth.ListSubject;
 
 public class ContentEntrySubject extends Subject<ContentEntrySubject, ContentEntry> {
 
   public static ContentEntrySubject assertThat(ContentEntry contentEntry) {
-    return assertAbout(ContentEntrySubject::new).that(contentEntry);
+    return assertAbout(contentEntries()).that(contentEntry);
+  }
+
+  public static Subject.Factory<ContentEntrySubject, ContentEntry> contentEntries() {
+    return ContentEntrySubject::new;
   }
 
   private ContentEntrySubject(FailureMetadata failureMetadata, ContentEntry contentEntry) {
@@ -38,54 +44,54 @@
   public void isDueToRebase() {
     isNotNull();
     ContentEntry contentEntry = actual();
-    Truth.assertWithMessage("Entry should be marked 'dueToRebase'")
-        .that(contentEntry.dueToRebase)
-        .named("dueToRebase")
-        .isTrue();
+    if (contentEntry.dueToRebase == null || !contentEntry.dueToRebase) {
+      failWithActual(simpleFact("expected entry to be marked 'dueToRebase'"));
+    }
   }
 
   public void isNotDueToRebase() {
     isNotNull();
     ContentEntry contentEntry = actual();
-    Truth.assertWithMessage("Entry should not be marked 'dueToRebase'")
-        .that(contentEntry.dueToRebase)
-        .named("dueToRebase")
-        .isNull();
+    if (contentEntry.dueToRebase != null && contentEntry.dueToRebase) {
+      failWithActual(simpleFact("expected entry not to be marked 'dueToRebase'"));
+    }
   }
 
   public ListSubject<StringSubject, String> commonLines() {
     isNotNull();
     ContentEntry contentEntry = actual();
-    return ListSubject.assertThat(contentEntry.ab, Truth::assertThat).named("common lines");
+    return check("commonLines()")
+        .about(elements())
+        .that(contentEntry.ab, StandardSubjectBuilder::that);
   }
 
   public ListSubject<StringSubject, String> linesOfA() {
     isNotNull();
     ContentEntry contentEntry = actual();
-    return ListSubject.assertThat(contentEntry.a, Truth::assertThat).named("lines of 'a'");
+    return check("linesOfA()").about(elements()).that(contentEntry.a, StandardSubjectBuilder::that);
   }
 
   public ListSubject<StringSubject, String> linesOfB() {
     isNotNull();
     ContentEntry contentEntry = actual();
-    return ListSubject.assertThat(contentEntry.b, Truth::assertThat).named("lines of 'b'");
+    return check("linesOfB()").about(elements()).that(contentEntry.b, StandardSubjectBuilder::that);
   }
 
   public IterableSubject intralineEditsOfA() {
     isNotNull();
     ContentEntry contentEntry = actual();
-    return Truth.assertThat(contentEntry.editA).named("intraline edits of 'a'");
+    return check("intralineEditsOfA()").that(contentEntry.editA);
   }
 
   public IterableSubject intralineEditsOfB() {
     isNotNull();
     ContentEntry contentEntry = actual();
-    return Truth.assertThat(contentEntry.editB).named("intraline edits of 'b'");
+    return check("intralineEditsOfB()").that(contentEntry.editB);
   }
 
   public IntegerSubject numberOfSkippedLines() {
     isNotNull();
     ContentEntry contentEntry = actual();
-    return Truth.assertThat(contentEntry.skip).named("number of skipped lines");
+    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 057a1a2..ee37bde 100644
--- a/java/com/google/gerrit/extensions/common/testing/DiffInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/DiffInfoSubject.java
@@ -15,11 +15,12 @@
 package com.google.gerrit.extensions.common.testing;
 
 import static com.google.common.truth.Truth.assertAbout;
+import static com.google.gerrit.extensions.common.testing.FileMetaSubject.fileMetas;
+import static com.google.gerrit.truth.ListSubject.elements;
 
 import com.google.common.truth.ComparableSubject;
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.extensions.common.ChangeType;
 import com.google.gerrit.extensions.common.DiffInfo;
 import com.google.gerrit.extensions.common.DiffInfo.ContentEntry;
@@ -38,25 +39,26 @@
   public ListSubject<ContentEntrySubject, ContentEntry> content() {
     isNotNull();
     DiffInfo diffInfo = actual();
-    return ListSubject.assertThat(diffInfo.content, ContentEntrySubject::assertThat)
-        .named("content");
+    return check("content()")
+        .about(elements())
+        .thatCustom(diffInfo.content, ContentEntrySubject.contentEntries());
   }
 
   public ComparableSubject<?, ChangeType> changeType() {
     isNotNull();
     DiffInfo diffInfo = actual();
-    return Truth.assertThat(diffInfo.changeType).named("changeType");
+    return check("changeType()").that(diffInfo.changeType);
   }
 
   public FileMetaSubject metaA() {
     isNotNull();
     DiffInfo diffInfo = actual();
-    return FileMetaSubject.assertThat(diffInfo.metaA).named("metaA");
+    return check("metaA()").about(fileMetas()).that(diffInfo.metaA);
   }
 
   public FileMetaSubject metaB() {
     isNotNull();
     DiffInfo diffInfo = actual();
-    return FileMetaSubject.assertThat(diffInfo.metaB).named("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 84ad61c..1c99141 100644
--- a/java/com/google/gerrit/extensions/common/testing/EditInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/EditInfoSubject.java
@@ -15,11 +15,11 @@
 package com.google.gerrit.extensions.common.testing;
 
 import static com.google.common.truth.Truth.assertAbout;
+import static com.google.gerrit.extensions.common.testing.CommitInfoSubject.commits;
 
 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.EditInfo;
 import com.google.gerrit.truth.OptionalSubject;
 import java.util.Optional;
@@ -27,12 +27,16 @@
 public class EditInfoSubject extends Subject<EditInfoSubject, EditInfo> {
 
   public static EditInfoSubject assertThat(EditInfo editInfo) {
-    return assertAbout(EditInfoSubject::new).that(editInfo);
+    return assertAbout(edits()).that(editInfo);
+  }
+
+  private static Subject.Factory<EditInfoSubject, EditInfo> edits() {
+    return EditInfoSubject::new;
   }
 
   public static OptionalSubject<EditInfoSubject, EditInfo> assertThat(
       Optional<EditInfo> editInfoOptional) {
-    return OptionalSubject.assertThat(editInfoOptional, EditInfoSubject::assertThat);
+    return OptionalSubject.assertThat(editInfoOptional, edits());
   }
 
   private EditInfoSubject(FailureMetadata failureMetadata, EditInfo editInfo) {
@@ -42,12 +46,12 @@
   public CommitInfoSubject commit() {
     isNotNull();
     EditInfo editInfo = actual();
-    return CommitInfoSubject.assertThat(editInfo.commit).named("commit");
+    return check("commit()").about(commits()).that(editInfo.commit);
   }
 
   public StringSubject baseRevision() {
     isNotNull();
     EditInfo editInfo = actual();
-    return Truth.assertThat(editInfo.baseRevision).named("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 b088016..3ebf838 100644
--- a/java/com/google/gerrit/extensions/common/testing/FileInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/FileInfoSubject.java
@@ -20,7 +20,6 @@
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.IntegerSubject;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.extensions.common.FileInfo;
 
 public class FileInfoSubject extends Subject<FileInfoSubject, FileInfo> {
@@ -36,18 +35,18 @@
   public IntegerSubject linesInserted() {
     isNotNull();
     FileInfo fileInfo = actual();
-    return Truth.assertThat(fileInfo.linesInserted).named("linesInserted");
+    return check("linesInserted()").that(fileInfo.linesInserted);
   }
 
   public IntegerSubject linesDeleted() {
     isNotNull();
     FileInfo fileInfo = actual();
-    return Truth.assertThat(fileInfo.linesDeleted).named("linesDeleted");
+    return check("linesDeleted()").that(fileInfo.linesDeleted);
   }
 
   public ComparableSubject<?, Character> status() {
     isNotNull();
     FileInfo fileInfo = actual();
-    return Truth.assertThat(fileInfo.status).named("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 e77eef1..d1b2031 100644
--- a/java/com/google/gerrit/extensions/common/testing/FileMetaSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/FileMetaSubject.java
@@ -19,13 +19,16 @@
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.IntegerSubject;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.extensions.common.DiffInfo.FileMeta;
 
 public class FileMetaSubject extends Subject<FileMetaSubject, FileMeta> {
 
   public static FileMetaSubject assertThat(FileMeta fileMeta) {
-    return assertAbout(FileMetaSubject::new).that(fileMeta);
+    return assertAbout(fileMetas()).that(fileMeta);
+  }
+
+  public static Subject.Factory<FileMetaSubject, FileMeta> fileMetas() {
+    return FileMetaSubject::new;
   }
 
   private FileMetaSubject(FailureMetadata failureMetadata, FileMeta fileMeta) {
@@ -35,6 +38,6 @@
   public IntegerSubject totalLineCount() {
     isNotNull();
     FileMeta fileMeta = actual();
-    return Truth.assertThat(fileMeta.lines).named("total line count");
+    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 b56d399..6ba5f8b 100644
--- a/java/com/google/gerrit/extensions/common/testing/FixReplacementInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/FixReplacementInfoSubject.java
@@ -15,18 +15,22 @@
 package com.google.gerrit.extensions.common.testing;
 
 import static com.google.common.truth.Truth.assertAbout;
+import static com.google.gerrit.extensions.common.testing.RangeSubject.ranges;
 
 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;
 
 public class FixReplacementInfoSubject
     extends Subject<FixReplacementInfoSubject, FixReplacementInfo> {
 
   public static FixReplacementInfoSubject assertThat(FixReplacementInfo fixReplacementInfo) {
-    return assertAbout(FixReplacementInfoSubject::new).that(fixReplacementInfo);
+    return assertAbout(fixReplacements()).that(fixReplacementInfo);
+  }
+
+  public static Subject.Factory<FixReplacementInfoSubject, FixReplacementInfo> fixReplacements() {
+    return FixReplacementInfoSubject::new;
   }
 
   private FixReplacementInfoSubject(
@@ -35,14 +39,17 @@
   }
 
   public StringSubject path() {
-    return Truth.assertThat(actual().path).named("path");
+    isNotNull();
+    return check("path()").that(actual().path);
   }
 
   public RangeSubject range() {
-    return RangeSubject.assertThat(actual().range).named("range");
+    isNotNull();
+    return check("range()").about(ranges()).that(actual().range);
   }
 
   public StringSubject replacement() {
-    return Truth.assertThat(actual().replacement).named("replacement");
+    isNotNull();
+    return check("replacement()").that(actual().replacement);
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/FixSuggestionInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/FixSuggestionInfoSubject.java
index 7a6da9c..98dac38 100644
--- a/java/com/google/gerrit/extensions/common/testing/FixSuggestionInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/FixSuggestionInfoSubject.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.extensions.common.testing;
 
 import static com.google.common.truth.Truth.assertAbout;
+import static com.google.gerrit.extensions.common.testing.FixReplacementInfoSubject.fixReplacements;
+import static com.google.gerrit.truth.ListSubject.elements;
 
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.StringSubject;
@@ -27,7 +29,11 @@
 public class FixSuggestionInfoSubject extends Subject<FixSuggestionInfoSubject, FixSuggestionInfo> {
 
   public static FixSuggestionInfoSubject assertThat(FixSuggestionInfo fixSuggestionInfo) {
-    return assertAbout(FixSuggestionInfoSubject::new).that(fixSuggestionInfo);
+    return assertAbout(fixSuggestions()).that(fixSuggestionInfo);
+  }
+
+  public static Subject.Factory<FixSuggestionInfoSubject, FixSuggestionInfo> fixSuggestions() {
+    return FixSuggestionInfoSubject::new;
   }
 
   private FixSuggestionInfoSubject(
@@ -40,8 +46,10 @@
   }
 
   public ListSubject<FixReplacementInfoSubject, FixReplacementInfo> replacements() {
-    return ListSubject.assertThat(actual().replacements, FixReplacementInfoSubject::assertThat)
-        .named("replacements");
+    isNotNull();
+    return check("replacements()")
+        .about(elements())
+        .thatCustom(actual().replacements, fixReplacements());
   }
 
   public FixReplacementInfoSubject onlyReplacement() {
@@ -49,6 +57,7 @@
   }
 
   public StringSubject description() {
-    return Truth.assertThat(actual().description).named("description");
+    isNotNull();
+    return check("description()").that(actual().description);
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java b/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java
index cdbef34..dee0636 100644
--- a/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.common.testing;
 
+import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.truth.Truth.assertAbout;
 
 import com.google.common.truth.ComparableSubject;
@@ -21,7 +22,6 @@
 import com.google.common.truth.IntegerSubject;
 import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.extensions.common.GitPerson;
 import java.sql.Timestamp;
 import java.util.Date;
@@ -30,7 +30,11 @@
 public class GitPersonSubject extends Subject<GitPersonSubject, GitPerson> {
 
   public static GitPersonSubject assertThat(GitPerson gitPerson) {
-    return assertAbout(GitPersonSubject::new).that(gitPerson);
+    return assertAbout(gitPersons()).that(gitPerson);
+  }
+
+  public static Factory<GitPersonSubject, GitPerson> gitPersons() {
+    return GitPersonSubject::new;
   }
 
   private GitPersonSubject(FailureMetadata failureMetadata, GitPerson gitPerson) {
@@ -40,30 +44,30 @@
   public StringSubject name() {
     isNotNull();
     GitPerson gitPerson = actual();
-    return Truth.assertThat(gitPerson.name).named("name");
+    return check("name()").that(gitPerson.name);
   }
 
   public StringSubject email() {
     isNotNull();
     GitPerson gitPerson = actual();
-    return Truth.assertThat(gitPerson.email).named("email");
+    return check("email()").that(gitPerson.email);
   }
 
   public ComparableSubject<?, Timestamp> date() {
     isNotNull();
     GitPerson gitPerson = actual();
-    return Truth.assertThat(gitPerson.date).named("date");
+    return check("date()").that(gitPerson.date);
   }
 
   public IntegerSubject tz() {
     isNotNull();
     GitPerson gitPerson = actual();
-    return Truth.assertThat(gitPerson.tz).named("tz");
+    return check("tz()").that(gitPerson.tz);
   }
 
   public void hasSameDateAs(GitPerson other) {
+    checkNotNull(other, "'other' GitPerson must not be null");
     isNotNull();
-    assertThat(other).named("other").isNotNull();
     date().isEqualTo(other.date);
     tz().isEqualTo(other.tz);
   }
@@ -72,9 +76,7 @@
     isNotNull();
     name().isEqualTo(ident.getName());
     email().isEqualTo(ident.getEmailAddress());
-    Truth.assertThat(new Date(actual().date.getTime()))
-        .named("rounded date")
-        .isEqualTo(ident.getWhen());
+    check("roundedDate()").that(new Date(actual().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 db7f0d1..12acb8d 100644
--- a/java/com/google/gerrit/extensions/common/testing/RangeSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/RangeSubject.java
@@ -14,19 +14,22 @@
 
 package com.google.gerrit.extensions.common.testing;
 
-import static com.google.common.truth.Fact.fact;
+import static com.google.common.truth.Fact.simpleFact;
 import static com.google.common.truth.Truth.assertAbout;
 
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.IntegerSubject;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.extensions.client.Comment;
 
 public class RangeSubject extends Subject<RangeSubject, Comment.Range> {
 
   public static RangeSubject assertThat(Comment.Range range) {
-    return assertAbout(RangeSubject::new).that(range);
+    return assertAbout(ranges()).that(range);
+  }
+
+  public static Subject.Factory<RangeSubject, Comment.Range> ranges() {
+    return RangeSubject::new;
   }
 
   private RangeSubject(FailureMetadata failureMetadata, Comment.Range range) {
@@ -34,32 +37,32 @@
   }
 
   public IntegerSubject startLine() {
-    return Truth.assertThat(actual().startLine).named("startLine");
+    return check("startLine()").that(actual().startLine);
   }
 
   public IntegerSubject startCharacter() {
-    return Truth.assertThat(actual().startCharacter).named("startCharacter");
+    return check("startCharacter()").that(actual().startCharacter);
   }
 
   public IntegerSubject endLine() {
-    return Truth.assertThat(actual().endLine).named("endLine");
+    return check("endLine()").that(actual().endLine);
   }
 
   public IntegerSubject endCharacter() {
-    return Truth.assertThat(actual().endCharacter).named("endCharacter");
+    return check("endCharacter()").that(actual().endCharacter);
   }
 
   public void isValid() {
     isNotNull();
     if (!actual().isValid()) {
-      failWithoutActual(fact("expected", "valid"));
+      failWithActual(simpleFact("expected to be valid"));
     }
   }
 
   public void isInvalid() {
     isNotNull();
     if (actual().isValid()) {
-      failWithoutActual(fact("expected", "not valid"));
+      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 c2bed86..033f54b 100644
--- a/java/com/google/gerrit/extensions/common/testing/RobotCommentInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/RobotCommentInfoSubject.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.extensions.common.testing;
 
 import static com.google.common.truth.Truth.assertAbout;
+import static com.google.gerrit.truth.ListSubject.elements;
 
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.Subject;
@@ -27,12 +28,15 @@
 
   public static ListSubject<RobotCommentInfoSubject, RobotCommentInfo> assertThatList(
       List<RobotCommentInfo> robotCommentInfos) {
-    return ListSubject.assertThat(robotCommentInfos, RobotCommentInfoSubject::assertThat)
-        .named("robotCommentInfos");
+    return ListSubject.assertThat(robotCommentInfos, robotComments()).named("robotCommentInfos");
   }
 
   public static RobotCommentInfoSubject assertThat(RobotCommentInfo robotCommentInfo) {
-    return assertAbout(RobotCommentInfoSubject::new).that(robotCommentInfo);
+    return assertAbout(robotComments()).that(robotCommentInfo);
+  }
+
+  private static Factory<RobotCommentInfoSubject, RobotCommentInfo> robotComments() {
+    return RobotCommentInfoSubject::new;
   }
 
   private RobotCommentInfoSubject(
@@ -41,8 +45,9 @@
   }
 
   public ListSubject<FixSuggestionInfoSubject, FixSuggestionInfo> fixSuggestions() {
-    return ListSubject.assertThat(actual().fixSuggestions, FixSuggestionInfoSubject::assertThat)
-        .named("fixSuggestions");
+    return check("fixSuggestions()")
+        .about(elements())
+        .thatCustom(actual().fixSuggestions, FixSuggestionInfoSubject.fixSuggestions());
   }
 
   public FixSuggestionInfoSubject onlyFixSuggestion() {
diff --git a/java/com/google/gerrit/extensions/config/CapabilityDefinition.java b/java/com/google/gerrit/extensions/config/CapabilityDefinition.java
index aafb583..c76ec6c 100644
--- a/java/com/google/gerrit/extensions/config/CapabilityDefinition.java
+++ b/java/com/google/gerrit/extensions/config/CapabilityDefinition.java
@@ -18,7 +18,4 @@
 
 /** Specifies a capability declared by a plugin. */
 @ExtensionPoint
-public abstract class CapabilityDefinition {
-  /** @return description of the capability. */
-  public abstract String getDescription();
-}
+public abstract class CapabilityDefinition implements PluginPermissionDefinition {}
diff --git a/java/com/google/gerrit/extensions/config/PluginPermissionDefinition.java b/java/com/google/gerrit/extensions/config/PluginPermissionDefinition.java
new file mode 100644
index 0000000..11b1981
--- /dev/null
+++ b/java/com/google/gerrit/extensions/config/PluginPermissionDefinition.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.extensions.config;
+
+/** Specifies a permission declared by a plugin. */
+public interface PluginPermissionDefinition {
+  /**
+   * Gets the description of a permission declared by a plugin.
+   *
+   * @return description of the permission.
+   */
+  String getDescription();
+}
diff --git a/java/com/google/gerrit/extensions/config/PluginProjectPermissionDefinition.java b/java/com/google/gerrit/extensions/config/PluginProjectPermissionDefinition.java
new file mode 100644
index 0000000..d1d9f9e
--- /dev/null
+++ b/java/com/google/gerrit/extensions/config/PluginProjectPermissionDefinition.java
@@ -0,0 +1,21 @@
+// 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.config;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+/** Specifies a repository permission declared by a plugin. */
+@ExtensionPoint
+public abstract class PluginProjectPermissionDefinition implements PluginPermissionDefinition {}
diff --git a/java/com/google/gerrit/extensions/persistence/DataSourceInterceptor.java b/java/com/google/gerrit/extensions/persistence/DataSourceInterceptor.java
deleted file mode 100644
index 0a83eea..0000000
--- a/java/com/google/gerrit/extensions/persistence/DataSourceInterceptor.java
+++ /dev/null
@@ -1,21 +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.extensions.persistence;
-
-import javax.sql.DataSource;
-
-public interface DataSourceInterceptor {
-  DataSource intercept(String name, DataSource dataSource);
-}
diff --git a/java/com/google/gerrit/extensions/registration/DynamicItem.java b/java/com/google/gerrit/extensions/registration/DynamicItem.java
index 67982d9..3f848cb 100644
--- a/java/com/google/gerrit/extensions/registration/DynamicItem.java
+++ b/java/com/google/gerrit/extensions/registration/DynamicItem.java
@@ -184,12 +184,7 @@
     }
 
     final Extension<T> defaultItem = old;
-    return new RegistrationHandle() {
-      @Override
-      public void remove() {
-        ref.compareAndSet(item, defaultItem);
-      }
-    };
+    return () -> ref.compareAndSet(item, defaultItem);
   }
 
   /**
diff --git a/java/com/google/gerrit/extensions/registration/DynamicSet.java b/java/com/google/gerrit/extensions/registration/DynamicSet.java
index 35f3567..b2e871e 100644
--- a/java/com/google/gerrit/extensions/registration/DynamicSet.java
+++ b/java/com/google/gerrit/extensions/registration/DynamicSet.java
@@ -137,7 +137,7 @@
   }
 
   public static <T> DynamicSet<T> emptySet() {
-    return new DynamicSet<>(Collections.<AtomicReference<Extension<T>>>emptySet());
+    return new DynamicSet<>(Collections.emptySet());
   }
 
   private final CopyOnWriteArrayList<AtomicReference<Extension<T>>> items;
@@ -169,10 +169,8 @@
 
   public Iterable<Extension<T>> entries() {
     final Iterator<AtomicReference<Extension<T>>> itr = items.iterator();
-    return new Iterable<Extension<T>>() {
-      @Override
-      public Iterator<Extension<T>> iterator() {
-        return new Iterator<Extension<T>>() {
+    return () ->
+        new Iterator<Extension<T>>() {
           private Extension<T> next;
 
           @Override
@@ -201,8 +199,6 @@
             throw new UnsupportedOperationException();
           }
         };
-      }
-    };
   }
 
   /**
@@ -266,12 +262,9 @@
     final AtomicReference<Extension<T>> ref =
         new AtomicReference<>(new Extension<>(pluginName, item));
     items.add(ref);
-    return new RegistrationHandle() {
-      @Override
-      public void remove() {
-        if (ref.compareAndSet(ref.get(), null)) {
-          items.remove(ref);
-        }
+    return () -> {
+      if (ref.compareAndSet(ref.get(), null)) {
+        items.remove(ref);
       }
     };
   }
diff --git a/java/com/google/gerrit/extensions/registration/Extension.java b/java/com/google/gerrit/extensions/registration/Extension.java
index aaec201..1031bb0 100644
--- a/java/com/google/gerrit/extensions/registration/Extension.java
+++ b/java/com/google/gerrit/extensions/registration/Extension.java
@@ -32,7 +32,7 @@
   private final @Nullable String exportName;
   private final Provider<T> provider;
 
-  protected Extension(String pluginName, Provider<T> provider) {
+  public Extension(String pluginName, Provider<T> provider) {
     this(pluginName, null, provider);
   }
 
diff --git a/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java b/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java
index cef9e0c..fb520b4 100644
--- a/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java
+++ b/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java
@@ -36,12 +36,7 @@
     requireNonNull(item);
     final NamePair key = new NamePair(pluginName, exportName);
     items.put(key, item);
-    return new RegistrationHandle() {
-      @Override
-      public void remove() {
-        items.remove(key, item);
-      }
-    };
+    return () -> items.remove(key, item);
   }
 
   /**
diff --git a/java/com/google/gerrit/extensions/restapi/testing/BinaryResultSubject.java b/java/com/google/gerrit/extensions/restapi/testing/BinaryResultSubject.java
index 1867308..5109205 100644
--- a/java/com/google/gerrit/extensions/restapi/testing/BinaryResultSubject.java
+++ b/java/com/google/gerrit/extensions/restapi/testing/BinaryResultSubject.java
@@ -20,7 +20,6 @@
 import com.google.common.truth.PrimitiveByteArraySubject;
 import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.truth.OptionalSubject;
 import java.io.ByteArrayOutputStream;
@@ -30,12 +29,16 @@
 public class BinaryResultSubject extends Subject<BinaryResultSubject, BinaryResult> {
 
   public static BinaryResultSubject assertThat(BinaryResult binaryResult) {
-    return assertAbout(BinaryResultSubject::new).that(binaryResult);
+    return assertAbout(binaryResults()).that(binaryResult);
+  }
+
+  private static Subject.Factory<BinaryResultSubject, BinaryResult> binaryResults() {
+    return BinaryResultSubject::new;
   }
 
   public static OptionalSubject<BinaryResultSubject, BinaryResult> assertThat(
       Optional<BinaryResult> binaryResultOptional) {
-    return OptionalSubject.assertThat(binaryResultOptional, BinaryResultSubject::assertThat);
+    return OptionalSubject.assertThat(binaryResultOptional, binaryResults());
   }
 
   private BinaryResultSubject(FailureMetadata failureMetadata, BinaryResult binaryResult) {
@@ -48,7 +51,7 @@
     // be used afterwards. Besides, closing it doesn't have an effect for most
     // implementations of a BinaryResult.
     BinaryResult binaryResult = actual();
-    return Truth.assertThat(binaryResult.asString());
+    return check("asString()").that(binaryResult.asString());
   }
 
   public PrimitiveByteArraySubject bytes() throws IOException {
@@ -60,6 +63,6 @@
     ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
     binaryResult.writeTo(byteArrayOutputStream);
     byte[] bytes = byteArrayOutputStream.toByteArray();
-    return Truth.assertThat(bytes);
+    return check("bytes()").that(bytes);
   }
 }
diff --git a/java/com/google/gerrit/extensions/webui/GwtPlugin.java b/java/com/google/gerrit/extensions/webui/GwtPlugin.java
deleted file mode 100644
index e8041c4..0000000
--- a/java/com/google/gerrit/extensions/webui/GwtPlugin.java
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.extensions.webui;
-
-/** Configures a web UI plugin compiled using GWT. */
-public class GwtPlugin extends WebUiPlugin {
-  private final String moduleName;
-
-  /**
-   * @param moduleName name of GWT module. The resource {@code static/$MODULE/$MODULE.nocache.js}
-   *     will be used.
-   */
-  public GwtPlugin(String moduleName) {
-    this.moduleName = moduleName;
-  }
-
-  @Override
-  public String getJavaScriptResourcePath() {
-    return String.format("static/%s/%s.nocache.js", moduleName, moduleName);
-  }
-}
diff --git a/java/com/google/gerrit/extensions/webui/WebUiPlugin.java b/java/com/google/gerrit/extensions/webui/WebUiPlugin.java
index 051d336..2d49e1c 100644
--- a/java/com/google/gerrit/extensions/webui/WebUiPlugin.java
+++ b/java/com/google/gerrit/extensions/webui/WebUiPlugin.java
@@ -34,15 +34,10 @@
  * }
  * </pre>
  *
- * @see GwtPlugin
  * @see JavaScriptPlugin
  */
 @ExtensionPoint
 public abstract class WebUiPlugin {
-  public static final GwtPlugin gwt(String moduleName) {
-    return new GwtPlugin(moduleName);
-  }
-
   public static final JavaScriptPlugin js(String scriptName) {
     return new JavaScriptPlugin(scriptName);
   }
diff --git a/java/com/google/gerrit/git/BUILD b/java/com/google/gerrit/git/BUILD
new file mode 100644
index 0000000..4c4d5bc
--- /dev/null
+++ b/java/com/google/gerrit/git/BUILD
@@ -0,0 +1,11 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
+java_library(
+    name = "git",
+    srcs = glob(["*.java"]),
+    visibility = ["//visibility:public"],
+    deps = [
+        "//lib:guava",
+        "//lib/jgit/org.eclipse.jgit:jgit",
+    ],
+)
diff --git a/java/com/google/gerrit/git/LockFailureException.java b/java/com/google/gerrit/git/LockFailureException.java
new file mode 100644
index 0000000..9e67d70
--- /dev/null
+++ b/java/com/google/gerrit/git/LockFailureException.java
@@ -0,0 +1,49 @@
+// 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.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 {
+  private static final long serialVersionUID = 1L;
+
+  private final ImmutableList<String> refs;
+
+  public LockFailureException(String message, RefUpdate refUpdate) {
+    super(message);
+    refs = ImmutableList.of(refUpdate.getName());
+  }
+
+  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;
+  }
+}
diff --git a/java/com/google/gerrit/git/RefUpdateUtil.java b/java/com/google/gerrit/git/RefUpdateUtil.java
new file mode 100644
index 0000000..5eaf253
--- /dev/null
+++ b/java/com/google/gerrit/git/RefUpdateUtil.java
@@ -0,0 +1,183 @@
+// 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.git;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.io.IOException;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+/** Static utilities for working with JGit's ref update APIs. */
+public class RefUpdateUtil {
+  /**
+   * Execute a batch ref update, throwing a checked exception if not all updates succeeded.
+   *
+   * <p>Creates a new {@link RevWalk} used only for this operation.
+   *
+   * @param bru batch update; should already have been executed.
+   * @param repo repository that created {@code bru}.
+   * @throws LockFailureException if the transaction was aborted due to lock failure; see {@link
+   *     #checkResults(BatchRefUpdate)} for details.
+   * @throws IOException if any result was not {@code OK}.
+   */
+  public static void executeChecked(BatchRefUpdate bru, Repository repo) throws IOException {
+    try (RevWalk rw = new RevWalk(repo)) {
+      executeChecked(bru, rw);
+    }
+  }
+
+  /**
+   * Execute a batch ref update, throwing a checked exception if not all updates succeeded.
+   *
+   * @param bru batch update; should already have been executed.
+   * @param rw walk for executing the update.
+   * @throws LockFailureException if the transaction was aborted due to lock failure; see {@link
+   *     #checkResults(BatchRefUpdate)} for details.
+   * @throws IOException if any result was not {@code OK}.
+   */
+  public static void executeChecked(BatchRefUpdate bru, RevWalk rw) throws IOException {
+    bru.execute(rw, NullProgressMonitor.INSTANCE);
+    checkResults(bru);
+  }
+
+  /**
+   * Check results of all commands in the update batch, reducing to a single exception if there was
+   * a failure.
+   *
+   * <p>Throws {@link LockFailureException} if at least one command failed with {@code
+   * LOCK_FAILURE}, and the entire transaction was aborted, i.e. any non-{@code LOCK_FAILURE}
+   * results, if there were any, failed with "transaction aborted".
+   *
+   * <p>In particular, if the underlying ref database does not {@link
+   * org.eclipse.jgit.lib.RefDatabase#performsAtomicTransactions() perform atomic transactions},
+   * then a combination of {@code LOCK_FAILURE} on one ref and {@code OK} or another result on other
+   * refs will <em>not</em> throw {@code LockFailureException}.
+   *
+   * @param bru batch update; should already have been executed.
+   * @throws LockFailureException if the transaction was aborted due to lock failure.
+   * @throws IOException if any result was not {@code OK}.
+   */
+  @VisibleForTesting
+  static void checkResults(BatchRefUpdate bru) throws IOException {
+    if (bru.getCommands().isEmpty()) {
+      return;
+    }
+
+    int lockFailure = 0;
+    int aborted = 0;
+    int failure = 0;
+
+    for (ReceiveCommand cmd : bru.getCommands()) {
+      if (cmd.getResult() != ReceiveCommand.Result.OK) {
+        failure++;
+      }
+      if (cmd.getResult() == ReceiveCommand.Result.LOCK_FAILURE) {
+        lockFailure++;
+      } else if (cmd.getResult() == ReceiveCommand.Result.REJECTED_OTHER_REASON
+          && JGitText.get().transactionAborted.equals(cmd.getMessage())) {
+        aborted++;
+      }
+    }
+
+    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);
+    }
+  }
+
+  /**
+   * Check results of a single ref update, throwing an exception if there was a failure.
+   *
+   * @param ru ref update; must already have been executed.
+   * @throws IllegalArgumentException if the result was {@code NOT_ATTEMPTED}.
+   * @throws LockFailureException if the result was {@code LOCK_FAILURE}.
+   * @throws IOException if the result failed for another reason.
+   */
+  public static void checkResult(RefUpdate ru) throws IOException {
+    RefUpdate.Result result = ru.getResult();
+    switch (result) {
+      case NOT_ATTEMPTED:
+        throw new IllegalArgumentException("Not attempted: " + ru.getName());
+      case NEW:
+      case FORCED:
+      case NO_CHANGE:
+      case FAST_FORWARD:
+      case RENAMED:
+        return;
+      case LOCK_FAILURE:
+        throw new LockFailureException("Failed to update " + ru.getName() + ": " + result, ru);
+      default:
+      case IO_FAILURE:
+      case REJECTED:
+      case REJECTED_CURRENT_BRANCH:
+      case REJECTED_MISSING_OBJECT:
+      case REJECTED_OTHER_REASON:
+        throw new IOException("Failed to update " + ru.getName() + ": " + ru.getResult());
+    }
+  }
+
+  /**
+   * Delete a single ref, throwing a checked exception on failure.
+   *
+   * <p>Does not require that the ref have any particular old value. Succeeds as a no-op if the ref
+   * did not exist.
+   *
+   * @param repo repository.
+   * @param refName ref name to delete.
+   * @throws LockFailureException if a low-level lock failure (e.g. compare-and-swap failure)
+   *     occurs.
+   * @throws IOException if an error occurred.
+   */
+  public static void deleteChecked(Repository repo, String refName) throws IOException {
+    RefUpdate ru = repo.updateRef(refName);
+    ru.setForceUpdate(true);
+    ru.setCheckConflicting(false);
+    switch (ru.delete()) {
+      case FORCED:
+        // Ref was deleted.
+        return;
+
+      case NEW:
+        // Ref didn't exist (yes, really).
+        return;
+
+      case LOCK_FAILURE:
+        throw new LockFailureException("Failed to delete " + refName + ": " + ru.getResult(), ru);
+
+        // Not really failures, but should not be the result of a deletion, so the best option is to
+        // throw.
+      case NO_CHANGE:
+      case FAST_FORWARD:
+      case RENAMED:
+      case NOT_ATTEMPTED:
+
+      case IO_FAILURE:
+      case REJECTED:
+      case REJECTED_CURRENT_BRANCH:
+      case REJECTED_MISSING_OBJECT:
+      case REJECTED_OTHER_REASON:
+      default:
+        throw new IOException("Failed to delete " + refName + ": " + ru.getResult());
+    }
+  }
+
+  private RefUpdateUtil() {}
+}
diff --git a/java/com/google/gerrit/git/testing/CommitSubject.java b/java/com/google/gerrit/git/testing/CommitSubject.java
new file mode 100644
index 0000000..0873107
--- /dev/null
+++ b/java/com/google/gerrit/git/testing/CommitSubject.java
@@ -0,0 +1,97 @@
+// 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.testing;
+
+import static com.google.common.truth.Truth.assertAbout;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.Subject;
+import java.sql.Timestamp;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+/** Subject over JGit {@link RevCommit}s. */
+public class CommitSubject extends Subject<CommitSubject, RevCommit> {
+
+  /**
+   * Constructs a new subject.
+   *
+   * @param commit the commit.
+   * @return a new subject over the commit.
+   */
+  public static CommitSubject assertThat(RevCommit commit) {
+    return assertAbout(CommitSubject::new).that(commit);
+  }
+
+  /**
+   * Performs some common assertions over a single commit.
+   *
+   * @param commit the commit.
+   * @param expectedCommitMessage exact expected commit message.
+   * @param expectedCommitTimestamp expected commit timestamp, to the tolerance specified in {@link
+   *     #hasCommitTimestamp(Timestamp)}.
+   * @param expectedSha1 expected commit SHA-1.
+   */
+  public static void assertCommit(
+      RevCommit commit,
+      String expectedCommitMessage,
+      Timestamp expectedCommitTimestamp,
+      ObjectId expectedSha1) {
+    CommitSubject commitSubject = assertThat(commit);
+    commitSubject.hasCommitMessage(expectedCommitMessage);
+    commitSubject.hasCommitTimestamp(expectedCommitTimestamp);
+    commitSubject.hasSha1(expectedSha1);
+  }
+
+  private CommitSubject(FailureMetadata metadata, RevCommit actual) {
+    super(metadata, actual);
+  }
+
+  /**
+   * Asserts that the commit has the given commit message.
+   *
+   * @param expectedCommitMessage exact expected commit message.
+   */
+  public void hasCommitMessage(String expectedCommitMessage) {
+    isNotNull();
+    RevCommit commit = actual();
+    check("commitMessage()").that(commit.getFullMessage()).isEqualTo(expectedCommitMessage);
+  }
+
+  /**
+   * Asserts that the commit has the given commit message, up to skew of at most 1 second.
+   *
+   * @param expectedCommitTimestamp expected commit timestamp.
+   */
+  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));
+  }
+
+  /**
+   * Asserts that the commit has the given SHA-1.
+   *
+   * @param expectedSha1 expected commit SHA-1.
+   */
+  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
new file mode 100644
index 0000000..5fe91f9
--- /dev/null
+++ b/java/com/google/gerrit/git/testing/ObjectIdSubject.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.git.testing;
+
+import static com.google.common.truth.Truth.assertAbout;
+
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.Subject;
+import org.eclipse.jgit.lib.ObjectId;
+
+public class ObjectIdSubject extends Subject<ObjectIdSubject, ObjectId> {
+  public static ObjectIdSubject assertThat(ObjectId objectId) {
+    return assertAbout(objectIds()).that(objectId);
+  }
+
+  public static Factory<ObjectIdSubject, ObjectId> objectIds() {
+    return ObjectIdSubject::new;
+  }
+
+  private ObjectIdSubject(FailureMetadata metadata, ObjectId actual) {
+    super(metadata, actual);
+  }
+
+  public void hasName(String expectedName) {
+    isNotNull();
+    ObjectId objectId = actual();
+    check("name()").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 929e182..f5c9810 100644
--- a/java/com/google/gerrit/git/testing/PushResultSubject.java
+++ b/java/com/google/gerrit/git/testing/PushResultSubject.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.truth.Truth.assertAbout;
-import static java.util.stream.Collectors.joining;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Splitter;
@@ -25,11 +24,10 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Maps;
 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.common.truth.Truth8;
 import com.google.gerrit.common.Nullable;
-import java.util.Arrays;
 import org.eclipse.jgit.transport.PushResult;
 import org.eclipse.jgit.transport.RemoteRefUpdate;
 
@@ -43,7 +41,8 @@
   }
 
   public void hasNoMessages() {
-    Truth.assertWithMessage("expected no messages")
+    check("hasNoMessages()")
+        .withMessage("expected no messages")
         .that(Strings.nullToEmpty(trimMessages()))
         .isEqualTo("");
   }
@@ -51,14 +50,14 @@
   public void hasMessages(String... expectedLines) {
     checkArgument(expectedLines.length > 0, "use hasNoMessages()");
     isNotNull();
-    Truth.assertThat(trimMessages()).isEqualTo(Arrays.stream(expectedLines).collect(joining("\n")));
+    check("messages()").that(trimMessages()).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());
-    Truth.assertThat(got).containsAllIn(expectedLines).inOrder();
+    check("messages()").that(got).containsAtLeastElementsIn(expectedLines).inOrder();
   }
 
   private String trimMessages() {
@@ -90,10 +89,7 @@
               messages, Throwables.getStackTraceAsString(e));
       return;
     }
-    Truth.assertThat(actual)
-        .named("processed commands")
-        .containsExactlyEntriesIn(expected)
-        .inOrder();
+    check("processedCommands()").that(actual).containsExactlyEntriesIn(expected).inOrder();
   }
 
   @VisibleForTesting
@@ -129,7 +125,9 @@
   }
 
   public RemoteRefUpdateSubject onlyRef(String refName) {
-    Truth8.assertThat(actual().getRemoteUpdates().stream().map(RemoteRefUpdate::getRemoteName))
+    check("setOfRefs()")
+        .about(StreamSubject.streams())
+        .that(actual().getRemoteUpdates().stream().map(RemoteRefUpdate::getRemoteName))
         .named("set of refs")
         .containsExactly(refName);
     return ref(refName);
diff --git a/java/com/google/gerrit/gpg/BUILD b/java/com/google/gerrit/gpg/BUILD
index 113b184..f11b9b9 100644
--- a/java/com/google/gerrit/gpg/BUILD
+++ b/java/com/google/gerrit/gpg/BUILD
@@ -5,12 +5,13 @@
     srcs = glob(["**/*.java"]),
     visibility = ["//visibility:public"],
     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/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib/bouncycastle:bcpg-neverlink",
         "//lib/bouncycastle:bcprov-neverlink",
         "//lib/flogger:api",
diff --git a/java/com/google/gerrit/gpg/CheckResult.java b/java/com/google/gerrit/gpg/CheckResult.java
index da891aa..8655b2a 100644
--- a/java/com/google/gerrit/gpg/CheckResult.java
+++ b/java/com/google/gerrit/gpg/CheckResult.java
@@ -31,14 +31,14 @@
   }
 
   static CheckResult trusted() {
-    return new CheckResult(Status.TRUSTED, Collections.<String>emptyList());
+    return new CheckResult(Status.TRUSTED, Collections.emptyList());
   }
 
   static CheckResult create(Status status, String... problems) {
     List<String> problemList =
         problems.length > 0
             ? Collections.unmodifiableList(Arrays.asList(problems))
-            : Collections.<String>emptyList();
+            : Collections.emptyList();
     return new CheckResult(status, problemList);
   }
 
diff --git a/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java b/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
index b6ecbc5..9c08857 100644
--- a/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
+++ b/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
@@ -29,7 +29,6 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.UrlFormatter;
 import com.google.gerrit.server.query.account.InternalAccountQuery;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -135,7 +134,7 @@
         return checkIdsForExpectedUser(key);
       }
       return checkIdsForArbitraryUser(key);
-    } catch (PGPException | OrmException e) {
+    } catch (PGPException | RuntimeException e) {
       String msg = "Error checking user IDs for key";
       logger.atWarning().withCause(e).log("%s %s", msg, keyIdToString(key.getKeyID()));
       return CheckResult.bad(msg);
@@ -156,7 +155,7 @@
     return CheckResult.bad(missingUserIds(allowedUserIds));
   }
 
-  private CheckResult checkIdsForArbitraryUser(PGPPublicKey key) throws PGPException, OrmException {
+  private CheckResult checkIdsForArbitraryUser(PGPPublicKey key) throws PGPException {
     List<AccountState> accountStates = accountQueryProvider.get().byExternalId(toExtIdKey(key));
     if (accountStates.isEmpty()) {
       return CheckResult.bad("Key is not associated with any users");
diff --git a/java/com/google/gerrit/gpg/PublicKeyChecker.java b/java/com/google/gerrit/gpg/PublicKeyChecker.java
index 07b42f1..27530e7 100644
--- a/java/com/google/gerrit/gpg/PublicKeyChecker.java
+++ b/java/com/google/gerrit/gpg/PublicKeyChecker.java
@@ -130,7 +130,7 @@
     if (store == null) {
       throw new IllegalStateException("PublicKeyStore is required");
     }
-    return check(key, 0, true, trusted != null ? new HashSet<Fingerprint>() : null);
+    return check(key, 0, true, trusted != null ? new HashSet<>() : null);
   }
 
   /**
diff --git a/java/com/google/gerrit/gpg/PublicKeyStore.java b/java/com/google/gerrit/gpg/PublicKeyStore.java
index 19d503f..7dd01d9 100644
--- a/java/com/google/gerrit/gpg/PublicKeyStore.java
+++ b/java/com/google/gerrit/gpg/PublicKeyStore.java
@@ -15,8 +15,12 @@
 package com.google.gerrit.gpg;
 
 import static com.google.common.base.Preconditions.checkState;
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 
+import com.google.common.base.Preconditions;
+import com.google.common.io.ByteStreams;
+import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -38,6 +42,8 @@
 import org.bouncycastle.openpgp.PGPSignature;
 import org.bouncycastle.openpgp.bc.BcPGPObjectFactory;
 import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider;
+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;
@@ -63,6 +69,8 @@
  * matching the ID. Multiple keys are supported because forging a key ID is possible, but such a key
  * cannot be used to verify signatures produced with the correct key.
  *
+ * <p>Subkeys are mapped to the master GPG key in the same NoteMap.
+ *
  * <p>No additional checks are performed on the key after reading; callers should only trust keys
  * after checking with a {@link PublicKeyChecker}.
  */
@@ -88,11 +96,19 @@
   public static PGPPublicKey getSigner(
       Iterable<PGPPublicKeyRing> keyRings, PGPSignature sig, byte[] data) throws PGPException {
     for (PGPPublicKeyRing kr : keyRings) {
-      PGPPublicKey k = kr.getPublicKey();
+      // Possibly return a signing subkey in case it differs from the master public key
+      PGPPublicKey k = kr.getPublicKey(sig.getKeyID());
+      if (k == null) {
+        throw new IllegalStateException(
+            "No public key found for ID: " + keyIdToString(sig.getKeyID()));
+      }
       sig.init(new BcPGPContentVerifierBuilderProvider(), k);
       sig.update(data);
       if (sig.verify()) {
-        return k;
+        // If the signature was made using a subkey, return the main public key.
+        // This enables further validity checks, like user ID checks, that can only
+        // be performed using the master public key.
+        return kr.getPublicKey();
       }
     }
     return null;
@@ -207,19 +223,34 @@
     if (notes == null) {
       return Collections.emptyList();
     }
-    Note note = notes.getNote(keyObjectId(keyId));
+
+    return get(keyObjectId(keyId), fp);
+  }
+
+  private List<PGPPublicKeyRing> get(ObjectId keyObjectId, byte[] fp) throws IOException {
+    Note note = notes.getNote(keyObjectId);
     if (note == null) {
       return Collections.emptyList();
     }
 
+    return readKeysFromNote(note, fp);
+  }
+
+  private List<PGPPublicKeyRing> readKeysFromNote(Note note, byte[] fp)
+      throws IOException, MissingObjectException, IncorrectObjectTypeException {
+    boolean foundAtLeastOneKey = false;
     List<PGPPublicKeyRing> keys = new ArrayList<>();
-    try (InputStream in = reader.open(note.getData(), OBJ_BLOB).openStream()) {
+    ObjectId data = note.getData();
+    try (InputStream stream = reader.open(data, OBJ_BLOB).openStream()) {
+      byte[] bytes = ByteStreams.toByteArray(stream);
+      InputStream in = new ByteArrayInputStream(bytes);
       while (true) {
         @SuppressWarnings("unchecked")
         Iterator<Object> it = new BcPGPObjectFactory(new ArmoredInputStream(in)).iterator();
         if (!it.hasNext()) {
           break;
         }
+        foundAtLeastOneKey = true;
         Object obj = it.next();
         if (obj instanceof PGPPublicKeyRing) {
           PGPPublicKeyRing kr = (PGPPublicKeyRing) obj;
@@ -229,7 +260,34 @@
         }
         checkState(!it.hasNext(), "expected one PGP object per ArmoredInputStream");
       }
-      return keys;
+
+      if (foundAtLeastOneKey) {
+        return keys;
+      }
+
+      // Subkey handling
+      String id = new String(bytes, UTF_8);
+      Preconditions.checkArgument(ObjectId.isId(id), "Not valid SHA1: " + id);
+      return get(ObjectId.fromString(id), fp);
+    }
+  }
+
+  public void rebuildSubkeyMasterKeyMap()
+      throws MissingObjectException, IncorrectObjectTypeException, IOException, PGPException {
+    if (reader == null) {
+      load();
+    }
+    if (notes != null) {
+      try (ObjectInserter ins = repo.newObjectInserter()) {
+        for (Note note : notes) {
+          for (PGPPublicKeyRing keyRing :
+              new PGPPublicKeyRingCollection(readKeysFromNote(note, null))) {
+            long masterKeyId = keyRing.getPublicKey().getKeyID();
+            ObjectId masterKeyObjectId = keyObjectId(masterKeyId);
+            saveSubkeyMapping(ins, keyRing, masterKeyId, masterKeyObjectId);
+          }
+        }
+      }
     }
   }
 
@@ -348,8 +406,8 @@
 
   private void saveToNotes(ObjectInserter ins, PGPPublicKeyRing keyRing)
       throws PGPException, IOException {
-    long keyId = keyRing.getPublicKey().getKeyID();
-    PGPPublicKeyRingCollection existing = get(keyId);
+    long masterKeyId = keyRing.getPublicKey().getKeyID();
+    PGPPublicKeyRingCollection existing = get(masterKeyId);
     List<PGPPublicKeyRing> toWrite = new ArrayList<>(existing.size() + 1);
     boolean replaced = false;
     for (PGPPublicKeyRing kr : existing) {
@@ -363,7 +421,37 @@
     if (!replaced) {
       toWrite.add(keyRing);
     }
-    notes.set(keyObjectId(keyId), ins.insert(OBJ_BLOB, keysToArmored(toWrite)));
+
+    ObjectId masterKeyObjectId = keyObjectId(masterKeyId);
+    notes.set(masterKeyObjectId, ins.insert(OBJ_BLOB, keysToArmored(toWrite)));
+
+    saveSubkeyMapping(ins, keyRing, masterKeyId, masterKeyObjectId);
+  }
+
+  private void saveSubkeyMapping(
+      ObjectInserter ins, PGPPublicKeyRing keyRing, long masterKeyId, ObjectId masterKeyObjectId)
+      throws IOException {
+    // Subkey handling
+    byte[] masterKeyBytes = masterKeyObjectId.name().getBytes(UTF_8);
+    ObjectId masterKeyObject = null;
+    for (PGPPublicKey key : keyRing) {
+      long subKeyId = key.getKeyID();
+      // Skip master public key
+      if (masterKeyId == subKeyId) {
+        continue;
+      }
+
+      // Insert master key object only once for all subkeys
+      if (masterKeyObject == null) {
+        masterKeyObject = ins.insert(OBJ_BLOB, masterKeyBytes);
+      }
+
+      ObjectId subkeyObjectId = keyObjectId(subKeyId);
+      Preconditions.checkArgument(
+          notes.get(subkeyObjectId) == null || notes.get(subkeyObjectId).equals(masterKeyObject),
+          "Master key differs for subkey: " + subkeyObjectId.name());
+      notes.set(subkeyObjectId, masterKeyObject);
+    }
   }
 
   private void deleteFromNotes(ObjectInserter ins, Fingerprint fp)
@@ -378,10 +466,24 @@
     }
     if (toWrite.size() == existing.size()) {
       return;
-    } else if (!toWrite.isEmpty()) {
-      notes.set(keyObjectId(keyId), ins.insert(OBJ_BLOB, keysToArmored(toWrite)));
+    }
+
+    ObjectId keyObjectId = keyObjectId(keyId);
+    if (!toWrite.isEmpty()) {
+      notes.set(keyObjectId, ins.insert(OBJ_BLOB, keysToArmored(toWrite)));
     } else {
-      notes.remove(keyObjectId(keyId));
+      PGPPublicKeyRing keyRing = get(fp.get());
+
+      for (PGPPublicKey key : keyRing) {
+        long subKeyId = key.getKeyID();
+        // Skip master public key
+        if (keyId == subKeyId) {
+          continue;
+        }
+        notes.remove(keyObjectId(subKeyId));
+      }
+
+      notes.remove(keyObjectId);
     }
   }
 
diff --git a/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java b/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java
index 967259a..b7b03db 100644
--- a/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java
+++ b/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java
@@ -28,7 +28,6 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.GpgApiAdapter;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -67,7 +66,7 @@
       throws RestApiException, GpgException {
     try {
       return gpgKeys.get().list().apply(account);
-    } catch (OrmException | PGPException | IOException e) {
+    } catch (PGPException | IOException e) {
       throw new GpgException(e);
     }
   }
@@ -81,7 +80,7 @@
     in.delete = delete;
     try {
       return postGpgKeys.get().apply(account, in);
-    } catch (PGPException | OrmException | IOException | ConfigInvalidException e) {
+    } catch (PGPException | IOException | ConfigInvalidException e) {
       throw new GpgException(e);
     }
   }
@@ -91,7 +90,7 @@
       throws RestApiException, GpgException {
     try {
       return gpgKeyApiFactory.create(gpgKeys.get().parse(account, idStr));
-    } catch (PGPException | OrmException | IOException e) {
+    } catch (PGPException | IOException e) {
       throw new GpgException(e);
     }
   }
diff --git a/java/com/google/gerrit/gpg/api/GpgKeyApiImpl.java b/java/com/google/gerrit/gpg/api/GpgKeyApiImpl.java
index 25b472d..cf09acf 100644
--- a/java/com/google/gerrit/gpg/api/GpgKeyApiImpl.java
+++ b/java/com/google/gerrit/gpg/api/GpgKeyApiImpl.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.gpg.server.DeleteGpgKey;
 import com.google.gerrit.gpg.server.GpgKey;
 import com.google.gerrit.gpg.server.GpgKeys;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
@@ -57,7 +56,7 @@
   public void delete() throws RestApiException {
     try {
       delete.apply(rsrc, new Input());
-    } catch (PGPException | OrmException | IOException | ConfigInvalidException e) {
+    } catch (PGPException | IOException | ConfigInvalidException e) {
       throw new RestApiException("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 b50dfeb..68bd2d9 100644
--- a/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
+++ b/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
@@ -20,9 +20,9 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.io.BaseEncoding;
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.common.Input;
-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;
@@ -34,7 +34,6 @@
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.mail.send.DeleteKeySender;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -71,7 +70,7 @@
 
   @Override
   public Response<?> apply(GpgKey rsrc, Input input)
-      throws RestApiException, PGPException, OrmException, IOException, ConfigInvalidException {
+      throws RestApiException, PGPException, IOException, ConfigInvalidException {
     PGPPublicKey key = rsrc.getKeyRing().getPublicKey();
     String fingerprint = BaseEncoding.base16().encode(key.getFingerprint());
     Optional<ExternalId> extId = externalIds.get(ExternalId.Key.create(SCHEME_GPGKEY, fingerprint));
@@ -109,9 +108,9 @@
                 rsrc.getUser().getAccount().getPreferredEmail());
           }
           break;
+        case LOCK_FAILURE:
         case FORCED:
         case IO_FAILURE:
-        case LOCK_FAILURE:
         case NEW:
         case NOT_ATTEMPTED:
         case REJECTED:
@@ -120,7 +119,7 @@
         case REJECTED_MISSING_OBJECT:
         case REJECTED_OTHER_REASON:
         default:
-          throw new ResourceConflictException("Failed to delete public key: " + saveResult);
+          throw new StorageException(String.format("Failed to delete public key: %s", saveResult));
       }
     }
     return Response.none();
diff --git a/java/com/google/gerrit/gpg/server/GpgKeys.java b/java/com/google/gerrit/gpg/server/GpgKeys.java
index e555f07..16592f8 100644
--- a/java/com/google/gerrit/gpg/server/GpgKeys.java
+++ b/java/com/google/gerrit/gpg/server/GpgKeys.java
@@ -39,7 +39,6 @@
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIds;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -86,7 +85,7 @@
 
   @Override
   public GpgKey parse(AccountResource parent, IdString id)
-      throws ResourceNotFoundException, PGPException, OrmException, IOException {
+      throws ResourceNotFoundException, PGPException, IOException {
     checkVisible(self, parent);
 
     ExternalId gpgKeyExtId = findGpgKey(id.get(), getGpgExtIds(parent));
@@ -142,7 +141,7 @@
   public class ListGpgKeys implements RestReadView<AccountResource> {
     @Override
     public Map<String, GpgKeyInfo> apply(AccountResource rsrc)
-        throws OrmException, PGPException, IOException, ResourceNotFoundException {
+        throws PGPException, IOException, ResourceNotFoundException {
       checkVisible(self, rsrc);
       Map<String, GpgKeyInfo> keys = new HashMap<>();
       try (PublicKeyStore store = storeProvider.get()) {
diff --git a/java/com/google/gerrit/gpg/server/PostGpgKeys.java b/java/com/google/gerrit/gpg/server/PostGpgKeys.java
index 2a7de58..0bb4ff7 100644
--- a/java/com/google/gerrit/gpg/server/PostGpgKeys.java
+++ b/java/com/google/gerrit/gpg/server/PostGpgKeys.java
@@ -27,7 +27,8 @@
 import com.google.common.collect.Maps;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.io.BaseEncoding;
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.accounts.GpgKeysInput;
 import com.google.gerrit.extensions.common.GpgKeyInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -52,7 +53,6 @@
 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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -113,7 +113,7 @@
   @Override
   public Map<String, GpgKeyInfo> apply(AccountResource rsrc, GpgKeysInput input)
       throws ResourceNotFoundException, BadRequestException, ResourceConflictException,
-          PGPException, OrmException, IOException, ConfigInvalidException {
+          PGPException, IOException, ConfigInvalidException {
     GpgKeys.checkVisible(self, rsrc);
 
     Collection<ExternalId> existingExtIds =
@@ -196,7 +196,7 @@
 
   private void storeKeys(
       AccountResource rsrc, List<PGPPublicKeyRing> keyRings, Collection<Fingerprint> toRemove)
-      throws BadRequestException, ResourceConflictException, PGPException, IOException {
+      throws BadRequestException, PGPException, IOException {
     try (PublicKeyStore store = storeProvider.get()) {
       List<String> addedKeys = new ArrayList<>();
       IdentifiedUser user = rsrc.getUser();
@@ -249,8 +249,8 @@
           break;
         case NO_CHANGE:
           break;
-        case IO_FAILURE:
         case LOCK_FAILURE:
+        case IO_FAILURE:
         case NOT_ATTEMPTED:
         case REJECTED:
         case REJECTED_CURRENT_BRANCH:
@@ -258,8 +258,7 @@
         case REJECTED_MISSING_OBJECT:
         case REJECTED_OTHER_REASON:
         default:
-          // TODO(dborowitz): Backoff and retry on LOCK_FAILURE.
-          throw new ResourceConflictException("Failed to save public keys: " + saveResult);
+          throw new StorageException(String.format("Failed to save public keys: %s", saveResult));
       }
     }
   }
@@ -268,7 +267,7 @@
     return ExternalId.Key.create(SCHEME_GPGKEY, BaseEncoding.base16().encode(fp));
   }
 
-  private Account getAccountByExternalId(ExternalId.Key extIdKey) throws OrmException {
+  private Account getAccountByExternalId(ExternalId.Key extIdKey) {
     List<AccountState> accountStates = accountQueryProvider.get().byExternalId(extIdKey);
 
     if (accountStates.isEmpty()) {
diff --git a/java/com/google/gerrit/gpg/testing/TestKeys.java b/java/com/google/gerrit/gpg/testing/TestKeys.java
index 00acedb..de66889 100644
--- a/java/com/google/gerrit/gpg/testing/TestKeys.java
+++ b/java/com/google/gerrit/gpg/testing/TestKeys.java
@@ -1029,4 +1029,81 @@
             + "=JxsF\n"
             + "-----END PGP PRIVATE KEY BLOCK-----\n");
   }
+
+  /**
+   * Master Key without expiration with subkey with expiration.
+   *
+   * <pre>
+   * pub   rsa1024 2018-11-17 [C]
+   *       5734 2C37 982A 843B 19C0  622B 6AAF 2D26 B481 02DB
+   * uid            [ultimate] Testuser 10 <testuser10@example.com>
+   * sub   rsa1024 2018-11-17 [S] [expires: 2065-11-05]
+   *       0A4A 9660 1B96 2DFC E898  E686 4305 C92E 626E B485
+   * </pre>
+   */
+  public static TestKey validKeyWithoutExpirationWithSubkeyWithExpiration() throws Exception {
+    return new TestKey(
+        "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+            + "\n"
+            + "mI0EW+98GgEEALLn87xX++daic3AzKwM7nY50Mx2eTaEZPlnaDAVvFlhbZuPG+n5\n"
+            + "g93vYX3wEfnFxI7IBEe7VMT1AyszLZgpFmbzW8eGQxCGpRd1hYrUUlC0IkGAwG9v\n"
+            + "LQB85GDDZUUH4p+A4oHX0yUm8iCpbO9+D82xzNDe/D8Xbw1foWMWGonLABEBAAG0\n"
+            + "JFRlc3R1c2VyIDEwIDx0ZXN0dXNlcjEwQGV4YW1wbGUuY29tPojOBBMBCAA4FiEE\n"
+            + "VzQsN5gqhDsZwGIraq8tJrSBAtsFAlvvfBoCGwEFCwkIBwIGFQoJCAsCBBYCAwEC\n"
+            + "HgECF4AACgkQaq8tJrSBAtvfFAP/VqV77KQZp9rjSGStDpxxlatr4Y5nrRBZfV5v\n"
+            + "jpAjwusIHRjr0OWXLxX7NDLYd+oIjhLFn26Lux1UXOQT+rGRPwnxoJZWrpDQidP7\n"
+            + "fDgfqnNa5UQGvoBPSIVEK1l0DlYAOUuciwz3HdMkeMuvEVEdyg7nOiVd1bF9V9i/\n"
+            + "8v7ABV24jQRb73xXAQQAssv5gwxWx5J0q4gGcqMIaJKzBaHAjiK3ryH6qnFQpsf1\n"
+            + "ODtU+a4NxFJsXGOd6jHEhBEHPgWAaaKZ7PEJVnwA/XOhPG+q9YimAbbZS0qmC/LH\n"
+            + "DpFtFbsJsMKZbIC69j9OcbmalIowspFQBVeAankGFReZVhh99Z/o81Y+Twm9eisA\n"
+            + "EQEAAYkBcQQYAQgAJhYhBFc0LDeYKoQ7GcBiK2qvLSa0gQLbBQJb73xXAhsCBQlY\n"
+            + "WHSAAL8JEGqvLSa0gQLbtCAEGQEIAB0WIQQKSpZgG5Yt/OiY5oZDBckuYm60hQUC\n"
+            + "W+98VwAKCRBDBckuYm60hafuBACSkvwXAYfxvAf7IOK6+Sp3oWkrq6vsjH5K7oup\n"
+            + "TimR1cVgN1CEAWh82UBCg3zR5Q5BAnvnjeugdQVrAY+sftkaoy8qO5YYHCPtHtQK\n"
+            + "mXGWM7Q33hU1E7IfgU06qkFLhIOL3Vwr8jOuOzHv0M3PbLNr7lOCaIJ7uCjPBZuo\n"
+            + "qRwqjHt2BACuXwA8RHbRGAxC65YgoSjGNu/da3q2J26E57KfSFprQ4TzAg33U4Ws\n"
+            + "qdx2vbbJxy1bfTYxb0AYXe/+k23W7EIdBtMGOYwrX01oTfSIKbM+gDrHswSYXdOy\n"
+            + "ziatLaUBSQfyG656lGGZO/aArLZb4dgGeBHBwhXTufFDIYl3X94zfQ==\n"
+            + "=BG9Z\n"
+            + "-----END PGP PUBLIC KEY BLOCK-----",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+            + "\n"
+            + "lQIGBFvvfBoBBACy5/O8V/vnWonNwMysDO52OdDMdnk2hGT5Z2gwFbxZYW2bjxvp\n"
+            + "+YPd72F98BH5xcSOyARHu1TE9QMrMy2YKRZm81vHhkMQhqUXdYWK1FJQtCJBgMBv\n"
+            + "by0AfORgw2VFB+KfgOKB19MlJvIgqWzvfg/NsczQ3vw/F28NX6FjFhqJywARAQAB\n"
+            + "/gcDAmvJS+mrsxlf7D58lnhHw8gBjP5JkY9anTgVhIdieJEIlitV4PyRQYPAGZZd\n"
+            + "asUC7bC7WnLCygiU59kXS5z63Ue/RM0tVwWy0FsigpseC90Mtwb4wjL2jTeebszD\n"
+            + "UcM33d6Tg9s4eNnsHzpmlC/CReW6MYJj0/06AsvgUgOxgWXf0YapOLRIr60reTUb\n"
+            + "ovVZtH76rsZXyQvR9qJv11F+BmIDDzg4EsipXDGVuEZ0SXJyq6OLAUPkV2ZdELaT\n"
+            + "P4RWp0Zsn22H8jm4MsZ7la2Ux3jD2AMdy2B9dpwhuxOegDSXUMRfXYwxQ1wioqpA\n"
+            + "pOZ1RjjFsID34XNtxGp3wMYcFleOl3CSpXyW/P1PYTVBQta/Y7xniEetzUk9NHVc\n"
+            + "2jMD8a8767+Tk0SChIPmWOhQYrHS1Ce309SjTRSRVexjKF0Mp5WXHxqz582IlPT9\n"
+            + "jdxvLjVIW4xbtZmnQ2JnPHInWbxoa3exaoPg5osvg8h7QlGxRY6H2/20JFRlc3R1\n"
+            + "c2VyIDEwIDx0ZXN0dXNlcjEwQGV4YW1wbGUuY29tPojOBBMBCAA4FiEEVzQsN5gq\n"
+            + "hDsZwGIraq8tJrSBAtsFAlvvfBoCGwEFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AA\n"
+            + "CgkQaq8tJrSBAtvfFAP/VqV77KQZp9rjSGStDpxxlatr4Y5nrRBZfV5vjpAjwusI\n"
+            + "HRjr0OWXLxX7NDLYd+oIjhLFn26Lux1UXOQT+rGRPwnxoJZWrpDQidP7fDgfqnNa\n"
+            + "5UQGvoBPSIVEK1l0DlYAOUuciwz3HdMkeMuvEVEdyg7nOiVd1bF9V9i/8v7ABV2d\n"
+            + "AgYEW+98VwEEALLL+YMMVseSdKuIBnKjCGiSswWhwI4it68h+qpxUKbH9Tg7VPmu\n"
+            + "DcRSbFxjneoxxIQRBz4FgGmimezxCVZ8AP1zoTxvqvWIpgG22UtKpgvyxw6RbRW7\n"
+            + "CbDCmWyAuvY/TnG5mpSKMLKRUAVXgGp5BhUXmVYYffWf6PNWPk8JvXorABEBAAH+\n"
+            + "BwMClgBcY/ItnafslgEWZOSqsxCSJatN72c9zzWZE+zmcx9NbDRuCVxXhTbHJZZs\n"
+            + "Hz44vsKqOKtyhrfr9Oke0mYyzH2CX6tv6ghJyC6znRCGoc/P84uJ1v3ibO/7/p85\n"
+            + "PDHzEEpHmdbef+UymnjZBYKGi45SINy3bLwOa/Vl80Q4wsppPe9oynerq6ig94HR\n"
+            + "e3mNDw/JggtgJA0X2VmmmXG8vHwIB5EziQrH7QGtLyjqdE+w7CLbbvAskL8Uw1qx\n"
+            + "Aowdpb7J8hrUdIDDCr/mlhT17+UM5yOXHKcixyrscqbjlG/nqwPvR10efo7D0rFR\n"
+            + "6tu5OU2y3N2PhGOysDLgupUXBLlpdByF6AYNV9zvU7ipO7QXzrUfYCb/WyAcjl+X\n"
+            + "Yl38sCVTVFGsB2ql9/fzFCxAB3FUNHDlI2sUbkdDPcjgf65SK0GGcckWfntfq9dj\n"
+            + "pQzEVen8X9dT3UhfuvHd98g3n6ju9gh8NucwHM5jITq9ItTY0whb+okBcQQYAQgA\n"
+            + "JhYhBFc0LDeYKoQ7GcBiK2qvLSa0gQLbBQJb73xXAhsCBQlYWHSAAL8JEGqvLSa0\n"
+            + "gQLbtCAEGQEIAB0WIQQKSpZgG5Yt/OiY5oZDBckuYm60hQUCW+98VwAKCRBDBcku\n"
+            + "Ym60hafuBACSkvwXAYfxvAf7IOK6+Sp3oWkrq6vsjH5K7oupTimR1cVgN1CEAWh8\n"
+            + "2UBCg3zR5Q5BAnvnjeugdQVrAY+sftkaoy8qO5YYHCPtHtQKmXGWM7Q33hU1E7If\n"
+            + "gU06qkFLhIOL3Vwr8jOuOzHv0M3PbLNr7lOCaIJ7uCjPBZuoqRwqjHt2BACuXwA8\n"
+            + "RHbRGAxC65YgoSjGNu/da3q2J26E57KfSFprQ4TzAg33U4Wsqdx2vbbJxy1bfTYx\n"
+            + "b0AYXe/+k23W7EIdBtMGOYwrX01oTfSIKbM+gDrHswSYXdOyziatLaUBSQfyG656\n"
+            + "lGGZO/aArLZb4dgGeBHBwhXTufFDIYl3X94zfQ==\n"
+            + "=RbPm\n"
+            + "-----END PGP PRIVATE KEY BLOCK-----");
+  }
 }
diff --git a/java/com/google/gerrit/httpd/BUILD b/java/com/google/gerrit/httpd/BUILD
index 9f39afa..96c56c8 100644
--- a/java/com/google/gerrit/httpd/BUILD
+++ b/java/com/google/gerrit/httpd/BUILD
@@ -12,7 +12,10 @@
     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/git",
+        "//java/com/google/gerrit/json",
         "//java/com/google/gerrit/launcher",
         "//java/com/google/gerrit/lifecycle",
         "//java/com/google/gerrit/metrics",
@@ -26,12 +29,9 @@
         "//java/com/google/gerrit/server/util/time",
         "//java/com/google/gerrit/util/cli",
         "//java/com/google/gerrit/util/http",
-        "//java/org/eclipse/jgit:server",
         "//lib:args4j",
         "//lib:gson",
         "//lib:guava",
-        "//lib:gwtjsonrpc",
-        "//lib:gwtorm",
         "//lib:jsch",
         "//lib:servlet-api-3_1",
         "//lib:soy",
diff --git a/java/com/google/gerrit/httpd/CacheBasedWebSession.java b/java/com/google/gerrit/httpd/CacheBasedWebSession.java
index 6a19be7..bbe15b5 100644
--- a/java/com/google/gerrit/httpd/CacheBasedWebSession.java
+++ b/java/com/google/gerrit/httpd/CacheBasedWebSession.java
@@ -18,7 +18,6 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.HostPageData;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.httpd.WebSessionManager.Key;
 import com.google.gerrit.httpd.WebSessionManager.Val;
@@ -96,7 +95,7 @@
   private void authFromCookie(String cookie) {
     key = new Key(cookie);
     val = manager.get(key);
-    String token = request.getHeader(HostPageData.XSRF_HEADER_NAME);
+    String token = request.getHeader(XsrfConstants.XSRF_HEADER_NAME);
     if (val != null && token != null && token.equals(val.getAuth())) {
       okPaths.add(AccessPath.REST_API);
     }
diff --git a/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index 08ff8a7..c97ee10 100644
--- a/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -14,8 +14,9 @@
 
 package com.google.gerrit.httpd;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.cache.Cache;
-import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.Capable;
@@ -38,6 +39,7 @@
 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.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.util.time.TimeUtil;
@@ -51,7 +53,9 @@
 import java.time.Duration;
 import java.util.Collections;
 import java.util.HashSet;
+import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.atomic.AtomicLong;
 import javax.servlet.Filter;
 import javax.servlet.FilterChain;
 import javax.servlet.FilterConfig;
@@ -60,6 +64,7 @@
 import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletResponseWrapper;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.http.server.GitServlet;
 import org.eclipse.jgit.http.server.GitSmartHttpTools;
@@ -126,6 +131,74 @@
                   .expireAfterWrite(Duration.ofMinutes(10));
             }
           });
+
+      // Don't bind Metrics, which is bound in a parent injector in tests.
+    }
+  }
+
+  @VisibleForTesting
+  @Singleton
+  public static class Metrics {
+    // Recording requests separately in this class is only necessary because of a bug in the
+    // implementation of the generic RequestMetricsFilter; see
+    // https://gerrit-review.googlesource.com/c/gerrit/+/211692
+    private final AtomicLong requestsStarted = new AtomicLong();
+
+    void requestStarted() {
+      requestsStarted.incrementAndGet();
+    }
+
+    public long getRequestsStarted() {
+      return requestsStarted.get();
+    }
+  }
+
+  static class HttpServletResponseWithStatusWrapper extends HttpServletResponseWrapper {
+    private int responseStatus;
+
+    HttpServletResponseWithStatusWrapper(HttpServletResponse response) {
+      super(response);
+      /* Even if we could read the status from response, we assume that it is all
+       * fine because we entered the filter without any prior issues.
+       * When Google will have upgraded to Servlet 3.0, we could actually
+       * call response.getStatus() and the code will be clearer.
+       */
+      responseStatus = HttpServletResponse.SC_OK;
+    }
+
+    @Override
+    public void setStatus(int sc) {
+      responseStatus = sc;
+      super.setStatus(sc);
+    }
+
+    @SuppressWarnings("deprecation")
+    @Override
+    public void setStatus(int sc, String sm) {
+      responseStatus = sc;
+      super.setStatus(sc, sm);
+    }
+
+    @Override
+    public void sendError(int sc) throws IOException {
+      this.responseStatus = sc;
+      super.sendError(sc);
+    }
+
+    @Override
+    public void sendError(int sc, String msg) throws IOException {
+      this.responseStatus = sc;
+      super.sendError(sc, msg);
+    }
+
+    @Override
+    public void sendRedirect(String location) throws IOException {
+      this.responseStatus = HttpServletResponse.SC_MOVED_TEMPORARILY;
+      super.sendRedirect(location);
+    }
+
+    public int getResponseStatus() {
+      return responseStatus;
     }
   }
 
@@ -155,19 +228,15 @@
   }
 
   private static ListMultimap<String, String> extractParameters(HttpServletRequest request) {
-
-    ListMultimap<String, String> multiMap = ArrayListMultimap.create();
-    if (request.getQueryString() != null) {
-      request
-          .getParameterMap()
-          .forEach(
-              (k, v) -> {
-                for (int i = 0; i < v.length; i++) {
-                  multiMap.put(k, v[i]);
-                }
-              });
+    if (request.getQueryString() == null) {
+      return ImmutableListMultimap.of();
     }
-    return multiMap;
+    // Explicit cast is required to compile under Servlet API 2.5, where the return type is raw Map.
+    @SuppressWarnings("cast")
+    Map<String, String[]> parameterMap = (Map<String, String[]>) request.getParameterMap();
+    ImmutableListMultimap.Builder<String, String> b = ImmutableListMultimap.builder();
+    parameterMap.forEach(b::putAll);
+    return b.build();
   }
 
   static class Resolver implements RepositoryResolver<HttpServletRequest> {
@@ -237,14 +306,14 @@
     private final TransferConfig config;
     private final DynamicSet<PreUploadHook> preUploadHooks;
     private final DynamicSet<PostUploadHook> postUploadHooks;
-    private final DynamicSet<UploadPackInitializer> uploadPackInitializers;
+    private final PluginSetContext<UploadPackInitializer> uploadPackInitializers;
 
     @Inject
     UploadFactory(
         TransferConfig tc,
         DynamicSet<PreUploadHook> preUploadHooks,
         DynamicSet<PostUploadHook> postUploadHooks,
-        DynamicSet<UploadPackInitializer> uploadPackInitializers) {
+        PluginSetContext<UploadPackInitializer> uploadPackInitializers) {
       this.config = tc;
       this.preUploadHooks = preUploadHooks;
       this.postUploadHooks = postUploadHooks;
@@ -259,9 +328,7 @@
       up.setPreUploadHook(PreUploadHookChain.newChain(Lists.newArrayList(preUploadHooks)));
       up.setPostUploadHook(PostUploadHookChain.newChain(Lists.newArrayList(postUploadHooks)));
       ProjectState state = (ProjectState) req.getAttribute(ATT_STATE);
-      for (UploadPackInitializer initializer : uploadPackInitializers) {
-        initializer.init(state.getNameKey(), up);
-      }
+      uploadPackInitializers.runEach(initializer -> initializer.init(state.getNameKey(), up));
       return up;
     }
   }
@@ -271,63 +338,74 @@
     private final PermissionBackend permissionBackend;
     private final Provider<CurrentUser> userProvider;
     private final GroupAuditService groupAuditService;
+    private final Metrics metrics;
 
     @Inject
     UploadFilter(
         UploadValidators.Factory uploadValidatorsFactory,
         PermissionBackend permissionBackend,
         Provider<CurrentUser> userProvider,
-        GroupAuditService groupAuditService) {
+        GroupAuditService groupAuditService,
+        Metrics metrics) {
       this.uploadValidatorsFactory = uploadValidatorsFactory;
       this.permissionBackend = permissionBackend;
       this.userProvider = userProvider;
       this.groupAuditService = groupAuditService;
+      this.metrics = metrics;
     }
 
     @Override
     public void doFilter(ServletRequest request, ServletResponse response, FilterChain next)
         throws IOException, ServletException {
+      metrics.requestStarted();
       // The Resolver above already checked READ access for us.
       Repository repo = ServletUtils.getRepository(request);
       ProjectState state = (ProjectState) request.getAttribute(ATT_STATE);
       UploadPack up = (UploadPack) request.getAttribute(ServletUtils.ATTRIBUTE_HANDLER);
       PermissionBackend.ForProject perm =
           permissionBackend.currentUser().project(state.getNameKey());
+      HttpServletResponseWithStatusWrapper responseWrapper =
+          new HttpServletResponseWithStatusWrapper((HttpServletResponse) response);
+      HttpServletRequest httpRequest = (HttpServletRequest) request;
+      String sessionId = httpRequest.getSession().getId();
+
       try {
-        perm.check(ProjectPermission.RUN_UPLOAD_PACK);
-      } catch (AuthException e) {
-        GitSmartHttpTools.sendError(
-            (HttpServletRequest) request,
-            (HttpServletResponse) response,
-            HttpServletResponse.SC_FORBIDDEN,
-            "upload-pack not permitted on this server");
-        return;
-      } catch (PermissionBackendException e) {
-        throw new ServletException(e);
+        try {
+          perm.check(ProjectPermission.RUN_UPLOAD_PACK);
+        } catch (AuthException e) {
+          GitSmartHttpTools.sendError(
+              (HttpServletRequest) request,
+              responseWrapper,
+              HttpServletResponse.SC_FORBIDDEN,
+              "upload-pack not permitted on this server");
+          return;
+        } catch (PermissionBackendException e) {
+          responseWrapper.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+          throw new ServletException(e);
+        }
+
+        // We use getRemoteHost() here instead of getRemoteAddr() because REMOTE_ADDR
+        // may have been overridden by a proxy server -- we'll try to avoid this.
+        UploadValidators uploadValidators =
+            uploadValidatorsFactory.create(state.getProject(), repo, request.getRemoteHost());
+        up.setPreUploadHook(
+            PreUploadHookChain.newChain(
+                Lists.newArrayList(up.getPreUploadHook(), uploadValidators)));
+        up.setAdvertiseRefsHook(new DefaultAdvertiseRefsHook(perm, RefFilterOptions.defaults()));
+        next.doFilter(httpRequest, responseWrapper);
       } finally {
-        HttpServletRequest httpRequest = (HttpServletRequest) request;
-        HttpServletResponse httpResponse = (HttpServletResponse) response;
         groupAuditService.dispatch(
             new HttpAuditEvent(
-                httpRequest.getSession().getId(),
+                sessionId,
                 userProvider.get(),
                 extractWhat(httpRequest),
                 TimeUtil.nowMs(),
                 extractParameters(httpRequest),
                 httpRequest.getMethod(),
                 httpRequest,
-                httpResponse.getStatus(),
-                httpResponse));
+                responseWrapper.getResponseStatus(),
+                responseWrapper));
       }
-
-      // We use getRemoteHost() here instead of getRemoteAddr() because REMOTE_ADDR
-      // may have been overridden by a proxy server -- we'll try to avoid this.
-      UploadValidators uploadValidators =
-          uploadValidatorsFactory.create(state.getProject(), repo, request.getRemoteHost());
-      up.setPreUploadHook(
-          PreUploadHookChain.newChain(Lists.newArrayList(up.getPreUploadHook(), uploadValidators)));
-      up.setAdvertiseRefsHook(new DefaultAdvertiseRefsHook(perm, RefFilterOptions.defaults()));
-      next.doFilter(request, response);
     }
 
     @Override
@@ -378,22 +456,26 @@
     private final PermissionBackend permissionBackend;
     private final Provider<CurrentUser> userProvider;
     private final GroupAuditService groupAuditService;
+    private final Metrics metrics;
 
     @Inject
     ReceiveFilter(
         @Named(ID_CACHE) Cache<AdvertisedObjectsCacheKey, Set<ObjectId>> cache,
         PermissionBackend permissionBackend,
         Provider<CurrentUser> userProvider,
-        GroupAuditService groupAuditService) {
+        GroupAuditService groupAuditService,
+        Metrics metrics) {
       this.cache = cache;
       this.permissionBackend = permissionBackend;
       this.userProvider = userProvider;
       this.groupAuditService = groupAuditService;
+      this.metrics = metrics;
     }
 
     @Override
     public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
         throws IOException, ServletException {
+      metrics.requestStarted();
       boolean isGet = "GET".equalsIgnoreCase(((HttpServletRequest) request).getMethod());
 
       AsyncReceiveCommits arc = (AsyncReceiveCommits) request.getAttribute(ATT_ARC);
@@ -403,25 +485,28 @@
       rp.getAdvertiseRefsHook().advertiseRefs(rp);
 
       ProjectState state = (ProjectState) request.getAttribute(ATT_STATE);
+      HttpServletResponseWithStatusWrapper responseWrapper =
+          new HttpServletResponseWithStatusWrapper((HttpServletResponse) response);
+      HttpServletRequest httpRequest = (HttpServletRequest) request;
       Capable canUpload;
       try {
-        permissionBackend
-            .currentUser()
-            .project(state.getNameKey())
-            .check(ProjectPermission.RUN_RECEIVE_PACK);
-        canUpload = arc.canUpload();
-      } catch (AuthException e) {
-        GitSmartHttpTools.sendError(
-            (HttpServletRequest) request,
-            (HttpServletResponse) response,
-            HttpServletResponse.SC_FORBIDDEN,
-            "receive-pack not permitted on this server");
-        return;
-      } catch (PermissionBackendException e) {
-        throw new RuntimeException(e);
+        try {
+          permissionBackend
+              .currentUser()
+              .project(state.getNameKey())
+              .check(ProjectPermission.RUN_RECEIVE_PACK);
+          canUpload = arc.canUpload();
+        } catch (AuthException e) {
+          GitSmartHttpTools.sendError(
+              httpRequest,
+              responseWrapper,
+              HttpServletResponse.SC_FORBIDDEN,
+              "receive-pack not permitted on this server");
+          return;
+        } catch (PermissionBackendException e) {
+          throw new RuntimeException(e);
+        }
       } finally {
-        HttpServletRequest httpRequest = (HttpServletRequest) request;
-        HttpServletResponse httpResponse = (HttpServletResponse) response;
         groupAuditService.dispatch(
             new HttpAuditEvent(
                 httpRequest.getSession().getId(),
@@ -431,26 +516,26 @@
                 extractParameters(httpRequest),
                 httpRequest.getMethod(),
                 httpRequest,
-                httpResponse.getStatus(),
-                httpResponse));
+                responseWrapper.getResponseStatus(),
+                responseWrapper));
       }
 
       if (canUpload != Capable.OK) {
         GitSmartHttpTools.sendError(
-            (HttpServletRequest) request,
-            (HttpServletResponse) response,
+            httpRequest,
+            responseWrapper,
             HttpServletResponse.SC_FORBIDDEN,
             "\n" + canUpload.getMessage());
         return;
       }
 
       if (!rp.isCheckReferencedObjectsAreReachable()) {
-        chain.doFilter(request, response);
+        chain.doFilter(request, responseWrapper);
         return;
       }
 
       if (!(userProvider.get().isIdentifiedUser())) {
-        chain.doFilter(request, response);
+        chain.doFilter(request, responseWrapper);
         return;
       }
 
@@ -467,7 +552,7 @@
         }
       }
 
-      chain.doFilter(request, response);
+      chain.doFilter(request, responseWrapper);
 
       if (isGet) {
         cache.put(cacheKey, Collections.unmodifiableSet(new HashSet<>(rp.getAdvertisedObjects())));
diff --git a/java/com/google/gerrit/httpd/GwtCacheControlFilter.java b/java/com/google/gerrit/httpd/GwtCacheControlFilter.java
deleted file mode 100644
index 5ac3d2f..0000000
--- a/java/com/google/gerrit/httpd/GwtCacheControlFilter.java
+++ /dev/null
@@ -1,104 +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.httpd;
-
-import com.google.gerrit.util.http.CacheHeaders;
-import com.google.inject.Singleton;
-import java.io.IOException;
-import java.util.concurrent.TimeUnit;
-import javax.servlet.Filter;
-import javax.servlet.FilterChain;
-import javax.servlet.FilterConfig;
-import javax.servlet.ServletException;
-import javax.servlet.ServletRequest;
-import javax.servlet.ServletResponse;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-/**
- * Forces GWT resources to cache for a very long time.
- *
- * <p>GWT compiled JavaScript and ImageBundles can be cached indefinitely by a browser and/or an
- * edge proxy, as they never contain user-specific data and are named by a unique checksum. If their
- * content is ever modified then the URL changes, so user agents would request a different resource.
- * We force these resources to have very long expiration times.
- *
- * <p>To use, add the following block to your {@code web.xml}:
- *
- * <pre>
- * &lt;filter&gt;
- *     &lt;filter-name&gt;CacheControl&lt;/filter-name&gt;
- *     &lt;filter-class&gt;com.google.gwtexpui.server.CacheControlFilter&lt;/filter-class&gt;
- *   &lt;/filter&gt;
- *   &lt;filter-mapping&gt;
- *     &lt;filter-name&gt;CacheControl&lt;/filter-name&gt;
- *     &lt;url-pattern&gt;/*&lt;/url-pattern&gt;
- *   &lt;/filter-mapping&gt;
- * </pre>
- */
-@Singleton
-class GwtCacheControlFilter implements Filter {
-  @Override
-  public void init(FilterConfig config) {}
-
-  @Override
-  public void destroy() {}
-
-  @Override
-  public void doFilter(final ServletRequest sreq, ServletResponse srsp, FilterChain chain)
-      throws IOException, ServletException {
-    final HttpServletRequest req = (HttpServletRequest) sreq;
-    final HttpServletResponse rsp = (HttpServletResponse) srsp;
-    final String pathInfo = pathInfo(req);
-
-    if (cacheForever(pathInfo, req)) {
-      CacheHeaders.setCacheable(req, rsp, 365, TimeUnit.DAYS);
-    } else if (nocache(pathInfo)) {
-      CacheHeaders.setNotCacheable(rsp);
-    }
-
-    chain.doFilter(req, rsp);
-  }
-
-  private static boolean cacheForever(String pathInfo, HttpServletRequest req) {
-    if (pathInfo.endsWith(".cache.html")
-        || pathInfo.endsWith(".cache.gif")
-        || pathInfo.endsWith(".cache.png")
-        || pathInfo.endsWith(".cache.css")
-        || pathInfo.endsWith(".cache.jar")
-        || pathInfo.endsWith(".cache.swf")
-        || pathInfo.endsWith(".cache.txt")
-        || pathInfo.endsWith(".cache.js")) {
-      return true;
-    } else if (pathInfo.endsWith(".nocache.js")) {
-      final String v = req.getParameter("content");
-      return v != null && v.length() > 20;
-    }
-    return false;
-  }
-
-  private static boolean nocache(String pathInfo) {
-    if (pathInfo.endsWith(".nocache.js")) {
-      return true;
-    }
-    return false;
-  }
-
-  private static String pathInfo(HttpServletRequest req) {
-    final String uri = req.getRequestURI();
-    final String ctx = req.getContextPath();
-    return uri.startsWith(ctx) ? uri.substring(ctx.length()) : uri;
-  }
-}
diff --git a/java/com/google/gerrit/httpd/HttpRequestContext.java b/java/com/google/gerrit/httpd/HttpRequestContext.java
index 6cc7a17..c30c74c 100644
--- a/java/com/google/gerrit/httpd/HttpRequestContext.java
+++ b/java/com/google/gerrit/httpd/HttpRequestContext.java
@@ -15,30 +15,20 @@
 package com.google.gerrit.httpd;
 
 import com.google.gerrit.extensions.registration.DynamicItem;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.config.RequestScopedReviewDbProvider;
 import com.google.gerrit.server.util.RequestContext;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 
 class HttpRequestContext implements RequestContext {
   private final DynamicItem<WebSession> session;
-  private final RequestScopedReviewDbProvider provider;
 
   @Inject
-  HttpRequestContext(DynamicItem<WebSession> session, RequestScopedReviewDbProvider provider) {
+  HttpRequestContext(DynamicItem<WebSession> session) {
     this.session = session;
-    this.provider = provider;
   }
 
   @Override
   public CurrentUser getUser() {
     return session.get().getUser();
   }
-
-  @Override
-  public Provider<ReviewDb> getReviewDbProvider() {
-    return provider;
-  }
 }
diff --git a/java/com/google/gerrit/httpd/NumericChangeIdRedirectServlet.java b/java/com/google/gerrit/httpd/NumericChangeIdRedirectServlet.java
new file mode 100644
index 0000000..d53a5c5
--- /dev/null
+++ b/java/com/google/gerrit/httpd/NumericChangeIdRedirectServlet.java
@@ -0,0 +1,70 @@
+// 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;
+
+import com.google.gerrit.common.PageLinks;
+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;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/** Redirects {@code domain.tld/123} to {@code domain.tld/c/project/+/123}. */
+@Singleton
+public class NumericChangeIdRedirectServlet extends HttpServlet {
+  private static final long serialVersionUID = 1L;
+
+  private final ChangesCollection changesCollection;
+
+  @Inject
+  NumericChangeIdRedirectServlet(ChangesCollection changesCollection) {
+    this.changesCollection = changesCollection;
+  }
+
+  @Override
+  protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException {
+    String idString = req.getPathInfo();
+    if (idString.endsWith("/")) {
+      idString = idString.substring(0, idString.length() - 1);
+    }
+    Change.Id id;
+    try {
+      id = Change.Id.parse(idString);
+    } catch (IllegalArgumentException e) {
+      rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
+      return;
+    }
+
+    ChangeResource changeResource;
+    try {
+      changeResource = changesCollection.parse(id);
+    } catch (ResourceConflictException | ResourceNotFoundException e) {
+      rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
+      return;
+    } catch (PermissionBackendException | RuntimeException e) {
+      throw new IOException("Unable to lookup change " + id.id, e);
+    }
+    String path =
+        PageLinks.toChange(changeResource.getProject(), changeResource.getChange().getId());
+    UrlModule.toGerrit(path, req, rsp);
+  }
+}
diff --git a/java/com/google/gerrit/httpd/RunAsFilter.java b/java/com/google/gerrit/httpd/RunAsFilter.java
index f3bf5af..15dbcab 100644
--- a/java/com/google/gerrit/httpd/RunAsFilter.java
+++ b/java/com/google/gerrit/httpd/RunAsFilter.java
@@ -21,6 +21,7 @@
 import com.google.common.flogger.FluentLogger;
 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;
@@ -28,7 +29,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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import com.google.inject.servlet.ServletModule;
@@ -103,19 +103,18 @@
         return;
       }
 
-      Account target;
+      Account.Id target;
       try {
-        target = accountResolver.find(runas);
-      } catch (OrmException | IOException | ConfigInvalidException e) {
+        target = accountResolver.resolve(runas).asUnique().getAccount().getId();
+      } catch (UnprocessableEntityException e) {
+        replyError(req, res, SC_FORBIDDEN, "no account matches " + RUN_AS, null);
+        return;
+      } catch (IOException | ConfigInvalidException | RuntimeException e) {
         logger.atWarning().withCause(e).log("cannot resolve account for %s", RUN_AS);
         replyError(req, res, SC_INTERNAL_SERVER_ERROR, "cannot resolve " + RUN_AS, e);
         return;
       }
-      if (target == null) {
-        replyError(req, res, SC_FORBIDDEN, "no account matches " + RUN_AS, null);
-        return;
-      }
-      session.get().setUserAccountId(target.getId());
+      session.get().setUserAccountId(target);
     }
 
     chain.doFilter(req, res);
diff --git a/java/com/google/gerrit/httpd/UniversalWebLoginFilter.java b/java/com/google/gerrit/httpd/UniversalWebLoginFilter.java
index 4878006..107a07e 100644
--- a/java/com/google/gerrit/httpd/UniversalWebLoginFilter.java
+++ b/java/com/google/gerrit/httpd/UniversalWebLoginFilter.java
@@ -14,10 +14,11 @@
 
 package com.google.gerrit.httpd;
 
-import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.plugincontext.PluginItemContext;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -34,8 +35,8 @@
 import javax.servlet.http.HttpServletResponse;
 
 public class UniversalWebLoginFilter implements Filter {
-  private final DynamicItem<WebSession> session;
-  private final DynamicSet<WebLoginListener> webLoginListeners;
+  private final PluginItemContext<WebSession> session;
+  private final PluginSetContext<WebLoginListener> webLoginListeners;
   private final Provider<CurrentUser> userProvider;
 
   public static ServletModule module() {
@@ -52,8 +53,8 @@
 
   @Inject
   public UniversalWebLoginFilter(
-      DynamicItem<WebSession> session,
-      DynamicSet<WebLoginListener> webLoginListeners,
+      PluginItemContext<WebSession> session,
+      PluginSetContext<WebLoginListener> webLoginListeners,
       Provider<CurrentUser> userProvider) {
     this.session = session;
     this.webLoginListeners = webLoginListeners;
@@ -75,20 +76,18 @@
     Optional<IdentifiedUser> loggedInUserAfter = loggedInUser();
 
     if (!loggedInUserBefore.isPresent() && loggedInUserAfter.isPresent()) {
-      for (WebLoginListener loginListener : webLoginListeners) {
-        loginListener.onLogin(loggedInUserAfter.get(), httpRequest, wrappedResponse);
-      }
+      webLoginListeners.runEach(
+          l -> l.onLogin(loggedInUserAfter.get(), httpRequest, wrappedResponse));
     } else if (loggedInUserBefore.isPresent() && !loggedInUserAfter.isPresent()) {
-      for (WebLoginListener loginListener : webLoginListeners) {
-        loginListener.onLogout(loggedInUserBefore.get(), httpRequest, wrappedResponse);
-      }
+      webLoginListeners.runEach(
+          l -> l.onLogout(loggedInUserBefore.get(), httpRequest, wrappedResponse));
     }
 
     wrappedResponse.play();
   }
 
   private Optional<IdentifiedUser> loggedInUser() {
-    return session.get().isSignedIn()
+    return session.call(WebSession::isSignedIn)
         ? Optional.of(userProvider.get().asIdentifiedUser())
         : Optional.empty();
   }
diff --git a/java/com/google/gerrit/httpd/UrlModule.java b/java/com/google/gerrit/httpd/UrlModule.java
index 94855b9..993a042 100644
--- a/java/com/google/gerrit/httpd/UrlModule.java
+++ b/java/com/google/gerrit/httpd/UrlModule.java
@@ -20,8 +20,6 @@
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.httpd.raw.CatServlet;
-import com.google.gerrit.httpd.raw.HostPageServlet;
-import com.google.gerrit.httpd.raw.LegacyGerritServlet;
 import com.google.gerrit.httpd.raw.SshInfoServlet;
 import com.google.gerrit.httpd.raw.ToolServlet;
 import com.google.gerrit.httpd.restapi.AccessRestApiServlet;
@@ -33,9 +31,7 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.AuthConfig;
-import com.google.gerrit.server.config.GerritOptions;
 import com.google.inject.Key;
-import com.google.inject.Provider;
 import com.google.inject.internal.UniqueAnnotations;
 import com.google.inject.servlet.ServletModule;
 import java.io.IOException;
@@ -45,27 +41,14 @@
 import org.eclipse.jgit.lib.Constants;
 
 class UrlModule extends ServletModule {
-  private GerritOptions options;
   private AuthConfig authConfig;
 
-  UrlModule(GerritOptions options, AuthConfig authConfig) {
-    this.options = options;
+  UrlModule(AuthConfig authConfig) {
     this.authConfig = authConfig;
   }
 
   @Override
   protected void configureServlets() {
-    filter("/*").through(GwtCacheControlFilter.class);
-
-    if (options.enableGwtUi()) {
-      filter("/").through(XsrfCookieFilter.class);
-      filter("/accounts/self/detail").through(XsrfCookieFilter.class);
-      serve("/").with(HostPageServlet.class);
-      serve("/Gerrit").with(LegacyGerritServlet.class);
-      serve("/Gerrit/*").with(legacyGerritScreen());
-      // Forward PolyGerrit URLs to their respective GWT equivalents.
-      serveRegex("^/(c|q|x|admin|dashboard|settings)/(.*)").with(gerritUrl());
-    }
     serve("/cat/*").with(CatServlet.class);
 
     if (authConfig.getAuthType() != AuthType.OAUTH && authConfig.getAuthType() != AuthType.OPENID) {
@@ -87,7 +70,7 @@
     serveRegex("^/settings/?$").with(screen(PageLinks.SETTINGS));
     serveRegex("^/register$").with(registerScreen(false));
     serveRegex("^/register/(.+)$").with(registerScreen(true));
-    serveRegex("^/([1-9][0-9]*)/?$").with(directChangeById());
+    serveRegex("^/([1-9][0-9]*)/?$").with(NumericChangeIdRedirectServlet.class);
     serveRegex("^/p/(.*)$").with(queryProjectNew());
     serveRegex("^/r/(.+)/?$").with(DirectChangeByCommit.class);
 
@@ -127,19 +110,6 @@
         });
   }
 
-  private Key<HttpServlet> gerritUrl() {
-    return key(
-        new HttpServlet() {
-          private static final long serialVersionUID = 1L;
-
-          @Override
-          protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException {
-            String path = req.getRequestURI().substring(req.getContextPath().length());
-            toGerrit(path, req, rsp, true);
-          }
-        });
-  }
-
   private Key<HttpServlet> screen(String target) {
     return key(
         new HttpServlet() {
@@ -152,43 +122,6 @@
         });
   }
 
-  private Key<HttpServlet> legacyGerritScreen() {
-    return key(
-        new HttpServlet() {
-          private static final long serialVersionUID = 1L;
-
-          @Override
-          protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException {
-            final String token = req.getPathInfo().substring(1);
-            toGerrit(token, req, rsp);
-          }
-        });
-  }
-
-  private Key<HttpServlet> directChangeById() {
-    return key(
-        new HttpServlet() {
-          private static final long serialVersionUID = 1L;
-
-          @Override
-          protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException {
-            try {
-              String idString = req.getPathInfo();
-              if (idString.endsWith("/")) {
-                idString = idString.substring(0, idString.length() - 1);
-              }
-              Change.Id id = Change.Id.parse(idString);
-              // User accessed Gerrit with /1234, so we have no project yet.
-              // TODO(hiesel) Replace with a preflight request to obtain project before we deprecate
-              // the numeric change id.
-              toGerrit(PageLinks.toChange(null, id), req, rsp);
-            } catch (IllegalArgumentException err) {
-              rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
-            }
-          }
-        });
-  }
-
   private Key<HttpServlet> queryProjectNew() {
     return key(
         new HttpServlet() {
@@ -237,15 +170,7 @@
 
   private Key<HttpServlet> key(HttpServlet servlet) {
     final Key<HttpServlet> srv = Key.get(HttpServlet.class, UniqueAnnotations.create());
-    bind(srv)
-        .toProvider(
-            new Provider<HttpServlet>() {
-              @Override
-              public HttpServlet get() {
-                return servlet;
-              }
-            })
-        .in(SINGLETON);
+    bind(srv).toProvider(() -> servlet).in(SINGLETON);
     return srv;
   }
 
@@ -277,16 +202,8 @@
 
   static void toGerrit(String target, HttpServletRequest req, HttpServletResponse rsp)
       throws IOException {
-    toGerrit(target, req, rsp, false);
-  }
-
-  static void toGerrit(String target, HttpServletRequest req, HttpServletResponse rsp, boolean gwt)
-      throws IOException {
     final StringBuilder url = new StringBuilder();
     url.append(req.getContextPath());
-    if (gwt) {
-      url.append("/#");
-    }
     url.append(target);
     rsp.sendRedirect(url.toString());
   }
diff --git a/java/com/google/gerrit/httpd/WebModule.java b/java/com/google/gerrit/httpd/WebModule.java
index d115f43..e416075 100644
--- a/java/com/google/gerrit/httpd/WebModule.java
+++ b/java/com/google/gerrit/httpd/WebModule.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.httpd.auth.container.HttpsClientSslCertModule;
 import com.google.gerrit.httpd.auth.ldap.LdapAuthModule;
 import com.google.gerrit.httpd.gitweb.GitwebModule;
-import com.google.gerrit.httpd.rpc.UiRpcModule;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.server.RemotePeer;
 import com.google.gerrit.server.config.AuthConfig;
@@ -55,10 +54,7 @@
 
     installAuthModule();
     if (options.enableMasterFeatures()) {
-      install(new UrlModule(options, authConfig));
-      if (options.enableGwtUi()) {
-        install(new UiRpcModule());
-      }
+      install(new UrlModule(authConfig));
     }
     install(new GerritRequestModule());
     install(new GitOverHttpServlet.Module(options.enableMasterFeatures()));
diff --git a/java/com/google/gerrit/httpd/XsrfConstants.java b/java/com/google/gerrit/httpd/XsrfConstants.java
new file mode 100644
index 0000000..0bbfe34
--- /dev/null
+++ b/java/com/google/gerrit/httpd/XsrfConstants.java
@@ -0,0 +1,30 @@
+// 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.httpd;
+
+/** XSRF Constants. */
+public class XsrfConstants {
+  /**
+   * Name of the cookie in which the XSRF token is sent from the server to the client during host
+   * page bootstrapping.
+   */
+  public static final String XSRF_COOKIE_NAME = "XSRF_TOKEN";
+
+  /**
+   * Name of the HTTP header in which the client must send the XSRF token to the server on each
+   * request.
+   */
+  public static final String XSRF_HEADER_NAME = "X-Gerrit-Auth";
+}
diff --git a/java/com/google/gerrit/httpd/XsrfCookieFilter.java b/java/com/google/gerrit/httpd/XsrfCookieFilter.java
index ff64c84..d15ecac 100644
--- a/java/com/google/gerrit/httpd/XsrfCookieFilter.java
+++ b/java/com/google/gerrit/httpd/XsrfCookieFilter.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.base.Strings.nullToEmpty;
 
-import com.google.gerrit.common.data.HostPageData;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.AuthConfig;
@@ -59,7 +58,7 @@
   private void setXsrfTokenCookie(
       HttpServletRequest req, HttpServletResponse rsp, WebSession session) {
     String v = session != null ? session.getXGerritAuth() : null;
-    Cookie c = new Cookie(HostPageData.XSRF_COOKIE_NAME, nullToEmpty(v));
+    Cookie c = new Cookie(XsrfConstants.XSRF_COOKIE_NAME, nullToEmpty(v));
     c.setPath("/");
     c.setSecure(authConfig.getCookieSecure() && isSecure(req));
     c.setMaxAge(
diff --git a/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java b/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
index 9cf8b58..552e667 100644
--- a/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
+++ b/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.httpd.template.SiteHeaderFooter;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
@@ -35,8 +34,6 @@
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.query.account.InternalAccountQuery;
 import com.google.gerrit.util.http.CacheHeaders;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -60,7 +57,6 @@
   private static final long serialVersionUID = 1L;
 
   private final DynamicItem<WebSession> webSession;
-  private final SchemaFactory<ReviewDb> schema;
   private final Accounts accounts;
   private final AccountCache accountCache;
   private final AccountManager accountManager;
@@ -70,14 +66,12 @@
   @Inject
   BecomeAnyAccountLoginServlet(
       DynamicItem<WebSession> ws,
-      SchemaFactory<ReviewDb> sf,
       Accounts a,
       AccountCache ac,
       AccountManager am,
       SiteHeaderFooter shf,
       Provider<InternalAccountQuery> qp) {
     webSession = ws;
-    schema = sf;
     accounts = a;
     accountCache = ac;
     accountManager = am;
@@ -92,8 +86,7 @@
   }
 
   @Override
-  protected void doPost(HttpServletRequest req, HttpServletResponse rsp)
-      throws IOException, ServletException {
+  protected void doPost(HttpServletRequest req, HttpServletResponse rsp) throws IOException {
     CacheHeaders.setNotCacheable(rsp);
 
     final AuthResult res;
@@ -111,11 +104,7 @@
 
     } else {
       byte[] raw;
-      try {
-        raw = prepareHtmlOutput();
-      } catch (OrmException e) {
-        throw new ServletException(e);
-      }
+      raw = prepareHtmlOutput();
       rsp.setContentType("text/html");
       rsp.setCharacterEncoding(HtmlDomUtil.ENC.name());
       rsp.setContentLength(raw.length);
@@ -151,7 +140,7 @@
     }
   }
 
-  private byte[] prepareHtmlOutput() throws IOException, OrmException {
+  private byte[] prepareHtmlOutput() throws IOException {
     final String pageName = "BecomeAnyAccount.html";
     Document doc = headers.parse(getClass(), pageName);
     if (doc == null) {
@@ -159,30 +148,28 @@
     }
 
     Element userlistElement = HtmlDomUtil.find(doc, "userlist");
-    try (ReviewDb db = schema.open()) {
-      for (Account.Id accountId : accounts.firstNIds(100)) {
-        Optional<AccountState> accountState = accountCache.get(accountId);
-        if (!accountState.isPresent()) {
-          continue;
-        }
-        Account account = accountState.get().getAccount();
-        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();
-        } else {
-          displayName = accountId.toString();
-        }
-
-        Element linkElement = doc.createElement("a");
-        linkElement.setAttribute("href", "?account_id=" + account.getId().toString());
-        linkElement.setTextContent(displayName);
-        userlistElement.appendChild(linkElement);
-        userlistElement.appendChild(doc.createElement("br"));
+    for (Account.Id accountId : accounts.firstNIds(100)) {
+      Optional<AccountState> accountState = accountCache.get(accountId);
+      if (!accountState.isPresent()) {
+        continue;
       }
+      Account account = accountState.get().getAccount();
+      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();
+      } else {
+        displayName = accountId.toString();
+      }
+
+      Element linkElement = doc.createElement("a");
+      linkElement.setAttribute("href", "?account_id=" + account.getId().toString());
+      linkElement.setTextContent(displayName);
+      userlistElement.appendChild(linkElement);
+      userlistElement.appendChild(doc.createElement("br"));
     }
 
     return HtmlDomUtil.toUTF8(doc);
@@ -200,33 +187,20 @@
   }
 
   private AuthResult byUserName(String userName) {
-    try {
-      List<AccountState> accountStates =
-          queryProvider.get().byExternalId(SCHEME_USERNAME, userName);
-      if (accountStates.isEmpty()) {
-        getServletContext().log("No accounts with username " + userName + " found");
-        return null;
-      }
-      if (accountStates.size() > 1) {
-        getServletContext().log("Multiple accounts with username " + userName + " found");
-        return null;
-      }
-      return auth(accountStates.get(0).getAccount().getId());
-    } catch (OrmException e) {
-      getServletContext().log("cannot query account index", e);
+    List<AccountState> accountStates = queryProvider.get().byExternalId(SCHEME_USERNAME, userName);
+    if (accountStates.isEmpty()) {
+      getServletContext().log("No accounts with username " + userName + " found");
       return null;
     }
+    if (accountStates.size() > 1) {
+      getServletContext().log("Multiple accounts with username " + userName + " found");
+      return null;
+    }
+    return auth(accountStates.get(0).getAccount().getId());
   }
 
   private Optional<AuthResult> byPreferredEmail(String email) {
-    try (ReviewDb db = schema.open()) {
-      Optional<AccountState> match =
-          queryProvider.get().byPreferredEmail(email).stream().findFirst();
-      return auth(match);
-    } catch (OrmException e) {
-      getServletContext().log("cannot query database", e);
-      return Optional.empty();
-    }
+    return auth(queryProvider.get().byPreferredEmail(email).stream().findFirst());
   }
 
   private Optional<AuthResult> byAccountId(String idStr) {
diff --git a/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java b/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
index 0b3c29d..509a9f1 100644
--- a/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
+++ b/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
@@ -25,11 +25,10 @@
 import com.google.gerrit.httpd.HtmlDomUtil;
 import com.google.gerrit.httpd.RemoteUserUtil;
 import com.google.gerrit.httpd.WebSession;
-import com.google.gerrit.httpd.raw.HostPageServlet;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.util.http.CacheHeaders;
-import com.google.gwtjsonrpc.server.RPCServletUtils;
+import com.google.gerrit.util.http.RequestUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.FileNotFoundException;
@@ -49,10 +48,10 @@
  * Watches request for the host page and requires login if not yet signed in.
  *
  * <p>If HTTP authentication has been enabled on this server this filter is bound in front of the
- * {@link HostPageServlet} and redirects users who are not yet signed in to visit {@code /login/},
- * so the web container can force login. This redirect is performed with JavaScript, such that any
- * existing anchor token in the URL can be rewritten and preserved through the authentication
- * process of any enterprise single sign-on solutions.
+ * Gerrit and redirects users who are not yet signed in to visit {@code /login/}, so the web
+ * container can force login. This redirect is performed with JavaScript, such that any existing
+ * anchor token in the URL can be rewritten and preserved through the authentication process of any
+ * enterprise single sign-on solutions.
  */
 @Singleton
 class HttpAuthFilter implements Filter {
@@ -98,7 +97,7 @@
       final HttpServletRequest req = (HttpServletRequest) request;
       final HttpServletResponse rsp = (HttpServletResponse) response;
       final byte[] tosend;
-      if (RPCServletUtils.acceptsGzipEncoding(req)) {
+      if (RequestUtil.acceptsGzipEncoding(req)) {
         rsp.setHeader("Content-Encoding", "gzip");
         tosend = signInGzip;
       } else {
diff --git a/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java b/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
index fd2f628..1b7e477 100644
--- a/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
+++ b/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
@@ -31,7 +31,6 @@
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.util.http.CacheHeaders;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -128,7 +127,7 @@
         logger.atFine().log(
             "Associating external identity \"%s\" to user \"%s\"", remoteExternalId, user);
         updateRemoteExternalId(arsp, remoteExternalId);
-      } catch (AccountException | OrmException | ConfigInvalidException e) {
+      } catch (AccountException | ConfigInvalidException e) {
         logger.atSevere().withCause(e).log(
             "Unable to associate external identity \"%s\" to user \"%s\"", remoteExternalId, user);
         rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
@@ -152,7 +151,7 @@
   }
 
   private void updateRemoteExternalId(AuthResult arsp, String remoteAuthToken)
-      throws AccountException, OrmException, IOException, ConfigInvalidException {
+      throws AccountException, IOException, ConfigInvalidException {
     accountManager.updateLink(
         arsp.getAccountId(),
         new AuthRequest(ExternalId.Key.create(SCHEME_EXTERNAL, remoteAuthToken)));
diff --git a/java/com/google/gerrit/httpd/auth/oauth/BUILD b/java/com/google/gerrit/httpd/auth/oauth/BUILD
index 4f60965..b74a65a 100644
--- a/java/com/google/gerrit/httpd/auth/oauth/BUILD
+++ b/java/com/google/gerrit/httpd/auth/oauth/BUILD
@@ -8,6 +8,7 @@
     visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/common:annotations",
+        "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/httpd",
         "//java/com/google/gerrit/reviewdb:server",
@@ -15,7 +16,6 @@
         "//java/com/google/gerrit/server/audit",
         "//lib:gson",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib:servlet-api-3_1",
         "//lib/commons:codec",
         "//lib/flogger:api",
diff --git a/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java b/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java
index b780fa0..0c8a1a10 100644
--- a/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java
+++ b/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java
@@ -35,7 +35,6 @@
 import com.google.gerrit.server.account.AuthResult;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.auth.oauth.OAuthTokenCache;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.servlet.SessionScoped;
@@ -188,7 +187,7 @@
       logger.atInfo().log("OAuth2: linking claimed identity to %s", claimedId.get().toString());
       try {
         accountManager.link(claimedId.get(), req);
-      } catch (OrmException | ConfigInvalidException e) {
+      } catch (ConfigInvalidException e) {
         logger.atSevere().log(
             "Cannot link: %s to user identity:\n  Claimed ID: %s is %s",
             user.getExternalId(), claimedId.get(), claimedIdentifier);
@@ -203,7 +202,7 @@
       throws AccountException, IOException {
     try {
       accountManager.link(identifiedUser.get().getAccountId(), areq);
-    } catch (OrmException | ConfigInvalidException e) {
+    } catch (ConfigInvalidException e) {
       logger.atSevere().log(
           "Cannot link: %s to user identity: %s",
           user.getExternalId(), identifiedUser.get().getAccountId());
diff --git a/java/com/google/gerrit/httpd/auth/openid/BUILD b/java/com/google/gerrit/httpd/auth/openid/BUILD
index 9f0767a..95b1efc 100644
--- a/java/com/google/gerrit/httpd/auth/openid/BUILD
+++ b/java/com/google/gerrit/httpd/auth/openid/BUILD
@@ -10,6 +10,7 @@
         # 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",
@@ -17,7 +18,7 @@
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/audit",
         "//lib:guava",
-        "//lib:gwtorm",
+        "//java/com/google/gwtorm",
         "//lib:servlet-api-3_1",
         "//lib/commons:codec",
         "//lib/flogger:api",
diff --git a/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java b/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
index a51a0ab..08f2d52 100644
--- a/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
+++ b/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
@@ -33,7 +33,6 @@
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AuthResult;
 import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.servlet.SessionScoped;
@@ -164,7 +163,7 @@
           logger.atFine().log("Claimed account already exists: link to it.");
           try {
             accountManager.link(claimedId.get(), areq);
-          } catch (OrmException | ConfigInvalidException e) {
+          } catch (ConfigInvalidException e) {
             logger.atSevere().log(
                 "Cannot link: %s to user identity:\n  Claimed ID: %s is %s",
                 user.getExternalId(), claimedId.get(), claimedIdentifier);
@@ -178,7 +177,7 @@
         try {
           logger.atFine().log("Linking \"%s\" to \"%s\"", user.getExternalId(), accountId);
           accountManager.link(accountId, areq);
-        } catch (OrmException | ConfigInvalidException e) {
+        } catch (ConfigInvalidException e) {
           logger.atSevere().log(
               "Cannot link: %s to user identity: %s", user.getExternalId(), accountId);
           rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
diff --git a/java/com/google/gerrit/httpd/auth/openid/XrdsFilter.java b/java/com/google/gerrit/httpd/auth/openid/XrdsFilter.java
index 2f7f4bd..864b160 100644
--- a/java/com/google/gerrit/httpd/auth/openid/XrdsFilter.java
+++ b/java/com/google/gerrit/httpd/auth/openid/XrdsFilter.java
@@ -1,3 +1,17 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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.auth.openid;
 
 import com.google.gerrit.server.config.CanonicalWebUrl;
diff --git a/java/com/google/gerrit/httpd/gitweb/GitwebCssServlet.java b/java/com/google/gerrit/httpd/gitweb/GitwebCssServlet.java
index 0074035..f7f1bd6 100644
--- a/java/com/google/gerrit/httpd/gitweb/GitwebCssServlet.java
+++ b/java/com/google/gerrit/httpd/gitweb/GitwebCssServlet.java
@@ -21,7 +21,7 @@
 import com.google.gerrit.server.config.GitwebCgiConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.util.http.CacheHeaders;
-import com.google.gwtjsonrpc.server.RPCServletUtils;
+import com.google.gerrit.util.http.RequestUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -91,7 +91,7 @@
       rsp.setContentType("text/css");
       rsp.setCharacterEncoding(UTF_8.name());
       final byte[] toSend;
-      if (RPCServletUtils.acceptsGzipEncoding(req)) {
+      if (RequestUtil.acceptsGzipEncoding(req)) {
         rsp.setHeader("Content-Encoding", "gzip");
         toSend = gz_css;
       } else {
diff --git a/java/com/google/gerrit/httpd/init/BUILD b/java/com/google/gerrit/httpd/init/BUILD
index 17f4cef..09772fd 100644
--- a/java/com/google/gerrit/httpd/init/BUILD
+++ b/java/com/google/gerrit/httpd/init/BUILD
@@ -28,7 +28,6 @@
         "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/sshd",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib:servlet-api-3_1",
         "//lib/flogger:api",
         "//lib/guice",
diff --git a/java/com/google/gerrit/httpd/init/ReviewDbDataSourceProvider.java b/java/com/google/gerrit/httpd/init/ReviewDbDataSourceProvider.java
deleted file mode 100644
index 6e65780..0000000
--- a/java/com/google/gerrit/httpd/init/ReviewDbDataSourceProvider.java
+++ /dev/null
@@ -1,77 +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.httpd.init;
-
-import com.google.gerrit.extensions.events.LifecycleListener;
-import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
-import com.google.inject.Singleton;
-import javax.naming.InitialContext;
-import javax.naming.NamingException;
-import javax.sql.DataSource;
-
-/** Provides access to the {@code ReviewDb} DataSource. */
-@Singleton
-final class ReviewDbDataSourceProvider implements Provider<DataSource>, LifecycleListener {
-  private DataSource ds;
-
-  @Override
-  public synchronized DataSource get() {
-    if (ds == null) {
-      ds = open();
-    }
-    return ds;
-  }
-
-  @Override
-  public void start() {}
-
-  @Override
-  public synchronized void stop() {
-    if (ds != null) {
-      closeDataSource(ds);
-    }
-  }
-
-  private DataSource open() {
-    final String dsName = "java:comp/env/jdbc/ReviewDb";
-    try {
-      return (DataSource) new InitialContext().lookup(dsName);
-    } catch (NamingException namingErr) {
-      throw new ProvisionException("No DataSource " + dsName, namingErr);
-    }
-  }
-
-  private void closeDataSource(DataSource ds) {
-    try {
-      Class<?> type = Class.forName("org.apache.commons.dbcp.BasicDataSource");
-      if (type.isInstance(ds)) {
-        type.getMethod("close").invoke(ds);
-        return;
-      }
-    } catch (Throwable bad) {
-      // Oh well, its not a Commons DBCP pooled connection.
-    }
-
-    try {
-      Class<?> type = Class.forName("com.mchange.v2.c3p0.DataSources");
-      if (type.isInstance(ds)) {
-        type.getMethod("destroy", DataSource.class).invoke(null, ds);
-      }
-    } catch (Throwable bad) {
-      // Oh well, its not a c3p0 pooled connection.
-    }
-  }
-}
diff --git a/java/com/google/gerrit/httpd/init/SiteInitializer.java b/java/com/google/gerrit/httpd/init/SiteInitializer.java
index 67510cd..04a0ddd 100644
--- a/java/com/google/gerrit/httpd/init/SiteInitializer.java
+++ b/java/com/google/gerrit/httpd/init/SiteInitializer.java
@@ -47,7 +47,7 @@
       if (sitePath != null) {
         Path site = Paths.get(sitePath);
         logger.atInfo().log("Initializing site at %s", site.toRealPath().normalize());
-        new BaseInit(site, false, true, pluginsDistribution, pluginsToInstall).run();
+        new BaseInit(site, false, pluginsDistribution, pluginsToInstall).run();
         return;
       }
 
@@ -62,14 +62,7 @@
       }
       if (site != null) {
         logger.atInfo().log("Initializing site at %s", site.toRealPath().normalize());
-        new BaseInit(
-                site,
-                new ReviewDbDataSourceProvider(),
-                false,
-                false,
-                pluginsDistribution,
-                pluginsToInstall)
-            .run();
+        new BaseInit(site, false, pluginsDistribution, pluginsToInstall).run();
       }
     } catch (Exception e) {
       logger.atSevere().withCause(e).log("Site init failed");
diff --git a/java/com/google/gerrit/httpd/init/WebAppInitializer.java b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
index c2d4a14..bf5cd2a 100644
--- a/java/com/google/gerrit/httpd/init/WebAppInitializer.java
+++ b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.httpd.init;
 
-import static com.google.inject.Scopes.SINGLETON;
 import static com.google.inject.Stage.PRODUCTION;
 
 import com.google.common.base.Splitter;
@@ -40,11 +39,11 @@
 import com.google.gerrit.httpd.plugins.HttpPluginModule;
 import com.google.gerrit.httpd.raw.StaticModule;
 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;
 import com.google.gerrit.server.LibModuleLoader;
+import com.google.gerrit.server.LibModuleType;
 import com.google.gerrit.server.ModuleOverloader;
 import com.google.gerrit.server.StartupChecks;
 import com.google.gerrit.server.account.AccountDeactivator;
@@ -82,20 +81,15 @@
 import com.google.gerrit.server.mail.receive.MailReceiver;
 import com.google.gerrit.server.mail.send.SmtpEmailSender;
 import com.google.gerrit.server.mime.MimeUtil2Module;
-import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.patch.DiffExecutorModule;
 import com.google.gerrit.server.permissions.DefaultPermissionBackendModule;
 import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
 import com.google.gerrit.server.plugins.PluginModule;
 import com.google.gerrit.server.project.DefaultProjectNameLockManager;
 import com.google.gerrit.server.restapi.RestApiModule;
-import com.google.gerrit.server.schema.DataSourceModule;
-import com.google.gerrit.server.schema.DataSourceProvider;
-import com.google.gerrit.server.schema.DataSourceType;
-import com.google.gerrit.server.schema.DatabaseModule;
 import com.google.gerrit.server.schema.JdbcAccountPatchReviewStore;
+import com.google.gerrit.server.schema.NoteDbSchemaVersionCheck;
 import com.google.gerrit.server.schema.SchemaModule;
-import com.google.gerrit.server.schema.SchemaVersionCheck;
 import com.google.gerrit.server.securestore.SecureStoreClassName;
 import com.google.gerrit.server.ssh.NoSshModule;
 import com.google.gerrit.server.ssh.SshAddressesModule;
@@ -114,7 +108,6 @@
 import com.google.inject.Module;
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
-import com.google.inject.name.Names;
 import com.google.inject.servlet.GuiceFilter;
 import com.google.inject.servlet.GuiceServletContextListener;
 import com.google.inject.spi.Message;
@@ -134,7 +127,6 @@
 import javax.servlet.ServletRequest;
 import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
-import javax.sql.DataSource;
 import org.eclipse.jgit.lib.Config;
 
 /** Configures the web application environment for Gerrit Code Review. */
@@ -189,7 +181,7 @@
       }
 
       try {
-        dbInjector = createDbInjector();
+        cfgInjector = createCfgInjector();
       } catch (CreationException ce) {
         final Message first = ce.getErrorMessages().iterator().next();
         final StringBuilder buf = new StringBuilder();
@@ -209,7 +201,7 @@
         throw new CreationException(Collections.singleton(first));
       }
 
-      cfgInjector = createCfgInjector();
+      dbInjector = createDbInjector();
       initIndexType();
       config = cfgInjector.getInstance(Key.get(Config.class, GerritServerConfig.class));
       sysInjector = createSysInjector();
@@ -254,69 +246,33 @@
     return new SshAddressesModule().getListenAddresses(config).isEmpty();
   }
 
-  private Injector createDbInjector() {
+  private Injector createCfgInjector() {
     final List<Module> modules = new ArrayList<>();
     AbstractModule secureStore = createSecureStoreModule();
     modules.add(secureStore);
-    if (sitePath != null) {
-      Module sitePathModule =
-          new AbstractModule() {
-            @Override
-            protected void configure() {
-              bind(Path.class).annotatedWith(SitePath.class).toInstance(sitePath);
-            }
-          };
-      modules.add(sitePathModule);
+    Module sitePathModule =
+        new AbstractModule() {
+          @Override
+          protected void configure() {
+            bind(Path.class).annotatedWith(SitePath.class).toInstance(sitePath);
+          }
+        };
+    modules.add(sitePathModule);
 
-      Module configModule = new GerritServerConfigModule();
-      modules.add(configModule);
-
-      Injector cfgInjector = Guice.createInjector(sitePathModule, configModule, secureStore);
-      Config cfg = cfgInjector.getInstance(Key.get(Config.class, GerritServerConfig.class));
-      String dbType = cfg.getString("database", null, "type");
-
-      final DataSourceType dst =
-          Guice.createInjector(new DataSourceModule(), configModule, sitePathModule, secureStore)
-              .getInstance(Key.get(DataSourceType.class, Names.named(dbType.toLowerCase())));
-      modules.add(
-          new LifecycleModule() {
-            @Override
-            protected void configure() {
-              bind(DataSourceType.class).toInstance(dst);
-              bind(DataSourceProvider.Context.class)
-                  .toInstance(DataSourceProvider.Context.MULTI_USER);
-              bind(Key.get(DataSource.class, Names.named("ReviewDb")))
-                  .toProvider(DataSourceProvider.class)
-                  .in(SINGLETON);
-              listener().to(DataSourceProvider.class);
-            }
-          });
-
-    } else {
-      modules.add(
-          new LifecycleModule() {
-            @Override
-            protected void configure() {
-              bind(Key.get(DataSource.class, Names.named("ReviewDb")))
-                  .toProvider(ReviewDbDataSourceProvider.class)
-                  .in(SINGLETON);
-              listener().to(ReviewDbDataSourceProvider.class);
-            }
-          });
-      modules.add(new GerritServerConfigModule());
-    }
-    modules.add(new DatabaseModule());
-    modules.add(new NotesMigration.Module());
+    Module configModule = new GerritServerConfigModule();
+    modules.add(configModule);
     modules.add(new DropWizardMetricMaker.ApiModule());
     return Guice.createInjector(PRODUCTION, modules);
   }
 
-  private Injector createCfgInjector() {
+  private Injector createDbInjector() {
     final List<Module> modules = new ArrayList<>();
     modules.add(new SchemaModule());
-    modules.add(SchemaVersionCheck.module());
+    modules.add(NoteDbSchemaVersionCheck.module());
     modules.add(new AuthConfigModule());
-    return dbInjector.createChildInjector(modules);
+    return cfgInjector.createChildInjector(
+        ModuleOverloader.override(
+            modules, LibModuleLoader.loadModules(cfgInjector, LibModuleType.DB_MODULE)));
   }
 
   private Injector createSysInjector() {
@@ -372,7 +328,7 @@
         new AbstractModule() {
           @Override
           protected void configure() {
-            bind(GerritOptions.class).toInstance(new GerritOptions(config, false, false, false));
+            bind(GerritOptions.class).toInstance(new GerritOptions(false, false, false));
             bind(GerritRuntime.class).toInstance(GerritRuntime.DAEMON);
           }
         });
@@ -380,8 +336,9 @@
     modules.add(new ChangeCleanupRunner.Module());
     modules.add(new AccountDeactivator.Module());
     modules.add(new DefaultProjectNameLockManager.Module());
-    return cfgInjector.createChildInjector(
-        ModuleOverloader.override(modules, LibModuleLoader.loadModules(cfgInjector)));
+    return dbInjector.createChildInjector(
+        ModuleOverloader.override(
+            modules, LibModuleLoader.loadModules(cfgInjector, LibModuleType.SYS_MODULE)));
   }
 
   private Module createIndexModule() {
diff --git a/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java b/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java
index 937b24a..279903c 100644
--- a/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java
+++ b/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java
@@ -15,11 +15,9 @@
 package com.google.gerrit.httpd.plugins;
 
 import com.google.gerrit.extensions.api.lfs.LfsDefinitions;
-import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.httpd.resources.Resource;
 import com.google.gerrit.httpd.resources.ResourceKey;
 import com.google.gerrit.httpd.resources.ResourceWeigher;
-import com.google.gerrit.server.DynamicOptions;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.plugins.ModuleGenerator;
 import com.google.gerrit.server.plugins.ReloadPluginListener;
@@ -65,7 +63,5 @@
                 .weigher(ResourceWeigher.class);
           }
         });
-
-    DynamicMap.mapOf(binder(), DynamicOptions.DynamicBean.class);
   }
 }
diff --git a/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java b/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
index 74cadd3..43eb3a0 100644
--- a/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
+++ b/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
@@ -35,7 +35,6 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.common.io.ByteStreams;
 import com.google.common.net.HttpHeaders;
-import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.httpd.resources.Resource;
 import com.google.gerrit.httpd.resources.ResourceKey;
 import com.google.gerrit.httpd.resources.SmallResource;
@@ -84,8 +83,6 @@
 import javax.servlet.ServletConfig;
 import javax.servlet.ServletContext;
 import javax.servlet.ServletException;
-import javax.servlet.ServletRequest;
-import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
@@ -174,13 +171,7 @@
     GuiceFilter filter = load(plugin);
     final String name = plugin.getName();
     final PluginHolder holder = new PluginHolder(plugin, filter);
-    plugin.add(
-        new RegistrationHandle() {
-          @Override
-          public void remove() {
-            plugins.remove(name, holder);
-          }
-        });
+    plugin.add(() -> plugins.remove(name, holder));
     plugins.put(name, holder);
   }
 
@@ -234,12 +225,7 @@
 
     HttpServletRequest wr = wrapper.create(req, name);
     FilterChain chain =
-        new FilterChain() {
-          @Override
-          public void doFilter(ServletRequest req, ServletResponse res) throws IOException {
-            onDefault(holder, (HttpServletRequest) req, (HttpServletResponse) res);
-          }
-        };
+        (sreq, sres) -> onDefault(holder, (HttpServletRequest) sreq, (HttpServletResponse) sres);
     if (holder.filter != null) {
       holder.filter.doFilter(wr, res, chain);
     } else {
diff --git a/java/com/google/gerrit/httpd/plugins/LfsPluginServlet.java b/java/com/google/gerrit/httpd/plugins/LfsPluginServlet.java
index 67ee3ba..fc0ec39 100644
--- a/java/com/google/gerrit/httpd/plugins/LfsPluginServlet.java
+++ b/java/com/google/gerrit/httpd/plugins/LfsPluginServlet.java
@@ -19,7 +19,6 @@
 import static javax.servlet.http.HttpServletResponse.SC_NOT_IMPLEMENTED;
 
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.httpd.resources.Resource;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.plugins.Plugin;
@@ -40,8 +39,6 @@
 import javax.servlet.ServletConfig;
 import javax.servlet.ServletContext;
 import javax.servlet.ServletException;
-import javax.servlet.ServletRequest;
-import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
@@ -66,12 +63,7 @@
   LfsPluginServlet(@GerritServerConfig Config cfg) {
     this.pluginName = cfg.getString("lfs", null, "plugin");
     this.chain =
-        new FilterChain() {
-          @Override
-          public void doFilter(ServletRequest req, ServletResponse res) throws IOException {
-            Resource.NOT_FOUND.send((HttpServletRequest) req, (HttpServletResponse) res);
-          }
-        };
+        (req, res) -> Resource.NOT_FOUND.send((HttpServletRequest) req, (HttpServletResponse) res);
     this.filter = new AtomicReference<>();
   }
 
@@ -123,13 +115,7 @@
       return;
     }
     final GuiceFilter guiceFilter = load(plugin);
-    plugin.add(
-        new RegistrationHandle() {
-          @Override
-          public void remove() {
-            filter.compareAndSet(guiceFilter, null);
-          }
-        });
+    plugin.add(() -> filter.compareAndSet(guiceFilter, null));
     filter.set(guiceFilter);
   }
 
diff --git a/java/com/google/gerrit/httpd/raw/BazelBuild.java b/java/com/google/gerrit/httpd/raw/BazelBuild.java
index 3dd4635..7677e97 100644
--- a/java/com/google/gerrit/httpd/raw/BazelBuild.java
+++ b/java/com/google/gerrit/httpd/raw/BazelBuild.java
@@ -148,11 +148,6 @@
     return sourceRoot.resolve("bazel-bin").resolve(l.pkg).resolve(l.name);
   }
 
-  /** Label for the agent specific GWT zip. */
-  public Label gwtZipLabel(String agent) {
-    return new Label("gerrit-gwtui", "ui_" + agent + ".zip");
-  }
-
   /** Label for the polygerrit component zip. */
   public Label polygerritComponents() {
     return new Label("polygerrit-ui", "polygerrit_components.bower_components.zip");
diff --git a/java/com/google/gerrit/httpd/raw/CatServlet.java b/java/com/google/gerrit/httpd/raw/CatServlet.java
index 70ae3ae..1d0e7d8b 100644
--- a/java/com/google/gerrit/httpd/raw/CatServlet.java
+++ b/java/com/google/gerrit/httpd/raw/CatServlet.java
@@ -20,7 +20,6 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.edit.ChangeEdit;
 import com.google.gerrit.server.edit.ChangeEditUtil;
@@ -30,9 +29,7 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.Optional;
@@ -53,7 +50,6 @@
 public class CatServlet extends HttpServlet {
   private static final long serialVersionUID = 1L;
 
-  private final Provider<ReviewDb> requestDb;
   private final ChangeEditUtil changeEditUtil;
   private final PatchSetUtil psUtil;
   private final ChangeNotes.Factory changeNotesFactory;
@@ -62,13 +58,11 @@
 
   @Inject
   CatServlet(
-      Provider<ReviewDb> sf,
       ChangeEditUtil ceu,
       PatchSetUtil psu,
       ChangeNotes.Factory cnf,
       PermissionBackend pb,
       ProjectCache pc) {
-    requestDb = sf;
     changeEditUtil = ceu;
     psUtil = psu;
     changeNotesFactory = cnf;
@@ -128,11 +122,7 @@
     String revision;
     try {
       ChangeNotes notes = changeNotesFactory.createChecked(changeId);
-      permissionBackend
-          .currentUser()
-          .change(notes)
-          .database(requestDb)
-          .check(ChangePermission.READ);
+      permissionBackend.currentUser().change(notes).check(ChangePermission.READ);
       projectCache.checkedGet(notes.getProjectName()).checkStatePermitsRead();
       if (patchKey.getParentKey().get() == 0) {
         // change edit
@@ -144,7 +134,7 @@
           return;
         }
       } else {
-        PatchSet patchSet = psUtil.get(requestDb.get(), notes, patchKey.getParentKey());
+        PatchSet patchSet = psUtil.get(notes, patchKey.getParentKey());
         if (patchSet == null) {
           rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
           return;
@@ -154,7 +144,7 @@
     } catch (ResourceConflictException | NoSuchChangeException | AuthException e) {
       rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
       return;
-    } catch (OrmException | PermissionBackendException | IOException e) {
+    } catch (PermissionBackendException | IOException e) {
       getServletContext().log("Cannot query database", e);
       rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
       return;
diff --git a/java/com/google/gerrit/httpd/raw/DirectoryGwtUiServlet.java b/java/com/google/gerrit/httpd/raw/DirectoryGwtUiServlet.java
deleted file mode 100644
index 8ac1601..0000000
--- a/java/com/google/gerrit/httpd/raw/DirectoryGwtUiServlet.java
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.raw;
-
-import com.google.common.cache.Cache;
-import com.google.gerrit.server.util.time.TimeUtil;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.attribute.FileTime;
-
-class DirectoryGwtUiServlet extends ResourceServlet {
-  private static final long serialVersionUID = 1L;
-
-  private static final FileTime NOW = FileTime.fromMillis(TimeUtil.nowMs());
-
-  private final Path ui;
-
-  DirectoryGwtUiServlet(Cache<Path, Resource> cache, Path unpackedWar, boolean dev)
-      throws IOException {
-    super(cache, false);
-    ui = unpackedWar.resolve("gerrit_ui");
-    if (!Files.exists(ui)) {
-      Files.createDirectory(ui);
-    }
-    if (dev) {
-      ui.toFile().deleteOnExit();
-    }
-  }
-
-  @Override
-  protected Path getResourcePath(String pathInfo) {
-    return ui.resolve(pathInfo);
-  }
-
-  @Override
-  protected FileTime getLastModifiedTime(Path p) {
-    // Return initialization time of this class, since the GWT outputs from the
-    // build process all have mtimes of 1980/1/1.
-    return NOW;
-  }
-}
diff --git a/java/com/google/gerrit/httpd/raw/HostPageServlet.java b/java/com/google/gerrit/httpd/raw/HostPageServlet.java
deleted file mode 100644
index b593723..0000000
--- a/java/com/google/gerrit/httpd/raw/HostPageServlet.java
+++ /dev/null
@@ -1,411 +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.httpd.raw;
-
-import static com.google.gerrit.common.FileUtil.lastModified;
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.common.base.Strings;
-import com.google.common.flogger.FluentLogger;
-import com.google.common.hash.Hasher;
-import com.google.common.hash.Hashing;
-import com.google.common.primitives.Bytes;
-import com.google.gerrit.common.Version;
-import com.google.gerrit.common.data.HostPageData;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo;
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.systemstatus.MessageOfTheDay;
-import com.google.gerrit.extensions.webui.WebUiPlugin;
-import com.google.gerrit.httpd.HtmlDomUtil;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountResource;
-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.notedb.NotesMigration;
-import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.restapi.account.GetDiffPreferences;
-import com.google.gerrit.util.http.CacheHeaders;
-import com.google.gwtjsonrpc.server.JsonServlet;
-import com.google.gwtjsonrpc.server.RPCServletUtils;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.StringWriter;
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-import javax.servlet.ServletContext;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.Config;
-import org.w3c.dom.Document;
-import org.w3c.dom.Element;
-import org.w3c.dom.Node;
-
-/** Sends the Gerrit host page to clients. */
-@Singleton
-public class HostPageServlet extends HttpServlet {
-  private static final long serialVersionUID = 1L;
-
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
-  private static final String HPD_ID = "gerrit_hostpagedata";
-  private static final int DEFAULT_JS_LOAD_TIMEOUT = 5000;
-
-  private final Provider<CurrentUser> currentUser;
-  private final DynamicSet<WebUiPlugin> plugins;
-  private final DynamicSet<MessageOfTheDay> messages;
-  private final HostPageData.Theme signedOutTheme;
-  private final HostPageData.Theme signedInTheme;
-  private final SitePaths site;
-  private final Document template;
-  private final String noCacheName;
-  private final boolean refreshHeaderFooter;
-  private final SiteStaticDirectoryServlet staticServlet;
-  private final boolean isNoteDbEnabled;
-  private final Integer pluginsLoadTimeout;
-  private final boolean canLoadInIFrame;
-  private final GetDiffPreferences getDiff;
-  private volatile Page page;
-
-  @Inject
-  HostPageServlet(
-      Provider<CurrentUser> cu,
-      SitePaths sp,
-      ThemeFactory themeFactory,
-      ServletContext servletContext,
-      DynamicSet<WebUiPlugin> webUiPlugins,
-      DynamicSet<MessageOfTheDay> motd,
-      @GerritServerConfig Config cfg,
-      SiteStaticDirectoryServlet ss,
-      NotesMigration migration,
-      GetDiffPreferences diffPref)
-      throws IOException, ServletException {
-    currentUser = cu;
-    plugins = webUiPlugins;
-    messages = motd;
-    signedOutTheme = themeFactory.getSignedOutTheme();
-    signedInTheme = themeFactory.getSignedInTheme();
-    site = sp;
-    refreshHeaderFooter = cfg.getBoolean("site", "refreshHeaderFooter", true);
-    staticServlet = ss;
-    isNoteDbEnabled = migration.readChanges();
-    pluginsLoadTimeout = getPluginsLoadTimeout(cfg);
-    canLoadInIFrame = cfg.getBoolean("gerrit", "canLoadInIFrame", false);
-    getDiff = diffPref;
-
-    String pageName = "HostPage.html";
-    template = HtmlDomUtil.parseFile(getClass(), pageName);
-    if (template == null) {
-      throw new FileNotFoundException("No " + pageName + " in webapp");
-    }
-
-    if (HtmlDomUtil.find(template, "gerrit_module") == null) {
-      throw new ServletException("No gerrit_module in " + pageName);
-    }
-    if (HtmlDomUtil.find(template, HPD_ID) == null) {
-      throw new ServletException("No " + HPD_ID + " in " + pageName);
-    }
-
-    String src = "gerrit_ui/gerrit_ui.nocache.js";
-    try (InputStream in = servletContext.getResourceAsStream("/" + src)) {
-      if (in != null) {
-        Hasher md = Hashing.murmur3_128().newHasher();
-        byte[] buf = new byte[1024];
-        int n;
-        while ((n = in.read(buf)) > 0) {
-          md.putBytes(buf, 0, n);
-        }
-        src += "?content=" + md.hash().toString();
-      } else {
-        logger.atFine().log("No %s in webapp root; keeping noncache.js URL", src);
-      }
-    } catch (IOException e) {
-      throw new IOException("Failed reading " + src, e);
-    }
-
-    noCacheName = src;
-    page = new Page();
-  }
-
-  private static int getPluginsLoadTimeout(Config cfg) {
-    long cfgValue =
-        ConfigUtil.getTimeUnit(
-            cfg, "plugins", null, "jsLoadTimeout", DEFAULT_JS_LOAD_TIMEOUT, TimeUnit.MILLISECONDS);
-    if (cfgValue < 0) {
-      return 0;
-    }
-    return (int) cfgValue;
-  }
-
-  private void json(Object data, StringWriter w) {
-    JsonServlet.defaultGsonBuilder().create().toJson(data, w);
-  }
-
-  private Page get() {
-    Page p = page;
-    try {
-      if (refreshHeaderFooter && p.isStale()) {
-        p = new Page();
-        page = p;
-      }
-    } catch (IOException e) {
-      logger.atSevere().withCause(e).log("Cannot refresh site header/footer");
-    }
-    return p;
-  }
-
-  @Override
-  protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException {
-    Page.Content page = select(req);
-    StringWriter w = new StringWriter();
-    CurrentUser user = currentUser.get();
-    if (user.isIdentifiedUser()) {
-      w.write(HPD_ID + ".accountDiffPref=");
-      json(getDiffPreferences(user.asIdentifiedUser()), w);
-      w.write(";");
-
-      w.write(HPD_ID + ".theme=");
-      json(signedInTheme, w);
-      w.write(";");
-    } else {
-      w.write(HPD_ID + ".theme=");
-      json(signedOutTheme, w);
-      w.write(";");
-    }
-    plugins(w);
-    messages(w);
-
-    byte[] hpd = w.toString().getBytes(UTF_8);
-    byte[] raw = Bytes.concat(page.part1, hpd, page.part2);
-    byte[] tosend;
-    if (RPCServletUtils.acceptsGzipEncoding(req)) {
-      rsp.setHeader("Content-Encoding", "gzip");
-      tosend = HtmlDomUtil.compress(raw);
-    } else {
-      tosend = raw;
-    }
-
-    CacheHeaders.setNotCacheable(rsp);
-    rsp.setContentType("text/html");
-    rsp.setCharacterEncoding(HtmlDomUtil.ENC.name());
-    rsp.setContentLength(tosend.length);
-    try (OutputStream out = rsp.getOutputStream()) {
-      out.write(tosend);
-    }
-  }
-
-  private DiffPreferencesInfo getDiffPreferences(IdentifiedUser user) {
-    try {
-      return getDiff.apply(new AccountResource(user));
-    } catch (RestApiException
-        | ConfigInvalidException
-        | IOException
-        | PermissionBackendException e) {
-      logger.atWarning().withCause(e).log("Cannot query account diff preferences");
-    }
-    return DiffPreferencesInfo.defaults();
-  }
-
-  private void plugins(StringWriter w) {
-    List<String> urls = new ArrayList<>();
-    for (WebUiPlugin u : plugins) {
-      urls.add(String.format("plugins/%s/%s", u.getPluginName(), u.getJavaScriptResourcePath()));
-    }
-    if (!urls.isEmpty()) {
-      w.write(HPD_ID + ".plugins=");
-      json(urls, w);
-      w.write(";");
-    }
-  }
-
-  private void messages(StringWriter w) {
-    List<HostPageData.Message> list = new ArrayList<>(2);
-    for (MessageOfTheDay motd : messages) {
-      String html = motd.getHtmlMessage();
-      if (!Strings.isNullOrEmpty(html)) {
-        HostPageData.Message m = new HostPageData.Message();
-        m.id = motd.getMessageId();
-        m.redisplay = motd.getRedisplay();
-        m.html = html;
-        list.add(m);
-      }
-    }
-    if (!list.isEmpty()) {
-      w.write(HPD_ID + ".messages=");
-      json(list, w);
-      w.write(";");
-    }
-  }
-
-  private Page.Content select(HttpServletRequest req) {
-    Page pg = get();
-    if ("1".equals(req.getParameter("dbg"))) {
-      return pg.debug;
-    }
-    return pg.opt;
-  }
-
-  private void insertETags(Element e) {
-    if ("img".equalsIgnoreCase(e.getTagName()) || "script".equalsIgnoreCase(e.getTagName())) {
-      String src = e.getAttribute("src");
-      if (src != null && src.startsWith("static/")) {
-        String name = src.substring("static/".length());
-        ResourceServlet.Resource r = staticServlet.getResource(name);
-        if (r != null) {
-          e.setAttribute("src", src + "?e=" + r.etag);
-        }
-      }
-    }
-
-    for (Node n = e.getFirstChild(); n != null; n = n.getNextSibling()) {
-      if (n instanceof Element) {
-        insertETags((Element) n);
-      }
-    }
-  }
-
-  private static class FileInfo {
-    private final Path path;
-    private final long time;
-
-    FileInfo(Path p) {
-      path = p;
-      time = lastModified(path);
-    }
-
-    boolean isStale() {
-      return time != lastModified(path);
-    }
-  }
-
-  private class Page {
-    private final FileInfo css;
-    private final FileInfo header;
-    private final FileInfo footer;
-    private final Content opt;
-    private final Content debug;
-
-    Page() throws IOException {
-      Document hostDoc = HtmlDomUtil.clone(template);
-
-      css = injectCssFile(hostDoc, "gerrit_sitecss", site.site_css);
-      header = injectXmlFile(hostDoc, "gerrit_header", site.site_header);
-      footer = injectXmlFile(hostDoc, "gerrit_footer", site.site_footer);
-
-      HostPageData pageData = new HostPageData();
-      pageData.version = Version.getVersion();
-      pageData.isNoteDbEnabled = isNoteDbEnabled;
-      pageData.pluginsLoadTimeout = pluginsLoadTimeout;
-      pageData.canLoadInIFrame = canLoadInIFrame;
-
-      StringWriter w = new StringWriter();
-      w.write("var " + HPD_ID + "=");
-      json(pageData, w);
-      w.write(";");
-
-      Element data = HtmlDomUtil.find(hostDoc, HPD_ID);
-      asScript(data);
-      data.appendChild(hostDoc.createTextNode(w.toString()));
-      data.appendChild(hostDoc.createComment(HPD_ID));
-
-      Element nocache = HtmlDomUtil.find(hostDoc, "gerrit_module");
-      asScript(nocache);
-      nocache.removeAttribute("id");
-      nocache.setAttribute("src", noCacheName);
-      opt = new Content(hostDoc);
-
-      nocache.setAttribute("src", "gerrit_ui/dbg_gerrit_ui.nocache.js");
-      debug = new Content(hostDoc);
-    }
-
-    boolean isStale() {
-      return css.isStale() || header.isStale() || footer.isStale();
-    }
-
-    private void asScript(Element scriptNode) {
-      scriptNode.setAttribute("type", "text/javascript");
-      scriptNode.setAttribute("language", "javascript");
-    }
-
-    class Content {
-      final byte[] part1;
-      final byte[] part2;
-
-      Content(Document hostDoc) throws IOException {
-        String raw = HtmlDomUtil.toString(hostDoc);
-        int p = raw.indexOf("<!--" + HPD_ID);
-        if (p < 0) {
-          throw new IOException("No tag in transformed host page HTML");
-        }
-        part1 = raw.substring(0, p).getBytes(UTF_8);
-        part2 = raw.substring(raw.indexOf('>', p) + 1).getBytes(UTF_8);
-      }
-    }
-
-    private FileInfo injectCssFile(Document hostDoc, String id, Path src) throws IOException {
-      FileInfo info = new FileInfo(src);
-      Element banner = HtmlDomUtil.find(hostDoc, id);
-      if (banner == null) {
-        return info;
-      }
-
-      while (banner.getFirstChild() != null) {
-        banner.removeChild(banner.getFirstChild());
-      }
-
-      String css = HtmlDomUtil.readFile(src.getParent(), src.getFileName().toString());
-      if (css == null) {
-        return info;
-      }
-
-      banner.appendChild(hostDoc.createCDATASection("\n" + css + "\n"));
-      return info;
-    }
-
-    private FileInfo injectXmlFile(Document hostDoc, String id, Path src) throws IOException {
-      FileInfo info = new FileInfo(src);
-      Element banner = HtmlDomUtil.find(hostDoc, id);
-      if (banner == null) {
-        return info;
-      }
-
-      while (banner.getFirstChild() != null) {
-        banner.removeChild(banner.getFirstChild());
-      }
-
-      Document html = HtmlDomUtil.parseFile(src);
-      if (html == null) {
-        return info;
-      }
-
-      Element content = html.getDocumentElement();
-      insertETags(content);
-      banner.appendChild(hostDoc.importNode(content, true));
-      return info;
-    }
-  }
-}
diff --git a/java/com/google/gerrit/httpd/raw/IndexServlet.java b/java/com/google/gerrit/httpd/raw/IndexServlet.java
index a414e84..b6594bc 100644
--- a/java/com/google/gerrit/httpd/raw/IndexServlet.java
+++ b/java/com/google/gerrit/httpd/raw/IndexServlet.java
@@ -18,17 +18,18 @@
 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.template.soy.SoyFileSet;
 import com.google.template.soy.data.SanitizedContent;
-import com.google.template.soy.data.SoyMapData;
 import com.google.template.soy.data.UnsafeSanitizedContentOrdainer;
 import com.google.template.soy.tofu.SoyTofu;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.util.Map;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
@@ -74,8 +75,8 @@
     return uri.getPath().replaceAll("/$", "");
   }
 
-  static SoyMapData getTemplateData(String canonicalURL, String cdnPath, String faviconPath)
-      throws URISyntaxException {
+  static Map<String, Object> getTemplateData(
+      String canonicalURL, String cdnPath, String faviconPath) throws URISyntaxException {
     String canonicalPath = computeCanonicalPath(canonicalURL);
 
     String staticPath = "";
@@ -91,9 +92,16 @@
         UnsafeSanitizedContentOrdainer.ordainAsSafe(
             staticPath, SanitizedContent.ContentKind.TRUSTED_RESOURCE_URI);
 
-    return new SoyMapData(
-        "canonicalPath", canonicalPath,
-        "staticResourcePath", sanitizedStaticPath,
-        "faviconPath", faviconPath);
+    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/LegacyGerritServlet.java b/java/com/google/gerrit/httpd/raw/LegacyGerritServlet.java
index 7bc56a6..d4647ae 100644
--- a/java/com/google/gerrit/httpd/raw/LegacyGerritServlet.java
+++ b/java/com/google/gerrit/httpd/raw/LegacyGerritServlet.java
@@ -16,7 +16,7 @@
 
 import com.google.gerrit.httpd.HtmlDomUtil;
 import com.google.gerrit.util.http.CacheHeaders;
-import com.google.gwtjsonrpc.server.RPCServletUtils;
+import com.google.gerrit.util.http.RequestUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.FileNotFoundException;
@@ -56,7 +56,7 @@
   @Override
   protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException {
     final byte[] tosend;
-    if (RPCServletUtils.acceptsGzipEncoding(req)) {
+    if (RequestUtil.acceptsGzipEncoding(req)) {
       rsp.setHeader("Content-Encoding", "gzip");
       tosend = compressed;
     } else {
diff --git a/java/com/google/gerrit/httpd/raw/RecompileGwtUiFilter.java b/java/com/google/gerrit/httpd/raw/RecompileGwtUiFilter.java
deleted file mode 100644
index c6c3367..0000000
--- a/java/com/google/gerrit/httpd/raw/RecompileGwtUiFilter.java
+++ /dev/null
@@ -1,126 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.raw;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Enumeration;
-import java.util.HashSet;
-import java.util.Set;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipFile;
-import javax.servlet.Filter;
-import javax.servlet.FilterChain;
-import javax.servlet.FilterConfig;
-import javax.servlet.ServletException;
-import javax.servlet.ServletRequest;
-import javax.servlet.ServletResponse;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-class RecompileGwtUiFilter implements Filter {
-  private final boolean gwtuiRecompile =
-      System.getProperty("gerrit.disable-gwtui-recompile") == null;
-  private final UserAgentRule rule = new UserAgentRule();
-  private final Set<String> uaInitialized = new HashSet<>();
-  private final Path unpackedWar;
-  private final BazelBuild builder;
-
-  private String lastAgent;
-  private long lastTime;
-
-  RecompileGwtUiFilter(BazelBuild builder, Path unpackedWar) {
-    this.builder = builder;
-    this.unpackedWar = unpackedWar;
-  }
-
-  @Override
-  public void doFilter(ServletRequest request, ServletResponse res, FilterChain chain)
-      throws IOException, ServletException {
-    String agent = rule.select((HttpServletRequest) request);
-    if (unpackedWar != null && (gwtuiRecompile || !uaInitialized.contains(agent))) {
-      BazelBuild.Label label = builder.gwtZipLabel(agent);
-      File zip = builder.targetPath(label).toFile();
-
-      synchronized (this) {
-        try {
-          builder.build(label);
-        } catch (BazelBuild.BuildFailureException e) {
-          e.display(label.toString(), (HttpServletResponse) res);
-          return;
-        }
-
-        if (!agent.equals(lastAgent) || lastTime != zip.lastModified()) {
-          lastAgent = agent;
-          lastTime = zip.lastModified();
-          unpack(zip, unpackedWar.toFile());
-        }
-      }
-      uaInitialized.add(agent);
-    }
-    chain.doFilter(request, res);
-  }
-
-  @Override
-  public void init(FilterConfig config) {}
-
-  @Override
-  public void destroy() {}
-
-  private static void unpack(File srcwar, File dstwar) throws IOException {
-    try (ZipFile zf = new ZipFile(srcwar)) {
-      final Enumeration<? extends ZipEntry> e = zf.entries();
-      while (e.hasMoreElements()) {
-        final ZipEntry ze = e.nextElement();
-        final String name = ze.getName();
-
-        if (ze.isDirectory()
-            || name.startsWith("WEB-INF/")
-            || name.startsWith("META-INF/")
-            || name.startsWith("com/google/gerrit/launcher/")
-            || name.equals("Main.class")) {
-          continue;
-        }
-
-        final File rawtmp = new File(dstwar, name);
-        mkdir(rawtmp.getParentFile());
-        rawtmp.deleteOnExit();
-
-        try (OutputStream rawout = Files.newOutputStream(rawtmp.toPath());
-            InputStream in = zf.getInputStream(ze)) {
-          final byte[] buf = new byte[4096];
-          int n;
-          while ((n = in.read(buf, 0, buf.length)) > 0) {
-            rawout.write(buf, 0, n);
-          }
-        }
-      }
-    }
-  }
-
-  private static void mkdir(File dir) throws IOException {
-    if (!dir.isDirectory()) {
-      mkdir(dir.getParentFile());
-      if (!dir.mkdir()) {
-        throw new IOException("Cannot mkdir " + dir.getAbsolutePath());
-      }
-      dir.deleteOnExit();
-    }
-  }
-}
diff --git a/java/com/google/gerrit/httpd/raw/ResourceServlet.java b/java/com/google/gerrit/httpd/raw/ResourceServlet.java
index 4b44af4..8be4abc 100644
--- a/java/com/google/gerrit/httpd/raw/ResourceServlet.java
+++ b/java/com/google/gerrit/httpd/raw/ResourceServlet.java
@@ -33,10 +33,10 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.common.hash.Hashing;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.httpd.HtmlDomUtil;
-import com.google.gerrit.server.UsedAt;
 import com.google.gerrit.util.http.CacheHeaders;
-import com.google.gwtjsonrpc.server.RPCServletUtils;
+import com.google.gerrit.util.http.RequestUtil;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.nio.file.Files;
@@ -182,7 +182,7 @@
     }
 
     byte[] tosend = r.raw;
-    if (!r.contentType.equals(JS) && RPCServletUtils.acceptsGzipEncoding(req)) {
+    if (!r.contentType.equals(JS) && RequestUtil.acceptsGzipEncoding(req)) {
       byte[] gz = HtmlDomUtil.compress(tosend);
       if ((gz.length + 24) < tosend.length) {
         rsp.setHeader(CONTENT_ENCODING, "gzip");
@@ -267,7 +267,7 @@
 
     OutputStream out = rsp.getOutputStream();
     GZIPOutputStream gz = null;
-    if (RPCServletUtils.acceptsGzipEncoding(req)) {
+    if (RequestUtil.acceptsGzipEncoding(req)) {
       rsp.setHeader(CONTENT_ENCODING, "gzip");
       gz = new GZIPOutputStream(out);
       out = gz;
diff --git a/java/com/google/gerrit/httpd/raw/StaticModule.java b/java/com/google/gerrit/httpd/raw/StaticModule.java
index 98fbdfe..06ac886 100644
--- a/java/com/google/gerrit/httpd/raw/StaticModule.java
+++ b/java/com/google/gerrit/httpd/raw/StaticModule.java
@@ -22,7 +22,6 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.extensions.client.UiType;
 import com.google.gerrit.httpd.XsrfCookieFilter;
 import com.google.gerrit.httpd.raw.ResourceServlet.Resource;
 import com.google.gerrit.launcher.GerritLauncher;
@@ -51,7 +50,6 @@
 import javax.servlet.ServletException;
 import javax.servlet.ServletRequest;
 import javax.servlet.ServletResponse;
-import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletRequestWrapper;
@@ -62,7 +60,6 @@
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static final String CACHE = "static_content";
-  public static final String GERRIT_UI_COOKIE = "GERRIT_UI";
 
   /**
    * Paths at which we should serve the main PolyGerrit application {@code index.html}.
@@ -78,6 +75,7 @@
           "/x/*",
           "/admin/*",
           "/dashboard/*",
+          "/groups/self",
           "/settings/*",
           "/Documentation/q/*");
   // TODO(dborowitz): These fragments conflict with the REST API
@@ -102,12 +100,9 @@
 
   private static final String DOC_SERVLET = "DocServlet";
   private static final String FAVICON_SERVLET = "FaviconServlet";
-  private static final String GWT_UI_SERVLET = "GwtUiServlet";
   private static final String POLYGERRIT_INDEX_SERVLET = "PolyGerritUiIndexServlet";
   private static final String ROBOTS_TXT_SERVLET = "RobotsTxtServlet";
 
-  private static final int GERRIT_UI_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
-
   private final GerritOptions options;
   private Paths paths;
 
@@ -144,9 +139,6 @@
       install(new CoreStaticModule());
       install(new PolyGerritModule());
     }
-    if (options.enableGwtUi()) {
-      install(new GwtUiModule());
-    }
   }
 
   @Provides
@@ -219,38 +211,11 @@
     }
   }
 
-  private class GwtUiModule extends ServletModule {
-    @Override
-    public void configureServlets() {
-      serveRegex("^/gerrit_ui/(?!rpc/)(.*)$")
-          .with(Key.get(HttpServlet.class, Names.named(GWT_UI_SERVLET)));
-      Paths p = getPaths();
-      if (p.isDev()) {
-        filter("/").through(new RecompileGwtUiFilter(p.builder, p.unpackedWar));
-      }
-    }
-
-    @Provides
-    @Singleton
-    @Named(GWT_UI_SERVLET)
-    HttpServlet getGwtUiServlet(@Named(CACHE) Cache<Path, Resource> cache) throws IOException {
-      Paths p = getPaths();
-      if (p.warFs != null) {
-        return new WarGwtUiServlet(cache, p.warFs);
-      }
-      return new DirectoryGwtUiServlet(cache, p.unpackedWar, p.isDev());
-    }
-  }
-
   private class PolyGerritModule extends ServletModule {
     @Override
     public void configureServlets() {
       for (String p : POLYGERRIT_INDEX_PATHS) {
-        // Skip XsrfCookieFilter for /, since that is already done in the GWT UI
-        // path (UrlModule) if it is enabled.
-        if (!(p.equals("/") && options.enableGwtUi())) {
-          filter(p).through(XsrfCookieFilter.class);
-        }
+        filter(p).through(XsrfCookieFilter.class);
       }
       filter("/*").through(PolyGerritFilter.class);
     }
@@ -412,7 +377,6 @@
 
   @Singleton
   private static class PolyGerritFilter implements Filter {
-    private final GerritOptions options;
     private final Paths paths;
     private final HttpServlet polyGerritIndex;
     private final PolyGerritUiServlet polygerritUI;
@@ -421,14 +385,12 @@
 
     @Inject
     PolyGerritFilter(
-        GerritOptions options,
         Paths paths,
         @Named(POLYGERRIT_INDEX_SERVLET) HttpServlet polyGerritIndex,
         PolyGerritUiServlet polygerritUI,
         @Nullable BowerComponentsDevServlet bowerComponentServlet,
         @Nullable FontsDevServlet fontServlet) {
       this.paths = paths;
-      this.options = options;
       this.polyGerritIndex = polyGerritIndex;
       this.polygerritUI = polygerritUI;
       this.bowerComponentServlet = bowerComponentServlet;
@@ -446,13 +408,6 @@
         throws IOException, ServletException {
       HttpServletRequest req = (HttpServletRequest) request;
       HttpServletResponse res = (HttpServletResponse) response;
-      if (handlePolyGerritParam(req, res)) {
-        return;
-      }
-      if (!isPolyGerritEnabled(req)) {
-        chain.doFilter(req, res);
-        return;
-      }
 
       GuiceFilterRequestWrapper reqWrapper = new GuiceFilterRequestWrapper(req);
       String path = pathInfo(req);
@@ -491,71 +446,6 @@
       return uri.startsWith(ctx) ? uri.substring(ctx.length()) : uri;
     }
 
-    private boolean handlePolyGerritParam(HttpServletRequest req, HttpServletResponse res)
-        throws IOException {
-      if (!options.enableGwtUi() || !"GET".equals(req.getMethod())) {
-        return false;
-      }
-      boolean redirect = false;
-      String param = req.getParameter("polygerrit");
-      if ("1".equals(param)) {
-        setPolyGerritCookie(req, res, UiType.POLYGERRIT);
-        redirect = true;
-      } else if ("0".equals(param)) {
-        setPolyGerritCookie(req, res, UiType.GWT);
-        redirect = true;
-      }
-      if (redirect) {
-        // Strip polygerrit param from URL. This actually strips all params,
-        // which is a similar behavior to the JS PolyGerrit redirector code.
-        // Stripping just one param is frustratingly difficult without the use
-        // of Apache httpclient, which is a dep we don't want here:
-        // https://gerrit-review.googlesource.com/#/c/57570/57/gerrit-httpd/BUCK@32
-        res.sendRedirect(req.getRequestURL().toString());
-      }
-      return redirect;
-    }
-
-    private boolean isPolyGerritEnabled(HttpServletRequest req) {
-      return !options.enableGwtUi() || isPolyGerritCookie(req);
-    }
-
-    private boolean isPolyGerritCookie(HttpServletRequest req) {
-      UiType type = UiType.POLYGERRIT;
-      Cookie[] all = req.getCookies();
-      if (all != null) {
-        for (Cookie c : all) {
-          if (GERRIT_UI_COOKIE.equals(c.getName())) {
-            UiType t = UiType.parse(c.getValue());
-            if (t != null) {
-              type = t;
-              break;
-            }
-          }
-        }
-      }
-      return type == UiType.POLYGERRIT;
-    }
-
-    private void setPolyGerritCookie(HttpServletRequest req, HttpServletResponse res, UiType pref) {
-      // Only actually set a cookie if GWT UI is enabled in addition to default PG UI;
-      // otherwise clear it.
-      Cookie cookie = new Cookie(GERRIT_UI_COOKIE, pref.name());
-      if (options.enableGwtUi()) {
-        cookie.setPath("/");
-        cookie.setSecure(isSecure(req));
-        cookie.setMaxAge(GERRIT_UI_COOKIE_MAX_AGE);
-      } else {
-        cookie.setValue("");
-        cookie.setMaxAge(0);
-      }
-      res.addCookie(cookie);
-    }
-
-    private static boolean isSecure(HttpServletRequest req) {
-      return req.isSecure() || "https".equals(req.getScheme());
-    }
-
     private static boolean isPolyGerritAsset(String path) {
       return matchPath(POLYGERRIT_ASSET_PATHS, path);
     }
diff --git a/java/com/google/gerrit/httpd/raw/ThemeFactory.java b/java/com/google/gerrit/httpd/raw/ThemeFactory.java
deleted file mode 100644
index 6a75e07..0000000
--- a/java/com/google/gerrit/httpd/raw/ThemeFactory.java
+++ /dev/null
@@ -1,64 +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.httpd.raw;
-
-import com.google.gerrit.common.data.HostPageData;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.inject.Inject;
-import org.eclipse.jgit.lib.Config;
-
-class ThemeFactory {
-  private final Config cfg;
-
-  @Inject
-  ThemeFactory(@GerritServerConfig Config cfg) {
-    this.cfg = cfg;
-  }
-
-  HostPageData.Theme getSignedOutTheme() {
-    return getTheme("signed-out");
-  }
-
-  HostPageData.Theme getSignedInTheme() {
-    return getTheme("signed-in");
-  }
-
-  private HostPageData.Theme getTheme(String name) {
-    HostPageData.Theme theme = new HostPageData.Theme();
-    theme.backgroundColor = color(name, "backgroundColor", "#FFFFFF");
-    theme.textColor = color(name, "textColor", "#353535");
-    theme.trimColor = color(name, "trimColor", "#EEEEEE");
-    theme.selectionColor = color(name, "selectionColor", "#D8EDF9");
-    theme.topMenuColor = color(name, "topMenuColor", "#FFFFFF");
-    theme.changeTableOutdatedColor = color(name, "changeTableOutdatedColor", "#F08080");
-    theme.tableOddRowColor = color(name, "tableOddRowColor", "transparent");
-    theme.tableEvenRowColor = color(name, "tableEvenRowColor", "transparent");
-    return theme;
-  }
-
-  private String color(String section, String name, String defaultValue) {
-    String v = cfg.getString("theme", section, name);
-    if (v == null || v.isEmpty()) {
-      v = cfg.getString("theme", null, name);
-      if (v == null || v.isEmpty()) {
-        v = defaultValue;
-      }
-    }
-    if (!v.startsWith("#") && v.matches("^[0-9a-fA-F]{2,6}$")) {
-      v = "#" + v;
-    }
-    return v;
-  }
-}
diff --git a/java/com/google/gerrit/httpd/raw/ToolServlet.java b/java/com/google/gerrit/httpd/raw/ToolServlet.java
index fcdd21d..0d707a6 100644
--- a/java/com/google/gerrit/httpd/raw/ToolServlet.java
+++ b/java/com/google/gerrit/httpd/raw/ToolServlet.java
@@ -25,8 +25,7 @@
 
 import com.google.gerrit.common.Version;
 import com.google.gerrit.server.tools.ToolsCatalog;
-import com.google.gerrit.server.tools.ToolsCatalog.Entry;
-import com.google.gwtjsonrpc.server.RPCServletUtils;
+import com.google.gerrit.util.http.RequestUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -50,7 +49,7 @@
 
   @Override
   protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException {
-    Entry ent = toc.get(req.getPathInfo());
+    ToolsCatalog.Entry ent = toc.get(req.getPathInfo());
     if (ent == null) {
       rsp.sendError(SC_NOT_FOUND);
       return;
@@ -71,7 +70,7 @@
     }
   }
 
-  private void doGetFile(Entry ent, HttpServletResponse rsp) throws IOException {
+  private void doGetFile(ToolsCatalog.Entry ent, HttpServletResponse rsp) throws IOException {
     byte[] tosend = ent.getBytes();
 
     rsp.setDateHeader(HDR_EXPIRES, 0L);
@@ -84,8 +83,8 @@
     }
   }
 
-  private void doGetDirectory(Entry ent, HttpServletRequest req, HttpServletResponse rsp)
-      throws IOException {
+  private void doGetDirectory(
+      ToolsCatalog.Entry ent, HttpServletRequest req, HttpServletResponse rsp) throws IOException {
     String path = "/tools/" + ent.getPath();
     Document page = newDocument();
 
@@ -108,9 +107,9 @@
     Element ul = page.createElement("ul");
     body.appendChild(ul);
 
-    for (Entry e : ent.getChildren()) {
+    for (ToolsCatalog.Entry e : ent.getChildren()) {
       String name = e.getName();
-      if (e.getType() == Entry.Type.DIR && !name.endsWith("/")) {
+      if (e.getType() == ToolsCatalog.Entry.Type.DIR && !name.endsWith("/")) {
         name += "/";
       }
 
@@ -130,7 +129,7 @@
     body.appendChild(footer);
 
     byte[] tosend = toUTF8(page);
-    if (RPCServletUtils.acceptsGzipEncoding(req)) {
+    if (RequestUtil.acceptsGzipEncoding(req)) {
       rsp.setHeader("Content-Encoding", "gzip");
       tosend = compress(tosend);
     }
diff --git a/java/com/google/gerrit/httpd/raw/UserAgentRule.java b/java/com/google/gerrit/httpd/raw/UserAgentRule.java
deleted file mode 100644
index 4aac243..0000000
--- a/java/com/google/gerrit/httpd/raw/UserAgentRule.java
+++ /dev/null
@@ -1,93 +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.httpd.raw;
-
-import static java.util.regex.Pattern.compile;
-
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import javax.servlet.http.HttpServletRequest;
-
-/**
- * Selects the value for the {@code user.agent} property.
- *
- * <p>Examines the {@code User-Agent} HTTP request header, and tries to match it to known {@code
- * user.agent} values.
- *
- * <p>Ported from JavaScript in {@code com.google.gwt.user.UserAgent.gwt.xml}.
- */
-class UserAgentRule {
-  private static final Pattern msie = compile(".*msie ([0-11]+)\\.([0-11]+).*");
-  private static final Pattern gecko = compile(".*rv:([0-9]+)\\.([0-9]+).*");
-
-  String getName() {
-    return "user.agent";
-  }
-
-  String select(HttpServletRequest req) {
-    String ua = req.getHeader("User-Agent");
-    if (ua == null) {
-      return null;
-    }
-
-    ua = ua.toLowerCase();
-
-    if (ua.contains("opera")) {
-      return "opera";
-
-    } else if (ua.contains("webkit")) {
-      return "safari";
-
-    } else if (ua.contains("msie")) {
-      // GWT 2.0 uses document.documentMode here, which we can't do
-      // on the server side.
-
-      Matcher m = msie.matcher(ua);
-      if (m.matches() && m.groupCount() == 2) {
-        int v = makeVersion(m);
-        if (v >= 11000) {
-          return "ie11";
-        }
-        if (v >= 10000) {
-          return "ie10";
-        }
-        if (v >= 9000) {
-          return "ie9";
-        }
-        if (v >= 8000) {
-          return "ie8";
-        }
-      }
-      return null;
-
-    } else if (ua.contains("edge")) {
-      return "edge";
-    } else if (ua.contains("gecko")) {
-      Matcher m = gecko.matcher(ua);
-      if (m.matches() && m.groupCount() == 2) {
-        if (makeVersion(m) >= 1008) {
-          return "gecko1_8";
-        }
-      }
-      return "gecko";
-    }
-
-    return null;
-  }
-
-  private int makeVersion(Matcher result) {
-    return (Integer.parseInt(result.group(1)) * 1000) + Integer.parseInt(result.group(2));
-  }
-}
diff --git a/java/com/google/gerrit/httpd/raw/WarGwtUiServlet.java b/java/com/google/gerrit/httpd/raw/WarGwtUiServlet.java
deleted file mode 100644
index 5fe7054..0000000
--- a/java/com/google/gerrit/httpd/raw/WarGwtUiServlet.java
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.raw;
-
-import com.google.common.cache.Cache;
-import com.google.gerrit.server.util.time.TimeUtil;
-import java.nio.file.FileSystem;
-import java.nio.file.Path;
-import java.nio.file.attribute.FileTime;
-
-class WarGwtUiServlet extends ResourceServlet {
-  private static final long serialVersionUID = 1L;
-
-  private static final FileTime NOW = FileTime.fromMillis(TimeUtil.nowMs());
-
-  private final FileSystem warFs;
-
-  WarGwtUiServlet(Cache<Path, Resource> cache, FileSystem warFs) {
-    super(cache, false);
-    this.warFs = warFs;
-  }
-
-  @Override
-  protected Path getResourcePath(String pathInfo) {
-    return warFs.getPath("/gerrit_ui/" + pathInfo);
-  }
-
-  @Override
-  protected FileTime getLastModifiedTime(Path p) {
-    // Return initialization time of this class, since the GWT outputs from the
-    // build process all have mtimes of 1980/1/1.
-    return NOW;
-  }
-}
diff --git a/java/com/google/gerrit/httpd/restapi/LogRedactUtil.java b/java/com/google/gerrit/httpd/restapi/LogRedactUtil.java
index 5a2a033..5a37b7b 100644
--- a/java/com/google/gerrit/httpd/restapi/LogRedactUtil.java
+++ b/java/com/google/gerrit/httpd/restapi/LogRedactUtil.java
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// WARNING: NoteDbUpdateManager cares about the package name RestApiServlet lives in.
-
 package com.google.gerrit.httpd.restapi;
 
 import static com.google.gerrit.httpd.restapi.RestApiServlet.XD_AUTHORIZATION;
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiQuotaEnforcer.java b/java/com/google/gerrit/httpd/restapi/RestApiQuotaEnforcer.java
new file mode 100644
index 0000000..a3aba6d
--- /dev/null
+++ b/java/com/google/gerrit/httpd/restapi/RestApiQuotaEnforcer.java
@@ -0,0 +1,83 @@
+// 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.restapi;
+
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.quota.QuotaBackend;
+import com.google.gerrit.server.quota.QuotaException;
+import com.google.gerrit.util.http.RequestUtil;
+import com.google.inject.Inject;
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Enforces quota on specific REST API endpoints.
+ *
+ * <p>Examples:
+ *
+ * <ul>
+ *   <li>GET /a/accounts/self/detail => /restapi/accounts/detail:GET
+ *   <li>GET /changes/123/revisions/current/detail => /restapi/changes/revisions/detail:GET
+ *   <li>PUT /changes/10/reviewed => /restapi/changes/reviewed:PUT
+ * </ul>
+ *
+ * <p>Adds context (change, project, account) to the quota check if the call is for an existing
+ * entity that was successfully parsed. This quota check is generally enforced after the resource
+ * was parsed, but before the view is executed. If a quota enforcer desires to throttle earlier,
+ * they should consider quota groups in the {@code /http/*} space.
+ */
+public class RestApiQuotaEnforcer {
+  private final QuotaBackend quotaBackend;
+
+  @Inject
+  RestApiQuotaEnforcer(QuotaBackend quotaBackend) {
+    this.quotaBackend = quotaBackend;
+  }
+
+  /** Enforce quota on a request not tied to any {@code RestResource}. */
+  void enforce(HttpServletRequest req) throws QuotaException {
+    String pathForQuotaReporting = RequestUtil.getRestPathWithoutIds(req);
+    quotaBackend
+        .currentUser()
+        .requestToken(quotaGroup(pathForQuotaReporting, req.getMethod()))
+        .throwOnError();
+  }
+
+  /** Enforce quota on a request for a given resource. */
+  void enforce(RestResource rsrc, HttpServletRequest req) throws QuotaException {
+    String pathForQuotaReporting = RequestUtil.getRestPathWithoutIds(req);
+    // Enrich the quota request we are operating on an interesting collection
+    QuotaBackend.WithResource report = quotaBackend.currentUser();
+    if (rsrc instanceof ChangeResource) {
+      ChangeResource changeResource = (ChangeResource) rsrc;
+      report =
+          quotaBackend.currentUser().change(changeResource.getId(), changeResource.getProject());
+    } else if (rsrc instanceof AccountResource) {
+      AccountResource accountResource = (AccountResource) rsrc;
+      report = quotaBackend.currentUser().account(accountResource.getUser().getAccountId());
+    } else if (rsrc instanceof ProjectResource) {
+      ProjectResource projectResource = (ProjectResource) rsrc;
+      report = quotaBackend.currentUser().project(projectResource.getNameKey());
+    }
+
+    report.requestToken(quotaGroup(pathForQuotaReporting, req.getMethod())).throwOnError();
+  }
+
+  private static String quotaGroup(String path, String method) {
+    return "/restapi" + path + ":" + method;
+  }
+}
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 5f14795..4726df3 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -97,23 +97,24 @@
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.git.LockFailureException;
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.httpd.restapi.ParameterParser.QueryParams;
+import com.google.gerrit.json.OutputFormat;
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.OptionUtil;
-import com.google.gerrit.server.OutputFormat;
 import com.google.gerrit.server.audit.ExtendedHttpAuditEvent;
 import com.google.gerrit.server.cache.PerThreadCache;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.git.LockFailureException;
 import com.google.gerrit.server.group.GroupAuditService;
 import com.google.gerrit.server.logging.RequestId;
 import com.google.gerrit.server.logging.TraceContext;
 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.quota.QuotaException;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.util.http.CacheHeaders;
@@ -227,6 +228,7 @@
     final GroupAuditService auditService;
     final RestApiMetrics metrics;
     final Pattern allowOrigin;
+    final RestApiQuotaEnforcer quotaChecker;
 
     @Inject
     Globals(
@@ -236,6 +238,7 @@
         PermissionBackend permissionBackend,
         GroupAuditService auditService,
         RestApiMetrics metrics,
+        RestApiQuotaEnforcer quotaChecker,
         @GerritServerConfig Config cfg) {
       this.currentUser = currentUser;
       this.webSession = webSession;
@@ -243,6 +246,7 @@
       this.permissionBackend = permissionBackend;
       this.auditService = auditService;
       this.metrics = metrics;
+      this.quotaChecker = quotaChecker;
       allowOrigin = makeAllowOrigin(cfg);
     }
 
@@ -317,6 +321,7 @@
         viewData = new ViewData(null, null);
 
         if (path.isEmpty()) {
+          globals.quotaChecker.enforce(req);
           if (rc instanceof NeedsParams) {
             ((NeedsParams) rc).setParams(qp.params());
           }
@@ -339,6 +344,7 @@
           IdString id = path.remove(0);
           try {
             rsrc = rc.parse(rsrc, id);
+            globals.quotaChecker.enforce(rsrc, req);
             if (path.isEmpty()) {
               checkPreconditions(req);
             }
@@ -346,6 +352,7 @@
             if (!path.isEmpty()) {
               throw e;
             }
+            globals.quotaChecker.enforce(req);
 
             if (isPost(req) || isPut(req)) {
               RestView<RestResource> createView = rc.views().get(PluginName.GERRIT, "CREATE./");
@@ -385,8 +392,12 @@
             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(viewData.pluginName, "POST_ON_COLLECTION./");
+                  c.views().get(PluginName.GERRIT, "POST_ON_COLLECTION./");
               if (restCollectionView != null) {
                 viewData = new ViewData(null, restCollectionView);
               } else {
@@ -394,7 +405,7 @@
               }
             } else if (isDelete(req)) {
               RestView<RestResource> restCollectionView =
-                  c.views().get(viewData.pluginName, "DELETE_ON_COLLECTION./");
+                  c.views().get(PluginName.GERRIT, "DELETE_ON_COLLECTION./");
               if (restCollectionView != null) {
                 viewData = new ViewData(null, restCollectionView);
               } else {
@@ -602,6 +613,9 @@
           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);
       } catch (Exception e) {
         status = SC_INTERNAL_SERVER_ERROR;
         responseBytes = handleException(e, req, res);
@@ -1254,6 +1268,8 @@
       return new ViewData(PluginName.GERRIT, core);
     }
 
+    // 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);
@@ -1267,6 +1283,17 @@
       }
     }
 
+    if (r.isEmpty()) {
+      // 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.
+      for (String plugin : views.plugins()) {
+        RestView<RestResource> action = views.get(plugin, "GET." + p.get(0));
+        if (action != null) {
+          r.put(plugin, action);
+        }
+      }
+    }
+
     if (r.size() == 1) {
       Map.Entry<String, RestView<RestResource>> entry = Iterables.getOnlyElement(r.entrySet());
       return new ViewData(entry.getKey(), entry.getValue());
@@ -1351,9 +1378,7 @@
     // generated.
     TraceContext traceContext =
         TraceContext.newTrace(
-            doTrace,
-            traceId1,
-            (tagName, traceId) -> res.setHeader(X_GERRIT_TRACE, traceId.toString()));
+            doTrace, traceId1, (tagName, traceId) -> res.setHeader(X_GERRIT_TRACE, traceId));
     // If a second trace ID was specified, add a tag for it as well.
     if (traceId2 != null) {
       traceContext.addTag(RequestId.Type.TRACE_ID, traceId2);
diff --git a/java/com/google/gerrit/httpd/rpc/AuditedHttpServletResponse.java b/java/com/google/gerrit/httpd/rpc/AuditedHttpServletResponse.java
deleted file mode 100644
index 31819e8..0000000
--- a/java/com/google/gerrit/httpd/rpc/AuditedHttpServletResponse.java
+++ /dev/null
@@ -1,63 +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.httpd.rpc;
-
-import java.io.IOException;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpServletResponseWrapper;
-
-class AuditedHttpServletResponse extends HttpServletResponseWrapper implements HttpServletResponse {
-  private int status;
-
-  AuditedHttpServletResponse(HttpServletResponse response) {
-    super(response);
-  }
-
-  @SuppressWarnings("all") // @Override for servlet API 3.0+ only.
-  public int getStatus() {
-    return status;
-  }
-
-  @Override
-  public void setStatus(int sc) {
-    super.setStatus(sc);
-    this.status = sc;
-  }
-
-  @Override
-  @Deprecated
-  public void setStatus(int sc, String sm) {
-    super.setStatus(sc, sm);
-    this.status = sc;
-  }
-
-  @Override
-  public void sendError(int sc) throws IOException {
-    super.sendError(sc);
-    this.status = sc;
-  }
-
-  @Override
-  public void sendError(int sc, String msg) throws IOException {
-    super.sendError(sc, msg);
-    this.status = sc;
-  }
-
-  @Override
-  public void sendRedirect(String location) throws IOException {
-    super.sendRedirect(location);
-    this.status = SC_MOVED_TEMPORARILY;
-  }
-}
diff --git a/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java b/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
deleted file mode 100644
index 10307e6..0000000
--- a/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
+++ /dev/null
@@ -1,289 +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.httpd.rpc;
-
-import com.google.common.collect.ListMultimap;
-import com.google.common.collect.MultimapBuilder;
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.audit.Audit;
-import com.google.gerrit.common.auth.SignInRequired;
-import com.google.gerrit.common.errors.NotSignedInException;
-import com.google.gerrit.extensions.registration.DynamicItem;
-import com.google.gerrit.httpd.WebSession;
-import com.google.gerrit.server.AccessPath;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.audit.RpcAuditEvent;
-import com.google.gerrit.server.group.GroupAuditService;
-import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gson.GsonBuilder;
-import com.google.gwtjsonrpc.common.RemoteJsonService;
-import com.google.gwtjsonrpc.server.ActiveCall;
-import com.google.gwtjsonrpc.server.JsonServlet;
-import com.google.gwtjsonrpc.server.MethodHandle;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import java.io.IOException;
-import java.lang.reflect.Field;
-import java.lang.reflect.Method;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-/** Base JSON servlet to ensure the current user is not forged. */
-final class GerritJsonServlet extends JsonServlet<GerritJsonServlet.GerritCall> {
-  private static final long serialVersionUID = 1L;
-
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
-  private static final ThreadLocal<GerritCall> currentCall = new ThreadLocal<>();
-  private static final ThreadLocal<MethodHandle> currentMethod = new ThreadLocal<>();
-  private final DynamicItem<WebSession> session;
-  private final RemoteJsonService service;
-  private final GroupAuditService audit;
-
-  @Inject
-  GerritJsonServlet(final DynamicItem<WebSession> w, RemoteJsonService s, GroupAuditService a) {
-    session = w;
-    service = s;
-    audit = a;
-  }
-
-  @Override
-  protected GerritCall createActiveCall(final HttpServletRequest req, HttpServletResponse rsp) {
-    final GerritCall call = new GerritCall(session.get(), req, new AuditedHttpServletResponse(rsp));
-    currentCall.set(call);
-    return call;
-  }
-
-  @Override
-  protected GsonBuilder createGsonBuilder() {
-    return gerritDefaultGsonBuilder();
-  }
-
-  private static GsonBuilder gerritDefaultGsonBuilder() {
-    final GsonBuilder g = defaultGsonBuilder();
-
-    g.registerTypeAdapter(
-        org.eclipse.jgit.diff.Edit.class, new org.eclipse.jgit.diff.EditDeserializer());
-
-    return g;
-  }
-
-  @Override
-  protected void preInvoke(GerritCall call) {
-    super.preInvoke(call);
-
-    if (call.isComplete()) {
-      return;
-    }
-
-    if (call.getMethod().getAnnotation(SignInRequired.class) != null) {
-      // If SignInRequired is set on this method we must have both a
-      // valid XSRF token *and* have the user signed in. Doing these
-      // checks also validates that they agree on the user identity.
-      //
-      if (!call.requireXsrfValid() || !session.get().isSignedIn()) {
-        call.onFailure(new NotSignedInException());
-      }
-    }
-  }
-
-  @Override
-  protected Object createServiceHandle() {
-    return service;
-  }
-
-  @Override
-  protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
-    try {
-      super.service(req, resp);
-    } finally {
-      audit();
-      currentCall.set(null);
-    }
-  }
-
-  private void audit() {
-    try {
-      GerritCall call = currentCall.get();
-      MethodHandle method = call.getMethod();
-      if (method == null) {
-        return;
-      }
-      Audit note = method.getAnnotation(Audit.class);
-      if (note != null) {
-        String sid = call.getWebSession().getSessionId();
-        CurrentUser username = call.getWebSession().getUser();
-        ListMultimap<String, ?> args = extractParams(note, call);
-        String what = extractWhat(note, call);
-        Object result = call.getResult();
-
-        audit.dispatch(
-            new RpcAuditEvent(
-                sid,
-                username,
-                what,
-                call.getWhen(),
-                args,
-                call.getHttpServletRequest().getMethod(),
-                call.getHttpServletRequest().getMethod(),
-                ((AuditedHttpServletResponse) (call.getHttpServletResponse())).getStatus(),
-                result));
-      }
-    } catch (Throwable all) {
-      logger.atSevere().withCause(all).log("Unable to log the call");
-    }
-  }
-
-  private ListMultimap<String, ?> extractParams(Audit note, GerritCall call) {
-    ListMultimap<String, Object> args = MultimapBuilder.hashKeys().arrayListValues().build();
-
-    Object[] params = call.getParams();
-    for (int i = 0; i < params.length; i++) {
-      args.put("$" + i, params[i]);
-    }
-
-    for (int idx : note.obfuscate()) {
-      args.removeAll("$" + idx);
-      args.put("$" + idx, "*****");
-    }
-    return args;
-  }
-
-  private String extractWhat(Audit note, GerritCall call) {
-    Class<?> methodClass = call.getMethodClass();
-    String methodClassName = methodClass != null ? methodClass.getName() : "<UNKNOWN_CLASS>";
-    methodClassName = methodClassName.substring(methodClassName.lastIndexOf('.') + 1);
-    String what = note.action();
-    if (what.length() == 0) {
-      what = call.getMethod().getName();
-    }
-
-    return methodClassName + "." + what;
-  }
-
-  static class GerritCall extends ActiveCall {
-    private final WebSession session;
-    private final long when;
-    private static final Field resultField;
-    private static final Field methodField;
-
-    // Needed to allow access to non-public result field in GWT/JSON-RPC
-    static {
-      resultField = getPrivateField(ActiveCall.class, "result");
-      methodField = getPrivateField(MethodHandle.class, "method");
-    }
-
-    private static Field getPrivateField(Class<?> clazz, String fieldName) {
-      Field declaredField = null;
-      try {
-        declaredField = clazz.getDeclaredField(fieldName);
-        declaredField.setAccessible(true);
-      } catch (Exception e) {
-        logger.atSevere().log("Unable to expose RPS/JSON result field");
-      }
-      return declaredField;
-    }
-
-    // Surrogate of the missing getMethodClass() in GWT/JSON-RPC
-    public Class<?> getMethodClass() {
-      if (methodField == null) {
-        return null;
-      }
-
-      try {
-        Method method = (Method) methodField.get(this.getMethod());
-        return method.getDeclaringClass();
-      } catch (IllegalArgumentException e) {
-        logger.atSevere().log("Cannot access result field");
-      } catch (IllegalAccessException e) {
-        logger.atSevere().log("No permissions to access result field");
-      }
-
-      return null;
-    }
-
-    // Surrogate of the missing getResult() in GWT/JSON-RPC
-    public Object getResult() {
-      if (resultField == null) {
-        return null;
-      }
-
-      try {
-        return resultField.get(this);
-      } catch (IllegalArgumentException e) {
-        logger.atSevere().log("Cannot access result field");
-      } catch (IllegalAccessException e) {
-        logger.atSevere().log("No permissions to access result field");
-      }
-
-      return null;
-    }
-
-    GerritCall(WebSession session, HttpServletRequest i, HttpServletResponse o) {
-      super(i, o);
-      this.session = session;
-      this.when = TimeUtil.nowMs();
-    }
-
-    @Override
-    public MethodHandle getMethod() {
-      if (currentMethod.get() == null) {
-        return super.getMethod();
-      }
-      return currentMethod.get();
-    }
-
-    @Override
-    public void onFailure(Throwable error) {
-      if (error instanceof IllegalArgumentException || error instanceof IllegalStateException) {
-        super.onFailure(error);
-      } else if (error instanceof OrmException || error instanceof RuntimeException) {
-        onInternalFailure(error);
-      } else {
-        super.onFailure(error);
-      }
-    }
-
-    @Override
-    public boolean xsrfValidate() {
-      final String keyIn = getXsrfKeyIn();
-      if (keyIn == null || "".equals(keyIn)) {
-        // Anonymous requests don't need XSRF protection, they shouldn't
-        // be able to cause critical state changes.
-        //
-        return !session.isSignedIn();
-
-      } else if (session.isSignedIn() && session.isValidXGerritAuth(keyIn)) {
-        // The session must exist, and must be using this token.
-        //
-        session.getUser().setAccessPath(AccessPath.JSON_RPC);
-        return true;
-      }
-      return false;
-    }
-
-    public WebSession getWebSession() {
-      return session;
-    }
-
-    public long getWhen() {
-      return when;
-    }
-
-    public long getElapsed() {
-      return TimeUtil.nowMs() - when;
-    }
-  }
-}
diff --git a/java/com/google/gerrit/httpd/rpc/GerritJsonServletProvider.java b/java/com/google/gerrit/httpd/rpc/GerritJsonServletProvider.java
deleted file mode 100644
index b167167..0000000
--- a/java/com/google/gerrit/httpd/rpc/GerritJsonServletProvider.java
+++ /dev/null
@@ -1,47 +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.httpd.rpc;
-
-import com.google.gwtjsonrpc.common.RemoteJsonService;
-import com.google.inject.AbstractModule;
-import com.google.inject.Inject;
-import com.google.inject.Injector;
-import com.google.inject.Provider;
-
-/** Creates {@link GerritJsonServlet} with a {@link RemoteJsonService}. */
-class GerritJsonServletProvider implements Provider<GerritJsonServlet> {
-  @Inject private Injector injector;
-
-  private final Class<? extends RemoteJsonService> serviceClass;
-
-  @Inject
-  GerritJsonServletProvider(Class<? extends RemoteJsonService> c) {
-    serviceClass = c;
-  }
-
-  @Override
-  public GerritJsonServlet get() {
-    final RemoteJsonService srv = injector.getInstance(serviceClass);
-    return injector
-        .createChildInjector(
-            new AbstractModule() {
-              @Override
-              protected void configure() {
-                bind(RemoteJsonService.class).toInstance(srv);
-              }
-            })
-        .getInstance(GerritJsonServlet.class);
-  }
-}
diff --git a/java/com/google/gerrit/httpd/rpc/Handler.java b/java/com/google/gerrit/httpd/rpc/Handler.java
deleted file mode 100644
index ae20571..0000000
--- a/java/com/google/gerrit/httpd/rpc/Handler.java
+++ /dev/null
@@ -1,92 +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.httpd.rpc;
-
-import com.google.gerrit.common.errors.NoSuchEntityException;
-import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.NoSuchRefException;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.gwtjsonrpc.common.VoidResult;
-import com.google.gwtorm.server.OrmException;
-import java.util.concurrent.Callable;
-
-/**
- * Base class for RPC service implementations.
- *
- * <p>Typically an RPC service implementation will extend this class and use Guice injection to
- * manage its state. For example:
- *
- * <pre>
- *   class Foo extends Handler&lt;Result&gt; {
- *     interface Factory {
- *       Foo create(... args ...);
- *     }
- *     &#064;Inject
- *     Foo(state, @Assisted args) { ... }
- *     Result get() throws Exception { ... }
- *   }
- * </pre>
- *
- * @param <T> type of result for {@link AsyncCallback#onSuccess(Object)} if the operation completed
- *     successfully.
- */
-public abstract class Handler<T> implements Callable<T> {
-  public static <T> Handler<T> wrap(Callable<T> r) {
-    return new Handler<T>() {
-      @Override
-      public T call() throws Exception {
-        return r.call();
-      }
-    };
-  }
-
-  /**
-   * Run the operation and pass the result to the callback.
-   *
-   * @param callback callback to receive the result of {@link #call()}.
-   */
-  public final void to(AsyncCallback<T> callback) {
-    try {
-      final T r = call();
-      if (r != null) {
-        callback.onSuccess(r);
-      }
-    } catch (NoSuchProjectException | NoSuchChangeException | NoSuchRefException e) {
-      callback.onFailure(new NoSuchEntityException());
-
-    } catch (OrmException e) {
-      if (e.getCause() instanceof NoSuchEntityException) {
-        callback.onFailure(e.getCause());
-
-      } else {
-        callback.onFailure(e);
-      }
-    } catch (Exception e) {
-      callback.onFailure(e);
-    }
-  }
-
-  /**
-   * Compute the operation result.
-   *
-   * @return the result of the operation. Return {@link VoidResult#INSTANCE} if there is no
-   *     meaningful return value for the operation.
-   * @throws Exception the operation failed. The caller will log the exception and the stack trace,
-   *     if it is worth logging on the server side.
-   */
-  @Override
-  public abstract T call() throws Exception;
-}
diff --git a/java/com/google/gerrit/httpd/rpc/RpcServletModule.java b/java/com/google/gerrit/httpd/rpc/RpcServletModule.java
deleted file mode 100644
index b03609e..0000000
--- a/java/com/google/gerrit/httpd/rpc/RpcServletModule.java
+++ /dev/null
@@ -1,48 +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.httpd.rpc;
-
-import com.google.gwtjsonrpc.common.RemoteJsonService;
-import com.google.inject.Key;
-import com.google.inject.Scopes;
-import com.google.inject.internal.UniqueAnnotations;
-import com.google.inject.servlet.ServletModule;
-
-/** Binds {@link RemoteJsonService} implementations to a JSON servlet. */
-public abstract class RpcServletModule extends ServletModule {
-  public static final String PREFIX = "/gerrit_ui/rpc/";
-
-  private final String prefix;
-
-  protected RpcServletModule(String pathPrefix) {
-    prefix = pathPrefix;
-  }
-
-  protected void rpc(Class<? extends RemoteJsonService> clazz) {
-    String name = clazz.getSimpleName();
-    if (name.endsWith("Impl")) {
-      name = name.substring(0, name.length() - 4);
-    }
-    rpc(name, clazz);
-  }
-
-  protected void rpc(String name, Class<? extends RemoteJsonService> clazz) {
-    final Key<GerritJsonServlet> srv = Key.get(GerritJsonServlet.class, UniqueAnnotations.create());
-    final GerritJsonServletProvider provider = new GerritJsonServletProvider(clazz);
-    bind(clazz);
-    serve(prefix + name).with(srv);
-    bind(srv).toProvider(provider).in(Scopes.SINGLETON);
-  }
-}
diff --git a/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java b/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java
deleted file mode 100644
index 634e8d8..0000000
--- a/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java
+++ /dev/null
@@ -1,68 +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.httpd.rpc;
-
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.data.SshHostKey;
-import com.google.gerrit.common.data.SystemInfoService;
-import com.google.gerrit.server.ssh.SshInfo;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.gwtjsonrpc.common.VoidResult;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.jcraft.jsch.HostKey;
-import com.jcraft.jsch.JSch;
-import java.util.ArrayList;
-import java.util.List;
-import javax.servlet.http.HttpServletRequest;
-
-class SystemInfoServiceImpl implements SystemInfoService {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
-  private static final JSch JSCH = new JSch();
-
-  private final List<HostKey> hostKeys;
-  private final Provider<HttpServletRequest> httpRequest;
-
-  @Inject
-  SystemInfoServiceImpl(SshInfo daemon, Provider<HttpServletRequest> hsr) {
-    hostKeys = daemon.getHostKeys();
-    httpRequest = hsr;
-  }
-
-  @Override
-  public void daemonHostKeys(AsyncCallback<List<SshHostKey>> callback) {
-    final ArrayList<SshHostKey> r = new ArrayList<>(hostKeys.size());
-    for (HostKey hk : hostKeys) {
-      String host = hk.getHost();
-      if (host.startsWith("*:")) {
-        final String port = host.substring(2);
-        host = "[" + httpRequest.get().getServerName() + "]:" + port;
-      }
-      final String fp = hk.getFingerPrint(JSCH);
-      r.add(new SshHostKey(host, hk.getType() + " " + hk.getKey(), fp));
-    }
-    callback.onSuccess(r);
-  }
-
-  @Override
-  public void clientError(String message, AsyncCallback<VoidResult> callback) {
-    HttpServletRequest r = httpRequest.get();
-    String ua = r.getHeader("User-Agent");
-    message = message.replaceAll("\n", "\n  ");
-    logger.atSevere().log("Client UI JavaScript error: User-Agent=%s: %s", ua, message);
-    callback.onSuccess(VoidResult.INSTANCE);
-  }
-}
diff --git a/java/com/google/gerrit/httpd/rpc/UiRpcModule.java b/java/com/google/gerrit/httpd/rpc/UiRpcModule.java
deleted file mode 100644
index 9aab920..0000000
--- a/java/com/google/gerrit/httpd/rpc/UiRpcModule.java
+++ /dev/null
@@ -1,31 +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.httpd.rpc;
-
-import com.google.gerrit.httpd.rpc.project.ProjectModule;
-
-/** Registers servlets to answer RPCs from client UI. */
-public class UiRpcModule extends RpcServletModule {
-  public UiRpcModule() {
-    super(PREFIX);
-  }
-
-  @Override
-  protected void configureServlets() {
-    rpc(SystemInfoServiceImpl.class);
-
-    install(new ProjectModule());
-  }
-}
diff --git a/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java b/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
deleted file mode 100644
index 24efb86..0000000
--- a/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
+++ /dev/null
@@ -1,116 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT 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.rpc.project;
-
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.ProjectAccess;
-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.CreateGroupPermissionSyncer;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.account.GroupBackend;
-import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-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.project.ContributorAgreementsChecker;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gerrit.server.restapi.project.SetParent;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.assistedinject.Assisted;
-import java.io.IOException;
-import java.util.List;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.revwalk.RevCommit;
-
-class ChangeProjectAccess extends ProjectAccessHandler<ProjectAccess> {
-  interface Factory {
-    ChangeProjectAccess create(
-        @Assisted("projectName") Project.NameKey projectName,
-        @Nullable @Assisted ObjectId base,
-        @Assisted List<AccessSection> sectionList,
-        @Nullable @Assisted("parentProjectName") Project.NameKey parentProjectName,
-        @Nullable @Assisted String message);
-  }
-
-  private final GitReferenceUpdated gitRefUpdated;
-  private final ProjectAccessFactory.Factory projectAccessFactory;
-  private final ProjectCache projectCache;
-  private final CreateGroupPermissionSyncer createGroupPermissionSyncer;
-
-  @Inject
-  ChangeProjectAccess(
-      ProjectAccessFactory.Factory projectAccessFactory,
-      ProjectCache projectCache,
-      GroupBackend groupBackend,
-      MetaDataUpdate.User metaDataUpdateFactory,
-      AllProjectsName allProjects,
-      Provider<SetParent> setParent,
-      GitReferenceUpdated gitRefUpdated,
-      ContributorAgreementsChecker contributorAgreements,
-      Provider<CurrentUser> user,
-      PermissionBackend permissionBackend,
-      CreateGroupPermissionSyncer createGroupPermissionSyncer,
-      @Assisted("projectName") Project.NameKey projectName,
-      @Nullable @Assisted ObjectId base,
-      @Assisted List<AccessSection> sectionList,
-      @Nullable @Assisted("parentProjectName") Project.NameKey parentProjectName,
-      @Nullable @Assisted String message) {
-    super(
-        groupBackend,
-        metaDataUpdateFactory,
-        allProjects,
-        setParent,
-        user.get(),
-        projectName,
-        base,
-        sectionList,
-        parentProjectName,
-        message,
-        contributorAgreements,
-        permissionBackend,
-        true);
-    this.projectAccessFactory = projectAccessFactory;
-    this.projectCache = projectCache;
-    this.gitRefUpdated = gitRefUpdated;
-    this.createGroupPermissionSyncer = createGroupPermissionSyncer;
-  }
-
-  @Override
-  protected ProjectAccess updateProjectConfig(
-      ProjectConfig config, MetaDataUpdate md, boolean parentProjectUpdate)
-      throws IOException, NoSuchProjectException, ConfigInvalidException,
-          PermissionBackendException, ResourceConflictException {
-    RevCommit commit = config.commit(md);
-
-    gitRefUpdated.fire(
-        config.getProject().getNameKey(),
-        RefNames.REFS_CONFIG,
-        base,
-        commit.getId(),
-        user.asIdentifiedUser().state());
-
-    projectCache.evict(config.getProject());
-    createGroupPermissionSyncer.syncIfNeeded();
-    return projectAccessFactory.create(projectName).call();
-  }
-}
diff --git a/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java b/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
deleted file mode 100644
index 3afb0e2..0000000
--- a/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
+++ /dev/null
@@ -1,295 +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.httpd.rpc.project;
-
-import static com.google.gerrit.server.permissions.GlobalPermission.ADMINISTRATE_SERVER;
-import static com.google.gerrit.server.permissions.RefPermission.CREATE_CHANGE;
-import static com.google.gerrit.server.permissions.RefPermission.READ;
-import static com.google.gerrit.server.permissions.RefPermission.WRITE_CONFIG;
-
-import com.google.common.collect.Maps;
-import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupInfo;
-import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.common.data.ProjectAccess;
-import com.google.gerrit.common.data.RefConfigSection;
-import com.google.gerrit.common.data.WebLinkInfoCommon;
-import com.google.gerrit.common.errors.NoSuchGroupException;
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.httpd.rpc.Handler;
-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.WebLinks;
-import com.google.gerrit.server.account.GroupBackend;
-import com.google.gerrit.server.account.GroupControl;
-import com.google.gerrit.server.config.AllProjectsName;
-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.ProjectPermission;
-import com.google.gerrit.server.permissions.RefPermission;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gerrit.server.project.ProjectState;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-
-class ProjectAccessFactory extends Handler<ProjectAccess> {
-  interface Factory {
-    ProjectAccessFactory create(@Assisted Project.NameKey name);
-  }
-
-  private final GroupBackend groupBackend;
-  private final ProjectCache projectCache;
-  private final PermissionBackend permissionBackend;
-  private final GroupControl.Factory groupControlFactory;
-  private final MetaDataUpdate.Server metaDataUpdateFactory;
-  private final AllProjectsName allProjectsName;
-
-  private final Project.NameKey projectName;
-  private WebLinks webLinks;
-
-  @Inject
-  ProjectAccessFactory(
-      GroupBackend groupBackend,
-      ProjectCache projectCache,
-      PermissionBackend permissionBackend,
-      GroupControl.Factory groupControlFactory,
-      MetaDataUpdate.Server metaDataUpdateFactory,
-      AllProjectsName allProjectsName,
-      WebLinks webLinks,
-      @Assisted final Project.NameKey name) {
-    this.groupBackend = groupBackend;
-    this.projectCache = projectCache;
-    this.permissionBackend = permissionBackend;
-    this.groupControlFactory = groupControlFactory;
-    this.metaDataUpdateFactory = metaDataUpdateFactory;
-    this.allProjectsName = allProjectsName;
-    this.webLinks = webLinks;
-
-    this.projectName = name;
-  }
-
-  @Override
-  public ProjectAccess call()
-      throws NoSuchProjectException, IOException, ConfigInvalidException,
-          PermissionBackendException, ResourceConflictException {
-    ProjectState projectState = checkProjectState();
-
-    // Load the current configuration from the repository, ensuring its the most
-    // recent version available. If it differs from what was in the project
-    // state, force a cache flush now.
-    //
-    ProjectConfig config;
-    try (MetaDataUpdate md = metaDataUpdateFactory.create(projectName)) {
-      config = ProjectConfig.read(md);
-      if (config.updateGroupNames(groupBackend)) {
-        md.setMessage("Update group names\n");
-        config.commit(md);
-        projectCache.evict(config.getProject());
-        projectState = checkProjectState();
-      } else if (config.getRevision() != null
-          && !config.getRevision().equals(projectState.getConfig().getRevision())) {
-        projectCache.evict(config.getProject());
-        projectState = checkProjectState();
-      }
-    }
-
-    // The following implementation must match the GetAccess REST API endpoint.
-
-    List<AccessSection> local = new ArrayList<>();
-    Set<String> ownerOf = new HashSet<>();
-    Map<AccountGroup.UUID, Boolean> visibleGroups = new HashMap<>();
-    PermissionBackend.ForProject perm = permissionBackend.currentUser().project(projectName);
-    boolean checkReadConfig = check(perm, RefNames.REFS_CONFIG, READ);
-    boolean canWriteProjectConfig = true;
-    try {
-      perm.check(ProjectPermission.WRITE_CONFIG);
-    } catch (AuthException e) {
-      canWriteProjectConfig = false;
-    }
-
-    for (AccessSection section : config.getAccessSections()) {
-      String name = section.getName();
-      if (AccessSection.GLOBAL_CAPABILITIES.equals(name)) {
-        if (canWriteProjectConfig) {
-          local.add(section);
-          ownerOf.add(name);
-
-        } else if (checkReadConfig) {
-          local.add(section);
-        }
-
-      } else if (RefConfigSection.isValid(name)) {
-        if (check(perm, name, WRITE_CONFIG)) {
-          local.add(section);
-          ownerOf.add(name);
-
-        } else if (checkReadConfig) {
-          local.add(section);
-
-        } else if (check(perm, name, READ)) {
-          // Filter the section to only add rules describing groups that
-          // are visible to the current-user. This includes any group the
-          // user is a member of, as well as groups they own or that
-          // are visible to all users.
-
-          AccessSection dst = null;
-          for (Permission srcPerm : section.getPermissions()) {
-            Permission dstPerm = null;
-
-            for (PermissionRule srcRule : srcPerm.getRules()) {
-              AccountGroup.UUID group = srcRule.getGroup().getUUID();
-              if (group == null) {
-                continue;
-              }
-
-              Boolean canSeeGroup = visibleGroups.get(group);
-              if (canSeeGroup == null) {
-                try {
-                  canSeeGroup = groupControlFactory.controlFor(group).isVisible();
-                } catch (NoSuchGroupException e) {
-                  canSeeGroup = Boolean.FALSE;
-                }
-                visibleGroups.put(group, canSeeGroup);
-              }
-
-              if (canSeeGroup) {
-                if (dstPerm == null) {
-                  if (dst == null) {
-                    dst = new AccessSection(name);
-                    local.add(dst);
-                  }
-                  dstPerm = dst.getPermission(srcPerm.getName(), true);
-                }
-                dstPerm.add(srcRule);
-              }
-            }
-          }
-        }
-      }
-    }
-
-    if (ownerOf.isEmpty() && isAdmin()) {
-      // Special case: If the section list is empty, this project has no current
-      // access control information. Fall back to site administrators.
-      ownerOf.add(AccessSection.ALL);
-    }
-
-    final ProjectAccess detail = new ProjectAccess();
-    detail.setProjectName(projectName);
-
-    if (config.getRevision() != null) {
-      detail.setRevision(config.getRevision().name());
-    }
-
-    detail.setInheritsFrom(config.getProject().getParent(allProjectsName));
-
-    if (projectName.equals(allProjectsName)
-        && permissionBackend.currentUser().testOrFalse(ADMINISTRATE_SERVER)) {
-      ownerOf.add(AccessSection.GLOBAL_CAPABILITIES);
-    }
-
-    detail.setLocal(local);
-    detail.setOwnerOf(ownerOf);
-    detail.setCanUpload(
-        canWriteProjectConfig
-            || (checkReadConfig
-                && perm.ref(RefNames.REFS_CONFIG).testOrFalse(CREATE_CHANGE)
-                && projectState.statePermitsWrite()));
-    detail.setConfigVisible(canWriteProjectConfig || checkReadConfig);
-    detail.setGroupInfo(buildGroupInfo(local));
-    detail.setLabelTypes(projectState.getLabelTypes());
-    detail.setFileHistoryLinks(getConfigFileLogLinks(projectName.get()));
-    return detail;
-  }
-
-  private List<WebLinkInfoCommon> getConfigFileLogLinks(String projectName) {
-    List<WebLinkInfoCommon> links =
-        webLinks.getFileHistoryLinks(
-            projectName, RefNames.REFS_CONFIG, ProjectConfig.PROJECT_CONFIG);
-    return links.isEmpty() ? null : links;
-  }
-
-  private Map<AccountGroup.UUID, GroupInfo> buildGroupInfo(List<AccessSection> local) {
-    Map<AccountGroup.UUID, GroupInfo> infos = new HashMap<>();
-    for (AccessSection section : local) {
-      for (Permission permission : section.getPermissions()) {
-        for (PermissionRule rule : permission.getRules()) {
-          if (rule.getGroup() != null) {
-            AccountGroup.UUID uuid = rule.getGroup().getUUID();
-            if (uuid != null && !infos.containsKey(uuid)) {
-              GroupDescription.Basic group = groupBackend.get(uuid);
-              infos.put(uuid, group != null ? new GroupInfo(group) : null);
-            }
-          }
-        }
-      }
-    }
-    return Maps.filterEntries(infos, in -> in.getValue() != null);
-  }
-
-  private ProjectState checkProjectState()
-      throws NoSuchProjectException, IOException, PermissionBackendException,
-          ResourceConflictException {
-    ProjectState state = projectCache.checkedGet(projectName);
-    // Hidden projects(permitsRead = false) should only be accessible by the project owners.
-    // READ_CONFIG is checked here because it's only allowed to project owners(ACCESS may also
-    // be allowed for other users). Allowing project owners to access here will help them to view
-    // and update the config of hidden projects easily.
-    ProjectPermission permissionToCheck =
-        state.statePermitsRead() ? ProjectPermission.ACCESS : ProjectPermission.READ_CONFIG;
-    try {
-      permissionBackend.currentUser().project(projectName).check(permissionToCheck);
-    } catch (AuthException e) {
-      throw new NoSuchProjectException(projectName, e);
-    }
-    state.checkStatePermitsRead();
-    return state;
-  }
-
-  private static boolean check(PermissionBackend.ForProject ctx, String ref, RefPermission perm)
-      throws PermissionBackendException {
-    try {
-      ctx.ref(ref).check(perm);
-      return true;
-    } catch (AuthException denied) {
-      return false;
-    }
-  }
-
-  private boolean isAdmin() throws PermissionBackendException {
-    try {
-      permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
-      return true;
-    } catch (AuthException e) {
-      return false;
-    }
-  }
-}
diff --git a/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java b/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
deleted file mode 100644
index 552e712..0000000
--- a/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
+++ /dev/null
@@ -1,251 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.rpc.project;
-
-import static com.google.gerrit.common.ProjectAccessUtil.mergeSections;
-import static java.util.Objects.requireNonNull;
-
-import com.google.common.base.MoreObjects;
-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.common.errors.InvalidNameException;
-import com.google.gerrit.common.errors.NoSuchGroupException;
-import com.google.gerrit.common.errors.UpdateParentFailedException;
-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.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.account.GroupBackend;
-import com.google.gerrit.server.account.GroupBackends;
-import com.google.gerrit.server.config.AllProjectsName;
-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.permissions.RefPermission;
-import com.google.gerrit.server.project.ContributorAgreementsChecker;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gerrit.server.project.RefPattern;
-import com.google.gerrit.server.restapi.project.SetParent;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Provider;
-import java.io.IOException;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.ObjectId;
-
-public abstract class ProjectAccessHandler<T> extends Handler<T> {
-
-  protected final GroupBackend groupBackend;
-  protected final Project.NameKey projectName;
-  protected final ObjectId base;
-  protected final CurrentUser user;
-
-  private final MetaDataUpdate.User metaDataUpdateFactory;
-  private final AllProjectsName allProjects;
-  private final Provider<SetParent> setParent;
-  private final ContributorAgreementsChecker contributorAgreements;
-  private final PermissionBackend permissionBackend;
-  private final Project.NameKey parentProjectName;
-
-  protected String message;
-
-  private List<AccessSection> sectionList;
-  private boolean checkIfOwner;
-  private Boolean canWriteConfig;
-
-  protected ProjectAccessHandler(
-      GroupBackend groupBackend,
-      MetaDataUpdate.User metaDataUpdateFactory,
-      AllProjectsName allProjects,
-      Provider<SetParent> setParent,
-      CurrentUser user,
-      Project.NameKey projectName,
-      ObjectId base,
-      List<AccessSection> sectionList,
-      Project.NameKey parentProjectName,
-      String message,
-      ContributorAgreementsChecker contributorAgreements,
-      PermissionBackend permissionBackend,
-      boolean checkIfOwner) {
-    this.groupBackend = groupBackend;
-    this.metaDataUpdateFactory = metaDataUpdateFactory;
-    this.allProjects = allProjects;
-    this.setParent = setParent;
-    this.user = user;
-
-    this.projectName = projectName;
-    this.base = base;
-    this.sectionList = sectionList;
-    this.parentProjectName = parentProjectName;
-    this.message = message;
-    this.contributorAgreements = contributorAgreements;
-    this.permissionBackend = permissionBackend;
-    this.checkIfOwner = checkIfOwner;
-  }
-
-  @Override
-  public final T call()
-      throws NoSuchProjectException, IOException, ConfigInvalidException, InvalidNameException,
-          NoSuchGroupException, OrmException, UpdateParentFailedException, AuthException,
-          PermissionBackendException, ResourceConflictException {
-    contributorAgreements.check(projectName, user);
-
-    try (MetaDataUpdate md = metaDataUpdateFactory.create(projectName)) {
-      ProjectConfig config = ProjectConfig.read(md, base);
-      Set<String> toDelete = scanSectionNames(config);
-      PermissionBackend.ForProject forProject = permissionBackend.user(user).project(projectName);
-
-      for (AccessSection section : mergeSections(sectionList)) {
-        String name = section.getName();
-
-        if (AccessSection.GLOBAL_CAPABILITIES.equals(name)) {
-          if (checkIfOwner && !canWriteConfig()) {
-            continue;
-          }
-          replace(config, toDelete, section);
-
-        } else if (AccessSection.isValid(name)) {
-          if (checkIfOwner) {
-            try {
-              forProject.ref(name).check(RefPermission.WRITE_CONFIG);
-            } catch (AuthException e) {
-              continue;
-            }
-          }
-
-          RefPattern.validate(name);
-
-          replace(config, toDelete, section);
-        }
-      }
-
-      for (String name : toDelete) {
-        if (AccessSection.GLOBAL_CAPABILITIES.equals(name)) {
-          if (!checkIfOwner || canWriteConfig()) {
-            config.remove(config.getAccessSection(name));
-          }
-
-        } else if (!checkIfOwner) {
-          config.remove(config.getAccessSection(name));
-        } else {
-          try {
-            forProject.ref(name).check(RefPermission.WRITE_CONFIG);
-            config.remove(config.getAccessSection(name));
-          } catch (AuthException e) {
-            // Do nothing.
-          }
-        }
-      }
-
-      boolean parentProjectUpdate = false;
-      if (!config.getProject().getNameKey().equals(allProjects)
-          && !config.getProject().getParent(allProjects).equals(parentProjectName)) {
-        parentProjectUpdate = true;
-        try {
-          setParent
-              .get()
-              .validateParentUpdate(
-                  projectName,
-                  user.asIdentifiedUser(),
-                  MoreObjects.firstNonNull(parentProjectName, allProjects).get(),
-                  checkIfOwner);
-        } catch (AuthException e) {
-          throw new UpdateParentFailedException(
-              "You are not allowed to change the parent project since you are "
-                  + "not an administrator. You may save the modifications for review "
-                  + "so that an administrator can approve them.",
-              e);
-        } catch (ResourceConflictException | UnprocessableEntityException | BadRequestException e) {
-          throw new UpdateParentFailedException(e.getMessage(), e);
-        }
-        config.getProject().setParentName(parentProjectName);
-      }
-
-      if (message != null && !message.isEmpty()) {
-        if (!message.endsWith("\n")) {
-          message += "\n";
-        }
-        md.setMessage(message);
-      } else {
-        md.setMessage("Modify access rules\n");
-      }
-
-      return updateProjectConfig(config, md, parentProjectUpdate);
-    } catch (RepositoryNotFoundException notFound) {
-      throw new NoSuchProjectException(projectName, notFound);
-    }
-  }
-
-  protected abstract T updateProjectConfig(
-      ProjectConfig config, MetaDataUpdate md, boolean parentProjectUpdate)
-      throws IOException, NoSuchProjectException, ConfigInvalidException, OrmException,
-          AuthException, PermissionBackendException, ResourceConflictException;
-
-  private void replace(ProjectConfig config, Set<String> toDelete, AccessSection section)
-      throws NoSuchGroupException {
-    for (Permission permission : section.getPermissions()) {
-      for (PermissionRule rule : permission.getRules()) {
-        lookupGroup(rule);
-      }
-    }
-    config.replace(section);
-    toDelete.remove(section.getName());
-  }
-
-  private static Set<String> scanSectionNames(ProjectConfig config) {
-    Set<String> names = new HashSet<>();
-    for (AccessSection section : config.getAccessSections()) {
-      names.add(section.getName());
-    }
-    return names;
-  }
-
-  private void lookupGroup(PermissionRule rule) throws NoSuchGroupException {
-    GroupReference ref = rule.getGroup();
-    if (ref.getUUID() == null) {
-      final GroupReference group = GroupBackends.findBestSuggestion(groupBackend, ref.getName());
-      if (group == null) {
-        throw new NoSuchGroupException(ref.getName());
-      }
-      ref.setUUID(group.getUUID());
-    }
-  }
-
-  /** Provide a local cache for {@code ProjectPermission.WRITE_CONFIG} capability. */
-  private boolean canWriteConfig() throws PermissionBackendException {
-    requireNonNull(user);
-
-    if (canWriteConfig != null) {
-      return canWriteConfig;
-    }
-    try {
-      permissionBackend.user(user).project(projectName).check(ProjectPermission.WRITE_CONFIG);
-      canWriteConfig = true;
-    } catch (AuthException e) {
-      canWriteConfig = false;
-    }
-    return canWriteConfig;
-  }
-}
diff --git a/java/com/google/gerrit/httpd/rpc/project/ProjectAdminServiceImpl.java b/java/com/google/gerrit/httpd/rpc/project/ProjectAdminServiceImpl.java
deleted file mode 100644
index da471c3..0000000
--- a/java/com/google/gerrit/httpd/rpc/project/ProjectAdminServiceImpl.java
+++ /dev/null
@@ -1,80 +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.httpd.rpc.project;
-
-import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.ProjectAccess;
-import com.google.gerrit.common.data.ProjectAdminService;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.inject.Inject;
-import java.util.List;
-import org.eclipse.jgit.lib.ObjectId;
-
-class ProjectAdminServiceImpl implements ProjectAdminService {
-  private final ChangeProjectAccess.Factory changeProjectAccessFactory;
-  private final ReviewProjectAccess.Factory reviewProjectAccessFactory;
-  private final ProjectAccessFactory.Factory projectAccessFactory;
-
-  @Inject
-  ProjectAdminServiceImpl(
-      final ChangeProjectAccess.Factory changeProjectAccessFactory,
-      final ReviewProjectAccess.Factory reviewProjectAccessFactory,
-      final ProjectAccessFactory.Factory projectAccessFactory) {
-    this.changeProjectAccessFactory = changeProjectAccessFactory;
-    this.reviewProjectAccessFactory = reviewProjectAccessFactory;
-    this.projectAccessFactory = projectAccessFactory;
-  }
-
-  @Override
-  public void projectAccess(
-      final Project.NameKey projectName, AsyncCallback<ProjectAccess> callback) {
-    projectAccessFactory.create(projectName).to(callback);
-  }
-
-  private static ObjectId getBase(String baseRevision) {
-    if (baseRevision != null && !baseRevision.isEmpty()) {
-      return ObjectId.fromString(baseRevision);
-    }
-    return null;
-  }
-
-  @Override
-  public void changeProjectAccess(
-      Project.NameKey projectName,
-      String baseRevision,
-      String msg,
-      List<AccessSection> sections,
-      Project.NameKey parentProjectName,
-      AsyncCallback<ProjectAccess> cb) {
-    changeProjectAccessFactory
-        .create(projectName, getBase(baseRevision), sections, parentProjectName, msg)
-        .to(cb);
-  }
-
-  @Override
-  public void reviewProjectAccess(
-      Project.NameKey projectName,
-      String baseRevision,
-      String msg,
-      List<AccessSection> sections,
-      Project.NameKey parentProjectName,
-      AsyncCallback<Change.Id> cb) {
-    reviewProjectAccessFactory
-        .create(projectName, getBase(baseRevision), sections, parentProjectName, msg)
-        .to(cb);
-  }
-}
diff --git a/java/com/google/gerrit/httpd/rpc/project/ProjectModule.java b/java/com/google/gerrit/httpd/rpc/project/ProjectModule.java
deleted file mode 100644
index 3d7d80f..0000000
--- a/java/com/google/gerrit/httpd/rpc/project/ProjectModule.java
+++ /dev/null
@@ -1,39 +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.httpd.rpc.project;
-
-import com.google.gerrit.extensions.config.FactoryModule;
-import com.google.gerrit.httpd.rpc.RpcServletModule;
-import com.google.gerrit.httpd.rpc.UiRpcModule;
-
-public class ProjectModule extends RpcServletModule {
-  public ProjectModule() {
-    super(UiRpcModule.PREFIX);
-  }
-
-  @Override
-  protected void configureServlets() {
-    install(
-        new FactoryModule() {
-          @Override
-          protected void configure() {
-            factory(ChangeProjectAccess.Factory.class);
-            factory(ReviewProjectAccess.Factory.class);
-            factory(ProjectAccessFactory.Factory.class);
-          }
-        });
-    rpc(ProjectAdminServiceImpl.class);
-  }
-}
diff --git a/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java b/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
deleted file mode 100644
index 6a43a1d..0000000
--- a/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
+++ /dev/null
@@ -1,245 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.rpc.project;
-
-import com.google.common.base.Throwables;
-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.PermissionRule;
-import com.google.gerrit.extensions.api.changes.AddReviewerInput;
-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.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.Sequences;
-import com.google.gerrit.server.account.GroupBackend;
-import com.google.gerrit.server.change.ChangeInserter;
-import com.google.gerrit.server.change.ChangeResource;
-import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.git.meta.MetaDataUpdate;
-import com.google.gerrit.server.group.SystemGroupBackend;
-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.permissions.RefPermission;
-import com.google.gerrit.server.project.ContributorAgreementsChecker;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gerrit.server.restapi.change.ChangesCollection;
-import com.google.gerrit.server.restapi.change.PostReviewers;
-import com.google.gerrit.server.restapi.project.SetParent;
-import com.google.gerrit.server.update.BatchUpdate;
-import com.google.gerrit.server.update.UpdateException;
-import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.assistedinject.Assisted;
-import java.io.IOException;
-import java.util.List;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-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.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
-
-public class ReviewProjectAccess extends ProjectAccessHandler<Change.Id> {
-  interface Factory {
-    ReviewProjectAccess create(
-        @Assisted("projectName") Project.NameKey projectName,
-        @Nullable @Assisted ObjectId base,
-        @Assisted List<AccessSection> sectionList,
-        @Nullable @Assisted("parentProjectName") Project.NameKey parentProjectName,
-        @Nullable @Assisted String message);
-  }
-
-  private final ReviewDb db;
-  private final PermissionBackend permissionBackend;
-  private final Sequences seq;
-  private final Provider<PostReviewers> reviewersProvider;
-  private final ProjectCache projectCache;
-  private final ChangesCollection changes;
-  private final ChangeInserter.Factory changeInserterFactory;
-  private final BatchUpdate.Factory updateFactory;
-  private final boolean allowProjectOwnersToChangeParent;
-
-  @Inject
-  ReviewProjectAccess(
-      PermissionBackend permissionBackend,
-      GroupBackend groupBackend,
-      MetaDataUpdate.User metaDataUpdateFactory,
-      ReviewDb db,
-      Provider<PostReviewers> reviewersProvider,
-      ProjectCache projectCache,
-      AllProjectsName allProjects,
-      ChangesCollection changes,
-      ChangeInserter.Factory changeInserterFactory,
-      BatchUpdate.Factory updateFactory,
-      Provider<SetParent> setParent,
-      Sequences seq,
-      ContributorAgreementsChecker contributorAgreements,
-      Provider<CurrentUser> user,
-      @GerritServerConfig Config config,
-      @Assisted("projectName") Project.NameKey projectName,
-      @Nullable @Assisted ObjectId base,
-      @Assisted List<AccessSection> sectionList,
-      @Nullable @Assisted("parentProjectName") Project.NameKey parentProjectName,
-      @Nullable @Assisted String message) {
-    super(
-        groupBackend,
-        metaDataUpdateFactory,
-        allProjects,
-        setParent,
-        user.get(),
-        projectName,
-        base,
-        sectionList,
-        parentProjectName,
-        message,
-        contributorAgreements,
-        permissionBackend,
-        false);
-    this.db = db;
-    this.permissionBackend = permissionBackend;
-    this.seq = seq;
-    this.reviewersProvider = reviewersProvider;
-    this.projectCache = projectCache;
-    this.changes = changes;
-    this.changeInserterFactory = changeInserterFactory;
-    this.updateFactory = updateFactory;
-    this.allowProjectOwnersToChangeParent =
-        config.getBoolean("receive", "allowProjectOwnersToChangeParent", false);
-  }
-
-  // TODO(dborowitz): Hack MetaDataUpdate so it can be created within a BatchUpdate and we can avoid
-  // calling setUpdateRef(false).
-  @SuppressWarnings("deprecation")
-  @Override
-  protected Change.Id updateProjectConfig(
-      ProjectConfig config, MetaDataUpdate md, boolean parentProjectUpdate)
-      throws IOException, OrmException, AuthException, PermissionBackendException,
-          ConfigInvalidException, ResourceConflictException {
-    PermissionBackend.ForProject perm = permissionBackend.user(user).project(config.getName());
-    if (!check(perm, ProjectPermission.READ_CONFIG)) {
-      throw new AuthException(RefNames.REFS_CONFIG + " not visible");
-    }
-
-    if (!check(perm, ProjectPermission.WRITE_CONFIG)
-        && !check(perm.ref(RefNames.REFS_CONFIG), RefPermission.CREATE_CHANGE)) {
-      throw new AuthException("cannot create change for " + RefNames.REFS_CONFIG);
-    }
-
-    projectCache.checkedGet(config.getName()).checkStatePermitsWrite();
-
-    md.setInsertChangeId(true);
-    Change.Id changeId = new Change.Id(seq.nextChangeId());
-    RevCommit commit =
-        config.commitToNewRef(
-            md, new PatchSet.Id(changeId, Change.INITIAL_PATCH_SET_ID).toRefName());
-    if (commit.getId().equals(base)) {
-      return null;
-    }
-
-    try (ObjectInserter objInserter = md.getRepository().newObjectInserter();
-        ObjectReader objReader = objInserter.newReader();
-        RevWalk rw = new RevWalk(objReader);
-        BatchUpdate bu =
-            updateFactory.create(db, config.getProject().getNameKey(), user, TimeUtil.nowTs())) {
-      bu.setRepository(md.getRepository(), rw, objInserter);
-      bu.insertChange(
-          changeInserterFactory
-              .create(changeId, commit, RefNames.REFS_CONFIG)
-              .setValidate(false)
-              .setUpdateRef(false)); // Created by commitToNewRef.
-      bu.execute();
-    } catch (UpdateException | RestApiException e) {
-      throw new IOException(e);
-    }
-
-    ChangeResource rsrc;
-    try {
-      rsrc = changes.parse(changeId);
-    } catch (RestApiException e) {
-      throw new IOException(e);
-    }
-    addProjectOwnersAsReviewers(rsrc);
-    if (parentProjectUpdate && !allowProjectOwnersToChangeParent) {
-      addAdministratorsAsReviewers(rsrc);
-    }
-    return changeId;
-  }
-
-  private void addProjectOwnersAsReviewers(ChangeResource rsrc) {
-    final String projectOwners = groupBackend.get(SystemGroupBackend.PROJECT_OWNERS).getName();
-    try {
-      AddReviewerInput input = new AddReviewerInput();
-      input.reviewer = projectOwners;
-      reviewersProvider.get().apply(rsrc, input);
-    } catch (Exception e) {
-      // one of the owner groups is not visible to the user and this it why it
-      // can't be added as reviewer
-      Throwables.throwIfUnchecked(e);
-    }
-  }
-
-  private void addAdministratorsAsReviewers(ChangeResource rsrc) {
-    List<PermissionRule> adminRules =
-        projectCache
-            .getAllProjects()
-            .getConfig()
-            .getAccessSection(AccessSection.GLOBAL_CAPABILITIES)
-            .getPermission(GlobalCapability.ADMINISTRATE_SERVER)
-            .getRules();
-    for (PermissionRule r : adminRules) {
-      try {
-        AddReviewerInput input = new AddReviewerInput();
-        input.reviewer = r.getGroup().getUUID().get();
-        reviewersProvider.get().apply(rsrc, input);
-      } catch (Exception e) {
-        // ignore
-        Throwables.throwIfUnchecked(e);
-      }
-    }
-  }
-
-  private boolean check(PermissionBackend.ForRef perm, RefPermission p)
-      throws PermissionBackendException {
-    try {
-      perm.check(p);
-      return true;
-    } catch (AuthException denied) {
-      return false;
-    }
-  }
-
-  private boolean check(PermissionBackend.ForProject perm, ProjectPermission p)
-      throws PermissionBackendException {
-    try {
-      perm.check(p);
-      return true;
-    } catch (AuthException denied) {
-      return false;
-    }
-  }
-}
diff --git a/java/com/google/gerrit/index/BUILD b/java/com/google/gerrit/index/BUILD
index 13f07e1..55c7746 100644
--- a/java/com/google/gerrit/index/BUILD
+++ b/java/com/google/gerrit/index/BUILD
@@ -22,13 +22,13 @@
         ":query_exception",
         "//antlr3:query_parser",
         "//java/com/google/gerrit/common:annotations",
+        "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
+        "//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:gwtjsonrpc",
-        "//lib:gwtorm",
         "//lib/antlr:java-runtime",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
diff --git a/java/com/google/gerrit/index/FieldDef.java b/java/com/google/gerrit/index/FieldDef.java
index beb9c07..fb48104 100644
--- a/java/com/google/gerrit/index/FieldDef.java
+++ b/java/com/google/gerrit/index/FieldDef.java
@@ -18,7 +18,8 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.base.CharMatcher;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.exceptions.StorageException;
 import java.io.IOException;
 import java.sql.Timestamp;
 
@@ -60,7 +61,8 @@
 
   @FunctionalInterface
   public interface Getter<I, T> {
-    T get(I input) throws OrmException, IOException;
+    @Nullable
+    T get(I input) throws IOException;
   }
 
   public static class Builder<T> {
@@ -131,13 +133,13 @@
    *
    * @param input input object.
    * @return the field value(s) to index.
-   * @throws OrmException
    */
-  public T get(I input) throws OrmException {
+  @Nullable
+  public T get(I input) {
     try {
       return getter.get(input);
     } catch (IOException e) {
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
   }
 
diff --git a/java/com/google/gerrit/index/Index.java b/java/com/google/gerrit/index/Index.java
index 2d7e31e..44f8b42 100644
--- a/java/com/google/gerrit/index/Index.java
+++ b/java/com/google/gerrit/index/Index.java
@@ -14,14 +14,13 @@
 
 package com.google.gerrit.index;
 
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.query.DataSource;
 import com.google.gerrit.index.query.FieldBundle;
 import com.google.gerrit.index.query.IndexPredicate;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
-import com.google.gwtorm.server.OrmException;
-import java.io.IOException;
-import java.util.List;
 import java.util.Optional;
 
 /**
@@ -48,24 +47,18 @@
    * searchers, but should be visible within a reasonable amount of time.
    *
    * @param obj document object
-   * @throws IOException
    */
-  void replace(V obj) throws IOException;
+  void replace(V obj);
 
   /**
    * Delete a document from the index by key.
    *
    * @param key document key
-   * @throws IOException
    */
-  void delete(K key) throws IOException;
+  void delete(K key);
 
-  /**
-   * Delete all documents from the index.
-   *
-   * @throws IOException
-   */
-  void deleteAll() throws IOException;
+  /** Delete all documents from the index. */
+  void deleteAll();
 
   /**
    * Convert the given operator predicate into a source searching the index and returning only the
@@ -91,20 +84,17 @@
    * @param opts query options. Options that do not make sense in the context of a single document,
    *     such as start, will be ignored.
    * @return a single document if present.
-   * @throws IOException
    */
-  default Optional<V> get(K key, QueryOptions opts) throws IOException {
+  default Optional<V> get(K key, QueryOptions opts) {
     opts = opts.withStart(0).withLimit(2);
-    List<V> results;
+    ImmutableList<V> results;
     try {
       results = getSource(keyPredicate(key), opts).read().toList();
     } catch (QueryParseException e) {
-      throw new IOException("Unexpected QueryParseException during get()", e);
-    } catch (OrmException e) {
-      throw new IOException(e);
+      throw new StorageException("Unexpected QueryParseException during get()", e);
     }
     if (results.size() > 1) {
-      throw new IOException("Multiple results found in index for key " + key + ": " + results);
+      throw new StorageException("Multiple results found in index for key " + key + ": " + results);
     }
     return results.stream().findFirst();
   }
@@ -116,20 +106,17 @@
    * @param opts query options. Options that do not make sense in the context of a single document,
    *     such as start, will be ignored.
    * @return an abstraction of a raw index document to retrieve fields from.
-   * @throws IOException
    */
-  default Optional<FieldBundle> getRaw(K key, QueryOptions opts) throws IOException {
+  default Optional<FieldBundle> getRaw(K key, QueryOptions opts) {
     opts = opts.withStart(0).withLimit(2);
-    List<FieldBundle> results;
+    ImmutableList<FieldBundle> results;
     try {
       results = getSource(keyPredicate(key), opts).readRaw().toList();
     } catch (QueryParseException e) {
-      throw new IOException("Unexpected QueryParseException during get()", e);
-    } catch (OrmException e) {
-      throw new IOException(e);
+      throw new StorageException("Unexpected QueryParseException during get()", e);
     }
     if (results.size() > 1) {
-      throw new IOException("Multiple results found in index for key " + key + ": " + results);
+      throw new StorageException("Multiple results found in index for key " + key + ": " + results);
     }
     return results.stream().findFirst();
   }
@@ -146,7 +133,6 @@
    * Mark whether this index is up-to-date and ready to serve reads.
    *
    * @param ready whether the index is ready
-   * @throws IOException
    */
-  void markReady(boolean ready) throws IOException;
+  void markReady(boolean ready);
 }
diff --git a/java/com/google/gerrit/index/IndexedQuery.java b/java/com/google/gerrit/index/IndexedQuery.java
deleted file mode 100644
index 143cc26..0000000
--- a/java/com/google/gerrit/index/IndexedQuery.java
+++ /dev/null
@@ -1,133 +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.index;
-
-import com.google.common.base.MoreObjects;
-import com.google.common.collect.ImmutableList;
-import com.google.gerrit.index.query.DataSource;
-import com.google.gerrit.index.query.FieldBundle;
-import com.google.gerrit.index.query.IndexPredicate;
-import com.google.gerrit.index.query.Paginated;
-import com.google.gerrit.index.query.Predicate;
-import com.google.gerrit.index.query.QueryParseException;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
-import java.util.Collection;
-import java.util.List;
-
-/**
- * Wrapper combining an {@link IndexPredicate} together with a {@link DataSource} that returns
- * matching results from the index.
- *
- * <p>Appropriate to return as the rootmost predicate that can be processed using the secondary
- * index; such predicates must also implement {@link DataSource} to be chosen by the query
- * processor.
- *
- * @param <I> The type of the IDs by which the entities are stored in the index.
- * @param <T> The type of the entities that are stored in the index.
- */
-public class IndexedQuery<I, T> extends Predicate<T> implements DataSource<T>, Paginated<T> {
-  protected final Index<I, T> index;
-
-  private QueryOptions opts;
-  private final Predicate<T> pred;
-  protected DataSource<T> source;
-
-  public IndexedQuery(Index<I, T> index, Predicate<T> pred, QueryOptions opts)
-      throws QueryParseException {
-    this.index = index;
-    this.opts = opts;
-    this.pred = pred;
-    this.source = index.getSource(pred, this.opts);
-  }
-
-  @Override
-  public int getChildCount() {
-    return 1;
-  }
-
-  @Override
-  public Predicate<T> getChild(int i) {
-    if (i == 0) {
-      return pred;
-    }
-    throw new ArrayIndexOutOfBoundsException(i);
-  }
-
-  @Override
-  public List<Predicate<T>> getChildren() {
-    return ImmutableList.of(pred);
-  }
-
-  @Override
-  public QueryOptions getOptions() {
-    return opts;
-  }
-
-  @Override
-  public int getCardinality() {
-    return source != null ? source.getCardinality() : opts.limit();
-  }
-
-  @Override
-  public ResultSet<T> read() throws OrmException {
-    return source.read();
-  }
-
-  @Override
-  public ResultSet<FieldBundle> readRaw() throws OrmException {
-    return source.readRaw();
-  }
-
-  @Override
-  public ResultSet<T> restart(int start) throws OrmException {
-    opts = opts.withStart(start);
-    try {
-      source = index.getSource(pred, opts);
-    } catch (QueryParseException e) {
-      // Don't need to show this exception to the user; the only thing that
-      // changed about pred was its start, and any other QPEs that might happen
-      // should have already thrown from the constructor.
-      throw new OrmException(e);
-    }
-    // Don't convert start to a limit, since the caller of this method (see
-    // AndSource) has calculated the actual number to skip.
-    return read();
-  }
-
-  @Override
-  public Predicate<T> copy(Collection<? extends Predicate<T>> children) {
-    return this;
-  }
-
-  @Override
-  public int hashCode() {
-    return pred.hashCode();
-  }
-
-  @Override
-  public boolean equals(Object other) {
-    if (other == null || getClass() != other.getClass()) {
-      return false;
-    }
-    IndexedQuery<?, ?> o = (IndexedQuery<?, ?>) other;
-    return pred.equals(o.pred) && opts.equals(o.opts);
-  }
-
-  @Override
-  public String toString() {
-    return MoreObjects.toStringHelper("index").add("p", pred).add("opts", opts).toString();
-  }
-}
diff --git a/java/com/google/gerrit/index/Schema.java b/java/com/google/gerrit/index/Schema.java
index 18563ab..e633bfa 100644
--- a/java/com/google/gerrit/index/Schema.java
+++ b/java/com/google/gerrit/index/Schema.java
@@ -16,14 +16,12 @@
 
 import static com.google.common.base.Preconditions.checkState;
 
-import com.google.common.base.Function;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Predicates;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.flogger.FluentLogger;
-import com.google.gwtorm.server.OrmException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -176,24 +174,21 @@
   public final Iterable<Values<T>> buildFields(T obj) {
     return FluentIterable.from(fields.values())
         .transform(
-            new Function<FieldDef<T, ?>, Values<T>>() {
-              @Override
-              public Values<T> apply(FieldDef<T, ?> f) {
-                Object v;
-                try {
-                  v = f.get(obj);
-                } catch (OrmException e) {
-                  logger.atSevere().withCause(e).log(
-                      "error getting field %s of %s", f.getName(), obj);
-                  return null;
-                }
-                if (v == null) {
-                  return null;
-                } else if (f.isRepeatable()) {
-                  return new Values<>(f, (Iterable<?>) v);
-                } else {
-                  return new Values<>(f, Collections.singleton(v));
-                }
+            f -> {
+              Object v;
+              try {
+                v = f.get(obj);
+              } catch (RuntimeException e) {
+                logger.atSevere().withCause(e).log(
+                    "error getting field %s of %s", f.getName(), obj);
+                return null;
+              }
+              if (v == null) {
+                return null;
+              } else if (f.isRepeatable()) {
+                return new Values<>(f, (Iterable<?>) v);
+              } else {
+                return new Values<>(f, Collections.singleton(v));
               }
             })
         .filter(Predicates.notNull());
diff --git a/java/com/google/gerrit/index/project/IndexedProjectQuery.java b/java/com/google/gerrit/index/project/IndexedProjectQuery.java
index 4409ccb..383ba1c 100644
--- a/java/com/google/gerrit/index/project/IndexedProjectQuery.java
+++ b/java/com/google/gerrit/index/project/IndexedProjectQuery.java
@@ -15,9 +15,9 @@
 package com.google.gerrit.index.project;
 
 import com.google.gerrit.index.Index;
-import com.google.gerrit.index.IndexedQuery;
 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;
diff --git a/java/com/google/gerrit/index/project/ProjectField.java b/java/com/google/gerrit/index/project/ProjectField.java
index 3bc028d..119980c 100644
--- a/java/com/google/gerrit/index/project/ProjectField.java
+++ b/java/com/google/gerrit/index/project/ProjectField.java
@@ -49,7 +49,7 @@
       exact("state").stored().build(p -> p.getProject().getState().name());
 
   public static final FieldDef<ProjectData, Iterable<String>> ANCESTOR_NAME =
-      exact("ancestor_name").buildRepeatable(p -> p.getParentNames());
+      exact("ancestor_name").buildRepeatable(ProjectData::getParentNames);
 
   /**
    * All values of all refs that were used in the course of indexing this document. This covers
diff --git a/java/com/google/gerrit/index/project/ProjectIndexer.java b/java/com/google/gerrit/index/project/ProjectIndexer.java
index 44dccfe..1ca29f5 100644
--- a/java/com/google/gerrit/index/project/ProjectIndexer.java
+++ b/java/com/google/gerrit/index/project/ProjectIndexer.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.index.project;
 
 import com.google.gerrit.reviewdb.client.Project;
-import java.io.IOException;
 
 public interface ProjectIndexer {
 
@@ -24,5 +23,5 @@
    *
    * @param nameKey name key of project to index.
    */
-  void index(Project.NameKey nameKey) throws IOException;
+  void index(Project.NameKey nameKey);
 }
diff --git a/java/com/google/gerrit/index/query/AndPredicate.java b/java/com/google/gerrit/index/query/AndPredicate.java
index 7fba05f..ae13fb3 100644
--- a/java/com/google/gerrit/index/query/AndPredicate.java
+++ b/java/com/google/gerrit/index/query/AndPredicate.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.base.Preconditions.checkState;
 
-import com.google.gwtorm.server.OrmException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -82,7 +81,7 @@
   }
 
   @Override
-  public boolean match(T object) throws OrmException {
+  public boolean match(T object) {
     for (Predicate<T> c : children) {
       checkState(
           c.isMatchable(),
diff --git a/java/com/google/gerrit/index/query/AndSource.java b/java/com/google/gerrit/index/query/AndSource.java
index d1e1c30..7d817d2 100644
--- a/java/com/google/gerrit/index/query/AndSource.java
+++ b/java/com/google/gerrit/index/query/AndSource.java
@@ -17,14 +17,10 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.collect.ImmutableList.toImmutableList;
 
-import com.google.common.base.Throwables;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
-import com.google.gwtorm.server.ListResultSet;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.OrmRuntimeException;
-import com.google.gwtorm.server.ResultSet;
+import com.google.gerrit.exceptions.StorageException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Comparator;
@@ -78,26 +74,9 @@
   }
 
   @Override
-  public ResultSet<T> read() throws OrmException {
-    try {
-      return readImpl();
-    } catch (OrmRuntimeException err) {
-      if (err.getCause() != null) {
-        Throwables.throwIfInstanceOf(err.getCause(), OrmException.class);
-      }
-      throw new OrmException(err);
-    }
-  }
-
-  @Override
-  public ResultSet<FieldBundle> readRaw() throws OrmException {
-    // TOOD(hiesel): Implement
-    throw new UnsupportedOperationException("not implemented");
-  }
-
-  private ResultSet<T> readImpl() throws OrmException {
+  public ResultSet<T> read() {
     if (source == null) {
-      throw new OrmException("No DataSource: " + this);
+      throw new StorageException("No DataSource: " + this);
     }
     List<T> r = new ArrayList<>();
     T last = null;
@@ -144,12 +123,18 @@
   }
 
   @Override
+  public ResultSet<FieldBundle> readRaw() {
+    // TOOD(hiesel): Implement
+    throw new UnsupportedOperationException("not implemented");
+  }
+
+  @Override
   public boolean isMatchable() {
     return isVisibleToPredicate != null || super.isMatchable();
   }
 
   @Override
-  public boolean match(T object) throws OrmException {
+  public boolean match(T object) {
     if (isVisibleToPredicate != null && !isVisibleToPredicate.match(object)) {
       return false;
     }
@@ -166,7 +151,7 @@
         .transformAndConcat(this::transformBuffer);
   }
 
-  protected List<T> transformBuffer(List<T> buffer) throws OrmRuntimeException {
+  protected List<T> transformBuffer(List<T> buffer) {
     return buffer;
   }
 
diff --git a/java/com/google/gerrit/index/query/DataSource.java b/java/com/google/gerrit/index/query/DataSource.java
index 88cc0e3c..2c2ba53 100644
--- a/java/com/google/gerrit/index/query/DataSource.java
+++ b/java/com/google/gerrit/index/query/DataSource.java
@@ -14,16 +14,13 @@
 
 package com.google.gerrit.index.query;
 
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
-
 public interface DataSource<T> {
   /** @return an estimate of the number of results from {@link #read()}. */
   int getCardinality();
 
   /** @return read from the database and return the results. */
-  ResultSet<T> read() throws OrmException;
+  ResultSet<T> read();
 
   /** @return read from the database and return the raw results. */
-  ResultSet<FieldBundle> readRaw() throws OrmException;
+  ResultSet<FieldBundle> readRaw();
 }
diff --git a/java/com/google/gerrit/index/query/IndexedQuery.java b/java/com/google/gerrit/index/query/IndexedQuery.java
new file mode 100644
index 0000000..d9e33ea
--- /dev/null
+++ b/java/com/google/gerrit/index/query/IndexedQuery.java
@@ -0,0 +1,128 @@
+// 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.index.query;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.index.Index;
+import com.google.gerrit.index.QueryOptions;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Wrapper combining an {@link IndexPredicate} together with a {@link DataSource} that returns
+ * matching results from the index.
+ *
+ * <p>Appropriate to return as the rootmost predicate that can be processed using the secondary
+ * index; such predicates must also implement {@link DataSource} to be chosen by the query
+ * processor.
+ *
+ * @param <I> The type of the IDs by which the entities are stored in the index.
+ * @param <T> The type of the entities that are stored in the index.
+ */
+public class IndexedQuery<I, T> extends Predicate<T> implements DataSource<T>, Paginated<T> {
+  protected final Index<I, T> index;
+
+  private QueryOptions opts;
+  private final Predicate<T> pred;
+  protected DataSource<T> source;
+
+  public IndexedQuery(Index<I, T> index, Predicate<T> pred, QueryOptions opts)
+      throws QueryParseException {
+    this.index = index;
+    this.opts = opts;
+    this.pred = pred;
+    this.source = index.getSource(pred, this.opts);
+  }
+
+  @Override
+  public int getChildCount() {
+    return 1;
+  }
+
+  @Override
+  public Predicate<T> getChild(int i) {
+    if (i == 0) {
+      return pred;
+    }
+    throw new ArrayIndexOutOfBoundsException(i);
+  }
+
+  @Override
+  public List<Predicate<T>> getChildren() {
+    return ImmutableList.of(pred);
+  }
+
+  @Override
+  public QueryOptions getOptions() {
+    return opts;
+  }
+
+  @Override
+  public int getCardinality() {
+    return source != null ? source.getCardinality() : opts.limit();
+  }
+
+  @Override
+  public ResultSet<T> read() {
+    return source.read();
+  }
+
+  @Override
+  public ResultSet<FieldBundle> readRaw() {
+    return source.readRaw();
+  }
+
+  @Override
+  public ResultSet<T> restart(int start) {
+    opts = opts.withStart(start);
+    try {
+      source = index.getSource(pred, opts);
+    } catch (QueryParseException e) {
+      // Don't need to show this exception to the user; the only thing that
+      // changed about pred was its start, and any other QPEs that might happen
+      // should have already thrown from the constructor.
+      throw new StorageException(e);
+    }
+    // Don't convert start to a limit, since the caller of this method (see
+    // AndSource) has calculated the actual number to skip.
+    return read();
+  }
+
+  @Override
+  public Predicate<T> copy(Collection<? extends Predicate<T>> children) {
+    return this;
+  }
+
+  @Override
+  public int hashCode() {
+    return pred.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (other == null || getClass() != other.getClass()) {
+      return false;
+    }
+    IndexedQuery<?, ?> o = (IndexedQuery<?, ?>) other;
+    return pred.equals(o.pred) && opts.equals(o.opts);
+  }
+
+  @Override
+  public String toString() {
+    return MoreObjects.toStringHelper("index").add("p", pred).add("opts", opts).toString();
+  }
+}
diff --git a/java/com/google/gerrit/index/query/IntegerRangePredicate.java b/java/com/google/gerrit/index/query/IntegerRangePredicate.java
index 66351a8..6780867 100644
--- a/java/com/google/gerrit/index/query/IntegerRangePredicate.java
+++ b/java/com/google/gerrit/index/query/IntegerRangePredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.query.RangeUtil.Range;
-import com.google.gwtorm.server.OrmException;
 
 public abstract class IntegerRangePredicate<T> extends IndexPredicate<T> {
   private final Range range;
@@ -30,9 +29,9 @@
     }
   }
 
-  protected abstract Integer getValueInt(T object) throws OrmException;
+  protected abstract Integer getValueInt(T object);
 
-  public boolean match(T object) throws OrmException {
+  public boolean match(T object) {
     Integer valueInt = getValueInt(object);
     if (valueInt == null) {
       return false;
diff --git a/java/com/google/gerrit/index/query/InternalQuery.java b/java/com/google/gerrit/index/query/InternalQuery.java
index 3a4b372..48e214e 100644
--- a/java/com/google/gerrit/index/query/InternalQuery.java
+++ b/java/com/google/gerrit/index/query/InternalQuery.java
@@ -17,16 +17,18 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static java.util.stream.Collectors.toSet;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.Index;
 import com.google.gerrit.index.IndexCollection;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.Schema;
-import com.google.gwtorm.server.OrmException;
 import java.util.Arrays;
 import java.util.List;
+import java.util.function.Supplier;
 
 /**
  * Execute a single query over a secondary index, for use by Gerrit internals.
@@ -38,7 +40,7 @@
  * <p>Instances are one-time-use. Other singleton classes should inject a Provider rather than
  * holding on to a single instance.
  */
-public class InternalQuery<T> {
+public class InternalQuery<T, Q extends InternalQuery<T, Q>> {
   private final QueryProcessor<T> queryProcessor;
   private final IndexCollection<?, T, ? extends Index<?, T>> indexes;
 
@@ -53,34 +55,48 @@
     this.indexConfig = indexConfig;
   }
 
-  public InternalQuery<T> setLimit(int n) {
+  @SuppressWarnings("unchecked")
+  protected final Q self() {
+    return (Q) this;
+  }
+
+  final Q setStart(int start) {
+    queryProcessor.setStart(start);
+    return self();
+  }
+
+  public final Q setLimit(int n) {
     queryProcessor.setUserProvidedLimit(n);
-    return this;
+    return self();
   }
 
-  public InternalQuery<T> enforceVisibility(boolean enforce) {
+  public final Q enforceVisibility(boolean enforce) {
     queryProcessor.enforceVisibility(enforce);
-    return this;
+    return self();
   }
 
-  @SuppressWarnings("unchecked") // Can't set @SafeVarargs on a non-final method.
-  public InternalQuery<T> setRequestedFields(FieldDef<T, ?>... fields) {
+  @SafeVarargs
+  public final Q setRequestedFields(FieldDef<T, ?>... fields) {
     checkArgument(fields.length > 0, "requested field list is empty");
     queryProcessor.setRequestedFields(
         Arrays.stream(fields).map(FieldDef::getName).collect(toSet()));
-    return this;
+    return self();
   }
 
-  public InternalQuery<T> noFields() {
+  public final Q noFields() {
     queryProcessor.setRequestedFields(ImmutableSet.of());
-    return this;
+    return self();
   }
 
-  public List<T> query(Predicate<T> p) throws OrmException {
+  public final List<T> query(Predicate<T> p) {
+    return queryResults(p).entities();
+  }
+
+  final QueryResult<T> queryResults(Predicate<T> p) {
     try {
-      return queryProcessor.query(p).entities();
+      return queryProcessor.query(p);
     } catch (QueryParseException e) {
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
   }
 
@@ -94,16 +110,58 @@
    * @return results of the queries, one list of results per input query, in the same order as the
    *     input.
    */
-  public List<List<T>> query(List<Predicate<T>> queries) throws OrmException {
+  public final List<List<T>> query(List<Predicate<T>> queries) {
     try {
       return Lists.transform(queryProcessor.query(queries), QueryResult::entities);
     } catch (QueryParseException e) {
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
   }
 
-  protected Schema<T> schema() {
+  protected final Schema<T> schema() {
     Index<?, T> index = indexes != null ? indexes.getSearchIndex() : null;
     return index != null ? index.getSchema() : null;
   }
+
+  /**
+   * Query a predicate repeatedly until all results are exhausted.
+   *
+   * <p>Capable of iterating through all results regardless of limits. The passed {@code
+   * querySupplier} may choose to pre-set limits or not; this only affects the number of queries
+   * that may be issued, not the size of the final results.
+   *
+   * <p>Since multiple queries may be issued, this method is subject to races when the result set
+   * changes mid-iteration. This may result in skipped results, if an entity gets modified to jump
+   * to the front of the list after this method has passed it. It may also result in duplicate
+   * results, if an entity at the end of one batch of results gets pushed back further, putting it
+   * at the beginning of the next batch. This race cannot be avoided unless we change the underlying
+   * index interface to support true continuation tokens.
+   *
+   * @param querySupplier supplier for queries. Callers will generally pass a lambda that invokes an
+   *     underlying {@code Provider<InternalFooQuery>}, since the instances are not reusable. The
+   *     lambda may also call additional methods on the newly-created query, such as {@link
+   *     #enforceVisibility(boolean)}.
+   * @param predicate predicate to search for.
+   * @param <T> result type.
+   * @return exhaustive list of results, subject to the race condition described above.
+   */
+  protected static <T> ImmutableList<T> queryExhaustively(
+      Supplier<? extends InternalQuery<T, ?>> querySupplier, Predicate<T> predicate) {
+    ImmutableList.Builder<T> b = null;
+    int start = 0;
+    while (true) {
+      QueryResult<T> qr = querySupplier.get().setStart(start).queryResults(predicate);
+      if (b == null) {
+        if (!qr.more()) {
+          return qr.entities();
+        }
+        b = ImmutableList.builder();
+      }
+      b.addAll(qr.entities());
+      if (!qr.more()) {
+        return b.build();
+      }
+      start += qr.entities().size();
+    }
+  }
 }
diff --git a/java/com/google/gerrit/index/query/ListResultSet.java b/java/com/google/gerrit/index/query/ListResultSet.java
new file mode 100644
index 0000000..4cf48c8
--- /dev/null
+++ b/java/com/google/gerrit/index/query/ListResultSet.java
@@ -0,0 +1,47 @@
+// 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.gerrit.index.query;
+
+import com.google.common.collect.ImmutableList;
+import java.util.Iterator;
+import java.util.List;
+
+public class ListResultSet<T> implements ResultSet<T> {
+  private ImmutableList<T> items;
+
+  public ListResultSet(List<T> r) {
+    items = ImmutableList.copyOf(r);
+  }
+
+  @Override
+  public Iterator<T> iterator() {
+    return toList().iterator();
+  }
+
+  @Override
+  public ImmutableList<T> toList() {
+    if (items == null) {
+      throw new IllegalStateException("Results already obtained");
+    }
+    ImmutableList<T> r = items;
+    items = null;
+    return r;
+  }
+
+  @Override
+  public void close() {
+    items = null;
+  }
+}
diff --git a/java/com/google/gerrit/index/query/Matchable.java b/java/com/google/gerrit/index/query/Matchable.java
index 3d07943..7a16ae8 100644
--- a/java/com/google/gerrit/index/query/Matchable.java
+++ b/java/com/google/gerrit/index/query/Matchable.java
@@ -14,15 +14,9 @@
 
 package com.google.gerrit.index.query;
 
-import com.google.gwtorm.server.OrmException;
-
 public interface Matchable<T> {
-  /**
-   * Does this predicate match this object?
-   *
-   * @throws OrmException
-   */
-  boolean match(T object) throws OrmException;
+  /** Does this predicate match this object? */
+  boolean match(T object);
 
   /** @return a cost estimate to run this predicate, higher figures cost more. */
   int getCost();
diff --git a/java/com/google/gerrit/index/query/NotPredicate.java b/java/com/google/gerrit/index/query/NotPredicate.java
index 750759d..14cb740 100644
--- a/java/com/google/gerrit/index/query/NotPredicate.java
+++ b/java/com/google/gerrit/index/query/NotPredicate.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.base.Preconditions.checkState;
 
-import com.google.gwtorm.server.OrmException;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
@@ -64,7 +63,7 @@
   }
 
   @Override
-  public boolean match(T object) throws OrmException {
+  public boolean match(T object) {
     checkState(
         that.isMatchable(),
         "match invoked, but child predicate %s doesn't implement %s",
diff --git a/java/com/google/gerrit/index/query/OrPredicate.java b/java/com/google/gerrit/index/query/OrPredicate.java
index 8c3ed1c..9bc3769 100644
--- a/java/com/google/gerrit/index/query/OrPredicate.java
+++ b/java/com/google/gerrit/index/query/OrPredicate.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.base.Preconditions.checkState;
 
-import com.google.gwtorm.server.OrmException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -82,7 +81,7 @@
   }
 
   @Override
-  public boolean match(T object) throws OrmException {
+  public boolean match(T object) {
     for (Predicate<T> c : children) {
       checkState(
           c.isMatchable(),
diff --git a/java/com/google/gerrit/index/query/Paginated.java b/java/com/google/gerrit/index/query/Paginated.java
index 20f65dc..e61dd53 100644
--- a/java/com/google/gerrit/index/query/Paginated.java
+++ b/java/com/google/gerrit/index/query/Paginated.java
@@ -15,11 +15,9 @@
 package com.google.gerrit.index.query;
 
 import com.google.gerrit.index.QueryOptions;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
 
 public interface Paginated<T> {
   QueryOptions getOptions();
 
-  ResultSet<T> restart(int start) throws OrmException;
+  ResultSet<T> restart(int start);
 }
diff --git a/java/com/google/gerrit/index/query/Predicate.java b/java/com/google/gerrit/index/query/Predicate.java
index 53c92c9..b5ed82d 100644
--- a/java/com/google/gerrit/index/query/Predicate.java
+++ b/java/com/google/gerrit/index/query/Predicate.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.index.query;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
 
 import com.google.common.collect.Iterables;
@@ -82,6 +83,8 @@
 
   /** Invert the passed node. */
   public static <T> Predicate<T> not(Predicate<T> that) {
+    checkArgument(
+        !(that instanceof Any), "negating any() is unsafe because it post-filters all results");
     if (that instanceof NotPredicate) {
       // Negate of a negate is the original predicate.
       //
diff --git a/java/com/google/gerrit/index/query/QueryBuilder.java b/java/com/google/gerrit/index/query/QueryBuilder.java
index c6c39c3..d24cfeb 100644
--- a/java/com/google/gerrit/index/query/QueryBuilder.java
+++ b/java/com/google/gerrit/index/query/QueryBuilder.java
@@ -14,10 +14,12 @@
 
 package com.google.gerrit.index.query;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.gerrit.index.query.Predicate.and;
 import static com.google.gerrit.index.query.Predicate.not;
 import static com.google.gerrit.index.query.Predicate.or;
 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;
 import static com.google.gerrit.index.query.QueryParser.EXACT_PHRASE;
 import static com.google.gerrit.index.query.QueryParser.FIELD_NAME;
@@ -25,7 +27,13 @@
 import static com.google.gerrit.index.query.QueryParser.OR;
 import static com.google.gerrit.index.query.QueryParser.SINGLE_WORD;
 
+import com.google.common.base.Ascii;
+import com.google.common.base.CharMatcher;
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.Extension;
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -34,9 +42,7 @@
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 import org.antlr.runtime.tree.Tree;
 
 /**
@@ -68,45 +74,60 @@
  * <p>Subclasses may also declare a handler for values which appear without operator by overriding
  * {@link #defaultField(String)}.
  *
+ * <p>Instances are non-singletons and should only be used once, in order to rescan the {@code
+ * DynamicMap} of plugin-provided operators on each query invocation.
+ *
  * @param <T> type of object the predicates can evaluate in memory.
  */
-public abstract class QueryBuilder<T> {
+public abstract class QueryBuilder<T, Q extends QueryBuilder<T, Q>> {
   /** Converts a value string passed to an operator into a {@link Predicate}. */
-  public interface OperatorFactory<T, Q extends QueryBuilder<T>> {
+  public interface OperatorFactory<T, Q extends QueryBuilder<T, Q>> {
     Predicate<T> create(Q builder, String value) throws QueryParseException;
   }
 
   /**
    * Defines the operators known by a QueryBuilder.
    *
-   * <p>This class is thread-safe and may be reused or cached.
+   * <p>Operators are discovered by scanning for methods annotated with {@link Operator}. Operator
+   * methods must be public, non-abstract, return a {@code Predicate}, and take a single string as
+   * an argument.
    *
-   * @param <T> type of object the predicates can evaluate in memory.
+   * <p>This class is deeply immutable.
+   *
+   * @param <T> type of object the predicates can evaluate.
    * @param <Q> type of the query builder subclass.
    */
-  public static class Definition<T, Q extends QueryBuilder<T>> {
-    private final Map<String, OperatorFactory<T, Q>> opFactories = new HashMap<>();
+  public static class Definition<T, Q extends QueryBuilder<T, Q>> {
+    private final ImmutableMap<String, OperatorFactory<T, Q>> opFactories;
 
-    public Definition(Class<Q> clazz) {
-      // Guess at the supported operators by scanning methods.
-      //
+    public Definition(Class<? extends Q> clazz) {
+      ImmutableMap.Builder<String, OperatorFactory<T, Q>> b = ImmutableMap.builder();
       Class<?> c = clazz;
       while (c != QueryBuilder.class) {
         for (Method method : c.getDeclaredMethods()) {
-          if (method.getAnnotation(Operator.class) != null
-              && Predicate.class.isAssignableFrom(method.getReturnType())
-              && method.getParameterTypes().length == 1
-              && method.getParameterTypes()[0] == String.class
-              && (method.getModifiers() & Modifier.ABSTRACT) == 0
-              && (method.getModifiers() & Modifier.PUBLIC) == Modifier.PUBLIC) {
-            final String name = method.getName().toLowerCase();
-            if (!opFactories.containsKey(name)) {
-              opFactories.put(name, new ReflectionFactory<T, Q>(name, method));
-            }
+          if (method.getAnnotation(Operator.class) == null) {
+            continue;
           }
+          checkArgument(
+              CharMatcher.ascii().matchesAllOf(method.getName()),
+              "method name must be ASCII: %s",
+              method.getName());
+          checkArgument(
+              Predicate.class.isAssignableFrom(method.getReturnType())
+                  && method.getParameterTypes().length == 1
+                  && method.getParameterTypes()[0] == String.class
+                  && Modifier.isPublic(method.getModifiers())
+                  && !Modifier.isAbstract(method.getModifiers()),
+              "method must be of the form \"@%s public Predicate<T> %s(String value)\": %s",
+              Operator.class.getSimpleName(),
+              method.getName(),
+              method);
+          String name = Ascii.toLowerCase(method.getName());
+          b.put(name, new ReflectionFactory<>(name, method));
         }
         c = c.getSuperclass();
       }
+      opFactories = b.build();
     }
   }
 
@@ -161,14 +182,26 @@
     return null;
   }
 
-  protected final Definition<T, ? extends QueryBuilder<T>> builderDef;
+  protected final Definition<T, Q> builderDef;
+  private final ImmutableMap<String, OperatorFactory<T, Q>> opFactories;
 
-  protected final Map<String, OperatorFactory<?, ?>> opFactories;
-
-  @SuppressWarnings({"unchecked", "rawtypes"})
-  protected QueryBuilder(Definition<T, ? extends QueryBuilder<T>> def) {
+  protected QueryBuilder(
+      Definition<T, Q> def,
+      @Nullable DynamicMap<? extends OperatorFactory<T, Q>> dynamicOpFactories) {
     builderDef = def;
-    opFactories = (Map) def.opFactories;
+
+    if (dynamicOpFactories != null) {
+      ImmutableMap.Builder<String, OperatorFactory<T, Q>> opFactoriesBuilder =
+          ImmutableMap.builder();
+      opFactoriesBuilder.putAll(def.opFactories);
+      for (Extension<? extends OperatorFactory<T, Q>> e : dynamicOpFactories) {
+        String name = e.getExportName() + "_" + e.getPluginName();
+        opFactoriesBuilder.put(name, e.getProvider().get());
+      }
+      opFactories = opFactoriesBuilder.build();
+    } else {
+      opFactories = def.opFactories;
+    }
   }
 
   /**
@@ -214,44 +247,44 @@
         return not(toPredicate(onlyChildOf(r)));
 
       case DEFAULT_FIELD:
-        return defaultField(onlyChildOf(r));
+        return defaultField(concatenateChildText(r));
 
       case FIELD_NAME:
-        return operator(r.getText(), onlyChildOf(r));
+        return operator(r.getText(), concatenateChildText(r));
 
       default:
         throw error("Unsupported operator: " + r);
     }
   }
 
-  private Predicate<T> operator(String name, Tree val) throws QueryParseException {
-    switch (val.getType()) {
-        // Expand multiple values, "foo:(a b c)", as though they were written
-        // out with the longer form, "foo:a foo:b foo:c".
-        //
-      case AND:
-      case OR:
-        {
-          List<Predicate<T>> p = new ArrayList<>(val.getChildCount());
-          for (int i = 0; i < val.getChildCount(); i++) {
-            final Tree c = val.getChild(i);
-            if (c.getType() != DEFAULT_FIELD) {
-              throw error("Nested operator not expected: " + c);
-            }
-            p.add(operator(name, onlyChildOf(c)));
-          }
-          return val.getType() == AND ? and(p) : or(p);
-        }
+  private static String concatenateChildText(Tree r) throws QueryParseException {
+    if (r.getChildCount() == 0) {
+      throw error("Expected children under: " + r);
+    }
+    if (r.getChildCount() == 1) {
+      return getFieldValue(r.getChild(0));
+    }
+    StringBuilder sb = new StringBuilder();
+    for (int i = 0; i < r.getChildCount(); i++) {
+      sb.append(getFieldValue(r.getChild(i)));
+    }
+    return sb.toString();
+  }
 
+  private static String getFieldValue(Tree r) throws QueryParseException {
+    if (r.getChildCount() != 0) {
+      throw error("Expected no children under: " + r);
+    }
+    switch (r.getType()) {
       case SINGLE_WORD:
+      case COLON:
       case EXACT_PHRASE:
-        if (val.getChildCount() != 0) {
-          throw error("Expected no children under: " + val);
-        }
-        return operator(name, val.getText());
-
+        return r.getText();
       default:
-        throw error("Unsupported node in operator " + name + ": " + val);
+        throw error(
+            String.format(
+                "Unsupported %s node in operator %s: %s",
+                QueryParser.tokenNames[r.getType()], r.getParent(), r));
     }
   }
 
@@ -265,20 +298,6 @@
     return f.create(this, value);
   }
 
-  private Predicate<T> defaultField(Tree r) throws QueryParseException {
-    switch (r.getType()) {
-      case SINGLE_WORD:
-      case EXACT_PHRASE:
-        if (r.getChildCount() != 0) {
-          throw error("Expected no children under: " + r);
-        }
-        return defaultField(r.getText());
-
-      default:
-        throw error("Unsupported node: " + r);
-    }
-  }
-
   /**
    * Handle a value present outside of an operator.
    *
@@ -322,7 +341,7 @@
   @Target(ElementType.METHOD)
   protected @interface Operator {}
 
-  private static class ReflectionFactory<T, Q extends QueryBuilder<T>>
+  private static class ReflectionFactory<T, Q extends QueryBuilder<T, Q>>
       implements OperatorFactory<T, Q> {
     private final String name;
     private final Method method;
diff --git a/java/com/google/gerrit/index/query/QueryProcessor.java b/java/com/google/gerrit/index/query/QueryProcessor.java
index 64d8742..7077245 100644
--- a/java/com/google/gerrit/index/query/QueryProcessor.java
+++ b/java/com/google/gerrit/index/query/QueryProcessor.java
@@ -26,11 +26,11 @@
 import com.google.common.collect.Ordering;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.Index;
 import com.google.gerrit.index.IndexCollection;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.IndexRewriter;
-import com.google.gerrit.index.IndexedQuery;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.SchemaDefinitions;
 import com.google.gerrit.metrics.Description;
@@ -38,9 +38,6 @@
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.Timer1;
 import com.google.gerrit.server.logging.CallerFinder;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.OrmRuntimeException;
-import com.google.gwtorm.server.ResultSet;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
@@ -91,6 +88,7 @@
 
   private boolean enforceVisibility = true;
   private int userProvidedLimit;
+  private boolean isNoLimit;
   private Set<String> requestedFields;
 
   protected QueryProcessor(
@@ -157,6 +155,11 @@
     return this;
   }
 
+  public QueryProcessor<T> setNoLimit(boolean isNoLimit) {
+    this.isNoLimit = isNoLimit;
+    return this;
+  }
+
   public QueryProcessor<T> setRequestedFields(Set<String> fields) {
     requestedFields = fields;
     return this;
@@ -169,7 +172,7 @@
    * @param query the query.
    * @return results of the query.
    */
-  public QueryResult<T> query(Predicate<T> query) throws OrmException, QueryParseException {
+  public QueryResult<T> query(Predicate<T> query) throws QueryParseException {
     return query(ImmutableList.of(query)).get(0);
   }
 
@@ -184,13 +187,10 @@
    * @return results of the queries, one QueryResult per input query, in the same order as the
    *     input.
    */
-  public List<QueryResult<T>> query(List<Predicate<T>> queries)
-      throws OrmException, QueryParseException {
+  public List<QueryResult<T>> query(List<Predicate<T>> queries) throws QueryParseException {
     try {
       return query(null, queries);
-    } catch (OrmRuntimeException e) {
-      throw new OrmException(e.getMessage(), e);
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       if (e.getCause() != null) {
         Throwables.throwIfInstanceOf(e.getCause(), QueryParseException.class);
       }
@@ -199,8 +199,7 @@
   }
 
   private List<QueryResult<T>> query(
-      @Nullable List<String> queryStrings, List<Predicate<T>> queries)
-      throws OrmException, QueryParseException {
+      @Nullable List<String> queryStrings, List<Predicate<T>> queries) throws QueryParseException {
     long startNanos = System.nanoTime();
     checkState(!used.getAndSet(true), "%s has already been used", getClass().getSimpleName());
     int cnt = queries.size();
@@ -268,7 +267,7 @@
 
       out = new ArrayList<>(cnt);
       for (int i = 0; i < cnt; i++) {
-        List<T> matchesList = matches.get(i).toList();
+        ImmutableList<T> matchesList = matches.get(i).toList();
         logger.atFine().log(
             "Matches[%d]:\n%s",
             i, lazy(() -> matchesList.stream().map(this::formatForLogging).collect(toSet())));
@@ -283,7 +282,7 @@
       // Only measure successful queries that actually touched the index.
       metrics.executionTime.record(
           schemaDef.getName(), System.nanoTime() - startNanos, TimeUnit.NANOSECONDS);
-    } catch (OrmException | OrmRuntimeException e) {
+    } catch (StorageException e) {
       Optional<QueryParseException> qpe = findQueryParseException(e);
       if (qpe.isPresent()) {
         throw new QueryParseException(qpe.get().getMessage(), e);
@@ -325,7 +324,7 @@
       return requestedFields;
     }
     Index<?, T> index = indexes.getSearchIndex();
-    return index != null ? index.getSchema().getStoredFields().keySet() : ImmutableSet.<String>of();
+    return index != null ? index.getSchema().getStoredFields().keySet() : ImmutableSet.of();
   }
 
   /**
@@ -354,6 +353,9 @@
   }
 
   private int getEffectiveLimit(Predicate<T> p) {
+    if (isNoLimit == true) {
+      return Integer.MAX_VALUE;
+    }
     List<Integer> possibleLimits = new ArrayList<>(4);
     possibleLimits.add(getBackendSupportedLimit());
     possibleLimits.add(getPermittedLimit());
diff --git a/java/com/google/gerrit/index/query/QueryResult.java b/java/com/google/gerrit/index/query/QueryResult.java
index 341e2b6..33fcef0 100644
--- a/java/com/google/gerrit/index/query/QueryResult.java
+++ b/java/com/google/gerrit/index/query/QueryResult.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.index.query;
 
 import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.Nullable;
 import java.util.List;
 
@@ -22,15 +23,15 @@
 @AutoValue
 public abstract class QueryResult<T> {
   public static <T> QueryResult<T> create(
-      @Nullable String query, Predicate<T> predicate, int limit, List<T> entites) {
+      @Nullable String query, Predicate<T> predicate, int limit, List<T> entities) {
     boolean more;
-    if (entites.size() > limit) {
+    if (entities.size() > limit) {
       more = true;
-      entites = entites.subList(0, limit);
+      entities = entities.subList(0, limit);
     } else {
       more = false;
     }
-    return new AutoValue_QueryResult<>(query, predicate, entites, more);
+    return new AutoValue_QueryResult<>(query, predicate, ImmutableList.copyOf(entities), more);
   }
 
   /** @return the original query string, or null if the query was created programmatically. */
@@ -41,7 +42,7 @@
   public abstract Predicate<T> predicate();
 
   /** @return the query results. */
-  public abstract List<T> entities();
+  public abstract ImmutableList<T> entities();
 
   /**
    * @return whether the query could be retried with a higher start/limit to produce more results.
diff --git a/java/com/google/gerrit/index/query/ResultSet.java b/java/com/google/gerrit/index/query/ResultSet.java
new file mode 100644
index 0000000..65fcd45
--- /dev/null
+++ b/java/com/google/gerrit/index/query/ResultSet.java
@@ -0,0 +1,52 @@
+// 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.gerrit.index.query;
+
+import com.google.common.collect.ImmutableList;
+import java.util.Iterator;
+
+/**
+ * Result from any data store query function.
+ *
+ * @param <T> type of entity being returned by the query.
+ */
+public interface ResultSet<T> extends Iterable<T> {
+  /**
+   * Obtain an iterator to loop through the results.
+   *
+   * <p>The iterator can be obtained only once. When the iterator completes ( <code>hasNext()</code>
+   * returns false) {@link #close()} will be automatically called.
+   */
+  @Override
+  Iterator<T> iterator();
+
+  /**
+   * Materialize all results as a single list.
+   *
+   * <p>Prior to returning {@link #close()} is invoked. This method must not be combined with {@link
+   * #iterator()} on the same instance.
+   *
+   * @return immutable list of the complete results.
+   */
+  ImmutableList<T> toList();
+
+  /**
+   * Close the result, discarding any further results.
+   *
+   * <p>This method may be invoked more than once. Its main use is to stop obtaining results before
+   * the iterator has finished.
+   */
+  void close();
+}
diff --git a/java/com/google/gerrit/index/query/TimestampRangePredicate.java b/java/com/google/gerrit/index/query/TimestampRangePredicate.java
index edc2120..38b2b73 100644
--- a/java/com/google/gerrit/index/query/TimestampRangePredicate.java
+++ b/java/com/google/gerrit/index/query/TimestampRangePredicate.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.index.query;
 
 import com.google.gerrit.index.FieldDef;
-import com.google.gwtjsonrpc.common.JavaSqlTimestampHelper;
+import com.google.gerrit.json.JavaSqlTimestampHelper;
 import java.sql.Timestamp;
 import java.util.Date;
 
diff --git a/java/com/google/gerrit/index/query/testing/BUILD b/java/com/google/gerrit/index/query/testing/BUILD
new file mode 100644
index 0000000..1785f49
--- /dev/null
+++ b/java/com/google/gerrit/index/query/testing/BUILD
@@ -0,0 +1,17 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
+package(
+    default_testonly = True,
+    default_visibility = ["//visibility:public"],
+)
+
+java_library(
+    name = "testing",
+    srcs = glob(["*.java"]),
+    deps = [
+        "//antlr3:query_parser",
+        "//lib:guava",
+        "//lib/antlr:java-runtime",
+        "//lib/truth",
+    ],
+)
diff --git a/java/com/google/gerrit/index/query/testing/TreeSubject.java b/java/com/google/gerrit/index/query/testing/TreeSubject.java
new file mode 100644
index 0000000..c60b363
--- /dev/null
+++ b/java/com/google/gerrit/index/query/testing/TreeSubject.java
@@ -0,0 +1,73 @@
+// 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.testing;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.truth.Truth.assertAbout;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.Subject;
+import com.google.gerrit.index.query.QueryParser;
+import org.antlr.runtime.tree.Tree;
+
+public class TreeSubject extends Subject<TreeSubject, Tree> {
+  public static TreeSubject assertThat(Tree actual) {
+    return assertAbout(TreeSubject::new).that(actual);
+  }
+
+  private TreeSubject(FailureMetadata failureMetadata, Tree tree) {
+    super(failureMetadata, tree);
+  }
+
+  public void hasType(int expectedType) {
+    isNotNull();
+    check("getType()").that(typeName(actual().getType())).isEqualTo(typeName(expectedType));
+  }
+
+  public void hasText(String expectedText) {
+    requireNonNull(expectedText);
+    isNotNull();
+    check("getText()").that(actual().getText()).isEqualTo(expectedText);
+  }
+
+  public void hasNoChildren() {
+    isNotNull();
+    check("getChildCount()").that(actual().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);
+  }
+
+  public TreeSubject child(int childIndex) {
+    isNotNull();
+    return check("getChild(%s)", childIndex)
+        .about(TreeSubject::new)
+        .that(actual().getChild(childIndex));
+  }
+
+  private static String typeName(int type) {
+    checkArgument(
+        type >= 0 && type < QueryParser.tokenNames.length,
+        "invalid token type %s, max is %s",
+        type,
+        QueryParser.tokenNames.length - 1);
+    return QueryParser.tokenNames[type];
+  }
+}
diff --git a/java/com/google/gerrit/jgit/BUILD b/java/com/google/gerrit/jgit/BUILD
new file mode 100644
index 0000000..e67ebfe
--- /dev/null
+++ b/java/com/google/gerrit/jgit/BUILD
@@ -0,0 +1,13 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
+java_library(
+    name = "jgit",
+    srcs = [
+        "diff/ReplaceEdit.java",
+    ],
+    visibility = ["//visibility:public"],
+    deps = [
+        "//lib:gson",
+        "//lib/jgit/org.eclipse.jgit:jgit",
+    ],
+)
diff --git a/java/com/google/gerrit/jgit/diff/ReplaceEdit.java b/java/com/google/gerrit/jgit/diff/ReplaceEdit.java
new file mode 100644
index 0000000..45bfad2
--- /dev/null
+++ b/java/com/google/gerrit/jgit/diff/ReplaceEdit.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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.jgit.diff;
+
+import java.util.List;
+import org.eclipse.jgit.diff.Edit;
+
+public class ReplaceEdit extends Edit {
+  private List<Edit> internalEdit;
+
+  public ReplaceEdit(int as, int ae, int bs, int be, List<Edit> internal) {
+    super(as, ae, bs, be);
+    internalEdit = internal;
+  }
+
+  public ReplaceEdit(Edit orig, List<Edit> internal) {
+    super(orig.getBeginA(), orig.getEndA(), orig.getBeginB(), orig.getEndB());
+    internalEdit = internal;
+  }
+
+  public List<Edit> getInternalEdits() {
+    return internalEdit;
+  }
+}
diff --git a/java/com/google/gerrit/json/BUILD b/java/com/google/gerrit/json/BUILD
new file mode 100644
index 0000000..d9cec45
--- /dev/null
+++ b/java/com/google/gerrit/json/BUILD
@@ -0,0 +1,10 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
+java_library(
+    name = "json",
+    srcs = glob(["*.java"]),
+    visibility = ["//visibility:public"],
+    deps = [
+        "//lib:gson",
+    ],
+)
diff --git a/java/com/google/gerrit/json/JavaSqlTimestampHelper.java b/java/com/google/gerrit/json/JavaSqlTimestampHelper.java
new file mode 100644
index 0000000..b59cbd0d
--- /dev/null
+++ b/java/com/google/gerrit/json/JavaSqlTimestampHelper.java
@@ -0,0 +1,108 @@
+// 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.gerrit.json;
+
+import java.sql.Timestamp;
+import java.util.Date;
+
+/** Utility to parse Timestamp from a string. */
+public class JavaSqlTimestampHelper {
+  /**
+   * Parse a string into a timestamp.
+   *
+   * <p>Note that {@link Timestamp}s have no timezone, so the result is relative to the UTC epoch.
+   *
+   * <p>Supports the format {@code yyyy-MM-dd[ HH:mm:ss[.SSS][ Z]]} where {@code Z} is a 4-digit
+   * offset with sign, e.g. {@code -0500}.
+   *
+   * @param s input string.
+   * @return resulting timestamp.
+   */
+  public static Timestamp parseTimestamp(String s) {
+    String[] components = s.split(" ");
+    if (components.length < 1 || components.length > 3) {
+      throw new IllegalArgumentException("Expected date and optional time: " + s);
+    }
+    String date = components[0];
+    String time = components.length >= 2 ? components[1] : null;
+    int off = components.length == 3 ? parseTimeZone(components[2]) : 0;
+    String[] dSplit = date.split("-");
+    if (dSplit.length != 3) {
+      throw new IllegalArgumentException("Invalid date format: " + date);
+    }
+    int yy, mm, dd;
+    try {
+      yy = Integer.parseInt(dSplit[0]) - 1900;
+      mm = Integer.parseInt(dSplit[1]) - 1;
+      dd = Integer.parseInt(dSplit[2]);
+    } catch (NumberFormatException e) {
+      throw new IllegalArgumentException("Invalid date format: " + date, e);
+    }
+
+    int hh, mi, ss, ns;
+    if (time != null) {
+      int p = time.indexOf('.');
+      String t;
+      double f;
+      try {
+        if (p >= 0) {
+          t = time.substring(0, p);
+          f = Double.parseDouble("0." + time.substring(p + 1));
+        } else {
+          t = time;
+          f = 0;
+        }
+        String[] tSplit = t.split(":");
+        if (tSplit.length != 3) {
+          throw new IllegalArgumentException("Invalid time format: " + time);
+        }
+        hh = Integer.parseInt(tSplit[0]);
+        mi = Integer.parseInt(tSplit[1]);
+        ss = Integer.parseInt(tSplit[2]);
+        ns = (int) Math.round(f * 1e9);
+      } catch (NumberFormatException e) {
+        throw new IllegalArgumentException("Invalid time format: " + time, e);
+      }
+    } else {
+      hh = 0;
+      mi = 0;
+      ss = 0;
+      ns = 0;
+    }
+    @SuppressWarnings("deprecation")
+    Timestamp result = new Timestamp(Date.UTC(yy, mm, dd, hh, mi, ss) - off);
+    result.setNanos(ns);
+    return result;
+  }
+
+  private static int parseTimeZone(String s) {
+    if (s.length() != 5 || (s.charAt(0) != '-' && s.charAt(0) != '+')) {
+      throw new IllegalArgumentException("Invalid time zone: " + s);
+    }
+    for (int i = 1; i < s.length(); i++) {
+      if (s.charAt(i) < '0' || s.charAt(i) > '9') {
+        throw new IllegalArgumentException("Invalid time zone: " + s);
+      }
+    }
+    int off =
+        (s.charAt(0) == '-' ? -1 : 1)
+            * 60
+            * 1000
+            * ((60 * Integer.parseInt(s.substring(1, 3))) + Integer.parseInt(s.substring(3, 5)));
+    return off;
+  }
+
+  private JavaSqlTimestampHelper() {}
+}
diff --git a/java/com/google/gerrit/json/OutputFormat.java b/java/com/google/gerrit/json/OutputFormat.java
new file mode 100644
index 0000000..a2d174f
--- /dev/null
+++ b/java/com/google/gerrit/json/OutputFormat.java
@@ -0,0 +1,69 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.json;
+
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import java.sql.Timestamp;
+
+/** Standard output format used by an API call. */
+public enum OutputFormat {
+  /**
+   * The output is a human readable text format. It may also be regular enough to be machine
+   * readable. Whether or not the text format is machine readable and will be committed to as a long
+   * term format that tools can build upon is specific to each API call.
+   */
+  TEXT,
+
+  /**
+   * Pretty-printed JSON format. This format uses whitespace to make the output readable by a human,
+   * but is also machine readable with a JSON library. The structure of the output is a long term
+   * format that tools can rely upon.
+   */
+  JSON,
+
+  /**
+   * Same as {@link #JSON}, but with unnecessary whitespace removed to save generation time and copy
+   * costs. Typically JSON_COMPACT format is used by a browser based HTML client running over the
+   * network.
+   */
+  JSON_COMPACT;
+
+  /** @return true when the format is either JSON or JSON_COMPACT. */
+  public boolean isJson() {
+    return this == JSON_COMPACT || this == JSON;
+  }
+
+  /** @return a new Gson instance configured according to the format. */
+  public GsonBuilder newGsonBuilder() {
+    if (!isJson()) {
+      throw new IllegalStateException(String.format("%s is not JSON", this));
+    }
+    GsonBuilder gb =
+        new GsonBuilder()
+            .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
+            .registerTypeAdapter(Timestamp.class, new SqlTimestampDeserializer());
+    if (this == OutputFormat.JSON) {
+      gb.setPrettyPrinting();
+    }
+    return gb;
+  }
+
+  /** @return a new Gson instance configured according to the format. */
+  public Gson newGson() {
+    return newGsonBuilder().create();
+  }
+}
diff --git a/java/com/google/gerrit/json/SqlTimestampDeserializer.java b/java/com/google/gerrit/json/SqlTimestampDeserializer.java
new file mode 100644
index 0000000..0148226
--- /dev/null
+++ b/java/com/google/gerrit/json/SqlTimestampDeserializer.java
@@ -0,0 +1,64 @@
+// 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.gerrit.json;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
+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;
+import java.sql.Timestamp;
+import java.text.SimpleDateFormat;
+import java.util.TimeZone;
+
+class SqlTimestampDeserializer implements JsonDeserializer<Timestamp>, JsonSerializer<Timestamp> {
+  private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
+
+  @Override
+  public Timestamp deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
+      throws JsonParseException {
+    if (json.isJsonNull()) {
+      return null;
+    }
+    if (!json.isJsonPrimitive()) {
+      throw new JsonParseException("Expected string for timestamp type");
+    }
+    JsonPrimitive p = (JsonPrimitive) json;
+    if (!p.isString()) {
+      throw new JsonParseException("Expected string for timestamp type");
+    }
+
+    return JavaSqlTimestampHelper.parseTimestamp(p.getAsString());
+  }
+
+  @Override
+  public JsonElement serialize(Timestamp src, Type typeOfSrc, JsonSerializationContext context) {
+    if (src == null) {
+      return JsonNull.INSTANCE;
+    }
+    return new JsonPrimitive(newFormat().format(src) + "000000");
+  }
+
+  private static SimpleDateFormat newFormat() {
+    SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
+    f.setTimeZone(UTC);
+    f.setLenient(true);
+    return f;
+  }
+}
diff --git a/java/com/google/gerrit/launcher/GerritLauncher.java b/java/com/google/gerrit/launcher/GerritLauncher.java
index 60d21f9..f6c395e 100644
--- a/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -93,7 +93,6 @@
       System.err.println("  init            Initialize a Gerrit installation");
       System.err.println("  reindex         Rebuild the secondary index");
       System.err.println("  daemon          Run the Gerrit network daemons");
-      System.err.println("  gsql            Run the interactive query console");
       System.err.println("  version         Display the build version number");
       System.err.println("  passwd          Set or change password in secure.config");
 
@@ -324,7 +323,7 @@
     }
 
     String name = ze.getName();
-    jars.put(name.substring(name.lastIndexOf('/'), name.length()), tmp.toURI().toURL());
+    jars.put(name.substring(name.lastIndexOf('/')), tmp.toURI().toURL());
   }
 
   private static void move(SortedMap<String, URL> jars, String prefix, List<URL> extapi) {
diff --git a/java/com/google/gerrit/lifecycle/LifecycleModule.java b/java/com/google/gerrit/lifecycle/LifecycleModule.java
index bfb61d2..0fb4653 100644
--- a/java/com/google/gerrit/lifecycle/LifecycleModule.java
+++ b/java/com/google/gerrit/lifecycle/LifecycleModule.java
@@ -1,3 +1,17 @@
+// 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.lifecycle;
 
 import com.google.gerrit.extensions.config.FactoryModule;
diff --git a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
index cb26488..7a0430c 100644
--- a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
+++ b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
@@ -20,6 +20,7 @@
 
 import com.google.common.base.Joiner;
 import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
@@ -30,6 +31,7 @@
 import com.google.common.util.concurrent.MoreExecutors;
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.FieldType;
 import com.google.gerrit.index.Index;
@@ -38,18 +40,14 @@
 import com.google.gerrit.index.Schema.Values;
 import com.google.gerrit.index.query.DataSource;
 import com.google.gerrit.index.query.FieldBundle;
+import com.google.gerrit.index.query.ListResultSet;
+import com.google.gerrit.index.query.ResultSet;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.index.IndexUtils;
 import com.google.gerrit.server.logging.LoggingContextAwareExecutorService;
 import com.google.gerrit.server.logging.LoggingContextAwareScheduledExecutorService;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
 import java.io.IOException;
 import java.sql.Timestamp;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.Callable;
@@ -215,7 +213,7 @@
   }
 
   @Override
-  public void markReady(boolean ready) throws IOException {
+  public void markReady(boolean ready) {
     IndexUtils.setReady(sitePaths, name, schema.getVersion(), ready);
   }
 
@@ -289,8 +287,12 @@
   }
 
   @Override
-  public void deleteAll() throws IOException {
-    writer.deleteAll();
+  public void deleteAll() {
+    try {
+      writer.deleteAll();
+    } catch (IOException e) {
+      throw new StorageException(e);
+    }
   }
 
   public IndexWriter getWriter() {
@@ -488,49 +490,33 @@
     }
 
     @Override
-    public ResultSet<V> read() throws OrmException {
+    public ResultSet<V> read() {
       return readImpl(AbstractLuceneIndex.this::fromDocument);
     }
 
     @Override
-    public ResultSet<FieldBundle> readRaw() throws OrmException {
+    public ResultSet<FieldBundle> readRaw() {
       return readImpl(AbstractLuceneIndex.this::toFieldBundle);
     }
 
-    private <T> ResultSet<T> readImpl(Function<Document, T> mapper) throws OrmException {
+    private <T> ResultSet<T> readImpl(Function<Document, T> mapper) {
       IndexSearcher searcher = null;
       try {
         searcher = acquire();
         int realLimit = opts.start() + opts.limit();
         TopFieldDocs docs = searcher.search(query, realLimit, sort);
-        List<T> result = new ArrayList<>(docs.scoreDocs.length);
+        ImmutableList.Builder<T> b = ImmutableList.builderWithExpectedSize(docs.scoreDocs.length);
         for (int i = opts.start(); i < docs.scoreDocs.length; i++) {
           ScoreDoc sd = docs.scoreDocs[i];
           Document doc = searcher.doc(sd.doc, opts.fields());
           T mapperResult = mapper.apply(doc);
           if (mapperResult != null) {
-            result.add(mapperResult);
+            b.add(mapperResult);
           }
         }
-        final List<T> r = Collections.unmodifiableList(result);
-        return new ResultSet<T>() {
-          @Override
-          public Iterator<T> iterator() {
-            return r.iterator();
-          }
-
-          @Override
-          public List<T> toList() {
-            return r;
-          }
-
-          @Override
-          public void close() {
-            // Do nothing.
-          }
-        };
+        return new ListResultSet<>(b.build());
       } catch (IOException e) {
-        throw new OrmException(e);
+        throw new StorageException(e);
       } finally {
         if (searcher != null) {
           try {
diff --git a/java/com/google/gerrit/lucene/BUILD b/java/com/google/gerrit/lucene/BUILD
index ad08ff5..40b2548 100644
--- a/java/com/google/gerrit/lucene/BUILD
+++ b/java/com/google/gerrit/lucene/BUILD
@@ -12,7 +12,6 @@
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib/lucene:lucene-core-and-backward-codecs",
     ],
 )
@@ -28,16 +27,18 @@
         ":query_builder",
         "//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/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:gwtorm",
+        "//lib:protobuf",
         "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
diff --git a/java/com/google/gerrit/lucene/ChangeSubIndex.java b/java/com/google/gerrit/lucene/ChangeSubIndex.java
index 7d7cbef..98424b5 100644
--- a/java/com/google/gerrit/lucene/ChangeSubIndex.java
+++ b/java/com/google/gerrit/lucene/ChangeSubIndex.java
@@ -71,12 +71,12 @@
   }
 
   @Override
-  public void replace(ChangeData obj) throws IOException {
+  public void replace(ChangeData obj) {
     throw new UnsupportedOperationException("don't use ChangeSubIndex directly");
   }
 
   @Override
-  public void delete(Change.Id key) throws IOException {
+  public void delete(Change.Id key) {
     throw new UnsupportedOperationException("don't use ChangeSubIndex directly");
   }
 
diff --git a/java/com/google/gerrit/lucene/LuceneAccountIndex.java b/java/com/google/gerrit/lucene/LuceneAccountIndex.java
index 86a2111..0b787b6 100644
--- a/java/com/google/gerrit/lucene/LuceneAccountIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneAccountIndex.java
@@ -19,6 +19,7 @@
 import static com.google.gerrit.server.index.account.AccountField.ID;
 import static com.google.gerrit.server.index.account.AccountField.PREFERRED_EMAIL_EXACT;
 
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.Schema;
@@ -120,20 +121,20 @@
   }
 
   @Override
-  public void replace(AccountState as) throws IOException {
+  public void replace(AccountState as) {
     try {
       replace(idTerm(as), toDocument(as)).get();
     } catch (ExecutionException | InterruptedException e) {
-      throw new IOException(e);
+      throw new StorageException(e);
     }
   }
 
   @Override
-  public void delete(Account.Id key) throws IOException {
+  public void delete(Account.Id key) {
     try {
       delete(idTerm(key)).get();
     } catch (ExecutionException | InterruptedException e) {
-      throw new IOException(e);
+      throw new StorageException(e);
     }
   }
 
diff --git a/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index 9944c8a..8bf0b6b 100644
--- a/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -14,10 +14,8 @@
 
 package com.google.gerrit.lucene;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.gerrit.lucene.AbstractLuceneIndex.sortFieldName;
-import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.APPROVAL_CODEC;
-import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.CHANGE_CODEC;
-import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.PATCH_SET_CODEC;
 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.PROJECT;
@@ -30,6 +28,7 @@
 import com.google.common.base.Throwables;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.MultimapBuilder;
@@ -37,16 +36,22 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.gerrit.exceptions.StorageException;
 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.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.server.ReviewDb;
+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;
@@ -58,24 +63,20 @@
 import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeDataSource;
-import com.google.gwtorm.protobuf.ProtobufCodec;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.OrmRuntimeException;
-import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
+import com.google.protobuf.MessageLite;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
+import java.util.function.Consumer;
 import org.apache.lucene.document.Document;
 import org.apache.lucene.index.IndexWriter;
 import org.apache.lucene.index.IndexableField;
@@ -129,6 +130,7 @@
       ChangeField.STORED_SUBMIT_RECORD_LENIENT.getName();
   private static final String SUBMIT_RECORD_STRICT_FIELD =
       ChangeField.STORED_SUBMIT_RECORD_STRICT.getName();
+  private static final String TOTAL_COMMENT_COUNT_FIELD = ChangeField.TOTAL_COMMENT_COUNT.getName();
   private static final String UNRESOLVED_COMMENT_COUNT_FIELD =
       ChangeField.UNRESOLVED_COMMENT_COUNT.getName();
 
@@ -141,7 +143,6 @@
   }
 
   private final ListeningExecutorService executor;
-  private final Provider<ReviewDb> db;
   private final ChangeData.Factory changeDataFactory;
   private final Schema<ChangeData> schema;
   private final QueryBuilder<ChangeData> queryBuilder;
@@ -153,12 +154,10 @@
       @GerritServerConfig Config cfg,
       SitePaths sitePaths,
       @IndexExecutor(INTERACTIVE) ListeningExecutorService executor,
-      Provider<ReviewDb> db,
       ChangeData.Factory changeDataFactory,
       @Assisted Schema<ChangeData> schema)
       throws IOException {
     this.executor = executor;
-    this.db = db;
     this.changeDataFactory = changeDataFactory;
     this.schema = schema;
 
@@ -201,34 +200,34 @@
   }
 
   @Override
-  public void replace(ChangeData cd) throws IOException {
+  public void replace(ChangeData cd) {
     Term id = LuceneChangeIndex.idTerm(cd);
     // toDocument is essentially static and doesn't depend on the specific
     // sub-index, so just pick one.
     Document doc = openIndex.toDocument(cd);
     try {
-      if (cd.change().getStatus().isOpen()) {
+      if (cd.change().isNew()) {
         Futures.allAsList(closedIndex.delete(id), openIndex.replace(id, doc)).get();
       } else {
         Futures.allAsList(openIndex.delete(id), closedIndex.replace(id, doc)).get();
       }
-    } catch (OrmException | ExecutionException | InterruptedException e) {
-      throw new IOException(e);
+    } catch (ExecutionException | InterruptedException e) {
+      throw new StorageException(e);
     }
   }
 
   @Override
-  public void delete(Change.Id id) throws IOException {
+  public void delete(Change.Id id) {
     Term idTerm = LuceneChangeIndex.idTerm(id);
     try {
       Futures.allAsList(openIndex.delete(idTerm), closedIndex.delete(idTerm)).get();
     } catch (ExecutionException | InterruptedException e) {
-      throw new IOException(e);
+      throw new StorageException(e);
     }
   }
 
   @Override
-  public void deleteAll() throws IOException {
+  public void deleteAll() {
     openIndex.deleteAll();
     closedIndex.deleteAll();
   }
@@ -248,7 +247,7 @@
   }
 
   @Override
-  public void markReady(boolean ready) throws IOException {
+  public void markReady(boolean ready) {
     // Arbitrary done on open index, as ready bit is set
     // per index and not sub index
     openIndex.markReady(ready);
@@ -303,10 +302,10 @@
     }
 
     @Override
-    public ResultSet<ChangeData> read() throws OrmException {
+    public ResultSet<ChangeData> read() {
       if (Thread.interrupted()) {
         Thread.currentThread().interrupt();
-        throw new OrmException("interrupted");
+        throw new StorageException("interrupted");
       }
 
       final Set<String> fields = IndexUtils.changeFields(opts);
@@ -327,14 +326,15 @@
     }
 
     @Override
-    public ResultSet<FieldBundle> readRaw() throws OrmException {
+    public ResultSet<FieldBundle> readRaw() {
       List<Document> documents;
       try {
         documents = doRead(IndexUtils.changeFields(opts));
       } catch (IOException e) {
-        throw new OrmException(e);
+        throw new StorageException(e);
       }
-      List<FieldBundle> fieldBundles = documents.stream().map(rawDocumentMapper).collect(toList());
+      ImmutableList<FieldBundle> fieldBundles =
+          documents.stream().map(rawDocumentMapper).collect(toImmutableList());
       return new ResultSet<FieldBundle>() {
         @Override
         public Iterator<FieldBundle> iterator() {
@@ -342,7 +342,7 @@
         }
 
         @Override
-        public List<FieldBundle> toList() {
+        public ImmutableList<FieldBundle> toList() {
           return fieldBundles;
         }
 
@@ -402,21 +402,22 @@
     }
 
     @Override
-    public List<ChangeData> toList() {
+    public ImmutableList<ChangeData> toList() {
       try {
         List<Document> docs = future.get();
-        List<ChangeData> result = new ArrayList<>(docs.size());
+        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));
         }
-        return result;
+        return result.build();
       } catch (InterruptedException e) {
         close();
-        throw new OrmRuntimeException(e);
+        throw new StorageException(e);
       } catch (ExecutionException e) {
         Throwables.throwIfUnchecked(e.getCause());
-        throw new OrmRuntimeException(e.getCause());
+        throw new StorageException(e.getCause());
       }
     }
 
@@ -446,15 +447,13 @@
     IndexableField cb = Iterables.getFirst(doc.get(CHANGE_FIELD), null);
     if (cb != null) {
       BytesRef proto = cb.binaryValue();
-      cd =
-          changeDataFactory.create(
-              db.get(), CHANGE_CODEC.decode(proto.bytes, proto.offset, proto.length));
+      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(db.get(), new Project.NameKey(project.stringValue()), id);
+      cd = changeDataFactory.create(new Project.NameKey(project.stringValue()), id);
     }
 
     // Any decoding that is done here must also be done in {@link ElasticChangeIndex}.
@@ -504,11 +503,12 @@
     }
 
     decodeUnresolvedCommentCount(doc, cd);
+    decodeTotalCommentCount(doc, cd);
     return cd;
   }
 
   private void decodePatchSets(ListMultimap<String, IndexableField> doc, ChangeData cd) {
-    List<PatchSet> patchSets = decodeProtos(doc, PATCH_SET_FIELD, PATCH_SET_CODEC);
+    List<PatchSet> patchSets = decodeProtos(doc, PATCH_SET_FIELD, PatchSetProtoConverter.INSTANCE);
     if (!patchSets.isEmpty()) {
       // Will be an empty list for schemas prior to when this field was stored;
       // this cannot be valid since a change needs at least one patch set.
@@ -517,7 +517,8 @@
   }
 
   private void decodeApprovals(ListMultimap<String, IndexableField> doc, ChangeData cd) {
-    cd.setCurrentApprovals(decodeProtos(doc, APPROVAL_FIELD, APPROVAL_CODEC));
+    cd.setCurrentApprovals(
+        decodeProtos(doc, APPROVAL_FIELD, PatchSetApprovalProtoConverter.INSTANCE));
   }
 
   private void decodeChangedLines(ListMultimap<String, IndexableField> doc, ChangeData cd) {
@@ -633,25 +634,35 @@
 
   private void decodeUnresolvedCommentCount(
       ListMultimap<String, IndexableField> doc, ChangeData cd) {
-    IndexableField f = Iterables.getFirst(doc.get(UNRESOLVED_COMMENT_COUNT_FIELD), null);
+    decodeIntField(doc, UNRESOLVED_COMMENT_COUNT_FIELD, cd::setUnresolvedCommentCount);
+  }
+
+  private void decodeTotalCommentCount(ListMultimap<String, IndexableField> doc, ChangeData cd) {
+    decodeIntField(doc, TOTAL_COMMENT_COUNT_FIELD, cd::setTotalCommentCount);
+  }
+
+  private static void decodeIntField(
+      ListMultimap<String, IndexableField> doc, String fieldName, Consumer<Integer> consumer) {
+    IndexableField f = Iterables.getFirst(doc.get(fieldName), null);
     if (f != null && f.numericValue() != null) {
-      cd.setUnresolvedCommentCount(f.numericValue().intValue());
+      consumer.accept(f.numericValue().intValue());
     }
   }
 
   private static <T> List<T> decodeProtos(
-      ListMultimap<String, IndexableField> doc, String fieldName, ProtobufCodec<T> codec) {
-    Collection<IndexableField> fields = doc.get(fieldName);
-    if (fields.isEmpty()) {
-      return Collections.emptyList();
-    }
+      ListMultimap<String, IndexableField> doc, String fieldName, ProtoConverter<?, T> converter) {
+    return doc.get(fieldName).stream()
+        .map(IndexableField::binaryValue)
+        .map(bytesRef -> parseProtoFrom(bytesRef, converter))
+        .collect(toImmutableList());
+  }
 
-    List<T> result = new ArrayList<>(fields.size());
-    for (IndexableField f : fields) {
-      BytesRef r = f.binaryValue();
-      result.add(codec.decode(r.bytes, r.offset, r.length));
-    }
-    return result;
+  private static <P extends MessageLite, T> T parseProtoFrom(
+      BytesRef bytesRef, ProtoConverter<P, T> converter) {
+    P message =
+        Protos.parseUnchecked(
+            converter.getParser(), bytesRef.bytes, bytesRef.offset, bytesRef.length);
+    return converter.fromProto(message);
   }
 
   private static List<byte[]> copyAsBytes(Collection<IndexableField> fields) {
diff --git a/java/com/google/gerrit/lucene/LuceneGroupIndex.java b/java/com/google/gerrit/lucene/LuceneGroupIndex.java
index 95e2ab9..0fdef77 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.exceptions.StorageException;
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.Schema;
@@ -110,20 +111,20 @@
   }
 
   @Override
-  public void replace(InternalGroup group) throws IOException {
+  public void replace(InternalGroup group) {
     try {
       replace(idTerm(group), toDocument(group)).get();
     } catch (ExecutionException | InterruptedException e) {
-      throw new IOException(e);
+      throw new StorageException(e);
     }
   }
 
   @Override
-  public void delete(AccountGroup.UUID key) throws IOException {
+  public void delete(AccountGroup.UUID key) {
     try {
       delete(idTerm(key)).get();
     } catch (ExecutionException | InterruptedException e) {
-      throw new IOException(e);
+      throw new StorageException(e);
     }
   }
 
diff --git a/java/com/google/gerrit/lucene/LuceneProjectIndex.java b/java/com/google/gerrit/lucene/LuceneProjectIndex.java
index 02d8655..950e206 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.exceptions.StorageException;
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.Schema;
@@ -110,20 +111,20 @@
   }
 
   @Override
-  public void replace(ProjectData projectState) throws IOException {
+  public void replace(ProjectData projectState) {
     try {
       replace(idTerm(projectState), toDocument(projectState)).get();
     } catch (ExecutionException | InterruptedException e) {
-      throw new IOException(e);
+      throw new StorageException(e);
     }
   }
 
   @Override
-  public void delete(Project.NameKey nameKey) throws IOException {
+  public void delete(Project.NameKey nameKey) {
     try {
       delete(idTerm(nameKey)).get();
     } catch (ExecutionException | InterruptedException e) {
-      throw new IOException(e);
+      throw new StorageException(e);
     }
   }
 
diff --git a/java/com/google/gerrit/lucene/LuceneVersionManager.java b/java/com/google/gerrit/lucene/LuceneVersionManager.java
index 63abea8..f3ba73d 100644
--- a/java/com/google/gerrit/lucene/LuceneVersionManager.java
+++ b/java/com/google/gerrit/lucene/LuceneVersionManager.java
@@ -16,7 +16,6 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
-import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.index.Index;
 import com.google.gerrit.index.IndexDefinition;
 import com.google.gerrit.index.Schema;
@@ -25,6 +24,7 @@
 import com.google.gerrit.server.index.GerritIndexStatus;
 import com.google.gerrit.server.index.OnlineUpgradeListener;
 import com.google.gerrit.server.index.VersionManager;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -47,7 +47,7 @@
   LuceneVersionManager(
       @GerritServerConfig Config cfg,
       SitePaths sitePaths,
-      DynamicSet<OnlineUpgradeListener> listeners,
+      PluginSetContext<OnlineUpgradeListener> listeners,
       Collection<IndexDefinition<?, ?, ?>> defs) {
     super(sitePaths, listeners, defs, VersionManager.getOnlineUpgrade(cfg));
   }
@@ -81,7 +81,7 @@
           continue;
         }
         if (!versions.containsKey(v)) {
-          versions.put(v, new Version<V>(null, v, true, cfg.getReady(def.getName(), v)));
+          versions.put(v, new Version<>(null, v, true, cfg.getReady(def.getName(), v)));
         }
       }
     } catch (IOException e) {
diff --git a/java/com/google/gerrit/mail/MailHeaderParser.java b/java/com/google/gerrit/mail/MailHeaderParser.java
index 8eb4d97..a4a6a03 100644
--- a/java/com/google/gerrit/mail/MailHeaderParser.java
+++ b/java/com/google/gerrit/mail/MailHeaderParser.java
@@ -103,6 +103,6 @@
   }
 
   private static String extractFooter(String key, String line) {
-    return line.substring(line.indexOf(key) + key.length(), line.length()).trim();
+    return line.substring(line.indexOf(key) + key.length()).trim();
   }
 }
diff --git a/java/com/google/gerrit/metrics/BUILD b/java/com/google/gerrit/metrics/BUILD
index 1e32a03..786ef68 100644
--- a/java/com/google/gerrit/metrics/BUILD
+++ b/java/com/google/gerrit/metrics/BUILD
@@ -8,7 +8,6 @@
         "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/lifecycle",
-        "//java/org/eclipse/jgit:server",
         "//lib:guava",
         "//lib/flogger:api",
         "//lib/guice",
diff --git a/java/com/google/gerrit/metrics/DisabledMetricMaker.java b/java/com/google/gerrit/metrics/DisabledMetricMaker.java
index bc88a60..adf6a66 100644
--- a/java/com/google/gerrit/metrics/DisabledMetricMaker.java
+++ b/java/com/google/gerrit/metrics/DisabledMetricMaker.java
@@ -187,9 +187,6 @@
 
   @Override
   public RegistrationHandle newTrigger(Set<CallbackMetric<?>> metrics, Runnable trigger) {
-    return new RegistrationHandle() {
-      @Override
-      public void remove() {}
-    };
+    return () -> {};
   }
 }
diff --git a/java/com/google/gerrit/metrics/MetricMaker.java b/java/com/google/gerrit/metrics/MetricMaker.java
index 401a6d6..42ec8a0 100644
--- a/java/com/google/gerrit/metrics/MetricMaker.java
+++ b/java/com/google/gerrit/metrics/MetricMaker.java
@@ -136,7 +136,7 @@
    * @return registration handle
    */
   public RegistrationHandle newTrigger(CallbackMetric<?> metric1, Runnable trigger) {
-    return newTrigger(ImmutableSet.<CallbackMetric<?>>of(metric1), trigger);
+    return newTrigger(ImmutableSet.of(metric1), trigger);
   }
 
   public RegistrationHandle newTrigger(
diff --git a/java/com/google/gerrit/metrics/dropwizard/BucketedCallback.java b/java/com/google/gerrit/metrics/dropwizard/BucketedCallback.java
index ee87397..e33c242 100644
--- a/java/com/google/gerrit/metrics/dropwizard/BucketedCallback.java
+++ b/java/com/google/gerrit/metrics/dropwizard/BucketedCallback.java
@@ -120,7 +120,7 @@
 
   @Override
   public Map<Object, Metric> getCells() {
-    return Maps.transformValues(cells, in -> (Metric) in);
+    return Maps.transformValues(cells, in -> in);
   }
 
   final class ValueGauge implements Gauge<V> {
diff --git a/java/com/google/gerrit/metrics/dropwizard/CallbackMetricImpl0.java b/java/com/google/gerrit/metrics/dropwizard/CallbackMetricImpl0.java
index 5e25651..6e7e7d9 100644
--- a/java/com/google/gerrit/metrics/dropwizard/CallbackMetricImpl0.java
+++ b/java/com/google/gerrit/metrics/dropwizard/CallbackMetricImpl0.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.metrics.dropwizard;
 
+import com.codahale.metrics.Gauge;
 import com.codahale.metrics.MetricRegistry;
 import com.google.gerrit.metrics.CallbackMetric0;
 
@@ -71,12 +72,10 @@
   public void register(Runnable trigger) {
     registry.register(
         name,
-        new com.codahale.metrics.Gauge<V>() {
-          @Override
-          public V getValue() {
-            trigger.run();
-            return value;
-          }
-        });
+        (Gauge<V>)
+            () -> {
+              trigger.run();
+              return value;
+            });
   }
 }
diff --git a/java/com/google/gerrit/metrics/dropwizard/DropWizardMetricMaker.java b/java/com/google/gerrit/metrics/dropwizard/DropWizardMetricMaker.java
index b57ea92..fcba0ee 100644
--- a/java/com/google/gerrit/metrics/dropwizard/DropWizardMetricMaker.java
+++ b/java/com/google/gerrit/metrics/dropwizard/DropWizardMetricMaker.java
@@ -305,14 +305,7 @@
     }
     trigger.run();
 
-    return new RegistrationHandle() {
-      @Override
-      public void remove() {
-        for (CallbackMetricGlue m : all) {
-          m.remove();
-        }
-      }
-    };
+    return () -> all.forEach(CallbackMetricGlue::remove);
   }
 
   synchronized void remove(String name) {
diff --git a/java/com/google/gerrit/metrics/dropwizard/MetricJson.java b/java/com/google/gerrit/metrics/dropwizard/MetricJson.java
index 2080623..20f4fa3 100644
--- a/java/com/google/gerrit/metrics/dropwizard/MetricJson.java
+++ b/java/com/google/gerrit/metrics/dropwizard/MetricJson.java
@@ -137,7 +137,7 @@
       p99_9 = s.get999thPercentile();
 
       min = (double) s.getMin();
-      avg = (double) s.getMean();
+      avg = s.getMean();
       max = (double) s.getMax();
       sum = s.getMean() * m.getCount();
       std_dev = s.getStdDev();
diff --git a/java/com/google/gerrit/metrics/proc/ProcMetricModule.java b/java/com/google/gerrit/metrics/proc/ProcMetricModule.java
index 5af2672..939560f 100644
--- a/java/com/google/gerrit/metrics/proc/ProcMetricModule.java
+++ b/java/com/google/gerrit/metrics/proc/ProcMetricModule.java
@@ -15,10 +15,8 @@
 package com.google.gerrit.metrics.proc;
 
 import com.google.common.base.Strings;
-import com.google.common.base.Supplier;
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.Version;
-import com.google.gerrit.metrics.CallbackMetric;
 import com.google.gerrit.metrics.CallbackMetric0;
 import com.google.gerrit.metrics.CallbackMetric1;
 import com.google.gerrit.metrics.Description;
@@ -75,12 +73,7 @@
           "proc/cpu/usage",
           Double.class,
           new Description("CPU time used by the process").setCumulative().setUnit(Units.SECONDS),
-          new Supplier<Double>() {
-            @Override
-            public Double get() {
-              return provider.getProcessCpuTime() / 1e9;
-            }
-          });
+          () -> provider.getProcessCpuTime() / 1e9);
     }
 
     if (provider.getOpenFileDescriptorCount() != -1) {
@@ -135,7 +128,7 @@
 
     MemoryMXBean memory = ManagementFactory.getMemoryMXBean();
     metrics.newTrigger(
-        ImmutableSet.<CallbackMetric<?>>of(
+        ImmutableSet.of(
             heapCommitted, heapUsed, nonHeapCommitted, nonHeapUsed, objectPendingFinalizationCount),
         () -> {
           try {
diff --git a/java/com/google/gerrit/pgm/BUILD b/java/com/google/gerrit/pgm/BUILD
index e022e86..2dd4ba5 100644
--- a/java/com/google/gerrit/pgm/BUILD
+++ b/java/com/google/gerrit/pgm/BUILD
@@ -20,6 +20,7 @@
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/elasticsearch",
+        "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/gpg",
         "//java/com/google/gerrit/httpd",
@@ -51,7 +52,6 @@
         "//java/com/google/gerrit/util/http",
         "//lib:args4j",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib:protobuf",
         "//lib:servlet-api-3_1-without-neverlink",
         "//lib/auto:auto-value",
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index 65707fa..6553477 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -15,9 +15,7 @@
 package com.google.gerrit.pgm;
 
 import static com.google.gerrit.common.Version.getVersion;
-import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.Objects.requireNonNull;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Joiner;
@@ -55,6 +53,7 @@
 import com.google.gerrit.pgm.util.RuntimeShutdown;
 import com.google.gerrit.pgm.util.SiteProgram;
 import com.google.gerrit.server.LibModuleLoader;
+import com.google.gerrit.server.LibModuleType;
 import com.google.gerrit.server.ModuleOverloader;
 import com.google.gerrit.server.StartupChecks;
 import com.google.gerrit.server.account.AccountDeactivator;
@@ -91,18 +90,14 @@
 import com.google.gerrit.server.mail.receive.MailReceiver;
 import com.google.gerrit.server.mail.send.SmtpEmailSender;
 import com.google.gerrit.server.mime.MimeUtil2Module;
-import com.google.gerrit.server.notedb.rebuild.NoteDbMigrator;
-import com.google.gerrit.server.notedb.rebuild.OnlineNoteDbMigrator;
 import com.google.gerrit.server.patch.DiffExecutorModule;
 import com.google.gerrit.server.permissions.DefaultPermissionBackendModule;
 import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
 import com.google.gerrit.server.plugins.PluginModule;
 import com.google.gerrit.server.project.DefaultProjectNameLockManager;
 import com.google.gerrit.server.restapi.RestApiModule;
-import com.google.gerrit.server.schema.DataSourceProvider;
-import com.google.gerrit.server.schema.InMemoryAccountPatchReviewStore;
 import com.google.gerrit.server.schema.JdbcAccountPatchReviewStore;
-import com.google.gerrit.server.schema.SchemaVersionCheck;
+import com.google.gerrit.server.schema.NoteDbSchemaVersionCheck;
 import com.google.gerrit.server.securestore.DefaultSecureStore;
 import com.google.gerrit.server.securestore.SecureStore;
 import com.google.gerrit.server.securestore.SecureStoreClassName;
@@ -125,7 +120,6 @@
 import com.google.inject.Provider;
 import com.google.inject.Stage;
 import java.io.IOException;
-import java.lang.Thread.UncaughtExceptionHandler;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
@@ -134,7 +128,6 @@
 import javax.servlet.http.HttpServletRequest;
 import org.eclipse.jgit.lib.Config;
 import org.kohsuke.args4j.Option;
-import org.kohsuke.args4j.spi.ExplicitBooleanOptionHandler;
 
 /** Run SSH daemon portions of Gerrit. */
 public class Daemon extends SiteProgram {
@@ -183,15 +176,6 @@
   @Option(name = "--stop-only", usage = "Stop the daemon", hidden = true)
   private boolean stopOnly;
 
-  @Option(
-      name = "--migrate-to-note-db",
-      usage = "Automatically migrate changes to NoteDb",
-      handler = ExplicitBooleanOptionHandler.class)
-  private boolean migrateToNoteDb;
-
-  @Option(name = "--trial", usage = "(With --migrate-to-note-db) " + MigrateToNoteDb.TRIAL_USAGE)
-  private boolean trial;
-
   private final LifecycleManager manager = new LifecycleManager();
   private Injector dbInjector;
   private Injector cfgInjector;
@@ -251,12 +235,7 @@
     }
     mustHaveValidSite();
     Thread.setDefaultUncaughtExceptionHandler(
-        new UncaughtExceptionHandler() {
-          @Override
-          public void uncaughtException(Thread t, Throwable e) {
-            logger.atSevere().withCause(e).log("Thread %s threw exception", t.getName());
-          }
-        });
+        (t, e) -> logger.atSevere().withCause(e).log("Thread %s threw exception", t.getName()));
 
     if (runId != null) {
       runFile = getSitePath().resolve("logs").resolve("gerrit.run");
@@ -295,8 +274,6 @@
       if (inspector) {
         JythonShell shell = new JythonShell();
         shell.set("m", manager);
-        shell.set("ds", dbInjector.getInstance(DataSourceProvider.class));
-        shell.set("schk", dbInjector.getInstance(SchemaVersionCheck.class));
         shell.set("d", this);
         shell.run();
       } else {
@@ -345,7 +322,7 @@
   @VisibleForTesting
   public void start() throws IOException {
     if (dbInjector == null) {
-      dbInjector = createDbInjector(true /* enableMetrics */, MULTI_USER);
+      dbInjector = createDbInjector(true /* enableMetrics */);
     }
     cfgInjector = createCfgInjector();
     config = cfgInjector.getInstance(Key.get(Config.class, GerritServerConfig.class));
@@ -411,7 +388,7 @@
 
   private Injector createSysInjector() {
     final List<Module> modules = new ArrayList<>();
-    modules.add(SchemaVersionCheck.module());
+    modules.add(NoteDbSchemaVersionCheck.module());
     modules.add(new DropWizardMetricMaker.RestModule());
     modules.add(new LogFileCompressor.Module());
 
@@ -422,10 +399,7 @@
     modules.add(new WorkQueue.Module());
     modules.add(new StreamEventsApiListener.Module());
     modules.add(new EventBroker.Module());
-    modules.add(
-        inMemoryTest
-            ? new InMemoryAccountPatchReviewStore.Module()
-            : new JdbcAccountPatchReviewStore.Module(config));
+    modules.add(new JdbcAccountPatchReviewStore.Module(config));
     modules.add(new SysExecutorModule());
     modules.add(new DiffExecutorModule());
     modules.add(new MimeUtil2Module());
@@ -451,9 +425,7 @@
     }
     modules.add(new SignedTokenEmailTokenVerifier.Module());
     modules.add(new PluginModule());
-    if (VersionManager.getOnlineUpgrade(config)
-        // Schema upgrade is handled by OnlineNoteDbMigrator in this case.
-        && !migrateToNoteDb()) {
+    if (VersionManager.getOnlineUpgrade(config)) {
       modules.add(new OnlineUpgrader.Module());
     }
     modules.add(new RestApiModule());
@@ -487,8 +459,7 @@
         new AbstractModule() {
           @Override
           protected void configure() {
-            bind(GerritOptions.class)
-                .toInstance(new GerritOptions(config, headless, slave, polyGerritDev));
+            bind(GerritOptions.class).toInstance(new GerritOptions(headless, slave, polyGerritDev));
             if (inMemoryTest) {
               bind(String.class)
                   .annotatedWith(SecureStoreClassName.class)
@@ -504,18 +475,12 @@
       modules.add(new AccountDeactivator.Module());
       modules.add(new ChangeCleanupRunner.Module());
     }
-    if (migrateToNoteDb()) {
-      modules.add(new OnlineNoteDbMigrator.Module(trial));
-    }
     modules.addAll(testSysModules);
     modules.add(new LocalMergeSuperSetComputation.Module());
     modules.add(new DefaultProjectNameLockManager.Module());
     return cfgInjector.createChildInjector(
-        ModuleOverloader.override(modules, LibModuleLoader.loadModules(cfgInjector)));
-  }
-
-  private boolean migrateToNoteDb() {
-    return migrateToNoteDb || NoteDbMigrator.getAutoMigrate(requireNonNull(config));
+        ModuleOverloader.override(
+            modules, LibModuleLoader.loadModules(cfgInjector, LibModuleType.SYS_MODULE)));
   }
 
   private Module createIndexModule() {
diff --git a/java/com/google/gerrit/pgm/Gsql.java b/java/com/google/gerrit/pgm/Gsql.java
deleted file mode 100644
index 004486b..0000000
--- a/java/com/google/gerrit/pgm/Gsql.java
+++ /dev/null
@@ -1,77 +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.pgm;
-
-import static com.google.gerrit.server.schema.DataSourceProvider.Context.SINGLE_USER;
-
-import com.google.gerrit.extensions.config.FactoryModule;
-import com.google.gerrit.lifecycle.LifecycleManager;
-import com.google.gerrit.pgm.util.RuntimeShutdown;
-import com.google.gerrit.pgm.util.SiteProgram;
-import com.google.gerrit.sshd.commands.QueryShell;
-import com.google.gerrit.sshd.commands.QueryShell.Factory;
-import com.google.inject.Injector;
-import java.io.IOException;
-import org.kohsuke.args4j.Option;
-
-/** Run Gerrit's SQL query tool */
-public class Gsql extends SiteProgram {
-  private final LifecycleManager manager = new LifecycleManager();
-  private Injector dbInjector;
-
-  @Option(name = "--format", usage = "Set output format")
-  private QueryShell.OutputFormat format = QueryShell.OutputFormat.PRETTY;
-
-  @Option(name = "-c", metaVar = "SQL QUERY", usage = "Query to execute")
-  private String query;
-
-  @Override
-  public int run() throws Exception {
-    mustHaveValidSite();
-
-    dbInjector = createDbInjector(SINGLE_USER);
-    manager.add(dbInjector);
-    manager.start();
-    RuntimeShutdown.add(
-        () -> {
-          try {
-            System.in.close();
-          } catch (IOException e) {
-            // Ignored
-          }
-          manager.stop();
-        });
-    final QueryShell shell = shellFactory().create(System.in, System.out);
-    shell.setOutputFormat(format);
-    if (query != null) {
-      shell.execute(query);
-    } else {
-      shell.run();
-    }
-    return 0;
-  }
-
-  private Factory shellFactory() {
-    return dbInjector
-        .createChildInjector(
-            new FactoryModule() {
-              @Override
-              protected void configure() {
-                factory(QueryShell.Factory.class);
-              }
-            })
-        .getInstance(QueryShell.Factory.class);
-  }
-}
diff --git a/java/com/google/gerrit/pgm/Init.java b/java/com/google/gerrit/pgm/Init.java
index 4ea31da..0537fe9 100644
--- a/java/com/google/gerrit/pgm/Init.java
+++ b/java/com/google/gerrit/pgm/Init.java
@@ -95,7 +95,7 @@
   }
 
   public Init(Path sitePath) {
-    super(sitePath, true, true, new WarDistribution(), null);
+    super(sitePath, true, new WarDistribution(), null);
     batchMode = true;
     noAutoStart = true;
   }
@@ -144,7 +144,9 @@
         });
     modules.add(new GerritServerConfigModule());
     Guice.createInjector(modules).injectMembers(this);
-    reindexProjects();
+    if (!run.flags.cfg.getBoolean("container", "slave", false)) {
+      reindexProjects();
+    }
     start(run);
   }
 
@@ -190,7 +192,7 @@
 
   @Override
   protected List<String> getSkippedDownloads() {
-    return skippedDownloads != null ? skippedDownloads : Collections.<String>emptyList();
+    return skippedDownloads != null ? skippedDownloads : Collections.emptyList();
   }
 
   @Override
diff --git a/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java b/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
index 5bfc00f..e6e091c 100644
--- a/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
+++ b/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.pgm;
 
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GERRIT;
-import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
 
+import com.google.gerrit.exceptions.DuplicateKeyException;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.pgm.util.SiteProgram;
@@ -29,8 +29,7 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.index.account.AccountSchemaDefinitions;
-import com.google.gerrit.server.schema.SchemaVersionCheck;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
+import com.google.gerrit.server.schema.NoteDbSchemaVersionCheck;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.Provider;
@@ -54,8 +53,8 @@
 
   @Override
   public int run() throws Exception {
-    Injector dbInjector = createDbInjector(MULTI_USER);
-    manager.add(dbInjector, dbInjector.createChildInjector(SchemaVersionCheck.module()));
+    Injector dbInjector = createDbInjector();
+    manager.add(dbInjector, dbInjector.createChildInjector(NoteDbSchemaVersionCheck.module()));
     manager.start();
     dbInjector
         .createChildInjector(
@@ -97,7 +96,7 @@
   }
 
   private void convertLocalUserToLowerCase(ExternalIdNotes extIdNotes, ExternalId extId)
-      throws OrmDuplicateKeyException, IOException {
+      throws DuplicateKeyException, IOException {
     if (extId.isScheme(SCHEME_GERRIT)) {
       String localUser = extId.key().id();
       String localUserLowerCase = localUser.toLowerCase(Locale.US);
diff --git a/java/com/google/gerrit/pgm/MigrateAccountPatchReviewDb.java b/java/com/google/gerrit/pgm/MigrateAccountPatchReviewDb.java
index 4ace62b..e12bde2 100644
--- a/java/com/google/gerrit/pgm/MigrateAccountPatchReviewDb.java
+++ b/java/com/google/gerrit/pgm/MigrateAccountPatchReviewDb.java
@@ -21,7 +21,6 @@
 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.schema.DataSourceProvider;
 import com.google.gerrit.server.schema.JdbcAccountPatchReviewStore;
 import com.google.inject.Injector;
 import com.google.inject.Key;
@@ -49,7 +48,7 @@
 
   @Override
   public int run() throws Exception {
-    Injector dbInjector = createDbInjector(DataSourceProvider.Context.SINGLE_USER);
+    Injector dbInjector = createDbInjector();
     SitePaths sitePaths = new SitePaths(getSitePath());
     ThreadSettingsConfig threadSettingsConfig = dbInjector.getInstance(ThreadSettingsConfig.class);
     Config fakeCfg = new Config();
diff --git a/java/com/google/gerrit/pgm/MigrateToNoteDb.java b/java/com/google/gerrit/pgm/MigrateToNoteDb.java
deleted file mode 100644
index 09422b2..0000000
--- a/java/com/google/gerrit/pgm/MigrateToNoteDb.java
+++ /dev/null
@@ -1,228 +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.pgm;
-
-import static com.google.common.base.MoreObjects.firstNonNull;
-import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
-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.collect.ImmutableList;
-import com.google.gerrit.extensions.config.FactoryModule;
-import com.google.gerrit.lifecycle.LifecycleManager;
-import com.google.gerrit.pgm.util.BatchProgramModule;
-import com.google.gerrit.pgm.util.RuntimeShutdown;
-import com.google.gerrit.pgm.util.SiteProgram;
-import com.google.gerrit.pgm.util.ThreadLimiter;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.change.ChangeResource;
-import com.google.gerrit.server.git.GarbageCollection;
-import com.google.gerrit.server.index.DummyIndexModule;
-import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
-import com.google.gerrit.server.notedb.rebuild.GcAllUsers;
-import com.google.gerrit.server.notedb.rebuild.NoteDbMigrator;
-import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
-import com.google.gerrit.server.schema.DataSourceType;
-import com.google.inject.Inject;
-import com.google.inject.Injector;
-import com.google.inject.Provider;
-import java.io.OutputStreamWriter;
-import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.List;
-import org.kohsuke.args4j.Option;
-import org.kohsuke.args4j.spi.ExplicitBooleanOptionHandler;
-
-public class MigrateToNoteDb extends SiteProgram {
-  static final String TRIAL_USAGE =
-      "Trial mode: migrate changes and turn on reading from NoteDb, but leave ReviewDb as the"
-          + " source of truth";
-
-  private static final int ISSUE_8022_THREAD_LIMIT = 4;
-
-  @Option(name = "--threads", usage = "Number of threads to use for rebuilding NoteDb")
-  private Integer threads;
-
-  @Option(
-      name = "--project",
-      usage =
-          "Only rebuild these projects, do no other migration; incompatible with --change"
-              + " and --skip-project; recommended for debugging only")
-  private List<String> projects = new ArrayList<>();
-
-  @Option(
-      name = "--skip-project",
-      usage = "Rebuild all projects except these; incompatible with the --project and --change")
-  private List<String> skipProjects = new ArrayList<>();
-
-  @Option(
-      name = "--change",
-      usage =
-          "Only rebuild these changes, do no other migration; incompatible with --project and"
-              + " --skip-project; recommended for debugging only")
-  private List<Integer> changes = new ArrayList<>();
-
-  @Option(
-      name = "--force",
-      usage =
-          "Force rebuilding changes where ReviewDb is still the source of truth, even if they"
-              + " were previously migrated")
-  private boolean force;
-
-  @Option(name = "--trial", usage = TRIAL_USAGE)
-  private boolean trial;
-
-  @Option(
-      name = "--sequence-gap",
-      usage =
-          "gap in change sequence numbers between last ReviewDb number and first NoteDb number;"
-              + " negative indicates using the value of noteDb.changes.initialSequenceGap (default"
-              + " 1000)")
-  private int sequenceGap;
-
-  @Option(
-      name = "--reindex",
-      usage =
-          "Reindex all changes after migration; defaults to false in trial mode, true otherwise",
-      handler = ExplicitBooleanOptionHandler.class)
-  private Boolean reindex;
-
-  private Injector dbInjector;
-  private Injector sysInjector;
-  private LifecycleManager dbManager;
-  private LifecycleManager sysManager;
-
-  @Inject private GcAllUsers gcAllUsers;
-  @Inject private Provider<NoteDbMigrator.Builder> migratorBuilderProvider;
-
-  @Override
-  public int run() throws Exception {
-    RuntimeShutdown.add(this::stop);
-    try {
-      mustHaveValidSite();
-      dbInjector = createDbInjector(MULTI_USER);
-
-      dbManager = new LifecycleManager();
-      dbManager.add(dbInjector);
-      dbManager.start();
-
-      threads = limitThreads();
-
-      sysInjector = createSysInjector();
-      sysInjector.injectMembers(this);
-      sysManager = new LifecycleManager();
-      sysManager.add(sysInjector);
-      sysInjector
-          .getInstance(PluginGuiceEnvironment.class)
-          .setDbCfgInjector(dbInjector, dbInjector);
-      sysManager.start();
-
-      try (NoteDbMigrator migrator =
-          migratorBuilderProvider
-              .get()
-              .setThreads(threads)
-              .setProgressOut(System.err)
-              .setProjects(projects.stream().map(Project.NameKey::new).collect(toList()))
-              .setSkipProjects(skipProjects.stream().map(Project.NameKey::new).collect(toList()))
-              .setChanges(changes.stream().map(Change.Id::new).collect(toList()))
-              .setTrialMode(trial)
-              .setForceRebuild(force)
-              .setSequenceGap(sequenceGap)
-              .build()) {
-        if (!projects.isEmpty() || !changes.isEmpty() || !skipProjects.isEmpty()) {
-          migrator.rebuild();
-        } else {
-          migrator.migrate();
-        }
-      }
-      try (PrintWriter w = new PrintWriter(new OutputStreamWriter(System.out, UTF_8), true)) {
-        gcAllUsers.run(w);
-      }
-    } finally {
-      stop();
-    }
-
-    boolean reindex = firstNonNull(this.reindex, !trial);
-    if (!reindex) {
-      return 0;
-    }
-    // Reindex all indices, to save the user from having to run yet another program by hand while
-    // their server is offline.
-    List<String> reindexArgs =
-        ImmutableList.of(
-            "--site-path",
-            getSitePath().toString(),
-            "--threads",
-            Integer.toString(threads),
-            "--index",
-            ChangeSchemaDefinitions.NAME);
-    System.out.println("Migration complete, reindexing changes with:");
-    System.out.println("  reindex " + reindexArgs.stream().collect(joining(" ")));
-    Reindex reindexPgm = new Reindex();
-    return reindexPgm.main(reindexArgs.stream().toArray(String[]::new));
-  }
-
-  private int limitThreads() {
-    if (threads != null) {
-      return threads;
-    }
-    int actualThreads;
-    int procs = Runtime.getRuntime().availableProcessors();
-    DataSourceType dsType = dbInjector.getInstance(DataSourceType.class);
-    if (dsType.getDriver().equals("org.h2.Driver") && procs > ISSUE_8022_THREAD_LIMIT) {
-      System.out.println(
-          "Not using more than "
-              + ISSUE_8022_THREAD_LIMIT
-              + " threads due to http://crbug.com/gerrit/8022");
-      System.out.println("Can be increased by passing --threads, but may cause errors");
-      actualThreads = ISSUE_8022_THREAD_LIMIT;
-    } else {
-      actualThreads = procs;
-    }
-    actualThreads = ThreadLimiter.limitThreads(dbInjector, actualThreads);
-    return actualThreads;
-  }
-
-  private Injector createSysInjector() {
-    return dbInjector.createChildInjector(
-        new FactoryModule() {
-          @Override
-          public void configure() {
-            install(dbInjector.getInstance(BatchProgramModule.class));
-            install(new DummyIndexModule());
-            factory(ChangeResource.Factory.class);
-            factory(GarbageCollection.Factory.class);
-          }
-        });
-  }
-
-  private void stop() {
-    try {
-      LifecycleManager m = sysManager;
-      sysManager = null;
-      if (m != null) {
-        m.stop();
-      }
-    } finally {
-      LifecycleManager m = dbManager;
-      dbManager = null;
-      if (m != null) {
-        m.stop();
-      }
-    }
-  }
-}
diff --git a/java/com/google/gerrit/pgm/Passwd.java b/java/com/google/gerrit/pgm/Passwd.java
index f63d2f4..10ed07d 100644
--- a/java/com/google/gerrit/pgm/Passwd.java
+++ b/java/com/google/gerrit/pgm/Passwd.java
@@ -78,7 +78,7 @@
             bind(Boolean.class).annotatedWith(InstallAllPlugins.class).toInstance(Boolean.FALSE);
             bind(new TypeLiteral<List<String>>() {})
                 .annotatedWith(InstallPlugins.class)
-                .toInstance(new ArrayList<String>());
+                .toInstance(new ArrayList<>());
             bind(String.class)
                 .annotatedWith(SecureStoreClassName.class)
                 .toProvider(Providers.of(getConfiguredSecureStoreClass()));
diff --git a/java/com/google/gerrit/pgm/PrologShell.java b/java/com/google/gerrit/pgm/PrologShell.java
index 5decd68..2780f84 100644
--- a/java/com/google/gerrit/pgm/PrologShell.java
+++ b/java/com/google/gerrit/pgm/PrologShell.java
@@ -30,9 +30,14 @@
   @Option(name = "-s", metaVar = "FILE.pl", usage = "file to load")
   private List<String> fileName = new ArrayList<>();
 
+  @Option(name = "-q", usage = "quiet mode without banner")
+  private boolean quiet;
+
   @Override
   public int run() {
-    banner();
+    if (!quiet) {
+      banner();
+    }
 
     BufferingPrologControl pcl = new BufferingPrologControl();
     pcl.setPrologClassLoader(new PrologClassLoader(getClass().getClassLoader()));
@@ -55,7 +60,9 @@
 
     try {
       pcl.execute(Prolog.BUILTIN, "cafeteria");
-      write("% halt\n");
+      if (!quiet) {
+        write("% halt\n");
+      }
       return 0;
     } catch (HaltException halt) {
       write("% halt(" + halt.getStatus() + ")\n");
diff --git a/java/com/google/gerrit/pgm/ProtobufImport.java b/java/com/google/gerrit/pgm/ProtobufImport.java
deleted file mode 100644
index 0179b1d..0000000
--- a/java/com/google/gerrit/pgm/ProtobufImport.java
+++ /dev/null
@@ -1,146 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.pgm;
-
-import static com.google.common.base.Preconditions.checkState;
-import static com.google.gerrit.server.schema.DataSourceProvider.Context.SINGLE_USER;
-import static java.util.Objects.requireNonNull;
-
-import com.google.auto.value.AutoValue;
-import com.google.common.collect.Iterables;
-import com.google.gerrit.lifecycle.LifecycleManager;
-import com.google.gerrit.pgm.util.RuntimeShutdown;
-import com.google.gerrit.pgm.util.SiteProgram;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.protobuf.CodecFactory;
-import com.google.gwtorm.protobuf.ProtobufCodec;
-import com.google.gwtorm.schema.RelationModel;
-import com.google.gwtorm.schema.java.JavaSchemaModel;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-import com.google.inject.Injector;
-import com.google.protobuf.ByteString;
-import com.google.protobuf.Parser;
-import com.google.protobuf.UnknownFieldSet;
-import java.io.BufferedInputStream;
-import java.io.File;
-import java.io.InputStream;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.nio.file.Files;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import org.eclipse.jgit.lib.ProgressMonitor;
-import org.eclipse.jgit.lib.TextProgressMonitor;
-import org.kohsuke.args4j.Option;
-
-/**
- * Import data from a protocol buffer dump into the database.
- *
- * <p>Takes as input a file containing protocol buffers concatenated together with varint length
- * encoding, as in {@link Parser#parseDelimitedFrom(InputStream)}. Each message contains a single
- * field with a tag corresponding to the relation ID in the {@link
- * com.google.gwtorm.server.Relation} annotation.
- *
- * <p><strong>Warning</strong>: This method blindly upserts data into the database. It should only
- * be used to restore a protobuf-formatted backup into a new, empty site.
- */
-public class ProtobufImport extends SiteProgram {
-  @Option(
-      name = "--file",
-      aliases = {"-f"},
-      required = true,
-      metaVar = "FILE",
-      usage = "File to import from")
-  private File file;
-
-  private final LifecycleManager manager = new LifecycleManager();
-  private final Map<Integer, Relation> relations = new HashMap<>();
-
-  @Inject private SchemaFactory<ReviewDb> schemaFactory;
-
-  @Override
-  public int run() throws Exception {
-    mustHaveValidSite();
-
-    Injector dbInjector = createDbInjector(SINGLE_USER);
-    manager.add(dbInjector);
-    manager.start();
-    RuntimeShutdown.add(manager::stop);
-    dbInjector.injectMembers(this);
-
-    ProgressMonitor progress = new TextProgressMonitor();
-    progress.beginTask("Importing entities", ProgressMonitor.UNKNOWN);
-    try (ReviewDb db = schemaFactory.open()) {
-      for (RelationModel model : new JavaSchemaModel(ReviewDb.class).getRelations()) {
-        relations.put(model.getRelationID(), Relation.create(model, db));
-      }
-
-      Parser<UnknownFieldSet> parser = UnknownFieldSet.getDefaultInstance().getParserForType();
-      try (InputStream in = new BufferedInputStream(Files.newInputStream(file.toPath()))) {
-        UnknownFieldSet msg;
-        while ((msg = parser.parseDelimitedFrom(in)) != null) {
-          Map.Entry<Integer, UnknownFieldSet.Field> e =
-              Iterables.getOnlyElement(msg.asMap().entrySet());
-          Relation rel =
-              requireNonNull(
-                  relations.get(e.getKey()),
-                  String.format("unknown relation ID %s in message: %s", e.getKey(), msg));
-          List<ByteString> values = e.getValue().getLengthDelimitedList();
-          checkState(values.size() == 1, "expected one string field in message: %s", msg);
-          upsert(rel, values.get(0));
-          progress.update(1);
-        }
-      }
-      progress.endTask();
-    }
-
-    return 0;
-  }
-
-  @SuppressWarnings({"rawtypes", "unchecked"})
-  private static void upsert(Relation rel, ByteString s) throws OrmException {
-    Collection ents = Collections.singleton(rel.codec().decode(s));
-    try {
-      // Not all relations support update; fall back manually.
-      rel.access().insert(ents);
-    } catch (OrmDuplicateKeyException e) {
-      rel.access().delete(ents);
-      rel.access().insert(ents);
-    }
-  }
-
-  @AutoValue
-  abstract static class Relation {
-    private static Relation create(RelationModel model, ReviewDb db)
-        throws IllegalAccessException, InvocationTargetException, NoSuchMethodException,
-            ClassNotFoundException {
-      Method m = db.getClass().getMethod(model.getMethodName());
-      Class<?> clazz = Class.forName(model.getEntityTypeClassName());
-      return new AutoValue_ProtobufImport_Relation(
-          (Access<?, ?>) m.invoke(db), CodecFactory.encoder(clazz));
-    }
-
-    abstract Access<?, ?> access();
-
-    abstract ProtobufCodec<?> codec();
-  }
-}
diff --git a/java/com/google/gerrit/pgm/Reindex.java b/java/com/google/gerrit/pgm/Reindex.java
index 52d3bd3..0e5f659 100644
--- a/java/com/google/gerrit/pgm/Reindex.java
+++ b/java/com/google/gerrit/pgm/Reindex.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.pgm;
 
-import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
 import static java.util.Objects.requireNonNull;
 import static java.util.stream.Collectors.toSet;
 
@@ -29,7 +28,6 @@
 import com.google.gerrit.lucene.LuceneIndexModule;
 import com.google.gerrit.pgm.util.BatchProgramModule;
 import com.google.gerrit.pgm.util.SiteProgram;
-import com.google.gerrit.pgm.util.ThreadLimiter;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.index.IndexModule;
@@ -40,7 +38,6 @@
 import com.google.inject.Injector;
 import com.google.inject.Key;
 import com.google.inject.Module;
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
@@ -81,10 +78,9 @@
   @Override
   public int run() throws Exception {
     mustHaveValidSite();
-    dbInjector = createDbInjector(MULTI_USER);
+    dbInjector = createDbInjector();
     cfgInjector = dbInjector.createChildInjector();
     globalConfig = dbInjector.getInstance(Key.get(Config.class, GerritServerConfig.class));
-    threads = ThreadLimiter.limitThreads(dbInjector, threads);
     overrideConfig();
     LifecycleManager dbManager = new LifecycleManager();
     dbManager.add(dbInjector);
@@ -117,7 +113,7 @@
     return true;
   }
 
-  private boolean reindex() throws IOException {
+  private boolean reindex() {
     boolean ok = true;
     for (IndexDefinition<?, ?, ?> def : indexDefs) {
       if (indices.isEmpty() || indices.contains(def.getName())) {
@@ -163,7 +159,7 @@
         throw new IllegalStateException("unsupported index.type");
     }
     modules.add(indexModule);
-    modules.add(dbInjector.getInstance(BatchProgramModule.class));
+    modules.add(new BatchProgramModule());
     modules.add(
         new FactoryModule() {
           @Override
@@ -189,8 +185,7 @@
     globalConfig.setBoolean("index", null, "autoReindexIfStale", false);
   }
 
-  private <K, V, I extends Index<K, V>> boolean reindex(IndexDefinition<K, V, I> def)
-      throws IOException {
+  private <K, V, I extends Index<K, V>> boolean reindex(IndexDefinition<K, V, I> def) {
     I index = def.getIndexCollection().getSearchIndex();
     requireNonNull(
         index, () -> String.format("no active search index configured for %s", def.getName()));
diff --git a/java/com/google/gerrit/pgm/Rulec.java b/java/com/google/gerrit/pgm/Rulec.java
index add06ef..aa72ae0 100644
--- a/java/com/google/gerrit/pgm/Rulec.java
+++ b/java/com/google/gerrit/pgm/Rulec.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.pgm;
 
-import static com.google.gerrit.server.schema.DataSourceProvider.Context.SINGLE_USER;
-
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.pgm.rules.PrologCompiler;
@@ -60,7 +58,7 @@
 
   @Override
   public int run() throws Exception {
-    dbInjector = createDbInjector(SINGLE_USER);
+    dbInjector = createDbInjector();
     manager.add(dbInjector);
     manager.start();
     dbInjector
diff --git a/java/com/google/gerrit/pgm/SwitchSecureStore.java b/java/com/google/gerrit/pgm/SwitchSecureStore.java
index 3f10130..733c9d1 100644
--- a/java/com/google/gerrit/pgm/SwitchSecureStore.java
+++ b/java/com/google/gerrit/pgm/SwitchSecureStore.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.pgm;
 
-import static com.google.gerrit.server.schema.DataSourceProvider.Context.SINGLE_USER;
-
 import com.google.common.base.Joiner;
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
@@ -86,7 +84,7 @@
     logger.atInfo().log(
         "Current secureStoreClass property (%s) will be replaced with %s",
         currentSecureStoreName, newSecureStore);
-    Injector dbInjector = createDbInjector(SINGLE_USER);
+    Injector dbInjector = createDbInjector();
     SecureStore currentStore = getSecureStore(currentSecureStoreName, dbInjector);
     SecureStore newStore = getSecureStore(newSecureStore, dbInjector);
 
diff --git a/java/com/google/gerrit/pgm/http/jetty/HttpLog.java b/java/com/google/gerrit/pgm/http/jetty/HttpLog.java
index a1d1d73..809f7bc 100644
--- a/java/com/google/gerrit/pgm/http/jetty/HttpLog.java
+++ b/java/com/google/gerrit/pgm/http/jetty/HttpLog.java
@@ -45,6 +45,7 @@
   protected static final String P_PROTOCOL = "Version";
   protected static final String P_STATUS = "Status";
   protected static final String P_CONTENT_LENGTH = "Content-Length";
+  protected static final String P_LATENCY = "Latency";
   protected static final String P_REFERER = "Referer";
   protected static final String P_USER_AGENT = "User-Agent";
 
@@ -94,6 +95,7 @@
     set(event, P_PROTOCOL, req.getProtocol());
     set(event, P_STATUS, rsp.getStatus());
     set(event, P_CONTENT_LENGTH, rsp.getContentCount());
+    set(event, P_LATENCY, System.currentTimeMillis() - req.getTimeStamp());
     set(event, P_REFERER, req.getHeader("Referer"));
     set(event, P_USER_AGENT, req.getHeader("User-Agent"));
 
diff --git a/java/com/google/gerrit/pgm/http/jetty/HttpLogLayout.java b/java/com/google/gerrit/pgm/http/jetty/HttpLogLayout.java
index 9f240ac..4b52c6f 100644
--- a/java/com/google/gerrit/pgm/http/jetty/HttpLogLayout.java
+++ b/java/com/google/gerrit/pgm/http/jetty/HttpLogLayout.java
@@ -72,6 +72,9 @@
     opt(buf, event, HttpLog.P_CONTENT_LENGTH);
 
     buf.append(' ');
+    opt(buf, event, HttpLog.P_LATENCY);
+
+    buf.append(' ');
     dq_opt(buf, event, HttpLog.P_REFERER);
 
     buf.append(' ');
diff --git a/java/com/google/gerrit/pgm/http/jetty/JettyServer.java b/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
index 25a28a4..7745ad4 100644
--- a/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
+++ b/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
@@ -49,7 +49,6 @@
 import org.eclipse.jetty.server.Handler;
 import org.eclipse.jetty.server.HttpConfiguration;
 import org.eclipse.jetty.server.HttpConnectionFactory;
-import org.eclipse.jetty.server.Request;
 import org.eclipse.jetty.server.SecureRequestCustomizer;
 import org.eclipse.jetty.server.Server;
 import org.eclipse.jetty.server.ServerConnector;
@@ -241,13 +240,9 @@
         defaultPort = 8080;
         config.addCustomizer(new ForwardedRequestCustomizer());
         config.addCustomizer(
-            new HttpConfiguration.Customizer() {
-              @Override
-              public void customize(
-                  Connector connector, HttpConfiguration channelConfig, Request request) {
-                request.setScheme(HttpScheme.HTTPS.asString());
-                request.setSecure(true);
-              }
+            (connector, channelConfig, request) -> {
+              request.setScheme(HttpScheme.HTTPS.asString());
+              request.setSecure(true);
             });
         c = newServerConnector(server, acceptors, config);
 
@@ -350,7 +345,7 @@
             maxThreads,
             minThreads,
             idleTimeout,
-            new BlockingArrayQueue<Runnable>(
+            new BlockingArrayQueue<>(
                 minThreads, // capacity,
                 minThreads, // growBy,
                 maxCapacity // maxCapacity
diff --git a/java/com/google/gerrit/pgm/init/BUILD b/java/com/google/gerrit/pgm/init/BUILD
index 9298dd0..eb0d49e 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/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/index",
         "//java/com/google/gerrit/launcher",
@@ -23,8 +24,6 @@
         "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/server/util/time",
         "//lib:guava",
-        "//lib:gwtjsonrpc",
-        "//lib:gwtorm",
         "//lib:h2",
         "//lib/commons:validator",
         "//lib/flogger:api",
diff --git a/java/com/google/gerrit/pgm/init/BaseInit.java b/java/com/google/gerrit/pgm/init/BaseInit.java
index 234abfd..0bc1860 100644
--- a/java/com/google/gerrit/pgm/init/BaseInit.java
+++ b/java/com/google/gerrit/pgm/init/BaseInit.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.pgm.init;
 
-import static com.google.gerrit.reviewdb.server.ReviewDbUtil.unwrapDb;
-import static com.google.gerrit.server.schema.DataSourceProvider.Context.SINGLE_USER;
 import static com.google.inject.Scopes.SINGLETON;
 import static com.google.inject.Stage.PRODUCTION;
 
@@ -24,6 +22,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Die;
 import com.google.gerrit.common.IoUtil;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.metrics.DisabledMetricMaker;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.pgm.init.api.ConsoleUI;
@@ -35,31 +34,23 @@
 import com.google.gerrit.pgm.init.index.elasticsearch.ElasticIndexModuleOnInit;
 import com.google.gerrit.pgm.init.index.lucene.LuceneIndexModuleOnInit;
 import com.google.gerrit.pgm.util.SiteProgram;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.GerritServerConfigModule;
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.index.IndexModule;
 import com.google.gerrit.server.plugins.JarScanner;
-import com.google.gerrit.server.schema.ReviewDbFactory;
-import com.google.gerrit.server.schema.SchemaUpdater;
+import com.google.gerrit.server.schema.NoteDbSchemaUpdater;
 import com.google.gerrit.server.schema.UpdateUI;
 import com.google.gerrit.server.securestore.SecureStore;
 import com.google.gerrit.server.securestore.SecureStoreClassName;
 import com.google.gerrit.server.securestore.SecureStoreProvider;
-import com.google.gwtorm.jdbc.JdbcExecutor;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.gwtorm.server.StatementExecutor;
 import com.google.inject.AbstractModule;
 import com.google.inject.CreationException;
 import com.google.inject.Guice;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.Module;
-import com.google.inject.Provider;
 import com.google.inject.TypeLiteral;
 import com.google.inject.spi.Message;
 import com.google.inject.util.Providers;
@@ -75,14 +66,12 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
-import javax.sql.DataSource;
 
 /** Initialize a new Gerrit installation. */
 public class BaseInit extends SiteProgram {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final boolean standalone;
-  private final boolean initDb;
   protected final PluginsDistribution pluginsDistribution;
   private final List<String> pluginsToInstall;
 
@@ -90,7 +79,6 @@
 
   protected BaseInit(PluginsDistribution pluginsDistribution, List<String> pluginsToInstall) {
     this.standalone = true;
-    this.initDb = true;
     this.pluginsDistribution = pluginsDistribution;
     this.pluginsToInstall = pluginsToInstall;
   }
@@ -98,22 +86,10 @@
   public BaseInit(
       Path sitePath,
       boolean standalone,
-      boolean initDb,
       PluginsDistribution pluginsDistribution,
       List<String> pluginsToInstall) {
-    this(sitePath, null, standalone, initDb, pluginsDistribution, pluginsToInstall);
-  }
-
-  public BaseInit(
-      Path sitePath,
-      final Provider<DataSource> dsProvider,
-      boolean standalone,
-      boolean initDb,
-      PluginsDistribution pluginsDistribution,
-      List<String> pluginsToInstall) {
-    super(sitePath, dsProvider);
+    super(sitePath);
     this.standalone = standalone;
-    this.initDb = initDb;
     this.pluginsDistribution = pluginsDistribution;
     this.pluginsToInstall = pluginsToInstall;
   }
@@ -143,7 +119,7 @@
         run = createSiteRun(init);
         try {
           run.upgradeSchema();
-        } catch (OrmException e) {
+        } catch (StorageException e) {
           String msg = "Couldn't upgrade schema. Expected if slave and read-only database";
           System.err.println(msg);
           logger.atSevere().withCause(e).log(msg);
@@ -262,15 +238,14 @@
     }
 
     m.add(new GerritServerConfigModule());
-    m.add(new InitModule(standalone, initDb));
+    m.add(new InitModule(standalone));
     m.add(
         new AbstractModule() {
           @Override
           protected void configure() {
             bind(ConsoleUI.class).toInstance(ui);
             bind(Path.class).annotatedWith(SitePath.class).toInstance(sitePath);
-            List<String> plugins =
-                MoreObjects.firstNonNull(getInstallPlugins(), new ArrayList<String>());
+            List<String> plugins = MoreObjects.firstNonNull(getInstallPlugins(), new ArrayList<>());
             bind(new TypeLiteral<List<String>>() {})
                 .annotatedWith(InstallPlugins.class)
                 .toInstance(plugins);
@@ -364,8 +339,7 @@
     public final ConsoleUI ui;
     public final SitePaths site;
     public final InitFlags flags;
-    final SchemaUpdater schemaUpdater;
-    final SchemaFactory<ReviewDb> schema;
+    final NoteDbSchemaUpdater noteDbSchemaUpdater;
     final GitRepositoryManager repositoryManager;
 
     @Inject
@@ -373,80 +347,50 @@
         ConsoleUI ui,
         SitePaths site,
         InitFlags flags,
-        SchemaUpdater schemaUpdater,
-        @ReviewDbFactory SchemaFactory<ReviewDb> schema,
+        NoteDbSchemaUpdater noteDbSchemaUpdater,
         GitRepositoryManager repositoryManager) {
       this.ui = ui;
       this.site = site;
       this.flags = flags;
-      this.schemaUpdater = schemaUpdater;
-      this.schema = schema;
+      this.noteDbSchemaUpdater = noteDbSchemaUpdater;
       this.repositoryManager = repositoryManager;
     }
 
-    void upgradeSchema() throws OrmException {
-      final List<String> pruneList = new ArrayList<>();
-      schemaUpdater.update(
-          new UpdateUI() {
-            @Override
-            public void message(String message) {
-              System.err.println(message);
-              System.err.flush();
-            }
+    void upgradeSchema() {
+      noteDbSchemaUpdater.update(new UpdateUIImpl(ui));
+    }
 
-            @Override
-            public boolean yesno(boolean defaultValue, String message) {
-              return ui.yesno(defaultValue, message);
-            }
+    private static class UpdateUIImpl implements UpdateUI {
+      private final ConsoleUI consoleUi;
 
-            @Override
-            public void waitForUser() {
-              ui.waitForUser();
-            }
+      UpdateUIImpl(ConsoleUI consoleUi) {
+        this.consoleUi = consoleUi;
+      }
 
-            @Override
-            public String readString(
-                String defaultValue, Set<String> allowedValues, String message) {
-              return ui.readString(defaultValue, allowedValues, message);
-            }
+      @Override
+      public void message(String message) {
+        System.err.println(message);
+        System.err.flush();
+      }
 
-            @Override
-            public boolean isBatch() {
-              return ui.isBatch();
-            }
+      @Override
+      public boolean yesno(boolean defaultValue, String message) {
+        return consoleUi.yesno(defaultValue, message);
+      }
 
-            @Override
-            public void pruneSchema(StatementExecutor e, List<String> prune) {
-              for (String p : prune) {
-                if (!pruneList.contains(p)) {
-                  pruneList.add(p);
-                }
-              }
-            }
-          });
+      @Override
+      public void waitForUser() {
+        consoleUi.waitForUser();
+      }
 
-      if (!pruneList.isEmpty()) {
-        StringBuilder msg = new StringBuilder();
-        msg.append("Execute the following SQL to drop unused objects:\n");
-        msg.append("\n");
-        for (String sql : pruneList) {
-          msg.append("  ");
-          msg.append(sql);
-          msg.append(";\n");
-        }
+      @Override
+      public String readString(String defaultValue, Set<String> allowedValues, String message) {
+        return consoleUi.readString(defaultValue, allowedValues, message);
+      }
 
-        if (ui.isBatch()) {
-          System.err.print(msg);
-          System.err.flush();
-
-        } else if (ui.yesno(true, "%s\nExecute now", msg)) {
-          try (JdbcSchema db = (JdbcSchema) unwrapDb(schema.open());
-              JdbcExecutor e = new JdbcExecutor(db)) {
-            for (String sql : pruneList) {
-              e.execute(sql);
-            }
-          }
-        }
+      @Override
+      public boolean isBatch() {
+        return consoleUi.isBatch();
       }
     }
   }
@@ -466,7 +410,7 @@
               bind(InitFlags.class).toInstance(init.flags);
             }
           });
-      Injector dbInjector = createDbInjector(SINGLE_USER);
+      Injector dbInjector = createDbInjector();
 
       switch (IndexModule.getIndexType(dbInjector)) {
         case LUCENE:
diff --git a/java/com/google/gerrit/pgm/init/DB2Initializer.java b/java/com/google/gerrit/pgm/init/DB2Initializer.java
deleted file mode 100644
index 9dc1088..0000000
--- a/java/com/google/gerrit/pgm/init/DB2Initializer.java
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.pgm.init;
-
-import static com.google.gerrit.pgm.init.api.InitUtil.username;
-
-import com.google.gerrit.pgm.init.api.Section;
-
-public class DB2Initializer implements DatabaseConfigInitializer {
-
-  @Override
-  public void initConfig(Section databaseSection) {
-    final String defPort = "50001";
-    databaseSection.string("Server hostname", "hostname", "localhost");
-    databaseSection.string("Server port", "port", defPort, false);
-    databaseSection.string("Database name", "database", "gerrit");
-    databaseSection.string("Database username", "username", username());
-    databaseSection.password("username", "password");
-  }
-}
diff --git a/java/com/google/gerrit/pgm/init/DatabaseConfigInitializer.java b/java/com/google/gerrit/pgm/init/DatabaseConfigInitializer.java
deleted file mode 100644
index 2701957..0000000
--- a/java/com/google/gerrit/pgm/init/DatabaseConfigInitializer.java
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.pgm.init;
-
-import com.google.gerrit.pgm.init.api.Section;
-
-/** Abstraction of initializer for the database section */
-interface DatabaseConfigInitializer {
-
-  /**
-   * Performs database platform specific configuration steps and writes configuration parameters
-   * into the given database section
-   */
-  void initConfig(Section databaseSection);
-}
diff --git a/java/com/google/gerrit/pgm/init/DatabaseConfigModule.java b/java/com/google/gerrit/pgm/init/DatabaseConfigModule.java
deleted file mode 100644
index 44f883a..0000000
--- a/java/com/google/gerrit/pgm/init/DatabaseConfigModule.java
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.pgm.init;
-
-import com.google.gerrit.server.config.SitePaths;
-import com.google.inject.AbstractModule;
-import com.google.inject.name.Names;
-
-public class DatabaseConfigModule extends AbstractModule {
-
-  private final SitePaths site;
-
-  public DatabaseConfigModule(SitePaths site) {
-    this.site = site;
-  }
-
-  @Override
-  protected void configure() {
-    bind(SitePaths.class).toInstance(site);
-    bind(DatabaseConfigInitializer.class)
-        .annotatedWith(Names.named("db2"))
-        .to(DB2Initializer.class);
-    bind(DatabaseConfigInitializer.class)
-        .annotatedWith(Names.named("derby"))
-        .to(DerbyInitializer.class);
-    bind(DatabaseConfigInitializer.class).annotatedWith(Names.named("h2")).to(H2Initializer.class);
-    bind(DatabaseConfigInitializer.class)
-        .annotatedWith(Names.named("jdbc"))
-        .to(JDBCInitializer.class);
-    bind(DatabaseConfigInitializer.class)
-        .annotatedWith(Names.named("mariadb"))
-        .to(MariaDbInitializer.class);
-    bind(DatabaseConfigInitializer.class)
-        .annotatedWith(Names.named("mysql"))
-        .to(MySqlInitializer.class);
-    bind(DatabaseConfigInitializer.class)
-        .annotatedWith(Names.named("oracle"))
-        .to(OracleInitializer.class);
-    bind(DatabaseConfigInitializer.class)
-        .annotatedWith(Names.named("postgresql"))
-        .to(PostgreSQLInitializer.class);
-    bind(DatabaseConfigInitializer.class)
-        .annotatedWith(Names.named("maxdb"))
-        .to(MaxDbInitializer.class);
-    bind(DatabaseConfigInitializer.class)
-        .annotatedWith(Names.named("hana"))
-        .to(HANAInitializer.class);
-  }
-}
diff --git a/java/com/google/gerrit/pgm/init/DerbyInitializer.java b/java/com/google/gerrit/pgm/init/DerbyInitializer.java
deleted file mode 100644
index 3aad0f4..0000000
--- a/java/com/google/gerrit/pgm/init/DerbyInitializer.java
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.pgm.init;
-
-import static com.google.gerrit.pgm.init.api.InitUtil.die;
-
-import com.google.gerrit.common.FileUtil;
-import com.google.gerrit.pgm.init.api.Section;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.inject.Inject;
-import java.nio.file.Path;
-
-class DerbyInitializer implements DatabaseConfigInitializer {
-
-  private final SitePaths site;
-
-  @Inject
-  DerbyInitializer(SitePaths site) {
-    this.site = site;
-  }
-
-  @Override
-  public void initConfig(Section databaseSection) {
-    String path = databaseSection.get("database");
-    Path db;
-    if (path == null) {
-      db = site.resolve("db").resolve("ReviewDB");
-      databaseSection.set("database", db.toString());
-    } else {
-      db = site.resolve(path);
-    }
-    if (db == null) {
-      throw die("database.database must be supplied for Derby");
-    }
-    db = db.getParent();
-    FileUtil.mkdirsOrDie(db, "cannot create database.database");
-  }
-}
diff --git a/java/com/google/gerrit/pgm/init/ExternalIdsOnInit.java b/java/com/google/gerrit/pgm/init/ExternalIdsOnInit.java
index 6336c93..9519653 100644
--- a/java/com/google/gerrit/pgm/init/ExternalIdsOnInit.java
+++ b/java/com/google/gerrit/pgm/init/ExternalIdsOnInit.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.File;
 import java.io.IOException;
@@ -49,7 +48,7 @@
   }
 
   public synchronized void insert(String commitMessage, Collection<ExternalId> extIds)
-      throws OrmException, IOException, ConfigInvalidException {
+      throws IOException, ConfigInvalidException {
     File path = getPath();
     if (path != null) {
       try (Repository allUsersRepo = new FileRepository(path)) {
diff --git a/java/com/google/gerrit/pgm/init/GroupsOnInit.java b/java/com/google/gerrit/pgm/init/GroupsOnInit.java
index 8e06aa1..273ebfb 100644
--- a/java/com/google/gerrit/pgm/init/GroupsOnInit.java
+++ b/java/com/google/gerrit/pgm/init/GroupsOnInit.java
@@ -20,12 +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.common.errors.NoSuchGroupException;
+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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdentProvider;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.GerritServerIdProvider;
@@ -37,7 +36,6 @@
 import com.google.gerrit.server.group.db.GroupConfig;
 import com.google.gerrit.server.group.db.GroupNameNotes;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.File;
 import java.io.IOException;
@@ -54,9 +52,8 @@
 /**
  * A database accessor for calls related to groups.
  *
- * <p>All calls which read or write group related details to the database <strong>during
- * init</strong> (either ReviewDb or NoteDb) are gathered here. For non-init cases, use {@code
- * Groups} or {@code GroupsUpdate} instead.
+ * <p>All calls which read or write group related details to the NoteDb <strong>during init</strong>
+ * are gathered here. For non-init cases, use {@code Groups} or {@code GroupsUpdate} instead.
  *
  * <p>All methods of this class refer to <em>internal</em> groups.
  */
@@ -76,16 +73,14 @@
   /**
    * Returns the {@code AccountGroup} for the specified {@code GroupReference}.
    *
-   * @param db the {@code ReviewDb} instance to use for lookups
    * @param groupReference the {@code GroupReference} of the group
    * @return the {@code InternalGroup} represented by the {@code GroupReference}
-   * @throws OrmException if the group couldn't be retrieved from ReviewDb
    * @throws IOException if an error occurs while reading from NoteDb
    * @throws ConfigInvalidException if the data in NoteDb is in an incorrect format
    * @throws NoSuchGroupException if a group with such a name doesn't exist
    */
-  public InternalGroup getExistingGroup(ReviewDb db, GroupReference groupReference)
-      throws OrmException, NoSuchGroupException, IOException, ConfigInvalidException {
+  public InternalGroup getExistingGroup(GroupReference groupReference)
+      throws NoSuchGroupException, IOException, ConfigInvalidException {
     File allUsersRepoPath = getPathToAllUsersRepository();
     if (allUsersRepoPath != null) {
       try (Repository allUsersRepo = new FileRepository(allUsersRepoPath)) {
@@ -102,14 +97,11 @@
   /**
    * Returns {@code GroupReference}s for all internal groups.
    *
-   * @param db the {@code ReviewDb} instance to use for lookups
    * @return a stream of the {@code GroupReference}s of all internal groups
-   * @throws OrmException if an error occurs while reading from ReviewDb
    * @throws IOException if an error occurs while reading from NoteDb
    * @throws ConfigInvalidException if the data in NoteDb is in an incorrect format
    */
-  public Stream<GroupReference> getAllGroupReferences(ReviewDb db)
-      throws OrmException, IOException, ConfigInvalidException {
+  public Stream<GroupReference> getAllGroupReferences() throws IOException, ConfigInvalidException {
     File allUsersRepoPath = getPathToAllUsersRepository();
     if (allUsersRepoPath != null) {
       try (Repository allUsersRepo = new FileRepository(allUsersRepoPath)) {
@@ -126,14 +118,12 @@
    * <p><strong>Note</strong>: This method doesn't check whether the account exists! It also doesn't
    * update the account index!
    *
-   * @param db the {@code ReviewDb} instance to update
    * @param groupUuid the UUID of the group
    * @param account the account to add
-   * @throws OrmException if an error occurs while reading/writing from/to ReviewDb
    * @throws NoSuchGroupException if the specified group doesn't exist
    */
-  public void addGroupMember(ReviewDb db, AccountGroup.UUID groupUuid, Account account)
-      throws OrmException, NoSuchGroupException, IOException, ConfigInvalidException {
+  public void addGroupMember(AccountGroup.UUID groupUuid, Account account)
+      throws NoSuchGroupException, IOException, ConfigInvalidException {
     File allUsersRepoPath = getPathToAllUsersRepository();
     if (allUsersRepoPath != null) {
       try (Repository repository = new FileRepository(allUsersRepoPath)) {
diff --git a/java/com/google/gerrit/pgm/init/H2Initializer.java b/java/com/google/gerrit/pgm/init/H2Initializer.java
deleted file mode 100644
index 63aa6ec..0000000
--- a/java/com/google/gerrit/pgm/init/H2Initializer.java
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.pgm.init;
-
-import static com.google.gerrit.pgm.init.api.InitUtil.die;
-
-import com.google.gerrit.common.FileUtil;
-import com.google.gerrit.pgm.init.api.Section;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.inject.Inject;
-import java.nio.file.Path;
-
-class H2Initializer implements DatabaseConfigInitializer {
-
-  private final SitePaths site;
-
-  @Inject
-  H2Initializer(SitePaths site) {
-    this.site = site;
-  }
-
-  @Override
-  public void initConfig(Section databaseSection) {
-    String path = databaseSection.get("database");
-    Path db;
-    if (path == null) {
-      db = site.resolve("db").resolve("ReviewDB");
-      databaseSection.set("database", db.toString());
-    } else {
-      db = site.resolve(path);
-    }
-    if (db == null) {
-      throw die("database.database must be supplied for H2");
-    }
-    db = db.getParent();
-    FileUtil.mkdirsOrDie(db, "cannot create database.database");
-  }
-}
diff --git a/java/com/google/gerrit/pgm/init/HANAInitializer.java b/java/com/google/gerrit/pgm/init/HANAInitializer.java
deleted file mode 100644
index 713392d..0000000
--- a/java/com/google/gerrit/pgm/init/HANAInitializer.java
+++ /dev/null
@@ -1,32 +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.pgm.init;
-
-import static com.google.gerrit.pgm.init.api.InitUtil.username;
-
-import com.google.gerrit.pgm.init.api.Section;
-
-public class HANAInitializer implements DatabaseConfigInitializer {
-
-  @Override
-  public void initConfig(Section databaseSection) {
-    final String defPort = "(hana default)";
-    databaseSection.string("Server hostname", "hostname", "localhost");
-    databaseSection.string("Server port", "port", defPort, true);
-    databaseSection.string("Database name", "database", null);
-    databaseSection.string("Database username", "username", username());
-    databaseSection.password("username", "password");
-  }
-}
diff --git a/java/com/google/gerrit/pgm/init/InitAdminUser.java b/java/com/google/gerrit/pgm/init/InitAdminUser.java
index f12fa50..27e6ce9 100644
--- a/java/com/google/gerrit/pgm/init/InitAdminUser.java
+++ b/java/com/google/gerrit/pgm/init/InitAdminUser.java
@@ -18,7 +18,7 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+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;
@@ -26,7 +26,6 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.AccountSshKey;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.externalids.ExternalId;
@@ -37,7 +36,6 @@
 import com.google.gerrit.server.index.group.GroupIndex;
 import com.google.gerrit.server.index.group.GroupIndexCollection;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.nio.file.Files;
@@ -57,7 +55,6 @@
   private final ExternalIdsOnInit externalIds;
   private final SequencesOnInit sequencesOnInit;
   private final GroupsOnInit groupsOnInit;
-  private SchemaFactory<ReviewDb> dbFactory;
   private AccountIndexCollection accountIndexCollection;
   private GroupIndexCollection groupIndexCollection;
 
@@ -84,11 +81,6 @@
   @Override
   public void run() {}
 
-  @Inject(optional = true)
-  void set(SchemaFactory<ReviewDb> dbFactory) {
-    this.dbFactory = dbFactory;
-  }
-
   @Inject
   void set(AccountIndexCollection accountIndexCollection) {
     this.accountIndexCollection = accountIndexCollection;
@@ -106,58 +98,56 @@
       return;
     }
 
-    try (ReviewDb db = dbFactory.open()) {
-      if (!accounts.hasAnyAccount()) {
-        ui.header("Gerrit Administrator");
-        if (ui.yesno(true, "Create administrator user")) {
-          Account.Id id = new Account.Id(sequencesOnInit.nextAccountId(db));
-          String username = ui.readString("admin", "username");
-          String name = ui.readString("Administrator", "name");
-          String httpPassword = ui.readString("secret", "HTTP password");
-          AccountSshKey sshKey = readSshKey(id);
-          String email = readEmail(sshKey);
+    if (!accounts.hasAnyAccount()) {
+      ui.header("Gerrit Administrator");
+      if (ui.yesno(true, "Create administrator user")) {
+        Account.Id id = new Account.Id(sequencesOnInit.nextAccountId());
+        String username = ui.readString("admin", "username");
+        String name = ui.readString("Administrator", "name");
+        String httpPassword = ui.readString("secret", "HTTP password");
+        AccountSshKey sshKey = readSshKey(id);
+        String email = readEmail(sshKey);
 
-          List<ExternalId> extIds = new ArrayList<>(2);
-          extIds.add(ExternalId.createUsername(username, id, httpPassword));
+        List<ExternalId> extIds = new ArrayList<>(2);
+        extIds.add(ExternalId.createUsername(username, id, httpPassword));
 
-          if (email != null) {
-            extIds.add(ExternalId.createEmail(id, email));
-          }
-          externalIds.insert("Add external IDs for initial admin user", extIds);
+        if (email != null) {
+          extIds.add(ExternalId.createEmail(id, email));
+        }
+        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 a = new Account(id, TimeUtil.nowTs());
+        a.setFullName(name);
+        a.setPreferredEmail(email);
+        accounts.insert(a);
 
-          // Only two groups should exist at this point in time and hence iterating over all of them
-          // is cheap.
-          Optional<GroupReference> adminGroupReference =
-              groupsOnInit
-                  .getAllGroupReferences(db)
-                  .filter(group -> group.getName().equals("Administrators"))
-                  .findAny();
-          if (!adminGroupReference.isPresent()) {
-            throw new NoSuchGroupException("Administrators");
-          }
-          GroupReference adminGroup = adminGroupReference.get();
-          groupsOnInit.addGroupMember(db, adminGroup.getUUID(), a);
+        // Only two groups should exist at this point in time and hence iterating over all of them
+        // is cheap.
+        Optional<GroupReference> adminGroupReference =
+            groupsOnInit
+                .getAllGroupReferences()
+                .filter(group -> group.getName().equals("Administrators"))
+                .findAny();
+        if (!adminGroupReference.isPresent()) {
+          throw new NoSuchGroupException("Administrators");
+        }
+        GroupReference adminGroup = adminGroupReference.get();
+        groupsOnInit.addGroupMember(adminGroup.getUUID(), a);
 
-          if (sshKey != null) {
-            VersionedAuthorizedKeysOnInit authorizedKeys = authorizedKeysFactory.create(id).load();
-            authorizedKeys.addKey(sshKey.sshPublicKey());
-            authorizedKeys.save("Add SSH key for initial admin user\n");
-          }
+        if (sshKey != null) {
+          VersionedAuthorizedKeysOnInit authorizedKeys = authorizedKeysFactory.create(id).load();
+          authorizedKeys.addKey(sshKey.sshPublicKey());
+          authorizedKeys.save("Add SSH key for initial admin user\n");
+        }
 
-          AccountState as = AccountState.forAccount(new AllUsersName(allUsers.get()), a, extIds);
-          for (AccountIndex accountIndex : accountIndexCollection.getWriteIndexes()) {
-            accountIndex.replace(as);
-          }
+        AccountState as = AccountState.forAccount(new AllUsersName(allUsers.get()), a, extIds);
+        for (AccountIndex accountIndex : accountIndexCollection.getWriteIndexes()) {
+          accountIndex.replace(as);
+        }
 
-          InternalGroup adminInternalGroup = groupsOnInit.getExistingGroup(db, adminGroup);
-          for (GroupIndex groupIndex : groupIndexCollection.getWriteIndexes()) {
-            groupIndex.replace(adminInternalGroup);
-          }
+        InternalGroup adminInternalGroup = groupsOnInit.getExistingGroup(adminGroup);
+        for (GroupIndex groupIndex : groupIndexCollection.getWriteIndexes()) {
+          groupIndex.replace(adminInternalGroup);
         }
       }
     }
diff --git a/java/com/google/gerrit/pgm/init/InitAuth.java b/java/com/google/gerrit/pgm/init/InitAuth.java
index a52d8ba..c15cff3 100644
--- a/java/com/google/gerrit/pgm/init/InitAuth.java
+++ b/java/com/google/gerrit/pgm/init/InitAuth.java
@@ -26,7 +26,7 @@
 import com.google.gerrit.pgm.init.api.InitFlags;
 import com.google.gerrit.pgm.init.api.InitStep;
 import com.google.gerrit.pgm.init.api.Section;
-import com.google.gwtjsonrpc.server.SignedToken;
+import com.google.gerrit.server.mail.SignedToken;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.EnumSet;
diff --git a/java/com/google/gerrit/pgm/init/InitDatabase.java b/java/com/google/gerrit/pgm/init/InitDatabase.java
deleted file mode 100644
index 558716c..0000000
--- a/java/com/google/gerrit/pgm/init/InitDatabase.java
+++ /dev/null
@@ -1,140 +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.pgm.init;
-
-import static com.google.gerrit.server.notedb.NoteDbTable.CHANGES;
-import static com.google.gerrit.server.notedb.NotesMigration.SECTION_NOTE_DB;
-import static com.google.inject.Stage.PRODUCTION;
-
-import com.google.common.base.Strings;
-import com.google.common.collect.Sets;
-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.Section;
-import com.google.gerrit.server.config.GerritServerIdProvider;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.notedb.NotesMigrationState;
-import com.google.inject.Binding;
-import com.google.inject.Guice;
-import com.google.inject.Inject;
-import com.google.inject.Injector;
-import com.google.inject.Key;
-import com.google.inject.Singleton;
-import com.google.inject.TypeLiteral;
-import com.google.inject.name.Named;
-import com.google.inject.name.Names;
-import java.lang.annotation.Annotation;
-import java.util.List;
-import java.util.Set;
-import org.eclipse.jgit.lib.Config;
-
-/** Initialize the {@code database} configuration section. */
-@Singleton
-class InitDatabase implements InitStep {
-  private final ConsoleUI ui;
-  private final SitePaths site;
-  private final Libraries libraries;
-  private final InitFlags flags;
-  private final Section database;
-  private final Section idSection;
-  private final Section noteDbChanges;
-
-  @Inject
-  InitDatabase(
-      ConsoleUI ui,
-      SitePaths site,
-      Libraries libraries,
-      InitFlags flags,
-      Section.Factory sections) {
-    this.ui = ui;
-    this.site = site;
-    this.libraries = libraries;
-    this.flags = flags; // Don't grab any flags yet; they aren't initialized until BaseInit#run.
-    this.database = sections.get("database", null);
-    this.idSection = sections.get(GerritServerIdProvider.SECTION, null);
-    this.noteDbChanges = sections.get(SECTION_NOTE_DB, CHANGES.key());
-  }
-
-  @Override
-  public void run() {
-    initSqlDb();
-    if (flags.isNew) {
-      initNoteDb();
-    }
-  }
-
-  private void initSqlDb() {
-    ui.header("SQL Database");
-
-    Set<String> allowedValues = Sets.newTreeSet();
-    Injector i = Guice.createInjector(PRODUCTION, new DatabaseConfigModule(site));
-    List<Binding<DatabaseConfigInitializer>> dbConfigBindings =
-        i.findBindingsByType(new TypeLiteral<DatabaseConfigInitializer>() {});
-    for (Binding<DatabaseConfigInitializer> binding : dbConfigBindings) {
-      Annotation annotation = binding.getKey().getAnnotation();
-      if (annotation instanceof Named) {
-        allowedValues.add(((Named) annotation).value());
-      }
-    }
-
-    if (!Strings.isNullOrEmpty(database.get("url"))
-        && Strings.isNullOrEmpty(database.get("type"))) {
-      database.set("type", "jdbc");
-    }
-
-    String dbType = database.select("Database server type", "type", "h2", allowedValues);
-
-    DatabaseConfigInitializer dci =
-        i.getInstance(Key.get(DatabaseConfigInitializer.class, Names.named(dbType.toLowerCase())));
-
-    if (dci instanceof MySqlInitializer) {
-      libraries.mysqlDriver.downloadRequired();
-    } else if (dci instanceof MariaDbInitializer) {
-      libraries.mariadbDriver.downloadRequired();
-    } else if (dci instanceof OracleInitializer) {
-      libraries.oracleDriver.downloadRequired();
-    } else if (dci instanceof DB2Initializer) {
-      libraries.db2Driver.downloadRequired();
-    } else if (dci instanceof HANAInitializer) {
-      libraries.hanaDriver.downloadRequired();
-    }
-
-    dci.initConfig(database);
-
-    // Initialize UUID for NoteDb on first init.
-    String id = idSection.get(GerritServerIdProvider.KEY);
-    if (Strings.isNullOrEmpty(id)) {
-      idSection.set(GerritServerIdProvider.KEY, GerritServerIdProvider.generate());
-    }
-  }
-
-  private void initNoteDb() {
-    ui.header("NoteDb Database");
-    ui.message(
-        "Use NoteDb for change metadata?\n"
-            + "  See documentation:\n"
-            + "  https://gerrit-review.googlesource.com/Documentation/note-db.html\n");
-    if (!ui.yesno(true, "Enable")) {
-      return;
-    }
-
-    Config defaultConfig = new Config();
-    NotesMigrationState.FINAL.setConfigValues(defaultConfig);
-    for (String name : defaultConfig.getNames(SECTION_NOTE_DB, CHANGES.key())) {
-      noteDbChanges.set(name, defaultConfig.getString(SECTION_NOTE_DB, CHANGES.key(), name));
-    }
-  }
-}
diff --git a/java/com/google/gerrit/pgm/init/InitHttpd.java b/java/com/google/gerrit/pgm/init/InitHttpd.java
index ada4ac5..b9fe874 100644
--- a/java/com/google/gerrit/pgm/init/InitHttpd.java
+++ b/java/com/google/gerrit/pgm/init/InitHttpd.java
@@ -25,7 +25,7 @@
 import com.google.gerrit.pgm.init.api.InitStep;
 import com.google.gerrit.pgm.init.api.Section;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gwtjsonrpc.server.SignedToken;
+import com.google.gerrit.server.mail.SignedToken;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
diff --git a/java/com/google/gerrit/pgm/init/InitLabels.java b/java/com/google/gerrit/pgm/init/InitLabels.java
index 3d1ec7b..0797cf9 100644
--- a/java/com/google/gerrit/pgm/init/InitLabels.java
+++ b/java/com/google/gerrit/pgm/init/InitLabels.java
@@ -61,7 +61,7 @@
           KEY_LABEL,
           LABEL_VERIFIED,
           KEY_VALUE,
-          Arrays.asList(new String[] {"-1 Fails", "0 No score", "+1 Verified"}));
+          Arrays.asList("-1 Fails", "0 No score", "+1 Verified"));
       cfg.setBoolean(KEY_LABEL, LABEL_VERIFIED, KEY_COPY_ALL_SCORES_IF_NO_CODE_CHANGE, true);
       allProjectsConfig.save("Configure 'Verified' label");
     }
diff --git a/java/com/google/gerrit/pgm/init/InitModule.java b/java/com/google/gerrit/pgm/init/InitModule.java
index f677ceb..b658675 100644
--- a/java/com/google/gerrit/pgm/init/InitModule.java
+++ b/java/com/google/gerrit/pgm/init/InitModule.java
@@ -26,29 +26,20 @@
 public class InitModule extends FactoryModule {
 
   private final boolean standalone;
-  private final boolean initDb;
 
-  public InitModule(boolean standalone, boolean initDb) {
+  public InitModule(boolean standalone) {
     this.standalone = standalone;
-    this.initDb = initDb;
   }
 
   @Override
   protected void configure() {
     bind(SitePaths.class);
-    bind(Libraries.class);
-    bind(LibraryDownloader.class);
     factory(Section.Factory.class);
     factory(VersionedAuthorizedKeysOnInit.Factory.class);
 
     // Steps are executed in the order listed here.
     //
-    step().to(UpgradeFrom2_0_x.class);
-
     step().to(InitGitManager.class);
-    if (initDb) {
-      step().to(InitDatabase.class);
-    }
     step().to(InitLogging.class);
     step().to(InitIndex.class);
     step().to(InitAuth.class);
diff --git a/java/com/google/gerrit/pgm/init/JDBCInitializer.java b/java/com/google/gerrit/pgm/init/JDBCInitializer.java
deleted file mode 100644
index e3a1d66..0000000
--- a/java/com/google/gerrit/pgm/init/JDBCInitializer.java
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.pgm.init;
-
-import static com.google.gerrit.pgm.init.api.InitUtil.username;
-
-import com.google.common.base.Strings;
-import com.google.gerrit.pgm.init.api.Section;
-
-class JDBCInitializer implements DatabaseConfigInitializer {
-  @Override
-  public void initConfig(Section database) {
-    boolean hasUrl = Strings.emptyToNull(database.get("url")) != null;
-    database.string("URL", "url", null);
-    guessDriver(database);
-    database.string("Driver class name", "driver", null);
-    database.string("Database username", "username", hasUrl ? null : username());
-    database.password("username", "password");
-  }
-
-  private void guessDriver(Section database) {
-    String url = Strings.emptyToNull(database.get("url"));
-    if (url != null && Strings.isNullOrEmpty(database.get("driver"))) {
-      if (url.startsWith("jdbc:derby:")) {
-        database.set("driver", "org.apache.derby.jdbc.EmbeddedDriver");
-      } else if (url.startsWith("jdbc:h2:")) {
-        database.set("driver", "org.h2.Driver");
-      } else if (url.startsWith("jdbc:mariadb:")) {
-        database.set("driver", "org.mariadb.jdbc.Driver");
-      } else if (url.startsWith("jdbc:mysql:")) {
-        database.set("driver", "com.mysql.jdbc.Driver");
-      } else if (url.startsWith("jdbc:postgresql:")) {
-        database.set("driver", "org.postgresql.Driver");
-      }
-    }
-  }
-}
diff --git a/java/com/google/gerrit/pgm/init/Libraries.java b/java/com/google/gerrit/pgm/init/Libraries.java
deleted file mode 100644
index 826a79b..0000000
--- a/java/com/google/gerrit/pgm/init/Libraries.java
+++ /dev/null
@@ -1,141 +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.pgm.init;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.gerrit.pgm.init.api.LibraryDownload;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.lang.reflect.Field;
-import java.lang.reflect.Modifier;
-import java.util.List;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.Config;
-
-/** Standard {@link LibraryDownloader} instances derived from configuration. */
-@Singleton
-class Libraries {
-  private static final String RESOURCE_FILE = "com/google/gerrit/pgm/init/libraries.config";
-
-  private final Provider<LibraryDownloader> downloadProvider;
-  private final List<String> skippedDownloads;
-  private final boolean skipAllDownloads;
-
-  /* final */ LibraryDownloader db2Driver;
-  /* final */ LibraryDownloader db2DriverLicense;
-  /* final */ LibraryDownloader hanaDriver;
-  /* final */ LibraryDownloader mariadbDriver;
-  /* final */ LibraryDownloader mysqlDriver;
-  /* final */ LibraryDownloader oracleDriver;
-
-  @Inject
-  Libraries(
-      final Provider<LibraryDownloader> downloadProvider,
-      @LibraryDownload List<String> skippedDownloads,
-      @LibraryDownload Boolean skipAllDownloads) {
-    this.downloadProvider = downloadProvider;
-    this.skippedDownloads = skippedDownloads;
-    this.skipAllDownloads = skipAllDownloads;
-    init();
-  }
-
-  private void init() {
-    final Config cfg = new Config();
-    try {
-      cfg.fromText(read(RESOURCE_FILE));
-    } catch (IOException | ConfigInvalidException e) {
-      throw new RuntimeException(e.getMessage(), e);
-    }
-
-    for (Field f : Libraries.class.getDeclaredFields()) {
-      if ((f.getModifiers() & Modifier.STATIC) == 0 && f.getType() == LibraryDownloader.class) {
-        try {
-          f.set(this, downloadProvider.get());
-        } catch (IllegalArgumentException | IllegalAccessException e) {
-          throw new IllegalStateException("Cannot initialize " + f.getName(), e);
-        }
-      }
-    }
-
-    for (Field f : Libraries.class.getDeclaredFields()) {
-      if ((f.getModifiers() & Modifier.STATIC) == 0 && f.getType() == LibraryDownloader.class) {
-        try {
-          init(f, cfg);
-        } catch (IllegalArgumentException
-            | IllegalAccessException
-            | NoSuchFieldException
-            | SecurityException e) {
-          throw new IllegalStateException("Cannot configure " + f.getName(), e);
-        }
-      }
-    }
-  }
-
-  private void init(Field field, Config cfg)
-      throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
-          SecurityException {
-    String n = field.getName();
-    LibraryDownloader dl = (LibraryDownloader) field.get(this);
-    dl.setName(get(cfg, n, "name"));
-    dl.setJarUrl(get(cfg, n, "url"));
-    dl.setSHA1(getOptional(cfg, n, "sha1"));
-    dl.setRemove(get(cfg, n, "remove"));
-    for (String d : cfg.getStringList("library", n, "needs")) {
-      dl.addNeeds((LibraryDownloader) getClass().getDeclaredField(d).get(this));
-    }
-    dl.setSkipDownload(skipAllDownloads || skippedDownloads.contains(n));
-  }
-
-  private static String getOptional(Config cfg, String name, String key) {
-    return doGet(cfg, name, key, false);
-  }
-
-  private static String get(Config cfg, String name, String key) {
-    return doGet(cfg, name, key, true);
-  }
-
-  private static String doGet(Config cfg, String name, String key, boolean required) {
-    String val = cfg.getString("library", name, key);
-    if ((val == null || val.isEmpty()) && required) {
-      throw new IllegalStateException(
-          "Variable library." + name + "." + key + " is required within " + RESOURCE_FILE);
-    }
-    return val;
-  }
-
-  private static String read(String p) throws IOException {
-    try (InputStream in = Libraries.class.getClassLoader().getResourceAsStream(p)) {
-      if (in == null) {
-        throw new FileNotFoundException("Cannot load resource " + p);
-      }
-      try (Reader r = new InputStreamReader(in, UTF_8)) {
-        final StringBuilder buf = new StringBuilder();
-        final char[] tmp = new char[512];
-        int n;
-        while (0 < (n = r.read(tmp))) {
-          buf.append(tmp, 0, n);
-        }
-        return buf.toString();
-      }
-    }
-  }
-}
diff --git a/java/com/google/gerrit/pgm/init/LibraryDownloader.java b/java/com/google/gerrit/pgm/init/LibraryDownloader.java
deleted file mode 100644
index 0b31ee2..0000000
--- a/java/com/google/gerrit/pgm/init/LibraryDownloader.java
+++ /dev/null
@@ -1,316 +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.pgm.init;
-
-import com.google.common.hash.Funnels;
-import com.google.common.hash.Hasher;
-import com.google.common.hash.Hashing;
-import com.google.common.io.ByteStreams;
-import com.google.gerrit.common.Die;
-import com.google.gerrit.common.IoUtil;
-import com.google.gerrit.pgm.init.api.ConsoleUI;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.inject.Inject;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.HttpURLConnection;
-import java.net.Proxy;
-import java.net.ProxySelector;
-import java.net.URISyntaxException;
-import java.net.URL;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.List;
-import org.eclipse.jgit.util.HttpSupport;
-
-/** Get optional or required 3rd party library files into $site_path/lib. */
-class LibraryDownloader {
-  private final ConsoleUI ui;
-  private final Path lib_dir;
-  private final StaleLibraryRemover remover;
-
-  private boolean required;
-  private String name;
-  private String jarUrl;
-  private String sha1;
-  private String remove;
-  private List<LibraryDownloader> needs;
-  private LibraryDownloader neededBy;
-  private Path dst;
-  private boolean download; // download or copy
-  private boolean exists;
-  private boolean skipDownload;
-
-  @Inject
-  LibraryDownloader(ConsoleUI ui, SitePaths site, StaleLibraryRemover remover) {
-    this.ui = ui;
-    this.lib_dir = site.lib_dir;
-    this.remover = remover;
-    this.needs = new ArrayList<>(2);
-  }
-
-  void setName(String name) {
-    this.name = name;
-  }
-
-  void setJarUrl(String url) {
-    this.jarUrl = url;
-    download = jarUrl.startsWith("http");
-  }
-
-  void setSHA1(String sha1) {
-    this.sha1 = sha1;
-  }
-
-  void setRemove(String remove) {
-    this.remove = remove;
-  }
-
-  void addNeeds(LibraryDownloader lib) {
-    needs.add(lib);
-  }
-
-  void setSkipDownload(boolean skipDownload) {
-    this.skipDownload = skipDownload;
-  }
-
-  void downloadRequired() {
-    setRequired(true);
-    download();
-  }
-
-  void downloadOptional() {
-    required = false;
-    download();
-  }
-
-  private void setRequired(boolean r) {
-    required = r;
-    for (LibraryDownloader d : needs) {
-      d.setRequired(r);
-    }
-  }
-
-  private void download() {
-    if (skipDownload) {
-      return;
-    }
-
-    if (jarUrl == null || !jarUrl.contains("/")) {
-      throw new IllegalStateException("Invalid JarUrl for " + name);
-    }
-
-    final String jarName = jarUrl.substring(jarUrl.lastIndexOf('/') + 1);
-    if (jarName.contains("/") || jarName.contains("\\")) {
-      throw new IllegalStateException("Invalid JarUrl: " + jarUrl);
-    }
-
-    if (name == null) {
-      name = jarName;
-    }
-
-    dst = lib_dir.resolve(jarName);
-    if (Files.exists(dst)) {
-      exists = true;
-    } else if (shouldGet()) {
-      doGet();
-    }
-
-    if (exists) {
-      for (LibraryDownloader d : needs) {
-        d.neededBy = this;
-        d.downloadRequired();
-      }
-    }
-  }
-
-  private boolean shouldGet() {
-    if (ui.isBatch()) {
-      return required;
-    }
-    final StringBuilder msg = new StringBuilder();
-    msg.append("\n");
-    msg.append("Gerrit Code Review is not shipped with %s\n");
-    if (neededBy != null) {
-      msg.append(String.format("** This library is required by %s. **\n", neededBy.name));
-    } else if (required) {
-      msg.append("**  This library is required for your configuration. **\n");
-    } else {
-      msg.append("  If available, Gerrit can take advantage of features\n");
-      msg.append("  in the library, but will also function without it.\n");
-    }
-    msg.append(String.format("%s and install it now", download ? "Download" : "Copy"));
-    return ui.yesno(true, msg.toString(), name);
-  }
-
-  private void doGet() {
-    if (!Files.exists(lib_dir)) {
-      try {
-        Files.createDirectories(lib_dir);
-      } catch (IOException e) {
-        throw new Die("Cannot create " + lib_dir, e);
-      }
-    }
-
-    try {
-      remover.remove(remove);
-      if (download) {
-        doGetByHttp();
-      } else {
-        doGetByLocalCopy();
-      }
-      verifyFileChecksum();
-    } catch (IOException err) {
-      try {
-        Files.delete(dst);
-      } catch (IOException e) {
-        // Delete failed; leave alone.
-      }
-
-      if (ui.isBatch()) {
-        throw new Die("error: Cannot get " + jarUrl, err);
-      }
-
-      System.err.println();
-      System.err.println();
-      System.err.println("error: " + err.getMessage());
-      System.err.println("Please download:");
-      System.err.println();
-      System.err.println("  " + jarUrl);
-      System.err.println();
-      System.err.println("and save as:");
-      System.err.println();
-      System.err.println("  " + dst.toAbsolutePath());
-      System.err.println();
-      System.err.flush();
-
-      ui.waitForUser();
-
-      if (Files.exists(dst)) {
-        verifyFileChecksum();
-
-      } else if (!ui.yesno(!required, "Continue without this library")) {
-        throw new Die("aborted by user");
-      }
-    }
-
-    if (Files.exists(dst)) {
-      exists = true;
-      IoUtil.loadJARs(dst);
-    }
-  }
-
-  private void doGetByLocalCopy() throws IOException {
-    System.err.print("Copying " + jarUrl + " ...");
-    Path p = url2file(jarUrl);
-    if (!Files.exists(p)) {
-      StringBuilder msg =
-          new StringBuilder()
-              .append("\n")
-              .append("Can not find the %s at this location: %s\n")
-              .append("Please provide alternative URL");
-      p = url2file(ui.readString(null, msg.toString(), name, jarUrl));
-    }
-    Files.copy(p, dst);
-  }
-
-  private static Path url2file(String urlString) throws IOException {
-    final URL url = new URL(urlString);
-    try {
-      return Paths.get(url.toURI());
-    } catch (URISyntaxException e) {
-      return Paths.get(url.getPath());
-    }
-  }
-
-  private void doGetByHttp() throws IOException {
-    System.err.print("Downloading " + jarUrl + " ...");
-    System.err.flush();
-    try (InputStream in = openHttpStream(jarUrl);
-        OutputStream out = Files.newOutputStream(dst)) {
-      ByteStreams.copy(in, out);
-      System.err.println(" OK");
-      System.err.flush();
-    } catch (IOException err) {
-      deleteDst();
-      System.err.println(" !! FAIL !!");
-      System.err.println(err);
-      System.err.flush();
-      throw err;
-    }
-  }
-
-  private static InputStream openHttpStream(String urlStr) throws IOException {
-    ProxySelector proxySelector = ProxySelector.getDefault();
-    URL url = new URL(urlStr);
-    Proxy proxy = HttpSupport.proxyFor(proxySelector, url);
-    HttpURLConnection c = (HttpURLConnection) url.openConnection(proxy);
-
-    switch (HttpSupport.response(c)) {
-      case HttpURLConnection.HTTP_OK:
-        return c.getInputStream();
-
-      case HttpURLConnection.HTTP_NOT_FOUND:
-        throw new FileNotFoundException(url.toString());
-
-      default:
-        throw new IOException(
-            url.toString() + ": " + HttpSupport.response(c) + " " + c.getResponseMessage());
-    }
-  }
-
-  @SuppressWarnings("deprecation") // Use Hashing.sha1 for compatibility.
-  private void verifyFileChecksum() {
-    if (sha1 == null) {
-      System.err.println();
-      System.err.flush();
-      return;
-    }
-    Hasher h = Hashing.sha1().newHasher();
-    try (InputStream in = Files.newInputStream(dst);
-        OutputStream out = Funnels.asOutputStream(h)) {
-      ByteStreams.copy(in, out);
-    } catch (IOException e) {
-      deleteDst();
-      throw new Die("cannot checksum " + dst, e);
-    }
-    if (sha1.equals(h.hash().toString())) {
-      System.err.println("Checksum " + dst.getFileName() + " OK");
-      System.err.flush();
-    } else if (ui.isBatch()) {
-      deleteDst();
-      throw new Die(dst + " SHA-1 checksum does not match");
-
-    } else if (!ui.yesno(
-        null /* force an answer */,
-        "error: SHA-1 checksum does not match\nUse %s anyway", //
-        dst.getFileName())) {
-      deleteDst();
-      throw new Die("aborted by user");
-    }
-  }
-
-  private void deleteDst() {
-    try {
-      Files.delete(dst);
-    } catch (IOException e) {
-      System.err.println(" Failed to clean up lib: " + dst);
-    }
-  }
-}
diff --git a/java/com/google/gerrit/pgm/init/MariaDbInitializer.java b/java/com/google/gerrit/pgm/init/MariaDbInitializer.java
deleted file mode 100644
index db32113..0000000
--- a/java/com/google/gerrit/pgm/init/MariaDbInitializer.java
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.pgm.init;
-
-import static com.google.gerrit.pgm.init.api.InitUtil.username;
-
-import com.google.gerrit.pgm.init.api.Section;
-
-class MariaDbInitializer implements DatabaseConfigInitializer {
-
-  @Override
-  public void initConfig(Section databaseSection) {
-    final String defPort = "(mariadb default)";
-    databaseSection.string("Server hostname", "hostname", "localhost");
-    databaseSection.string("Server port", "port", defPort, true);
-    databaseSection.string("Database name", "database", "reviewdb");
-    databaseSection.string("Database username", "username", username());
-    databaseSection.password("username", "password");
-  }
-}
diff --git a/java/com/google/gerrit/pgm/init/MaxDbInitializer.java b/java/com/google/gerrit/pgm/init/MaxDbInitializer.java
deleted file mode 100644
index 0f696b7..0000000
--- a/java/com/google/gerrit/pgm/init/MaxDbInitializer.java
+++ /dev/null
@@ -1,32 +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.pgm.init;
-
-import static com.google.gerrit.pgm.init.api.InitUtil.username;
-
-import com.google.gerrit.pgm.init.api.Section;
-
-public class MaxDbInitializer implements DatabaseConfigInitializer {
-
-  @Override
-  public void initConfig(Section databaseSection) {
-    final String defPort = "(maxdb default)";
-    databaseSection.string("Server hostname", "hostname", "localhost");
-    databaseSection.string("Server port", "port", defPort, true);
-    databaseSection.string("Database name", "database", "reviewdb");
-    databaseSection.string("Database username", "username", username());
-    databaseSection.password("username", "password");
-  }
-}
diff --git a/java/com/google/gerrit/pgm/init/MySqlInitializer.java b/java/com/google/gerrit/pgm/init/MySqlInitializer.java
deleted file mode 100644
index 037b52b..0000000
--- a/java/com/google/gerrit/pgm/init/MySqlInitializer.java
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.pgm.init;
-
-import static com.google.gerrit.pgm.init.api.InitUtil.username;
-
-import com.google.gerrit.pgm.init.api.Section;
-
-class MySqlInitializer implements DatabaseConfigInitializer {
-
-  @Override
-  public void initConfig(Section databaseSection) {
-    final String defPort = "(mysql default)";
-    databaseSection.string("Server hostname", "hostname", "localhost");
-    databaseSection.string("Server port", "port", defPort, true);
-    databaseSection.string("Database name", "database", "reviewdb");
-    databaseSection.string("Database username", "username", username());
-    databaseSection.password("username", "password");
-  }
-}
diff --git a/java/com/google/gerrit/pgm/init/OracleInitializer.java b/java/com/google/gerrit/pgm/init/OracleInitializer.java
deleted file mode 100644
index ffbaf34..0000000
--- a/java/com/google/gerrit/pgm/init/OracleInitializer.java
+++ /dev/null
@@ -1,32 +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.pgm.init;
-
-import static com.google.gerrit.pgm.init.api.InitUtil.username;
-
-import com.google.gerrit.pgm.init.api.Section;
-
-public class OracleInitializer implements DatabaseConfigInitializer {
-
-  @Override
-  public void initConfig(Section databaseSection) {
-    final String defPort = "1521";
-    databaseSection.string("Server hostname", "hostname", "localhost");
-    databaseSection.string("Server port", "port", defPort, false);
-    databaseSection.string("Instance name", "instance", "xe");
-    databaseSection.string("Database username", "username", username());
-    databaseSection.password("username", "password");
-  }
-}
diff --git a/java/com/google/gerrit/pgm/init/PostgreSQLInitializer.java b/java/com/google/gerrit/pgm/init/PostgreSQLInitializer.java
deleted file mode 100644
index 65a66de..0000000
--- a/java/com/google/gerrit/pgm/init/PostgreSQLInitializer.java
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.pgm.init;
-
-import static com.google.gerrit.pgm.init.api.InitUtil.username;
-
-import com.google.gerrit.pgm.init.api.Section;
-
-class PostgreSQLInitializer implements DatabaseConfigInitializer {
-
-  @Override
-  public void initConfig(Section databaseSection) {
-    final String defPort = "(postgresql default)";
-    databaseSection.string("Server hostname", "hostname", "localhost");
-    databaseSection.string("Server port", "port", defPort, true);
-    databaseSection.string("Database name", "database", "reviewdb");
-    databaseSection.string("Database username", "username", username());
-    databaseSection.password("username", "password");
-  }
-}
diff --git a/java/com/google/gerrit/pgm/init/StaleLibraryRemover.java b/java/com/google/gerrit/pgm/init/StaleLibraryRemover.java
index c454cce..d0db8ab 100644
--- a/java/com/google/gerrit/pgm/init/StaleLibraryRemover.java
+++ b/java/com/google/gerrit/pgm/init/StaleLibraryRemover.java
@@ -39,12 +39,7 @@
   public void remove(String pattern) {
     if (!Strings.isNullOrEmpty(pattern)) {
       DirectoryStream.Filter<Path> filter =
-          new DirectoryStream.Filter<Path>() {
-            @Override
-            public boolean accept(Path entry) {
-              return entry.getFileName().toString().matches("^" + pattern + "$");
-            }
-          };
+          entry -> entry.getFileName().toString().matches("^" + pattern + "$");
       try (DirectoryStream<Path> paths = Files.newDirectoryStream(lib_dir, filter)) {
         for (Path p : paths) {
           String old = p.getFileName().toString();
diff --git a/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java b/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java
deleted file mode 100644
index 95ff8d7..0000000
--- a/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java
+++ /dev/null
@@ -1,291 +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.pgm.init;
-
-import static com.google.gerrit.pgm.init.api.InitUtil.die;
-import static com.google.gerrit.pgm.init.api.InitUtil.savePublic;
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.common.base.Splitter;
-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.Section;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.securestore.SecureStore;
-import com.google.gerrit.server.util.SocketUtil;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
-import java.net.InetSocketAddress;
-import java.net.URLDecoder;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.Arrays;
-import java.util.Map;
-import java.util.Properties;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-
-/** Upgrade from a 2.0.x site to a 2.1 site. */
-@Singleton
-class UpgradeFrom2_0_x implements InitStep {
-  static final String[] etcFiles = {
-    "gerrit.config", //
-    "secure.config", //
-    "replication.config", //
-    "ssh_host_rsa_key", //
-    "ssh_host_rsa_key.pub", //
-    "ssh_host_dsa_key", //
-    "ssh_host_dsa_key.pub", //
-    "ssh_host_key", //
-    "contact_information.pub", //
-    "gitweb_config.perl", //
-    "keystore", //
-    "GerritSite.css", //
-    "GerritSiteFooter.html", //
-    "GerritSiteHeader.html", //
-  };
-
-  private final ConsoleUI ui;
-
-  private final FileBasedConfig cfg;
-  private final SecureStore sec;
-  private final Path site_path;
-  private final Path etc_dir;
-  private final Section.Factory sections;
-
-  @Inject
-  UpgradeFrom2_0_x(
-      final SitePaths site,
-      final InitFlags flags,
-      final ConsoleUI ui,
-      final Section.Factory sections) {
-    this.ui = ui;
-    this.sections = sections;
-
-    this.cfg = flags.cfg;
-    this.sec = flags.sec;
-    this.site_path = site.site_path;
-    this.etc_dir = site.etc_dir;
-  }
-
-  boolean isNeedUpgrade() {
-    for (String name : etcFiles) {
-      if (Files.exists(site_path.resolve(name))) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  @Override
-  public void run() throws IOException, ConfigInvalidException {
-    if (!isNeedUpgrade()) {
-      return;
-    }
-
-    if (!ui.yesno(true, "Upgrade '%s'", site_path.toAbsolutePath())) {
-      throw die("aborted by user");
-    }
-
-    for (String name : etcFiles) {
-      Path src = site_path.resolve(name);
-      Path dst = etc_dir.resolve(name);
-      if (Files.exists(src)) {
-        if (Files.exists(dst)) {
-          throw die("File " + src + " would overwrite " + dst);
-        }
-        try {
-          Files.move(src, dst);
-        } catch (IOException e) {
-          throw die("Cannot rename " + src + " to " + dst, e);
-        }
-      }
-    }
-
-    // We have to reload the configuration after the rename as
-    // the initial load pulled up an non-existent (and thus
-    // believed to be empty) file.
-    //
-    cfg.load();
-
-    final Properties oldprop = readGerritServerProperties();
-    if (oldprop != null) {
-      final Section database = sections.get("database", null);
-
-      String url = oldprop.getProperty("url");
-      if (url != null && !convertUrl(database, url)) {
-        database.set("type", "jdbc");
-        database.set("driver", oldprop.getProperty("driver"));
-        database.set("url", url);
-      }
-
-      String username = oldprop.getProperty("user");
-      if (username == null || username.isEmpty()) {
-        username = oldprop.getProperty("username");
-      }
-      if (username != null && !username.isEmpty()) {
-        cfg.setString("database", null, "username", username);
-      }
-
-      String password = oldprop.getProperty("password");
-      if (password != null && !password.isEmpty()) {
-        sec.set("database", null, "password", password);
-      }
-    }
-
-    String[] values;
-
-    values = cfg.getStringList("ldap", null, "password");
-    cfg.unset("ldap", null, "password");
-    sec.setList("ldap", null, "password", Arrays.asList(values));
-
-    values = cfg.getStringList("sendemail", null, "smtpPass");
-    cfg.unset("sendemail", null, "smtpPass");
-    sec.setList("sendemail", null, "smtpPass", Arrays.asList(values));
-
-    savePublic(cfg);
-  }
-
-  private boolean convertUrl(Section database, String url) throws UnsupportedEncodingException {
-    String username = null;
-    String password = null;
-
-    if (url.contains("?")) {
-      final int q = url.indexOf('?');
-      for (String pair : Splitter.on('&').split(url.substring(q + 1))) {
-        final int eq = pair.indexOf('=');
-        if (0 < eq) {
-          return false;
-        }
-
-        String n = URLDecoder.decode(pair.substring(0, eq), UTF_8.name());
-        String v = URLDecoder.decode(pair.substring(eq + 1), UTF_8.name());
-
-        if ("user".equals(n) || "username".equals(n)) {
-          username = v;
-
-        } else if ("password".equals(n)) {
-          password = v;
-
-        } else {
-          // There is a parameter setting we don't recognize, use the
-          // JDBC URL format instead to preserve the configuration.
-          //
-          return false;
-        }
-      }
-      url = url.substring(0, q);
-    }
-
-    if (url.startsWith("jdbc:h2:file:")) {
-      url = url.substring("jdbc:h2:file:".length());
-      database.set("type", "h2");
-      database.set("database", url);
-      return true;
-    }
-
-    if (url.startsWith("jdbc:postgresql://")) {
-      url = url.substring("jdbc:postgresql://".length());
-      final int sl = url.indexOf('/');
-      if (sl < 0) {
-        return false;
-      }
-
-      final InetSocketAddress addr = SocketUtil.parse(url.substring(0, sl), 0);
-      database.set("type", "postgresql");
-      sethost(database, addr);
-      database.set("database", url.substring(sl + 1));
-      setuser(database, username, password);
-      return true;
-    }
-
-    if (url.startsWith("jdbc:postgresql:")) {
-      url = url.substring("jdbc:postgresql:".length());
-      database.set("type", "postgresql");
-      database.set("hostname", "localhost");
-      database.set("database", url);
-      setuser(database, username, password);
-      return true;
-    }
-
-    if (url.startsWith("jdbc:mysql://")) {
-      url = url.substring("jdbc:mysql://".length());
-      final int sl = url.indexOf('/');
-      if (sl < 0) {
-        return false;
-      }
-
-      final InetSocketAddress addr = SocketUtil.parse(url.substring(0, sl), 0);
-      database.set("type", "mysql");
-      sethost(database, addr);
-      database.set("database", url.substring(sl + 1));
-      setuser(database, username, password);
-      return true;
-    }
-
-    return false;
-  }
-
-  private void sethost(Section database, InetSocketAddress addr) {
-    database.set("hostname", SocketUtil.hostname(addr));
-    if (0 < addr.getPort()) {
-      database.set("port", String.valueOf(addr.getPort()));
-    }
-  }
-
-  private void setuser(Section database, String username, String password) {
-    if (username != null && !username.isEmpty()) {
-      database.set("username", username);
-    }
-    if (password != null && !password.isEmpty()) {
-      sec.set("database", null, "password", password);
-    }
-  }
-
-  private Properties readGerritServerProperties() throws IOException {
-    final Properties srvprop = new Properties();
-    final String name = System.getProperty("GerritServer");
-    Path path;
-    if (name != null) {
-      path = Paths.get(name);
-    } else {
-      path = site_path.resolve("GerritServer.properties");
-      if (!Files.exists(path)) {
-        path = Paths.get("GerritServer.properties");
-      }
-    }
-    if (Files.exists(path)) {
-      try (InputStream in = Files.newInputStream(path)) {
-        srvprop.load(in);
-      } catch (IOException e) {
-        throw new IOException("Cannot read " + name, e);
-      }
-      final Properties dbprop = new Properties();
-      for (Map.Entry<Object, Object> e : srvprop.entrySet()) {
-        final String key = (String) e.getKey();
-        if (key.startsWith("database.")) {
-          dbprop.put(key.substring("database.".length()), e.getValue());
-        }
-      }
-      return dbprop;
-    }
-    return null;
-  }
-}
diff --git a/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java b/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
index 9fd3f16..20e7ba2 100644
--- a/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
+++ b/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
@@ -15,8 +15,10 @@
 package com.google.gerrit.pgm.init.api;
 
 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.server.config.AllProjectsName;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.project.GroupList;
 import com.google.gerrit.server.project.ProjectConfig;
@@ -27,16 +29,21 @@
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.RepositoryCache;
+import org.eclipse.jgit.lib.StoredConfig;
 
 public class AllProjectsConfig extends VersionedMetaDataOnInit {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
+  @Nullable private final StoredConfig baseConfig;
   private Config cfg;
   private GroupList groupList;
 
   @Inject
   AllProjectsConfig(AllProjectsNameOnInitProvider allProjects, SitePaths site, InitFlags flags) {
     super(flags, site, allProjects.get(), RefNames.REFS_CONFIG);
+    this.baseConfig =
+        ProjectConfig.Factory.getBaseConfig(
+            site, new AllProjectsName(allProjects.get()), new Project.NameKey(allProjects.get()));
   }
 
   public Config getConfig() {
@@ -55,8 +62,11 @@
 
   @Override
   protected void onLoad() throws IOException, ConfigInvalidException {
+    if (baseConfig != null) {
+      baseConfig.load();
+    }
     groupList = readGroupList();
-    cfg = readConfig(ProjectConfig.PROJECT_CONFIG);
+    cfg = readConfig(ProjectConfig.PROJECT_CONFIG, baseConfig);
   }
 
   private GroupList readGroupList() throws IOException {
diff --git a/java/com/google/gerrit/pgm/init/api/BUILD b/java/com/google/gerrit/pgm/init/api/BUILD
index aa98990..19203fc 100644
--- a/java/com/google/gerrit/pgm/init/api/BUILD
+++ b/java/com/google/gerrit/pgm/init/api/BUILD
@@ -7,10 +7,10 @@
     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/server",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
diff --git a/java/com/google/gerrit/pgm/init/api/ConsoleUI.java b/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
index 444f64f..ea39a44 100644
--- a/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
+++ b/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
@@ -224,7 +224,7 @@
 
     @Override
     public void header(String fmt, Object... args) {
-      fmt = fmt.replaceAll("\n", "\n*** ");
+      fmt = fmt.replace("\n", "\n*** ");
       console.printf("\n*** " + fmt + "\n*** \n\n", args);
     }
 
diff --git a/java/com/google/gerrit/pgm/init/api/InitUtil.java b/java/com/google/gerrit/pgm/init/api/InitUtil.java
index 656f53a..d038de7 100644
--- a/java/com/google/gerrit/pgm/init/api/InitUtil.java
+++ b/java/com/google/gerrit/pgm/init/api/InitUtil.java
@@ -97,7 +97,7 @@
       p = name.indexOf(".");
       if (0 < p) {
         name = name.substring(p + 1);
-        name = "DC=" + name.replaceAll("\\.", ",DC=");
+        name = "DC=" + name.replace(".", ",DC=");
       } else {
         name = null;
       }
diff --git a/java/com/google/gerrit/pgm/init/api/SequencesOnInit.java b/java/com/google/gerrit/pgm/init/api/SequencesOnInit.java
index c9c3a64..d3d22cb 100644
--- a/java/com/google/gerrit/pgm/init/api/SequencesOnInit.java
+++ b/java/com/google/gerrit/pgm/init/api/SequencesOnInit.java
@@ -15,12 +15,10 @@
 package com.google.gerrit.pgm.init.api;
 
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.RepoSequence;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.server.notedb.Sequences;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -35,16 +33,14 @@
     this.allUsersName = allUsersName;
   }
 
-  public int nextAccountId(ReviewDb db) throws OrmException {
-    @SuppressWarnings("deprecation")
-    RepoSequence.Seed accountSeed = db::nextAccountId;
+  public int nextAccountId() {
     RepoSequence accountSeq =
         new RepoSequence(
             repoManager,
             GitReferenceUpdated.DISABLED,
             new Project.NameKey(allUsersName.get()),
             Sequences.NAME_ACCOUNTS,
-            accountSeed,
+            () -> Sequences.FIRST_ACCOUNT_ID,
             1);
     return accountSeq.next();
   }
diff --git a/java/com/google/gerrit/pgm/init/index/IndexModuleOnInit.java b/java/com/google/gerrit/pgm/init/index/IndexModuleOnInit.java
index b417d05..80de1e5 100644
--- a/java/com/google/gerrit/pgm/init/index/IndexModuleOnInit.java
+++ b/java/com/google/gerrit/pgm/init/index/IndexModuleOnInit.java
@@ -30,7 +30,6 @@
 import com.google.gerrit.server.index.account.AccountSchemaDefinitions;
 import com.google.gerrit.server.index.account.AllAccountsIndexer;
 import com.google.gerrit.server.index.group.AllGroupsIndexer;
-import com.google.gerrit.server.index.group.GroupIndexCollection;
 import com.google.gerrit.server.index.group.GroupIndexDefinition;
 import com.google.gerrit.server.index.group.GroupSchemaDefinitions;
 import com.google.inject.AbstractModule;
@@ -47,8 +46,7 @@
   static final String INDEX_MANAGER = "IndexModuleOnInit/IndexManager";
 
   private static final ImmutableCollection<SchemaDefinitions<?>> ALL_SCHEMA_DEFS =
-      ImmutableList.<SchemaDefinitions<?>>of(
-          AccountSchemaDefinitions.INSTANCE, GroupSchemaDefinitions.INSTANCE);
+      ImmutableList.of(AccountSchemaDefinitions.INSTANCE, GroupSchemaDefinitions.INSTANCE);
 
   @Override
   protected void configure() {
@@ -75,11 +73,9 @@
     // during init, hence we don't need AllGroupsIndexer.
     bind(AllGroupsIndexer.class).toProvider(Providers.of(null));
 
-    bind(GroupIndexCollection.class);
-
     bind(new TypeLiteral<Map<String, Integer>>() {})
         .annotatedWith(Names.named(SingleVersionModule.SINGLE_VERSIONS))
-        .toInstance(ImmutableMap.<String, Integer>of());
+        .toInstance(ImmutableMap.of());
     bind(LifecycleListener.class)
         .annotatedWith(Names.named(INDEX_MANAGER))
         .to(SingleVersionListener.class);
@@ -88,8 +84,7 @@
   @Provides
   Collection<IndexDefinition<?, ?, ?>> getIndexDefinitions(
       AccountIndexDefinition accounts, GroupIndexDefinition groups) {
-    Collection<IndexDefinition<?, ?, ?>> result =
-        ImmutableList.<IndexDefinition<?, ?, ?>>of(accounts, groups);
+    Collection<IndexDefinition<?, ?, ?>> result = ImmutableList.of(accounts, groups);
     Set<String> expected =
         FluentIterable.from(ALL_SCHEMA_DEFS).transform(SchemaDefinitions::getName).toSet();
     Set<String> actual = FluentIterable.from(result).transform(IndexDefinition::getName).toSet();
diff --git a/java/com/google/gerrit/pgm/util/AbstractProgram.java b/java/com/google/gerrit/pgm/util/AbstractProgram.java
index fca5551..96b042a 100644
--- a/java/com/google/gerrit/pgm/util/AbstractProgram.java
+++ b/java/com/google/gerrit/pgm/util/AbstractProgram.java
@@ -66,9 +66,9 @@
         final Throwable cause = err.getCause();
         final String diemsg = err.getMessage();
         if (cause != null && !cause.getMessage().equals(diemsg)) {
-          System.err.println("fatal: " + cause.getMessage().replaceAll("\n", "\nfatal: "));
+          System.err.println("fatal: " + cause.getMessage().replace("\n", "\nfatal: "));
         }
-        System.err.println("fatal: " + diemsg.replaceAll("\n", "\nfatal: "));
+        System.err.println("fatal: " + diemsg.replace("\n", "\nfatal: "));
       }
       return 128;
     }
diff --git a/java/com/google/gerrit/pgm/util/BUILD b/java/com/google/gerrit/pgm/util/BUILD
index e31208f..94798f7 100644
--- a/java/com/google/gerrit/pgm/util/BUILD
+++ b/java/com/google/gerrit/pgm/util/BUILD
@@ -21,8 +21,6 @@
         "//java/com/google/gerrit/util/cli",
         "//lib:args4j",
         "//lib:guava",
-        "//lib:gwtorm",
-        "//lib/commons:dbcp",
         "//lib/flogger:api",
         "//lib/guice",
         "//lib/jgit/org.eclipse.jgit:jgit",
diff --git a/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index 540c5f3..956ec75 100644
--- a/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -39,6 +39,7 @@
 import com.google.gerrit.server.cache.CacheRemovalListener;
 import com.google.gerrit.server.cache.h2.H2CacheModule;
 import com.google.gerrit.server.cache.mem.DefaultMemoryCacheModule;
+import com.google.gerrit.server.change.ChangeAttributeFactory;
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.ChangeKindCacheImpl;
 import com.google.gerrit.server.change.MergeabilityCacheImpl;
@@ -48,9 +49,8 @@
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.CanonicalWebUrlProvider;
 import com.google.gerrit.server.config.DefaultUrlFormatter;
-import com.google.gerrit.server.config.DisableReverseDnsLookup;
-import com.google.gerrit.server.config.DisableReverseDnsLookupProvider;
-import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.EnableReverseDnsLookup;
+import com.google.gerrit.server.config.EnableReverseDnsLookupProvider;
 import com.google.gerrit.server.config.GitReceivePackGroups;
 import com.google.gerrit.server.config.GitUploadPackGroups;
 import com.google.gerrit.server.config.SysExecutorModule;
@@ -58,6 +58,7 @@
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.extensions.events.RevisionCreated;
 import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.git.PureRevertCache;
 import com.google.gerrit.server.git.SearchingChangeCacheImpl;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.mail.send.ReplacePatchSetSender;
@@ -73,42 +74,23 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.ChangeQueryProcessor;
 import com.google.gerrit.server.restapi.group.GroupModule;
 import com.google.gerrit.server.rules.DefaultSubmitRule;
 import com.google.gerrit.server.rules.IgnoreSelfApprovalRule;
 import com.google.gerrit.server.rules.PrologModule;
 import com.google.gerrit.server.rules.SubmitRule;
 import com.google.gerrit.server.update.BatchUpdate;
-import com.google.inject.Inject;
-import com.google.inject.Module;
 import com.google.inject.TypeLiteral;
 import com.google.inject.util.Providers;
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
-import org.eclipse.jgit.lib.Config;
 
-/**
- * Module for programs that perform batch operations on a site.
- *
- * <p>Any program that requires this module likely also requires using {@link ThreadLimiter} to
- * limit the number of threads accessing the database concurrently.
- */
+/** Module for programs that perform batch operations on a site. */
 public class BatchProgramModule extends FactoryModule {
-  private final Config cfg;
-  private final Module reviewDbModule;
-
-  @Inject
-  BatchProgramModule(@GerritServerConfig Config cfg, PerThreadReviewDbModule reviewDbModule) {
-    this.cfg = cfg;
-    this.reviewDbModule = reviewDbModule;
-  }
-
   @SuppressWarnings("rawtypes")
   @Override
   protected void configure() {
-    install(reviewDbModule);
     install(new DiffExecutorModule());
     install(new SysExecutorModule());
     install(BatchUpdate.module());
@@ -126,28 +108,25 @@
 
     // We're just running through each change
     // once, so don't worry about cache removal.
-    bind(new TypeLiteral<DynamicSet<CacheRemovalListener>>() {})
-        .toInstance(DynamicSet.<CacheRemovalListener>emptySet());
-    bind(new TypeLiteral<DynamicMap<Cache<?, ?>>>() {})
-        .toInstance(DynamicMap.<Cache<?, ?>>emptyMap());
+    bind(new TypeLiteral<DynamicSet<CacheRemovalListener>>() {}).toInstance(DynamicSet.emptySet());
+    bind(new TypeLiteral<DynamicMap<Cache<?, ?>>>() {}).toInstance(DynamicMap.emptyMap());
     bind(new TypeLiteral<List<CommentLinkInfo>>() {})
         .toProvider(CommentLinkProvider.class)
         .in(SINGLETON);
-    bind(new TypeLiteral<DynamicMap<ChangeQueryProcessor.ChangeAttributeFactory>>() {})
-        .toInstance(DynamicMap.<ChangeQueryProcessor.ChangeAttributeFactory>emptyMap());
+    bind(new TypeLiteral<DynamicSet<ChangeAttributeFactory>>() {})
+        .toInstance(DynamicSet.emptySet());
     bind(new TypeLiteral<DynamicMap<RestView<CommitResource>>>() {})
-        .toInstance(DynamicMap.<RestView<CommitResource>>emptyMap());
+        .toInstance(DynamicMap.emptyMap());
     bind(String.class)
         .annotatedWith(CanonicalWebUrl.class)
         .toProvider(CanonicalWebUrlProvider.class);
     bind(Boolean.class)
-        .annotatedWith(DisableReverseDnsLookup.class)
-        .toProvider(DisableReverseDnsLookupProvider.class)
+        .annotatedWith(EnableReverseDnsLookup.class)
+        .toProvider(EnableReverseDnsLookupProvider.class)
         .in(SINGLETON);
     bind(Realm.class).to(FakeRealm.class);
-    bind(IdentifiedUser.class).toProvider(Providers.<IdentifiedUser>of(null));
-    bind(ReplacePatchSetSender.Factory.class)
-        .toProvider(Providers.<ReplacePatchSetSender.Factory>of(null));
+    bind(IdentifiedUser.class).toProvider(Providers.of(null));
+    bind(ReplacePatchSetSender.Factory.class).toProvider(Providers.of(null));
     bind(CurrentUser.class).to(IdentifiedUser.class);
     factory(MergeUtil.Factory.class);
     factory(PatchSetInserter.Factory.class);
@@ -155,17 +134,17 @@
 
     // As Reindex is a batch program, don't assume the index is available for
     // the change cache.
-    bind(SearchingChangeCacheImpl.class).toProvider(Providers.<SearchingChangeCacheImpl>of(null));
+    bind(SearchingChangeCacheImpl.class).toProvider(Providers.of(null));
 
     bind(new TypeLiteral<ImmutableSet<GroupReference>>() {})
         .annotatedWith(AdministrateServerGroups.class)
-        .toInstance(ImmutableSet.<GroupReference>of());
+        .toInstance(ImmutableSet.of());
     bind(new TypeLiteral<Set<AccountGroup.UUID>>() {})
         .annotatedWith(GitUploadPackGroups.class)
-        .toInstance(Collections.<AccountGroup.UUID>emptySet());
+        .toInstance(Collections.emptySet());
     bind(new TypeLiteral<Set<AccountGroup.UUID>>() {})
         .annotatedWith(GitReceivePackGroups.class)
-        .toInstance(Collections.<AccountGroup.UUID>emptySet());
+        .toInstance(Collections.emptySet());
 
     install(new BatchGitModule());
     install(new DefaultPermissionBackendModule());
@@ -173,7 +152,7 @@
     install(new H2CacheModule());
     install(new ExternalIdModule());
     install(new GroupModule());
-    install(new NoteDbModule(cfg));
+    install(new NoteDbModule());
     install(AccountCacheImpl.module());
     install(GroupCacheImpl.module());
     install(GroupIncludeCacheImpl.module());
@@ -182,6 +161,7 @@
     install(ChangeKindCacheImpl.module());
     install(MergeabilityCacheImpl.module());
     install(TagCache.module());
+    install(PureRevertCache.module());
     factory(CapabilityCollection.Factory.class);
     factory(ChangeData.AssistedFactory.class);
     factory(ProjectState.Factory.class);
@@ -193,8 +173,8 @@
     install(new DefaultSubmitRule.Module());
     install(new IgnoreSelfApprovalRule.Module());
 
-    bind(ChangeJson.Factory.class).toProvider(Providers.<ChangeJson.Factory>of(null));
-    bind(EventUtil.class).toProvider(Providers.<EventUtil>of(null));
+    bind(ChangeJson.Factory.class).toProvider(Providers.of(null));
+    bind(EventUtil.class).toProvider(Providers.of(null));
     bind(GitReferenceUpdated.class).toInstance(GitReferenceUpdated.DISABLED);
     bind(RevisionCreated.class).toInstance(RevisionCreated.DISABLED);
     bind(AccountVisibility.class).toProvider(AccountVisibilityProvider.class).in(SINGLETON);
diff --git a/java/com/google/gerrit/pgm/util/PerThreadReviewDbModule.java b/java/com/google/gerrit/pgm/util/PerThreadReviewDbModule.java
deleted file mode 100644
index bdd939f..0000000
--- a/java/com/google/gerrit/pgm/util/PerThreadReviewDbModule.java
+++ /dev/null
@@ -1,81 +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.pgm.util;
-
-import com.google.gerrit.extensions.events.LifecycleListener;
-import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * Module to bind a single {@link ReviewDb} instance per thread.
- *
- * <p>New instances are opened on demand, but are closed only at shutdown.
- */
-class PerThreadReviewDbModule extends LifecycleModule {
-  private final SchemaFactory<ReviewDb> schema;
-
-  @Inject
-  PerThreadReviewDbModule(SchemaFactory<ReviewDb> schema) {
-    this.schema = schema;
-  }
-
-  @Override
-  protected void configure() {
-    final List<ReviewDb> dbs = Collections.synchronizedList(new ArrayList<ReviewDb>());
-    final ThreadLocal<ReviewDb> localDb = new ThreadLocal<>();
-
-    bind(ReviewDb.class)
-        .toProvider(
-            new Provider<ReviewDb>() {
-              @Override
-              public ReviewDb get() {
-                ReviewDb db = localDb.get();
-                if (db == null) {
-                  try {
-                    db = schema.open();
-                    dbs.add(db);
-                    localDb.set(db);
-                  } catch (OrmException e) {
-                    throw new ProvisionException("unable to open ReviewDb", e);
-                  }
-                }
-                return db;
-              }
-            });
-    listener()
-        .toInstance(
-            new LifecycleListener() {
-              @Override
-              public void start() {
-                // Do nothing.
-              }
-
-              @Override
-              public void stop() {
-                for (ReviewDb db : dbs) {
-                  db.close();
-                }
-              }
-            });
-  }
-}
diff --git a/java/com/google/gerrit/pgm/util/SiteLibraryBasedDataSourceProvider.java b/java/com/google/gerrit/pgm/util/SiteLibraryBasedDataSourceProvider.java
deleted file mode 100644
index 1c7dc52..0000000
--- a/java/com/google/gerrit/pgm/util/SiteLibraryBasedDataSourceProvider.java
+++ /dev/null
@@ -1,56 +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.pgm.util;
-
-import com.google.gerrit.common.SiteLibraryLoaderUtil;
-import com.google.gerrit.metrics.MetricMaker;
-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.schema.DataSourceProvider;
-import com.google.gerrit.server.schema.DataSourceType;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import java.nio.file.Path;
-import javax.sql.DataSource;
-import org.eclipse.jgit.lib.Config;
-
-/** Loads the site library if not yet loaded. */
-@Singleton
-public class SiteLibraryBasedDataSourceProvider extends DataSourceProvider {
-  private final Path libdir;
-  private boolean init;
-
-  @Inject
-  SiteLibraryBasedDataSourceProvider(
-      SitePaths site,
-      @GerritServerConfig Config cfg,
-      MetricMaker metrics,
-      ThreadSettingsConfig tsc,
-      DataSourceProvider.Context ctx,
-      DataSourceType dst) {
-    super(cfg, metrics, tsc, ctx, dst);
-    libdir = site.lib_dir;
-  }
-
-  @Override
-  public synchronized DataSource get() {
-    if (!init) {
-      SiteLibraryLoaderUtil.loadSiteLib(libdir);
-      init = true;
-    }
-    return super.get();
-  }
-}
diff --git a/java/com/google/gerrit/pgm/util/SiteProgram.java b/java/com/google/gerrit/pgm/util/SiteProgram.java
index 5930c6c..eed307f 100644
--- a/java/com/google/gerrit/pgm/util/SiteProgram.java
+++ b/java/com/google/gerrit/pgm/util/SiteProgram.java
@@ -15,57 +15,36 @@
 package com.google.gerrit.pgm.util;
 
 import static com.google.gerrit.server.config.GerritServerConfigModule.getSecureStoreClassName;
-import static com.google.inject.Scopes.SINGLETON;
 import static com.google.inject.Stage.PRODUCTION;
 
 import com.google.gerrit.common.Die;
-import com.google.gerrit.extensions.events.LifecycleListener;
-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;
+import com.google.gerrit.server.LibModuleLoader;
+import com.google.gerrit.server.LibModuleType;
+import com.google.gerrit.server.ModuleOverloader;
 import com.google.gerrit.server.config.GerritRuntime;
-import com.google.gerrit.server.config.GerritServerConfig;
 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.notedb.NotesMigration;
-import com.google.gerrit.server.schema.DataSourceModule;
-import com.google.gerrit.server.schema.DataSourceProvider;
-import com.google.gerrit.server.schema.DataSourceType;
-import com.google.gerrit.server.schema.DatabaseModule;
 import com.google.gerrit.server.schema.SchemaModule;
 import com.google.gerrit.server.securestore.SecureStoreClassName;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.AbstractModule;
-import com.google.inject.Binding;
 import com.google.inject.CreationException;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
-import com.google.inject.Key;
 import com.google.inject.Module;
-import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
-import com.google.inject.TypeLiteral;
-import com.google.inject.name.Named;
-import com.google.inject.name.Names;
 import com.google.inject.spi.Message;
 import com.google.inject.util.Providers;
-import java.lang.annotation.Annotation;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.sql.Connection;
-import java.sql.SQLException;
 import java.util.ArrayList;
 import java.util.List;
-import javax.sql.DataSource;
-import org.eclipse.jgit.lib.Config;
 import org.kohsuke.args4j.Option;
 
 public abstract class SiteProgram extends AbstractProgram {
-  private static final String CONNECTION_ERROR = "Cannot connect to SQL database";
-
   @Option(
       name = "--site-path",
       aliases = {"-d"},
@@ -74,19 +53,12 @@
     sitePath = Paths.get(path).normalize();
   }
 
-  protected Provider<DataSource> dsProvider;
-
   private Path sitePath = Paths.get(".");
 
   protected SiteProgram() {}
 
-  protected SiteProgram(Path sitePath, Provider<DataSource> dsProvider) {
-    this.sitePath = sitePath.normalize();
-    this.dsProvider = dsProvider;
-  }
-
   protected SiteProgram(Path sitePath) {
-    this(sitePath, null);
+    this.sitePath = sitePath.normalize();
   }
 
   /** @return the site path specified on the command line. */
@@ -102,12 +74,12 @@
   }
 
   /** @return provides database connectivity and site path. */
-  protected Injector createDbInjector(DataSourceProvider.Context context) {
-    return createDbInjector(false, context);
+  protected Injector createDbInjector() {
+    return createDbInjector(false);
   }
 
   /** @return provides database connectivity and site path. */
-  protected Injector createDbInjector(boolean enableMetrics, DataSourceProvider.Context context) {
+  protected Injector createDbInjector(boolean enableMetrics) {
     List<Module> modules = new ArrayList<>();
 
     Module sitePathModule =
@@ -134,26 +106,6 @@
           });
     }
 
-    modules.add(
-        new LifecycleModule() {
-          @Override
-          protected void configure() {
-            bind(DataSourceProvider.Context.class).toInstance(context);
-            if (dsProvider != null) {
-              bind(Key.get(DataSource.class, Names.named("ReviewDb")))
-                  .toProvider(dsProvider)
-                  .in(SINGLETON);
-              if (LifecycleListener.class.isAssignableFrom(dsProvider.getClass())) {
-                listener().toInstance((LifecycleListener) dsProvider);
-              }
-            } else {
-              bind(Key.get(DataSource.class, Names.named("ReviewDb")))
-                  .toProvider(SiteLibraryBasedDataSourceProvider.class)
-                  .in(SINGLETON);
-              listener().to(SiteLibraryBasedDataSourceProvider.class);
-            }
-          }
-        });
     Module configModule = new GerritServerConfigModule();
     modules.add(configModule);
     modules.add(
@@ -164,53 +116,19 @@
           }
         });
     Injector cfgInjector = Guice.createInjector(sitePathModule, configModule);
-    Config cfg = cfgInjector.getInstance(Key.get(Config.class, GerritServerConfig.class));
-    String dbType;
-    if (dsProvider != null) {
-      dbType = getDbType(dsProvider);
-    } else {
-      dbType = cfg.getString("database", null, "type");
-    }
 
-    if (dbType == null) {
-      throw new ProvisionException("database.type must be defined");
-    }
-
-    DataSourceType dst =
-        Guice.createInjector(new DataSourceModule(), configModule, sitePathModule)
-            .getInstance(Key.get(DataSourceType.class, Names.named(dbType.toLowerCase())));
-
-    modules.add(
-        new AbstractModule() {
-          @Override
-          protected void configure() {
-            bind(DataSourceType.class).toInstance(dst);
-          }
-        });
-    modules.add(new DatabaseModule());
     modules.add(new SchemaModule());
     modules.add(cfgInjector.getInstance(GitRepositoryManagerModule.class));
-    modules.add(new NotesMigration.Module());
 
     try {
-      return Guice.createInjector(PRODUCTION, modules);
+      return Guice.createInjector(
+          PRODUCTION,
+          ModuleOverloader.override(
+              modules, LibModuleLoader.loadModules(cfgInjector, LibModuleType.DB_MODULE)));
     } catch (CreationException ce) {
       Message first = ce.getErrorMessages().iterator().next();
       Throwable why = first.getCause();
 
-      if (why instanceof SQLException) {
-        throw die(CONNECTION_ERROR, why);
-      }
-      if (why instanceof OrmException
-          && why.getCause() != null
-          && "Unable to determine driver URL".equals(why.getMessage())) {
-        why = why.getCause();
-        if (isCannotCreatePoolException(why)) {
-          throw die(CONNECTION_ERROR, why.getCause());
-        }
-        throw die(CONNECTION_ERROR, why);
-      }
-
       StringBuilder buf = new StringBuilder();
       if (why != null) {
         buf.append(why.getMessage());
@@ -235,46 +153,4 @@
   protected final String getConfiguredSecureStoreClass() {
     return getSecureStoreClassName(sitePath);
   }
-
-  private String getDbType(Provider<DataSource> dsProvider) {
-    String dbProductName;
-    try (Connection conn = dsProvider.get().getConnection()) {
-      dbProductName = conn.getMetaData().getDatabaseProductName().toLowerCase();
-    } catch (SQLException e) {
-      throw new RuntimeException(e);
-    }
-
-    List<Module> modules = new ArrayList<>();
-    modules.add(
-        new AbstractModule() {
-          @Override
-          protected void configure() {
-            bind(Path.class).annotatedWith(SitePath.class).toInstance(getSitePath());
-          }
-        });
-    modules.add(new GerritServerConfigModule());
-    modules.add(new DataSourceModule());
-    Injector i = Guice.createInjector(modules);
-    List<Binding<DataSourceType>> dsTypeBindings =
-        i.findBindingsByType(new TypeLiteral<DataSourceType>() {});
-    for (Binding<DataSourceType> binding : dsTypeBindings) {
-      Annotation annotation = binding.getKey().getAnnotation();
-      if (annotation instanceof Named) {
-        Named named = (Named) annotation;
-        if (named.value().toLowerCase().contains(dbProductName)) {
-          return named.value();
-        }
-      }
-    }
-    throw new IllegalStateException(
-        String.format(
-            "Cannot guess database type from the database product name '%s'", dbProductName));
-  }
-
-  @SuppressWarnings("deprecation")
-  private static boolean isCannotCreatePoolException(Throwable why) {
-    return why instanceof org.apache.commons.dbcp.SQLNestedException
-        && why.getCause() != null
-        && why.getMessage().startsWith("Cannot create PoolableConnectionFactory");
-  }
 }
diff --git a/java/com/google/gerrit/pgm/util/ThreadLimiter.java b/java/com/google/gerrit/pgm/util/ThreadLimiter.java
deleted file mode 100644
index 64f703bd..0000000
--- a/java/com/google/gerrit/pgm/util/ThreadLimiter.java
+++ /dev/null
@@ -1,50 +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.pgm.util;
-
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.config.ThreadSettingsConfig;
-import com.google.gerrit.server.schema.DataSourceType;
-import com.google.inject.Injector;
-import com.google.inject.Key;
-import org.eclipse.jgit.lib.Config;
-
-// TODO(dborowitz): Not necessary once we switch to NoteDb.
-/** Utility to limit threads used by a batch program. */
-public class ThreadLimiter {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
-  public static int limitThreads(Injector dbInjector, int threads) {
-    return limitThreads(
-        dbInjector.getInstance(Key.get(Config.class, GerritServerConfig.class)),
-        dbInjector.getInstance(DataSourceType.class),
-        dbInjector.getInstance(ThreadSettingsConfig.class),
-        threads);
-  }
-
-  private static int limitThreads(
-      Config cfg, DataSourceType dst, ThreadSettingsConfig threadSettingsConfig, int threads) {
-    boolean usePool = cfg.getBoolean("database", "connectionpool", dst.usePool());
-    int poolLimit = threadSettingsConfig.getDatabasePoolLimit();
-    if (usePool && threads > poolLimit) {
-      logger.atWarning().log("Limiting program to %d threads due to database.poolLimit", poolLimit);
-      return poolLimit;
-    }
-    return threads;
-  }
-
-  private ThreadLimiter() {}
-}
diff --git a/java/com/google/gerrit/prettify/BUILD b/java/com/google/gerrit/prettify/BUILD
index 8a11376..76afbe7 100644
--- a/java/com/google/gerrit/prettify/BUILD
+++ b/java/com/google/gerrit/prettify/BUILD
@@ -1,22 +1,4 @@
 load("@rules_java//java:defs.bzl", "java_library")
-load("//tools/bzl:gwt.bzl", "gwt_module")
-
-gwt_module(
-    name = "client",
-    srcs = glob(["common/**/*.java"]),
-    exported_deps = [
-        "//java/com/google/gerrit/extensions:client",
-        "//java/com/google/gerrit/reviewdb:client",
-        "//java/com/google/gwtexpui/safehtml",
-        "//java/org/eclipse/jgit:Edit",
-        "//java/org/eclipse/jgit:client",
-        "//lib:gwtjsonrpc",
-        "//lib:gwtjsonrpc_src",
-    ],
-    gwt_xml = "PrettyFormatter.gwt.xml",
-    visibility = ["//visibility:public"],
-    deps = ["//lib/gwt:user-neverlink"],
-)
 
 java_library(
     name = "server",
@@ -24,9 +6,7 @@
     visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/reviewdb:server",
-        "//java/org/eclipse/jgit:server",
         "//lib:guava",
-        "//lib:gwtjsonrpc",
         "//lib/jgit/org.eclipse.jgit:jgit",
     ],
 )
diff --git a/java/com/google/gerrit/prettify/PrettyFormatter.gwt.xml b/java/com/google/gerrit/prettify/PrettyFormatter.gwt.xml
deleted file mode 100644
index 06035d27..0000000
--- a/java/com/google/gerrit/prettify/PrettyFormatter.gwt.xml
+++ /dev/null
@@ -1,27 +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.
--->
-<module>
-  <replace-with class='com.google.gerrit.prettify.client.PrivateScopeImplIE8'>
-    <when-type-is class='com.google.gerrit.prettify.client.PrivateScopeImpl'/>
-    <any>
-      <when-property-is name="user.agent" value="ie8" />
-    </any>
-  </replace-with>
-
-  <inherits name='com.google.gwt.resources.Resources'/>
-  <inherits name='com.google.gwtexpui.safehtml.SafeHtml'/>
-  <source path='common' />
-</module>
diff --git a/java/com/google/gerrit/prettify/common/EditList.java b/java/com/google/gerrit/prettify/common/EditList.java
index 61c807c..172a346 100644
--- a/java/com/google/gerrit/prettify/common/EditList.java
+++ b/java/com/google/gerrit/prettify/common/EditList.java
@@ -36,10 +36,8 @@
   }
 
   public Iterable<Hunk> getHunks() {
-    return new Iterable<Hunk>() {
-      @Override
-      public Iterator<Hunk> iterator() {
-        return new Iterator<Hunk>() {
+    return () ->
+        new Iterator<Hunk>() {
           private int curIdx;
 
           @Override
@@ -60,8 +58,6 @@
             throw new UnsupportedOperationException();
           }
         };
-      }
-    };
   }
 
   private int findCombinedEnd(int i) {
diff --git a/java/com/google/gerrit/proto/BUILD b/java/com/google/gerrit/proto/BUILD
index 0e9d3cd..98558c5 100644
--- a/java/com/google/gerrit/proto/BUILD
+++ b/java/com/google/gerrit/proto/BUILD
@@ -1,16 +1,10 @@
-load("@rules_java//java:defs.bzl", "java_binary")
+load("@rules_java//java:defs.bzl", "java_library")
 
-java_binary(
-    name = "ProtoGen",
-    srcs = ["ProtoGen.java"],
-    resource_strip_prefix = "resources",
-    resources = ["//resources/com/google/gerrit/proto"],
-    visibility = ["//proto:__pkg__"],
+java_library(
+    name = "proto",
+    srcs = ["Protos.java"],
+    visibility = ["//visibility:public"],
     deps = [
-        "//java/com/google/gerrit/reviewdb:server",
-        "//lib:args4j",
-        "//lib:guava",
-        "//lib:gwtorm",
-        "//lib/jgit/org.eclipse.jgit:jgit",
+        "//lib:protobuf",
     ],
 )
diff --git a/java/com/google/gerrit/proto/ProtoGen.java b/java/com/google/gerrit/proto/ProtoGen.java
deleted file mode 100644
index c130241..0000000
--- a/java/com/google/gerrit/proto/ProtoGen.java
+++ /dev/null
@@ -1,84 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT 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.proto;
-
-import static com.google.common.base.Preconditions.checkState;
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.schema.java.JavaSchemaModel;
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.PrintWriter;
-import java.nio.ByteBuffer;
-import org.eclipse.jgit.internal.storage.file.LockFile;
-import org.eclipse.jgit.util.IO;
-import org.kohsuke.args4j.CmdLineException;
-import org.kohsuke.args4j.CmdLineParser;
-import org.kohsuke.args4j.Option;
-import org.kohsuke.args4j.ParserProperties;
-
-public class ProtoGen {
-  @Option(
-      name = "--output",
-      aliases = {"-o"},
-      required = true,
-      metaVar = "FILE",
-      usage = "File to write .proto into")
-  private File file;
-
-  public static void main(String[] argv) throws Exception {
-    System.exit(new ProtoGen().run(argv));
-  }
-
-  private int run(String[] argv) throws Exception {
-    CmdLineParser parser = new CmdLineParser(this, ParserProperties.defaults().withAtSyntax(false));
-    try {
-      parser.parseArgument(argv);
-    } catch (CmdLineException e) {
-      System.err.println(e.getMessage());
-      System.err.println(getClass().getSimpleName() + " -o output.proto");
-      parser.printUsage(System.err);
-      return 1;
-    }
-
-    LockFile lock = new LockFile(file.getAbsoluteFile());
-    checkState(lock.lock(), "cannot lock %s", file);
-    try {
-      JavaSchemaModel jsm = new JavaSchemaModel(ReviewDb.class);
-      try (OutputStream o = lock.getOutputStream();
-          PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(o, UTF_8)))) {
-        String header;
-        try (InputStream in = getClass().getResourceAsStream("ProtoGenHeader.txt")) {
-          ByteBuffer buf = IO.readWholeStream(in, 1024);
-          int ptr = buf.arrayOffset() + buf.position();
-          int len = buf.remaining();
-          header = new String(buf.array(), ptr, len, UTF_8);
-        }
-
-        out.write(header);
-        jsm.generateProto(out);
-        out.flush();
-      }
-      checkState(lock.commit(), "Could not write to %s", file);
-    } finally {
-      lock.unlock();
-    }
-    return 0;
-  }
-}
diff --git a/java/com/google/gerrit/proto/Protos.java b/java/com/google/gerrit/proto/Protos.java
new file mode 100644
index 0000000..b4f0b55
--- /dev/null
+++ b/java/com/google/gerrit/proto/Protos.java
@@ -0,0 +1,119 @@
+// 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.proto;
+
+import com.google.protobuf.ByteString;
+import com.google.protobuf.CodedOutputStream;
+import com.google.protobuf.MessageLite;
+import com.google.protobuf.Parser;
+import java.io.IOException;
+
+/** Static utilities for dealing with protobuf-based objects. */
+public class Protos {
+  /**
+   * Serializes a proto to a byte array.
+   *
+   * <p>Guarantees deterministic serialization. No matter whether the use case cares about
+   * determinism or not, always use this method in preference to {@link MessageLite#toByteArray()},
+   * which is not guaranteed deterministic.
+   *
+   * @param message the proto message to serialize.
+   * @return a byte array with the message contents.
+   */
+  public static byte[] toByteArray(MessageLite message) {
+    byte[] bytes = new byte[message.getSerializedSize()];
+    CodedOutputStream cout = CodedOutputStream.newInstance(bytes);
+    cout.useDeterministicSerialization();
+    try {
+      message.writeTo(cout);
+      cout.checkNoSpaceLeft();
+      return bytes;
+    } catch (IOException e) {
+      throw new IllegalStateException("exception writing to byte array", e);
+    }
+  }
+
+  /**
+   * Serializes a proto to a {@code ByteString}.
+   *
+   * <p>Guarantees deterministic serialization. No matter whether the use case cares about
+   * determinism or not, always use this method in preference to {@link MessageLite#toByteString()},
+   * which is not guaranteed deterministic.
+   *
+   * @param message the proto message to serialize
+   * @return a {@code ByteString} with the message contents
+   */
+  public static ByteString toByteString(MessageLite message) {
+    try (ByteString.Output bout = ByteString.newOutput(message.getSerializedSize())) {
+      CodedOutputStream outputStream = CodedOutputStream.newInstance(bout);
+      outputStream.useDeterministicSerialization();
+      message.writeTo(outputStream);
+      outputStream.flush();
+      return bout.toByteString();
+    } catch (IOException e) {
+      throw new IllegalStateException("exception writing to ByteString", e);
+    }
+  }
+
+  /**
+   * Parses a byte array to a protobuf message.
+   *
+   * @param parser parser for the proto type.
+   * @param in byte array with the message contents.
+   * @return parsed proto.
+   */
+  public static <M extends MessageLite> M parseUnchecked(Parser<M> parser, byte[] in) {
+    try {
+      return parser.parseFrom(in);
+    } catch (IOException e) {
+      throw new IllegalArgumentException("exception parsing byte array to proto", e);
+    }
+  }
+
+  /**
+   * Parses a specific segment of a byte array to a protobuf message.
+   *
+   * @param parser parser for the proto type
+   * @param in byte array with the message contents
+   * @param offset offset in the byte array to start reading from
+   * @param length amount of read bytes
+   * @return parsed proto
+   */
+  public static <M extends MessageLite> M parseUnchecked(
+      Parser<M> parser, byte[] in, int offset, int length) {
+    try {
+      return parser.parseFrom(in, offset, length);
+    } catch (IOException e) {
+      throw new IllegalArgumentException("exception parsing byte array to proto", e);
+    }
+  }
+
+  /**
+   * Parses a {@code ByteString} to a protobuf message.
+   *
+   * @param parser parser for the proto type
+   * @param byteString {@code ByteString} with the message contents
+   * @return parsed proto
+   */
+  public static <M extends MessageLite> M parseUnchecked(Parser<M> parser, ByteString byteString) {
+    try {
+      return parser.parseFrom(byteString);
+    } catch (IOException e) {
+      throw new IllegalArgumentException("exception parsing ByteString to proto", e);
+    }
+  }
+
+  private Protos() {}
+}
diff --git a/java/com/google/gerrit/proto/testing/BUILD b/java/com/google/gerrit/proto/testing/BUILD
new file mode 100644
index 0000000..acfa8f0
--- /dev/null
+++ b/java/com/google/gerrit/proto/testing/BUILD
@@ -0,0 +1,15 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
+package(default_testonly = True)
+
+java_library(
+    name = "testing",
+    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
new file mode 100644
index 0000000..546ff89
--- /dev/null
+++ b/java/com/google/gerrit/proto/testing/SerializedClassSubject.java
@@ -0,0 +1,110 @@
+// 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.proto.testing;
+
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
+import static com.google.common.truth.Fact.simpleFact;
+import static com.google.common.truth.Truth.assertAbout;
+
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.Subject;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Type;
+import java.util.Arrays;
+import java.util.Map;
+import org.apache.commons.lang3.reflect.FieldUtils;
+
+/**
+ * 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
+ * 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
+ * implementation.
+ *
+ * <p>Changing fields of a serialized class (or abstract methods, in the case of {@code @AutoValue}
+ * classes) will likely require changes to the serializer implementation, and may require bumping
+ * the {@link com.google.gerrit.server.cache.PersistentCacheBinding#version(int) version} in the
+ * cache binding, in case the representation has changed in such a way that old serialized data
+ * becomes unreadable.
+ *
+ * <p>Changes to a serialized class such as adding or removing fields generally requires a change to
+ * 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 static SerializedClassSubject assertThatSerializedClass(Class<?> actual) {
+    // This formulation fails in Eclipse 4.7.3a with "The type
+    // SerializedClassSubject does not define SerializedClassSubject() that is
+    // applicable here", due to
+    // https://bugs.eclipse.org/bugs/show_bug.cgi?id=534694 or a similar bug:
+    // return assertAbout(SerializedClassSubject::new).that(actual);
+    Subject.Factory<SerializedClassSubject, Class<?>> factory =
+        (m, a) -> new SerializedClassSubject(m, a);
+    return assertAbout(factory).that(actual);
+  }
+
+  private SerializedClassSubject(FailureMetadata metadata, Class<?> actual) {
+    super(metadata, actual);
+  }
+
+  public void isAbstract() {
+    isNotNull();
+    if (!Modifier.isAbstract(actual().getModifiers())) {
+      failWithActual(simpleFact("expected class to be abstract"));
+    }
+  }
+
+  public void isConcrete() {
+    isNotNull();
+    if (Modifier.isAbstract(actual().getModifiers())) {
+      failWithActual(simpleFact("expected class to be concrete"));
+    }
+  }
+
+  public void hasFields(Map<String, Type> expectedFields) {
+    isConcrete();
+    check("fields()")
+        .that(
+            FieldUtils.getAllFieldsList(actual()).stream()
+                .filter(f -> !Modifier.isStatic(f.getModifiers()))
+                .collect(toImmutableMap(Field::getName, Field::getGenericType)))
+        .containsExactlyEntriesIn(expectedFields);
+  }
+
+  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())
+        .that(
+            Arrays.stream(actual().getDeclaredMethods())
+                .filter(m -> !Modifier.isStatic(m.getModifiers()))
+                .filter(m -> Modifier.isAbstract(m.getModifiers()))
+                .filter(m -> m.getParameters().length == 0)
+                .collect(toImmutableMap(Method::getName, Method::getGenericReturnType)))
+        .isEqualTo(expectedMethods);
+  }
+
+  public void extendsClass(Type superclassType) {
+    isNotNull();
+    check("superclass(%s)", actual().getName())
+        .that(actual().getGenericSuperclass())
+        .isEqualTo(superclassType);
+  }
+}
diff --git a/java/com/google/gerrit/reviewdb/BUILD b/java/com/google/gerrit/reviewdb/BUILD
index 05fad66..3bc6528 100644
--- a/java/com/google/gerrit/reviewdb/BUILD
+++ b/java/com/google/gerrit/reviewdb/BUILD
@@ -1,29 +1,18 @@
 load("@rules_java//java:defs.bzl", "java_library")
-load("//tools/bzl:gwt.bzl", "gwt_module")
 
 package(
     default_visibility = ["//visibility:public"],
 )
 
-gwt_module(
-    name = "client",
-    srcs = glob(["client/**/*.java"]),
-    gwt_xml = "ReviewDB.gwt.xml",
-    deps = [
-        "//java/com/google/gerrit/extensions:client",
-        "//lib:gwtorm-client",
-        "//lib:gwtorm-client_src",
-    ],
-)
-
 java_library(
     name = "server",
     srcs = glob(["**/*.java"]),
-    resource_strip_prefix = "resources",
-    resources = ["//resources/com/google/gerrit/reviewdb"],
     deps = [
+        "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/extensions:api",
+        "//java/com/google/gwtorm",
         "//lib:guava",
-        "//lib:gwtorm",
+        "//lib:protobuf",
+        "//proto:entities_java_proto",
     ],
 )
diff --git a/java/com/google/gerrit/reviewdb/ReviewDB.gwt.xml b/java/com/google/gerrit/reviewdb/ReviewDB.gwt.xml
deleted file mode 100644
index 4402826..0000000
--- a/java/com/google/gerrit/reviewdb/ReviewDB.gwt.xml
+++ /dev/null
@@ -1,18 +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.
--->
-<module>
-  <inherits name='com.google.gwtorm.GWTORM'/>
-</module>
diff --git a/java/com/google/gerrit/reviewdb/client/Account.java b/java/com/google/gerrit/reviewdb/client/Account.java
index 717090e..6a26f62 100644
--- a/java/com/google/gerrit/reviewdb/client/Account.java
+++ b/java/com/google/gerrit/reviewdb/client/Account.java
@@ -18,8 +18,8 @@
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_STARRED_CHANGES;
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_USERS;
 
+import com.google.common.primitives.Ints;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
-import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.IntKey;
 import java.sql.Timestamp;
 import java.util.Optional;
@@ -46,18 +46,14 @@
  * </ul>
  */
 public final class Account {
-  /**
-   * Key local to Gerrit to identify a user.
-   *
-   * <p>Fields in this type must be annotated with {@link Column} so that account IDs can be
-   * converted into protos (protobuf requires the {@link Column} annotations for decoding/encoding).
-   * We need to be able to store account IDs as protos because we store change protos in the change
-   * index and a change references account IDs for the change owner and the assignee.
-   */
+  public static Id id(int id) {
+    return new 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;
 
-    @Column(id = 1)
     protected int id;
 
     protected Id() {}
@@ -78,11 +74,7 @@
 
     /** Parse an Account.Id out of a string representation. */
     public static Optional<Id> tryParse(String str) {
-      try {
-        return Optional.of(new Id(Integer.parseInt(str)));
-      } catch (NumberFormatException e) {
-        return Optional.empty();
-      }
+      return Optional.ofNullable(Ints.tryParse(str)).map(Id::new);
     }
 
     public static Id fromRef(String name) {
@@ -151,10 +143,7 @@
   /** The user-settable status of this account (e.g. busy, OOO, available) */
   private String status;
 
-  /**
-   * ID of the user branch from which the account was read, {@code null} if the account was read
-   * from ReviewDb.
-   */
+  /** ID of the user branch from which the account was read. */
   private String metaId;
 
   protected Account() {}
@@ -162,7 +151,7 @@
   /**
    * Create a new account.
    *
-   * @param newId unique id, see {@link com.google.gerrit.server.Sequences#nextAccountId()}.
+   * @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) {
diff --git a/java/com/google/gerrit/reviewdb/client/AccountGroup.java b/java/com/google/gerrit/reviewdb/client/AccountGroup.java
index c7dc420..0db7bbd 100644
--- a/java/com/google/gerrit/reviewdb/client/AccountGroup.java
+++ b/java/com/google/gerrit/reviewdb/client/AccountGroup.java
@@ -14,10 +14,11 @@
 
 package com.google.gerrit.reviewdb.client;
 
-import com.google.gwtorm.client.Column;
+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;
 
 /** Named group of one or more accounts, typically used for access controls. */
@@ -26,19 +27,20 @@
    * Time when the audit subsystem was implemented, used as the default value for {@link #createdOn}
    * when one couldn't be determined from the audit log.
    */
-  // Can't use Instant here because GWT. This is verified against a readable time in the tests,
-  // which don't need to compile under GWT.
-  private static final long AUDIT_CREATION_INSTANT_MS = 1244489460000L;
+  private static final Instant AUDIT_CREATION_INSTANT_MS = Instant.ofEpochMilli(1244489460000L);
 
   public static Timestamp auditCreationInstantTs() {
-    return new Timestamp(AUDIT_CREATION_INSTANT_MS);
+    return Timestamp.from(AUDIT_CREATION_INSTANT_MS);
+  }
+
+  public static NameKey nameKey(String n) {
+    return new NameKey(n);
   }
 
   /** Group name key */
   public static class NameKey extends StringKey<com.google.gwtorm.client.Key<?>> {
     private static final long serialVersionUID = 1L;
 
-    @Column(id = 1)
     protected String name;
 
     protected NameKey() {}
@@ -58,11 +60,14 @@
     }
   }
 
+  public static UUID uuid(String n) {
+    return new UUID(n);
+  }
+
   /** Globally unique identifier. */
   public static class UUID extends StringKey<com.google.gwtorm.client.Key<?>> {
     private static final long serialVersionUID = 1L;
 
-    @Column(id = 1)
     protected String uuid;
 
     protected UUID() {}
@@ -116,11 +121,14 @@
     return uuid.get().matches("^[0-9a-f]{40}$");
   }
 
+  public static Id id(int id) {
+    return new 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;
 
-    @Column(id = 1)
     protected int id;
 
     protected Id() {}
@@ -148,29 +156,24 @@
   }
 
   /** Unique name of this group within the system. */
-  @Column(id = 1)
   protected NameKey name;
 
   /** Unique identity, to link entities as {@link #name} can change. */
-  @Column(id = 2)
   protected Id groupId;
 
   // DELETED: id = 3 (ownerGroupId)
 
   /** A textual description of the group's purpose. */
-  @Column(id = 4, length = Integer.MAX_VALUE, notNull = false)
-  protected String description;
+  @Nullable protected String description;
 
   // DELETED: id = 5 (groupType)
   // DELETED: id = 6 (externalName)
 
-  @Column(id = 7)
   protected boolean visibleToAll;
 
   // DELETED: id = 8 (emailOnlyAuthors)
 
   /** Globally unique identifier name for this group. */
-  @Column(id = 9)
   protected UUID groupUUID;
 
   /**
@@ -178,11 +181,9 @@
    *
    * <p>This can be a self-reference to indicate the group's members manage itself.
    */
-  @Column(id = 10)
   protected UUID ownerGroupUUID;
 
-  @Column(id = 11, notNull = false)
-  protected Timestamp createdOn;
+  @Nullable protected Timestamp createdOn;
 
   protected AccountGroup() {}
 
diff --git a/java/com/google/gerrit/reviewdb/client/AccountGroupById.java b/java/com/google/gerrit/reviewdb/client/AccountGroupById.java
index 17a205e..578865c 100644
--- a/java/com/google/gerrit/reviewdb/client/AccountGroupById.java
+++ b/java/com/google/gerrit/reviewdb/client/AccountGroupById.java
@@ -14,19 +14,20 @@
 
 package com.google.gerrit.reviewdb.client;
 
-import com.google.gwtorm.client.Column;
 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;
 
-    @Column(id = 1)
     protected AccountGroup.Id groupId;
 
-    @Column(id = 2)
     protected AccountGroup.UUID includeUUID;
 
     protected Key() {
@@ -48,17 +49,24 @@
       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};
     }
   }
 
-  @Column(id = 1, name = Column.NONE)
   protected Key key;
 
   protected AccountGroupById() {}
diff --git a/java/com/google/gerrit/reviewdb/client/AccountGroupByIdAud.java b/java/com/google/gerrit/reviewdb/client/AccountGroupByIdAud.java
index 759e4f6..308e1e1 100644
--- a/java/com/google/gerrit/reviewdb/client/AccountGroupByIdAud.java
+++ b/java/com/google/gerrit/reviewdb/client/AccountGroupByIdAud.java
@@ -14,23 +14,24 @@
 
 package com.google.gerrit.reviewdb.client;
 
-import com.google.gwtorm.client.Column;
+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;
 
-    @Column(id = 1)
     protected AccountGroup.Id groupId;
 
-    @Column(id = 2)
     protected AccountGroup.UUID includeUUID;
 
-    @Column(id = 3)
     protected Timestamp addedOn;
 
     protected Key() {
@@ -49,14 +50,26 @@
       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};
@@ -75,17 +88,13 @@
     }
   }
 
-  @Column(id = 1, name = Column.NONE)
   protected Key key;
 
-  @Column(id = 2)
   protected Account.Id addedBy;
 
-  @Column(id = 3, notNull = false)
-  protected Account.Id removedBy;
+  @Nullable protected Account.Id removedBy;
 
-  @Column(id = 4, notNull = false)
-  protected Timestamp removedOn;
+  @Nullable protected Timestamp removedOn;
 
   protected AccountGroupByIdAud() {}
 
diff --git a/java/com/google/gerrit/reviewdb/client/AccountGroupMember.java b/java/com/google/gerrit/reviewdb/client/AccountGroupMember.java
index e1e0754..dfa7d24 100644
--- a/java/com/google/gerrit/reviewdb/client/AccountGroupMember.java
+++ b/java/com/google/gerrit/reviewdb/client/AccountGroupMember.java
@@ -14,19 +14,20 @@
 
 package com.google.gerrit.reviewdb.client;
 
-import com.google.gwtorm.client.Column;
 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;
 
-    @Column(id = 1)
     protected Account.Id accountId;
 
-    @Column(id = 2)
     protected AccountGroup.Id groupId;
 
     protected Key() {
@@ -44,17 +45,24 @@
       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};
     }
   }
 
-  @Column(id = 1, name = Column.NONE)
   protected Key key;
 
   protected AccountGroupMember() {}
diff --git a/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java b/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java
index fc7b2d8..5d43b4a 100644
--- a/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java
+++ b/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java
@@ -14,23 +14,24 @@
 
 package com.google.gerrit.reviewdb.client;
 
-import com.google.gwtorm.client.Column;
+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;
 
-    @Column(id = 1)
     protected Account.Id accountId;
 
-    @Column(id = 2)
     protected AccountGroup.Id groupId;
 
-    @Column(id = 3)
     protected Timestamp addedOn;
 
     protected Key() {
@@ -49,14 +50,26 @@
       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};
@@ -75,17 +88,13 @@
     }
   }
 
-  @Column(id = 1, name = Column.NONE)
   protected Key key;
 
-  @Column(id = 2)
   protected Account.Id addedBy;
 
-  @Column(id = 3, notNull = false)
-  protected Account.Id removedBy;
+  @Nullable protected Account.Id removedBy;
 
-  @Column(id = 4, notNull = false)
-  protected Timestamp removedOn;
+  @Nullable protected Timestamp removedOn;
 
   protected AccountGroupMemberAudit() {}
 
diff --git a/java/com/google/gerrit/reviewdb/client/AccountGroupName.java b/java/com/google/gerrit/reviewdb/client/AccountGroupName.java
deleted file mode 100644
index 924f457..0000000
--- a/java/com/google/gerrit/reviewdb/client/AccountGroupName.java
+++ /dev/null
@@ -1,53 +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.gwtorm.client.Column;
-
-/** Unique name of an {@link AccountGroup}. */
-public class AccountGroupName {
-  @Column(id = 1)
-  protected AccountGroup.NameKey name;
-
-  @Column(id = 2)
-  protected AccountGroup.Id groupId;
-
-  protected AccountGroupName() {}
-
-  public AccountGroupName(AccountGroup.NameKey name, AccountGroup.Id groupId) {
-    this.name = name;
-    this.groupId = groupId;
-  }
-
-  public AccountGroupName(AccountGroup group) {
-    this(group.getNameKey(), group.getId());
-  }
-
-  public String getName() {
-    return getNameKey().get();
-  }
-
-  public AccountGroup.NameKey getNameKey() {
-    return name;
-  }
-
-  public AccountGroup.Id getId() {
-    return groupId;
-  }
-
-  public void setId(AccountGroup.Id id) {
-    groupId = id;
-  }
-}
diff --git a/java/com/google/gerrit/reviewdb/client/Branch.java b/java/com/google/gerrit/reviewdb/client/Branch.java
index fd8bbfd..4ea49b7 100644
--- a/java/com/google/gerrit/reviewdb/client/Branch.java
+++ b/java/com/google/gerrit/reviewdb/client/Branch.java
@@ -14,19 +14,24 @@
 
 package com.google.gerrit.reviewdb.client;
 
-import com.google.gwtorm.client.Column;
 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;
 
-    @Column(id = 1)
     protected Project.NameKey projectName;
 
-    @Column(id = 2)
     protected String branchName;
 
     protected NameKey() {
@@ -47,6 +52,10 @@
       return branchName;
     }
 
+    public String branch() {
+      return get();
+    }
+
     @Override
     protected void set(String newValue) {
       branchName = RefNames.fullName(newValue);
@@ -57,6 +66,10 @@
       return projectName;
     }
 
+    public Project.NameKey project() {
+      return getParentKey();
+    }
+
     public String getShortName() {
       return RefNames.shortName(get());
     }
diff --git a/java/com/google/gerrit/reviewdb/client/Change.java b/java/com/google/gerrit/reviewdb/client/Change.java
index 8d4de05..79739dc 100644
--- a/java/com/google/gerrit/reviewdb/client/Change.java
+++ b/java/com/google/gerrit/reviewdb/client/Change.java
@@ -16,10 +16,9 @@
 
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_CHANGES;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.ChangeStatus;
-import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.IntKey;
-import com.google.gwtorm.client.RowVersion;
 import com.google.gwtorm.client.StringKey;
 import java.sql.Timestamp;
 import java.util.Arrays;
@@ -94,10 +93,13 @@
  * 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);
+  }
+
   public static class Id extends IntKey<com.google.gwtorm.client.Key<?>> {
     private static final long serialVersionUID = 1L;
 
-    @Column(id = 1)
     public int id;
 
     protected Id() {}
@@ -253,6 +255,10 @@
     }
   }
 
+  public static Key key(String key) {
+    return new 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.
@@ -260,7 +266,6 @@
   public static class Key extends StringKey<com.google.gwtorm.client.Key<?>> {
     private static final long serialVersionUID = 1L;
 
-    @Column(id = 1, length = 60)
     protected String id;
 
     protected Key() {}
@@ -428,20 +433,15 @@
   }
 
   /** Locally assigned unique identifier of the change */
-  @Column(id = 1)
   protected Id changeId;
 
   /** Globally assigned unique identifier of the change */
-  @Column(id = 2)
   protected Key changeKey;
 
   /** optimistic locking */
-  @Column(id = 3)
-  @RowVersion
   protected int rowVersion;
 
   /** When this change was first introduced into the database. */
-  @Column(id = 4)
   protected Timestamp createdOn;
 
   /**
@@ -449,37 +449,30 @@
    *
    * <p>Note, this update timestamp includes its children.
    */
-  @Column(id = 5)
   protected Timestamp lastUpdatedOn;
 
   // DELETED: id = 6 (sortkey)
 
-  @Column(id = 7, name = "owner_account_id")
   protected Account.Id owner;
 
   /** The branch (and project) this change merges into. */
-  @Column(id = 8)
   protected Branch.NameKey dest;
 
   // DELETED: id = 9 (open)
 
   /** Current state code; see {@link Status}. */
-  @Column(id = 10)
   protected char status;
 
   // DELETED: id = 11 (nbrPatchSets)
 
   /** The current patch set. */
-  @Column(id = 12)
   protected int currentPatchSetId;
 
   /** Subject from the current patch set. */
-  @Column(id = 13)
   protected String subject;
 
   /** Topic name assigned by the user, if any. */
-  @Column(id = 14, notNull = false)
-  protected String topic;
+  @Nullable protected String topic;
 
   // DELETED: id = 15 (lastSha1MergeTested)
   // DELETED: id = 16 (mergeable)
@@ -490,39 +483,28 @@
    * <p>Unlike {@link #subject}, this string does not change if future patch sets change the first
    * line.
    */
-  @Column(id = 17, notNull = false)
-  protected String originalSubject;
+  @Nullable protected String originalSubject;
 
   /**
    * Unique id for the changes submitted together assigned during merging. Only set if the status is
    * MERGED.
    */
-  @Column(id = 18, notNull = false)
-  protected String submissionId;
+  @Nullable protected String submissionId;
 
   /** Allows assigning a change to a user. */
-  @Column(id = 19, notNull = false)
-  protected Account.Id assignee;
+  @Nullable protected Account.Id assignee;
 
   /** Whether the change is private. */
-  @Column(id = 20)
   protected boolean isPrivate;
 
   /** Whether the change is work in progress. */
-  @Column(id = 21)
   protected boolean workInProgress;
 
   /** Whether the change has started review. */
-  @Column(id = 22)
   protected boolean reviewStarted;
 
   /** References a change that this change reverts. */
-  @Column(id = 23, notNull = false)
-  protected Id revertOf;
-
-  /** @see com.google.gerrit.server.notedb.NoteDbChangeState */
-  @Column(id = 101, notNull = false, length = Integer.MAX_VALUE)
-  protected String noteDbState;
+  @Nullable protected Id revertOf;
 
   protected Change() {}
 
@@ -559,7 +541,6 @@
     isPrivate = other.isPrivate;
     workInProgress = other.workInProgress;
     reviewStarted = other.reviewStarted;
-    noteDbState = other.noteDbState;
     revertOf = other.revertOf;
   }
 
@@ -698,6 +679,22 @@
     status = newStatus.getCode();
   }
 
+  public boolean isNew() {
+    return getStatus().equals(Status.NEW);
+  }
+
+  public boolean isMerged() {
+    return getStatus().equals(Status.MERGED);
+  }
+
+  public boolean isAbandoned() {
+    return getStatus().equals(Status.ABANDONED);
+  }
+
+  public boolean isClosed() {
+    return isAbandoned() || isMerged();
+  }
+
   public String getTopic() {
     return topic;
   }
@@ -738,14 +735,6 @@
     return this.revertOf;
   }
 
-  public String getNoteDbState() {
-    return noteDbState;
-  }
-
-  public void setNoteDbState(String state) {
-    noteDbState = state;
-  }
-
   @Override
   public String toString() {
     return new StringBuilder(getClass().getSimpleName())
diff --git a/java/com/google/gerrit/reviewdb/client/ChangeMessage.java b/java/com/google/gerrit/reviewdb/client/ChangeMessage.java
index 8e397f0..de318bd 100644
--- a/java/com/google/gerrit/reviewdb/client/ChangeMessage.java
+++ b/java/com/google/gerrit/reviewdb/client/ChangeMessage.java
@@ -14,20 +14,22 @@
 
 package com.google.gerrit.reviewdb.client;
 
-import com.google.gwtorm.client.Column;
+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);
+  }
+
   public static class Key extends StringKey<Change.Id> {
     private static final long serialVersionUID = 1L;
 
-    @Column(id = 1)
     protected Change.Id changeId;
 
-    @Column(id = 2, length = 40)
     protected String uuid;
 
     protected Key() {
@@ -44,43 +46,44 @@
       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;
     }
   }
 
-  @Column(id = 1, name = Column.NONE)
   protected Key key;
 
   /** Who wrote this comment; null if it was written by the Gerrit system. */
-  @Column(id = 2, name = "author_id", notNull = false)
-  protected Account.Id author;
+  @Nullable protected Account.Id author;
 
   /** When this comment was drafted. */
-  @Column(id = 3)
   protected Timestamp writtenOn;
 
   /** The text left by the user. */
-  @Column(id = 4, notNull = false, length = Integer.MAX_VALUE)
-  protected String message;
+  @Nullable protected String message;
 
   /** Which patchset (if any) was this message generated from? */
-  @Column(id = 5, notNull = false)
-  protected PatchSet.Id patchset;
+  @Nullable protected PatchSet.Id patchset;
 
   /** Tag associated with change message */
-  @Column(id = 6, notNull = false)
-  protected String tag;
+  @Nullable protected String tag;
 
   /** Real user that added this message on behalf of the user recorded in {@link #author}. */
-  @Column(id = 7, notNull = false)
-  protected Account.Id realAuthor;
+  @Nullable protected Account.Id realAuthor;
 
   protected ChangeMessage() {}
 
diff --git a/java/com/google/gerrit/reviewdb/client/Comment.java b/java/com/google/gerrit/reviewdb/client/Comment.java
index 207643e..e03d0fa 100644
--- a/java/com/google/gerrit/reviewdb/client/Comment.java
+++ b/java/com/google/gerrit/reviewdb/client/Comment.java
@@ -25,7 +25,7 @@
  * <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} also represents inline comments, but in ReviewDb. There are a few
+ * <p>{@link PatchLineComment} historically represented comments in ReviewDb. There are a few
  * notable differences:
  *
  * <ul>
@@ -41,9 +41,9 @@
  * </ul>
  *
  * <p>For all utility classes and middle layer functionality using Comment over PatchLineComment is
- * preferred, as PatchLineComment will go away together with ReviewDb. 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
+ * 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).
  */
@@ -283,15 +283,41 @@
 
   @Override
   public boolean equals(Object o) {
-    if (o instanceof Comment) {
-      return Objects.equals(key, ((Comment) o).key);
+    if (!(o instanceof Comment)) {
+      return false;
     }
-    return false;
+    Comment c = (Comment) o;
+    return Objects.equals(key, c.key)
+        && lineNbr == c.lineNbr
+        && Objects.equals(author, c.author)
+        && Objects.equals(realAuthor, c.realAuthor)
+        && Objects.equals(writtenOn, c.writtenOn)
+        && side == c.side
+        && Objects.equals(message, c.message)
+        && Objects.equals(parentUuid, c.parentUuid)
+        && Objects.equals(range, c.range)
+        && Objects.equals(tag, c.tag)
+        && Objects.equals(revId, c.revId)
+        && Objects.equals(serverId, c.serverId)
+        && unresolved == c.unresolved;
   }
 
   @Override
   public int hashCode() {
-    return key.hashCode();
+    return Objects.hash(
+        key,
+        lineNbr,
+        author,
+        realAuthor,
+        writtenOn,
+        side,
+        message,
+        parentUuid,
+        range,
+        tag,
+        revId,
+        serverId,
+        unresolved);
   }
 
   @Override
diff --git a/java/com/google/gerrit/reviewdb/client/CommentRange.java b/java/com/google/gerrit/reviewdb/client/CommentRange.java
index 3c69538..e6c5078 100644
--- a/java/com/google/gerrit/reviewdb/client/CommentRange.java
+++ b/java/com/google/gerrit/reviewdb/client/CommentRange.java
@@ -14,20 +14,14 @@
 
 package com.google.gerrit.reviewdb.client;
 
-import com.google.gwtorm.client.Column;
-
 public class CommentRange {
 
-  @Column(id = 1)
   protected int startLine;
 
-  @Column(id = 2)
   protected int startCharacter;
 
-  @Column(id = 3)
   protected int endLine;
 
-  @Column(id = 4)
   protected int endCharacter;
 
   protected CommentRange() {}
diff --git a/java/com/google/gerrit/reviewdb/client/CurrentSchemaVersion.java b/java/com/google/gerrit/reviewdb/client/CurrentSchemaVersion.java
deleted file mode 100644
index 6a3b69c..0000000
--- a/java/com/google/gerrit/reviewdb/client/CurrentSchemaVersion.java
+++ /dev/null
@@ -1,58 +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.gwtorm.client.Column;
-import com.google.gwtorm.client.StringKey;
-
-/** Current version of the database schema, to facilitate live upgrades. */
-public final class CurrentSchemaVersion {
-  public static final class Key extends StringKey<com.google.gwtorm.client.Key<?>> {
-    private static final long serialVersionUID = 1L;
-
-    private static final String VALUE = "X";
-
-    @Column(id = 1, length = 1)
-    public String one = VALUE;
-
-    public Key() {}
-
-    @Override
-    public String get() {
-      return VALUE;
-    }
-
-    @Override
-    protected void set(String newValue) {
-      assert get().equals(newValue);
-    }
-  }
-
-  /** Construct a new, unconfigured instance. */
-  public static CurrentSchemaVersion create() {
-    final CurrentSchemaVersion r = new CurrentSchemaVersion();
-    r.singleton = new CurrentSchemaVersion.Key();
-    return r;
-  }
-
-  @Column(id = 1)
-  public Key singleton;
-
-  /** Current version number of the schema. */
-  @Column(id = 2)
-  public transient int versionNbr;
-
-  public CurrentSchemaVersion() {}
-}
diff --git a/java/com/google/gerrit/reviewdb/client/LabelId.java b/java/com/google/gerrit/reviewdb/client/LabelId.java
index e69cab2..abf131b 100644
--- a/java/com/google/gerrit/reviewdb/client/LabelId.java
+++ b/java/com/google/gerrit/reviewdb/client/LabelId.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.reviewdb.client;
 
-import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.StringKey;
 
 public class LabelId extends StringKey<com.google.gwtorm.client.Key<?>> {
@@ -22,11 +21,14 @@
 
   static final String LEGACY_SUBMIT_NAME = "SUBM";
 
+  public static LabelId create(String n) {
+    return new LabelId(n);
+  }
+
   public static LabelId legacySubmit() {
     return new LabelId(LEGACY_SUBMIT_NAME);
   }
 
-  @Column(id = 1)
   public String id;
 
   public LabelId() {}
diff --git a/java/com/google/gerrit/reviewdb/client/Patch.java b/java/com/google/gerrit/reviewdb/client/Patch.java
index 0492c6c..d192df0 100644
--- a/java/com/google/gerrit/reviewdb/client/Patch.java
+++ b/java/com/google/gerrit/reviewdb/client/Patch.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.reviewdb.client;
 
-import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.StringKey;
 
 /** A single modified file in a {@link PatchSet}. */
@@ -36,13 +35,15 @@
     return COMMIT_MSG.equals(path) || MERGE_LIST.equals(path);
   }
 
+  public static Key key(PatchSet.Id patchSetId, String fileName) {
+    return new Key(patchSetId, fileName);
+  }
+
   public static class Key extends StringKey<PatchSet.Id> {
     private static final long serialVersionUID = 1L;
 
-    @Column(id = 1, name = Column.NONE)
     protected PatchSet.Id patchSetId;
 
-    @Column(id = 2)
     protected String fileName;
 
     protected Key() {
@@ -59,11 +60,19 @@
       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;
diff --git a/java/com/google/gerrit/reviewdb/client/PatchLineComment.java b/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
index de953dc..ce218c0 100644
--- a/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
+++ b/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
@@ -14,8 +14,8 @@
 
 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.Column;
 import com.google.gwtorm.client.StringKey;
 import java.sql.Timestamp;
 import java.util.Objects;
@@ -23,9 +23,7 @@
 /**
  * A comment left by a user on a specific line of a {@link Patch}.
  *
- * <p>This class represents an inline comment in ReviewDb. It should only be used for
- * writing/reading inline comments to/from ReviewDb. For all other purposes inline comments should
- * be represented by {@link Comment}.
+ * <p>New APIs should not expose this class.
  *
  * @see Comment
  */
@@ -38,10 +36,8 @@
           new Patch.Key(new PatchSet.Id(changeId, key.patchSetId), key.filename), key.uuid);
     }
 
-    @Column(id = 1, name = Column.NONE)
     protected Patch.Key patchKey;
 
-    @Column(id = 2, length = 40)
     protected String uuid;
 
     protected Key() {
@@ -124,57 +120,40 @@
     return plc;
   }
 
-  @Column(id = 1, name = Column.NONE)
   protected Key key;
 
   /** Line number this comment applies to; it should display after the line. */
-  @Column(id = 2)
   protected int lineNbr;
 
   /** Who wrote this comment. */
-  @Column(id = 3, name = "author_id")
   protected Account.Id author;
 
   /** When this comment was drafted. */
-  @Column(id = 4)
   protected Timestamp writtenOn;
 
   /** Current publication state of the comment; see {@link Status}. */
-  @Column(id = 5)
   protected char status;
 
   /** Which file is this comment; 0 is ancestor, 1 is new version. */
-  @Column(id = 6)
   protected short side;
 
   /** The text left by the user. */
-  @Column(id = 7, notNull = false, length = Integer.MAX_VALUE)
-  protected String message;
+  @Nullable protected String message;
 
   /** The parent of this comment, or null if this is the first comment on this line */
-  @Column(id = 8, length = 40, notNull = false)
-  protected String parentUuid;
+  @Nullable protected String parentUuid;
 
-  @Column(id = 9, notNull = false)
-  protected CommentRange range;
+  @Nullable protected CommentRange range;
 
-  @Column(id = 10, notNull = false)
-  protected String tag;
+  @Nullable protected String tag;
 
   /** Real user that added this comment on behalf of the user recorded in {@link #author}. */
-  @Column(id = 11, notNull = false)
-  protected Account.Id realAuthor;
+  @Nullable protected Account.Id realAuthor;
 
   /** True if this comment requires further action. */
-  @Column(id = 12)
   protected boolean unresolved;
 
-  /**
-   * The RevId for the commit to which this comment is referring.
-   *
-   * <p>Note that this field is not stored in the database. It is just provided for users of this
-   * class to avoid a lookup when they don't have easy access to a ReviewDb.
-   */
+  /** The RevId for the commit to which this comment is referring. */
   protected RevId revId;
 
   protected PatchLineComment() {}
diff --git a/java/com/google/gerrit/reviewdb/client/PatchSet.java b/java/com/google/gerrit/reviewdb/client/PatchSet.java
index 849fd75..684f092 100644
--- a/java/com/google/gerrit/reviewdb/client/PatchSet.java
+++ b/java/com/google/gerrit/reviewdb/client/PatchSet.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.reviewdb.client;
 
-import com.google.gwtorm.client.Column;
+import com.google.gerrit.common.Nullable;
 import com.google.gwtorm.client.IntKey;
 import java.sql.Timestamp;
 import java.util.ArrayList;
@@ -39,7 +39,7 @@
     return isChangeRef(name);
   }
 
-  static String joinGroups(List<String> groups) {
+  public static String joinGroups(List<String> groups) {
     if (groups == null) {
       throw new IllegalArgumentException("groups may not be null");
     }
@@ -65,7 +65,7 @@
     while (true) {
       int idx = joinedGroups.indexOf(',', i);
       if (idx < 0) {
-        groups.add(joinedGroups.substring(i, joinedGroups.length()));
+        groups.add(joinedGroups.substring(i));
         break;
       }
       groups.add(joinedGroups.substring(i, idx));
@@ -74,13 +74,15 @@
     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;
 
-    @Column(id = 1)
     public Change.Id changeId;
 
-    @Column(id = 2)
     public int patchSetId;
 
     public Id() {
@@ -97,6 +99,10 @@
       return changeId;
     }
 
+    public Change.Id changeId() {
+      return getParentKey();
+    }
+
     @Override
     public int get() {
       return patchSetId;
@@ -156,21 +162,15 @@
     }
   }
 
-  @Column(id = 1, name = Column.NONE)
   protected Id id;
 
-  @Column(id = 2, notNull = false)
-  protected RevId revision;
+  @Nullable protected RevId revision;
 
-  @Column(id = 3, name = "uploader_account_id")
   protected Account.Id uploader;
 
   /** When this patch set was first introduced onto the change. */
-  @Column(id = 4)
   protected Timestamp createdOn;
 
-  // @Column(id = 5)
-
   /**
    * Opaque group identifier, usually assigned during creation.
    *
@@ -180,14 +180,12 @@
    * <p>Changes on the same branch having patch sets with intersecting groups are considered
    * related, as in the "Related Changes" tab.
    */
-  @Column(id = 6, notNull = false, length = Integer.MAX_VALUE)
-  protected String groups;
+  @Nullable protected String groups;
 
   // DELETED id = 7 (pushCertficate)
 
   /** Certificate sent with a push that created this patch set. */
-  @Column(id = 8, notNull = false, length = Integer.MAX_VALUE)
-  protected String pushCertificate;
+  @Nullable protected String pushCertificate;
 
   /**
    * Optional user-supplied description for this patch set.
@@ -195,8 +193,7 @@
    * <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.
    */
-  @Column(id = 9, notNull = false, length = Integer.MAX_VALUE)
-  protected String description;
+  @Nullable protected String description;
 
   protected PatchSet() {}
 
diff --git a/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java b/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
index 5adf002..e1c4ea9 100644
--- a/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
+++ b/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.reviewdb.client;
 
-import com.google.gwtorm.client.Column;
+import com.google.gerrit.common.Nullable;
 import com.google.gwtorm.client.CompoundKey;
 import java.sql.Timestamp;
 import java.util.Date;
@@ -22,16 +22,17 @@
 
 /** 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;
 
-    @Column(id = 1, name = Column.NONE)
     protected PatchSet.Id patchSetId;
 
-    @Column(id = 2)
     protected Account.Id accountId;
 
-    @Column(id = 3)
     protected LabelId categoryId;
 
     protected Key() {
@@ -51,21 +52,32 @@
       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};
     }
   }
 
-  @Column(id = 1, name = Column.NONE)
   protected Key key;
 
   /**
@@ -84,20 +96,15 @@
    * and in the negative and positive direction a magnitude can be assumed.The further from 0 the
    * more assertive the approval.
    */
-  @Column(id = 2)
   protected short value;
 
-  @Column(id = 3)
   protected Timestamp granted;
 
-  @Column(id = 6, notNull = false)
-  protected String tag;
+  @Nullable protected String tag;
 
   /** Real user that made this approval on behalf of the user recorded in {@link Key#accountId}. */
-  @Column(id = 7, notNull = false)
-  protected Account.Id realAccountId;
+  @Nullable protected Account.Id realAccountId;
 
-  @Column(id = 8)
   protected boolean postSubmit;
 
   // DELETED: id = 4 (changeOpen)
diff --git a/java/com/google/gerrit/reviewdb/client/Project.java b/java/com/google/gerrit/reviewdb/client/Project.java
index 996f1ec..52becec 100644
--- a/java/com/google/gerrit/reviewdb/client/Project.java
+++ b/java/com/google/gerrit/reviewdb/client/Project.java
@@ -17,7 +17,6 @@
 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.Column;
 import com.google.gwtorm.client.StringKey;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -31,11 +30,14 @@
   /** Default submit type for root project (All-Projects). */
   public static final SubmitType DEFAULT_ALL_PROJECTS_SUBMIT_TYPE = SubmitType.MERGE_IF_NECESSARY;
 
+  public static NameKey nameKey(String name) {
+    return new NameKey(name);
+  }
+
   /** Project name key */
   public static class NameKey extends StringKey<com.google.gwtorm.client.Key<?>> {
     private static final long serialVersionUID = 1L;
 
-    @Column(id = 1)
     protected String name;
 
     protected NameKey() {}
@@ -97,8 +99,6 @@
 
   protected String localDefaultDashboardId;
 
-  protected String themeName;
-
   protected String configRefState;
 
   protected Project() {}
@@ -184,22 +184,6 @@
     this.localDefaultDashboardId = localDefaultDashboardId;
   }
 
-  public String getThemeName() {
-    return themeName;
-  }
-
-  public void setThemeName(String themeName) {
-    this.themeName = themeName;
-  }
-
-  public void copySettingsFrom(Project update) {
-    description = update.description;
-    booleanConfigs = new HashMap<>(update.booleanConfigs);
-    submitType = update.submitType;
-    state = update.state;
-    maxObjectSizeLimit = update.maxObjectSizeLimit;
-  }
-
   /**
    * Returns the name key of the parent project.
    *
diff --git a/java/com/google/gerrit/reviewdb/client/RefNames.java b/java/com/google/gerrit/reviewdb/client/RefNames.java
index fd2fb56..1f11921 100644
--- a/java/com/google/gerrit/reviewdb/client/RefNames.java
+++ b/java/com/google/gerrit/reviewdb/client/RefNames.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.reviewdb.client;
 
+import com.google.gerrit.common.UsedAt;
+
 /** Constants and utilities for Gerrit-specific ref names. */
 public class RefNames {
   public static final String HEAD = "HEAD";
@@ -49,6 +51,9 @@
   /** Sequence counters in NoteDb. */
   public static final String REFS_SEQUENCES = "refs/sequences/";
 
+  /** NoteDb schema version number. */
+  public static final String REFS_VERSION = "refs/meta/version";
+
   /**
    * Prefix applied to merge commit base nodes.
    *
@@ -134,6 +139,11 @@
     return false;
   }
 
+  /** True if the provided ref is in {@code refs/changes/*}. */
+  public static boolean isRefsChanges(String ref) {
+    return ref.startsWith(REFS_CHANGES);
+  }
+
   public static String refsGroups(AccountGroup.UUID groupUuid) {
     return REFS_GROUPS + shardUuid(groupUuid.get());
   }
@@ -190,7 +200,8 @@
     return sb;
   }
 
-  private static String shardUuid(String uuid) {
+  @UsedAt(UsedAt.Project.PLUGINS_ALL)
+  public static String shardUuid(String uuid) {
     if (uuid == null || uuid.length() < 2) {
       throw new IllegalArgumentException("UUIDs must consist of at least two characters");
     }
@@ -263,6 +274,24 @@
     return REFS_CONFIG.equals(ref);
   }
 
+  /**
+   * Whether the ref is managed by Gerrit. Covers all Gerrit-internal refs like refs/cache-automerge
+   * and refs/meta as well as refs/changes. Does not cover user-created refs like branches or custom
+   * ref namespaces like refs/my-company.
+   */
+  public static boolean isGerritRef(String ref) {
+    return ref.startsWith(REFS_CHANGES)
+        || ref.startsWith(REFS_META)
+        || ref.startsWith(REFS_CACHE_AUTOMERGE)
+        || ref.startsWith(REFS_DRAFT_COMMENTS)
+        || ref.startsWith(REFS_DELETED_GROUPS)
+        || ref.startsWith(REFS_SEQUENCES)
+        || ref.startsWith(REFS_GROUPS)
+        || ref.startsWith(REFS_GROUPNAMES)
+        || ref.startsWith(REFS_USERS)
+        || ref.startsWith(REFS_STARRED_CHANGES);
+  }
+
   static Integer parseShardedRefPart(String name) {
     if (name == null) {
       return null;
@@ -305,7 +334,8 @@
     return id;
   }
 
-  static String parseShardedUuidFromRefPart(String name) {
+  @UsedAt(UsedAt.Project.PLUGINS_ALL)
+  public static String parseShardedUuidFromRefPart(String name) {
     if (name == null) {
       return null;
     }
@@ -429,7 +459,7 @@
     if (i == 0) {
       return null;
     }
-    return Integer.valueOf(name.substring(i, name.length()));
+    return Integer.valueOf(name.substring(i));
   }
 
   private static StringBuilder newStringBuilder() {
diff --git a/java/com/google/gerrit/reviewdb/client/RevId.java b/java/com/google/gerrit/reviewdb/client/RevId.java
index 0b0f74a..99b6c2c 100644
--- a/java/com/google/gerrit/reviewdb/client/RevId.java
+++ b/java/com/google/gerrit/reviewdb/client/RevId.java
@@ -14,14 +14,11 @@
 
 package com.google.gerrit.reviewdb.client;
 
-import com.google.gwtorm.client.Column;
-
 /** 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;
 
-  @Column(id = 1, length = LEN)
   protected String id;
 
   protected RevId() {}
diff --git a/java/com/google/gerrit/reviewdb/client/SubmoduleSubscription.java b/java/com/google/gerrit/reviewdb/client/SubmoduleSubscription.java
index bd00d37..b297dfb 100644
--- a/java/com/google/gerrit/reviewdb/client/SubmoduleSubscription.java
+++ b/java/com/google/gerrit/reviewdb/client/SubmoduleSubscription.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.reviewdb.client;
 
-import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.StringKey;
 
 /**
@@ -34,10 +33,8 @@
      * Indicates the super project, aka subscriber: the project owner of the gitlinks to the
      * submodules.
      */
-    @Column(id = 1)
     protected Branch.NameKey superProject;
 
-    @Column(id = 2)
     protected String submodulePath;
 
     protected Key() {
@@ -65,10 +62,8 @@
     }
   }
 
-  @Column(id = 1, name = Column.NONE)
   protected Key key;
 
-  @Column(id = 2)
   protected Branch.NameKey submodule;
 
   protected SubmoduleSubscription() {}
diff --git a/java/com/google/gerrit/reviewdb/client/TrackingId.java b/java/com/google/gerrit/reviewdb/client/TrackingId.java
deleted file mode 100644
index 2f6008f..0000000
--- a/java/com/google/gerrit/reviewdb/client/TrackingId.java
+++ /dev/null
@@ -1,159 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT 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.Column;
-import com.google.gwtorm.client.CompoundKey;
-import com.google.gwtorm.client.StringKey;
-
-/** External tracking id associated with a {@link Change} */
-public final class TrackingId {
-  public static final int TRACKING_ID_MAX_CHAR = 32;
-  public static final int TRACKING_SYSTEM_MAX_CHAR = 10;
-
-  /** External tracking id */
-  public static class Id extends StringKey<com.google.gwtorm.client.Key<?>> {
-    private static final long serialVersionUID = 1L;
-
-    @Column(id = 1, length = TrackingId.TRACKING_ID_MAX_CHAR)
-    protected String id;
-
-    protected Id() {}
-
-    public Id(String id) {
-      this.id = id;
-    }
-
-    @Override
-    public String get() {
-      return id;
-    }
-
-    @Override
-    protected void set(String newValue) {
-      id = newValue;
-    }
-  }
-
-  /** Name of external tracking system */
-  public static class System extends StringKey<com.google.gwtorm.client.Key<?>> {
-    private static final long serialVersionUID = 1L;
-
-    @Column(id = 1, length = TrackingId.TRACKING_SYSTEM_MAX_CHAR)
-    protected String system;
-
-    protected System() {}
-
-    public System(String s) {
-      this.system = s;
-    }
-
-    @Override
-    public String get() {
-      return system;
-    }
-
-    @Override
-    protected void set(String newValue) {
-      system = newValue;
-    }
-  }
-
-  public static class Key extends CompoundKey<Change.Id> {
-    private static final long serialVersionUID = 1L;
-
-    @Column(id = 1)
-    protected Change.Id changeId;
-
-    @Column(id = 2)
-    protected Id trackingKey;
-
-    @Column(id = 3)
-    protected System trackingSystem;
-
-    protected Key() {
-      changeId = new Change.Id();
-      trackingKey = new Id();
-      trackingSystem = new System();
-    }
-
-    protected Key(Change.Id ch, Id id, System s) {
-      changeId = ch;
-      trackingKey = id;
-      trackingSystem = s;
-    }
-
-    @Override
-    public Change.Id getParentKey() {
-      return changeId;
-    }
-
-    public TrackingId.Id getTrackingId() {
-      return trackingKey;
-    }
-
-    public TrackingId.System getTrackingSystem() {
-      return trackingSystem;
-    }
-
-    @Override
-    public com.google.gwtorm.client.Key<?>[] members() {
-      return new com.google.gwtorm.client.Key<?>[] {trackingKey, trackingSystem};
-    }
-  }
-
-  @Column(id = 1, name = Column.NONE)
-  protected Key key;
-
-  protected TrackingId() {}
-
-  public TrackingId(Change.Id ch, TrackingId.Id id, TrackingId.System s) {
-    key = new Key(ch, id, s);
-  }
-
-  public TrackingId(Change.Id ch, String id, String s) {
-    key = new Key(ch, new TrackingId.Id(id), new TrackingId.System(s));
-  }
-
-  public TrackingId.Key getKey() {
-    return key;
-  }
-
-  public Change.Id getChangeId() {
-    return key.changeId;
-  }
-
-  public String getTrackingId() {
-    return key.trackingKey.get();
-  }
-
-  public String getSystem() {
-    return key.trackingSystem.get();
-  }
-
-  @Override
-  public int hashCode() {
-    return key.hashCode();
-  }
-
-  @Override
-  public boolean equals(Object obj) {
-    if (obj instanceof TrackingId) {
-      final TrackingId tr = (TrackingId) obj;
-      return key.equals(tr.key);
-    }
-    return false;
-  }
-}
diff --git a/java/com/google/gerrit/reviewdb/converter/AccountIdProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/AccountIdProtoConverter.java
new file mode 100644
index 0000000..8dd1794
--- /dev/null
+++ b/java/com/google/gerrit/reviewdb/converter/AccountIdProtoConverter.java
@@ -0,0 +1,38 @@
+// 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.protobuf.Parser;
+
+public enum AccountIdProtoConverter implements ProtoConverter<Entities.Account_Id, Account.Id> {
+  INSTANCE;
+
+  @Override
+  public Entities.Account_Id toProto(Account.Id accountId) {
+    return Entities.Account_Id.newBuilder().setId(accountId.get()).build();
+  }
+
+  @Override
+  public Account.Id fromProto(Entities.Account_Id proto) {
+    return new Account.Id(proto.getId());
+  }
+
+  @Override
+  public Parser<Entities.Account_Id> getParser() {
+    return Entities.Account_Id.parser();
+  }
+}
diff --git a/java/com/google/gerrit/reviewdb/converter/BranchNameKeyProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/BranchNameKeyProtoConverter.java
new file mode 100644
index 0000000..4558f9b
--- /dev/null
+++ b/java/com/google/gerrit/reviewdb/converter/BranchNameKeyProtoConverter.java
@@ -0,0 +1,47 @@
+// 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.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.protobuf.Parser;
+
+public enum BranchNameKeyProtoConverter
+    implements ProtoConverter<Entities.Branch_NameKey, Branch.NameKey> {
+  INSTANCE;
+
+  private final ProtoConverter<Entities.Project_NameKey, Project.NameKey> projectNameConverter =
+      ProjectNameKeyProtoConverter.INSTANCE;
+
+  @Override
+  public Entities.Branch_NameKey toProto(Branch.NameKey nameKey) {
+    return Entities.Branch_NameKey.newBuilder()
+        .setProjectName(projectNameConverter.toProto(nameKey.getParentKey()))
+        .setBranchName(nameKey.get())
+        .build();
+  }
+
+  @Override
+  public Branch.NameKey fromProto(Entities.Branch_NameKey proto) {
+    return new Branch.NameKey(
+        projectNameConverter.fromProto(proto.getProjectName()), proto.getBranchName());
+  }
+
+  @Override
+  public Parser<Entities.Branch_NameKey> getParser() {
+    return Entities.Branch_NameKey.parser();
+  }
+}
diff --git a/java/com/google/gerrit/reviewdb/converter/ChangeIdProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/ChangeIdProtoConverter.java
new file mode 100644
index 0000000..14ed59c
--- /dev/null
+++ b/java/com/google/gerrit/reviewdb/converter/ChangeIdProtoConverter.java
@@ -0,0 +1,38 @@
+// 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.Change;
+import com.google.protobuf.Parser;
+
+public enum ChangeIdProtoConverter implements ProtoConverter<Entities.Change_Id, Change.Id> {
+  INSTANCE;
+
+  @Override
+  public Entities.Change_Id toProto(Change.Id changeId) {
+    return Entities.Change_Id.newBuilder().setId(changeId.get()).build();
+  }
+
+  @Override
+  public Change.Id fromProto(Entities.Change_Id proto) {
+    return new Change.Id(proto.getId());
+  }
+
+  @Override
+  public Parser<Entities.Change_Id> getParser() {
+    return Entities.Change_Id.parser();
+  }
+}
diff --git a/java/com/google/gerrit/reviewdb/converter/ChangeKeyProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/ChangeKeyProtoConverter.java
new file mode 100644
index 0000000..dccd7d9
--- /dev/null
+++ b/java/com/google/gerrit/reviewdb/converter/ChangeKeyProtoConverter.java
@@ -0,0 +1,38 @@
+// 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.Change;
+import com.google.protobuf.Parser;
+
+public enum ChangeKeyProtoConverter implements ProtoConverter<Entities.Change_Key, Change.Key> {
+  INSTANCE;
+
+  @Override
+  public Entities.Change_Key toProto(Change.Key key) {
+    return Entities.Change_Key.newBuilder().setId(key.get()).build();
+  }
+
+  @Override
+  public Change.Key fromProto(Entities.Change_Key proto) {
+    return new Change.Key(proto.getId());
+  }
+
+  @Override
+  public Parser<Entities.Change_Key> getParser() {
+    return Entities.Change_Key.parser();
+  }
+}
diff --git a/java/com/google/gerrit/reviewdb/converter/ChangeMessageKeyProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/ChangeMessageKeyProtoConverter.java
new file mode 100644
index 0000000..bb532df
--- /dev/null
+++ b/java/com/google/gerrit/reviewdb/converter/ChangeMessageKeyProtoConverter.java
@@ -0,0 +1,46 @@
+// 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.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.protobuf.Parser;
+
+public enum ChangeMessageKeyProtoConverter
+    implements ProtoConverter<Entities.ChangeMessage_Key, ChangeMessage.Key> {
+  INSTANCE;
+
+  private final ProtoConverter<Entities.Change_Id, Change.Id> changeIdConverter =
+      ChangeIdProtoConverter.INSTANCE;
+
+  @Override
+  public Entities.ChangeMessage_Key toProto(ChangeMessage.Key messageKey) {
+    return Entities.ChangeMessage_Key.newBuilder()
+        .setChangeId(changeIdConverter.toProto(messageKey.getParentKey()))
+        .setUuid(messageKey.get())
+        .build();
+  }
+
+  @Override
+  public ChangeMessage.Key fromProto(Entities.ChangeMessage_Key proto) {
+    return new ChangeMessage.Key(changeIdConverter.fromProto(proto.getChangeId()), proto.getUuid());
+  }
+
+  @Override
+  public Parser<Entities.ChangeMessage_Key> getParser() {
+    return Entities.ChangeMessage_Key.parser();
+  }
+}
diff --git a/java/com/google/gerrit/reviewdb/converter/ChangeMessageProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/ChangeMessageProtoConverter.java
new file mode 100644
index 0000000..0895d8d
--- /dev/null
+++ b/java/com/google/gerrit/reviewdb/converter/ChangeMessageProtoConverter.java
@@ -0,0 +1,97 @@
+// 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.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.protobuf.Parser;
+import java.sql.Timestamp;
+import java.util.Objects;
+
+public enum ChangeMessageProtoConverter
+    implements ProtoConverter<Entities.ChangeMessage, ChangeMessage> {
+  INSTANCE;
+
+  private final ProtoConverter<Entities.ChangeMessage_Key, ChangeMessage.Key>
+      changeMessageKeyConverter = ChangeMessageKeyProtoConverter.INSTANCE;
+  private final ProtoConverter<Entities.Account_Id, Account.Id> accountIdConverter =
+      AccountIdProtoConverter.INSTANCE;
+  private final ProtoConverter<Entities.PatchSet_Id, PatchSet.Id> patchSetIdConverter =
+      PatchSetIdProtoConverter.INSTANCE;
+
+  @Override
+  public Entities.ChangeMessage toProto(ChangeMessage changeMessage) {
+    Entities.ChangeMessage.Builder builder =
+        Entities.ChangeMessage.newBuilder()
+            .setKey(changeMessageKeyConverter.toProto(changeMessage.getKey()));
+    Account.Id author = changeMessage.getAuthor();
+    if (author != null) {
+      builder.setAuthorId(accountIdConverter.toProto(author));
+    }
+    Timestamp writtenOn = changeMessage.getWrittenOn();
+    if (writtenOn != null) {
+      builder.setWrittenOn(writtenOn.getTime());
+    }
+    String message = changeMessage.getMessage();
+    if (message != null) {
+      builder.setMessage(message);
+    }
+    PatchSet.Id patchSetId = changeMessage.getPatchSetId();
+    if (patchSetId != null) {
+      builder.setPatchset(patchSetIdConverter.toProto(patchSetId));
+    }
+    String tag = changeMessage.getTag();
+    if (tag != null) {
+      builder.setTag(tag);
+    }
+    Account.Id realAuthor = changeMessage.getRealAuthor();
+    // ChangeMessage#getRealAuthor automatically delegates to ChangeMessage#getAuthor if the real
+    // author is not set. However, the previous protobuf representation kept 'realAuthor' empty if
+    // it wasn't set. To ensure binary compatibility, simulate the previous behavior.
+    if (realAuthor != null && !Objects.equals(realAuthor, author)) {
+      builder.setRealAuthor(accountIdConverter.toProto(realAuthor));
+    }
+    return builder.build();
+  }
+
+  @Override
+  public ChangeMessage fromProto(Entities.ChangeMessage proto) {
+    ChangeMessage.Key key =
+        proto.hasKey() ? changeMessageKeyConverter.fromProto(proto.getKey()) : null;
+    Account.Id author =
+        proto.hasAuthorId() ? accountIdConverter.fromProto(proto.getAuthorId()) : null;
+    Timestamp writtenOn = proto.hasWrittenOn() ? new Timestamp(proto.getWrittenOn()) : null;
+    PatchSet.Id patchSetId =
+        proto.hasPatchset() ? patchSetIdConverter.fromProto(proto.getPatchset()) : null;
+    ChangeMessage changeMessage = new ChangeMessage(key, author, writtenOn, patchSetId);
+    if (proto.hasMessage()) {
+      changeMessage.setMessage(proto.getMessage());
+    }
+    if (proto.hasTag()) {
+      changeMessage.setTag(proto.getTag());
+    }
+    if (proto.hasRealAuthor()) {
+      changeMessage.setRealAuthor(accountIdConverter.fromProto(proto.getRealAuthor()));
+    }
+    return changeMessage;
+  }
+
+  @Override
+  public Parser<Entities.ChangeMessage> getParser() {
+    return Entities.ChangeMessage.parser();
+  }
+}
diff --git a/java/com/google/gerrit/reviewdb/converter/ChangeProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/ChangeProtoConverter.java
new file mode 100644
index 0000000..30b6d27
--- /dev/null
+++ b/java/com/google/gerrit/reviewdb/converter/ChangeProtoConverter.java
@@ -0,0 +1,126 @@
+// 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.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.protobuf.Parser;
+import java.sql.Timestamp;
+
+public enum ChangeProtoConverter implements ProtoConverter<Entities.Change, Change> {
+  INSTANCE;
+
+  private final ProtoConverter<Entities.Change_Id, Change.Id> changeIdConverter =
+      ChangeIdProtoConverter.INSTANCE;
+  private final ProtoConverter<Entities.Change_Key, Change.Key> changeKeyConverter =
+      ChangeKeyProtoConverter.INSTANCE;
+  private final ProtoConverter<Entities.Account_Id, Account.Id> accountIdConverter =
+      AccountIdProtoConverter.INSTANCE;
+  private final ProtoConverter<Entities.Branch_NameKey, Branch.NameKey> branchNameConverter =
+      BranchNameKeyProtoConverter.INSTANCE;
+
+  @Override
+  public Entities.Change toProto(Change change) {
+    Entities.Change.Builder builder =
+        Entities.Change.newBuilder()
+            .setChangeId(changeIdConverter.toProto(change.getId()))
+            .setRowVersion(change.getRowVersion())
+            .setChangeKey(changeKeyConverter.toProto(change.getKey()))
+            .setCreatedOn(change.getCreatedOn().getTime())
+            .setLastUpdatedOn(change.getLastUpdatedOn().getTime())
+            .setOwnerAccountId(accountIdConverter.toProto(change.getOwner()))
+            .setDest(branchNameConverter.toProto(change.getDest()))
+            .setStatus(change.getStatus().getCode())
+            .setIsPrivate(change.isPrivate())
+            .setWorkInProgress(change.isWorkInProgress())
+            .setReviewStarted(change.hasReviewStarted());
+    PatchSet.Id currentPatchSetId = change.currentPatchSetId();
+    // Special behavior necessary to ensure binary compatibility.
+    builder.setCurrentPatchSetId(currentPatchSetId == null ? 0 : currentPatchSetId.get());
+    String subject = change.getSubject();
+    if (subject != null) {
+      builder.setSubject(subject);
+    }
+    String topic = change.getTopic();
+    if (topic != null) {
+      builder.setTopic(topic);
+    }
+    String originalSubject = change.getOriginalSubjectOrNull();
+    if (originalSubject != null) {
+      builder.setOriginalSubject(originalSubject);
+    }
+    String submissionId = change.getSubmissionId();
+    if (submissionId != null) {
+      builder.setSubmissionId(submissionId);
+    }
+    Account.Id assignee = change.getAssignee();
+    if (assignee != null) {
+      builder.setAssignee(accountIdConverter.toProto(assignee));
+    }
+    Change.Id revertOf = change.getRevertOf();
+    if (revertOf != null) {
+      builder.setRevertOf(changeIdConverter.toProto(revertOf));
+    }
+    return builder.build();
+  }
+
+  @Override
+  public Change fromProto(Entities.Change proto) {
+    Change.Id changeId = changeIdConverter.fromProto(proto.getChangeId());
+    Change.Key key =
+        proto.hasChangeKey() ? changeKeyConverter.fromProto(proto.getChangeKey()) : null;
+    Account.Id owner =
+        proto.hasOwnerAccountId() ? accountIdConverter.fromProto(proto.getOwnerAccountId()) : null;
+    Branch.NameKey destination =
+        proto.hasDest() ? branchNameConverter.fromProto(proto.getDest()) : null;
+    Change change =
+        new Change(key, changeId, owner, destination, new Timestamp(proto.getCreatedOn()));
+    if (proto.hasLastUpdatedOn()) {
+      change.setLastUpdatedOn(new Timestamp(proto.getLastUpdatedOn()));
+    }
+    Change.Status status = Change.Status.forCode((char) proto.getStatus());
+    if (status != null) {
+      change.setStatus(status);
+    }
+    String subject = proto.hasSubject() ? proto.getSubject() : null;
+    String originalSubject = proto.hasOriginalSubject() ? proto.getOriginalSubject() : null;
+    change.setCurrentPatchSet(
+        new PatchSet.Id(changeId, proto.getCurrentPatchSetId()), subject, originalSubject);
+    if (proto.hasTopic()) {
+      change.setTopic(proto.getTopic());
+    }
+    if (proto.hasSubmissionId()) {
+      change.setSubmissionId(proto.getSubmissionId());
+    }
+    if (proto.hasAssignee()) {
+      change.setAssignee(accountIdConverter.fromProto(proto.getAssignee()));
+    }
+    change.setPrivate(proto.getIsPrivate());
+    change.setWorkInProgress(proto.getWorkInProgress());
+    change.setReviewStarted(proto.getReviewStarted());
+    if (proto.hasRevertOf()) {
+      change.setRevertOf(changeIdConverter.fromProto(proto.getRevertOf()));
+    }
+    return change;
+  }
+
+  @Override
+  public Parser<Entities.Change> getParser() {
+    return Entities.Change.parser();
+  }
+}
diff --git a/java/com/google/gerrit/reviewdb/converter/LabelIdProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/LabelIdProtoConverter.java
new file mode 100644
index 0000000..274f23b
--- /dev/null
+++ b/java/com/google/gerrit/reviewdb/converter/LabelIdProtoConverter.java
@@ -0,0 +1,38 @@
+// 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.LabelId;
+import com.google.protobuf.Parser;
+
+public enum LabelIdProtoConverter implements ProtoConverter<Entities.LabelId, LabelId> {
+  INSTANCE;
+
+  @Override
+  public Entities.LabelId toProto(LabelId labelId) {
+    return Entities.LabelId.newBuilder().setId(labelId.get()).build();
+  }
+
+  @Override
+  public LabelId fromProto(Entities.LabelId proto) {
+    return new LabelId(proto.getId());
+  }
+
+  @Override
+  public Parser<Entities.LabelId> getParser() {
+    return Entities.LabelId.parser();
+  }
+}
diff --git a/java/com/google/gerrit/reviewdb/converter/PatchSetApprovalKeyProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/PatchSetApprovalKeyProtoConverter.java
new file mode 100644
index 0000000..3538301
--- /dev/null
+++ b/java/com/google/gerrit/reviewdb/converter/PatchSetApprovalKeyProtoConverter.java
@@ -0,0 +1,56 @@
+// 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.LabelId;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.protobuf.Parser;
+
+public enum PatchSetApprovalKeyProtoConverter
+    implements ProtoConverter<Entities.PatchSetApproval_Key, PatchSetApproval.Key> {
+  INSTANCE;
+
+  private final ProtoConverter<Entities.PatchSet_Id, PatchSet.Id> patchSetIdConverter =
+      PatchSetIdProtoConverter.INSTANCE;
+  private final ProtoConverter<Entities.Account_Id, Account.Id> accountIdConverter =
+      AccountIdProtoConverter.INSTANCE;
+  private final ProtoConverter<Entities.LabelId, LabelId> labelIdConverter =
+      LabelIdProtoConverter.INSTANCE;
+
+  @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()))
+        .build();
+  }
+
+  @Override
+  public PatchSetApproval.Key fromProto(Entities.PatchSetApproval_Key proto) {
+    return new PatchSetApproval.Key(
+        patchSetIdConverter.fromProto(proto.getPatchSetId()),
+        accountIdConverter.fromProto(proto.getAccountId()),
+        labelIdConverter.fromProto(proto.getCategoryId()));
+  }
+
+  @Override
+  public Parser<Entities.PatchSetApproval_Key> getParser() {
+    return Entities.PatchSetApproval_Key.parser();
+  }
+}
diff --git a/java/com/google/gerrit/reviewdb/converter/PatchSetApprovalProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/PatchSetApprovalProtoConverter.java
new file mode 100644
index 0000000..418076f
--- /dev/null
+++ b/java/com/google/gerrit/reviewdb/converter/PatchSetApprovalProtoConverter.java
@@ -0,0 +1,81 @@
+// 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.PatchSetApproval;
+import com.google.protobuf.Parser;
+import java.sql.Timestamp;
+import java.util.Objects;
+
+public enum PatchSetApprovalProtoConverter
+    implements ProtoConverter<Entities.PatchSetApproval, PatchSetApproval> {
+  INSTANCE;
+
+  private final ProtoConverter<Entities.PatchSetApproval_Key, PatchSetApproval.Key>
+      patchSetApprovalKeyProtoConverter = PatchSetApprovalKeyProtoConverter.INSTANCE;
+  private final ProtoConverter<Entities.Account_Id, Account.Id> accountIdConverter =
+      AccountIdProtoConverter.INSTANCE;
+
+  @Override
+  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());
+
+    String tag = patchSetApproval.getTag();
+    if (tag != null) {
+      builder.setTag(tag);
+    }
+    Account.Id realAccountId = patchSetApproval.getRealAccountId();
+    // 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())) {
+      builder.setRealAccountId(accountIdConverter.toProto(realAccountId));
+    }
+
+    return builder.build();
+  }
+
+  @Override
+  public PatchSetApproval fromProto(Entities.PatchSetApproval proto) {
+    PatchSetApproval patchSetApproval =
+        new PatchSetApproval(
+            patchSetApprovalKeyProtoConverter.fromProto(proto.getKey()),
+            (short) proto.getValue(),
+            new Timestamp(proto.getGranted()));
+    if (proto.hasTag()) {
+      patchSetApproval.setTag(proto.getTag());
+    }
+    if (proto.hasRealAccountId()) {
+      patchSetApproval.setRealAccountId(accountIdConverter.fromProto(proto.getRealAccountId()));
+    }
+    if (proto.hasPostSubmit()) {
+      patchSetApproval.setPostSubmit(proto.getPostSubmit());
+    }
+    return patchSetApproval;
+  }
+
+  @Override
+  public Parser<Entities.PatchSetApproval> getParser() {
+    return Entities.PatchSetApproval.parser();
+  }
+}
diff --git a/java/com/google/gerrit/reviewdb/converter/PatchSetIdProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/PatchSetIdProtoConverter.java
new file mode 100644
index 0000000..a2b95bd
--- /dev/null
+++ b/java/com/google/gerrit/reviewdb/converter/PatchSetIdProtoConverter.java
@@ -0,0 +1,45 @@
+// 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.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.protobuf.Parser;
+
+public enum PatchSetIdProtoConverter implements ProtoConverter<Entities.PatchSet_Id, PatchSet.Id> {
+  INSTANCE;
+
+  private final ProtoConverter<Entities.Change_Id, Change.Id> changeIdConverter =
+      ChangeIdProtoConverter.INSTANCE;
+
+  @Override
+  public Entities.PatchSet_Id toProto(PatchSet.Id patchSetId) {
+    return Entities.PatchSet_Id.newBuilder()
+        .setChangeId(changeIdConverter.toProto(patchSetId.getParentKey()))
+        .setPatchSetId(patchSetId.get())
+        .build();
+  }
+
+  @Override
+  public PatchSet.Id fromProto(Entities.PatchSet_Id proto) {
+    return new PatchSet.Id(changeIdConverter.fromProto(proto.getChangeId()), proto.getPatchSetId());
+  }
+
+  @Override
+  public Parser<Entities.PatchSet_Id> getParser() {
+    return Entities.PatchSet_Id.parser();
+  }
+}
diff --git a/java/com/google/gerrit/reviewdb/converter/PatchSetProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/PatchSetProtoConverter.java
new file mode 100644
index 0000000..75ee800
--- /dev/null
+++ b/java/com/google/gerrit/reviewdb/converter/PatchSetProtoConverter.java
@@ -0,0 +1,93 @@
+// 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/ProjectNameKeyProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/ProjectNameKeyProtoConverter.java
new file mode 100644
index 0000000..f7d809e
--- /dev/null
+++ b/java/com/google/gerrit/reviewdb/converter/ProjectNameKeyProtoConverter.java
@@ -0,0 +1,39 @@
+// 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.Project;
+import com.google.protobuf.Parser;
+
+public enum ProjectNameKeyProtoConverter
+    implements ProtoConverter<Entities.Project_NameKey, Project.NameKey> {
+  INSTANCE;
+
+  @Override
+  public Entities.Project_NameKey toProto(Project.NameKey nameKey) {
+    return Entities.Project_NameKey.newBuilder().setName(nameKey.get()).build();
+  }
+
+  @Override
+  public Project.NameKey fromProto(Entities.Project_NameKey proto) {
+    return new Project.NameKey(proto.getName());
+  }
+
+  @Override
+  public Parser<Entities.Project_NameKey> getParser() {
+    return Entities.Project_NameKey.parser();
+  }
+}
diff --git a/java/com/google/gerrit/reviewdb/converter/ProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/ProtoConverter.java
new file mode 100644
index 0000000..568759c
--- /dev/null
+++ b/java/com/google/gerrit/reviewdb/converter/ProtoConverter.java
@@ -0,0 +1,27 @@
+// 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.protobuf.MessageLite;
+import com.google.protobuf.Parser;
+
+public interface ProtoConverter<P extends MessageLite, C> {
+
+  P toProto(C valueClass);
+
+  C fromProto(P proto);
+
+  Parser<P> getParser();
+}
diff --git a/java/com/google/gerrit/reviewdb/converter/RevIdProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/RevIdProtoConverter.java
new file mode 100644
index 0000000..b3c998b
--- /dev/null
+++ b/java/com/google/gerrit/reviewdb/converter/RevIdProtoConverter.java
@@ -0,0 +1,38 @@
+// 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/reviewdb/server/AccountGroupAccess.java b/java/com/google/gerrit/reviewdb/server/AccountGroupAccess.java
deleted file mode 100644
index c5a1caf..0000000
--- a/java/com/google/gerrit/reviewdb/server/AccountGroupAccess.java
+++ /dev/null
@@ -1,34 +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.server;
-
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.PrimaryKey;
-import com.google.gwtorm.server.Query;
-import com.google.gwtorm.server.ResultSet;
-
-public interface AccountGroupAccess extends Access<AccountGroup, AccountGroup.Id> {
-  @Override
-  @PrimaryKey("groupId")
-  AccountGroup get(AccountGroup.Id id) throws OrmException;
-
-  @Query("WHERE groupUUID = ?")
-  ResultSet<AccountGroup> byUUID(AccountGroup.UUID uuid) throws OrmException;
-
-  @Query
-  ResultSet<AccountGroup> all() throws OrmException;
-}
diff --git a/java/com/google/gerrit/reviewdb/server/AccountGroupByIdAccess.java b/java/com/google/gerrit/reviewdb/server/AccountGroupByIdAccess.java
deleted file mode 100644
index 1634fda..0000000
--- a/java/com/google/gerrit/reviewdb/server/AccountGroupByIdAccess.java
+++ /dev/null
@@ -1,38 +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.server;
-
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupById;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.PrimaryKey;
-import com.google.gwtorm.server.Query;
-import com.google.gwtorm.server.ResultSet;
-
-public interface AccountGroupByIdAccess extends Access<AccountGroupById, AccountGroupById.Key> {
-  @Override
-  @PrimaryKey("key")
-  AccountGroupById get(AccountGroupById.Key key) throws OrmException;
-
-  @Query("WHERE key.includeUUID = ?")
-  ResultSet<AccountGroupById> byIncludeUUID(AccountGroup.UUID uuid) throws OrmException;
-
-  @Query("WHERE key.groupId = ?")
-  ResultSet<AccountGroupById> byGroup(AccountGroup.Id id) throws OrmException;
-
-  @Query("")
-  ResultSet<AccountGroupById> all() throws OrmException;
-}
diff --git a/java/com/google/gerrit/reviewdb/server/AccountGroupByIdAudAccess.java b/java/com/google/gerrit/reviewdb/server/AccountGroupByIdAudAccess.java
deleted file mode 100644
index 08f9a0a..0000000
--- a/java/com/google/gerrit/reviewdb/server/AccountGroupByIdAudAccess.java
+++ /dev/null
@@ -1,37 +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.server;
-
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.PrimaryKey;
-import com.google.gwtorm.server.Query;
-import com.google.gwtorm.server.ResultSet;
-
-public interface AccountGroupByIdAudAccess
-    extends Access<AccountGroupByIdAud, AccountGroupByIdAud.Key> {
-  @Override
-  @PrimaryKey("key")
-  AccountGroupByIdAud get(AccountGroupByIdAud.Key key) throws OrmException;
-
-  @Query("WHERE key.groupId = ? AND key.includeUUID = ?")
-  ResultSet<AccountGroupByIdAud> byGroupInclude(
-      AccountGroup.Id groupId, AccountGroup.UUID incGroupUUID) throws OrmException;
-
-  @Query("WHERE key.groupId = ?")
-  ResultSet<AccountGroupByIdAud> byGroup(AccountGroup.Id groupId) throws OrmException;
-}
diff --git a/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAccess.java b/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAccess.java
deleted file mode 100644
index 0213f25a..0000000
--- a/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAccess.java
+++ /dev/null
@@ -1,37 +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.server;
-
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.PrimaryKey;
-import com.google.gwtorm.server.Query;
-import com.google.gwtorm.server.ResultSet;
-
-public interface AccountGroupMemberAccess
-    extends Access<AccountGroupMember, AccountGroupMember.Key> {
-  @Override
-  @PrimaryKey("key")
-  AccountGroupMember get(AccountGroupMember.Key key) throws OrmException;
-
-  @Query("WHERE key.accountId = ?")
-  ResultSet<AccountGroupMember> byAccount(Account.Id id) throws OrmException;
-
-  @Query("WHERE key.groupId = ?")
-  ResultSet<AccountGroupMember> byGroup(AccountGroup.Id id) throws OrmException;
-}
diff --git a/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAuditAccess.java b/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAuditAccess.java
deleted file mode 100644
index aa2f7c4..0000000
--- a/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAuditAccess.java
+++ /dev/null
@@ -1,38 +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.server;
-
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.PrimaryKey;
-import com.google.gwtorm.server.Query;
-import com.google.gwtorm.server.ResultSet;
-
-public interface AccountGroupMemberAuditAccess
-    extends Access<AccountGroupMemberAudit, AccountGroupMemberAudit.Key> {
-  @Override
-  @PrimaryKey("key")
-  AccountGroupMemberAudit get(AccountGroupMemberAudit.Key key) throws OrmException;
-
-  @Query("WHERE key.groupId = ? AND key.accountId = ?")
-  ResultSet<AccountGroupMemberAudit> byGroupAccount(AccountGroup.Id groupId, Account.Id accountId)
-      throws OrmException;
-
-  @Query("WHERE key.groupId = ?")
-  ResultSet<AccountGroupMemberAudit> byGroup(AccountGroup.Id groupId) throws OrmException;
-}
diff --git a/java/com/google/gerrit/reviewdb/server/AccountGroupNameAccess.java b/java/com/google/gerrit/reviewdb/server/AccountGroupNameAccess.java
deleted file mode 100644
index b8bc9f0..0000000
--- a/java/com/google/gerrit/reviewdb/server/AccountGroupNameAccess.java
+++ /dev/null
@@ -1,32 +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.server;
-
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupName;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.PrimaryKey;
-import com.google.gwtorm.server.Query;
-import com.google.gwtorm.server.ResultSet;
-
-public interface AccountGroupNameAccess extends Access<AccountGroupName, AccountGroup.NameKey> {
-  @Override
-  @PrimaryKey("name")
-  AccountGroupName get(AccountGroup.NameKey name) throws OrmException;
-
-  @Query("ORDER BY name")
-  ResultSet<AccountGroupName> all() throws OrmException;
-}
diff --git a/java/com/google/gerrit/reviewdb/server/ChangeAccess.java b/java/com/google/gerrit/reviewdb/server/ChangeAccess.java
deleted file mode 100644
index 4e46efb..0000000
--- a/java/com/google/gerrit/reviewdb/server/ChangeAccess.java
+++ /dev/null
@@ -1,31 +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.server;
-
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.PrimaryKey;
-import com.google.gwtorm.server.Query;
-import com.google.gwtorm.server.ResultSet;
-
-public interface ChangeAccess extends Access<Change, Change.Id> {
-  @Override
-  @PrimaryKey("changeId")
-  Change get(Change.Id id) throws OrmException;
-
-  @Query
-  ResultSet<Change> all() throws OrmException;
-}
diff --git a/java/com/google/gerrit/reviewdb/server/ChangeMessageAccess.java b/java/com/google/gerrit/reviewdb/server/ChangeMessageAccess.java
deleted file mode 100644
index fe87e59..0000000
--- a/java/com/google/gerrit/reviewdb/server/ChangeMessageAccess.java
+++ /dev/null
@@ -1,39 +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.server;
-
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.PrimaryKey;
-import com.google.gwtorm.server.Query;
-import com.google.gwtorm.server.ResultSet;
-
-public interface ChangeMessageAccess extends Access<ChangeMessage, ChangeMessage.Key> {
-  @Override
-  @PrimaryKey("key")
-  ChangeMessage get(ChangeMessage.Key id) throws OrmException;
-
-  @Query("WHERE key.changeId = ? ORDER BY writtenOn")
-  ResultSet<ChangeMessage> byChange(Change.Id id) throws OrmException;
-
-  @Query("WHERE patchset = ?")
-  ResultSet<ChangeMessage> byPatchSet(PatchSet.Id id) throws OrmException;
-
-  @Query
-  ResultSet<ChangeMessage> all() throws OrmException;
-}
diff --git a/java/com/google/gerrit/reviewdb/server/DisallowReadFromChangesReviewDbWrapper.java b/java/com/google/gerrit/reviewdb/server/DisallowReadFromChangesReviewDbWrapper.java
deleted file mode 100644
index fdf3d6c..0000000
--- a/java/com/google/gerrit/reviewdb/server/DisallowReadFromChangesReviewDbWrapper.java
+++ /dev/null
@@ -1,281 +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.server;
-
-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.PatchLineComment;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
-
-public class DisallowReadFromChangesReviewDbWrapper extends ReviewDbWrapper {
-  private static final String MSG = "This table has been migrated to NoteDb";
-
-  private final Changes changes;
-  private final PatchSetApprovals patchSetApprovals;
-  private final ChangeMessages changeMessages;
-  private final PatchSets patchSets;
-  private final PatchLineComments patchComments;
-
-  public DisallowReadFromChangesReviewDbWrapper(ReviewDb db) {
-    super(db);
-    changes = new Changes(delegate.changes());
-    patchSetApprovals = new PatchSetApprovals(delegate.patchSetApprovals());
-    changeMessages = new ChangeMessages(delegate.changeMessages());
-    patchSets = new PatchSets(delegate.patchSets());
-    patchComments = new PatchLineComments(delegate.patchComments());
-  }
-
-  @Override
-  public ChangeAccess changes() {
-    return changes;
-  }
-
-  @Override
-  public PatchSetApprovalAccess patchSetApprovals() {
-    return patchSetApprovals;
-  }
-
-  @Override
-  public ChangeMessageAccess changeMessages() {
-    return changeMessages;
-  }
-
-  @Override
-  public PatchSetAccess patchSets() {
-    return patchSets;
-  }
-
-  @Override
-  public PatchLineCommentAccess patchComments() {
-    return patchComments;
-  }
-
-  private static class Changes extends ChangeAccessWrapper {
-
-    protected Changes(ChangeAccess delegate) {
-      super(delegate);
-    }
-
-    @Override
-    public ResultSet<Change> iterateAllEntities() {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @SuppressWarnings("deprecation")
-    @Override
-    public com.google.common.util.concurrent.CheckedFuture<Change, OrmException> getAsync(
-        Change.Id key) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<Change> get(Iterable<Change.Id> keys) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public Change get(Change.Id id) throws OrmException {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<Change> all() throws OrmException {
-      throw new UnsupportedOperationException(MSG);
-    }
-  }
-
-  private static class PatchSetApprovals extends PatchSetApprovalAccessWrapper {
-    PatchSetApprovals(PatchSetApprovalAccess delegate) {
-      super(delegate);
-    }
-
-    @Override
-    public ResultSet<PatchSetApproval> iterateAllEntities() {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @SuppressWarnings("deprecation")
-    @Override
-    public com.google.common.util.concurrent.CheckedFuture<PatchSetApproval, OrmException> getAsync(
-        PatchSetApproval.Key key) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<PatchSetApproval> get(Iterable<PatchSetApproval.Key> keys) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public PatchSetApproval get(PatchSetApproval.Key key) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<PatchSetApproval> byChange(Change.Id id) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<PatchSetApproval> byPatchSet(PatchSet.Id id) {
-      throw new UnsupportedOperationException(MSG);
-    }
-  }
-
-  private static class ChangeMessages extends ChangeMessageAccessWrapper {
-    ChangeMessages(ChangeMessageAccess delegate) {
-      super(delegate);
-    }
-
-    @Override
-    public ResultSet<ChangeMessage> iterateAllEntities() {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @SuppressWarnings("deprecation")
-    @Override
-    public com.google.common.util.concurrent.CheckedFuture<ChangeMessage, OrmException> getAsync(
-        ChangeMessage.Key key) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<ChangeMessage> get(Iterable<ChangeMessage.Key> keys) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ChangeMessage get(ChangeMessage.Key id) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<ChangeMessage> byChange(Change.Id id) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<ChangeMessage> byPatchSet(PatchSet.Id id) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<ChangeMessage> all() {
-      throw new UnsupportedOperationException(MSG);
-    }
-  }
-
-  private static class PatchSets extends PatchSetAccessWrapper {
-    PatchSets(PatchSetAccess delegate) {
-      super(delegate);
-    }
-
-    @Override
-    public ResultSet<PatchSet> iterateAllEntities() {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @SuppressWarnings("deprecation")
-    @Override
-    public com.google.common.util.concurrent.CheckedFuture<PatchSet, OrmException> getAsync(
-        PatchSet.Id key) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<PatchSet> get(Iterable<PatchSet.Id> keys) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public PatchSet get(PatchSet.Id id) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<PatchSet> byChange(Change.Id id) {
-      throw new UnsupportedOperationException(MSG);
-    }
-  }
-
-  private static class PatchLineComments extends PatchLineCommentAccessWrapper {
-    PatchLineComments(PatchLineCommentAccess delegate) {
-      super(delegate);
-    }
-
-    @Override
-    public ResultSet<PatchLineComment> iterateAllEntities() {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @SuppressWarnings("deprecation")
-    @Override
-    public com.google.common.util.concurrent.CheckedFuture<PatchLineComment, OrmException> getAsync(
-        PatchLineComment.Key key) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<PatchLineComment> get(Iterable<PatchLineComment.Key> keys) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public PatchLineComment get(PatchLineComment.Key id) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<PatchLineComment> byChange(Change.Id id) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<PatchLineComment> byPatchSet(PatchSet.Id id) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<PatchLineComment> publishedByChangeFile(Change.Id id, String file) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<PatchLineComment> publishedByPatchSet(PatchSet.Id patchset) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<PatchLineComment> draftByPatchSetAuthor(
-        PatchSet.Id patchset, Account.Id author) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<PatchLineComment> draftByChangeFileAuthor(
-        Change.Id id, String file, Account.Id author) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<PatchLineComment> draftByAuthor(Account.Id author) {
-      throw new UnsupportedOperationException(MSG);
-    }
-  }
-}
diff --git a/java/com/google/gerrit/reviewdb/server/PatchLineCommentAccess.java b/java/com/google/gerrit/reviewdb/server/PatchLineCommentAccess.java
deleted file mode 100644
index 08b8484..0000000
--- a/java/com/google/gerrit/reviewdb/server/PatchLineCommentAccess.java
+++ /dev/null
@@ -1,69 +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.server;
-
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.PrimaryKey;
-import com.google.gwtorm.server.Query;
-import com.google.gwtorm.server.ResultSet;
-
-public interface PatchLineCommentAccess extends Access<PatchLineComment, PatchLineComment.Key> {
-  @Override
-  @PrimaryKey("key")
-  PatchLineComment get(PatchLineComment.Key id) throws OrmException;
-
-  @Query("WHERE key.patchKey.patchSetId.changeId = ?")
-  ResultSet<PatchLineComment> byChange(Change.Id id) throws OrmException;
-
-  @Query("WHERE key.patchKey.patchSetId = ?")
-  ResultSet<PatchLineComment> byPatchSet(PatchSet.Id id) throws OrmException;
-
-  @Query(
-      "WHERE key.patchKey.patchSetId.changeId = ?"
-          + " AND key.patchKey.fileName = ? AND status = '"
-          + PatchLineComment.STATUS_PUBLISHED
-          + "' ORDER BY lineNbr,writtenOn")
-  ResultSet<PatchLineComment> publishedByChangeFile(Change.Id id, String file) throws OrmException;
-
-  @Query(
-      "WHERE key.patchKey.patchSetId = ? AND status = '" + PatchLineComment.STATUS_PUBLISHED + "'")
-  ResultSet<PatchLineComment> publishedByPatchSet(PatchSet.Id patchset) throws OrmException;
-
-  @Query(
-      "WHERE key.patchKey.patchSetId = ? AND status = '"
-          + PatchLineComment.STATUS_DRAFT
-          + "' AND author = ? ORDER BY key.patchKey,lineNbr,writtenOn")
-  ResultSet<PatchLineComment> draftByPatchSetAuthor(PatchSet.Id patchset, Account.Id author)
-      throws OrmException;
-
-  @Query(
-      "WHERE key.patchKey.patchSetId.changeId = ?"
-          + " AND key.patchKey.fileName = ? AND author = ? AND status = '"
-          + PatchLineComment.STATUS_DRAFT
-          + "' ORDER BY lineNbr,writtenOn")
-  ResultSet<PatchLineComment> draftByChangeFileAuthor(Change.Id id, String file, Account.Id author)
-      throws OrmException;
-
-  @Query("WHERE status = '" + PatchLineComment.STATUS_DRAFT + "' AND author = ?")
-  ResultSet<PatchLineComment> draftByAuthor(Account.Id author) throws OrmException;
-
-  @Query
-  ResultSet<PatchLineComment> all() throws OrmException;
-}
diff --git a/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java b/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java
deleted file mode 100644
index bf3c9e4..0000000
--- a/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java
+++ /dev/null
@@ -1,35 +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.server;
-
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.PrimaryKey;
-import com.google.gwtorm.server.Query;
-import com.google.gwtorm.server.ResultSet;
-
-public interface PatchSetAccess extends Access<PatchSet, PatchSet.Id> {
-  @Override
-  @PrimaryKey("id")
-  PatchSet get(PatchSet.Id id) throws OrmException;
-
-  @Query("WHERE id.changeId = ? ORDER BY id.patchSetId")
-  ResultSet<PatchSet> byChange(Change.Id id) throws OrmException;
-
-  @Query
-  ResultSet<PatchSet> all() throws OrmException;
-}
diff --git a/java/com/google/gerrit/reviewdb/server/PatchSetApprovalAccess.java b/java/com/google/gerrit/reviewdb/server/PatchSetApprovalAccess.java
deleted file mode 100644
index 69357bc..0000000
--- a/java/com/google/gerrit/reviewdb/server/PatchSetApprovalAccess.java
+++ /dev/null
@@ -1,44 +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.server;
-
-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.gwtorm.server.Access;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.PrimaryKey;
-import com.google.gwtorm.server.Query;
-import com.google.gwtorm.server.ResultSet;
-
-public interface PatchSetApprovalAccess extends Access<PatchSetApproval, PatchSetApproval.Key> {
-  @Override
-  @PrimaryKey("key")
-  PatchSetApproval get(PatchSetApproval.Key key) throws OrmException;
-
-  @Query("WHERE key.patchSetId.changeId = ?")
-  ResultSet<PatchSetApproval> byChange(Change.Id id) throws OrmException;
-
-  @Query("WHERE key.patchSetId = ?")
-  ResultSet<PatchSetApproval> byPatchSet(PatchSet.Id id) throws OrmException;
-
-  @Query("WHERE key.patchSetId = ? AND key.accountId = ?")
-  ResultSet<PatchSetApproval> byPatchSetUser(PatchSet.Id patchSet, Account.Id account)
-      throws OrmException;
-
-  @Query
-  ResultSet<PatchSetApproval> all() throws OrmException;
-}
diff --git a/java/com/google/gerrit/reviewdb/server/ReviewDb.java b/java/com/google/gerrit/reviewdb/server/ReviewDb.java
deleted file mode 100644
index f0661e9..0000000
--- a/java/com/google/gerrit/reviewdb/server/ReviewDb.java
+++ /dev/null
@@ -1,120 +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.server;
-
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.Relation;
-import com.google.gwtorm.server.Schema;
-import com.google.gwtorm.server.Sequence;
-
-/**
- * The review service database schema.
- *
- * <p>Root entities that are at the top level of some important data graph:
- *
- * <ul>
- *   <li>{@link Account}: Per-user account registration, preferences, identity.
- *   <li>{@link Change}: All review information about a single proposed change.
- * </ul>
- */
-public interface ReviewDb extends Schema {
-  /* If you change anything, update SchemaVersion.C to use a new version. */
-
-  @Relation(id = 1)
-  SchemaVersionAccess schemaVersion();
-
-  // Deleted @Relation(id = 2)
-
-  // Deleted @Relation(id = 3)
-
-  // Deleted @Relation(id = 4)
-
-  // Deleted @Relation(id = 6)
-
-  // Deleted @Relation(id = 7)
-
-  // Deleted @Relation(id = 8)
-
-  // Deleted @Relation(id = 10)
-
-  // Deleted @Relation(id = 11)
-
-  // Deleted @Relation(id = 12)
-
-  // Deleted @Relation(id = 13)
-
-  // Deleted @Relation(id = 17)
-
-  // Deleted @Relation(id = 18)
-
-  // Deleted @Relation(id = 19)
-
-  // Deleted @Relation(id = 20)
-
-  @Relation(id = 21)
-  ChangeAccess changes();
-
-  @Relation(id = 22)
-  PatchSetApprovalAccess patchSetApprovals();
-
-  @Relation(id = 23)
-  ChangeMessageAccess changeMessages();
-
-  @Relation(id = 24)
-  PatchSetAccess patchSets();
-
-  // Deleted @Relation(id = 25)
-
-  @Relation(id = 26)
-  PatchLineCommentAccess patchComments();
-
-  // Deleted @Relation(id = 28)
-
-  // Deleted @Relation(id = 29)
-
-  // Deleted @Relation(id = 30)
-
-  int FIRST_ACCOUNT_ID = 1000000;
-
-  /**
-   * Next unique id for a {@link Account}.
-   *
-   * @deprecated use {@link com.google.gerrit.server.Sequences#nextAccountId()}.
-   */
-  @Sequence(startWith = FIRST_ACCOUNT_ID)
-  @Deprecated
-  int nextAccountId() throws OrmException;
-
-  int FIRST_GROUP_ID = 1;
-
-  /** Next unique id for a {@link AccountGroup}. */
-  @Sequence(startWith = FIRST_GROUP_ID)
-  @Deprecated
-  int nextAccountGroupId() throws OrmException;
-
-  int FIRST_CHANGE_ID = 1;
-
-  /**
-   * Next unique id for a {@link Change}.
-   *
-   * @deprecated use {@link com.google.gerrit.server.Sequences#nextChangeId()}.
-   */
-  @Sequence(startWith = FIRST_CHANGE_ID)
-  @Deprecated
-  int nextChangeId() throws OrmException;
-}
diff --git a/java/com/google/gerrit/reviewdb/server/ReviewDbCodecs.java b/java/com/google/gerrit/reviewdb/server/ReviewDbCodecs.java
deleted file mode 100644
index 2958464..0000000
--- a/java/com/google/gerrit/reviewdb/server/ReviewDbCodecs.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.server;
-
-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.gwtorm.protobuf.CodecFactory;
-import com.google.gwtorm.protobuf.ProtobufCodec;
-
-/** {@link ProtobufCodec} instances for ReviewDb types. */
-public class ReviewDbCodecs {
-  public static final ProtobufCodec<PatchSetApproval> APPROVAL_CODEC =
-      CodecFactory.encoder(PatchSetApproval.class);
-
-  public static final ProtobufCodec<Change> CHANGE_CODEC = CodecFactory.encoder(Change.class);
-
-  public static final ProtobufCodec<ChangeMessage> MESSAGE_CODEC =
-      CodecFactory.encoder(ChangeMessage.class);
-
-  public static final ProtobufCodec<PatchSet> PATCH_SET_CODEC =
-      CodecFactory.encoder(PatchSet.class);
-
-  private ReviewDbCodecs() {}
-}
diff --git a/java/com/google/gerrit/reviewdb/server/ReviewDbUtil.java b/java/com/google/gerrit/reviewdb/server/ReviewDbUtil.java
deleted file mode 100644
index aed9778..0000000
--- a/java/com/google/gerrit/reviewdb/server/ReviewDbUtil.java
+++ /dev/null
@@ -1,77 +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.server;
-
-import static com.google.common.base.Preconditions.checkState;
-
-import com.google.common.collect.Ordering;
-import com.google.common.collect.Sets;
-import com.google.gwtorm.client.Column;
-import com.google.gwtorm.client.IntKey;
-import java.lang.reflect.Field;
-import java.util.Arrays;
-import java.util.Set;
-import java.util.TreeSet;
-
-/** Static utilities for ReviewDb types. */
-public class ReviewDbUtil {
-  private static final Ordering<? extends IntKey<?>> INT_KEY_ORDERING =
-      Ordering.natural().nullsFirst().<IntKey<?>>onResultOf(IntKey::get).nullsFirst();
-
-  /**
-   * Null-safe ordering over arbitrary subclass of {@code IntKey}.
-   *
-   * <p>In some cases, {@code Comparator.comparing(Change.Id::get)} may be shorter and cleaner.
-   * However, this method may be preferable in some cases:
-   *
-   * <ul>
-   *   <li>This ordering is null-safe over both input and the result of {@link IntKey#get()}; {@code
-   *       comparing} is only a good idea if all inputs are obviously non-null.
-   *   <li>{@code intKeyOrdering().sortedCopy(iterable)} is shorter than the stream equivalent.
-   *   <li>Creating derived comparators may be more readable with {@link Ordering} method chaining
-   *       rather than static {@code Comparator} methods.
-   * </ul>
-   */
-  @SuppressWarnings("unchecked")
-  public static <K extends IntKey<?>> Ordering<K> intKeyOrdering() {
-    return (Ordering<K>) INT_KEY_ORDERING;
-  }
-
-  public static ReviewDb unwrapDb(ReviewDb db) {
-    if (db instanceof DisallowReadFromChangesReviewDbWrapper) {
-      return unwrapDb(((DisallowReadFromChangesReviewDbWrapper) db).unsafeGetDelegate());
-    }
-    return db;
-  }
-
-  public static void checkColumns(Class<?> clazz, Integer... expected) {
-    Set<Integer> ids = new TreeSet<>();
-    for (Field f : clazz.getDeclaredFields()) {
-      Column col = f.getAnnotation(Column.class);
-      if (col != null) {
-        ids.add(col.id());
-      }
-    }
-    Set<Integer> expectedIds = Sets.newTreeSet(Arrays.asList(expected));
-    checkState(
-        ids.equals(expectedIds),
-        "Unexpected column set for %s: %s != %s",
-        clazz.getSimpleName(),
-        ids,
-        expectedIds);
-  }
-
-  private ReviewDbUtil() {}
-}
diff --git a/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java b/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
deleted file mode 100644
index 202729e..0000000
--- a/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
+++ /dev/null
@@ -1,1267 +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.server;
-
-import static java.util.Objects.requireNonNull;
-
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupById;
-import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
-import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
-import com.google.gerrit.reviewdb.client.AccountGroupName;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.AtomicUpdate;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
-import com.google.gwtorm.server.StatementExecutor;
-import java.util.Map;
-
-public class ReviewDbWrapper implements ReviewDb {
-  public static JdbcSchema unwrapJbdcSchema(ReviewDb db) {
-    if (db instanceof ReviewDbWrapper) {
-      return unwrapJbdcSchema(((ReviewDbWrapper) db).unsafeGetDelegate());
-    }
-    return (JdbcSchema) db;
-  }
-
-  protected final ReviewDb delegate;
-
-  private boolean inTransaction;
-
-  protected ReviewDbWrapper(ReviewDb delegate) {
-    this.delegate = requireNonNull(delegate);
-  }
-
-  public ReviewDb unsafeGetDelegate() {
-    return delegate;
-  }
-
-  public boolean inTransaction() {
-    return inTransaction;
-  }
-
-  public void beginTransaction() {
-    inTransaction = true;
-  }
-
-  @Override
-  public void commit() throws OrmException {
-    if (!inTransaction) {
-      // This reads a little weird, we're not in a transaction, so why are we calling commit?
-      // Because we want to let the underlying ReviewDb do its normal thing in this case (which may
-      // be throwing an exception, or not, depending on implementation).
-      delegate.commit();
-    }
-  }
-
-  @Override
-  public void rollback() throws OrmException {
-    if (inTransaction) {
-      inTransaction = false;
-    } else {
-      // See comment in commit(): we want to let the underlying ReviewDb do its thing.
-      delegate.rollback();
-    }
-  }
-
-  @Override
-  public void updateSchema(StatementExecutor e) throws OrmException {
-    delegate.updateSchema(e);
-  }
-
-  @Override
-  public void pruneSchema(StatementExecutor e) throws OrmException {
-    delegate.pruneSchema(e);
-  }
-
-  @Override
-  public Access<?, ?>[] allRelations() {
-    return delegate.allRelations();
-  }
-
-  @Override
-  public void close() {
-    delegate.close();
-  }
-
-  @Override
-  public SchemaVersionAccess schemaVersion() {
-    return delegate.schemaVersion();
-  }
-
-  @Override
-  public ChangeAccess changes() {
-    return delegate.changes();
-  }
-
-  @Override
-  public PatchSetApprovalAccess patchSetApprovals() {
-    return delegate.patchSetApprovals();
-  }
-
-  @Override
-  public ChangeMessageAccess changeMessages() {
-    return delegate.changeMessages();
-  }
-
-  @Override
-  public PatchSetAccess patchSets() {
-    return delegate.patchSets();
-  }
-
-  @Override
-  public PatchLineCommentAccess patchComments() {
-    return delegate.patchComments();
-  }
-
-  @Override
-  @SuppressWarnings("deprecation")
-  public int nextAccountId() throws OrmException {
-    return delegate.nextAccountId();
-  }
-
-  @Override
-  @SuppressWarnings("deprecation")
-  public int nextAccountGroupId() throws OrmException {
-    return delegate.nextAccountGroupId();
-  }
-
-  @Override
-  @SuppressWarnings("deprecation")
-  public int nextChangeId() throws OrmException {
-    return delegate.nextChangeId();
-  }
-
-  public static class ChangeAccessWrapper implements ChangeAccess {
-    protected final ChangeAccess delegate;
-
-    protected ChangeAccessWrapper(ChangeAccess delegate) {
-      this.delegate = requireNonNull(delegate);
-    }
-
-    @Override
-    public String getRelationName() {
-      return delegate.getRelationName();
-    }
-
-    @Override
-    public int getRelationID() {
-      return delegate.getRelationID();
-    }
-
-    @Override
-    public ResultSet<Change> iterateAllEntities() throws OrmException {
-      return delegate.iterateAllEntities();
-    }
-
-    @Override
-    public Change.Id primaryKey(Change entity) {
-      return delegate.primaryKey(entity);
-    }
-
-    @Override
-    public Map<Change.Id, Change> toMap(Iterable<Change> c) {
-      return delegate.toMap(c);
-    }
-
-    @SuppressWarnings("deprecation")
-    @Override
-    public com.google.common.util.concurrent.CheckedFuture<Change, OrmException> getAsync(
-        Change.Id key) {
-      return delegate.getAsync(key);
-    }
-
-    @Override
-    public ResultSet<Change> get(Iterable<Change.Id> keys) throws OrmException {
-      return delegate.get(keys);
-    }
-
-    @Override
-    public void insert(Iterable<Change> instances) throws OrmException {
-      delegate.insert(instances);
-    }
-
-    @Override
-    public void update(Iterable<Change> instances) throws OrmException {
-      delegate.update(instances);
-    }
-
-    @Override
-    public void upsert(Iterable<Change> instances) throws OrmException {
-      delegate.upsert(instances);
-    }
-
-    @Override
-    public void deleteKeys(Iterable<Change.Id> keys) throws OrmException {
-      delegate.deleteKeys(keys);
-    }
-
-    @Override
-    public void delete(Iterable<Change> instances) throws OrmException {
-      delegate.delete(instances);
-    }
-
-    @Override
-    public void beginTransaction(Change.Id key) throws OrmException {
-      delegate.beginTransaction(key);
-    }
-
-    @Override
-    public Change atomicUpdate(Change.Id key, AtomicUpdate<Change> update) throws OrmException {
-      return delegate.atomicUpdate(key, update);
-    }
-
-    @Override
-    public Change get(Change.Id id) throws OrmException {
-      return delegate.get(id);
-    }
-
-    @Override
-    public ResultSet<Change> all() throws OrmException {
-      return delegate.all();
-    }
-  }
-
-  public static class PatchSetApprovalAccessWrapper implements PatchSetApprovalAccess {
-    protected final PatchSetApprovalAccess delegate;
-
-    protected PatchSetApprovalAccessWrapper(PatchSetApprovalAccess delegate) {
-      this.delegate = delegate;
-    }
-
-    @Override
-    public String getRelationName() {
-      return delegate.getRelationName();
-    }
-
-    @Override
-    public int getRelationID() {
-      return delegate.getRelationID();
-    }
-
-    @Override
-    public ResultSet<PatchSetApproval> iterateAllEntities() throws OrmException {
-      return delegate.iterateAllEntities();
-    }
-
-    @Override
-    public PatchSetApproval.Key primaryKey(PatchSetApproval entity) {
-      return delegate.primaryKey(entity);
-    }
-
-    @Override
-    public Map<PatchSetApproval.Key, PatchSetApproval> toMap(Iterable<PatchSetApproval> c) {
-      return delegate.toMap(c);
-    }
-
-    @SuppressWarnings("deprecation")
-    @Override
-    public com.google.common.util.concurrent.CheckedFuture<PatchSetApproval, OrmException> getAsync(
-        PatchSetApproval.Key key) {
-      return delegate.getAsync(key);
-    }
-
-    @Override
-    public ResultSet<PatchSetApproval> get(Iterable<PatchSetApproval.Key> keys)
-        throws OrmException {
-      return delegate.get(keys);
-    }
-
-    @Override
-    public void insert(Iterable<PatchSetApproval> instances) throws OrmException {
-      delegate.insert(instances);
-    }
-
-    @Override
-    public void update(Iterable<PatchSetApproval> instances) throws OrmException {
-      delegate.update(instances);
-    }
-
-    @Override
-    public void upsert(Iterable<PatchSetApproval> instances) throws OrmException {
-      delegate.upsert(instances);
-    }
-
-    @Override
-    public void deleteKeys(Iterable<PatchSetApproval.Key> keys) throws OrmException {
-      delegate.deleteKeys(keys);
-    }
-
-    @Override
-    public void delete(Iterable<PatchSetApproval> instances) throws OrmException {
-      delegate.delete(instances);
-    }
-
-    @Override
-    public void beginTransaction(PatchSetApproval.Key key) throws OrmException {
-      delegate.beginTransaction(key);
-    }
-
-    @Override
-    public PatchSetApproval atomicUpdate(
-        PatchSetApproval.Key key, AtomicUpdate<PatchSetApproval> update) throws OrmException {
-      return delegate.atomicUpdate(key, update);
-    }
-
-    @Override
-    public PatchSetApproval get(PatchSetApproval.Key key) throws OrmException {
-      return delegate.get(key);
-    }
-
-    @Override
-    public ResultSet<PatchSetApproval> byChange(Change.Id id) throws OrmException {
-      return delegate.byChange(id);
-    }
-
-    @Override
-    public ResultSet<PatchSetApproval> byPatchSet(PatchSet.Id id) throws OrmException {
-      return delegate.byPatchSet(id);
-    }
-
-    @Override
-    public ResultSet<PatchSetApproval> byPatchSetUser(PatchSet.Id patchSet, Account.Id account)
-        throws OrmException {
-      return delegate.byPatchSetUser(patchSet, account);
-    }
-
-    @Override
-    public ResultSet<PatchSetApproval> all() throws OrmException {
-      return delegate.all();
-    }
-  }
-
-  public static class ChangeMessageAccessWrapper implements ChangeMessageAccess {
-    protected final ChangeMessageAccess delegate;
-
-    protected ChangeMessageAccessWrapper(ChangeMessageAccess delegate) {
-      this.delegate = delegate;
-    }
-
-    @Override
-    public String getRelationName() {
-      return delegate.getRelationName();
-    }
-
-    @Override
-    public int getRelationID() {
-      return delegate.getRelationID();
-    }
-
-    @Override
-    public ResultSet<ChangeMessage> iterateAllEntities() throws OrmException {
-      return delegate.iterateAllEntities();
-    }
-
-    @Override
-    public ChangeMessage.Key primaryKey(ChangeMessage entity) {
-      return delegate.primaryKey(entity);
-    }
-
-    @Override
-    public Map<ChangeMessage.Key, ChangeMessage> toMap(Iterable<ChangeMessage> c) {
-      return delegate.toMap(c);
-    }
-
-    @SuppressWarnings("deprecation")
-    @Override
-    public com.google.common.util.concurrent.CheckedFuture<ChangeMessage, OrmException> getAsync(
-        ChangeMessage.Key key) {
-      return delegate.getAsync(key);
-    }
-
-    @Override
-    public ResultSet<ChangeMessage> get(Iterable<ChangeMessage.Key> keys) throws OrmException {
-      return delegate.get(keys);
-    }
-
-    @Override
-    public void insert(Iterable<ChangeMessage> instances) throws OrmException {
-      delegate.insert(instances);
-    }
-
-    @Override
-    public void update(Iterable<ChangeMessage> instances) throws OrmException {
-      delegate.update(instances);
-    }
-
-    @Override
-    public void upsert(Iterable<ChangeMessage> instances) throws OrmException {
-      delegate.upsert(instances);
-    }
-
-    @Override
-    public void deleteKeys(Iterable<ChangeMessage.Key> keys) throws OrmException {
-      delegate.deleteKeys(keys);
-    }
-
-    @Override
-    public void delete(Iterable<ChangeMessage> instances) throws OrmException {
-      delegate.delete(instances);
-    }
-
-    @Override
-    public void beginTransaction(ChangeMessage.Key key) throws OrmException {
-      delegate.beginTransaction(key);
-    }
-
-    @Override
-    public ChangeMessage atomicUpdate(ChangeMessage.Key key, AtomicUpdate<ChangeMessage> update)
-        throws OrmException {
-      return delegate.atomicUpdate(key, update);
-    }
-
-    @Override
-    public ChangeMessage get(ChangeMessage.Key id) throws OrmException {
-      return delegate.get(id);
-    }
-
-    @Override
-    public ResultSet<ChangeMessage> byChange(Change.Id id) throws OrmException {
-      return delegate.byChange(id);
-    }
-
-    @Override
-    public ResultSet<ChangeMessage> byPatchSet(PatchSet.Id id) throws OrmException {
-      return delegate.byPatchSet(id);
-    }
-
-    @Override
-    public ResultSet<ChangeMessage> all() throws OrmException {
-      return delegate.all();
-    }
-  }
-
-  public static class PatchSetAccessWrapper implements PatchSetAccess {
-    protected final PatchSetAccess delegate;
-
-    protected PatchSetAccessWrapper(PatchSetAccess delegate) {
-      this.delegate = delegate;
-    }
-
-    @Override
-    public String getRelationName() {
-      return delegate.getRelationName();
-    }
-
-    @Override
-    public int getRelationID() {
-      return delegate.getRelationID();
-    }
-
-    @Override
-    public ResultSet<PatchSet> iterateAllEntities() throws OrmException {
-      return delegate.iterateAllEntities();
-    }
-
-    @Override
-    public PatchSet.Id primaryKey(PatchSet entity) {
-      return delegate.primaryKey(entity);
-    }
-
-    @Override
-    public Map<PatchSet.Id, PatchSet> toMap(Iterable<PatchSet> c) {
-      return delegate.toMap(c);
-    }
-
-    @SuppressWarnings("deprecation")
-    @Override
-    public com.google.common.util.concurrent.CheckedFuture<PatchSet, OrmException> getAsync(
-        PatchSet.Id key) {
-      return delegate.getAsync(key);
-    }
-
-    @Override
-    public ResultSet<PatchSet> get(Iterable<PatchSet.Id> keys) throws OrmException {
-      return delegate.get(keys);
-    }
-
-    @Override
-    public void insert(Iterable<PatchSet> instances) throws OrmException {
-      delegate.insert(instances);
-    }
-
-    @Override
-    public void update(Iterable<PatchSet> instances) throws OrmException {
-      delegate.update(instances);
-    }
-
-    @Override
-    public void upsert(Iterable<PatchSet> instances) throws OrmException {
-      delegate.upsert(instances);
-    }
-
-    @Override
-    public void deleteKeys(Iterable<PatchSet.Id> keys) throws OrmException {
-      delegate.deleteKeys(keys);
-    }
-
-    @Override
-    public void delete(Iterable<PatchSet> instances) throws OrmException {
-      delegate.delete(instances);
-    }
-
-    @Override
-    public void beginTransaction(PatchSet.Id key) throws OrmException {
-      delegate.beginTransaction(key);
-    }
-
-    @Override
-    public PatchSet atomicUpdate(PatchSet.Id key, AtomicUpdate<PatchSet> update)
-        throws OrmException {
-      return delegate.atomicUpdate(key, update);
-    }
-
-    @Override
-    public PatchSet get(PatchSet.Id id) throws OrmException {
-      return delegate.get(id);
-    }
-
-    @Override
-    public ResultSet<PatchSet> byChange(Change.Id id) throws OrmException {
-      return delegate.byChange(id);
-    }
-
-    @Override
-    public ResultSet<PatchSet> all() throws OrmException {
-      return delegate.all();
-    }
-  }
-
-  public static class PatchLineCommentAccessWrapper implements PatchLineCommentAccess {
-    protected PatchLineCommentAccess delegate;
-
-    protected PatchLineCommentAccessWrapper(PatchLineCommentAccess delegate) {
-      this.delegate = delegate;
-    }
-
-    @Override
-    public String getRelationName() {
-      return delegate.getRelationName();
-    }
-
-    @Override
-    public int getRelationID() {
-      return delegate.getRelationID();
-    }
-
-    @Override
-    public ResultSet<PatchLineComment> iterateAllEntities() throws OrmException {
-      return delegate.iterateAllEntities();
-    }
-
-    @Override
-    public PatchLineComment.Key primaryKey(PatchLineComment entity) {
-      return delegate.primaryKey(entity);
-    }
-
-    @Override
-    public Map<PatchLineComment.Key, PatchLineComment> toMap(Iterable<PatchLineComment> c) {
-      return delegate.toMap(c);
-    }
-
-    @SuppressWarnings("deprecation")
-    @Override
-    public com.google.common.util.concurrent.CheckedFuture<PatchLineComment, OrmException> getAsync(
-        PatchLineComment.Key key) {
-      return delegate.getAsync(key);
-    }
-
-    @Override
-    public ResultSet<PatchLineComment> get(Iterable<PatchLineComment.Key> keys)
-        throws OrmException {
-      return delegate.get(keys);
-    }
-
-    @Override
-    public void insert(Iterable<PatchLineComment> instances) throws OrmException {
-      delegate.insert(instances);
-    }
-
-    @Override
-    public void update(Iterable<PatchLineComment> instances) throws OrmException {
-      delegate.update(instances);
-    }
-
-    @Override
-    public void upsert(Iterable<PatchLineComment> instances) throws OrmException {
-      delegate.upsert(instances);
-    }
-
-    @Override
-    public void deleteKeys(Iterable<PatchLineComment.Key> keys) throws OrmException {
-      delegate.deleteKeys(keys);
-    }
-
-    @Override
-    public void delete(Iterable<PatchLineComment> instances) throws OrmException {
-      delegate.delete(instances);
-    }
-
-    @Override
-    public void beginTransaction(PatchLineComment.Key key) throws OrmException {
-      delegate.beginTransaction(key);
-    }
-
-    @Override
-    public PatchLineComment atomicUpdate(
-        PatchLineComment.Key key, AtomicUpdate<PatchLineComment> update) throws OrmException {
-      return delegate.atomicUpdate(key, update);
-    }
-
-    @Override
-    public PatchLineComment get(PatchLineComment.Key id) throws OrmException {
-      return delegate.get(id);
-    }
-
-    @Override
-    public ResultSet<PatchLineComment> byChange(Change.Id id) throws OrmException {
-      return delegate.byChange(id);
-    }
-
-    @Override
-    public ResultSet<PatchLineComment> byPatchSet(PatchSet.Id id) throws OrmException {
-      return delegate.byPatchSet(id);
-    }
-
-    @Override
-    public ResultSet<PatchLineComment> publishedByChangeFile(Change.Id id, String file)
-        throws OrmException {
-      return delegate.publishedByChangeFile(id, file);
-    }
-
-    @Override
-    public ResultSet<PatchLineComment> publishedByPatchSet(PatchSet.Id patchset)
-        throws OrmException {
-      return delegate.publishedByPatchSet(patchset);
-    }
-
-    @Override
-    public ResultSet<PatchLineComment> draftByPatchSetAuthor(
-        PatchSet.Id patchset, Account.Id author) throws OrmException {
-      return delegate.draftByPatchSetAuthor(patchset, author);
-    }
-
-    @Override
-    public ResultSet<PatchLineComment> draftByChangeFileAuthor(
-        Change.Id id, String file, Account.Id author) throws OrmException {
-      return delegate.draftByChangeFileAuthor(id, file, author);
-    }
-
-    @Override
-    public ResultSet<PatchLineComment> draftByAuthor(Account.Id author) throws OrmException {
-      return delegate.draftByAuthor(author);
-    }
-
-    @Override
-    public ResultSet<PatchLineComment> all() throws OrmException {
-      return delegate.all();
-    }
-  }
-
-  public static class AccountGroupAccessWrapper implements AccountGroupAccess {
-    protected final AccountGroupAccess delegate;
-
-    protected AccountGroupAccessWrapper(AccountGroupAccess delegate) {
-      this.delegate = requireNonNull(delegate);
-    }
-
-    @Override
-    public String getRelationName() {
-      return delegate.getRelationName();
-    }
-
-    @Override
-    public int getRelationID() {
-      return delegate.getRelationID();
-    }
-
-    @Override
-    public ResultSet<AccountGroup> iterateAllEntities() throws OrmException {
-      return delegate.iterateAllEntities();
-    }
-
-    @Override
-    public AccountGroup.Id primaryKey(AccountGroup entity) {
-      return delegate.primaryKey(entity);
-    }
-
-    @Override
-    public Map<AccountGroup.Id, AccountGroup> toMap(Iterable<AccountGroup> c) {
-      return delegate.toMap(c);
-    }
-
-    @SuppressWarnings("deprecation")
-    @Override
-    public com.google.common.util.concurrent.CheckedFuture<AccountGroup, OrmException> getAsync(
-        AccountGroup.Id key) {
-      return delegate.getAsync(key);
-    }
-
-    @Override
-    public ResultSet<AccountGroup> get(Iterable<AccountGroup.Id> keys) throws OrmException {
-      return delegate.get(keys);
-    }
-
-    @Override
-    public void insert(Iterable<AccountGroup> instances) throws OrmException {
-      delegate.insert(instances);
-    }
-
-    @Override
-    public void update(Iterable<AccountGroup> instances) throws OrmException {
-      delegate.update(instances);
-    }
-
-    @Override
-    public void upsert(Iterable<AccountGroup> instances) throws OrmException {
-      delegate.upsert(instances);
-    }
-
-    @Override
-    public void deleteKeys(Iterable<AccountGroup.Id> keys) throws OrmException {
-      delegate.deleteKeys(keys);
-    }
-
-    @Override
-    public void delete(Iterable<AccountGroup> instances) throws OrmException {
-      delegate.delete(instances);
-    }
-
-    @Override
-    public void beginTransaction(AccountGroup.Id key) throws OrmException {
-      delegate.beginTransaction(key);
-    }
-
-    @Override
-    public AccountGroup atomicUpdate(AccountGroup.Id key, AtomicUpdate<AccountGroup> update)
-        throws OrmException {
-      return delegate.atomicUpdate(key, update);
-    }
-
-    @Override
-    public AccountGroup get(AccountGroup.Id id) throws OrmException {
-      return delegate.get(id);
-    }
-
-    @Override
-    public ResultSet<AccountGroup> byUUID(AccountGroup.UUID uuid) throws OrmException {
-      return delegate.byUUID(uuid);
-    }
-
-    @Override
-    public ResultSet<AccountGroup> all() throws OrmException {
-      return delegate.all();
-    }
-  }
-
-  public static class AccountGroupNameAccessWrapper implements AccountGroupNameAccess {
-    protected final AccountGroupNameAccess delegate;
-
-    protected AccountGroupNameAccessWrapper(AccountGroupNameAccess delegate) {
-      this.delegate = requireNonNull(delegate);
-    }
-
-    @Override
-    public String getRelationName() {
-      return delegate.getRelationName();
-    }
-
-    @Override
-    public int getRelationID() {
-      return delegate.getRelationID();
-    }
-
-    @Override
-    public ResultSet<AccountGroupName> iterateAllEntities() throws OrmException {
-      return delegate.iterateAllEntities();
-    }
-
-    @Override
-    public AccountGroup.NameKey primaryKey(AccountGroupName entity) {
-      return delegate.primaryKey(entity);
-    }
-
-    @Override
-    public Map<AccountGroup.NameKey, AccountGroupName> toMap(Iterable<AccountGroupName> c) {
-      return delegate.toMap(c);
-    }
-
-    @SuppressWarnings("deprecation")
-    @Override
-    public com.google.common.util.concurrent.CheckedFuture<AccountGroupName, OrmException> getAsync(
-        AccountGroup.NameKey key) {
-      return delegate.getAsync(key);
-    }
-
-    @Override
-    public ResultSet<AccountGroupName> get(Iterable<AccountGroup.NameKey> keys)
-        throws OrmException {
-      return delegate.get(keys);
-    }
-
-    @Override
-    public void insert(Iterable<AccountGroupName> instances) throws OrmException {
-      delegate.insert(instances);
-    }
-
-    @Override
-    public void update(Iterable<AccountGroupName> instances) throws OrmException {
-      delegate.update(instances);
-    }
-
-    @Override
-    public void upsert(Iterable<AccountGroupName> instances) throws OrmException {
-      delegate.upsert(instances);
-    }
-
-    @Override
-    public void deleteKeys(Iterable<AccountGroup.NameKey> keys) throws OrmException {
-      delegate.deleteKeys(keys);
-    }
-
-    @Override
-    public void delete(Iterable<AccountGroupName> instances) throws OrmException {
-      delegate.delete(instances);
-    }
-
-    @Override
-    public void beginTransaction(AccountGroup.NameKey key) throws OrmException {
-      delegate.beginTransaction(key);
-    }
-
-    @Override
-    public AccountGroupName atomicUpdate(
-        AccountGroup.NameKey key, AtomicUpdate<AccountGroupName> update) throws OrmException {
-      return delegate.atomicUpdate(key, update);
-    }
-
-    @Override
-    public AccountGroupName get(AccountGroup.NameKey name) throws OrmException {
-      return delegate.get(name);
-    }
-
-    @Override
-    public ResultSet<AccountGroupName> all() throws OrmException {
-      return delegate.all();
-    }
-  }
-
-  public static class AccountGroupMemberAccessWrapper implements AccountGroupMemberAccess {
-    protected final AccountGroupMemberAccess delegate;
-
-    protected AccountGroupMemberAccessWrapper(AccountGroupMemberAccess delegate) {
-      this.delegate = requireNonNull(delegate);
-    }
-
-    @Override
-    public String getRelationName() {
-      return delegate.getRelationName();
-    }
-
-    @Override
-    public int getRelationID() {
-      return delegate.getRelationID();
-    }
-
-    @Override
-    public ResultSet<AccountGroupMember> iterateAllEntities() throws OrmException {
-      return delegate.iterateAllEntities();
-    }
-
-    @Override
-    public AccountGroupMember.Key primaryKey(AccountGroupMember entity) {
-      return delegate.primaryKey(entity);
-    }
-
-    @Override
-    public Map<AccountGroupMember.Key, AccountGroupMember> toMap(Iterable<AccountGroupMember> c) {
-      return delegate.toMap(c);
-    }
-
-    @SuppressWarnings("deprecation")
-    @Override
-    public com.google.common.util.concurrent.CheckedFuture<AccountGroupMember, OrmException>
-        getAsync(AccountGroupMember.Key key) {
-      return delegate.getAsync(key);
-    }
-
-    @Override
-    public ResultSet<AccountGroupMember> get(Iterable<AccountGroupMember.Key> keys)
-        throws OrmException {
-      return delegate.get(keys);
-    }
-
-    @Override
-    public void insert(Iterable<AccountGroupMember> instances) throws OrmException {
-      delegate.insert(instances);
-    }
-
-    @Override
-    public void update(Iterable<AccountGroupMember> instances) throws OrmException {
-      delegate.update(instances);
-    }
-
-    @Override
-    public void upsert(Iterable<AccountGroupMember> instances) throws OrmException {
-      delegate.upsert(instances);
-    }
-
-    @Override
-    public void deleteKeys(Iterable<AccountGroupMember.Key> keys) throws OrmException {
-      delegate.deleteKeys(keys);
-    }
-
-    @Override
-    public void delete(Iterable<AccountGroupMember> instances) throws OrmException {
-      delegate.delete(instances);
-    }
-
-    @Override
-    public void beginTransaction(AccountGroupMember.Key key) throws OrmException {
-      delegate.beginTransaction(key);
-    }
-
-    @Override
-    public AccountGroupMember atomicUpdate(
-        AccountGroupMember.Key key, AtomicUpdate<AccountGroupMember> update) throws OrmException {
-      return delegate.atomicUpdate(key, update);
-    }
-
-    @Override
-    public AccountGroupMember get(AccountGroupMember.Key key) throws OrmException {
-      return delegate.get(key);
-    }
-
-    @Override
-    public ResultSet<AccountGroupMember> byAccount(Account.Id id) throws OrmException {
-      return delegate.byAccount(id);
-    }
-
-    @Override
-    public ResultSet<AccountGroupMember> byGroup(AccountGroup.Id id) throws OrmException {
-      return delegate.byGroup(id);
-    }
-  }
-
-  public static class AccountGroupMemberAuditAccessWrapper
-      implements AccountGroupMemberAuditAccess {
-    protected final AccountGroupMemberAuditAccess delegate;
-
-    protected AccountGroupMemberAuditAccessWrapper(AccountGroupMemberAuditAccess delegate) {
-      this.delegate = requireNonNull(delegate);
-    }
-
-    @Override
-    public String getRelationName() {
-      return delegate.getRelationName();
-    }
-
-    @Override
-    public int getRelationID() {
-      return delegate.getRelationID();
-    }
-
-    @Override
-    public ResultSet<AccountGroupMemberAudit> iterateAllEntities() throws OrmException {
-      return delegate.iterateAllEntities();
-    }
-
-    @Override
-    public AccountGroupMemberAudit.Key primaryKey(AccountGroupMemberAudit entity) {
-      return delegate.primaryKey(entity);
-    }
-
-    @Override
-    public Map<AccountGroupMemberAudit.Key, AccountGroupMemberAudit> toMap(
-        Iterable<AccountGroupMemberAudit> c) {
-      return delegate.toMap(c);
-    }
-
-    @SuppressWarnings("deprecation")
-    @Override
-    public com.google.common.util.concurrent.CheckedFuture<AccountGroupMemberAudit, OrmException>
-        getAsync(AccountGroupMemberAudit.Key key) {
-      return delegate.getAsync(key);
-    }
-
-    @Override
-    public ResultSet<AccountGroupMemberAudit> get(Iterable<AccountGroupMemberAudit.Key> keys)
-        throws OrmException {
-      return delegate.get(keys);
-    }
-
-    @Override
-    public void insert(Iterable<AccountGroupMemberAudit> instances) throws OrmException {
-      delegate.insert(instances);
-    }
-
-    @Override
-    public void update(Iterable<AccountGroupMemberAudit> instances) throws OrmException {
-      delegate.update(instances);
-    }
-
-    @Override
-    public void upsert(Iterable<AccountGroupMemberAudit> instances) throws OrmException {
-      delegate.upsert(instances);
-    }
-
-    @Override
-    public void deleteKeys(Iterable<AccountGroupMemberAudit.Key> keys) throws OrmException {
-      delegate.deleteKeys(keys);
-    }
-
-    @Override
-    public void delete(Iterable<AccountGroupMemberAudit> instances) throws OrmException {
-      delegate.delete(instances);
-    }
-
-    @Override
-    public void beginTransaction(AccountGroupMemberAudit.Key key) throws OrmException {
-      delegate.beginTransaction(key);
-    }
-
-    @Override
-    public AccountGroupMemberAudit atomicUpdate(
-        AccountGroupMemberAudit.Key key, AtomicUpdate<AccountGroupMemberAudit> update)
-        throws OrmException {
-      return delegate.atomicUpdate(key, update);
-    }
-
-    @Override
-    public AccountGroupMemberAudit get(AccountGroupMemberAudit.Key key) throws OrmException {
-      return delegate.get(key);
-    }
-
-    @Override
-    public ResultSet<AccountGroupMemberAudit> byGroupAccount(
-        AccountGroup.Id groupId, Account.Id accountId) throws OrmException {
-      return delegate.byGroupAccount(groupId, accountId);
-    }
-
-    @Override
-    public ResultSet<AccountGroupMemberAudit> byGroup(AccountGroup.Id groupId) throws OrmException {
-      return delegate.byGroup(groupId);
-    }
-  }
-
-  public static class AccountGroupByIdAccessWrapper implements AccountGroupByIdAccess {
-    protected final AccountGroupByIdAccess delegate;
-
-    protected AccountGroupByIdAccessWrapper(AccountGroupByIdAccess delegate) {
-      this.delegate = requireNonNull(delegate);
-    }
-
-    @Override
-    public String getRelationName() {
-      return delegate.getRelationName();
-    }
-
-    @Override
-    public int getRelationID() {
-      return delegate.getRelationID();
-    }
-
-    @Override
-    public ResultSet<AccountGroupById> iterateAllEntities() throws OrmException {
-      return delegate.iterateAllEntities();
-    }
-
-    @Override
-    public AccountGroupById.Key primaryKey(AccountGroupById entity) {
-      return delegate.primaryKey(entity);
-    }
-
-    @Override
-    public Map<AccountGroupById.Key, AccountGroupById> toMap(Iterable<AccountGroupById> c) {
-      return delegate.toMap(c);
-    }
-
-    @SuppressWarnings("deprecation")
-    @Override
-    public com.google.common.util.concurrent.CheckedFuture<AccountGroupById, OrmException> getAsync(
-        AccountGroupById.Key key) {
-      return delegate.getAsync(key);
-    }
-
-    @Override
-    public ResultSet<AccountGroupById> get(Iterable<AccountGroupById.Key> keys)
-        throws OrmException {
-      return delegate.get(keys);
-    }
-
-    @Override
-    public void insert(Iterable<AccountGroupById> instances) throws OrmException {
-      delegate.insert(instances);
-    }
-
-    @Override
-    public void update(Iterable<AccountGroupById> instances) throws OrmException {
-      delegate.update(instances);
-    }
-
-    @Override
-    public void upsert(Iterable<AccountGroupById> instances) throws OrmException {
-      delegate.upsert(instances);
-    }
-
-    @Override
-    public void deleteKeys(Iterable<AccountGroupById.Key> keys) throws OrmException {
-      delegate.deleteKeys(keys);
-    }
-
-    @Override
-    public void delete(Iterable<AccountGroupById> instances) throws OrmException {
-      delegate.delete(instances);
-    }
-
-    @Override
-    public void beginTransaction(AccountGroupById.Key key) throws OrmException {
-      delegate.beginTransaction(key);
-    }
-
-    @Override
-    public AccountGroupById atomicUpdate(
-        AccountGroupById.Key key, AtomicUpdate<AccountGroupById> update) throws OrmException {
-      return delegate.atomicUpdate(key, update);
-    }
-
-    @Override
-    public AccountGroupById get(AccountGroupById.Key key) throws OrmException {
-      return delegate.get(key);
-    }
-
-    @Override
-    public ResultSet<AccountGroupById> byIncludeUUID(AccountGroup.UUID uuid) throws OrmException {
-      return delegate.byIncludeUUID(uuid);
-    }
-
-    @Override
-    public ResultSet<AccountGroupById> byGroup(AccountGroup.Id id) throws OrmException {
-      return delegate.byGroup(id);
-    }
-
-    @Override
-    public ResultSet<AccountGroupById> all() throws OrmException {
-      return delegate.all();
-    }
-  }
-
-  public static class AccountGroupByIdAudAccessWrapper implements AccountGroupByIdAudAccess {
-    protected final AccountGroupByIdAudAccess delegate;
-
-    protected AccountGroupByIdAudAccessWrapper(AccountGroupByIdAudAccess delegate) {
-      this.delegate = requireNonNull(delegate);
-    }
-
-    @Override
-    public String getRelationName() {
-      return delegate.getRelationName();
-    }
-
-    @Override
-    public int getRelationID() {
-      return delegate.getRelationID();
-    }
-
-    @Override
-    public ResultSet<AccountGroupByIdAud> iterateAllEntities() throws OrmException {
-      return delegate.iterateAllEntities();
-    }
-
-    @Override
-    public AccountGroupByIdAud.Key primaryKey(AccountGroupByIdAud entity) {
-      return delegate.primaryKey(entity);
-    }
-
-    @Override
-    public Map<AccountGroupByIdAud.Key, AccountGroupByIdAud> toMap(
-        Iterable<AccountGroupByIdAud> c) {
-      return delegate.toMap(c);
-    }
-
-    @SuppressWarnings("deprecation")
-    @Override
-    public com.google.common.util.concurrent.CheckedFuture<AccountGroupByIdAud, OrmException>
-        getAsync(AccountGroupByIdAud.Key key) {
-      return delegate.getAsync(key);
-    }
-
-    @Override
-    public ResultSet<AccountGroupByIdAud> get(Iterable<AccountGroupByIdAud.Key> keys)
-        throws OrmException {
-      return delegate.get(keys);
-    }
-
-    @Override
-    public void insert(Iterable<AccountGroupByIdAud> instances) throws OrmException {
-      delegate.insert(instances);
-    }
-
-    @Override
-    public void update(Iterable<AccountGroupByIdAud> instances) throws OrmException {
-      delegate.update(instances);
-    }
-
-    @Override
-    public void upsert(Iterable<AccountGroupByIdAud> instances) throws OrmException {
-      delegate.upsert(instances);
-    }
-
-    @Override
-    public void deleteKeys(Iterable<AccountGroupByIdAud.Key> keys) throws OrmException {
-      delegate.deleteKeys(keys);
-    }
-
-    @Override
-    public void delete(Iterable<AccountGroupByIdAud> instances) throws OrmException {
-      delegate.delete(instances);
-    }
-
-    @Override
-    public void beginTransaction(AccountGroupByIdAud.Key key) throws OrmException {
-      delegate.beginTransaction(key);
-    }
-
-    @Override
-    public AccountGroupByIdAud atomicUpdate(
-        AccountGroupByIdAud.Key key, AtomicUpdate<AccountGroupByIdAud> update) throws OrmException {
-      return delegate.atomicUpdate(key, update);
-    }
-
-    @Override
-    public AccountGroupByIdAud get(AccountGroupByIdAud.Key key) throws OrmException {
-      return delegate.get(key);
-    }
-
-    @Override
-    public ResultSet<AccountGroupByIdAud> byGroupInclude(
-        AccountGroup.Id groupId, AccountGroup.UUID incGroupUUID) throws OrmException {
-      return delegate.byGroupInclude(groupId, incGroupUUID);
-    }
-
-    @Override
-    public ResultSet<AccountGroupByIdAud> byGroup(AccountGroup.Id groupId) throws OrmException {
-      return delegate.byGroup(groupId);
-    }
-  }
-}
diff --git a/java/com/google/gerrit/reviewdb/server/SchemaVersionAccess.java b/java/com/google/gerrit/reviewdb/server/SchemaVersionAccess.java
deleted file mode 100644
index 8819a6c..0000000
--- a/java/com/google/gerrit/reviewdb/server/SchemaVersionAccess.java
+++ /dev/null
@@ -1,28 +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.server;
-
-import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.PrimaryKey;
-
-/** Access interface for {@link CurrentSchemaVersion}. */
-public interface SchemaVersionAccess
-    extends Access<CurrentSchemaVersion, CurrentSchemaVersion.Key> {
-  @Override
-  @PrimaryKey("singleton")
-  CurrentSchemaVersion get(CurrentSchemaVersion.Key key) throws OrmException;
-}
diff --git a/java/com/google/gerrit/server/AccessPath.java b/java/com/google/gerrit/server/AccessPath.java
index cb720c8..4d07d62 100644
--- a/java/com/google/gerrit/server/AccessPath.java
+++ b/java/com/google/gerrit/server/AccessPath.java
@@ -22,9 +22,6 @@
   /** Access through the REST API. */
   REST_API,
 
-  /** Access through the old JSON-RPC interface. */
-  JSON_RPC,
-
   /** Access by a web cookie. This path is not protected like REST_API. */
   WEB_BROWSER,
 
diff --git a/java/com/google/gerrit/server/ApprovalCopier.java b/java/com/google/gerrit/server/ApprovalCopier.java
index c3d3b60..a1df711 100644
--- a/java/com/google/gerrit/server/ApprovalCopier.java
+++ b/java/com/google/gerrit/server/ApprovalCopier.java
@@ -22,19 +22,17 @@
 import com.google.common.collect.Table;
 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.reviewdb.server.ReviewDb;
 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.notedb.NoteDbChangeState.PrimaryStorage;
 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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -74,86 +72,32 @@
     this.psUtil = psUtil;
   }
 
-  /**
-   * Apply approval copy settings from prior PatchSets to a new PatchSet.
-   *
-   * @param db review database.
-   * @param notes change notes for user uploading PatchSet
-   * @param ps new PatchSet
-   * @param rw open walk that can read the patch set commit; null to open the repo on demand.
-   * @param repoConfig repo config used for change kind detection; null to read from repo on demand.
-   * @throws OrmException
-   */
-  public void copyInReviewDb(
-      ReviewDb db,
-      ChangeNotes notes,
-      PatchSet ps,
-      @Nullable RevWalk rw,
-      @Nullable Config repoConfig)
-      throws OrmException {
-    copyInReviewDb(db, notes, ps, rw, repoConfig, Collections.emptyList());
-  }
-
-  /**
-   * Apply approval copy settings from prior PatchSets to a new PatchSet.
-   *
-   * @param db review database.
-   * @param notes change notes for user uploading PatchSet
-   * @param ps new PatchSet
-   * @param rw open walk that can read the patch set commit; null to open the repo on demand.
-   * @param repoConfig repo config used for change kind detection; null to read from repo on demand.
-   * @param dontCopy PatchSetApprovals indicating which (account, label) pairs should not be copied
-   * @throws OrmException
-   */
-  public void copyInReviewDb(
-      ReviewDb db,
-      ChangeNotes notes,
-      PatchSet ps,
-      @Nullable RevWalk rw,
-      @Nullable Config repoConfig,
-      Iterable<PatchSetApproval> dontCopy)
-      throws OrmException {
-    if (PrimaryStorage.of(notes.getChange()) == PrimaryStorage.REVIEW_DB) {
-      db.patchSetApprovals().insert(getForPatchSet(db, notes, ps, rw, repoConfig, dontCopy));
-    }
+  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(
-      ReviewDb db,
-      ChangeNotes notes,
-      PatchSet.Id psId,
-      @Nullable RevWalk rw,
-      @Nullable Config repoConfig)
-      throws OrmException {
-    return getForPatchSet(
-        db, notes, psId, rw, repoConfig, Collections.<PatchSetApproval>emptyList());
-  }
-
-  Iterable<PatchSetApproval> getForPatchSet(
-      ReviewDb db,
       ChangeNotes notes,
       PatchSet.Id psId,
       @Nullable RevWalk rw,
       @Nullable Config repoConfig,
-      Iterable<PatchSetApproval> dontCopy)
-      throws OrmException {
-    PatchSet ps = psUtil.get(db, notes, psId);
+      Iterable<PatchSetApproval> dontCopy) {
+    PatchSet ps = psUtil.get(notes, psId);
     if (ps == null) {
       return Collections.emptyList();
     }
-    return getForPatchSet(db, notes, ps, rw, repoConfig, dontCopy);
+    return getForPatchSet(notes, ps, rw, repoConfig, dontCopy);
   }
 
   private Iterable<PatchSetApproval> getForPatchSet(
-      ReviewDb db,
       ChangeNotes notes,
       PatchSet ps,
       @Nullable RevWalk rw,
       @Nullable Config repoConfig,
-      Iterable<PatchSetApproval> dontCopy)
-      throws OrmException {
+      Iterable<PatchSetApproval> dontCopy) {
     requireNonNull(ps, "ps should not be null");
-    ChangeData cd = changeDataFactory.create(db, notes);
+    ChangeData cd = changeDataFactory.create(notes);
     try {
       ProjectState project = projectCache.checkedGet(cd.change().getDest().getParentKey());
       ListMultimap<PatchSet.Id, PatchSetApproval> all = cd.approvals();
@@ -206,11 +150,11 @@
       }
       return labelNormalizer.normalize(notes, byUser.values()).getNormalized();
     } catch (IOException e) {
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
   }
 
-  private static TreeMap<Integer, PatchSet> getPatchSets(ChangeData cd) throws OrmException {
+  private static TreeMap<Integer, PatchSet> getPatchSets(ChangeData cd) {
     Collection<PatchSet> patchSets = cd.patchSets();
     TreeMap<Integer, PatchSet> result = new TreeMap<>();
     for (PatchSet ps : patchSets) {
diff --git a/java/com/google/gerrit/server/ApprovalsUtil.java b/java/com/google/gerrit/server/ApprovalsUtil.java
index e2b7c69..135276e 100644
--- a/java/com/google/gerrit/server/ApprovalsUtil.java
+++ b/java/com/google/gerrit/server/ApprovalsUtil.java
@@ -17,21 +17,19 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.CC;
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
-import static java.util.Comparator.comparing;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
-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.Ordering;
 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.exceptions.StorageException;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -41,10 +39,8 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.LabelPermission;
@@ -52,7 +48,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.util.LabelVote;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -83,13 +78,6 @@
 public class ApprovalsUtil {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  private static final Ordering<PatchSetApproval> SORT_APPROVALS =
-      Ordering.from(comparing(PatchSetApproval::getGranted));
-
-  public static List<PatchSetApproval> sortApprovals(Iterable<PatchSetApproval> approvals) {
-    return SORT_APPROVALS.sortedCopy(approvals);
-  }
-
   public static PatchSetApproval newApproval(
       PatchSet.Id psId, CurrentUser user, LabelId labelId, int value, Date when) {
     PatchSetApproval psa =
@@ -106,7 +94,6 @@
     return Iterables.filter(psas, a -> Objects.equals(a.getAccountId(), accountId));
   }
 
-  private final NotesMigration migration;
   private final ApprovalCopier copier;
   private final PermissionBackend permissionBackend;
   private final ProjectCache projectCache;
@@ -114,11 +101,7 @@
   @VisibleForTesting
   @Inject
   public ApprovalsUtil(
-      NotesMigration migration,
-      ApprovalCopier copier,
-      PermissionBackend permissionBackend,
-      ProjectCache projectCache) {
-    this.migration = migration;
+      ApprovalCopier copier, PermissionBackend permissionBackend, ProjectCache projectCache) {
     this.copier = copier;
     this.permissionBackend = permissionBackend;
     this.projectCache = projectCache;
@@ -127,15 +110,10 @@
   /**
    * Get all reviewers for a change.
    *
-   * @param db review database.
    * @param notes change notes.
    * @return reviewers for the change.
-   * @throws OrmException if reviewers for the change could not be read.
    */
-  public ReviewerSet getReviewers(ReviewDb db, ChangeNotes notes) throws OrmException {
-    if (!migration.readChanges()) {
-      return ReviewerSet.fromApprovals(db.patchSetApprovals().byChange(notes.getChangeId()));
-    }
+  public ReviewerSet getReviewers(ChangeNotes notes) {
     return notes.load().getReviewers();
   }
 
@@ -144,42 +122,30 @@
    *
    * @param allApprovals all approvals to consider; must all belong to the same change.
    * @return reviewers for the change.
-   * @throws OrmException if reviewers for the change could not be read.
    */
-  public ReviewerSet getReviewers(ChangeNotes notes, Iterable<PatchSetApproval> allApprovals)
-      throws OrmException {
-    if (!migration.readChanges()) {
-      return ReviewerSet.fromApprovals(allApprovals);
-    }
+  public ReviewerSet getReviewers(ChangeNotes notes, Iterable<PatchSetApproval> allApprovals) {
     return notes.load().getReviewers();
   }
 
   /**
-   * Get updates to reviewer set. Always returns empty list for ReviewDb.
+   * Get updates to reviewer set.
    *
    * @param notes change notes.
    * @return reviewer updates for the change.
-   * @throws OrmException if reviewer updates for the change could not be read.
    */
-  public List<ReviewerStatusUpdate> getReviewerUpdates(ChangeNotes notes) throws OrmException {
-    if (!migration.readChanges()) {
-      return ImmutableList.of();
-    }
+  public List<ReviewerStatusUpdate> getReviewerUpdates(ChangeNotes notes) {
     return notes.load().getReviewerUpdates();
   }
 
   public List<PatchSetApproval> addReviewers(
-      ReviewDb db,
       ChangeUpdate update,
       LabelTypes labelTypes,
       Change change,
       PatchSet ps,
       PatchSetInfo info,
       Iterable<Account.Id> wantReviewers,
-      Collection<Account.Id> existingReviewers)
-      throws OrmException {
+      Collection<Account.Id> existingReviewers) {
     return addReviewers(
-        db,
         update,
         labelTypes,
         change,
@@ -191,22 +157,14 @@
   }
 
   public List<PatchSetApproval> addReviewers(
-      ReviewDb db,
       ChangeNotes notes,
       ChangeUpdate update,
       LabelTypes labelTypes,
       Change change,
-      Iterable<Account.Id> wantReviewers)
-      throws OrmException {
+      Iterable<Account.Id> wantReviewers) {
     PatchSet.Id psId = change.currentPatchSetId();
     Collection<Account.Id> existingReviewers;
-    if (migration.readChanges()) {
-      // If using NoteDB, we only want reviewers in the REVIEWER state.
-      existingReviewers = notes.load().getReviewers().byState(REVIEWER);
-    } else {
-      // Prior to NoteDB, we gather all reviewers regardless of state.
-      existingReviewers = getReviewers(db, notes).all();
-    }
+    existingReviewers = notes.load().getReviewers().byState(REVIEWER);
     // Existing reviewers should include pending additions in the REVIEWER
     // state, taken from ChangeUpdate.
     existingReviewers = Lists.newArrayList(existingReviewers);
@@ -216,11 +174,10 @@
       }
     }
     return addReviewers(
-        db, update, labelTypes, change, psId, null, null, wantReviewers, existingReviewers);
+        update, labelTypes, change, psId, null, null, wantReviewers, existingReviewers);
   }
 
   private List<PatchSetApproval> addReviewers(
-      ReviewDb db,
       ChangeUpdate update,
       LabelTypes labelTypes,
       Change change,
@@ -228,19 +185,18 @@
       Account.Id authorId,
       Account.Id committerId,
       Iterable<Account.Id> wantReviewers,
-      Collection<Account.Id> existingReviewers)
-      throws OrmException {
+      Collection<Account.Id> existingReviewers) {
     List<LabelType> allTypes = labelTypes.getLabelTypes();
     if (allTypes.isEmpty()) {
       return ImmutableList.of();
     }
 
     Set<Account.Id> need = Sets.newLinkedHashSet(wantReviewers);
-    if (authorId != null && canSee(db, update.getNotes(), authorId)) {
+    if (authorId != null && canSee(update.getNotes(), authorId)) {
       need.add(authorId);
     }
 
-    if (committerId != null && canSee(db, update.getNotes(), committerId)) {
+    if (committerId != null && canSee(update.getNotes(), committerId)) {
       need.add(committerId);
     }
     need.remove(change.getOwner());
@@ -257,20 +213,15 @@
               new PatchSetApproval.Key(psId, account, labelId), (short) 0, update.getWhen()));
       update.putReviewer(account, REVIEWER);
     }
-    db.patchSetApprovals().upsert(cells);
     return Collections.unmodifiableList(cells);
   }
 
-  private boolean canSee(ReviewDb db, ChangeNotes notes, Account.Id accountId) {
+  private boolean canSee(ChangeNotes notes, Account.Id accountId) {
     try {
       if (!projectCache.checkedGet(notes.getProjectName()).statePermitsRead()) {
         return false;
       }
-      permissionBackend
-          .absentUser(accountId)
-          .change(notes)
-          .database(db)
-          .check(ChangePermission.READ);
+      permissionBackend.absentUser(accountId).change(notes).check(ChangePermission.READ);
       return true;
     } catch (AuthException e) {
       return false;
@@ -289,10 +240,9 @@
    * @param update change update.
    * @param wantCCs accounts to CC.
    * @return whether a change was made.
-   * @throws OrmException
    */
   public Collection<Account.Id> addCcs(
-      ChangeNotes notes, ChangeUpdate update, Collection<Account.Id> wantCCs) throws OrmException {
+      ChangeNotes notes, ChangeUpdate update, Collection<Account.Id> wantCCs) {
     return addCcs(update, wantCCs, notes.load().getReviewers());
   }
 
@@ -308,25 +258,22 @@
   }
 
   /**
-   * Adds approvals to ChangeUpdate for a new patch set, and writes to ReviewDb.
+   * Adds approvals to ChangeUpdate for a new patch set, and writes to NoteDb.
    *
-   * @param db review database.
    * @param update change update.
    * @param labelTypes label types for the containing project.
    * @param ps patch set being approved.
    * @param user user adding approvals.
    * @param approvals approvals to add.
    * @throws RestApiException
-   * @throws OrmException
    */
   public Iterable<PatchSetApproval> addApprovalsForNewPatchSet(
-      ReviewDb db,
       ChangeUpdate update,
       LabelTypes labelTypes,
       PatchSet ps,
       CurrentUser user,
       Map<String, Short> approvals)
-      throws RestApiException, OrmException, PermissionBackendException {
+      throws RestApiException, PermissionBackendException {
     Account.Id accountId = user.getAccountId();
     checkArgument(
         accountId.equals(ps.getUploader()),
@@ -336,7 +283,7 @@
     if (approvals.isEmpty()) {
       return ImmutableList.of();
     }
-    checkApprovals(approvals, permissionBackend.user(user).database(db).change(update.getNotes()));
+    checkApprovals(approvals, permissionBackend.user(user).change(update.getNotes()));
     List<PatchSetApproval> cells = new ArrayList<>(approvals.size());
     Date ts = update.getWhen();
     for (Map.Entry<String, Short> vote : approvals.entrySet()) {
@@ -346,7 +293,6 @@
     for (PatchSetApproval psa : cells) {
       update.putApproval(psa.getLabel(), psa.getValue());
     }
-    db.patchSetApprovals().insert(cells);
     return cells;
   }
 
@@ -377,54 +323,32 @@
     }
   }
 
-  public ListMultimap<PatchSet.Id, PatchSetApproval> byChange(ReviewDb db, ChangeNotes notes)
-      throws OrmException {
-    if (!migration.readChanges()) {
-      ImmutableListMultimap.Builder<PatchSet.Id, PatchSetApproval> result =
-          ImmutableListMultimap.builder();
-      for (PatchSetApproval psa : db.patchSetApprovals().byChange(notes.getChangeId())) {
-        result.put(psa.getPatchSetId(), psa);
-      }
-      return result.build();
-    }
+  public ListMultimap<PatchSet.Id, PatchSetApproval> byChange(ChangeNotes notes) {
     return notes.load().getApprovals();
   }
 
   public Iterable<PatchSetApproval> byPatchSet(
-      ReviewDb db,
-      ChangeNotes notes,
-      PatchSet.Id psId,
-      @Nullable RevWalk rw,
-      @Nullable Config repoConfig)
-      throws OrmException {
-    if (!migration.readChanges()) {
-      return sortApprovals(db.patchSetApprovals().byPatchSet(psId));
-    }
-    return copier.getForPatchSet(db, notes, psId, rw, repoConfig);
+      ChangeNotes notes, PatchSet.Id psId, @Nullable RevWalk rw, @Nullable Config repoConfig) {
+    return copier.getForPatchSet(notes, psId, rw, repoConfig);
   }
 
   public Iterable<PatchSetApproval> byPatchSetUser(
-      ReviewDb db,
       ChangeNotes notes,
       PatchSet.Id psId,
       Account.Id accountId,
       @Nullable RevWalk rw,
-      @Nullable Config repoConfig)
-      throws OrmException {
-    if (!migration.readChanges()) {
-      return sortApprovals(db.patchSetApprovals().byPatchSetUser(psId, accountId));
-    }
-    return filterApprovals(byPatchSet(db, notes, psId, rw, repoConfig), accountId);
+      @Nullable Config repoConfig) {
+    return filterApprovals(byPatchSet(notes, psId, rw, repoConfig), accountId);
   }
 
-  public PatchSetApproval getSubmitter(ReviewDb db, ChangeNotes notes, PatchSet.Id c) {
+  public PatchSetApproval getSubmitter(ChangeNotes notes, PatchSet.Id c) {
     if (c == null) {
       return null;
     }
     try {
       // Submit approval is never copied, so bypass expensive byPatchSet call.
-      return getSubmitter(c, byChange(db, notes).get(c));
-    } catch (OrmException e) {
+      return getSubmitter(c, byChange(notes).get(c));
+    } catch (StorageException e) {
       return null;
     }
   }
diff --git a/java/com/google/gerrit/server/BUILD b/java/com/google/gerrit/server/BUILD
index cd7bd76..d2b7584 100644
--- a/java/com/google/gerrit/server/BUILD
+++ b/java/com/google/gerrit/server/BUILD
@@ -34,14 +34,19 @@
         ":constants",
         "//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/git",
         "//java/com/google/gerrit/index",
         "//java/com/google/gerrit/index:query_exception",
         "//java/com/google/gerrit/index/project",
+        "//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/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",
@@ -50,8 +55,8 @@
         "//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",
-        "//java/org/eclipse/jgit:server",
         "//lib:args4j",
         "//lib:autolink",
         "//lib:automaton",
@@ -84,8 +89,6 @@
         "//lib:gson",
         "//lib:guava",
         "//lib:guava-retrying",
-        "//lib:gwtjsonrpc",
-        "//lib:gwtorm",
         "//lib:jsch",
         "//lib:juniversalchardet",
         "//lib:mime-util",
diff --git a/java/com/google/gerrit/server/ChangeMessagesUtil.java b/java/com/google/gerrit/server/ChangeMessagesUtil.java
index 969cf38..97ba8f0 100644
--- a/java/com/google/gerrit/server/ChangeMessagesUtil.java
+++ b/java/com/google/gerrit/server/ChangeMessagesUtil.java
@@ -15,37 +15,23 @@
 package com.google.gerrit.server;
 
 import static com.google.common.base.Preconditions.checkState;
-import static com.google.gerrit.reviewdb.server.ReviewDbUtil.unwrapDb;
 import static java.util.Objects.requireNonNull;
 
-import com.google.common.annotations.VisibleForTesting;
 import com.google.gerrit.common.Nullable;
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
-import com.google.gerrit.server.notedb.NotesMigration;
-import com.google.gerrit.server.update.BatchUpdateReviewDb;
 import com.google.gerrit.server.update.ChangeContext;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.sql.Timestamp;
-import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
 
-/**
- * Utility functions to manipulate ChangeMessages.
- *
- * <p>These methods either query for and update ChangeMessages in the NoteDb or ReviewDb, depending
- * on the state of the NotesMigration.
- */
+/** Utility functions to manipulate ChangeMessages. */
 @Singleton
 public class ChangeMessagesUtil {
   public static final String AUTOGENERATED_TAG_PREFIX = "autogenerated:";
@@ -100,27 +86,11 @@
     return workInProgress ? TAG_UPLOADED_WIP_PATCH_SET : TAG_UPLOADED_PATCH_SET;
   }
 
-  private static List<ChangeMessage> sortChangeMessages(Iterable<ChangeMessage> changeMessage) {
-    return ChangeNotes.MESSAGE_BY_TIME.sortedCopy(changeMessage);
-  }
-
-  private final NotesMigration migration;
-
-  @VisibleForTesting
-  @Inject
-  public ChangeMessagesUtil(NotesMigration migration) {
-    this.migration = migration;
-  }
-
-  public List<ChangeMessage> byChange(ReviewDb db, ChangeNotes notes) throws OrmException {
-    if (!migration.readChanges()) {
-      return sortChangeMessages(db.changeMessages().byChange(notes.getChangeId()));
-    }
+  public List<ChangeMessage> byChange(ChangeNotes notes) {
     return notes.load().getChangeMessages();
   }
 
-  public void addChangeMessage(ReviewDb db, ChangeUpdate update, ChangeMessage changeMessage)
-      throws OrmException {
+  public void addChangeMessage(ChangeUpdate update, ChangeMessage changeMessage) {
     checkState(
         Objects.equals(changeMessage.getAuthor(), update.getNullableAccountId()),
         "cannot store change message by %s in update by %s",
@@ -128,72 +98,21 @@
         update.getNullableAccountId());
     update.setChangeMessage(changeMessage.getMessage());
     update.setTag(changeMessage.getTag());
-    db.changeMessages().insert(Collections.singleton(changeMessage));
   }
 
   /**
    * Replace an existing change message with the provided new message.
    *
    * <p>The ID of a change message is different between NoteDb and ReviewDb. In NoteDb, it's the
-   * commit SHA-1, but in ReviewDb it's generated randomly. To make sure the change message can be
-   * deleted from both NoteDb and ReviewDb, the index of the change message must be used rather than
-   * its ID.
+   * commit SHA-1, but in ReviewDb it was generated randomly. Taking the target message as an index
+   * rather than an ID allowed us to delete the message from both NoteDb and ReviewDb.
    *
-   * @param db the {@code ReviewDb} instance to update.
    * @param update change update.
-   * @param targetMessageIdx the index of the target change message.
+   * @param targetMessageId the id of the target change message.
    * @param newMessage the new message which is going to replace the old.
-   * @throws OrmException
    */
-  public void replaceChangeMessage(
-      ReviewDb db, ChangeUpdate update, int targetMessageIdx, String newMessage)
-      throws OrmException {
-    if (PrimaryStorage.of(update.getChange()).equals(PrimaryStorage.REVIEW_DB)) {
-      if (db instanceof BatchUpdateReviewDb) {
-        db = ((BatchUpdateReviewDb) db).unsafeGetDelegate();
-      }
-      db = unwrapDb(db);
-
-      List<ChangeMessage> messagesInReviewDb =
-          sortChangeMessages(db.changeMessages().byChange(update.getId()));
-      if (migration.readChanges()) {
-        sanityCheckForChangeMessages(messagesInReviewDb, update.getNotes().getChangeMessages());
-      }
-      ChangeMessage targetMessage = messagesInReviewDb.get(targetMessageIdx);
-      targetMessage.setMessage(newMessage);
-      db.changeMessages().upsert(Collections.singleton(targetMessage));
-    }
-
-    update.deleteChangeMessageByRewritingHistory(targetMessageIdx, newMessage);
-  }
-
-  private static void sanityCheckForChangeMessages(
-      List<ChangeMessage> messagesInReviewDb, List<ChangeMessage> messagesInNoteDb) {
-    String message =
-        String.format(
-            "Change messages in ReivewDb and NoteDb don't match: NoteDb %s; ReviewDb %s",
-            messagesInNoteDb, messagesInReviewDb);
-    if (messagesInReviewDb.size() != messagesInNoteDb.size()) {
-      throw new IllegalStateException(message);
-    }
-
-    for (int i = 0; i < messagesInReviewDb.size(); i++) {
-      ChangeMessage messageInReviewDb = messagesInReviewDb.get(i);
-      ChangeMessage messageInNoteDb = messagesInNoteDb.get(i);
-
-      // Don't compare the keys because they are different for the same change message in NoteDb and
-      // ReviewDb.
-      boolean isEqual =
-          Objects.equals(messageInReviewDb.getAuthor(), messageInNoteDb.getAuthor())
-              && Objects.equals(messageInReviewDb.getWrittenOn(), messageInNoteDb.getWrittenOn())
-              && Objects.equals(messageInReviewDb.getMessage(), messageInNoteDb.getMessage())
-              && Objects.equals(messageInReviewDb.getPatchSetId(), messageInNoteDb.getPatchSetId())
-              && Objects.equals(messageInReviewDb.getTag(), messageInNoteDb.getTag())
-              && Objects.equals(messageInReviewDb.getRealAuthor(), messageInNoteDb.getRealAuthor());
-      if (!isEqual) {
-        throw new IllegalStateException(message);
-      }
-    }
+  public void replaceChangeMessage(ChangeUpdate update, String targetMessageId, String newMessage) {
+    update.deleteChangeMessageByRewritingHistory(targetMessageId, newMessage);
   }
 
   /**
diff --git a/java/com/google/gerrit/server/ChangeUtil.java b/java/com/google/gerrit/server/ChangeUtil.java
index a13f105..b8a00f4 100644
--- a/java/com/google/gerrit/server/ChangeUtil.java
+++ b/java/com/google/gerrit/server/ChangeUtil.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server;
 
 import static java.util.Comparator.comparingInt;
+import static java.util.stream.Collectors.toSet;
 
-import com.google.common.collect.Maps;
 import com.google.common.collect.Ordering;
 import com.google.common.io.BaseEncoding;
 import com.google.gerrit.reviewdb.client.Change;
@@ -24,9 +24,11 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.security.SecureRandom;
+import java.util.Collection;
 import java.util.Map;
 import java.util.Random;
-import org.eclipse.jgit.lib.ObjectId;
+import java.util.Set;
+import java.util.stream.Stream;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 
@@ -66,18 +68,25 @@
   /**
    * Get the next patch set ID from a previously-read map of refs below the change prefix.
    *
-   * @param changeRefs map of ref suffix to SHA-1, where the keys are ref names with the {@code
-   *     refs/changes/CD/ABCD/} prefix stripped. All refs should be under {@code id}'s change ref
-   *     prefix.
+   * @param changeRefNames existing full change ref names with the same change ID as {@code id}.
    * @param id previous patch set ID.
    * @return next unused patch set ID for the same change, skipping any IDs whose corresponding ref
    *     names appear in the {@code changeRefs} map.
    */
-  public static PatchSet.Id nextPatchSetIdFromChangeRefsMap(
-      Map<String, ObjectId> changeRefs, PatchSet.Id id) {
-    int prefixLen = id.getParentKey().toRefPrefix().length();
+  public static PatchSet.Id nextPatchSetIdFromChangeRefs(
+      Collection<String> changeRefNames, PatchSet.Id id) {
+    return nextPatchSetIdFromChangeRefs(changeRefNames.stream(), id);
+  }
+
+  private static PatchSet.Id nextPatchSetIdFromChangeRefs(
+      Stream<String> changeRefNames, PatchSet.Id id) {
+    Set<PatchSet.Id> existing =
+        changeRefNames
+            .map(PatchSet.Id::fromRef)
+            .filter(psId -> psId != null && psId.getParentKey().equals(id.getParentKey()))
+            .collect(toSet());
     PatchSet.Id next = nextPatchSetId(id);
-    while (changeRefs.containsKey(next.toRefName().substring(prefixLen))) {
+    while (existing.contains(next)) {
       next = nextPatchSetId(next);
     }
     return next;
@@ -88,7 +97,7 @@
    *
    * <p>This patch set ID may or may not be available in the database; callers that want a
    * previously-unused ID should use {@link #nextPatchSetIdFromAllRefsMap} or {@link
-   * #nextPatchSetIdFromChangeRefsMap}.
+   * #nextPatchSetIdFromChangeRefs}.
    *
    * @param id previous patch set ID.
    * @return next patch set ID for the same change, incrementing by 1.
@@ -106,9 +115,9 @@
    *     names appear in the repository.
    */
   public static PatchSet.Id nextPatchSetId(Repository git, PatchSet.Id id) throws IOException {
-    return nextPatchSetIdFromChangeRefsMap(
-        Maps.transformValues(
-            git.getRefDatabase().getRefs(id.getParentKey().toRefPrefix()), Ref::getObjectId),
+    return nextPatchSetIdFromChangeRefs(
+        git.getRefDatabase().getRefsByPrefix(id.getParentKey().toRefPrefix()).stream()
+            .map(Ref::getName),
         id);
   }
 
diff --git a/java/com/google/gerrit/server/CommentsUtil.java b/java/com/google/gerrit/server/CommentsUtil.java
index d72d73a..a5332eb 100644
--- a/java/com/google/gerrit/server/CommentsUtil.java
+++ b/java/com/google/gerrit/server/CommentsUtil.java
@@ -16,17 +16,14 @@
 
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.gerrit.reviewdb.client.PatchLineComment.Status.PUBLISHED;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.ComparisonChain;
 import com.google.common.collect.FluentIterable;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Ordering;
-import com.google.common.collect.Streams;
 import com.google.gerrit.common.Nullable;
+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;
@@ -35,47 +32,29 @@
 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.PatchLineComment.Status;
 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.reviewdb.server.ReviewDb;
-import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 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.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
-import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
-import com.google.gerrit.server.update.BatchUpdateReviewDb;
 import com.google.gerrit.server.update.ChangeContext;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
-import org.eclipse.jgit.lib.BatchRefUpdate;
-import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.ReceiveCommand;
 
-/**
- * Utility functions to manipulate Comments.
- *
- * <p>These methods either query for and update Comments in the NoteDb or ReviewDb, depending on the
- * state of the NotesMigration.
- */
+/** Utility functions to manipulate Comments. */
 @Singleton
 public class CommentsUtil {
   public static final Ordering<Comment> COMMENT_ORDER =
@@ -127,18 +106,13 @@
 
   private final GitRepositoryManager repoManager;
   private final AllUsersName allUsers;
-  private final NotesMigration migration;
   private final String serverId;
 
   @Inject
   CommentsUtil(
-      GitRepositoryManager repoManager,
-      AllUsersName allUsers,
-      NotesMigration migration,
-      @GerritServerId String serverId) {
+      GitRepositoryManager repoManager, AllUsersName allUsers, @GerritServerId String serverId) {
     this.repoManager = repoManager;
     this.allUsers = allUsers;
-    this.migration = migration;
     this.serverId = serverId;
   }
 
@@ -150,7 +124,7 @@
       String message,
       @Nullable Boolean unresolved,
       @Nullable String parentUuid)
-      throws OrmException, UnprocessableEntityException {
+      throws UnprocessableEntityException {
     if (unresolved == null) {
       if (parentUuid == null) {
         // Default to false if comment is not descended from another.
@@ -158,7 +132,7 @@
       } else {
         // Inherit unresolved value from inReplyTo comment if not specified.
         Comment.Key key = new Comment.Key(parentUuid, path, psId.patchSetId);
-        Optional<Comment> parent = getPublished(ctx.getDb(), ctx.getNotes(), key);
+        Optional<Comment> parent = getPublished(ctx.getNotes(), key);
         if (!parent.isPresent()) {
           throw new UnprocessableEntityException("Invalid parentUuid supplied for comment");
         }
@@ -201,118 +175,60 @@
     return c;
   }
 
-  public Optional<Comment> getPublished(ReviewDb db, ChangeNotes notes, Comment.Key key)
-      throws OrmException {
-    if (!migration.readChanges()) {
-      return getReviewDb(db, notes, key);
-    }
-    return publishedByChange(db, notes).stream().filter(c -> key.equals(c.key)).findFirst();
+  public Optional<Comment> getPublished(ChangeNotes notes, Comment.Key key) {
+    return publishedByChange(notes).stream().filter(c -> key.equals(c.key)).findFirst();
   }
 
-  public Optional<Comment> getDraft(
-      ReviewDb db, ChangeNotes notes, IdentifiedUser user, Comment.Key key) throws OrmException {
-    if (!migration.readChanges()) {
-      Optional<Comment> c = getReviewDb(db, notes, key);
-      if (c.isPresent() && !c.get().author.getId().equals(user.getAccountId())) {
-        throw new OrmException(
-            String.format(
-                "Expected draft %s to belong to account %s, but it belongs to %s",
-                key, user.getAccountId(), c.get().author.getId()));
-      }
-      return c;
-    }
-    return draftByChangeAuthor(db, notes, user.getAccountId()).stream()
+  public Optional<Comment> getDraft(ChangeNotes notes, IdentifiedUser user, Comment.Key key) {
+    return draftByChangeAuthor(notes, user.getAccountId()).stream()
         .filter(c -> key.equals(c.key))
         .findFirst();
   }
 
-  private Optional<Comment> getReviewDb(ReviewDb db, ChangeNotes notes, Comment.Key key)
-      throws OrmException {
-    return Optional.ofNullable(
-            db.patchComments().get(PatchLineComment.Key.from(notes.getChangeId(), key)))
-        .map(plc -> plc.asComment(serverId));
-  }
-
-  public List<Comment> publishedByChange(ReviewDb db, ChangeNotes notes) throws OrmException {
-    if (!migration.readChanges()) {
-      return sort(byCommentStatus(db.patchComments().byChange(notes.getChangeId()), PUBLISHED));
-    }
-
+  public List<Comment> publishedByChange(ChangeNotes notes) {
     notes.load();
     return sort(Lists.newArrayList(notes.getComments().values()));
   }
 
-  public List<RobotComment> robotCommentsByChange(ChangeNotes notes) throws OrmException {
-    if (!migration.readChanges()) {
-      return ImmutableList.of();
-    }
-
+  public List<RobotComment> robotCommentsByChange(ChangeNotes notes) {
     notes.load();
     return sort(Lists.newArrayList(notes.getRobotComments().values()));
   }
 
-  public List<Comment> draftByChange(ReviewDb db, ChangeNotes notes) throws OrmException {
-    if (!migration.readChanges()) {
-      return sort(byCommentStatus(db.patchComments().byChange(notes.getChangeId()), Status.DRAFT));
-    }
-
+  public List<Comment> draftByChange(ChangeNotes notes) {
     List<Comment> comments = new ArrayList<>();
     for (Ref ref : getDraftRefs(notes.getChangeId())) {
       Account.Id account = Account.Id.fromRefSuffix(ref.getName());
       if (account != null) {
-        comments.addAll(draftByChangeAuthor(db, notes, account));
+        comments.addAll(draftByChangeAuthor(notes, account));
       }
     }
     return sort(comments);
   }
 
-  private List<Comment> byCommentStatus(
-      ResultSet<PatchLineComment> comments, PatchLineComment.Status status) {
-    return toComments(
-        serverId, Lists.newArrayList(Iterables.filter(comments, c -> c.getStatus() == status)));
-  }
-
-  public List<Comment> byPatchSet(ReviewDb db, ChangeNotes notes, PatchSet.Id psId)
-      throws OrmException {
-    if (!migration.readChanges()) {
-      return sort(toComments(serverId, db.patchComments().byPatchSet(psId).toList()));
-    }
+  public List<Comment> byPatchSet(ChangeNotes notes, PatchSet.Id psId) {
     List<Comment> comments = new ArrayList<>();
-    comments.addAll(publishedByPatchSet(db, notes, psId));
+    comments.addAll(publishedByPatchSet(notes, psId));
 
     for (Ref ref : getDraftRefs(notes.getChangeId())) {
       Account.Id account = Account.Id.fromRefSuffix(ref.getName());
       if (account != null) {
-        comments.addAll(draftByPatchSetAuthor(db, psId, account, notes));
+        comments.addAll(draftByPatchSetAuthor(psId, account, notes));
       }
     }
     return sort(comments);
   }
 
-  public List<Comment> publishedByChangeFile(
-      ReviewDb db, ChangeNotes notes, Change.Id changeId, String file) throws OrmException {
-    if (!migration.readChanges()) {
-      return sort(
-          toComments(serverId, db.patchComments().publishedByChangeFile(changeId, file).toList()));
-    }
+  public List<Comment> publishedByChangeFile(ChangeNotes notes, String file) {
     return commentsOnFile(notes.load().getComments().values(), file);
   }
 
-  public List<Comment> publishedByPatchSet(ReviewDb db, ChangeNotes notes, PatchSet.Id psId)
-      throws OrmException {
-    if (!migration.readChanges()) {
-      return removeCommentsOnAncestorOfCommitMessage(
-          sort(toComments(serverId, db.patchComments().publishedByPatchSet(psId).toList())));
-    }
+  public List<Comment> publishedByPatchSet(ChangeNotes notes, PatchSet.Id psId) {
     return removeCommentsOnAncestorOfCommitMessage(
         commentsOnPatchSet(notes.load().getComments().values(), psId));
   }
 
-  public List<RobotComment> robotCommentsByPatchSet(ChangeNotes notes, PatchSet.Id psId)
-      throws OrmException {
-    if (!migration.readChanges()) {
-      return ImmutableList.of();
-    }
+  public List<RobotComment> robotCommentsByPatchSet(ChangeNotes notes, PatchSet.Id psId) {
     return commentsOnPatchSet(notes.load().getRobotComments().values(), psId);
   }
 
@@ -330,48 +246,25 @@
   }
 
   public List<Comment> draftByPatchSetAuthor(
-      ReviewDb db, PatchSet.Id psId, Account.Id author, ChangeNotes notes) throws OrmException {
-    if (!migration.readChanges()) {
-      return sort(
-          toComments(serverId, db.patchComments().draftByPatchSetAuthor(psId, author).toList()));
-    }
+      PatchSet.Id psId, Account.Id author, ChangeNotes notes) {
     return commentsOnPatchSet(notes.load().getDraftComments(author).values(), psId);
   }
 
-  public List<Comment> draftByChangeFileAuthor(
-      ReviewDb db, ChangeNotes notes, String file, Account.Id author) throws OrmException {
-    if (!migration.readChanges()) {
-      return sort(
-          toComments(
-              serverId,
-              db.patchComments()
-                  .draftByChangeFileAuthor(notes.getChangeId(), file, author)
-                  .toList()));
-    }
+  public List<Comment> draftByChangeFileAuthor(ChangeNotes notes, String file, Account.Id author) {
     return commentsOnFile(notes.load().getDraftComments(author).values(), file);
   }
 
-  public List<Comment> draftByChangeAuthor(ReviewDb db, ChangeNotes notes, Account.Id author)
-      throws OrmException {
-    if (!migration.readChanges()) {
-      return Streams.stream(db.patchComments().draftByAuthor(author))
-          .filter(c -> c.getPatchSetId().getParentKey().equals(notes.getChangeId()))
-          .map(plc -> plc.asComment(serverId))
-          .sorted(COMMENT_ORDER)
-          .collect(toList());
-    }
+  public List<Comment> draftByChangeAuthor(ChangeNotes notes, Account.Id author) {
     List<Comment> comments = new ArrayList<>();
     comments.addAll(notes.getDraftComments(author).values());
     return sort(comments);
   }
 
   public void putComments(
-      ReviewDb db, ChangeUpdate update, PatchLineComment.Status status, Iterable<Comment> comments)
-      throws OrmException {
+      ChangeUpdate update, PatchLineComment.Status status, Iterable<Comment> comments) {
     for (Comment c : comments) {
       update.putComment(status, c);
     }
-    db.patchComments().upsert(toPatchLineComments(update.getId(), status, comments));
   }
 
   public void putRobotComments(ChangeUpdate update, Iterable<RobotComment> comments) {
@@ -380,60 +273,17 @@
     }
   }
 
-  public void deleteComments(ReviewDb db, ChangeUpdate update, Iterable<Comment> comments)
-      throws OrmException {
+  public void deleteComments(ChangeUpdate update, Iterable<Comment> comments) {
     for (Comment c : comments) {
       update.deleteComment(c);
     }
-    db.patchComments()
-        .delete(toPatchLineComments(update.getId(), PatchLineComment.Status.DRAFT, comments));
   }
 
   public void deleteCommentByRewritingHistory(
-      ReviewDb db, ChangeUpdate update, Comment.Key commentKey, PatchSet.Id psId, String newMessage)
-      throws OrmException {
-    if (PrimaryStorage.of(update.getChange()).equals(PrimaryStorage.REVIEW_DB)) {
-      PatchLineComment.Key key =
-          new PatchLineComment.Key(new Patch.Key(psId, commentKey.filename), commentKey.uuid);
-
-      if (db instanceof BatchUpdateReviewDb) {
-        db = ((BatchUpdateReviewDb) db).unsafeGetDelegate();
-      }
-      db = ReviewDbUtil.unwrapDb(db);
-
-      PatchLineComment patchLineComment = db.patchComments().get(key);
-
-      if (!patchLineComment.getStatus().equals(PUBLISHED)) {
-        throw new OrmException(String.format("comment %s is not published", key));
-      }
-
-      patchLineComment.setMessage(newMessage);
-      db.patchComments().upsert(Collections.singleton(patchLineComment));
-    }
-
+      ChangeUpdate update, Comment.Key commentKey, String newMessage) {
     update.deleteCommentByRewritingHistory(commentKey.uuid, newMessage);
   }
 
-  public void deleteAllDraftsFromAllUsers(Change.Id changeId) throws IOException {
-    try (Repository repo = repoManager.openRepository(allUsers);
-        RevWalk rw = new RevWalk(repo)) {
-      BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
-      for (Ref ref : getDraftRefs(repo, changeId)) {
-        bru.addCommand(new ReceiveCommand(ref.getObjectId(), ObjectId.zeroId(), ref.getName()));
-      }
-      bru.setRefLogMessage("Delete drafts from NoteDb", false);
-      bru.execute(rw, NullProgressMonitor.INSTANCE);
-      for (ReceiveCommand cmd : bru.getCommands()) {
-        if (cmd.getResult() != ReceiveCommand.Result.OK) {
-          throw new IOException(
-              String.format(
-                  "Failed to delete draft comment ref %s at %s: %s (%s)",
-                  cmd.getRefName(), cmd.getOldId(), cmd.getResult(), cmd.getMessage()));
-        }
-      }
-    }
-  }
-
   private static List<Comment> commentsOnFile(Collection<Comment> allComments, String file) {
     List<Comment> result = new ArrayList<>(allComments.size());
     for (Comment c : allComments) {
@@ -488,11 +338,11 @@
    * @param changeId change ID.
    * @return raw refs from All-Users repo.
    */
-  public Collection<Ref> getDraftRefs(Change.Id changeId) throws OrmException {
+  public Collection<Ref> getDraftRefs(Change.Id changeId) {
     try (Repository repo = repoManager.openRepository(allUsers)) {
       return getDraftRefs(repo, changeId);
     } catch (IOException e) {
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
   }
 
diff --git a/java/com/google/gerrit/server/CreateGroupPermissionSyncer.java b/java/com/google/gerrit/server/CreateGroupPermissionSyncer.java
index 72eb468..27fb9d7 100644
--- a/java/com/google/gerrit/server/CreateGroupPermissionSyncer.java
+++ b/java/com/google/gerrit/server/CreateGroupPermissionSyncer.java
@@ -57,17 +57,20 @@
   private final AllUsersName allUsers;
   private final ProjectCache projectCache;
   private final Provider<MetaDataUpdate.Server> metaDataUpdateFactory;
+  private final ProjectConfig.Factory projectConfigFactory;
 
   @Inject
   CreateGroupPermissionSyncer(
       AllProjectsName allProjects,
       AllUsersName allUsers,
       ProjectCache projectCache,
-      Provider<MetaDataUpdate.Server> metaDataUpdateFactory) {
+      Provider<MetaDataUpdate.Server> metaDataUpdateFactory,
+      ProjectConfig.Factory projectConfigFactory) {
     this.allProjects = allProjects;
     this.allUsers = allUsers;
     this.projectCache = projectCache;
     this.metaDataUpdateFactory = metaDataUpdateFactory;
+    this.projectConfigFactory = projectConfigFactory;
   }
 
   /**
@@ -102,7 +105,7 @@
     }
 
     try (MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsers)) {
-      ProjectConfig config = ProjectConfig.read(md);
+      ProjectConfig config = projectConfigFactory.read(md);
       AccessSection createGroupAccessSection =
           config.getAccessSection(RefNames.REFS_GROUPS + "*", true);
       if (createGroupsGlobal.isEmpty()) {
diff --git a/java/com/google/gerrit/server/DynamicOptions.java b/java/com/google/gerrit/server/DynamicOptions.java
index 3759f09..41dc082 100644
--- a/java/com/google/gerrit/server/DynamicOptions.java
+++ b/java/com/google/gerrit/server/DynamicOptions.java
@@ -26,7 +26,6 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
 import java.util.WeakHashMap;
 
 /** Helper class to define and parse options from plugins on ssh and RestAPI commands. */
@@ -50,8 +49,18 @@
    *   }
    * </pre>
    *
-   * The option will be prefixed by the plugin name. In the example above, if the plugin name was
+   * <p>The option will be prefixed by the plugin name. In the example above, if the plugin name was
    * my-plugin, then the --verbose option as used by the caller would be --my-plugin--verbose.
+   *
+   * <p>Additional options can be annotated with @RequiresOption which will cause them to be ignored
+   * unless the required option is present. For example:
+   *
+   * <pre>
+   *   {@literal @}RequiresOptions("--help")
+   *   {@literal @}Option(name = "--help-as-json",
+   *           usage = "display help text in json format")
+   *   public boolean displayHelpAsJson;
+   * </pre>
    */
   public interface DynamicBean {}
 
@@ -71,7 +80,7 @@
    *       .to(MyOptionsClassNameProvider.class);
    *
    *   static class MyOptionsClassNameProvider implements DynamicOptions.ClassNameProvider {
-   *     @Override
+   *     {@literal @}Override
    *     public String getClassName() {
    *       return "com.googlesource.gerrit.plugins.myplugin.CommandOptions";
    *     }
@@ -98,11 +107,11 @@
    *       .to(MyOptionsModulesClassNamesProvider.class);
    *
    *   static class MyOptionsModulesClassNamesProvider implements DynamicOptions.ClassNameProvider {
-   *     @Override
+   *     {@literal @}Override
    *     public String getClassName() {
    *       return "com.googlesource.gerrit.plugins.myplugin.CommandOptions";
    *     }
-   *     @Override
+   *     {@literal @}Override
    *     public Iterable<String> getModulesClassNames()() {
    *       return "com.googlesource.gerrit.plugins.myplugin.MyOptionsModule";
    *     }
@@ -144,6 +153,21 @@
    */
   public interface BeanReceiver {
     void setDynamicBean(String plugin, DynamicBean dynamicBean);
+
+    /**
+     * Returns the class that should be used for looking up exported DynamicBean bindings from
+     * plugins. Override when a particular REST/SSH endpoint should respect DynamicBeans bound on a
+     * different endpoint. For example, {@code GetDetail} is just a synonym for a variant of {@code
+     * GetChange}, and it should respect any DynamicBeans on GetChange. GetChange}. So it should
+     * return {@code GetChange.class} from this method.
+     */
+    default Class<? extends BeanReceiver> getExportedBeanReceiver() {
+      return getClass();
+    }
+  }
+
+  public interface BeanProvider {
+    DynamicBean getDynamicBean(String plugin);
   }
 
   /**
@@ -161,8 +185,7 @@
    * classloaders.
    */
   protected static Map<ClassLoader, Map<ClassLoader, WeakReference<ClassLoader>>> mergedClByCls =
-      Collections.synchronizedMap(
-          new WeakHashMap<ClassLoader, Map<ClassLoader, WeakReference<ClassLoader>>>());
+      Collections.synchronizedMap(new WeakHashMap<>());
 
   protected Object bean;
   protected Map<String, DynamicBean> beansByPlugin;
@@ -187,9 +210,13 @@
     this.bean = bean;
     this.injector = injector;
     beansByPlugin = new HashMap<>();
+    Class<?> beanClass =
+        (bean instanceof BeanReceiver)
+            ? ((BeanReceiver) bean).getExportedBeanReceiver()
+            : getClass();
     for (String plugin : dynamicBeans.plugins()) {
       Provider<DynamicBean> provider =
-          dynamicBeans.byPlugin(plugin).get(bean.getClass().getCanonicalName());
+          dynamicBeans.byPlugin(plugin).get(beanClass.getCanonicalName());
       if (provider != null) {
         beansByPlugin.put(plugin, getDynamicBean(bean, provider.get()));
       }
@@ -258,22 +285,23 @@
   }
 
   public void parseDynamicBeans(CmdLineParser clp) {
-    for (Entry<String, DynamicBean> e : beansByPlugin.entrySet()) {
+    for (Map.Entry<String, DynamicBean> e : beansByPlugin.entrySet()) {
       clp.parseWithPrefix("--" + e.getKey(), e.getValue());
     }
+    clp.drainOptionQueue();
   }
 
   public void setDynamicBeans() {
     if (bean instanceof BeanReceiver) {
       BeanReceiver receiver = (BeanReceiver) bean;
-      for (Entry<String, DynamicBean> e : beansByPlugin.entrySet()) {
+      for (Map.Entry<String, DynamicBean> e : beansByPlugin.entrySet()) {
         receiver.setDynamicBean(e.getKey(), e.getValue());
       }
     }
   }
 
   public void onBeanParseStart() {
-    for (Entry<String, DynamicBean> e : beansByPlugin.entrySet()) {
+    for (Map.Entry<String, DynamicBean> e : beansByPlugin.entrySet()) {
       DynamicBean instance = e.getValue();
       if (instance instanceof BeanParseListener) {
         BeanParseListener listener = (BeanParseListener) instance;
@@ -283,7 +311,7 @@
   }
 
   public void onBeanParseEnd() {
-    for (Entry<String, DynamicBean> e : beansByPlugin.entrySet()) {
+    for (Map.Entry<String, DynamicBean> e : beansByPlugin.entrySet()) {
       DynamicBean instance = e.getValue();
       if (instance instanceof BeanParseListener) {
         BeanParseListener listener = (BeanParseListener) instance;
diff --git a/java/com/google/gerrit/server/IdentifiedUser.java b/java/com/google/gerrit/server/IdentifiedUser.java
index d9a4cae..e65f562 100644
--- a/java/com/google/gerrit/server/IdentifiedUser.java
+++ b/java/com/google/gerrit/server/IdentifiedUser.java
@@ -32,7 +32,7 @@
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.CanonicalWebUrl;
-import com.google.gerrit.server.config.DisableReverseDnsLookup;
+import com.google.gerrit.server.config.EnableReverseDnsLookup;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.inject.Inject;
 import com.google.inject.OutOfScopeException;
@@ -67,7 +67,7 @@
     private final Provider<String> canonicalUrl;
     private final AccountCache accountCache;
     private final GroupBackend groupBackend;
-    private final Boolean disableReverseDnsLookup;
+    private final Boolean enableReverseDnsLookup;
 
     @Inject
     public GenericFactory(
@@ -75,7 +75,7 @@
         Realm realm,
         @AnonymousCowardName String anonymousCowardName,
         @CanonicalWebUrl Provider<String> canonicalUrl,
-        @DisableReverseDnsLookup Boolean disableReverseDnsLookup,
+        @EnableReverseDnsLookup Boolean enableReverseDnsLookup,
         AccountCache accountCache,
         GroupBackend groupBackend) {
       this.authConfig = authConfig;
@@ -84,7 +84,7 @@
       this.canonicalUrl = canonicalUrl;
       this.accountCache = accountCache;
       this.groupBackend = groupBackend;
-      this.disableReverseDnsLookup = disableReverseDnsLookup;
+      this.enableReverseDnsLookup = enableReverseDnsLookup;
     }
 
     public IdentifiedUser create(AccountState state) {
@@ -95,14 +95,14 @@
           canonicalUrl,
           accountCache,
           groupBackend,
-          disableReverseDnsLookup,
-          Providers.of((SocketAddress) null),
+          enableReverseDnsLookup,
+          Providers.of(null),
           state,
           null);
     }
 
     public IdentifiedUser create(Account.Id id) {
-      return create((SocketAddress) null, id);
+      return create(null, id);
     }
 
     public IdentifiedUser create(SocketAddress remotePeer, Account.Id id) {
@@ -118,7 +118,7 @@
           canonicalUrl,
           accountCache,
           groupBackend,
-          disableReverseDnsLookup,
+          enableReverseDnsLookup,
           Providers.of(remotePeer),
           id,
           caller);
@@ -128,8 +128,8 @@
   /**
    * Create an IdentifiedUser, relying on current request state.
    *
-   * <p>Can only be used from within a module that has defined request scoped {@code @RemotePeer
-   * SocketAddress} and {@code ReviewDb} providers.
+   * <p>Can only be used from within a module that has defined a request scoped {@code @RemotePeer
+   * SocketAddress} provider.
    */
   @Singleton
   public static class RequestFactory {
@@ -139,7 +139,7 @@
     private final Provider<String> canonicalUrl;
     private final AccountCache accountCache;
     private final GroupBackend groupBackend;
-    private final Boolean disableReverseDnsLookup;
+    private final Boolean enableReverseDnsLookup;
     private final Provider<SocketAddress> remotePeerProvider;
 
     @Inject
@@ -150,7 +150,7 @@
         @CanonicalWebUrl Provider<String> canonicalUrl,
         AccountCache accountCache,
         GroupBackend groupBackend,
-        @DisableReverseDnsLookup Boolean disableReverseDnsLookup,
+        @EnableReverseDnsLookup Boolean enableReverseDnsLookup,
         @RemotePeer Provider<SocketAddress> remotePeerProvider) {
       this.authConfig = authConfig;
       this.realm = realm;
@@ -158,7 +158,7 @@
       this.canonicalUrl = canonicalUrl;
       this.accountCache = accountCache;
       this.groupBackend = groupBackend;
-      this.disableReverseDnsLookup = disableReverseDnsLookup;
+      this.enableReverseDnsLookup = enableReverseDnsLookup;
       this.remotePeerProvider = remotePeerProvider;
     }
 
@@ -170,7 +170,7 @@
           canonicalUrl,
           accountCache,
           groupBackend,
-          disableReverseDnsLookup,
+          enableReverseDnsLookup,
           remotePeerProvider,
           id,
           null);
@@ -184,7 +184,7 @@
           canonicalUrl,
           accountCache,
           groupBackend,
-          disableReverseDnsLookup,
+          enableReverseDnsLookup,
           remotePeerProvider,
           id,
           caller);
@@ -201,7 +201,7 @@
   private final Realm realm;
   private final GroupBackend groupBackend;
   private final String anonymousCowardName;
-  private final Boolean disableReverseDnsLookup;
+  private final Boolean enableReverseDnsLookup;
   private final Set<String> validEmails = Sets.newTreeSet(String.CASE_INSENSITIVE_ORDER);
   private final CurrentUser realUser; // Must be final since cached properties depend on it.
 
@@ -221,7 +221,7 @@
       Provider<String> canonicalUrl,
       AccountCache accountCache,
       GroupBackend groupBackend,
-      Boolean disableReverseDnsLookup,
+      Boolean enableReverseDnsLookup,
       @Nullable Provider<SocketAddress> remotePeerProvider,
       AccountState state,
       @Nullable CurrentUser realUser) {
@@ -232,7 +232,7 @@
         canonicalUrl,
         accountCache,
         groupBackend,
-        disableReverseDnsLookup,
+        enableReverseDnsLookup,
         remotePeerProvider,
         state.getAccount().getId(),
         realUser);
@@ -246,7 +246,7 @@
       Provider<String> canonicalUrl,
       AccountCache accountCache,
       GroupBackend groupBackend,
-      Boolean disableReverseDnsLookup,
+      Boolean enableReverseDnsLookup,
       @Nullable Provider<SocketAddress> remotePeerProvider,
       Account.Id id,
       @Nullable CurrentUser realUser) {
@@ -256,7 +256,7 @@
     this.authConfig = authConfig;
     this.realm = realm;
     this.anonymousCowardName = anonymousCowardName;
-    this.disableReverseDnsLookup = disableReverseDnsLookup;
+    this.enableReverseDnsLookup = enableReverseDnsLookup;
     this.remotePeerProvider = remotePeerProvider;
     this.accountId = id;
     this.realUser = realUser != null ? realUser : this;
@@ -512,11 +512,8 @@
       remotePeer = Providers.of(remotePeerProvider.get());
     } catch (OutOfScopeException | ProvisionException e) {
       remotePeer =
-          new Provider<SocketAddress>() {
-            @Override
-            public SocketAddress get() {
-              throw e;
-            }
+          () -> {
+            throw e;
           };
     }
     return new IdentifiedUser(
@@ -526,7 +523,7 @@
         Providers.of(canonicalUrl.get()),
         accountCache,
         groupBackend,
-        disableReverseDnsLookup,
+        enableReverseDnsLookup,
         remotePeer,
         state,
         realUser);
@@ -557,7 +554,7 @@
   }
 
   private String getHost(InetAddress in) {
-    if (Boolean.FALSE.equals(disableReverseDnsLookup)) {
+    if (Boolean.TRUE.equals(enableReverseDnsLookup)) {
       return in.getCanonicalHostName();
     }
     return in.getHostAddress();
diff --git a/java/com/google/gerrit/server/LibModuleLoader.java b/java/com/google/gerrit/server/LibModuleLoader.java
index d1067e1..0a6fb9f 100644
--- a/java/com/google/gerrit/server/LibModuleLoader.java
+++ b/java/com/google/gerrit/server/LibModuleLoader.java
@@ -30,9 +30,9 @@
 public class LibModuleLoader {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  public static List<Module> loadModules(Injector parent) {
+  public static List<Module> loadModules(Injector parent, LibModuleType moduleType) {
     Config cfg = getConfig(parent);
-    return Arrays.stream(cfg.getStringList("gerrit", null, "installModule"))
+    return Arrays.stream(cfg.getStringList("gerrit", null, "install" + moduleType.getConfigKey()))
         .map(m -> createModule(parent, m))
         .collect(toList());
   }
diff --git a/java/com/google/gerrit/server/LibModuleType.java b/java/com/google/gerrit/server/LibModuleType.java
new file mode 100644
index 0000000..557f8c0
--- /dev/null
+++ b/java/com/google/gerrit/server/LibModuleType.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;
+
+/** Loadable module type for the different Gerrit injectors. */
+public enum LibModuleType {
+
+  /** Module for the sysInjector. */
+  SYS_MODULE("Module"),
+
+  /** Module for the dbInjector. */
+  DB_MODULE("DbModule");
+
+  private final String configKey;
+
+  LibModuleType(String configKey) {
+    this.configKey = configKey;
+  }
+
+  /**
+   * Returns the module type for libModule loaded from <gerrit_site/lib> directory.
+   *
+   * @return module type string
+   */
+  public String getConfigKey() {
+    return configKey;
+  }
+}
diff --git a/java/com/google/gerrit/server/OutputFormat.java b/java/com/google/gerrit/server/OutputFormat.java
deleted file mode 100644
index e555845..0000000
--- a/java/com/google/gerrit/server/OutputFormat.java
+++ /dev/null
@@ -1,70 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server;
-
-import com.google.gson.FieldNamingPolicy;
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import com.google.gwtjsonrpc.server.SqlTimestampDeserializer;
-import java.sql.Timestamp;
-
-/** Standard output format used by an API call. */
-public enum OutputFormat {
-  /**
-   * The output is a human readable text format. It may also be regular enough to be machine
-   * readable. Whether or not the text format is machine readable and will be committed to as a long
-   * term format that tools can build upon is specific to each API call.
-   */
-  TEXT,
-
-  /**
-   * Pretty-printed JSON format. This format uses whitespace to make the output readable by a human,
-   * but is also machine readable with a JSON library. The structure of the output is a long term
-   * format that tools can rely upon.
-   */
-  JSON,
-
-  /**
-   * Same as {@link #JSON}, but with unnecessary whitespace removed to save generation time and copy
-   * costs. Typically JSON_COMPACT format is used by a browser based HTML client running over the
-   * network.
-   */
-  JSON_COMPACT;
-
-  /** @return true when the format is either JSON or JSON_COMPACT. */
-  public boolean isJson() {
-    return this == JSON_COMPACT || this == JSON;
-  }
-
-  /** @return a new Gson instance configured according to the format. */
-  public GsonBuilder newGsonBuilder() {
-    if (!isJson()) {
-      throw new IllegalStateException(String.format("%s is not JSON", this));
-    }
-    GsonBuilder gb =
-        new GsonBuilder()
-            .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
-            .registerTypeAdapter(Timestamp.class, new SqlTimestampDeserializer());
-    if (this == OutputFormat.JSON) {
-      gb.setPrettyPrinting();
-    }
-    return gb;
-  }
-
-  /** @return a new Gson instance configured according to the format. */
-  public Gson newGson() {
-    return newGsonBuilder().create();
-  }
-}
diff --git a/java/com/google/gerrit/server/PatchSetUtil.java b/java/com/google/gerrit/server/PatchSetUtil.java
index f6c7abc..2a78eb6 100644
--- a/java/com/google/gerrit/server/PatchSetUtil.java
+++ b/java/com/google/gerrit/server/PatchSetUtil.java
@@ -15,17 +15,11 @@
 package com.google.gerrit.server;
 
 import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.collect.ImmutableMap.toImmutableMap;
-import static com.google.gerrit.server.ChangeUtil.PS_ID_ORDER;
-import static com.google.gerrit.server.notedb.PatchSetState.PUBLISHED;
 import static java.util.Objects.requireNonNull;
-import static java.util.function.Function.identity;
 
 import com.google.common.collect.ImmutableCollection;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Maps;
-import com.google.common.collect.Sets;
-import com.google.common.collect.Streams;
 import com.google.gerrit.common.data.LabelFunction;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -34,20 +28,16 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gwtorm.server.OrmException;
 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.util.Collections;
 import java.util.List;
 import java.util.Set;
 import org.eclipse.jgit.lib.ObjectId;
@@ -58,70 +48,42 @@
 /** Utilities for manipulating patch sets. */
 @Singleton
 public class PatchSetUtil {
-  private final NotesMigration migration;
   private final Provider<ApprovalsUtil> approvalsUtilProvider;
   private final ProjectCache projectCache;
-  private final Provider<ReviewDb> dbProvider;
   private final GitRepositoryManager repoManager;
 
   @Inject
   PatchSetUtil(
-      NotesMigration migration,
       Provider<ApprovalsUtil> approvalsUtilProvider,
       ProjectCache projectCache,
-      Provider<ReviewDb> dbProvider,
       GitRepositoryManager repoManager) {
-    this.migration = migration;
     this.approvalsUtilProvider = approvalsUtilProvider;
     this.projectCache = projectCache;
-    this.dbProvider = dbProvider;
     this.repoManager = repoManager;
   }
 
-  public PatchSet current(ReviewDb db, ChangeNotes notes) throws OrmException {
-    return get(db, notes, notes.getChange().currentPatchSetId());
+  public PatchSet current(ChangeNotes notes) {
+    return get(notes, notes.getChange().currentPatchSetId());
   }
 
-  public PatchSet get(ReviewDb db, ChangeNotes notes, PatchSet.Id psId) throws OrmException {
-    if (!migration.readChanges()) {
-      return db.patchSets().get(psId);
-    }
+  public PatchSet get(ChangeNotes notes, PatchSet.Id psId) {
     return notes.load().getPatchSets().get(psId);
   }
 
-  public ImmutableCollection<PatchSet> byChange(ReviewDb db, ChangeNotes notes)
-      throws OrmException {
-    if (!migration.readChanges()) {
-      return PS_ID_ORDER.immutableSortedCopy(db.patchSets().byChange(notes.getChangeId()));
-    }
+  public ImmutableCollection<PatchSet> byChange(ChangeNotes notes) {
     return notes.load().getPatchSets().values();
   }
 
-  public ImmutableMap<PatchSet.Id, PatchSet> byChangeAsMap(ReviewDb db, ChangeNotes notes)
-      throws OrmException {
-    if (!migration.readChanges()) {
-      ImmutableMap.Builder<PatchSet.Id, PatchSet> result = ImmutableMap.builder();
-      for (PatchSet ps : PS_ID_ORDER.sortedCopy(db.patchSets().byChange(notes.getChangeId()))) {
-        result.put(ps.getId(), ps);
-      }
-      return result.build();
-    }
+  public ImmutableMap<PatchSet.Id, PatchSet> byChangeAsMap(ChangeNotes notes) {
     return notes.load().getPatchSets();
   }
 
   public ImmutableMap<PatchSet.Id, PatchSet> getAsMap(
-      ReviewDb db, ChangeNotes notes, Set<PatchSet.Id> patchSetIds) throws OrmException {
-    if (!migration.readChanges()) {
-      patchSetIds = Sets.filter(patchSetIds, p -> p.getParentKey().equals(notes.getChangeId()));
-      return Streams.stream(db.patchSets().get(patchSetIds))
-          .sorted(PS_ID_ORDER)
-          .collect(toImmutableMap(PatchSet::getId, identity()));
-    }
+      ChangeNotes notes, Set<PatchSet.Id> patchSetIds) {
     return ImmutableMap.copyOf(Maps.filterKeys(notes.load().getPatchSets(), patchSetIds::contains));
   }
 
   public PatchSet insert(
-      ReviewDb db,
       RevWalk rw,
       ChangeUpdate update,
       PatchSet.Id psId,
@@ -129,10 +91,14 @@
       List<String> groups,
       String pushCertificate,
       String description)
-      throws OrmException, IOException {
+      throws IOException {
     requireNonNull(groups, "groups may not be null");
     ensurePatchSetMatches(psId, update);
 
+    update.setCommit(rw, commit, pushCertificate);
+    update.setPsDescription(description);
+    update.setGroups(groups);
+
     PatchSet ps = new PatchSet(psId);
     ps.setRevision(new RevId(commit.name()));
     ps.setUploader(update.getAccountId());
@@ -140,21 +106,9 @@
     ps.setGroups(groups);
     ps.setPushCertificate(pushCertificate);
     ps.setDescription(description);
-    db.patchSets().insert(Collections.singleton(ps));
-
-    update.setCommit(rw, commit, pushCertificate);
-    update.setPsDescription(description);
-    update.setGroups(groups);
-
     return ps;
   }
 
-  public void publish(ReviewDb db, ChangeUpdate update, PatchSet ps) throws OrmException {
-    ensurePatchSetMatches(ps.getId(), update);
-    update.setPatchSetState(PUBLISHED);
-    db.patchSets().update(Collections.singleton(ps));
-  }
-
   private static void ensurePatchSetMatches(PatchSet.Id psId, ChangeUpdate update) {
     Change.Id changeId = update.getChange().getId();
     checkArgument(
@@ -173,16 +127,14 @@
     }
   }
 
-  public void setGroups(ReviewDb db, ChangeUpdate update, PatchSet ps, List<String> groups)
-      throws OrmException {
+  public void setGroups(ChangeUpdate update, PatchSet ps, List<String> groups) {
     ps.setGroups(groups);
     update.setGroups(groups);
-    db.patchSets().update(Collections.singleton(ps));
   }
 
   /** Check if the current patch set of the change is locked. */
   public void checkPatchSetNotLocked(ChangeNotes notes)
-      throws OrmException, IOException, ResourceConflictException {
+      throws IOException, ResourceConflictException {
     if (isPatchSetLocked(notes)) {
       throw new ResourceConflictException(
           String.format("The current patch set of change %s is locked", notes.getChangeId()));
@@ -190,9 +142,9 @@
   }
 
   /** Is the current patch set locked against state changes? */
-  public boolean isPatchSetLocked(ChangeNotes notes) throws OrmException, IOException {
+  public boolean isPatchSetLocked(ChangeNotes notes) throws IOException {
     Change change = notes.getChange();
-    if (change.getStatus() == Change.Status.MERGED) {
+    if (change.isMerged()) {
       return false;
     }
 
@@ -202,7 +154,7 @@
 
     ApprovalsUtil approvalsUtil = approvalsUtilProvider.get();
     for (PatchSetApproval ap :
-        approvalsUtil.byPatchSet(dbProvider.get(), notes, change.currentPatchSetId(), null, null)) {
+        approvalsUtil.byPatchSet(notes, change.currentPatchSetId(), null, null)) {
       LabelType type = projectState.getLabelTypes(notes).byLabel(ap.getLabel());
       if (type != null
           && ap.getValue() == 1
diff --git a/java/com/google/gerrit/server/ProjectUtil.java b/java/com/google/gerrit/server/ProjectUtil.java
index 1a327db..1db4aa3 100644
--- a/java/com/google/gerrit/server/ProjectUtil.java
+++ b/java/com/google/gerrit/server/ProjectUtil.java
@@ -44,4 +44,28 @@
       return exists;
     }
   }
+
+  public static String sanitizeProjectName(String name) {
+    name = stripGitSuffix(name);
+    name = stripTrailingSlash(name);
+    return name;
+  }
+
+  public static String stripGitSuffix(String name) {
+    if (name.endsWith(".git")) {
+      // Be nice and drop the trailing ".git" suffix, which we never keep
+      // in our database, but clients might mistakenly provide anyway.
+      //
+      name = name.substring(0, name.length() - 4);
+      name = stripTrailingSlash(name);
+    }
+    return name;
+  }
+
+  private static String stripTrailingSlash(String name) {
+    while (name.endsWith("/")) {
+      name = name.substring(0, name.length() - 1);
+    }
+    return name;
+  }
 }
diff --git a/java/com/google/gerrit/server/PublishCommentUtil.java b/java/com/google/gerrit/server/PublishCommentUtil.java
index a90f3e7..ad93ef0 100644
--- a/java/com/google/gerrit/server/PublishCommentUtil.java
+++ b/java/com/google/gerrit/server/PublishCommentUtil.java
@@ -19,14 +19,13 @@
 import static java.util.stream.Collectors.toSet;
 
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.reviewdb.client.Comment;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSet.Id;
 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.update.ChangeContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.Collection;
@@ -47,21 +46,19 @@
   }
 
   public void publish(
-      ChangeContext ctx, PatchSet.Id psId, Collection<Comment> drafts, @Nullable String tag)
-      throws OrmException {
+      ChangeContext ctx, PatchSet.Id psId, Collection<Comment> drafts, @Nullable String tag) {
     ChangeNotes notes = ctx.getNotes();
     checkArgument(notes != null);
     if (drafts.isEmpty()) {
       return;
     }
 
-    Map<Id, PatchSet> patchSets =
-        psUtil.getAsMap(
-            ctx.getDb(), notes, drafts.stream().map(d -> psId(notes, d)).collect(toSet()));
+    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));
       if (ps == null) {
-        throw new OrmException("patch set " + ps + " not found");
+        throw new StorageException("patch set " + ps + " not found");
       }
       d.writtenOn = ctx.getWhen();
       d.tag = tag;
@@ -71,10 +68,10 @@
       try {
         CommentsUtil.setCommentRevId(d, patchListCache, notes.getChange(), ps);
       } catch (PatchListNotAvailableException e) {
-        throw new OrmException(e);
+        throw new StorageException(e);
       }
     }
-    commentsUtil.putComments(ctx.getDb(), ctx.getUpdate(psId), PUBLISHED, drafts);
+    commentsUtil.putComments(ctx.getUpdate(psId), PUBLISHED, drafts);
   }
 
   private static PatchSet.Id psId(ChangeNotes notes, Comment c) {
diff --git a/java/com/google/gerrit/server/ReviewerSet.java b/java/com/google/gerrit/server/ReviewerSet.java
index 67b1d9d..f36e3ab 100644
--- a/java/com/google/gerrit/server/ReviewerSet.java
+++ b/java/com/google/gerrit/server/ReviewerSet.java
@@ -34,8 +34,7 @@
  * state {@link ReviewerStateInternal#REMOVED} are ever exposed by this interface.
  */
 public class ReviewerSet {
-  private static final ReviewerSet EMPTY =
-      new ReviewerSet(ImmutableTable.<ReviewerStateInternal, Account.Id, Timestamp>of());
+  private static final ReviewerSet EMPTY = new ReviewerSet(ImmutableTable.of());
 
   public static ReviewerSet fromApprovals(Iterable<PatchSetApproval> approvals) {
     PatchSetApproval first = null;
diff --git a/java/com/google/gerrit/server/Sequences.java b/java/com/google/gerrit/server/Sequences.java
deleted file mode 100644
index fcf0759..0000000
--- a/java/com/google/gerrit/server/Sequences.java
+++ /dev/null
@@ -1,166 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server;
-
-import static com.google.common.base.Preconditions.checkArgument;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.ImmutableList;
-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.Timer2;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-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.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.notedb.NotesMigration;
-import com.google.gerrit.server.notedb.RepoSequence;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
-import java.util.ArrayList;
-import java.util.List;
-import org.eclipse.jgit.lib.Config;
-
-@Singleton
-public class Sequences {
-  public static final String NAME_ACCOUNTS = "accounts";
-  public static final String NAME_GROUPS = "groups";
-  public static final String NAME_CHANGES = "changes";
-
-  public static int getChangeSequenceGap(Config cfg) {
-    return cfg.getInt("noteDb", "changes", "initialSequenceGap", 1000);
-  }
-
-  private enum SequenceType {
-    ACCOUNTS,
-    CHANGES,
-    GROUPS;
-  }
-
-  private final Provider<ReviewDb> db;
-  private final NotesMigration migration;
-  private final RepoSequence accountSeq;
-  private final RepoSequence changeSeq;
-  private final RepoSequence groupSeq;
-  private final Timer2<SequenceType, Boolean> nextIdLatency;
-
-  @Inject
-  public Sequences(
-      @GerritServerConfig Config cfg,
-      Provider<ReviewDb> db,
-      NotesMigration migration,
-      GitRepositoryManager repoManager,
-      GitReferenceUpdated gitRefUpdated,
-      AllProjectsName allProjects,
-      AllUsersName allUsers,
-      MetricMaker metrics) {
-    this.db = db;
-    this.migration = migration;
-
-    int accountBatchSize = cfg.getInt("noteDb", "accounts", "sequenceBatchSize", 1);
-    accountSeq =
-        new RepoSequence(
-            repoManager,
-            gitRefUpdated,
-            allUsers,
-            NAME_ACCOUNTS,
-            () -> ReviewDb.FIRST_ACCOUNT_ID,
-            accountBatchSize);
-
-    int gap = getChangeSequenceGap(cfg);
-    @SuppressWarnings("deprecation")
-    RepoSequence.Seed changeSeed = () -> db.get().nextChangeId() + gap;
-    int changeBatchSize = cfg.getInt("noteDb", "changes", "sequenceBatchSize", 20);
-    changeSeq =
-        new RepoSequence(
-            repoManager, gitRefUpdated, allProjects, NAME_CHANGES, changeSeed, changeBatchSize);
-
-    RepoSequence.Seed groupSeed = () -> nextGroupId(db.get());
-    int groupBatchSize = 1;
-    groupSeq =
-        new RepoSequence(
-            repoManager, gitRefUpdated, allUsers, NAME_GROUPS, groupSeed, groupBatchSize);
-
-    nextIdLatency =
-        metrics.newTimer(
-            "sequence/next_id_latency",
-            new Description("Latency of requesting IDs from repo sequences")
-                .setCumulative()
-                .setUnit(Units.MILLISECONDS),
-            Field.ofEnum(SequenceType.class, "sequence"),
-            Field.ofBoolean("multiple"));
-  }
-
-  public int nextAccountId() throws OrmException {
-    try (Timer2.Context timer = nextIdLatency.start(SequenceType.ACCOUNTS, false)) {
-      return accountSeq.next();
-    }
-  }
-
-  public int nextChangeId() throws OrmException {
-    if (!migration.readChangeSequence()) {
-      return nextChangeId(db.get());
-    }
-    try (Timer2.Context timer = nextIdLatency.start(SequenceType.CHANGES, false)) {
-      return changeSeq.next();
-    }
-  }
-
-  public ImmutableList<Integer> nextChangeIds(int count) throws OrmException {
-    if (migration.readChangeSequence()) {
-      try (Timer2.Context timer = nextIdLatency.start(SequenceType.CHANGES, count > 1)) {
-        return changeSeq.next(count);
-      }
-    }
-
-    if (count == 0) {
-      return ImmutableList.of();
-    }
-    checkArgument(count > 0, "count is negative: %s", count);
-    List<Integer> ids = new ArrayList<>(count);
-    ReviewDb db = this.db.get();
-    for (int i = 0; i < count; i++) {
-      ids.add(nextChangeId(db));
-    }
-    return ImmutableList.copyOf(ids);
-  }
-
-  public int nextGroupId() throws OrmException {
-    try (Timer2.Context timer = nextIdLatency.start(SequenceType.GROUPS, false)) {
-      return groupSeq.next();
-    }
-  }
-
-  @VisibleForTesting
-  public RepoSequence getChangeIdRepoSequence() {
-    return changeSeq;
-  }
-
-  @SuppressWarnings("deprecation")
-  private static int nextChangeId(ReviewDb db) throws OrmException {
-    return db.nextChangeId();
-  }
-
-  @SuppressWarnings("deprecation")
-  static int nextGroupId(ReviewDb db) throws OrmException {
-    return db.nextAccountGroupId();
-  }
-}
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/server/StarredChangesUtil.java
index 67f5f85..9b4433e 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -31,16 +31,16 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.exceptions.StorageException;
+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.reviewdb.server.ReviewDb;
 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.git.LockFailureException;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.index.change.ChangeIndexer;
 import com.google.gerrit.server.logging.TraceContext;
@@ -48,7 +48,6 @@
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -168,7 +167,6 @@
   private final GitRepositoryManager repoManager;
   private final GitReferenceUpdated gitRefUpdated;
   private final AllUsersName allUsers;
-  private final Provider<ReviewDb> dbProvider;
   private final Provider<PersonIdent> serverIdent;
   private final ChangeIndexer indexer;
   private final Provider<InternalChangeQuery> queryProvider;
@@ -178,25 +176,22 @@
       GitRepositoryManager repoManager,
       GitReferenceUpdated gitRefUpdated,
       AllUsersName allUsers,
-      Provider<ReviewDb> dbProvider,
       @GerritPersonIdent Provider<PersonIdent> serverIdent,
       ChangeIndexer indexer,
       Provider<InternalChangeQuery> queryProvider) {
     this.repoManager = repoManager;
     this.gitRefUpdated = gitRefUpdated;
     this.allUsers = allUsers;
-    this.dbProvider = dbProvider;
     this.serverIdent = serverIdent;
     this.indexer = indexer;
     this.queryProvider = queryProvider;
   }
 
-  public ImmutableSortedSet<String> getLabels(Account.Id accountId, Change.Id changeId)
-      throws OrmException {
+  public ImmutableSortedSet<String> getLabels(Account.Id accountId, Change.Id changeId) {
     try (Repository repo = repoManager.openRepository(allUsers)) {
       return readLabels(repo, RefNames.refsStarredChanges(changeId, accountId)).labels();
     } catch (IOException e) {
-      throw new OrmException(
+      throw new StorageException(
           String.format(
               "Reading stars from change %d for account %d failed",
               changeId.get(), accountId.get()),
@@ -210,7 +205,7 @@
       Change.Id changeId,
       Set<String> labelsToAdd,
       Set<String> labelsToRemove)
-      throws OrmException, IllegalLabelException {
+      throws IllegalLabelException {
     try (Repository repo = repoManager.openRepository(allUsers)) {
       String refName = RefNames.refsStarredChanges(changeId, accountId);
       StarRef old = readLabels(repo, refName);
@@ -230,16 +225,16 @@
         updateLabels(repo, refName, old.objectId(), labels);
       }
 
-      indexer.index(dbProvider.get(), project, changeId);
+      indexer.index(project, changeId);
       return ImmutableSortedSet.copyOf(labels);
     } catch (IOException e) {
-      throw new OrmException(
+      throw new StorageException(
           String.format("Star change %d for account %d failed", changeId.get(), accountId.get()),
           e);
     }
   }
 
-  public void unstarAll(Project.NameKey project, Change.Id changeId) throws OrmException {
+  public void unstarAll(Project.NameKey project, Change.Id changeId) {
     try (Repository repo = repoManager.openRepository(allUsers);
         RevWalk rw = new RevWalk(repo)) {
       BatchRefUpdate batchUpdate = repo.getRefDatabase().newBatchUpdate();
@@ -248,10 +243,8 @@
       batchUpdate.setRefLogMessage("Unstar change " + changeId.get(), true);
       for (Account.Id accountId : byChangeFromIndex(changeId).keySet()) {
         String refName = RefNames.refsStarredChanges(changeId, accountId);
-        Ref ref = repo.getRefDatabase().getRef(refName);
-        if (ref != null) {
-          batchUpdate.addCommand(new ReceiveCommand(ref.getObjectId(), ObjectId.zeroId(), refName));
-        }
+        Ref ref = repo.getRefDatabase().exactRef(refName);
+        batchUpdate.addCommand(new ReceiveCommand(ref.getObjectId(), ObjectId.zeroId(), refName));
       }
       batchUpdate.execute(rw, NullProgressMonitor.INSTANCE);
       for (ReceiveCommand command : batchUpdate.getCommands()) {
@@ -266,13 +259,13 @@
           throw new IOException(message);
         }
       }
-      indexer.index(dbProvider.get(), project, changeId);
+      indexer.index(project, changeId);
     } catch (IOException e) {
-      throw new OrmException(String.format("Unstar change %d failed", changeId.get()), e);
+      throw new StorageException(String.format("Unstar change %d failed", changeId.get()), e);
     }
   }
 
-  public ImmutableMap<Account.Id, StarRef> byChange(Change.Id changeId) throws OrmException {
+  public ImmutableMap<Account.Id, StarRef> byChange(Change.Id changeId) {
     try (Repository repo = repoManager.openRepository(allUsers)) {
       ImmutableMap.Builder<Account.Id, StarRef> builder = ImmutableMap.builder();
       for (String refPart : getRefNames(repo, RefNames.refsStarredChangesPrefix(changeId))) {
@@ -285,13 +278,12 @@
       }
       return builder.build();
     } catch (IOException e) {
-      throw new OrmException(
+      throw new StorageException(
           String.format("Get accounts that starred change %d failed", changeId.get()), e);
     }
   }
 
-  public ImmutableListMultimap<Account.Id, String> byChangeFromIndex(Change.Id changeId)
-      throws OrmException {
+  public ImmutableListMultimap<Account.Id, String> byChangeFromIndex(Change.Id changeId) {
     List<ChangeData> changeData =
         queryProvider
             .get()
@@ -322,7 +314,7 @@
     }
   }
 
-  public void ignore(ChangeResource rsrc) throws OrmException, IllegalLabelException {
+  public void ignore(ChangeResource rsrc) throws IllegalLabelException {
     star(
         rsrc.getUser().asIdentifiedUser().getAccountId(),
         rsrc.getProject(),
@@ -331,7 +323,7 @@
         ImmutableSet.of());
   }
 
-  public void unignore(ChangeResource rsrc) throws OrmException, IllegalLabelException {
+  public void unignore(ChangeResource rsrc) throws IllegalLabelException {
     star(
         rsrc.getUser().asIdentifiedUser().getAccountId(),
         rsrc.getProject(),
@@ -340,11 +332,11 @@
         ImmutableSet.of(IGNORE_LABEL));
   }
 
-  public boolean isIgnoredBy(Change.Id changeId, Account.Id accountId) throws OrmException {
+  public boolean isIgnoredBy(Change.Id changeId, Account.Id accountId) {
     return getLabels(accountId, changeId).contains(IGNORE_LABEL);
   }
 
-  public boolean isIgnored(ChangeResource rsrc) throws OrmException {
+  public boolean isIgnored(ChangeResource rsrc) {
     return isIgnoredBy(rsrc.getChange().getId(), rsrc.getUser().asIdentifiedUser().getAccountId());
   }
 
@@ -364,7 +356,7 @@
     return UNREVIEWED_LABEL + "/" + ps;
   }
 
-  public void markAsReviewed(ChangeResource rsrc) throws OrmException, IllegalLabelException {
+  public void markAsReviewed(ChangeResource rsrc) throws IllegalLabelException {
     star(
         rsrc.getUser().asIdentifiedUser().getAccountId(),
         rsrc.getProject(),
@@ -373,7 +365,7 @@
         ImmutableSet.of(getUnreviewedLabel(rsrc.getChange())));
   }
 
-  public void markAsUnreviewed(ChangeResource rsrc) throws OrmException, IllegalLabelException {
+  public void markAsUnreviewed(ChangeResource rsrc) throws IllegalLabelException {
     star(
         rsrc.getUser().asIdentifiedUser().getAccountId(),
         rsrc.getProject(),
@@ -455,7 +447,7 @@
 
   private void updateLabels(
       Repository repo, String refName, ObjectId oldObjectId, Collection<String> labels)
-      throws IOException, OrmException, InvalidLabelsException {
+      throws IOException, InvalidLabelsException {
     try (TraceTimer traceTimer =
             TraceContext.newTimer("Update star labels in %s (labels=%s)", refName, labels);
         RevWalk rw = new RevWalk(repo)) {
@@ -482,14 +474,13 @@
         case REJECTED_MISSING_OBJECT:
         case REJECTED_OTHER_REASON:
         default:
-          throw new OrmException(
+          throw new StorageException(
               String.format("Update star labels on ref %s failed: %s", refName, result.name()));
       }
     }
   }
 
-  private void deleteRef(Repository repo, String refName, ObjectId oldObjectId)
-      throws IOException, OrmException {
+  private void deleteRef(Repository repo, String refName, ObjectId oldObjectId) throws IOException {
     if (ObjectId.zeroId().equals(oldObjectId)) {
       // ref doesn't exist
       return;
@@ -518,7 +509,7 @@
         case REJECTED_MISSING_OBJECT:
         case REJECTED_OTHER_REASON:
         default:
-          throw new OrmException(
+          throw new StorageException(
               String.format("Delete star ref %s failed: %s", refName, result.name()));
       }
     }
diff --git a/java/com/google/gerrit/server/StartupChecks.java b/java/com/google/gerrit/server/StartupChecks.java
index 5ece91d..9bf94ae 100644
--- a/java/com/google/gerrit/server/StartupChecks.java
+++ b/java/com/google/gerrit/server/StartupChecks.java
@@ -44,7 +44,7 @@
 
   @Override
   public void start() throws StartupException {
-    startupChecks.runEach(c -> c.check(), StartupException.class);
+    startupChecks.runEach(StartupCheck::check, StartupException.class);
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/UsedAt.java b/java/com/google/gerrit/server/UsedAt.java
deleted file mode 100644
index a5459d1..0000000
--- a/java/com/google/gerrit/server/UsedAt.java
+++ /dev/null
@@ -1,46 +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;
-
-import static java.lang.annotation.ElementType.METHOD;
-import static java.lang.annotation.ElementType.TYPE;
-import static java.lang.annotation.RetentionPolicy.RUNTIME;
-
-import com.google.common.annotations.GwtCompatible;
-import com.google.inject.BindingAnnotation;
-import java.lang.annotation.Retention;
-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.
- */
-@BindingAnnotation
-@Target({METHOD, TYPE})
-@Retention(RUNTIME)
-@GwtCompatible
-public @interface UsedAt {
-  /** Enumeration of projects that call a method that would otherwise be private. */
-  enum Project {
-    GOOGLE,
-    COLLABNET,
-    PLUGIN_DELETE_PROJECT,
-    PLUGIN_SERVICEUSER,
-    PLUGINS_ALL, // Use this project if a method/type is generally made available to all plugins.
-  }
-
-  /** Reference to the project that uses the method annotated with this annotation. */
-  Project value();
-}
diff --git a/java/com/google/gerrit/server/WebLinks.java b/java/com/google/gerrit/server/WebLinks.java
index 39a2328..589344c 100644
--- a/java/com/google/gerrit/server/WebLinks.java
+++ b/java/com/google/gerrit/server/WebLinks.java
@@ -19,7 +19,6 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.FluentIterable;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.data.WebLinkInfoCommon;
 import com.google.gerrit.extensions.common.DiffWebLinkInfo;
 import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.extensions.registration.DynamicSet;
@@ -54,17 +53,6 @@
         return true;
       };
 
-  private static final Predicate<WebLinkInfoCommon> INVALID_WEBLINK_COMMON =
-      link -> {
-        if (link == null) {
-          return false;
-        } else if (Strings.isNullOrEmpty(link.name) || Strings.isNullOrEmpty(link.url)) {
-          logger.atWarning().log("%s is missing name and/or url", link.getClass().getName());
-          return false;
-        }
-        return true;
-      };
-
   private final DynamicSet<PatchSetWebLink> patchSetLinks;
   private final DynamicSet<ParentWebLink> parentLinks;
   private final DynamicSet<FileWebLink> fileLinks;
@@ -130,25 +118,13 @@
    * @param file File name.
    * @return Links for file history
    */
-  public List<WebLinkInfoCommon> getFileHistoryLinks(String project, String revision, String file) {
+  public List<WebLinkInfo> getFileHistoryLinks(String project, String revision, String file) {
     if (Patch.isMagic(file)) {
       return Collections.emptyList();
     }
     return FluentIterable.from(fileHistoryLinks)
-        .transform(
-            webLink -> {
-              WebLinkInfo info = webLink.getFileHistoryWebLink(project, revision, file);
-              if (info == null) {
-                return null;
-              }
-              WebLinkInfoCommon commonInfo = new WebLinkInfoCommon();
-              commonInfo.name = info.name;
-              commonInfo.imageUrl = info.imageUrl;
-              commonInfo.url = info.url;
-              commonInfo.target = info.target;
-              return commonInfo;
-            })
-        .filter(INVALID_WEBLINK_COMMON)
+        .transform(webLink -> webLink.getFileHistoryWebLink(project, revision, file))
+        .filter(INVALID_WEBLINK)
         .toList();
   }
 
diff --git a/java/com/google/gerrit/server/account/AccountConfig.java b/java/com/google/gerrit/server/account/AccountConfig.java
index d58036d..06f7a08 100644
--- a/java/com/google/gerrit/server/account/AccountConfig.java
+++ b/java/com/google/gerrit/server/account/AccountConfig.java
@@ -21,6 +21,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+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;
@@ -34,7 +35,6 @@
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.git.meta.VersionedMetaData;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.ArrayList;
@@ -200,9 +200,9 @@
    * Creates a new account.
    *
    * @return the new account
-   * @throws OrmDuplicateKeyException if the user branch already exists
+   * @throws DuplicateKeyException if the user branch already exists
    */
-  public Account getNewAccount() throws OrmDuplicateKeyException {
+  public Account getNewAccount() throws DuplicateKeyException {
     return getNewAccount(TimeUtil.nowTs());
   }
 
@@ -210,12 +210,12 @@
    * Creates a new account.
    *
    * @return the new account
-   * @throws OrmDuplicateKeyException if the user branch already exists
+   * @throws DuplicateKeyException if the user branch already exists
    */
-  Account getNewAccount(Timestamp registeredOn) throws OrmDuplicateKeyException {
+  Account getNewAccount(Timestamp registeredOn) throws DuplicateKeyException {
     checkLoaded();
     if (revision != null) {
-      throw new OrmDuplicateKeyException(String.format("account %s already exists", accountId));
+      throw new DuplicateKeyException(String.format("account %s already exists", accountId));
     }
     this.loadedAccountProperties =
         Optional.of(new AccountProperties(accountId, registeredOn, new Config(), null));
diff --git a/java/com/google/gerrit/server/account/AccountControl.java b/java/com/google/gerrit/server/account/AccountControl.java
index b43e786..4b8be81 100644
--- a/java/com/google/gerrit/server/account/AccountControl.java
+++ b/java/com/google/gerrit/server/account/AccountControl.java
@@ -17,7 +17,7 @@
 import static java.util.stream.Collectors.toSet;
 
 import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+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;
@@ -80,7 +80,7 @@
 
   private Boolean viewAll;
 
-  AccountControl(
+  private AccountControl(
       PermissionBackend permissionBackend,
       ProjectCache projectCache,
       GroupControl.Factory groupControlFactory,
@@ -106,17 +106,6 @@
    * because {@link GroupMembership#getKnownGroups()} may only return a subset of the effective
    * groups.
    */
-  public boolean canSee(Account otherUser) {
-    return canSee(otherUser.getId());
-  }
-
-  /**
-   * Returns true if the current user is allowed to see the otherUser, based on the account
-   * visibility policy. Depending on the group membership realms supported, this may not be able to
-   * determine SAME_GROUP or VISIBLE_GROUP correctly (defaulting to not being visible). This is
-   * because {@link GroupMembership#getKnownGroups()} may only return a subset of the effective
-   * groups.
-   */
   public boolean canSee(Account.Id otherUser) {
     return canSee(
         new OtherUser() {
diff --git a/java/com/google/gerrit/server/account/AccountManager.java b/java/com/google/gerrit/server/account/AccountManager.java
index 0772bb3..82acbfd 100644
--- a/java/com/google/gerrit/server/account/AccountManager.java
+++ b/java/com/google/gerrit/server/account/AccountManager.java
@@ -25,12 +25,12 @@
 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.common.errors.NoSuchGroupException;
+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.Sequences;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.AccountsUpdate.AccountUpdater;
 import com.google.gerrit.server.account.externalids.DuplicateExternalIdKeyException;
@@ -40,9 +40,9 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.group.db.GroupsUpdate;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
+import com.google.gerrit.server.notedb.Sequences;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.ssh.SshKeyCache;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -163,7 +163,7 @@
       // return the identity to the caller.
       update(who, extId);
       return new AuthResult(extId.accountId(), who.getExternalIdKey(), false);
-    } catch (OrmException | ConfigInvalidException e) {
+    } catch (StorageException | ConfigInvalidException e) {
       throw new AccountException("Authentication error", e);
     }
   }
@@ -214,7 +214,7 @@
   }
 
   private void update(AuthRequest who, ExternalId extId)
-      throws OrmException, IOException, ConfigInvalidException, AccountException {
+      throws IOException, ConfigInvalidException, AccountException {
     IdentifiedUser user = userFactory.create(extId.accountId());
     List<Consumer<InternalAccountUpdate.Builder>> accountUpdates = new ArrayList<>();
 
@@ -265,12 +265,12 @@
               user.getAccountId(),
               AccountUpdater.joinConsumers(accountUpdates))
           .orElseThrow(
-              () -> new OrmException("Account " + user.getAccountId() + " has been deleted"));
+              () -> new StorageException("Account " + user.getAccountId() + " has been deleted"));
     }
   }
 
   private AuthResult create(AuthRequest who)
-      throws OrmException, AccountException, IOException, ConfigInvalidException {
+      throws AccountException, IOException, ConfigInvalidException {
     Account.Id newId = new Account.Id(sequences.nextAccountId());
     logger.atFine().log("Assigning new Id %s to account", newId);
 
@@ -379,7 +379,7 @@
   }
 
   private void addGroupMember(AccountGroup.UUID groupUuid, IdentifiedUser user)
-      throws OrmException, IOException, ConfigInvalidException, AccountException {
+      throws IOException, ConfigInvalidException, AccountException {
     // The user initiated this request by logging in. -> Attribute all modifications to that user.
     GroupsUpdate groupsUpdate = groupsUpdateFactory.create(user);
     InternalGroupUpdate groupUpdate =
@@ -404,7 +404,7 @@
    *     this time.
    */
   public AuthResult link(Account.Id to, AuthRequest who)
-      throws AccountException, OrmException, IOException, ConfigInvalidException {
+      throws AccountException, IOException, ConfigInvalidException {
     Optional<ExternalId> optionalExtId = externalIds.get(who.getExternalIdKey());
     if (optionalExtId.isPresent()) {
       ExternalId extId = optionalExtId.get();
@@ -441,12 +441,11 @@
    * @param to account to link the identity onto.
    * @param who the additional identity.
    * @return the result of linking the identity to the user.
-   * @throws OrmException
    * @throws AccountException the identity belongs to a different account, or it cannot be linked at
    *     this time.
    */
   public AuthResult updateLink(Account.Id to, AuthRequest who)
-      throws OrmException, AccountException, IOException, ConfigInvalidException {
+      throws AccountException, IOException, ConfigInvalidException {
     accountsUpdateProvider
         .get()
         .update(
@@ -478,7 +477,7 @@
    *     found
    */
   public void unlink(Account.Id from, ExternalId.Key extIdKey)
-      throws AccountException, OrmException, IOException, ConfigInvalidException {
+      throws AccountException, IOException, ConfigInvalidException {
     unlink(from, ImmutableList.of(extIdKey));
   }
 
@@ -491,7 +490,7 @@
    *     identity was not found
    */
   public void unlink(Account.Id from, Collection<ExternalId.Key> extIdKeys)
-      throws AccountException, OrmException, IOException, ConfigInvalidException {
+      throws AccountException, IOException, ConfigInvalidException {
     if (extIdKeys.isEmpty()) {
       return;
     }
diff --git a/java/com/google/gerrit/server/account/AccountResolver.java b/java/com/google/gerrit/server/account/AccountResolver.java
index 8c7cdb4..f9945b5 100644
--- a/java/com/google/gerrit/server/account/AccountResolver.java
+++ b/java/com/google/gerrit/server/account/AccountResolver.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.
@@ -14,269 +14,605 @@
 
 package com.google.gerrit.server.account;
 
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.collect.ImmutableSet.toImmutableSet;
-import static java.util.stream.Collectors.toSet;
+import static java.util.Comparator.comparing;
+import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.joining;
 
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Suppliers;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Streams;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.extensions.restapi.AuthException;
 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.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.query.account.InternalAccountQuery;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.util.Collections;
-import java.util.HashSet;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
+import java.util.TreeSet;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import java.util.stream.Stream;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
+/**
+ * Helper for resolving accounts given arbitrary user-provided input.
+ *
+ * <p>The {@code resolve*} methods each define a list of accepted formats for account resolution.
+ * The algorithm for resolving accounts from a list of formats is as follows:
+ *
+ * <ol>
+ *   <li>For each recognized format in the order listed in the method Javadoc, check whether the
+ *       input matches that format.
+ *   <li>If so, resolve accounts according to that format.
+ *   <li>Filter out invisible and inactive accounts.
+ *   <li>If the result list is non-empty, return.
+ *   <li>If the format is listed above as being short-circuiting, return.
+ *   <li>Otherwise, return to step 1 with the next format.
+ * </ol>
+ *
+ * <p>The result never includes accounts that are not visible to the calling user. It also never
+ * includes inactive accounts, with a small number of specific exceptions noted in method Javadoc.
+ */
 @Singleton
 public class AccountResolver {
-  private final Provider<CurrentUser> self;
-  private final Realm realm;
-  private final Accounts accounts;
-  private final AccountCache byId;
-  private final IdentifiedUser.GenericFactory userFactory;
+  public static class UnresolvableAccountException extends UnprocessableEntityException {
+    private static final long serialVersionUID = 1L;
+    private final Result result;
+
+    @VisibleForTesting
+    UnresolvableAccountException(Result result) {
+      super(exceptionMessage(result));
+      this.result = result;
+    }
+
+    public boolean isSelf() {
+      return result.isSelf();
+    }
+  }
+
+  public static String exceptionMessage(Result result) {
+    checkArgument(result.asList().size() != 1);
+    if (result.asList().isEmpty()) {
+      if (result.isSelf()) {
+        return "Resolving account '" + result.input() + "' requires login";
+      }
+      if (result.filteredInactive().isEmpty()) {
+        return "Account '" + result.input() + "' not found";
+      }
+      return result.filteredInactive().stream()
+          .map(a -> formatForException(result, a))
+          .collect(
+              joining(
+                  "\n",
+                  "Account '"
+                      + result.input()
+                      + "' only matches inactive accounts. To use an inactive account, retry with"
+                      + " one of the following exact account IDs:\n",
+                  ""));
+    }
+
+    return result.asList().stream()
+        .map(a -> formatForException(result, a))
+        .collect(joining("\n", "Account '" + result.input() + "' is ambiguous:\n", ""));
+  }
+
+  private static String formatForException(Result result, AccountState state) {
+    return state.getAccount().getId()
+        + ": "
+        + state.getAccount().getNameEmail(result.accountResolver().anonymousCowardName);
+  }
+
+  public static boolean isSelf(String input) {
+    return "self".equals(input) || "me".equals(input);
+  }
+
+  public class Result {
+    private final String input;
+    private final ImmutableList<AccountState> list;
+    private final ImmutableList<AccountState> filteredInactive;
+
+    @VisibleForTesting
+    Result(String input, List<AccountState> list, List<AccountState> filteredInactive) {
+      this.input = requireNonNull(input);
+      this.list = canonicalize(list);
+      this.filteredInactive = canonicalize(filteredInactive);
+    }
+
+    private ImmutableList<AccountState> canonicalize(List<AccountState> list) {
+      TreeSet<AccountState> set = new TreeSet<>(comparing(a -> a.getAccount().getId().get()));
+      set.addAll(requireNonNull(list));
+      return ImmutableList.copyOf(set);
+    }
+
+    public String input() {
+      return input;
+    }
+
+    public boolean isSelf() {
+      return AccountResolver.isSelf(input);
+    }
+
+    public ImmutableList<AccountState> asList() {
+      return list;
+    }
+
+    public ImmutableSet<Account.Id> asNonEmptyIdSet() throws UnresolvableAccountException {
+      if (list.isEmpty()) {
+        throw new UnresolvableAccountException(this);
+      }
+      return asIdSet();
+    }
+
+    public ImmutableSet<Account.Id> asIdSet() {
+      return list.stream().map(a -> a.getAccount().getId()).collect(toImmutableSet());
+    }
+
+    public AccountState asUnique() throws UnresolvableAccountException {
+      ensureUnique();
+      return list.get(0);
+    }
+
+    private void ensureUnique() throws UnresolvableAccountException {
+      if (list.size() != 1) {
+        throw new UnresolvableAccountException(this);
+      }
+    }
+
+    public IdentifiedUser asUniqueUser() throws UnresolvableAccountException {
+      ensureUnique();
+      if (isSelf()) {
+        // In the special case of "self", use the exact IdentifiedUser from the request context, to
+        // preserve the peer address and any other per-request state.
+        return self.get().asIdentifiedUser();
+      }
+      return userFactory.create(asUnique());
+    }
+
+    public IdentifiedUser asUniqueUserOnBehalfOf(CurrentUser caller)
+        throws UnresolvableAccountException {
+      ensureUnique();
+      if (isSelf()) {
+        // TODO(dborowitz): This preserves old behavior, but it seems wrong to discard the caller.
+        return self.get().asIdentifiedUser();
+      }
+      return userFactory.runAs(
+          null, list.get(0).getAccount().getId(), requireNonNull(caller).getRealUser());
+    }
+
+    @VisibleForTesting
+    ImmutableList<AccountState> filteredInactive() {
+      return filteredInactive;
+    }
+
+    private AccountResolver accountResolver() {
+      return AccountResolver.this;
+    }
+  }
+
+  @VisibleForTesting
+  interface Searcher<I> {
+    default boolean callerShouldFilterOutInactiveCandidates() {
+      return true;
+    }
+
+    default boolean callerMayAssumeCandidatesAreVisible() {
+      return false;
+    }
+
+    Optional<I> tryParse(String input) throws IOException;
+
+    Stream<AccountState> search(I input) throws IOException, ConfigInvalidException;
+
+    boolean shortCircuitIfNoResults();
+
+    default Optional<Stream<AccountState>> trySearch(String input)
+        throws IOException, ConfigInvalidException {
+      Optional<I> parsed = tryParse(input);
+      return parsed.isPresent() ? Optional.of(search(parsed.get())) : Optional.empty();
+    }
+  }
+
+  @VisibleForTesting
+  abstract static class StringSearcher implements Searcher<String> {
+    @Override
+    public final Optional<String> tryParse(String input) {
+      return matches(input) ? Optional.of(input) : Optional.empty();
+    }
+
+    protected abstract boolean matches(String input);
+  }
+
+  private abstract class AccountIdSearcher implements Searcher<Account.Id> {
+    @Override
+    public final Stream<AccountState> search(Account.Id input) {
+      return Streams.stream(accountCache.get(input));
+    }
+  }
+
+  private class BySelf extends StringSearcher {
+    @Override
+    public boolean callerShouldFilterOutInactiveCandidates() {
+      return false;
+    }
+
+    @Override
+    public boolean callerMayAssumeCandidatesAreVisible() {
+      return true;
+    }
+
+    @Override
+    protected boolean matches(String input) {
+      return "self".equals(input) || "me".equals(input);
+    }
+
+    @Override
+    public Stream<AccountState> search(String input) {
+      CurrentUser user = self.get();
+      if (!user.isIdentifiedUser()) {
+        return Stream.empty();
+      }
+      return Stream.of(user.asIdentifiedUser().state());
+    }
+
+    @Override
+    public boolean shortCircuitIfNoResults() {
+      return true;
+    }
+  }
+
+  private class ByExactAccountId extends AccountIdSearcher {
+    @Override
+    public boolean callerShouldFilterOutInactiveCandidates() {
+      return false;
+    }
+
+    @Override
+    public Optional<Account.Id> tryParse(String input) {
+      return Account.Id.tryParse(input);
+    }
+
+    @Override
+    public boolean shortCircuitIfNoResults() {
+      return true;
+    }
+  }
+
+  private class ByParenthesizedAccountId extends AccountIdSearcher {
+    private final Pattern pattern = Pattern.compile("^.* \\(([1-9][0-9]*)\\)$");
+
+    @Override
+    public Optional<Account.Id> tryParse(String input) {
+      Matcher m = pattern.matcher(input);
+      return m.matches() ? Account.Id.tryParse(m.group(1)) : Optional.empty();
+    }
+
+    @Override
+    public boolean shortCircuitIfNoResults() {
+      return true;
+    }
+  }
+
+  private class ByUsername extends StringSearcher {
+    @Override
+    public boolean matches(String input) {
+      return ExternalId.isValidUsername(input);
+    }
+
+    @Override
+    public Stream<AccountState> search(String input) {
+      return Streams.stream(accountCache.getByUsername(input));
+    }
+
+    @Override
+    public boolean shortCircuitIfNoResults() {
+      return false;
+    }
+  }
+
+  private class ByNameAndEmail extends StringSearcher {
+    @Override
+    protected boolean matches(String input) {
+      int lt = input.indexOf('<');
+      int gt = input.indexOf('>');
+      return lt >= 0 && gt > lt && input.contains("@");
+    }
+
+    @Override
+    public Stream<AccountState> search(String nameOrEmail) throws IOException {
+      // TODO(dborowitz): This would probably work as a Searcher<Address>
+      int lt = nameOrEmail.indexOf('<');
+      int gt = nameOrEmail.indexOf('>');
+      Set<Account.Id> ids = emails.getAccountFor(nameOrEmail.substring(lt + 1, gt));
+      ImmutableList<AccountState> allMatches = toAccountStates(ids).collect(toImmutableList());
+      if (allMatches.isEmpty() || allMatches.size() == 1) {
+        return allMatches.stream();
+      }
+
+      // More than one match. If there are any that match the full name as well, return only that
+      // subset. Otherwise, all are equally non-matching, so return the full set.
+      String name = nameOrEmail.substring(0, lt - 1);
+      ImmutableList<AccountState> nameMatches =
+          allMatches.stream()
+              .filter(a -> name.equals(a.getAccount().getFullName()))
+              .collect(toImmutableList());
+      return !nameMatches.isEmpty() ? nameMatches.stream() : allMatches.stream();
+    }
+
+    @Override
+    public boolean shortCircuitIfNoResults() {
+      return true;
+    }
+  }
+
+  private class ByEmail extends StringSearcher {
+    @Override
+    protected boolean matches(String input) {
+      return input.contains("@");
+    }
+
+    @Override
+    public Stream<AccountState> search(String input) throws IOException {
+      return toAccountStates(emails.getAccountFor(input));
+    }
+
+    @Override
+    public boolean shortCircuitIfNoResults() {
+      return true;
+    }
+  }
+
+  private class FromRealm extends AccountIdSearcher {
+    @Override
+    public Optional<Account.Id> tryParse(String input) throws IOException {
+      return Optional.ofNullable(realm.lookup(input));
+    }
+
+    @Override
+    public boolean shortCircuitIfNoResults() {
+      return false;
+    }
+  }
+
+  private class ByFullName implements Searcher<AccountState> {
+    @Override
+    public boolean callerMayAssumeCandidatesAreVisible() {
+      return true; // Rely on enforceVisibility from the index.
+    }
+
+    @Override
+    public Optional<AccountState> tryParse(String input) {
+      List<AccountState> results =
+          accountQueryProvider.get().enforceVisibility(true).byFullName(input);
+      return results.size() == 1 ? Optional.of(results.get(0)) : Optional.empty();
+    }
+
+    @Override
+    public Stream<AccountState> search(AccountState input) {
+      return Stream.of(input);
+    }
+
+    @Override
+    public boolean shortCircuitIfNoResults() {
+      return false;
+    }
+  }
+
+  private class ByDefaultSearch extends StringSearcher {
+    @Override
+    public boolean callerMayAssumeCandidatesAreVisible() {
+      return true; // Rely on enforceVisibility from the index.
+    }
+
+    @Override
+    protected boolean matches(String input) {
+      return true;
+    }
+
+    @Override
+    public Stream<AccountState> search(String input) {
+      // At this point we have no clue. Just perform a whole bunch of suggestions and pray we come
+      // up with a reasonable result list.
+      // TODO(dborowitz): This doesn't match the documentation; consider whether it's possible to be
+      // more strict here.
+      return accountQueryProvider.get().enforceVisibility(true).byDefault(input).stream();
+    }
+
+    @Override
+    public boolean shortCircuitIfNoResults() {
+      // In practice this doesn't matter since this is the last searcher in the list, but considered
+      // on its own, it doesn't necessarily need to be terminal.
+      return false;
+    }
+  }
+
+  private final ImmutableList<Searcher<?>> nameOrEmailSearchers =
+      ImmutableList.of(
+          new ByNameAndEmail(),
+          new ByEmail(),
+          new FromRealm(),
+          new ByFullName(),
+          new ByDefaultSearch());
+
+  private final ImmutableList<Searcher<?>> searchers =
+      ImmutableList.<Searcher<?>>builder()
+          .add(new BySelf())
+          .add(new ByExactAccountId())
+          .add(new ByParenthesizedAccountId())
+          .add(new ByUsername())
+          .addAll(nameOrEmailSearchers)
+          .build();
+
+  private final AccountCache accountCache;
   private final AccountControl.Factory accountControlFactory;
-  private final Provider<InternalAccountQuery> accountQueryProvider;
   private final Emails emails;
+  private final IdentifiedUser.GenericFactory userFactory;
+  private final Provider<CurrentUser> self;
+  private final Provider<InternalAccountQuery> accountQueryProvider;
+  private final Realm realm;
+  private final String anonymousCowardName;
 
   @Inject
   AccountResolver(
-      Provider<CurrentUser> self,
-      Realm realm,
-      Accounts accounts,
-      AccountCache byId,
-      IdentifiedUser.GenericFactory userFactory,
+      AccountCache accountCache,
+      Emails emails,
       AccountControl.Factory accountControlFactory,
+      IdentifiedUser.GenericFactory userFactory,
+      Provider<CurrentUser> self,
       Provider<InternalAccountQuery> accountQueryProvider,
-      Emails emails) {
-    this.self = self;
+      Realm realm,
+      @AnonymousCowardName String anonymousCowardName) {
     this.realm = realm;
-    this.accounts = accounts;
-    this.byId = byId;
-    this.userFactory = userFactory;
+    this.accountCache = accountCache;
     this.accountControlFactory = accountControlFactory;
+    this.userFactory = userFactory;
+    this.self = self;
     this.accountQueryProvider = accountQueryProvider;
     this.emails = emails;
+    this.anonymousCowardName = anonymousCowardName;
   }
 
   /**
-   * Locate exactly one account matching the input string.
+   * Resolves all accounts matching the input string.
    *
-   * @param input a string of the format "Full Name &lt;email@example&gt;", just the email address
-   *     ("email@example"), a full name ("Full Name"), an account ID ("18419") or a user name
-   *     ("username").
-   * @return the single account that matches; null if no account matches or there are multiple
-   *     candidates. If {@code input} is a numeric string, returns an account if and only if that
-   *     number corresponds to an actual account ID.
+   * <p>The following input formats are recognized:
+   *
+   * <ul>
+   *   <li>The strings {@code "self"} and {@code "me"}, if the current user is an {@link
+   *       IdentifiedUser}. In this case, may return exactly one inactive account.
+   *   <li>A bare account ID ({@code "18419"}). In this case, may return exactly one inactive
+   *       account. This case short-circuits if the input matches.
+   *   <li>An account ID in parentheses following a full name ({@code "Full Name (18419)"}). This
+   *       case short-circuits if the input matches.
+   *   <li>A username ({@code "username"}).
+   *   <li>A full name and email address ({@code "Full Name <email@example>"}). This case
+   *       short-circuits if the input matches.
+   *   <li>An email address ({@code "email@example"}. This case short-circuits if the input matches.
+   *   <li>An account name recognized by the configured {@link Realm#lookup(String)} Realm}.
+   *   <li>A full name ({@code "Full Name"}).
+   *   <li>As a fallback, a {@link
+   *       com.google.gerrit.server.query.account.AccountPredicates#defaultPredicate(Schema,
+   *       boolean, String) default search} against the account index.
+   * </ul>
+   *
+   * @param input input string.
+   * @return a result describing matching accounts. Never null even if the result set is empty.
+   * @throws ConfigInvalidException if an error occurs.
+   * @throws IOException if an error occurs.
    */
-  public Account find(String input) throws OrmException, IOException, ConfigInvalidException {
-    Set<Account.Id> r = findAll(input);
-    if (r.size() == 1) {
-      return byId.get(r.iterator().next()).map(AccountState::getAccount).orElse(null);
-    }
+  public Result resolve(String input) throws ConfigInvalidException, IOException {
+    return searchImpl(input, searchers, visibilitySupplier(), accountActivityPredicate());
+  }
 
-    Account match = null;
-    for (Account.Id id : r) {
-      Optional<Account> account = byId.get(id).map(AccountState::getAccount);
-      if (!account.map(Account::isActive).orElse(false)) {
+  public Result resolve(String input, Predicate<AccountState> accountActivityPredicate)
+      throws ConfigInvalidException, IOException {
+    return searchImpl(input, searchers, visibilitySupplier(), accountActivityPredicate);
+  }
+
+  /**
+   * Resolves all accounts matching the input string by name or email.
+   *
+   * <p>The following input formats are recognized:
+   *
+   * <ul>
+   *   <li>A full name and email address ({@code "Full Name <email@example>"}). This case
+   *       short-circuits if the input matches.
+   *   <li>An email address ({@code "email@example"}. This case short-circuits if the input matches.
+   *   <li>An account name recognized by the configured {@link Realm#lookup(String)} Realm}.
+   *   <li>A full name ({@code "Full Name"}).
+   *   <li>As a fallback, a {@link
+   *       com.google.gerrit.server.query.account.AccountPredicates#defaultPredicate(Schema,
+   *       boolean, String) default search} against the account index.
+   * </ul>
+   *
+   * @param input input string.
+   * @return a result describing matching accounts. Never null even if the result set is empty.
+   * @throws ConfigInvalidException if an error occurs.
+   * @throws IOException if an error occurs.
+   * @deprecated for use only by MailUtil for parsing commit footers; that class needs to be
+   *     reevaluated.
+   */
+  @Deprecated
+  public Result resolveByNameOrEmail(String input) throws ConfigInvalidException, IOException {
+    return searchImpl(
+        input, nameOrEmailSearchers, visibilitySupplier(), accountActivityPredicate());
+  }
+
+  private Supplier<Predicate<AccountState>> visibilitySupplier() {
+    return () -> accountControlFactory.get()::canSee;
+  }
+
+  private Predicate<AccountState> accountActivityPredicate() {
+    return (AccountState accountState) -> accountState.getAccount().isActive();
+  }
+
+  @VisibleForTesting
+  Result searchImpl(
+      String input,
+      ImmutableList<Searcher<?>> searchers,
+      Supplier<Predicate<AccountState>> visibilitySupplier,
+      Predicate<AccountState> accountActivityPredicate)
+      throws ConfigInvalidException, IOException {
+    visibilitySupplier = Suppliers.memoize(visibilitySupplier::get);
+    List<AccountState> inactive = new ArrayList<>();
+
+    for (Searcher<?> searcher : searchers) {
+      Optional<Stream<AccountState>> maybeResults = searcher.trySearch(input);
+      if (!maybeResults.isPresent()) {
         continue;
       }
-      if (match != null) {
-        return null;
-      }
-      match = account.get();
-    }
-    return match;
-  }
+      Stream<AccountState> results = maybeResults.get();
 
-  /**
-   * Find all accounts matching the input string.
-   *
-   * @param input a string of the format "Full Name &lt;email@example&gt;", just the email address
-   *     ("email@example"), a full name ("Full Name"), an account ID ("18419") or a user name
-   *     ("username").
-   * @return the accounts that match, empty set if none. Never null. If {@code input} is a numeric
-   *     string, returns a singleton set if that number corresponds to a real account ID, and an
-   *     empty set otherwise if it does not.
-   */
-  public Set<Account.Id> findAll(String input)
-      throws OrmException, IOException, ConfigInvalidException {
-    Matcher m = Pattern.compile("^.* \\(([1-9][0-9]*)\\)$").matcher(input);
-    if (m.matches()) {
-      Optional<Account.Id> id = Account.Id.tryParse(m.group(1));
-      if (id.isPresent()) {
-        return Streams.stream(accounts.get(id.get()))
-            .map(a -> a.getAccount().getId())
-            .collect(toImmutableSet());
-      }
-    }
-
-    if (input.matches("^[1-9][0-9]*$")) {
-      Optional<Account.Id> id = Account.Id.tryParse(input);
-      if (id.isPresent()) {
-        return Streams.stream(accounts.get(id.get()))
-            .map(a -> a.getAccount().getId())
-            .collect(toImmutableSet());
-      }
-    }
-
-    if (ExternalId.isValidUsername(input)) {
-      Optional<AccountState> who = byId.getByUsername(input);
-      if (who.isPresent()) {
-        return ImmutableSet.of(who.map(a -> a.getAccount().getId()).get());
-      }
-    }
-
-    return findAllByNameOrEmail(input);
-  }
-
-  /**
-   * Locate exactly one account matching the name or name/email string.
-   *
-   * @param nameOrEmail a string of the format "Full Name &lt;email@example&gt;", just the email
-   *     address ("email@example"), a full name ("Full Name").
-   * @return the single account that matches; null if no account matches or there are multiple
-   *     candidates.
-   */
-  public Account findByNameOrEmail(String nameOrEmail) throws OrmException, IOException {
-    Set<Account.Id> r = findAllByNameOrEmail(nameOrEmail);
-    return r.size() == 1
-        ? byId.get(r.iterator().next()).map(AccountState::getAccount).orElse(null)
-        : null;
-  }
-
-  /**
-   * Locate exactly one account matching the name or name/email string.
-   *
-   * @param nameOrEmail a string of the format "Full Name &lt;email@example&gt;", just the email
-   *     address ("email@example"), a full name ("Full Name").
-   * @return the accounts that match, empty collection if none. Never null.
-   */
-  public Set<Account.Id> findAllByNameOrEmail(String nameOrEmail) throws OrmException, IOException {
-    int lt = nameOrEmail.indexOf('<');
-    int gt = nameOrEmail.indexOf('>');
-    if (lt >= 0 && gt > lt && nameOrEmail.contains("@")) {
-      Set<Account.Id> ids = emails.getAccountFor(nameOrEmail.substring(lt + 1, gt));
-      if (ids.isEmpty() || ids.size() == 1) {
-        return ids;
+      if (!searcher.callerMayAssumeCandidatesAreVisible()) {
+        results = results.filter(visibilitySupplier.get());
       }
 
-      // more than one match, try to return the best one
-      String name = nameOrEmail.substring(0, lt - 1);
-      Set<Account.Id> nameMatches = new HashSet<>();
-      for (Account.Id id : ids) {
-        Optional<Account> a = byId.get(id).map(AccountState::getAccount);
-        if (a.isPresent() && name.equals(a.get().getFullName())) {
-          nameMatches.add(id);
-        }
-      }
-      return nameMatches.isEmpty() ? ids : nameMatches;
-    }
-
-    if (nameOrEmail.contains("@")) {
-      return emails.getAccountFor(nameOrEmail);
-    }
-
-    Account.Id id = realm.lookup(nameOrEmail);
-    if (id != null) {
-      return Collections.singleton(id);
-    }
-
-    List<AccountState> m = accountQueryProvider.get().byFullName(nameOrEmail);
-    if (m.size() == 1) {
-      return Collections.singleton(m.get(0).getAccount().getId());
-    }
-
-    // At this point we have no clue. Just perform a whole bunch of suggestions
-    // and pray we come up with a reasonable result list.
-    // TODO(dborowitz): This doesn't match the documentation; consider whether it's possible to be
-    // more strict here.
-    return accountQueryProvider.get().byDefault(nameOrEmail).stream()
-        .map(a -> a.getAccount().getId())
-        .collect(toSet());
-  }
-
-  /**
-   * Parses a account ID from a request body and returns the user.
-   *
-   * @param id ID of the account, can be a string of the format "{@code Full Name
-   *     <email@example.com>}", just the email address, a full name if it is unique, an account ID,
-   *     a user name or "{@code self}" for the calling user
-   * @return the user, never null.
-   * @throws UnprocessableEntityException thrown if the account ID cannot be resolved or if the
-   *     account is not visible to the calling user
-   */
-  public IdentifiedUser parse(String id)
-      throws AuthException, UnprocessableEntityException, OrmException, IOException,
-          ConfigInvalidException {
-    return parseOnBehalfOf(null, id);
-  }
-
-  /**
-   * Parses an account ID and returns the user without making any permission check whether the
-   * current user can see the account.
-   *
-   * @param id ID of the account, can be a string of the format "{@code Full Name
-   *     <email@example.com>}", just the email address, a full name if it is unique, an account ID,
-   *     a user name or "{@code self}" for the calling user
-   * @return the user, null if no user is found for the given account ID
-   * @throws AuthException thrown if 'self' is used as account ID and the current user is not
-   *     authenticated
-   * @throws OrmException
-   * @throws ConfigInvalidException
-   * @throws IOException
-   */
-  public IdentifiedUser parseId(String id)
-      throws AuthException, OrmException, IOException, ConfigInvalidException {
-    return parseIdOnBehalfOf(null, id);
-  }
-
-  /**
-   * Like {@link #parse(String)}, but also sets the {@link CurrentUser#getRealUser()} on the result.
-   */
-  public IdentifiedUser parseOnBehalfOf(@Nullable CurrentUser caller, String id)
-      throws AuthException, UnprocessableEntityException, OrmException, IOException,
-          ConfigInvalidException {
-    IdentifiedUser user = parseIdOnBehalfOf(caller, id);
-    if (user == null || !accountControlFactory.get().canSee(user.getAccount())) {
-      throw new UnprocessableEntityException(
-          String.format("Account '%s' is not found or ambiguous", id));
-    }
-    return user;
-  }
-
-  private IdentifiedUser parseIdOnBehalfOf(@Nullable CurrentUser caller, String id)
-      throws AuthException, OrmException, IOException, ConfigInvalidException {
-    if (id.equals("self")) {
-      CurrentUser user = self.get();
-      if (user.isIdentifiedUser()) {
-        return user.asIdentifiedUser();
-      } else if (user instanceof AnonymousUser) {
-        throw new AuthException("Authentication required");
+      List<AccountState> list;
+      if (searcher.callerShouldFilterOutInactiveCandidates()) {
+        // Keep track of all inactive candidates discovered by any searchers. If we end up short-
+        // circuiting, the inactive list will be discarded.
+        List<AccountState> active = new ArrayList<>();
+        results.forEach(a -> (accountActivityPredicate.test(a) ? active : inactive).add(a));
+        list = active;
       } else {
-        return null;
+        list = results.collect(toImmutableList());
+      }
+
+      if (!list.isEmpty()) {
+        return createResult(input, list);
+      }
+      if (searcher.shortCircuitIfNoResults()) {
+        // For a short-circuiting searcher, return results even if empty.
+        return !inactive.isEmpty() ? emptyResult(input, inactive) : createResult(input, list);
       }
     }
+    return emptyResult(input, inactive);
+  }
 
-    Account match = find(id);
-    if (match == null) {
-      return null;
-    }
-    CurrentUser realUser = caller != null ? caller.getRealUser() : null;
-    return userFactory.runAs(null, match.getId(), realUser);
+  private Result createResult(String input, List<AccountState> list) {
+    return new Result(input, list, ImmutableList.of());
+  }
+
+  private Result emptyResult(String input, List<AccountState> inactive) {
+    return new Result(input, ImmutableList.of(), inactive);
+  }
+
+  private Stream<AccountState> toAccountStates(Set<Account.Id> ids) {
+    return accountCache.get(ids).values().stream();
   }
 }
diff --git a/java/com/google/gerrit/server/account/AccountsUpdate.java b/java/com/google/gerrit/server/account/AccountsUpdate.java
index 0e3f379..20a1c97 100644
--- a/java/com/google/gerrit/server/account/AccountsUpdate.java
+++ b/java/com/google/gerrit/server/account/AccountsUpdate.java
@@ -24,29 +24,28 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.util.concurrent.Runnables;
-import com.google.gerrit.common.Nullable;
+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.InternalAccountUpdate.Builder;
 import com.google.gerrit.server.account.externalids.ExternalIdNotes;
 import com.google.gerrit.server.account.externalids.ExternalIdNotes.ExternalIdNotesLoader;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 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.git.LockFailureException;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.index.change.ReindexAfterRefUpdate;
-import com.google.gerrit.server.update.RefUpdateUtil;
+import com.google.gerrit.server.notedb.Sequences;
 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.gwtorm.server.OrmDuplicateKeyException;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.List;
@@ -81,7 +80,7 @@
  * commit in the user branch, and thus help debugging.
  *
  * <p>For creating a new account a new account ID can be retrieved from {@link
- * com.google.gerrit.server.Sequences#nextAccountId()}.
+ * Sequences#nextAccountId()}.
  *
  * <p>The account updates are written to NoteDb. In NoteDb accounts are represented as user branches
  * in the {@code All-Users} repository. Optionally a user branch can contain a 'account.config' file
@@ -123,16 +122,28 @@
   public interface Factory {
     /**
      * Creates an {@code AccountsUpdate} which uses the identity of the specified user as author for
-     * all commits related to accounts. The Gerrit server identity will be used as committer.
+     * all commits related to accounts. The server identity will be used as committer.
      *
-     * <p><strong>Note</strong>: Please use this method with care and rather consider to use the
-     * correct annotation on the provider of an {@code AccountsUpdate} instead.
+     * <p><strong>Note</strong>: Please use this method with care and consider using the {@link
+     * com.google.gerrit.server.UserInitiated} annotation on the provider of an {@code
+     * AccountsUpdate} instead.
      *
-     * @param currentUser the user to which modifications should be attributed, or {@code null} if
-     *     the Gerrit server identity should also be used as author
+     * @param currentUser the user to which modifications should be attributed
+     * @param externalIdNotesLoader the loader that should be used to load external ID notes
      */
-    AccountsUpdate create(
-        @Nullable IdentifiedUser currentUser, ExternalIdNotesLoader externalIdNotesLoader);
+    AccountsUpdate create(IdentifiedUser currentUser, ExternalIdNotesLoader externalIdNotesLoader);
+
+    /**
+     * Creates an {@code AccountsUpdate} which uses the server identity as author and committer for
+     * all commits related to accounts.
+     *
+     * <p><strong>Note</strong>: Please use this method with care and consider using the {@link
+     * com.google.gerrit.server.ServerInitiated} annotation on the provider of an {@code
+     * AccountsUpdate} instead.
+     *
+     * @param externalIdNotesLoader the loader that should be used to load external ID notes
+     */
+    AccountsUpdate createWithServerIdent(ExternalIdNotesLoader externalIdNotesLoader);
   }
 
   /**
@@ -154,12 +165,9 @@
     void update(AccountState accountState, InternalAccountUpdate.Builder update) throws IOException;
 
     static AccountUpdater join(List<AccountUpdater> updaters) {
-      return new AccountUpdater() {
-        @Override
-        public void update(AccountState accountState, Builder update) throws IOException {
-          for (AccountUpdater updater : updaters) {
-            updater.update(accountState, update);
-          }
+      return (accountState, update) -> {
+        for (AccountUpdater updater : updaters) {
+          updater.update(accountState, update);
         }
       };
     }
@@ -175,7 +183,7 @@
 
   private final GitRepositoryManager repoManager;
   private final GitReferenceUpdated gitRefUpdated;
-  @Nullable private final IdentifiedUser currentUser;
+  private final Optional<IdentifiedUser> currentUser;
   private final AllUsersName allUsersName;
   private final ExternalIds externalIds;
   private final Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory;
@@ -190,7 +198,7 @@
   // Invoked after updating the account but before committing the changes.
   private final Runnable beforeCommit;
 
-  @Inject
+  @AssistedInject
   AccountsUpdate(
       GitRepositoryManager repoManager,
       GitReferenceUpdated gitRefUpdated,
@@ -199,19 +207,44 @@
       Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory,
       RetryHelper retryHelper,
       @GerritPersonIdent PersonIdent serverIdent,
-      @Assisted @Nullable IdentifiedUser currentUser,
       @Assisted ExternalIdNotesLoader extIdNotesLoader) {
     this(
         repoManager,
         gitRefUpdated,
-        currentUser,
+        Optional.empty(),
         allUsersName,
         externalIds,
         metaDataUpdateInternalFactory,
         retryHelper,
         extIdNotesLoader,
         serverIdent,
-        createPersonIdent(serverIdent, currentUser),
+        createPersonIdent(serverIdent, Optional.empty()),
+        Runnables.doNothing(),
+        Runnables.doNothing());
+  }
+
+  @AssistedInject
+  AccountsUpdate(
+      GitRepositoryManager repoManager,
+      GitReferenceUpdated gitRefUpdated,
+      AllUsersName allUsersName,
+      ExternalIds externalIds,
+      Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory,
+      RetryHelper retryHelper,
+      @GerritPersonIdent PersonIdent serverIdent,
+      @Assisted IdentifiedUser currentUser,
+      @Assisted ExternalIdNotesLoader extIdNotesLoader) {
+    this(
+        repoManager,
+        gitRefUpdated,
+        Optional.of(currentUser),
+        allUsersName,
+        externalIds,
+        metaDataUpdateInternalFactory,
+        retryHelper,
+        extIdNotesLoader,
+        serverIdent,
+        createPersonIdent(serverIdent, Optional.of(currentUser)),
         Runnables.doNothing(),
         Runnables.doNothing());
   }
@@ -220,7 +253,7 @@
   public AccountsUpdate(
       GitRepositoryManager repoManager,
       GitReferenceUpdated gitRefUpdated,
-      @Nullable IdentifiedUser currentUser,
+      Optional<IdentifiedUser> currentUser,
       AllUsersName allUsersName,
       ExternalIds externalIds,
       Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory,
@@ -246,11 +279,11 @@
   }
 
   private static PersonIdent createPersonIdent(
-      PersonIdent serverIdent, @Nullable IdentifiedUser user) {
-    if (user == null) {
+      PersonIdent serverIdent, Optional<IdentifiedUser> user) {
+    if (!user.isPresent()) {
       return serverIdent;
     }
-    return user.newCommitterIdent(serverIdent.getWhen(), serverIdent.getTimeZone());
+    return user.get().newCommitterIdent(serverIdent.getWhen(), serverIdent.getTimeZone());
   }
 
   /**
@@ -260,14 +293,13 @@
    * @param accountId ID of the new account
    * @param init consumer to populate the new account
    * @return the newly created account
-   * @throws OrmDuplicateKeyException if the account already exists
+   * @throws DuplicateKeyException if the account already exists
    * @throws IOException if creating the user branch fails due to an IO error
-   * @throws OrmException if creating the user branch fails
    * @throws ConfigInvalidException if any of the account fields has an invalid value
    */
   public AccountState insert(
       String message, Account.Id accountId, Consumer<InternalAccountUpdate.Builder> init)
-      throws OrmException, IOException, ConfigInvalidException {
+      throws IOException, ConfigInvalidException {
     return insert(message, accountId, AccountUpdater.fromConsumer(init));
   }
 
@@ -278,13 +310,12 @@
    * @param accountId ID of the new account
    * @param updater updater to populate the new account
    * @return the newly created account
-   * @throws OrmDuplicateKeyException if the account already exists
+   * @throws DuplicateKeyException if the account already exists
    * @throws IOException if creating the user branch fails due to an IO error
-   * @throws OrmException if creating the user branch fails
    * @throws ConfigInvalidException if any of the account fields has an invalid value
    */
   public AccountState insert(String message, Account.Id accountId, AccountUpdater updater)
-      throws OrmException, IOException, ConfigInvalidException {
+      throws IOException, ConfigInvalidException {
     return updateAccount(
             r -> {
               AccountConfig accountConfig = read(r, accountId);
@@ -318,12 +349,11 @@
    * @throws IOException if updating the user branch fails due to an IO error
    * @throws LockFailureException if updating the user branch still fails due to concurrent updates
    *     after the retry timeout exceeded
-   * @throws OrmException if updating the user branch fails
    * @throws ConfigInvalidException if any of the account fields has an invalid value
    */
   public Optional<AccountState> update(
       String message, Account.Id accountId, Consumer<InternalAccountUpdate.Builder> update)
-      throws OrmException, LockFailureException, IOException, ConfigInvalidException {
+      throws LockFailureException, IOException, ConfigInvalidException {
     return update(message, accountId, AccountUpdater.fromConsumer(update));
   }
 
@@ -339,11 +369,10 @@
    * @throws IOException if updating the user branch fails due to an IO error
    * @throws LockFailureException if updating the user branch still fails due to concurrent updates
    *     after the retry timeout exceeded
-   * @throws OrmException if updating the user branch fails
    * @throws ConfigInvalidException if any of the account fields has an invalid value
    */
   public Optional<AccountState> update(String message, Account.Id accountId, AccountUpdater updater)
-      throws OrmException, LockFailureException, IOException, ConfigInvalidException {
+      throws LockFailureException, IOException, ConfigInvalidException {
     return updateAccount(
         r -> {
           AccountConfig accountConfig = read(r, accountId);
@@ -374,7 +403,7 @@
   }
 
   private Optional<AccountState> updateAccount(AccountUpdate accountUpdate)
-      throws IOException, ConfigInvalidException, OrmException {
+      throws IOException, ConfigInvalidException {
     return executeAccountUpdate(
         () -> {
           try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
@@ -390,7 +419,7 @@
   }
 
   private Optional<AccountState> executeAccountUpdate(Action<Optional<AccountState>> action)
-      throws IOException, ConfigInvalidException, OrmException {
+      throws IOException, ConfigInvalidException {
     try {
       return retryHelper.execute(
           ActionType.ACCOUNT_UPDATE, action, LockFailureException.class::isInstance);
@@ -398,8 +427,7 @@
       Throwables.throwIfUnchecked(e);
       Throwables.throwIfInstanceOf(e, IOException.class);
       Throwables.throwIfInstanceOf(e, ConfigInvalidException.class);
-      Throwables.throwIfInstanceOf(e, OrmException.class);
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
   }
 
@@ -408,7 +436,7 @@
       Optional<ObjectId> rev,
       Account.Id accountId,
       InternalAccountUpdate update)
-      throws IOException, ConfigInvalidException, OrmDuplicateKeyException {
+      throws IOException, ConfigInvalidException, DuplicateKeyException {
     ExternalIdNotes.checkSameAccount(
         Iterables.concat(
             update.getCreatedExternalIds(),
@@ -458,7 +486,7 @@
         .updateCaches(accountsThatWillBeReindexByReindexAfterRefUpdate);
 
     gitRefUpdated.fire(
-        allUsersName, batchRefUpdate, currentUser != null ? currentUser.state() : null);
+        allUsersName, batchRefUpdate, currentUser.map(user -> user.state()).orElse(null));
   }
 
   private static Set<Account.Id> getUpdatedAccounts(BatchRefUpdate batchRefUpdate) {
@@ -529,8 +557,7 @@
 
   @FunctionalInterface
   private static interface AccountUpdate {
-    UpdatedAccount update(Repository allUsersRepo)
-        throws IOException, ConfigInvalidException, OrmException;
+    UpdatedAccount update(Repository allUsersRepo) throws IOException, ConfigInvalidException;
   }
 
   private static class UpdatedAccount {
diff --git a/java/com/google/gerrit/server/account/CapabilityCollection.java b/java/com/google/gerrit/server/account/CapabilityCollection.java
index 1abc33f..b52d616 100644
--- a/java/com/google/gerrit/server/account/CapabilityCollection.java
+++ b/java/com/google/gerrit/server/account/CapabilityCollection.java
@@ -81,7 +81,7 @@
     }
     configureDefaults(tmp, section);
     if (!tmp.containsKey(GlobalCapability.ADMINISTRATE_SERVER) && !admins.isEmpty()) {
-      tmp.put(GlobalCapability.ADMINISTRATE_SERVER, ImmutableList.<PermissionRule>of());
+      tmp.put(GlobalCapability.ADMINISTRATE_SERVER, ImmutableList.of());
     }
 
     ImmutableMap.Builder<String, ImmutableList<PermissionRule>> m = ImmutableMap.builder();
@@ -123,7 +123,7 @@
 
   public ImmutableList<PermissionRule> getPermission(String permissionName) {
     ImmutableList<PermissionRule> r = permissions.get(permissionName);
-    return r != null ? r : ImmutableList.<PermissionRule>of();
+    return r != null ? r : ImmutableList.of();
   }
 
   private void configureDefaults(Map<String, List<PermissionRule>> out, AccessSection section) {
diff --git a/java/com/google/gerrit/server/account/DefaultRealm.java b/java/com/google/gerrit/server/account/DefaultRealm.java
index dde6e81..33de2d2 100644
--- a/java/com/google/gerrit/server/account/DefaultRealm.java
+++ b/java/com/google/gerrit/server/account/DefaultRealm.java
@@ -16,11 +16,11 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
+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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -87,7 +87,7 @@
         if (1 == c.size()) {
           return c.iterator().next();
         }
-      } catch (OrmException e) {
+      } catch (StorageException e) {
         throw new IOException("Failed to query accounts by email", e);
       }
     }
diff --git a/java/com/google/gerrit/server/account/Emails.java b/java/com/google/gerrit/server/account/Emails.java
index 7c798ca..426d6ea 100644
--- a/java/com/google/gerrit/server/account/Emails.java
+++ b/java/com/google/gerrit/server/account/Emails.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSetMultimap;
 import com.google.common.collect.Streams;
+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;
@@ -27,7 +28,6 @@
 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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -69,7 +69,7 @@
    *
    * @see #getAccountsFor(String...)
    */
-  public ImmutableSet<Account.Id> getAccountFor(String email) throws IOException, OrmException {
+  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())
@@ -83,7 +83,7 @@
    * @see #getAccountFor(String)
    */
   public ImmutableSetMultimap<String, Account.Id> getAccountsFor(String... emails)
-      throws IOException, OrmException {
+      throws IOException {
     ImmutableSetMultimap.Builder<String, Account.Id> builder = ImmutableSetMultimap.builder();
     externalIds.byEmails(emails).entries().stream()
         .forEach(e -> builder.put(e.getKey(), e.getValue().accountId()));
@@ -102,13 +102,13 @@
     return externalIds.byEmail(email).stream().map(ExternalId::accountId).collect(toImmutableSet());
   }
 
-  private <T> T executeIndexQuery(Action<T> action) throws OrmException {
+  private <T> T executeIndexQuery(Action<T> action) {
     try {
-      return retryHelper.execute(ActionType.INDEX_QUERY, action, OrmException.class::isInstance);
+      return retryHelper.execute(
+          ActionType.INDEX_QUERY, action, StorageException.class::isInstance);
     } catch (Exception e) {
       Throwables.throwIfUnchecked(e);
-      Throwables.throwIfInstanceOf(e, OrmException.class);
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
   }
 }
diff --git a/java/com/google/gerrit/server/account/GroupControl.java b/java/com/google/gerrit/server/account/GroupControl.java
index 5649629..b3e6739 100644
--- a/java/com/google/gerrit/server/account/GroupControl.java
+++ b/java/com/google/gerrit/server/account/GroupControl.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.account;
 
 import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+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;
diff --git a/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java b/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
index a089d9e..c27d6c3 100644
--- a/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
+++ b/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
@@ -30,7 +30,6 @@
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.query.group.InternalGroupQuery;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.Provider;
@@ -152,7 +151,7 @@
     }
 
     @Override
-    public ImmutableSet<AccountGroup.UUID> load(Account.Id memberId) throws OrmException {
+    public ImmutableSet<AccountGroup.UUID> load(Account.Id memberId) {
       try (TraceTimer timer = TraceContext.newTimer("Loading groups with member %s", memberId)) {
         return groupQueryProvider.get().byMember(memberId).stream()
             .map(InternalGroup::getGroupUUID)
@@ -171,7 +170,7 @@
     }
 
     @Override
-    public ImmutableList<AccountGroup.UUID> load(AccountGroup.UUID key) throws OrmException {
+    public ImmutableList<AccountGroup.UUID> load(AccountGroup.UUID key) {
       try (TraceTimer timer = TraceContext.newTimer("Loading parent groups of %s", key)) {
         return groupQueryProvider.get().bySubgroup(key).stream()
             .map(InternalGroup::getGroupUUID)
diff --git a/java/com/google/gerrit/server/account/GroupMembers.java b/java/com/google/gerrit/server/account/GroupMembers.java
index 710f553..d7e97ba 100644
--- a/java/com/google/gerrit/server/account/GroupMembers.java
+++ b/java/com/google/gerrit/server/account/GroupMembers.java
@@ -67,7 +67,7 @@
       throw new IllegalStateException("listAccounts called with PROJECT_OWNERS argument");
     }
     try {
-      return listAccounts(groupUUID, null, new HashSet<AccountGroup.UUID>());
+      return listAccounts(groupUUID, null, new HashSet<>());
     } catch (NoSuchProjectException e) {
       throw new IllegalStateException(e);
     }
@@ -81,7 +81,7 @@
    */
   public Set<Account> listAccounts(AccountGroup.UUID groupUUID, Project.NameKey project)
       throws NoSuchProjectException, IOException {
-    return listAccounts(groupUUID, project, new HashSet<AccountGroup.UUID>());
+    return listAccounts(groupUUID, project, new HashSet<>());
   }
 
   private Set<Account> listAccounts(
diff --git a/java/com/google/gerrit/server/account/GroupMembership.java b/java/com/google/gerrit/server/account/GroupMembership.java
index cc73222..b59b989 100644
--- a/java/com/google/gerrit/server/account/GroupMembership.java
+++ b/java/com/google/gerrit/server/account/GroupMembership.java
@@ -24,7 +24,7 @@
  * <p>Different accounts systems (eg. LDAP, gerrit groups) provide concrete implementations.
  */
 public interface GroupMembership {
-  GroupMembership EMPTY = new ListGroupMembership(Collections.<AccountGroup.UUID>emptySet());
+  GroupMembership EMPTY = new ListGroupMembership(Collections.emptySet());
 
   /**
    * Returns {@code true} when the user this object was created for is a member of the specified
diff --git a/java/com/google/gerrit/server/account/Preferences.java b/java/com/google/gerrit/server/account/Preferences.java
index 6767b85..cd849b8 100644
--- a/java/com/google/gerrit/server/account/Preferences.java
+++ b/java/com/google/gerrit/server/account/Preferences.java
@@ -21,11 +21,8 @@
 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_MATCH;
 import static com.google.gerrit.server.git.UserConfigSections.KEY_TARGET;
-import static com.google.gerrit.server.git.UserConfigSections.KEY_TOKEN;
 import static com.google.gerrit.server.git.UserConfigSections.KEY_URL;
-import static com.google.gerrit.server.git.UserConfigSections.URL_ALIAS;
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.base.Strings;
@@ -47,10 +44,7 @@
 import java.io.IOException;
 import java.lang.reflect.Field;
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
 import java.util.Optional;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.CommitBuilder;
@@ -154,7 +148,6 @@
           parseDefaultGeneralPreferences(defaultCfg, null));
       setChangeTable(cfg, mergedGeneralPreferencesInput.changeTable);
       setMy(cfg, mergedGeneralPreferencesInput.my);
-      setUrlAliases(cfg, mergedGeneralPreferencesInput.urlAliases);
 
       // evict the cached general preferences
       this.generalPreferences = null;
@@ -249,11 +242,9 @@
     if (input != null) {
       r.changeTable = input.changeTable;
       r.my = input.my;
-      r.urlAliases = input.urlAliases;
     } else {
       r.changeTable = parseChangeTableColumns(cfg, defaultCfg);
       r.my = parseMyMenus(cfg, defaultCfg);
-      r.urlAliases = parseUrlAliases(cfg, defaultCfg);
     }
     return r;
   }
@@ -407,14 +398,6 @@
     return my;
   }
 
-  private static Map<String, String> parseUrlAliases(Config cfg, @Nullable Config defaultCfg) {
-    Map<String, String> urlAliases = urlAliases(cfg);
-    if (urlAliases == null && defaultCfg != null) {
-      urlAliases = urlAliases(defaultCfg);
-    }
-    return urlAliases;
-  }
-
   public static GeneralPreferencesInfo readDefaultGeneralPreferences(
       AllUsersName allUsersName, Repository allUsersRepo)
       throws IOException, ConfigInvalidException {
@@ -452,7 +435,6 @@
         GeneralPreferencesInfo.defaults());
     setMy(defaultPrefs.getConfig(), input.my);
     setChangeTable(defaultPrefs.getConfig(), input.changeTable);
-    setUrlAliases(defaultPrefs.getConfig(), input.urlAliases);
     defaultPrefs.commit(md);
 
     return parseGeneralPreferences(defaultPrefs.getConfig(), null, null);
@@ -557,31 +539,6 @@
     }
   }
 
-  private static Map<String, String> urlAliases(Config cfg) {
-    HashMap<String, String> urlAliases = new HashMap<>();
-    for (String subsection : cfg.getSubsections(URL_ALIAS)) {
-      urlAliases.put(
-          cfg.getString(URL_ALIAS, subsection, KEY_MATCH),
-          cfg.getString(URL_ALIAS, subsection, KEY_TOKEN));
-    }
-    return !urlAliases.isEmpty() ? urlAliases : null;
-  }
-
-  private static void setUrlAliases(Config cfg, Map<String, String> urlAliases) {
-    if (urlAliases != null) {
-      for (String subsection : cfg.getSubsections(URL_ALIAS)) {
-        cfg.unsetSection(URL_ALIAS, subsection);
-      }
-
-      int i = 1;
-      for (Entry<String, String> e : urlAliases.entrySet()) {
-        cfg.setString(URL_ALIAS, URL_ALIAS + i, KEY_MATCH, e.getKey());
-        cfg.setString(URL_ALIAS, URL_ALIAS + i, KEY_TOKEN, e.getValue());
-        i++;
-      }
-    }
-  }
-
   private static void unsetSection(Config cfg, String section) {
     cfg.unsetSection(section, null);
     for (String subsection : cfg.getSubsections(section)) {
diff --git a/java/com/google/gerrit/server/account/SetInactiveFlag.java b/java/com/google/gerrit/server/account/SetInactiveFlag.java
index a683849..da2d640 100644
--- a/java/com/google/gerrit/server/account/SetInactiveFlag.java
+++ b/java/com/google/gerrit/server/account/SetInactiveFlag.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.gerrit.server.validators.AccountActivationValidationListener;
 import com.google.gerrit.server.validators.ValidationException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -48,7 +47,7 @@
   }
 
   public Response<?> deactivate(Account.Id accountId)
-      throws RestApiException, IOException, ConfigInvalidException, OrmException {
+      throws RestApiException, IOException, ConfigInvalidException {
     AtomicBoolean alreadyInactive = new AtomicBoolean(false);
     AtomicReference<Optional<RestApiException>> exception = new AtomicReference<>(Optional.empty());
     accountsUpdateProvider
@@ -81,7 +80,7 @@
   }
 
   public Response<String> activate(Account.Id accountId)
-      throws RestApiException, IOException, ConfigInvalidException, OrmException {
+      throws RestApiException, IOException, ConfigInvalidException {
     AtomicBoolean alreadyActive = new AtomicBoolean(false);
     AtomicReference<Optional<RestApiException>> exception = new AtomicReference<>(Optional.empty());
     accountsUpdateProvider
diff --git a/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java b/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
index 965f1ba..c7808de 100644
--- a/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
+++ b/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
@@ -20,7 +20,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.Ordering;
-import com.google.gerrit.common.errors.InvalidSshKeyException;
+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;
diff --git a/java/com/google/gerrit/server/account/externalids/AllExternalIds.java b/java/com/google/gerrit/server/account/externalids/AllExternalIds.java
index 8b23f6c..5d12ae1 100644
--- a/java/com/google/gerrit/server/account/externalids/AllExternalIds.java
+++ b/java/com/google/gerrit/server/account/externalids/AllExternalIds.java
@@ -21,12 +21,12 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSetMultimap;
 import com.google.common.collect.SetMultimap;
+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;
-import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers;
-import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers.ObjectIdConverter;
+import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
 import java.util.Collection;
 
 /** Cache value containing all external IDs. */
@@ -39,7 +39,7 @@
 
   static AllExternalIds create(Collection<ExternalId> externalIds) {
     return new AutoValue_AllExternalIds(
-        externalIds.stream().collect(toImmutableSetMultimap(e -> e.accountId(), e -> e)),
+        externalIds.stream().collect(toImmutableSetMultimap(ExternalId::accountId, e -> e)),
         byEmailCopy(externalIds));
   }
 
@@ -47,7 +47,7 @@
       Collection<ExternalId> externalIds) {
     return externalIds.stream()
         .filter(e -> !Strings.isNullOrEmpty(e.email()))
-        .collect(toImmutableSetMultimap(e -> e.email(), e -> e));
+        .collect(toImmutableSetMultimap(ExternalId::email, e -> e));
   }
 
   public abstract ImmutableSetMultimap<Account.Id, ExternalId> byAccount();
@@ -64,7 +64,7 @@
       object.byAccount().values().stream()
           .map(extId -> toProto(idConverter, extId))
           .forEach(allBuilder::addExternalId);
-      return ProtoCacheSerializers.toByteArray(allBuilder.build());
+      return Protos.toByteArray(allBuilder.build());
     }
 
     private static ExternalIdProto toProto(ObjectIdConverter idConverter, ExternalId externalId) {
@@ -88,8 +88,7 @@
     public AllExternalIds deserialize(byte[] in) {
       ObjectIdConverter idConverter = ObjectIdConverter.create();
       return create(
-          ProtoCacheSerializers.parseUnchecked(AllExternalIdsProto.parser(), in).getExternalIdList()
-              .stream()
+          Protos.parseUnchecked(AllExternalIdsProto.parser(), in).getExternalIdList().stream()
               .map(proto -> toExternalId(idConverter, proto))
               .collect(toList()));
     }
diff --git a/java/com/google/gerrit/server/account/externalids/DuplicateExternalIdKeyException.java b/java/com/google/gerrit/server/account/externalids/DuplicateExternalIdKeyException.java
index b4c82d0..aa09278 100644
--- a/java/com/google/gerrit/server/account/externalids/DuplicateExternalIdKeyException.java
+++ b/java/com/google/gerrit/server/account/externalids/DuplicateExternalIdKeyException.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.server.account.externalids;
 
-import com.google.gwtorm.server.OrmDuplicateKeyException;
+import com.google.gerrit.exceptions.DuplicateKeyException;
 
 /**
  * Exception that is thrown if an external ID cannot be inserted because an external ID with the
  * same key already exists.
  */
-public class DuplicateExternalIdKeyException extends OrmDuplicateKeyException {
+public class DuplicateExternalIdKeyException extends DuplicateKeyException {
   private static final long serialVersionUID = 1L;
 
   private final ExternalId.Key duplicateKey;
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java b/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
index 767bfd5..5aa19d8 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
@@ -122,6 +122,11 @@
       ObjectId oldNotesRev,
       ObjectId newNotesRev,
       Consumer<SetMultimap<Account.Id, ExternalId>> update) {
+    if (oldNotesRev.equals(newNotesRev)) {
+      // No need to update external id cache since there is no update to those external ids.
+      return;
+    }
+
     lock.lock();
     try {
       SetMultimap<Account.Id, ExternalId> m;
diff --git a/java/com/google/gerrit/server/api/BUILD b/java/com/google/gerrit/server/api/BUILD
index c4d735c..459c16a 100644
--- a/java/com/google/gerrit/server/api/BUILD
+++ b/java/com/google/gerrit/server/api/BUILD
@@ -9,13 +9,15 @@
     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/lifecycle",
         "//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:gwtorm",
         "//lib:servlet-api-3_1",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
diff --git a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index db023b6..f027c92 100644
--- a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -16,7 +16,10 @@
 
 import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
 
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ListMultimap;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.AbandonInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerResult;
@@ -44,18 +47,19 @@
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.common.CommitMessageInput;
-import com.google.gerrit.extensions.common.EditInfo;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.common.MergePatchSetInput;
 import com.google.gerrit.extensions.common.PureRevertInfo;
 import com.google.gerrit.extensions.common.RobotCommentInfo;
 import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 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.server.DynamicOptions;
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.StarredChangesUtil.IllegalLabelException;
-import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.ChangeMessageResource;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.SetPrivateOp;
@@ -69,6 +73,7 @@
 import com.google.gerrit.server.restapi.change.DeleteChange;
 import com.google.gerrit.server.restapi.change.DeletePrivate;
 import com.google.gerrit.server.restapi.change.GetAssignee;
+import com.google.gerrit.server.restapi.change.GetChange;
 import com.google.gerrit.server.restapi.change.GetHashtags;
 import com.google.gerrit.server.restapi.change.GetPastAssignees;
 import com.google.gerrit.server.restapi.change.GetPureRevert;
@@ -98,14 +103,17 @@
 import com.google.gerrit.server.restapi.change.SubmittedTogether;
 import com.google.gerrit.server.restapi.change.SuggestChangeReviewers;
 import com.google.gerrit.server.restapi.change.Unignore;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.util.cli.CmdLineParser;
 import com.google.inject.Inject;
+import com.google.inject.Injector;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 import com.google.inject.assistedinject.Assisted;
 import java.util.EnumSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import org.kohsuke.args4j.CmdLineException;
 
 class ChangeApiImpl implements ChangeApi {
   interface Factory {
@@ -133,7 +141,7 @@
   private final PutTopic putTopic;
   private final ChangeIncludedIn includedIn;
   private final PostReviewers postReviewers;
-  private final ChangeJson.Factory changeJson;
+  private final Provider<GetChange> getChangeProvider;
   private final PostHashtags postHashtags;
   private final GetHashtags getHashtags;
   private final PutAssignee putAssignee;
@@ -158,6 +166,7 @@
   private final PutMessage putMessage;
   private final Provider<GetPureRevert> getPureRevertProvider;
   private final StarredChangesUtil stars;
+  private final DynamicOptionParser dynamicOptionParser;
 
   @Inject
   ChangeApiImpl(
@@ -181,7 +190,7 @@
       PutTopic putTopic,
       ChangeIncludedIn includedIn,
       PostReviewers postReviewers,
-      ChangeJson.Factory changeJson,
+      Provider<GetChange> getChangeProvider,
       PostHashtags postHashtags,
       GetHashtags getHashtags,
       PutAssignee putAssignee,
@@ -206,6 +215,7 @@
       PutMessage putMessage,
       Provider<GetPureRevert> getPureRevertProvider,
       StarredChangesUtil stars,
+      DynamicOptionParser dynamicOptionParser,
       @Assisted ChangeResource change) {
     this.changeApi = changeApi;
     this.revert = revert;
@@ -227,7 +237,7 @@
     this.putTopic = putTopic;
     this.includedIn = includedIn;
     this.postReviewers = postReviewers;
-    this.changeJson = changeJson;
+    this.getChangeProvider = getChangeProvider;
     this.postHashtags = postHashtags;
     this.getHashtags = getHashtags;
     this.putAssignee = putAssignee;
@@ -252,6 +262,7 @@
     this.putMessage = putMessage;
     this.getPureRevertProvider = getPureRevertProvider;
     this.stars = stars;
+    this.dynamicOptionParser = dynamicOptionParser;
     this.change = change;
   }
 
@@ -261,16 +272,6 @@
   }
 
   @Override
-  public RevisionApi current() throws RestApiException {
-    return revision("current");
-  }
-
-  @Override
-  public RevisionApi revision(int id) throws RestApiException {
-    return revision(String.valueOf(id));
-  }
-
-  @Override
   public RevisionApi revision(String id) throws RestApiException {
     try {
       return revisionApi.create(revisions.parse(change, IdString.fromDecoded(id)));
@@ -289,11 +290,6 @@
   }
 
   @Override
-  public void abandon() throws RestApiException {
-    abandon(new AbandonInput());
-  }
-
-  @Override
   public void abandon(AbandonInput in) throws RestApiException {
     try {
       abandon.apply(change, in);
@@ -303,11 +299,6 @@
   }
 
   @Override
-  public void restore() throws RestApiException {
-    restore(new RestoreInput());
-  }
-
-  @Override
   public void restore(RestoreInput in) throws RestApiException {
     try {
       restore.apply(change, in);
@@ -317,13 +308,6 @@
   }
 
   @Override
-  public void move(String destination) throws RestApiException {
-    MoveInput in = new MoveInput();
-    in.destinationBranch = destination;
-    move(in);
-  }
-
-  @Override
   public void move(MoveInput in) throws RestApiException {
     try {
       move.apply(change, in);
@@ -365,11 +349,6 @@
   }
 
   @Override
-  public ChangeApi revert() throws RestApiException {
-    return revert(new RevertInput());
-  }
-
-  @Override
   public ChangeApi revert(RevertInput in) throws RestApiException {
     try {
       return changeApi.id(revert.apply(change, in)._number);
@@ -388,20 +367,6 @@
   }
 
   @Override
-  public List<ChangeInfo> submittedTogether() throws RestApiException {
-    SubmittedTogetherInfo info =
-        submittedTogether(
-            EnumSet.noneOf(ListChangesOption.class), EnumSet.noneOf(SubmittedTogetherOption.class));
-    return info.changes;
-  }
-
-  @Override
-  public SubmittedTogetherInfo submittedTogether(EnumSet<SubmittedTogetherOption> options)
-      throws RestApiException {
-    return submittedTogether(EnumSet.noneOf(ListChangesOption.class), options);
-  }
-
-  @Override
   public SubmittedTogetherInfo submittedTogether(
       EnumSet<ListChangesOption> listOptions, EnumSet<SubmittedTogetherOption> submitOptions)
       throws RestApiException {
@@ -416,17 +381,6 @@
     }
   }
 
-  @Deprecated
-  @Override
-  public void publish() throws RestApiException {
-    throw new UnsupportedOperationException("draft workflow is discontinued");
-  }
-
-  @Override
-  public void rebase() throws RestApiException {
-    rebase(new RebaseInput());
-  }
-
   @Override
   public void rebase(RebaseInput in) throws RestApiException {
     try {
@@ -471,13 +425,6 @@
   }
 
   @Override
-  public AddReviewerResult addReviewer(String reviewer) throws RestApiException {
-    AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = reviewer;
-    return addReviewer(in);
-  }
-
-  @Override
   public AddReviewerResult addReviewer(AddReviewerInput in) throws RestApiException {
     try {
       return postReviewers.apply(change, in);
@@ -496,11 +443,6 @@
     };
   }
 
-  @Override
-  public SuggestedReviewersRequest suggestReviewers(String query) throws RestApiException {
-    return suggestReviewers().withQuery(query);
-  }
-
   private List<SuggestedReviewerInfo> suggestReviewers(SuggestedReviewersRequest r)
       throws RestApiException {
     try {
@@ -522,39 +464,25 @@
   }
 
   @Override
-  public ChangeInfo get(EnumSet<ListChangesOption> s) throws RestApiException {
+  public ChangeInfo get(
+      EnumSet<ListChangesOption> options, ImmutableListMultimap<String, String> pluginOptions)
+      throws RestApiException {
     try {
-      return changeJson.create(s).format(change);
+      GetChange getChange = getChangeProvider.get();
+      options.forEach(getChange::addOption);
+      dynamicOptionParser.parseDynamicOptions(getChange, pluginOptions);
+      return getChange.apply(change).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot retrieve change", e);
     }
   }
 
   @Override
-  public ChangeInfo get() throws RestApiException {
-    return get(
-        EnumSet.complementOf(
-            EnumSet.of(ListChangesOption.CHECK, ListChangesOption.SKIP_MERGEABLE)));
-  }
-
-  @Override
-  public EditInfo getEdit() throws RestApiException {
-    return edit().get().orElse(null);
-  }
-
-  @Override
   public ChangeEditApi edit() throws RestApiException {
     return changeEditApi.create(change);
   }
 
   @Override
-  public void setMessage(String msg) throws RestApiException {
-    CommitMessageInput in = new CommitMessageInput();
-    in.message = msg;
-    setMessage(in);
-  }
-
-  @Override
   public void setMessage(CommitMessageInput in) throws RestApiException {
     try {
       putMessage.apply(change, in);
@@ -564,11 +492,6 @@
   }
 
   @Override
-  public ChangeInfo info() throws RestApiException {
-    return get(EnumSet.noneOf(ListChangesOption.class));
-  }
-
-  @Override
   public void setHashtags(HashtagsInput input) throws RestApiException {
     try {
       postHashtags.apply(change, input);
@@ -708,7 +631,7 @@
       } else {
         unignore.apply(change, new Input());
       }
-    } catch (OrmException | IllegalLabelException e) {
+    } catch (StorageException | IllegalLabelException e) {
       throw asRestApiException("Cannot ignore change", e);
     }
   }
@@ -717,7 +640,7 @@
   public boolean ignored() throws RestApiException {
     try {
       return stars.isIgnored(change);
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       throw asRestApiException("Cannot check if ignored", e);
     }
   }
@@ -732,7 +655,7 @@
       } else {
         markAsUnreviewed.apply(change, new Input());
       }
-    } catch (OrmException | IllegalLabelException e) {
+    } catch (StorageException | IllegalLabelException e) {
       throw asRestApiException(
           "Cannot mark change as " + (reviewed ? "reviewed" : "unreviewed"), e);
     }
@@ -772,4 +695,36 @@
       throw asRestApiException("Cannot parse change message " + id, e);
     }
   }
+
+  @Singleton
+  static class DynamicOptionParser {
+    private final CmdLineParser.Factory cmdLineParserFactory;
+    private final Injector injector;
+    private final DynamicMap<DynamicOptions.DynamicBean> dynamicBeans;
+
+    @Inject
+    DynamicOptionParser(
+        CmdLineParser.Factory cmdLineParserFactory,
+        Injector injector,
+        DynamicMap<DynamicOptions.DynamicBean> dynamicBeans) {
+      this.cmdLineParserFactory = cmdLineParserFactory;
+      this.injector = injector;
+      this.dynamicBeans = dynamicBeans;
+    }
+
+    void parseDynamicOptions(Object bean, ListMultimap<String, String> pluginOptions)
+        throws BadRequestException {
+      CmdLineParser clp = cmdLineParserFactory.create(bean);
+      DynamicOptions dynamicOptions = new DynamicOptions(bean, injector, dynamicBeans);
+      dynamicOptions.parseDynamicBeans(clp);
+      dynamicOptions.setDynamicBeans();
+      dynamicOptions.onBeanParseStart();
+      try {
+        clp.parseOptionMap(pluginOptions);
+      } catch (CmdLineException | NumberFormatException e) {
+        throw new BadRequestException(e.getMessage(), e);
+      }
+      dynamicOptions.onBeanParseEnd();
+    }
+  }
 }
diff --git a/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java b/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java
index 5aa4cf1..ffc6524 100644
--- a/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java
@@ -34,7 +34,6 @@
 import com.google.gerrit.server.restapi.change.DeleteChangeEdit;
 import com.google.gerrit.server.restapi.change.PublishChangeEdit;
 import com.google.gerrit.server.restapi.change.RebaseChangeEdit;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
@@ -242,7 +241,7 @@
   }
 
   private ChangeEditResource getChangeEditResource(String filePath)
-      throws ResourceNotFoundException, AuthException, IOException, OrmException {
+      throws ResourceNotFoundException, AuthException, IOException {
     return changeEdits.parse(changeResource, IdString.fromDecoded(filePath));
   }
 }
diff --git a/java/com/google/gerrit/server/api/changes/ChangesImpl.java b/java/com/google/gerrit/server/api/changes/ChangesImpl.java
index 2f19040..acff137 100644
--- a/java/com/google/gerrit/server/api/changes/ChangesImpl.java
+++ b/java/com/google/gerrit/server/api/changes/ChangesImpl.java
@@ -30,6 +30,7 @@
 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;
 import com.google.gerrit.server.restapi.change.QueryChanges;
@@ -43,6 +44,7 @@
   private final ChangesCollection changes;
   private final ChangeApiImpl.Factory api;
   private final CreateChange createChange;
+  private final DynamicOptionParser dynamicOptionParser;
   private final Provider<QueryChanges> queryProvider;
 
   @Inject
@@ -50,10 +52,12 @@
       ChangesCollection changes,
       ChangeApiImpl.Factory api,
       CreateChange createChange,
+      DynamicOptionParser dynamicOptionParser,
       Provider<QueryChanges> queryProvider) {
     this.changes = changes;
     this.api = api;
     this.createChange = createChange;
+    this.dynamicOptionParser = dynamicOptionParser;
     this.queryProvider = queryProvider;
   }
 
@@ -116,9 +120,11 @@
     }
     qc.setLimit(q.getLimit());
     qc.setStart(q.getStart());
+    qc.setNoLimit(q.getNoLimit());
     for (ListChangesOption option : q.getOptions()) {
       qc.addOption(option);
     }
+    dynamicOptionParser.parseDynamicOptions(qc, q.getPluginOptions());
 
     try {
       List<?> result = qc.apply(TopLevelResource.INSTANCE);
diff --git a/java/com/google/gerrit/server/api/changes/FileApiImpl.java b/java/com/google/gerrit/server/api/changes/FileApiImpl.java
index f2d0ef8..883ab99 100644
--- a/java/com/google/gerrit/server/api/changes/FileApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/FileApiImpl.java
@@ -17,16 +17,20 @@
 import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
 
 import com.google.gerrit.extensions.api.changes.FileApi;
+import com.google.gerrit.extensions.common.BlameInfo;
 import com.google.gerrit.extensions.common.DiffInfo;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.change.FileResource;
+import com.google.gerrit.server.restapi.change.GetBlame;
 import com.google.gerrit.server.restapi.change.GetContent;
 import com.google.gerrit.server.restapi.change.GetDiff;
 import com.google.gerrit.server.restapi.change.Reviewed;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
+import java.util.List;
 
 class FileApiImpl implements FileApi {
   interface Factory {
@@ -34,6 +38,7 @@
   }
 
   private final GetContent getContent;
+  private final Provider<GetBlame> getBlame;
   private final GetDiff getDiff;
   private final Reviewed.PutReviewed putReviewed;
   private final Reviewed.DeleteReviewed deleteReviewed;
@@ -42,11 +47,13 @@
   @Inject
   FileApiImpl(
       GetContent getContent,
+      Provider<GetBlame> getBlame,
       GetDiff getDiff,
       Reviewed.PutReviewed putReviewed,
       Reviewed.DeleteReviewed deleteReviewed,
       @Assisted FileResource file) {
     this.getContent = getContent;
+    this.getBlame = getBlame;
     this.getDiff = getDiff;
     this.putReviewed = putReviewed;
     this.deleteReviewed = deleteReviewed;
@@ -132,4 +139,18 @@
       throw asRestApiException("Cannot retrieve diff", e);
     }
   }
+
+  @Override
+  public BlameRequest blameRequest() throws RestApiException {
+    return new BlameRequest() {
+      @Override
+      public List<BlameInfo> get() throws RestApiException {
+        try {
+          return getBlame.get().setBase(isForBase()).apply(file).value();
+        } catch (Exception e) {
+          throw asRestApiException("Cannot retrieve blame", e);
+        }
+      }
+    };
+  }
 }
diff --git a/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
index dff29b0..2df7ae6 100644
--- a/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.MultimapBuilder.ListMultimapBuilder;
+import com.google.gerrit.common.Nullable;
 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.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.account.AccountDirectory.FillOptions;
 import com.google.gerrit.server.account.AccountLoader;
@@ -144,7 +144,6 @@
   private final PutDescription putDescription;
   private final GetDescription getDescription;
   private final ApprovalsUtil approvalsUtil;
-  private final Provider<ReviewDb> db;
   private final AccountLoader.Factory accountLoaderFactory;
 
   @Inject
@@ -188,7 +187,6 @@
       PutDescription putDescription,
       GetDescription getDescription,
       ApprovalsUtil approvalsUtil,
-      Provider<ReviewDb> db,
       AccountLoader.Factory accountLoaderFactory,
       @Assisted RevisionResource r) {
     this.repoManager = repoManager;
@@ -230,7 +228,6 @@
     this.putDescription = putDescription;
     this.getDescription = getDescription;
     this.approvalsUtil = approvalsUtil;
-    this.db = db;
     this.accountLoaderFactory = accountLoaderFactory;
     this.revision = r;
   }
@@ -245,12 +242,6 @@
   }
 
   @Override
-  public void submit() throws RestApiException {
-    SubmitInput in = new SubmitInput();
-    submit(in);
-  }
-
-  @Override
   public void submit(SubmitInput in) throws RestApiException {
     try {
       submit.apply(revision, in);
@@ -260,11 +251,6 @@
   }
 
   @Override
-  public BinaryResult submitPreview() throws RestApiException {
-    return submitPreview("zip");
-  }
-
-  @Override
   public BinaryResult submitPreview(String format) throws RestApiException {
     try {
       submitPreview.setFormat(format);
@@ -275,22 +261,6 @@
   }
 
   @Override
-  public void publish() throws RestApiException {
-    throw new UnsupportedOperationException("draft workflow is discontinued");
-  }
-
-  @Override
-  public void delete() throws RestApiException {
-    throw new UnsupportedOperationException("draft workflow is discontinued");
-  }
-
-  @Override
-  public ChangeApi rebase() throws RestApiException {
-    RebaseInput in = new RebaseInput();
-    return rebase(in);
-  }
-
-  @Override
   public ChangeApi rebase(RebaseInput in) throws RestApiException {
     try {
       return changes.id(rebase.apply(revision, in)._number);
@@ -384,17 +354,7 @@
 
   @SuppressWarnings("unchecked")
   @Override
-  public Map<String, FileInfo> files() throws RestApiException {
-    try {
-      return (Map<String, FileInfo>) listFiles.apply(revision).value();
-    } catch (Exception e) {
-      throw asRestApiException("Cannot retrieve files", e);
-    }
-  }
-
-  @SuppressWarnings("unchecked")
-  @Override
-  public Map<String, FileInfo> files(String base) throws RestApiException {
+  public Map<String, FileInfo> files(@Nullable String base) throws RestApiException {
     try {
       return (Map<String, FileInfo>) listFiles.setBase(base).apply(revision).value();
     } catch (Exception e) {
@@ -627,8 +587,7 @@
         ListMultimapBuilder.treeKeys().arrayListValues().build();
     try {
       Iterable<PatchSetApproval> approvals =
-          approvalsUtil.byPatchSet(
-              db.get(), revision.getNotes(), revision.getPatchSet().getId(), null, null);
+          approvalsUtil.byPatchSet(revision.getNotes(), revision.getPatchSet().getId(), null, null);
       AccountLoader accountLoader =
           accountLoaderFactory.create(
               EnumSet.of(
diff --git a/java/com/google/gerrit/server/api/config/ServerImpl.java b/java/com/google/gerrit/server/api/config/ServerImpl.java
index ec08507..6e78be2 100644
--- a/java/com/google/gerrit/server/api/config/ServerImpl.java
+++ b/java/com/google/gerrit/server/api/config/ServerImpl.java
@@ -25,18 +25,21 @@
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.common.ServerInfo;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.webui.TopMenu;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.restapi.config.CheckConsistency;
 import com.google.gerrit.server.restapi.config.GetDiffPreferences;
 import com.google.gerrit.server.restapi.config.GetEditPreferences;
 import com.google.gerrit.server.restapi.config.GetPreferences;
 import com.google.gerrit.server.restapi.config.GetServerInfo;
+import com.google.gerrit.server.restapi.config.ListTopMenus;
 import com.google.gerrit.server.restapi.config.SetDiffPreferences;
 import com.google.gerrit.server.restapi.config.SetEditPreferences;
 import com.google.gerrit.server.restapi.config.SetPreferences;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import java.util.List;
 
 @Singleton
 public class ServerImpl implements Server {
@@ -48,6 +51,7 @@
   private final SetEditPreferences setEditPreferences;
   private final GetServerInfo getServerInfo;
   private final Provider<CheckConsistency> checkConsistency;
+  private final ListTopMenus listTopMenus;
 
   @Inject
   ServerImpl(
@@ -58,7 +62,8 @@
       GetEditPreferences getEditPreferences,
       SetEditPreferences setEditPreferences,
       GetServerInfo getServerInfo,
-      Provider<CheckConsistency> checkConsistency) {
+      Provider<CheckConsistency> checkConsistency,
+      ListTopMenus listTopMenus) {
     this.getPreferences = getPreferences;
     this.setPreferences = setPreferences;
     this.getDiffPreferences = getDiffPreferences;
@@ -67,6 +72,7 @@
     this.setEditPreferences = setEditPreferences;
     this.getServerInfo = getServerInfo;
     this.checkConsistency = checkConsistency;
+    this.listTopMenus = listTopMenus;
   }
 
   @Override
@@ -148,4 +154,9 @@
       throw asRestApiException("Cannot check consistency", e);
     }
   }
+
+  @Override
+  public List<TopMenu.MenuEntry> topMenus() {
+    return listTopMenus.apply(new ConfigResource()).value();
+  }
 }
diff --git a/java/com/google/gerrit/server/api/groups/GroupApiImpl.java b/java/com/google/gerrit/server/api/groups/GroupApiImpl.java
index 9909ed7..b70a029 100644
--- a/java/com/google/gerrit/server/api/groups/GroupApiImpl.java
+++ b/java/com/google/gerrit/server/api/groups/GroupApiImpl.java
@@ -47,7 +47,6 @@
 import com.google.gerrit.server.restapi.group.PutOwner;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
-import java.util.Arrays;
 import java.util.List;
 
 class GroupApiImpl implements GroupApi {
@@ -217,18 +216,18 @@
   }
 
   @Override
-  public void addMembers(String... members) throws RestApiException {
+  public void addMembers(List<String> members) throws RestApiException {
     try {
-      addMembers.apply(rsrc, AddMembers.Input.fromMembers(Arrays.asList(members)));
+      addMembers.apply(rsrc, AddMembers.Input.fromMembers(members));
     } catch (Exception e) {
       throw asRestApiException("Cannot add group members", e);
     }
   }
 
   @Override
-  public void removeMembers(String... members) throws RestApiException {
+  public void removeMembers(List<String> members) throws RestApiException {
     try {
-      deleteMembers.apply(rsrc, AddMembers.Input.fromMembers(Arrays.asList(members)));
+      deleteMembers.apply(rsrc, AddMembers.Input.fromMembers(members));
     } catch (Exception e) {
       throw asRestApiException("Cannot remove group members", e);
     }
@@ -244,18 +243,18 @@
   }
 
   @Override
-  public void addGroups(String... groups) throws RestApiException {
+  public void addGroups(List<String> groups) throws RestApiException {
     try {
-      addSubgroups.apply(rsrc, AddSubgroups.Input.fromGroups(Arrays.asList(groups)));
+      addSubgroups.apply(rsrc, AddSubgroups.Input.fromGroups(groups));
     } catch (Exception e) {
       throw asRestApiException("Cannot add subgroups", e);
     }
   }
 
   @Override
-  public void removeGroups(String... groups) throws RestApiException {
+  public void removeGroups(List<String> groups) throws RestApiException {
     try {
-      deleteSubgroups.apply(rsrc, AddSubgroups.Input.fromGroups(Arrays.asList(groups)));
+      deleteSubgroups.apply(rsrc, AddSubgroups.Input.fromGroups(groups));
     } catch (Exception e) {
       throw asRestApiException("Cannot remove subgroups", e);
     }
diff --git a/java/com/google/gerrit/server/api/groups/GroupsImpl.java b/java/com/google/gerrit/server/api/groups/GroupsImpl.java
index e8d6cf4..bae75db 100644
--- a/java/com/google/gerrit/server/api/groups/GroupsImpl.java
+++ b/java/com/google/gerrit/server/api/groups/GroupsImpl.java
@@ -141,7 +141,7 @@
 
     if (req.getUser() != null) {
       try {
-        list.setUser(accountResolver.parse(req.getUser()).getAccountId());
+        list.setUser(accountResolver.resolve(req.getUser()).asUnique().getAccount().getId());
       } catch (Exception e) {
         throw asRestApiException("Error looking up user " + req.getUser(), e);
       }
diff --git a/java/com/google/gerrit/server/api/projects/ProjectsImpl.java b/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
index debb97c..5d25d1a 100644
--- a/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
+++ b/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
 
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.projects.ProjectApi;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
 import com.google.gerrit.extensions.api.projects.Projects;
@@ -28,7 +29,6 @@
 import com.google.gerrit.server.restapi.project.ListProjects.FilterType;
 import com.google.gerrit.server.restapi.project.ProjectsCollection;
 import com.google.gerrit.server.restapi.project.QueryProjects;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -117,9 +117,6 @@
       case CODE:
         type = FilterType.CODE;
         break;
-      case PARENT_CANDIDATES:
-        type = FilterType.PARENT_CANDIDATES;
-        break;
       case PERMISSIONS:
         type = FilterType.PERMISSIONS;
         break;
@@ -158,7 +155,7 @@
           .withLimit(r.getLimit())
           .withStart(r.getStart())
           .apply();
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       throw new RestApiException("Cannot query projects", e);
     }
   }
diff --git a/java/com/google/gerrit/server/args4j/AccountIdHandler.java b/java/com/google/gerrit/server/args4j/AccountIdHandler.java
index 512937b..37e52f4 100644
--- a/java/com/google/gerrit/server/args4j/AccountIdHandler.java
+++ b/java/com/google/gerrit/server/args4j/AccountIdHandler.java
@@ -17,7 +17,9 @@
 import static com.google.gerrit.util.cli.Localizable.localizable;
 
 import com.google.common.flogger.FluentLogger;
+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;
@@ -25,7 +27,6 @@
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.config.AuthConfig;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
@@ -63,10 +64,9 @@
     String token = params.getParameter(0);
     Account.Id accountId;
     try {
-      Account a = accountResolver.find(token);
-      if (a != null) {
-        accountId = a.getId();
-      } else {
+      try {
+        accountId = accountResolver.resolve(token).asUnique().getAccount().getId();
+      } catch (UnprocessableEntityException e) {
         switch (authType) {
           case HTTP_LDAP:
           case CLIENT_SSL_CERT_LDAP:
@@ -81,10 +81,12 @@
           case OPENID:
           case OPENID_SSO:
           default:
-            throw new CmdLineException(owner, localizable("user \"%s\" not found"), token);
+            String msg = "user \"%s\" not found";
+            logger.atSevere().withCause(e).log(msg, token);
+            throw new CmdLineException(owner, localizable(msg), token);
         }
       }
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       String msg = "database is down";
       logger.atSevere().withCause(e).log(msg);
       throw new CmdLineException(owner, localizable(msg));
diff --git a/java/com/google/gerrit/server/args4j/ChangeIdHandler.java b/java/com/google/gerrit/server/args4j/ChangeIdHandler.java
index eec3eb0..620bfa1 100644
--- a/java/com/google/gerrit/server/args4j/ChangeIdHandler.java
+++ b/java/com/google/gerrit/server/args4j/ChangeIdHandler.java
@@ -17,12 +17,12 @@
 import static com.google.gerrit.util.cli.Localizable.localizable;
 
 import com.google.common.base.Splitter;
+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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
@@ -67,7 +67,7 @@
       }
     } catch (IllegalArgumentException e) {
       throw new CmdLineException(owner, localizable("Change-Id is not valid: %s"), e.getMessage());
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       throw new CmdLineException(owner, localizable("Database error: %s"), e.getMessage());
     }
 
diff --git a/java/com/google/gerrit/server/args4j/ProjectHandler.java b/java/com/google/gerrit/server/args4j/ProjectHandler.java
index 52fadd2..e3af82b 100644
--- a/java/com/google/gerrit/server/args4j/ProjectHandler.java
+++ b/java/com/google/gerrit/server/args4j/ProjectHandler.java
@@ -17,9 +17,9 @@
 import static com.google.gerrit.util.cli.Localizable.localizable;
 
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.ProjectUtil;
 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;
 import com.google.gerrit.server.permissions.ProjectPermission;
diff --git a/java/com/google/gerrit/server/audit/BUILD b/java/com/google/gerrit/server/audit/BUILD
index 5d6b749..5c2a40a 100644
--- a/java/com/google/gerrit/server/audit/BUILD
+++ b/java/com/google/gerrit/server/audit/BUILD
@@ -24,7 +24,6 @@
         "//java/com/google/gerrit/util/cli",
         "//java/com/google/gerrit/util/ssl",
         "//java/org/apache/commons/net",
-        "//java/org/eclipse/jgit:server",
         "//lib:args4j",
         "//lib:autolink",
         "//lib:automaton",
@@ -57,8 +56,6 @@
         "//lib:gson",
         "//lib:guava",
         "//lib:guava-retrying",
-        "//lib:gwtjsonrpc",
-        "//lib:gwtorm",
         "//lib:jsch",
         "//lib:juniversalchardet",
         "//lib:mime-util",
diff --git a/java/com/google/gerrit/server/audit/RpcAuditEvent.java b/java/com/google/gerrit/server/audit/RpcAuditEvent.java
deleted file mode 100644
index 6c53bb2..0000000
--- a/java/com/google/gerrit/server/audit/RpcAuditEvent.java
+++ /dev/null
@@ -1,47 +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.audit;
-
-import com.google.common.collect.ListMultimap;
-import com.google.gerrit.server.CurrentUser;
-
-public class RpcAuditEvent extends HttpAuditEvent {
-
-  /**
-   * Creates a new audit event with results
-   *
-   * @param sessionId session id the event belongs to
-   * @param who principal that has generated the event
-   * @param what object of the event
-   * @param when time-stamp of when the event started
-   * @param params parameters of the event
-   * @param httpMethod HTTP method
-   * @param input input
-   * @param status HTTP status
-   * @param result result of the event
-   */
-  public RpcAuditEvent(
-      String sessionId,
-      CurrentUser who,
-      String what,
-      long when,
-      ListMultimap<String, ?> params,
-      String httpMethod,
-      Object input,
-      int status,
-      Object result) {
-    super(sessionId, who, what, when, params, httpMethod, input, status, result);
-  }
-}
diff --git a/java/com/google/gerrit/server/auth/UniversalAuthBackend.java b/java/com/google/gerrit/server/auth/UniversalAuthBackend.java
index 94faeef..4e93ff2 100644
--- a/java/com/google/gerrit/server/auth/UniversalAuthBackend.java
+++ b/java/com/google/gerrit/server/auth/UniversalAuthBackend.java
@@ -16,7 +16,7 @@
 
 import static java.util.Objects.requireNonNull;
 
-import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.ArrayList;
@@ -25,10 +25,10 @@
 /** Universal implementation of the AuthBackend that works with the injected set of AuthBackends. */
 @Singleton
 public final class UniversalAuthBackend implements AuthBackend {
-  private final DynamicSet<AuthBackend> authBackends;
+  private final PluginSetContext<AuthBackend> authBackends;
 
   @Inject
-  UniversalAuthBackend(DynamicSet<AuthBackend> authBackends) {
+  UniversalAuthBackend(PluginSetContext<AuthBackend> authBackends) {
     this.authBackends = authBackends;
   }
 
@@ -36,15 +36,16 @@
   public AuthUser authenticate(AuthRequest request) throws AuthException {
     List<AuthUser> authUsers = new ArrayList<>();
     List<AuthException> authExs = new ArrayList<>();
-    for (AuthBackend backend : authBackends) {
-      try {
-        authUsers.add(requireNonNull(backend.authenticate(request)));
-      } catch (MissingCredentialsException ex) {
-        // Not handled by this backend.
-      } catch (AuthException ex) {
-        authExs.add(ex);
-      }
-    }
+    authBackends.runEach(
+        backend -> {
+          try {
+            authUsers.add(requireNonNull(backend.authenticate(request)));
+          } catch (MissingCredentialsException ex) {
+            // Not handled by this backend.
+          } catch (AuthException ex) {
+            authExs.add(ex);
+          }
+        });
 
     // Handle the valid responses
     if (authUsers.size() == 1) {
diff --git a/java/com/google/gerrit/server/auth/ldap/Helper.java b/java/com/google/gerrit/server/auth/ldap/Helper.java
index 8e294b7..4b14c38 100644
--- a/java/com/google/gerrit/server/auth/ldap/Helper.java
+++ b/java/com/google/gerrit/server/auth/ldap/Helper.java
@@ -195,13 +195,7 @@
     Subject subject = ctx.getSubject();
     try {
       return Subject.doAs(
-          subject,
-          new PrivilegedExceptionAction<DirContext>() {
-            @Override
-            public DirContext run() throws IOException, NamingException {
-              return createContext(env);
-            }
-          });
+          subject, (PrivilegedExceptionAction<DirContext>) () -> createContext(env));
     } catch (PrivilegedActionException e) {
       Throwables.throwIfInstanceOf(e.getException(), IOException.class);
       Throwables.throwIfInstanceOf(e.getException(), NamingException.class);
@@ -422,7 +416,7 @@
                   groupBase,
                   groupScope,
                   new ParameterizedString(groupMemberPattern),
-                  Collections.<String>emptySet());
+                  Collections.emptySet());
           if (groupMemberQuery.getParameters().isEmpty()) {
             throw new IllegalArgumentException("No variables in ldap.groupMemberPattern");
           }
diff --git a/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java b/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java
index 3a6be0c..0980116 100644
--- a/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java
+++ b/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java
@@ -22,12 +22,12 @@
 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.ProtoCacheSerializers;
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.Singleton;
@@ -57,7 +57,7 @@
   static class Serializer implements CacheSerializer<OAuthToken> {
     @Override
     public byte[] serialize(OAuthToken object) {
-      return ProtoCacheSerializers.toByteArray(
+      return Protos.toByteArray(
           OAuthTokenProto.newBuilder()
               .setToken(object.getToken())
               .setSecret(object.getSecret())
@@ -69,7 +69,7 @@
 
     @Override
     public OAuthToken deserialize(byte[] in) {
-      OAuthTokenProto proto = ProtoCacheSerializers.parseUnchecked(OAuthTokenProto.parser(), in);
+      OAuthTokenProto proto = Protos.parseUnchecked(OAuthTokenProto.parser(), in);
       return new OAuthToken(
           proto.getToken(),
           proto.getSecret(),
diff --git a/java/com/google/gerrit/server/cache/CacheMetrics.java b/java/com/google/gerrit/server/cache/CacheMetrics.java
index c652d50..f2dd00e 100644
--- a/java/com/google/gerrit/server/cache/CacheMetrics.java
+++ b/java/com/google/gerrit/server/cache/CacheMetrics.java
@@ -67,7 +67,7 @@
             F_NAME);
 
     Set<CallbackMetric<?>> cacheMetrics =
-        ImmutableSet.<CallbackMetric<?>>of(memEnt, memHit, memEvict, perDiskEnt, perDiskHit);
+        ImmutableSet.of(memEnt, memHit, memEvict, perDiskEnt, perDiskHit);
 
     metrics.newTrigger(
         cacheMetrics,
diff --git a/java/com/google/gerrit/server/cache/ForwardingRemovalListener.java b/java/com/google/gerrit/server/cache/ForwardingRemovalListener.java
index a7fdbbd..ee672cd 100644
--- a/java/com/google/gerrit/server/cache/ForwardingRemovalListener.java
+++ b/java/com/google/gerrit/server/cache/ForwardingRemovalListener.java
@@ -17,8 +17,8 @@
 import com.google.common.base.Strings;
 import com.google.common.cache.RemovalListener;
 import com.google.common.cache.RemovalNotification;
-import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.registration.PluginName;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -35,13 +35,13 @@
     ForwardingRemovalListener create(String cacheName);
   }
 
-  private final DynamicSet<CacheRemovalListener> listeners;
+  private final PluginSetContext<CacheRemovalListener> listeners;
   private final String cacheName;
   private String pluginName = PluginName.GERRIT;
 
   @Inject
   ForwardingRemovalListener(
-      DynamicSet<CacheRemovalListener> listeners, @Assisted String cacheName) {
+      PluginSetContext<CacheRemovalListener> listeners, @Assisted String cacheName) {
     this.listeners = listeners;
     this.cacheName = cacheName;
   }
@@ -56,8 +56,6 @@
   @Override
   @SuppressWarnings("unchecked")
   public void onRemoval(RemovalNotification<K, V> notification) {
-    for (CacheRemovalListener<K, V> l : listeners) {
-      l.onRemoval(pluginName, cacheName, notification);
-    }
+    listeners.runEach(l -> l.onRemoval(pluginName, cacheName, notification));
   }
 }
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheDefProxy.java b/java/com/google/gerrit/server/cache/h2/H2CacheDefProxy.java
index 48c0a5b..5d9ce60 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheDefProxy.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheDefProxy.java
@@ -42,7 +42,6 @@
     return source.expireFromMemoryAfterAccess();
   }
 
-  @SuppressWarnings("unchecked")
   @Override
   public Weigher<K, V> weigher() {
     Weigher<K, V> weigher = source.weigher();
@@ -52,13 +51,10 @@
 
     // introduce weigher that performs calculations
     // on value that is being stored not on ValueHolder
-    return (Weigher<K, V>)
-        new Weigher<K, ValueHolder<V>>() {
-          @Override
-          public int weigh(K key, ValueHolder<V> value) {
-            return weigher.weigh(key, value.value);
-          }
-        };
+    Weigher<K, ValueHolder<V>> holderWeigher = (k, v) -> weigher.weigh(k, v.value);
+    @SuppressWarnings("unchecked")
+    Weigher<K, V> ret = (Weigher<K, V>) holderWeigher;
+    return ret;
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/cache/serialize/BUILD b/java/com/google/gerrit/server/cache/serialize/BUILD
index 786df8b..76dcbb1 100644
--- a/java/com/google/gerrit/server/cache/serialize/BUILD
+++ b/java/com/google/gerrit/server/cache/serialize/BUILD
@@ -6,8 +6,9 @@
     visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/common:annotations",
+        "//java/com/google/gerrit/proto",
+        "//java/com/google/gwtorm",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib:protobuf",
         "//lib/jgit/org.eclipse.jgit:jgit",
     ],
diff --git a/java/com/google/gerrit/server/cache/serialize/ObjectIdConverter.java b/java/com/google/gerrit/server/cache/serialize/ObjectIdConverter.java
new file mode 100644
index 0000000..eb946a9
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/serialize/ObjectIdConverter.java
@@ -0,0 +1,56 @@
+// 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.base.Preconditions.checkArgument;
+import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH;
+
+import com.google.protobuf.ByteString;
+import org.eclipse.jgit.lib.ObjectId;
+
+/**
+ * Helper for serializing {@link ObjectId} instances to/from protobuf fields.
+ *
+ * <p>Reuse a single instance's {@link #toByteString(ObjectId)} and {@link
+ * #fromByteString(ByteString)} within a single {@link CacheSerializer#serialize} or {@link
+ * CacheSerializer#deserialize} method body to minimize allocation of temporary buffers.
+ *
+ * <p><strong>Note:</strong> This class is not threadsafe. Instances must not be stored in {@link
+ * CacheSerializer} fields if the serializer instances will be used from multiple threads.
+ */
+public class ObjectIdConverter {
+  public static ObjectIdConverter create() {
+    return new ObjectIdConverter();
+  }
+
+  private final byte[] buf = new byte[OBJECT_ID_LENGTH];
+
+  private ObjectIdConverter() {}
+
+  public ByteString toByteString(ObjectId id) {
+    id.copyRawTo(buf, 0);
+    return ByteString.copyFrom(buf);
+  }
+
+  public ObjectId fromByteString(ByteString in) {
+    checkArgument(
+        in.size() == OBJECT_ID_LENGTH,
+        "expected ByteString of length %s: %s",
+        OBJECT_ID_LENGTH,
+        in);
+    in.copyTo(buf, 0);
+    return ObjectId.fromRaw(buf);
+  }
+}
diff --git a/java/com/google/gerrit/server/cache/serialize/ProtoCacheSerializers.java b/java/com/google/gerrit/server/cache/serialize/ProtoCacheSerializers.java
deleted file mode 100644
index 4e0b106..0000000
--- a/java/com/google/gerrit/server/cache/serialize/ProtoCacheSerializers.java
+++ /dev/null
@@ -1,127 +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.base.Preconditions.checkArgument;
-import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH;
-
-import com.google.gwtorm.protobuf.ProtobufCodec;
-import com.google.protobuf.ByteString;
-import com.google.protobuf.CodedOutputStream;
-import com.google.protobuf.MessageLite;
-import com.google.protobuf.Parser;
-import java.io.IOException;
-import org.eclipse.jgit.lib.ObjectId;
-
-/** Static utilities for writing protobuf-based {@link CacheSerializer} implementations. */
-public class ProtoCacheSerializers {
-  /**
-   * Serializes a proto to a byte array.
-   *
-   * <p>Guarantees deterministic serialization and thus is suitable for use in persistent caches.
-   * Should be used in preference to {@link MessageLite#toByteArray()}, which is not guaranteed
-   * deterministic.
-   *
-   * @param message the proto message to serialize.
-   * @return a byte array with the message contents.
-   */
-  public static byte[] toByteArray(MessageLite message) {
-    byte[] bytes = new byte[message.getSerializedSize()];
-    CodedOutputStream cout = CodedOutputStream.newInstance(bytes);
-    cout.useDeterministicSerialization();
-    try {
-      message.writeTo(cout);
-      cout.checkNoSpaceLeft();
-      return bytes;
-    } catch (IOException e) {
-      throw new IllegalStateException("exception writing to byte array", e);
-    }
-  }
-
-  /**
-   * Serializes an object to a {@link ByteString} using a protobuf codec.
-   *
-   * <p>Guarantees deterministic serialization and thus is suitable for use in persistent caches.
-   * Should be used in preference to {@link ProtobufCodec#encodeToByteString(Object)}, which is not
-   * guaranteed deterministic.
-   *
-   * @param object the object to serialize.
-   * @param codec codec for serializing.
-   * @return a {@code ByteString} with the message contents.
-   */
-  public static <T> ByteString toByteString(T object, ProtobufCodec<T> codec) {
-    try (ByteString.Output bout = ByteString.newOutput()) {
-      CodedOutputStream cout = CodedOutputStream.newInstance(bout);
-      cout.useDeterministicSerialization();
-      codec.encode(object, cout);
-      cout.flush();
-      return bout.toByteString();
-    } catch (IOException e) {
-      throw new IllegalStateException("exception writing to ByteString", e);
-    }
-  }
-
-  /**
-   * Parses a byte array to a protobuf message.
-   *
-   * @param parser parser for the proto type.
-   * @param in byte array with the message contents.
-   * @return parsed proto.
-   */
-  public static <M extends MessageLite> M parseUnchecked(Parser<M> parser, byte[] in) {
-    try {
-      return parser.parseFrom(in);
-    } catch (IOException e) {
-      throw new IllegalArgumentException("exception parsing byte array to proto", e);
-    }
-  }
-
-  /**
-   * Helper for serializing {@link ObjectId} instances to/from protobuf fields.
-   *
-   * <p>Reuse a single instance's {@link #toByteString(ObjectId)} and {@link
-   * #fromByteString(ByteString)} within a single {@link CacheSerializer#serialize} or {@link
-   * CacheSerializer#deserialize} method body to minimize allocation of temporary buffers.
-   *
-   * <p><strong>Note:</strong> This class is not threadsafe. Instances must not be stored in {@link
-   * CacheSerializer} fields if the serializer instances will be used from multiple threads.
-   */
-  public static class ObjectIdConverter {
-    public static ObjectIdConverter create() {
-      return new ObjectIdConverter();
-    }
-
-    private final byte[] buf = new byte[OBJECT_ID_LENGTH];
-
-    private ObjectIdConverter() {}
-
-    public ByteString toByteString(ObjectId id) {
-      id.copyRawTo(buf, 0);
-      return ByteString.copyFrom(buf);
-    }
-
-    public ObjectId fromByteString(ByteString in) {
-      checkArgument(
-          in.size() == OBJECT_ID_LENGTH,
-          "expected ByteString of length %s: %s",
-          OBJECT_ID_LENGTH,
-          in);
-      in.copyTo(buf, 0);
-      return ObjectId.fromRaw(buf);
-    }
-  }
-
-  private ProtoCacheSerializers() {}
-}
diff --git a/java/com/google/gerrit/server/cache/serialize/ProtobufSerializer.java b/java/com/google/gerrit/server/cache/serialize/ProtobufSerializer.java
new file mode 100644
index 0000000..180646b
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/serialize/ProtobufSerializer.java
@@ -0,0 +1,38 @@
+// 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.cache.serialize;
+
+import com.google.gerrit.proto.Protos;
+import com.google.protobuf.MessageLite;
+import com.google.protobuf.Parser;
+
+/** A CacheSerializer for Protobuf messages. */
+public class ProtobufSerializer<T extends MessageLite> implements CacheSerializer<T> {
+  private final Parser<T> parser;
+
+  public ProtobufSerializer(Parser<T> parser) {
+    this.parser = parser;
+  }
+
+  @Override
+  public byte[] serialize(T object) {
+    return Protos.toByteArray(object);
+  }
+
+  @Override
+  public T deserialize(byte[] in) {
+    return Protos.parseUnchecked(parser, in);
+  }
+}
diff --git a/java/com/google/gerrit/server/cache/testing/BUILD b/java/com/google/gerrit/server/cache/testing/BUILD
index 49879f2..09f698c 100644
--- a/java/com/google/gerrit/server/cache/testing/BUILD
+++ b/java/com/google/gerrit/server/cache/testing/BUILD
@@ -7,10 +7,6 @@
     srcs = glob(["*.java"]),
     visibility = ["//visibility:public"],
     deps = [
-        "//java/com/google/gerrit/server/cache/serialize",
-        "//lib:guava",
         "//lib:protobuf",
-        "//lib/commons:lang3",
-        "//lib/truth",
     ],
 )
diff --git a/java/com/google/gerrit/server/cache/testing/SerializedClassSubject.java b/java/com/google/gerrit/server/cache/testing/SerializedClassSubject.java
deleted file mode 100644
index 2351e2c..0000000
--- a/java/com/google/gerrit/server/cache/testing/SerializedClassSubject.java
+++ /dev/null
@@ -1,109 +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.testing;
-
-import static com.google.common.collect.ImmutableMap.toImmutableMap;
-import static com.google.common.truth.Truth.assertAbout;
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import com.google.common.truth.FailureMetadata;
-import com.google.common.truth.Subject;
-import java.lang.reflect.Field;
-import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
-import java.lang.reflect.Type;
-import java.util.Arrays;
-import java.util.Map;
-import org.apache.commons.lang3.reflect.FieldUtils;
-
-/**
- * Subject about classes that are serialized into persistent caches.
- *
- * <p>Hand-written {@link com.google.gerrit.server.cache.serialize.CacheSerializer CacheSerializer}
- * 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 implementation.
- *
- * <p>Changing fields of a serialized class (or abstract methods, in the case of {@code @AutoValue}
- * classes) will likely require changes to the serializer implementation, and may require bumping
- * the {@link com.google.gerrit.server.cache.PersistentCacheBinding#version(int) version} in the
- * cache binding, in case the representation has changed in such a way that old serialized data
- * becomes unreadable.
- *
- * <p>Changes to a serialized class such as adding or removing fields generally requires a change to
- * 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 static SerializedClassSubject assertThatSerializedClass(Class<?> actual) {
-    // This formulation fails in Eclipse 4.7.3a with "The type
-    // SerializedClassSubject does not define SerializedClassSubject() that is
-    // applicable here", due to
-    // https://bugs.eclipse.org/bugs/show_bug.cgi?id=534694 or a similar bug:
-    // return assertAbout(SerializedClassSubject::new).that(actual);
-    Subject.Factory<SerializedClassSubject, Class<?>> factory =
-        (m, a) -> new SerializedClassSubject(m, a);
-    return assertAbout(factory).that(actual);
-  }
-
-  private SerializedClassSubject(FailureMetadata metadata, Class<?> actual) {
-    super(metadata, actual);
-  }
-
-  public void isAbstract() {
-    isNotNull();
-    assertWithMessage("expected class %s to be abstract", actual().getName())
-        .that(Modifier.isAbstract(actual().getModifiers()))
-        .isTrue();
-  }
-
-  public void isConcrete() {
-    isNotNull();
-    assertWithMessage("expected class %s to be concrete", actual().getName())
-        .that(!Modifier.isAbstract(actual().getModifiers()))
-        .isTrue();
-  }
-
-  public void hasFields(Map<String, Type> expectedFields) {
-    isConcrete();
-    assertThat(
-            FieldUtils.getAllFieldsList(actual()).stream()
-                .filter(f -> !Modifier.isStatic(f.getModifiers()))
-                .collect(toImmutableMap(Field::getName, Field::getGenericType)))
-        .containsExactlyEntriesIn(expectedFields);
-  }
-
-  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();
-    assertThat(
-            Arrays.stream(actual().getDeclaredMethods())
-                .filter(m -> !Modifier.isStatic(m.getModifiers()))
-                .filter(m -> Modifier.isAbstract(m.getModifiers()))
-                .filter(m -> m.getParameters().length == 0)
-                .collect(toImmutableMap(Method::getName, Method::getGenericReturnType)))
-        .named("no-argument abstract methods on %s", actual().getName())
-        .isEqualTo(expectedMethods);
-  }
-
-  public void extendsClass(Type superclassType) {
-    isNotNull();
-    assertThat(actual().getGenericSuperclass())
-        .named("superclass of %s", actual().getName())
-        .isEqualTo(superclassType);
-  }
-}
diff --git a/java/com/google/gerrit/server/change/AbandonOp.java b/java/com/google/gerrit/server/change/AbandonOp.java
index 5affd5c..5ee5bc7 100644
--- a/java/com/google/gerrit/server/change/AbandonOp.java
+++ b/java/com/google/gerrit/server/change/AbandonOp.java
@@ -15,13 +15,9 @@
 package com.google.gerrit.server.change;
 
 import com.google.common.base.Strings;
-import com.google.common.collect.ListMultimap;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.api.changes.RecipientType;
 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.PatchSet;
@@ -36,7 +32,6 @@
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -49,8 +44,6 @@
   private final ChangeAbandoned changeAbandoned;
 
   private final String msgTxt;
-  private final NotifyHandling notifyHandling;
-  private final ListMultimap<RecipientType, Account.Id> accountsToNotify;
   private final AccountState accountState;
 
   private Change change;
@@ -59,10 +52,7 @@
 
   public interface Factory {
     AbandonOp create(
-        @Assisted @Nullable AccountState accountState,
-        @Assisted @Nullable String msgTxt,
-        @Assisted NotifyHandling notifyHandling,
-        @Assisted ListMultimap<RecipientType, Account.Id> accountsToNotify);
+        @Assisted @Nullable AccountState accountState, @Assisted @Nullable String msgTxt);
   }
 
   @Inject
@@ -72,9 +62,7 @@
       PatchSetUtil psUtil,
       ChangeAbandoned changeAbandoned,
       @Assisted @Nullable AccountState accountState,
-      @Assisted @Nullable String msgTxt,
-      @Assisted NotifyHandling notifyHandling,
-      @Assisted ListMultimap<RecipientType, Account.Id> accountsToNotify) {
+      @Assisted @Nullable String msgTxt) {
     this.abandonedSenderFactory = abandonedSenderFactory;
     this.cmUtil = cmUtil;
     this.psUtil = psUtil;
@@ -82,8 +70,6 @@
 
     this.accountState = accountState;
     this.msgTxt = Strings.nullToEmpty(msgTxt);
-    this.notifyHandling = notifyHandling;
-    this.accountsToNotify = accountsToNotify;
   }
 
   @Nullable
@@ -92,20 +78,20 @@
   }
 
   @Override
-  public boolean updateChange(ChangeContext ctx) throws OrmException, ResourceConflictException {
+  public boolean updateChange(ChangeContext ctx) throws ResourceConflictException {
     change = ctx.getChange();
     PatchSet.Id psId = change.currentPatchSetId();
     ChangeUpdate update = ctx.getUpdate(psId);
-    if (!change.getStatus().isOpen()) {
+    if (!change.isNew()) {
       throw new ResourceConflictException("change is " + ChangeUtil.status(change));
     }
-    patchSet = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
+    patchSet = psUtil.get(ctx.getNotes(), psId);
     change.setStatus(Change.Status.ABANDONED);
     change.setLastUpdatedOn(ctx.getWhen());
 
     update.setStatus(change.getStatus());
     message = newMessage(ctx);
-    cmUtil.addChangeMessage(ctx.getDb(), update, message);
+    cmUtil.addChangeMessage(update, message);
     return true;
   }
 
@@ -121,19 +107,19 @@
   }
 
   @Override
-  public void postUpdate(Context ctx) throws OrmException {
+  public void postUpdate(Context ctx) {
+    NotifyResolver.Result notify = ctx.getNotify(change.getId());
     try {
       ReplyToChangeSender cm = abandonedSenderFactory.create(ctx.getProject(), change.getId());
       if (accountState != null) {
         cm.setFrom(accountState.getAccount().getId());
       }
       cm.setChangeMessage(message.getMessage(), ctx.getWhen());
-      cm.setNotify(notifyHandling);
-      cm.setAccountsToNotify(accountsToNotify);
+      cm.setNotify(notify);
       cm.send();
     } catch (Exception e) {
       logger.atSevere().withCause(e).log("Cannot email update for change %s", change.getId());
     }
-    changeAbandoned.fire(change, patchSet, accountState, msgTxt, ctx.getWhen(), notifyHandling);
+    changeAbandoned.fire(change, patchSet, accountState, msgTxt, ctx.getWhen(), notify.handling());
   }
 }
diff --git a/java/com/google/gerrit/server/change/AbandonUtil.java b/java/com/google/gerrit/server/change/AbandonUtil.java
index f505f6d..6f46498 100644
--- a/java/com/google/gerrit/server/change/AbandonUtil.java
+++ b/java/com/google/gerrit/server/change/AbandonUtil.java
@@ -17,6 +17,7 @@
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.flogger.FluentLogger;
+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;
@@ -25,7 +26,6 @@
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gerrit.server.query.change.ChangeQueryProcessor;
 import com.google.gerrit.server.update.BatchUpdate;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -96,14 +96,14 @@
         }
       }
       logger.atInfo().log("Auto-Abandoned %d of %d changes.", count, changesToAbandon.size());
-    } catch (QueryParseException | OrmException e) {
+    } catch (QueryParseException | StorageException e) {
       logger.atSevere().withCause(e).log(
           "Failed to query inactive open changes for auto-abandoning.");
     }
   }
 
   private Collection<ChangeData> getValidChanges(Collection<ChangeData> changes, String query)
-      throws OrmException, QueryParseException {
+      throws QueryParseException {
     Collection<ChangeData> validChanges = new ArrayList<>();
     for (ChangeData cd : changes) {
       String newQuery = query + " change:" + cd.getId();
diff --git a/java/com/google/gerrit/server/change/AccountPatchReviewStore.java b/java/com/google/gerrit/server/change/AccountPatchReviewStore.java
index 69825ea..fff3274 100644
--- a/java/com/google/gerrit/server/change/AccountPatchReviewStore.java
+++ b/java/com/google/gerrit/server/change/AccountPatchReviewStore.java
@@ -17,8 +17,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.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwtorm.server.OrmException;
 import java.util.Collection;
 import java.util.Optional;
 
@@ -54,9 +54,8 @@
    * @param path file path
    * @return {@code true} if the reviewed flag was updated, {@code false} if the reviewed flag was
    *     already set
-   * @throws OrmException thrown if updating the reviewed flag failed
    */
-  boolean markReviewed(PatchSet.Id psId, Account.Id accountId, String path) throws OrmException;
+  boolean markReviewed(PatchSet.Id psId, Account.Id accountId, String path);
 
   /**
    * Marks the given files in the given patch set as reviewed by the given user.
@@ -64,10 +63,8 @@
    * @param psId patch set ID
    * @param accountId account ID of the user
    * @param paths file paths
-   * @throws OrmException thrown if updating the reviewed flag failed
    */
-  void markReviewed(PatchSet.Id psId, Account.Id accountId, Collection<String> paths)
-      throws OrmException;
+  void markReviewed(PatchSet.Id psId, Account.Id accountId, Collection<String> paths);
 
   /**
    * Clears the reviewed flag for the given file in the given patch set for the given user.
@@ -75,17 +72,22 @@
    * @param psId patch set ID
    * @param accountId account ID of the user
    * @param path file path
-   * @throws OrmException thrown if clearing the reviewed flag failed
    */
-  void clearReviewed(PatchSet.Id psId, Account.Id accountId, String path) throws OrmException;
+  void clearReviewed(PatchSet.Id psId, Account.Id accountId, String path);
 
   /**
    * Clears the reviewed flags for all files in the given patch set for all users.
    *
    * @param psId patch set ID
-   * @throws OrmException thrown if clearing the reviewed flags failed
    */
-  void clearReviewed(PatchSet.Id psId) throws OrmException;
+  void clearReviewed(PatchSet.Id psId);
+
+  /**
+   * Clears the reviewed flags for all files in all patch sets in the given change for all users.
+   *
+   * @param changeId change ID
+   */
+  void clearReviewed(Change.Id changeId);
 
   /**
    * Find the latest patch set, that is smaller or equals to the given patch set, where at least,
@@ -95,8 +97,6 @@
    * @param accountId account ID of the user
    * @return optionally, all files the have been reviewed by the given user that belong to the patch
    *     set that is smaller or equals to the given patch set
-   * @throws OrmException thrown if accessing the reviewed flags failed
    */
-  Optional<PatchSetWithReviewedFiles> findReviewed(PatchSet.Id psId, Account.Id accountId)
-      throws OrmException;
+  Optional<PatchSetWithReviewedFiles> findReviewed(PatchSet.Id psId, Account.Id accountId);
 }
diff --git a/java/com/google/gerrit/server/change/ActionJson.java b/java/com/google/gerrit/server/change/ActionJson.java
index 0a9fe81..d493b31 100644
--- a/java/com/google/gerrit/server/change/ActionJson.java
+++ b/java/com/google/gerrit/server/change/ActionJson.java
@@ -29,11 +29,9 @@
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.webui.PrivateInternals_UiActionDescription;
 import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.extensions.webui.UiActions;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -71,7 +69,7 @@
     this.userProvider = userProvider;
   }
 
-  public Map<String, ActionInfo> format(RevisionResource rsrc) throws OrmException {
+  public Map<String, ActionInfo> format(RevisionResource rsrc) {
     ChangeInfo changeInfo = null;
     RevisionInfo revisionInfo = null;
     List<ActionVisitor> visitors = visitors();
@@ -98,7 +96,7 @@
   }
 
   public RevisionInfo addRevisionActions(
-      @Nullable ChangeInfo changeInfo, RevisionInfo to, RevisionResource rsrc) throws OrmException {
+      @Nullable ChangeInfo changeInfo, RevisionInfo to, RevisionResource rsrc) {
     List<ActionVisitor> visitors = visitors();
     if (!visitors.isEmpty()) {
       if (changeInfo != null) {
@@ -180,8 +178,7 @@
     // The followup action is a client-side only operation that does not
     // have a server side handler. It must be manually registered into the
     // resulting action map.
-    Status status = notes.getChange().getStatus();
-    if (status.isOpen() || status.equals(Status.MERGED)) {
+    if (!notes.getChange().isAbandoned()) {
       UiAction.Description descr = new UiAction.Description();
       PrivateInternals_UiActionDescription.setId(descr, "followup");
       PrivateInternals_UiActionDescription.setMethod(descr, "POST");
diff --git a/java/com/google/gerrit/server/change/AddReviewersEmail.java b/java/com/google/gerrit/server/change/AddReviewersEmail.java
index 4173950..d9c5dad 100644
--- a/java/com/google/gerrit/server/change/AddReviewersEmail.java
+++ b/java/com/google/gerrit/server/change/AddReviewersEmail.java
@@ -16,12 +16,8 @@
 
 import static com.google.common.collect.ImmutableList.toImmutableList;
 
-import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ListMultimap;
 import com.google.common.flogger.FluentLogger;
-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.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
@@ -49,9 +45,7 @@
       Collection<Account.Id> copied,
       Collection<Address> addedByEmail,
       Collection<Address> copiedByEmail,
-      NotifyHandling notify,
-      ListMultimap<RecipientType, Account.Id> accountsToNotify,
-      boolean readyForReview) {
+      NotifyResolver.Result notify) {
     // The user knows they added themselves, don't bother emailing them.
     Account.Id userId = user.getAccountId();
     ImmutableList<Account.Id> toMail =
@@ -64,11 +58,7 @@
 
     try {
       AddReviewerSender cm = addReviewerSenderFactory.create(change.getProject(), change.getId());
-      // Default to silent operation on WIP changes.
-      NotifyHandling defaultNotifyHandling =
-          readyForReview ? NotifyHandling.ALL : NotifyHandling.NONE;
-      cm.setNotify(MoreObjects.firstNonNull(notify, defaultNotifyHandling));
-      cm.setAccountsToNotify(accountsToNotify);
+      cm.setNotify(notify);
       cm.setFrom(userId);
       cm.addReviewers(toMail);
       cm.addReviewersByEmail(addedByEmail);
diff --git a/java/com/google/gerrit/server/change/AddReviewersOp.java b/java/com/google/gerrit/server/change/AddReviewersOp.java
index 6c0aa94..610290d 100644
--- a/java/com/google/gerrit/server/change/AddReviewersOp.java
+++ b/java/com/google/gerrit/server/change/AddReviewersOp.java
@@ -25,12 +25,8 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Streams;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.mail.Address;
@@ -43,13 +39,11 @@
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.extensions.events.ReviewerAdded;
-import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
@@ -70,16 +64,10 @@
      * @param accountIds account IDs to add.
      * @param addresses email addresses to add.
      * @param state resulting reviewer state.
-     * @param notify notification handling.
-     * @param accountsToNotify additional accounts to notify.
      * @return batch update operation.
      */
     AddReviewersOp create(
-        Set<Account.Id> accountIds,
-        Collection<Address> addresses,
-        ReviewerState state,
-        @Nullable NotifyHandling notify,
-        ListMultimap<RecipientType, Account.Id> accountsToNotify);
+        Set<Account.Id> accountIds, Collection<Address> addresses, ReviewerState state);
   }
 
   @AutoValue
@@ -116,12 +104,9 @@
   private final AccountCache accountCache;
   private final ProjectCache projectCache;
   private final AddReviewersEmail addReviewersEmail;
-  private final NotesMigration migration;
   private final Set<Account.Id> accountIds;
   private final Collection<Address> addresses;
   private final ReviewerState state;
-  private final NotifyHandling notify;
-  private final ListMultimap<RecipientType, Account.Id> accountsToNotify;
 
   // Unlike addedCCs, addedReviewers is a PatchSetApproval because the AddReviewerResult returned
   // via the REST API is supposed to include vote information.
@@ -130,6 +115,7 @@
   private Collection<Account.Id> addedCCs = ImmutableList.of();
   private Collection<Address> addedCCsByEmail = ImmutableList.of();
 
+  private boolean sendEmail = true;
   private Change change;
   private PatchSet patchSet;
   private Result opResult;
@@ -142,12 +128,9 @@
       AccountCache accountCache,
       ProjectCache projectCache,
       AddReviewersEmail addReviewersEmail,
-      NotesMigration migration,
       @Assisted Set<Account.Id> accountIds,
       @Assisted Collection<Address> addresses,
-      @Assisted ReviewerState state,
-      @Assisted @Nullable NotifyHandling notify,
-      @Assisted ListMultimap<RecipientType, Account.Id> accountsToNotify) {
+      @Assisted ReviewerState state) {
     checkArgument(state == REVIEWER || state == CC, "must be %s or %s: %s", REVIEWER, CC, state);
     this.approvalsUtil = approvalsUtil;
     this.psUtil = psUtil;
@@ -155,13 +138,17 @@
     this.accountCache = accountCache;
     this.projectCache = projectCache;
     this.addReviewersEmail = addReviewersEmail;
-    this.migration = migration;
 
     this.accountIds = accountIds;
     this.addresses = addresses;
     this.state = state;
-    this.notify = notify;
-    this.accountsToNotify = accountsToNotify;
+  }
+
+  // TODO(dborowitz): This mutable setter is ugly, but a) it's less ugly than adding boolean args
+  // all the way through the constructor stack, and b) this class is slated to be completely
+  // rewritten.
+  public void suppressEmail() {
+    this.sendEmail = false;
   }
 
   void setPatchSet(PatchSet patchSet) {
@@ -169,18 +156,16 @@
   }
 
   @Override
-  public boolean updateChange(ChangeContext ctx)
-      throws RestApiException, OrmException, IOException {
+  public boolean updateChange(ChangeContext ctx) throws RestApiException, IOException {
     change = ctx.getChange();
     if (!accountIds.isEmpty()) {
-      if (migration.readChanges() && state == CC) {
+      if (state == CC) {
         addedCCs =
             approvalsUtil.addCcs(
                 ctx.getNotes(), ctx.getUpdate(change.currentPatchSetId()), accountIds);
       } else {
         addedReviewers =
             approvalsUtil.addReviewers(
-                ctx.getDb(),
                 ctx.getNotes(),
                 ctx.getUpdate(change.currentPatchSetId()),
                 projectCache.checkedGet(change.getProject()).getLabelTypes(change.getDest()),
@@ -191,21 +176,21 @@
 
     ImmutableList<Address> addressesToAdd = ImmutableList.of();
     ReviewerStateInternal internalState = ReviewerStateInternal.fromReviewerState(state);
-    if (migration.readChanges()) {
-      // TODO(dborowitz): This behavior should live in ApprovalsUtil or something, like addCcs does.
-      ImmutableSet<Address> existing = ctx.getNotes().getReviewersByEmail().byState(internalState);
-      addressesToAdd =
-          addresses.stream().filter(a -> !existing.contains(a)).collect(toImmutableList());
 
-      if (state == CC) {
-        addedCCsByEmail = addressesToAdd;
-      } else {
-        addedReviewersByEmail = addressesToAdd;
-      }
-      for (Address a : addressesToAdd) {
-        ctx.getUpdate(change.currentPatchSetId()).putReviewerByEmail(a, internalState);
-      }
+    // TODO(dborowitz): This behavior should live in ApprovalsUtil or something, like addCcs does.
+    ImmutableSet<Address> existing = ctx.getNotes().getReviewersByEmail().byState(internalState);
+    addressesToAdd =
+        addresses.stream().filter(a -> !existing.contains(a)).collect(toImmutableList());
+
+    if (state == CC) {
+      addedCCsByEmail = addressesToAdd;
+    } else {
+      addedReviewersByEmail = addressesToAdd;
     }
+    for (Address a : addressesToAdd) {
+      ctx.getUpdate(change.currentPatchSetId()).putReviewerByEmail(a, internalState);
+    }
+
     if (addedCCs.isEmpty() && addedReviewers.isEmpty() && addressesToAdd.isEmpty()) {
       return false;
     }
@@ -213,7 +198,7 @@
     checkAdded();
 
     if (patchSet == null) {
-      patchSet = requireNonNull(psUtil.current(ctx.getDb(), ctx.getNotes()));
+      patchSet = requireNonNull(psUtil.current(ctx.getNotes()));
     }
     return true;
   }
@@ -251,16 +236,16 @@
             .setAddedCCs(addedCCs)
             .setAddedCCsByEmail(addedCCsByEmail)
             .build();
-    addReviewersEmail.emailReviewers(
-        ctx.getUser().asIdentifiedUser(),
-        change,
-        Lists.transform(addedReviewers, PatchSetApproval::getAccountId),
-        addedCCs,
-        addedReviewersByEmail,
-        addedCCsByEmail,
-        notify,
-        accountsToNotify,
-        !change.isWorkInProgress());
+    if (sendEmail) {
+      addReviewersEmail.emailReviewers(
+          ctx.getUser().asIdentifiedUser(),
+          change,
+          Lists.transform(addedReviewers, PatchSetApproval::getAccountId),
+          addedCCs,
+          addedReviewersByEmail,
+          addedCCsByEmail,
+          ctx.getNotify(change.getId()));
+    }
     if (!addedReviewers.isEmpty()) {
       List<AccountState> reviewers =
           addedReviewers.stream()
diff --git a/java/com/google/gerrit/server/change/BatchAbandon.java b/java/com/google/gerrit/server/change/BatchAbandon.java
index a8e2407..bc29c5d 100644
--- a/java/com/google/gerrit/server/change/BatchAbandon.java
+++ b/java/com/google/gerrit/server/change/BatchAbandon.java
@@ -14,35 +14,35 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.ListMultimap;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.api.changes.RecipientType;
 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.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.config.ChangeCleanupConfig;
+import com.google.gerrit.server.plugincontext.PluginItemContext;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.UpdateException;
 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.util.Collection;
 
 @Singleton
 public class BatchAbandon {
-  private final Provider<ReviewDb> dbProvider;
   private final AbandonOp.Factory abandonOpFactory;
+  private final ChangeCleanupConfig cfg;
+  private final PluginItemContext<AccountPatchReviewStore> accountPatchReviewStore;
 
   @Inject
-  BatchAbandon(Provider<ReviewDb> dbProvider, AbandonOp.Factory abandonOpFactory) {
-    this.dbProvider = dbProvider;
+  BatchAbandon(
+      AbandonOp.Factory abandonOpFactory,
+      ChangeCleanupConfig cfg,
+      PluginItemContext<AccountPatchReviewStore> accountPatchReviewStore) {
     this.abandonOpFactory = abandonOpFactory;
+    this.cfg = cfg;
+    this.accountPatchReviewStore = accountPatchReviewStore;
   }
 
   /**
@@ -58,14 +58,14 @@
       CurrentUser user,
       Collection<ChangeData> changes,
       String msgTxt,
-      NotifyHandling notifyHandling,
-      ListMultimap<RecipientType, Account.Id> accountsToNotify)
+      NotifyResolver.Result notify)
       throws RestApiException, UpdateException {
     if (changes.isEmpty()) {
       return;
     }
     AccountState accountState = user.isIdentifiedUser() ? user.asIdentifiedUser().state() : null;
-    try (BatchUpdate u = updateFactory.create(dbProvider.get(), project, user, TimeUtil.nowTs())) {
+    try (BatchUpdate u = updateFactory.create(project, user, TimeUtil.nowTs())) {
+      u.setNotify(notify);
       for (ChangeData change : changes) {
         if (!project.equals(change.project())) {
           throw new ResourceConflictException(
@@ -73,11 +73,13 @@
                   "Project name \"%s\" doesn't match \"%s\"",
                   change.project().get(), project.get()));
         }
-        u.addOp(
-            change.getId(),
-            abandonOpFactory.create(accountState, msgTxt, notifyHandling, accountsToNotify));
+        u.addOp(change.getId(), abandonOpFactory.create(accountState, msgTxt));
       }
       u.execute();
+
+      if (cfg.getCleanupAccountPatchReview()) {
+        cleanupAccountPatchReview(changes);
+      }
     }
   }
 
@@ -88,14 +90,7 @@
       Collection<ChangeData> changes,
       String msgTxt)
       throws RestApiException, UpdateException {
-    batchAbandon(
-        updateFactory,
-        project,
-        user,
-        changes,
-        msgTxt,
-        NotifyHandling.ALL,
-        ImmutableListMultimap.of());
+    batchAbandon(updateFactory, project, user, changes, msgTxt, NotifyResolver.Result.all());
   }
 
   public void batchAbandon(
@@ -104,7 +99,12 @@
       CurrentUser user,
       Collection<ChangeData> changes)
       throws RestApiException, UpdateException {
-    batchAbandon(
-        updateFactory, project, user, changes, "", NotifyHandling.ALL, ImmutableListMultimap.of());
+    batchAbandon(updateFactory, project, user, changes, "", NotifyResolver.Result.all());
+  }
+
+  private void cleanupAccountPatchReview(Collection<ChangeData> changes) {
+    for (ChangeData change : changes) {
+      accountPatchReviewStore.run(s -> s.clearReviewed(change.getId()));
+    }
   }
 }
diff --git a/java/com/google/gerrit/server/change/ChangeAttributeFactory.java b/java/com/google/gerrit/server/change/ChangeAttributeFactory.java
new file mode 100644
index 0000000..95355cf
--- /dev/null
+++ b/java/com/google/gerrit/server/change/ChangeAttributeFactory.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.server.change;
+
+import com.google.gerrit.extensions.common.PluginDefinedInfo;
+import com.google.gerrit.server.DynamicOptions.BeanProvider;
+import com.google.gerrit.server.query.change.ChangeData;
+
+/**
+ * Interface for plugins to provide additional fields in {@link
+ * com.google.gerrit.extensions.common.ChangeInfo ChangeInfo}.
+ *
+ * <p>Register a {@code ChangeAttributeFactory} in a plugin {@code Module} like this:
+ *
+ * <pre>
+ * DynamicSet.bind(binder(), ChangeAttributeFactory.class).to(YourClass.class);
+ * </pre>
+ *
+ * <p>See the <a
+ * href="https://gerrit-review.googlesource.com/Documentation/dev-plugins.html#query_attributes">plugin
+ * developer documentation for more details and examples.
+ */
+public interface ChangeAttributeFactory {
+
+  /**
+   * Create a plugin-provided info field.
+   *
+   * <p>Typically, implementations will subclass {@code PluginDefinedInfo} to add additional fields.
+   *
+   * @param cd change.
+   * @param beanProvider provider of {@code DynamicBean}s, which may be used for reading options.
+   * @param plugin plugin name.
+   * @return the plugin's special change info.
+   */
+  PluginDefinedInfo create(ChangeData cd, BeanProvider beanProvider, String plugin);
+}
diff --git a/java/com/google/gerrit/server/change/ChangeCleanupRunner.java b/java/com/google/gerrit/server/change/ChangeCleanupRunner.java
index b24d3ce..0299f10 100644
--- a/java/com/google/gerrit/server/change/ChangeCleanupRunner.java
+++ b/java/com/google/gerrit/server/change/ChangeCleanupRunner.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.ManualRequestContext;
 import com.google.gerrit.server.util.OneOffRequestContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
 /** Runnable to enable scheduling change cleanups to run periodically */
@@ -85,7 +84,7 @@
             abandonUtil.abandonInactiveOpenChanges(updateFactory);
             return null;
           });
-    } catch (RestApiException | UpdateException | OrmException e) {
+    } catch (RestApiException | UpdateException e) {
       logger.atSevere().withCause(e).log("Failed to cleanup changes.");
     }
   }
diff --git a/java/com/google/gerrit/server/change/ChangeFinder.java b/java/com/google/gerrit/server/change/ChangeFinder.java
index 41d89ed..4ad3c67 100644
--- a/java/com/google/gerrit/server/change/ChangeFinder.java
+++ b/java/com/google/gerrit/server/change/ChangeFinder.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import com.google.common.primitives.Ints;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.restapi.DeprecatedIdentifierException;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.index.IndexConfig;
@@ -30,7 +31,6 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -38,7 +38,6 @@
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.Provider;
@@ -77,7 +76,6 @@
   private final IndexConfig indexConfig;
   private final Cache<Change.Id, String> changeIdProjectCache;
   private final Provider<InternalChangeQuery> queryProvider;
-  private final Provider<ReviewDb> reviewDb;
   private final ChangeNotes.Factory changeNotesFactory;
   private final Counter1<ChangeIdType> changeIdCounter;
   private final ImmutableSet<ChangeIdType> allowedIdTypes;
@@ -87,14 +85,12 @@
       IndexConfig indexConfig,
       @Named(CACHE_NAME) Cache<Change.Id, String> changeIdProjectCache,
       Provider<InternalChangeQuery> queryProvider,
-      Provider<ReviewDb> reviewDb,
       ChangeNotes.Factory changeNotesFactory,
       MetricMaker metricMaker,
       @GerritServerConfig Config config) {
     this.indexConfig = indexConfig;
     this.changeIdProjectCache = changeIdProjectCache;
     this.queryProvider = queryProvider;
-    this.reviewDb = reviewDb;
     this.changeNotesFactory = changeNotesFactory;
     this.changeIdCounter =
         metricMaker.newCounter(
@@ -110,7 +106,7 @@
     this.allowedIdTypes = ImmutableSet.copyOf(configuredChangeIdTypes);
   }
 
-  public ChangeNotes findOne(String id) throws OrmException {
+  public ChangeNotes findOne(String id) {
     List<ChangeNotes> ctls = find(id);
     if (ctls.size() != 1) {
       return null;
@@ -123,14 +119,13 @@
    *
    * @param id change identifier.
    * @return possibly-empty list of notes for all matching changes; may or may not be visible.
-   * @throws OrmException if an error occurred querying the database.
    */
-  public List<ChangeNotes> find(String id) throws OrmException {
+  public List<ChangeNotes> find(String id) {
     try {
       return find(id, false);
     } catch (DeprecatedIdentifierException e) {
       // This can't happen because we don't enforce deprecation
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
   }
 
@@ -141,11 +136,10 @@
    * @param enforceDeprecation boolean to see if we should throw {@link
    *     DeprecatedIdentifierException} in case the identifier is deprecated
    * @return possibly-empty list of notes for all matching changes; may or may not be visible.
-   * @throws OrmException if an error occurred querying the database
    * @throws DeprecatedIdentifierException if the identifier is deprecated.
    */
   public List<ChangeNotes> find(String id, boolean enforceDeprecation)
-      throws OrmException, DeprecatedIdentifierException {
+      throws DeprecatedIdentifierException {
     if (id.isEmpty()) {
       return Collections.emptyList();
     }
@@ -198,17 +192,16 @@
     return notes;
   }
 
-  private List<ChangeNotes> fromProjectNumber(String project, int changeNumber)
-      throws OrmException {
+  private List<ChangeNotes> fromProjectNumber(String project, int changeNumber) {
     Change.Id cId = new Change.Id(changeNumber);
     try {
       return ImmutableList.of(
-          changeNotesFactory.createChecked(reviewDb.get(), Project.NameKey.parse(project), cId));
+          changeNotesFactory.createChecked(Project.NameKey.parse(project), cId));
     } catch (NoSuchChangeException e) {
       return Collections.emptyList();
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       // Distinguish between a RepositoryNotFoundException (project argument invalid) and
-      // other OrmExceptions (failure in the persistence layer).
+      // other StorageExceptions (failure in the persistence layer).
       if (Throwables.getRootCause(e) instanceof RepositoryNotFoundException) {
         return Collections.emptyList();
       }
@@ -216,7 +209,7 @@
     }
   }
 
-  public ChangeNotes findOne(Change.Id id) throws OrmException {
+  public ChangeNotes findOne(Change.Id id) {
     List<ChangeNotes> notes = find(id);
     if (notes.size() != 1) {
       throw new NoSuchChangeException(id);
@@ -224,7 +217,7 @@
     return notes.get(0);
   }
 
-  public List<ChangeNotes> find(Change.Id id) throws OrmException {
+  public List<ChangeNotes> find(Change.Id id) {
     String project = changeIdProjectCache.getIfPresent(id);
     if (project != null) {
       return fromProjectNumber(project, id.get());
@@ -240,7 +233,7 @@
     return asChangeNotes(r);
   }
 
-  private List<ChangeNotes> asChangeNotes(List<ChangeData> cds) throws OrmException {
+  private List<ChangeNotes> asChangeNotes(List<ChangeData> cds) {
     List<ChangeNotes> notes = new ArrayList<>(cds.size());
     if (!indexConfig.separateChangeSubIndexes()) {
       for (ChangeData cd : cds) {
diff --git a/java/com/google/gerrit/server/change/ChangeInserter.java b/java/com/google/gerrit/server/change/ChangeInserter.java
index 80d05df..9f9ec1f 100644
--- a/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -24,16 +24,13 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Streams;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -45,7 +42,6 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.PatchSetUtil;
@@ -72,7 +68,6 @@
 import com.google.gerrit.server.update.InsertChangeOp;
 import com.google.gerrit.server.update.RepoContext;
 import com.google.gerrit.server.util.RequestScopePropagator;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
@@ -124,8 +119,6 @@
   private boolean workInProgress;
   private List<String> groups = Collections.emptyList();
   private boolean validate = true;
-  private NotifyHandling notify = NotifyHandling.ALL;
-  private ListMultimap<RecipientType, Account.Id> accountsToNotify = ImmutableListMultimap.of();
   private Map<String, Short> approvals;
   private RequestScopePropagator requestScopePropagator;
   private boolean fireRevisionCreated;
@@ -256,17 +249,6 @@
     return this;
   }
 
-  public ChangeInserter setNotify(NotifyHandling notify) {
-    this.notify = notify;
-    return this;
-  }
-
-  public ChangeInserter setAccountsToNotify(
-      ListMultimap<RecipientType, Account.Id> accountsToNotify) {
-    this.accountsToNotify = requireNonNull(accountsToNotify);
-    return this;
-  }
-
   public ChangeInserter setReviewersAndCcs(
       Iterable<Account.Id> reviewers, Iterable<Account.Id> ccs) {
     return setReviewersAndCcsAsStrings(
@@ -386,10 +368,8 @@
 
   @Override
   public boolean updateChange(ChangeContext ctx)
-      throws RestApiException, OrmException, IOException, PermissionBackendException,
-          ConfigInvalidException {
+      throws RestApiException, IOException, PermissionBackendException, ConfigInvalidException {
     change = ctx.getChange(); // Use defensive copy created by ChangeControl.
-    ReviewDb db = ctx.getDb();
     patchSetInfo =
         patchSetInfoFactory.get(ctx.getRevWalk(), ctx.getRevWalk().parseCommit(commitId), psId);
     ctx.getChange().setCurrentPatchSet(patchSetInfo);
@@ -412,14 +392,7 @@
     }
     patchSet =
         psUtil.insert(
-            ctx.getDb(),
-            ctx.getRevWalk(),
-            update,
-            psId,
-            commitId,
-            newGroups,
-            pushCert,
-            patchSetDescription);
+            ctx.getRevWalk(), update, psId, commitId, newGroups, pushCert, patchSetDescription);
 
     /* TODO: fixStatus is used here because the tests
      * (byStatusClosed() in AbstractQueryChangesTest)
@@ -432,8 +405,7 @@
     update.fixStatus(change.getStatus());
 
     reviewerAdditions =
-        reviewerAdder.prepare(
-            ctx.getDb(), ctx.getNotes(), ctx.getUser(), getReviewerInputs(), true);
+        reviewerAdder.prepare(ctx.getNotes(), ctx.getUser(), getReviewerInputs(), true);
     Optional<ReviewerAddition> reviewerError = reviewerAdditions.getFailures().stream().findFirst();
     if (reviewerError.isPresent()) {
       throw new UnprocessableEntityException(reviewerError.get().result.error);
@@ -442,7 +414,7 @@
 
     LabelTypes labelTypes = projectState.getLabelTypes();
     approvalsUtil.addApprovalsForNewPatchSet(
-        db, update, labelTypes, patchSet, ctx.getUser(), approvals);
+        update, labelTypes, patchSet, ctx.getUser(), approvals);
 
     // Check if approvals are changing in with this update. If so, add current user to reviewers.
     // Note that this is done separately as addReviewers is filtering out the change owner as
@@ -459,7 +431,7 @@
               patchSet.getCreatedOn(),
               message,
               ChangeMessagesUtil.uploadedPatchSetTag(workInProgress));
-      cmUtil.addChangeMessage(db, update, changeMessage);
+      cmUtil.addChangeMessage(update, changeMessage);
     }
     return true;
   }
@@ -467,7 +439,8 @@
   @Override
   public void postUpdate(Context ctx) throws Exception {
     reviewerAdditions.postUpdate(ctx);
-    if (sendMail && (notify != NotifyHandling.NONE || !accountsToNotify.isEmpty())) {
+    NotifyResolver.Result notify = ctx.getNotify(change.getId());
+    if (sendMail && notify.shouldNotify()) {
       Runnable sender =
           new Runnable() {
             @Override
@@ -478,7 +451,6 @@
                 cm.setFrom(change.getOwner());
                 cm.setPatchSet(patchSet, patchSetInfo);
                 cm.setNotify(notify);
-                cm.setAccountsToNotify(accountsToNotify);
                 cm.addReviewers(
                     reviewerAdditions.flattenResults(AddReviewersOp.Result::addedReviewers).stream()
                         .map(PatchSetApproval::getAccountId)
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index 2aaf4c5..1008a30 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -48,6 +48,7 @@
 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.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.FixInput;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.client.ReviewerState;
@@ -74,7 +75,6 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GpgException;
@@ -84,6 +84,7 @@
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.account.AccountInfoComparator;
 import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.notedb.ChangeNotes;
@@ -96,8 +97,6 @@
 import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeData.ChangedLines;
-import com.google.gerrit.server.query.change.PluginDefinedAttributesFactory;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -112,6 +111,7 @@
 import java.util.Optional;
 import java.util.Set;
 import java.util.function.Supplier;
+import org.eclipse.jgit.lib.Config;
 
 /**
  * Produces {@link ChangeInfo} (which is serialized to JSON afterwards) from {@link ChangeData}.
@@ -202,7 +202,6 @@
     }
   }
 
-  private final Provider<ReviewDb> db;
   private final Provider<CurrentUser> userProvider;
   private final PermissionBackend permissionBackend;
   private final ChangeData.Factory changeDataFactory;
@@ -218,6 +217,7 @@
   private final Metrics metrics;
   private final RevisionJson revisionJson;
   private final Optional<PluginDefinedAttributesFactory> pluginDefinedAttributesFactory;
+  private final boolean excludeMergeableInChangeInfo;
   private final boolean lazyLoad;
 
   private AccountLoader accountLoader;
@@ -225,7 +225,6 @@
 
   @Inject
   ChangeJson(
-      Provider<ReviewDb> db,
       Provider<CurrentUser> user,
       PermissionBackend permissionBackend,
       ChangeData.Factory cdf,
@@ -239,9 +238,9 @@
       TrackingFooters trackingFooters,
       Metrics metrics,
       RevisionJson.Factory revisionJsonFactory,
+      @GerritServerConfig Config cfg,
       @Assisted Iterable<ListChangesOption> options,
       @Assisted Optional<PluginDefinedAttributesFactory> pluginDefinedAttributesFactory) {
-    this.db = db;
     this.userProvider = user;
     this.changeDataFactory = cdf;
     this.permissionBackend = permissionBackend;
@@ -256,6 +255,8 @@
     this.metrics = metrics;
     this.revisionJson = revisionJsonFactory.create(options);
     this.options = Sets.immutableEnumSet(options);
+    this.excludeMergeableInChangeInfo =
+        cfg.getBoolean("change", "api", "excludeMergeableInChangeInfo", false);
     this.lazyLoad = containsAnyOf(this.options, REQUIRE_LAZY_LOAD);
     this.pluginDefinedAttributesFactory = pluginDefinedAttributesFactory;
 
@@ -267,24 +268,24 @@
     return this;
   }
 
-  public ChangeInfo format(ChangeResource rsrc) throws OrmException {
-    return format(changeDataFactory.create(db.get(), rsrc.getNotes()));
+  public ChangeInfo format(ChangeResource rsrc) {
+    return format(changeDataFactory.create(rsrc.getNotes()));
   }
 
-  public ChangeInfo format(Change change) throws OrmException {
-    return format(changeDataFactory.create(db.get(), change));
+  public ChangeInfo format(Change change) {
+    return format(changeDataFactory.create(change));
   }
 
-  public ChangeInfo format(Project.NameKey project, Change.Id id) throws OrmException {
+  public ChangeInfo format(Project.NameKey project, Change.Id id) {
     return format(project, id, ChangeInfo::new);
   }
 
-  public ChangeInfo format(ChangeData cd) throws OrmException {
+  public ChangeInfo format(ChangeData cd) {
     return format(cd, Optional.empty(), true, ChangeInfo::new);
   }
 
-  public ChangeInfo format(RevisionResource rsrc) throws OrmException {
-    ChangeData cd = changeDataFactory.create(db.get(), rsrc.getNotes());
+  public ChangeInfo format(RevisionResource rsrc) {
+    ChangeData cd = changeDataFactory.create(rsrc.getNotes());
     return format(cd, Optional.of(rsrc.getPatchSet().getId()), true, ChangeInfo::new);
   }
 
@@ -306,8 +307,7 @@
     }
   }
 
-  public List<ChangeInfo> format(Collection<ChangeData> in)
-      throws OrmException, PermissionBackendException {
+  public List<ChangeInfo> format(Collection<ChangeData> in) throws PermissionBackendException {
     accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
     ensureLoaded(in);
     List<ChangeInfo> out = new ArrayList<>(in.size());
@@ -319,18 +319,17 @@
   }
 
   public <I extends ChangeInfo> I format(
-      Project.NameKey project, Change.Id id, Supplier<I> changeInfoSupplier) throws OrmException {
+      Project.NameKey project, Change.Id id, Supplier<I> changeInfoSupplier) {
     ChangeNotes notes;
     try {
-      notes = notesFactory.createChecked(db.get(), project, id);
-    } catch (OrmException e) {
+      notes = notesFactory.createChecked(project, id);
+    } catch (StorageException e) {
       if (!has(CHECK)) {
         throw e;
       }
-      return checkOnly(changeDataFactory.create(db.get(), project, id), changeInfoSupplier);
+      return checkOnly(changeDataFactory.create(project, id), changeInfoSupplier);
     }
-    return format(
-        changeDataFactory.create(db.get(), notes), Optional.empty(), true, changeInfoSupplier);
+    return format(changeDataFactory.create(notes), Optional.empty(), true, changeInfoSupplier);
   }
 
   private static Collection<SubmitRequirementInfo> requirementsFor(ChangeData cd) {
@@ -365,8 +364,7 @@
       ChangeData cd,
       Optional<PatchSet.Id> limitToPsId,
       boolean fillAccountLoader,
-      Supplier<I> changeInfoSupplier)
-      throws OrmException {
+      Supplier<I> changeInfoSupplier) {
     try {
       if (fillAccountLoader) {
         accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
@@ -377,19 +375,18 @@
       return toChangeInfo(cd, limitToPsId, changeInfoSupplier);
     } catch (PatchListNotAvailableException
         | GpgException
-        | OrmException
         | IOException
         | PermissionBackendException
         | RuntimeException e) {
       if (!has(CHECK)) {
-        Throwables.throwIfInstanceOf(e, OrmException.class);
-        throw new OrmException(e);
+        Throwables.throwIfInstanceOf(e, StorageException.class);
+        throw new StorageException(e);
       }
       return checkOnly(cd, changeInfoSupplier);
     }
   }
 
-  private void ensureLoaded(Iterable<ChangeData> all) throws OrmException {
+  private void ensureLoaded(Iterable<ChangeData> all) {
     if (lazyLoad) {
       ChangeData.ensureChangeLoaded(all);
       if (has(ALL_REVISIONS)) {
@@ -439,7 +436,7 @@
           if (isCacheable) {
             cache.put(new Change.Id(info._number), info);
           }
-        } catch (OrmException | RuntimeException e) {
+        } catch (RuntimeException e) {
           logger.atWarning().withCause(e).log(
               "Omitting corrupt change %s from results", cd.getId());
         }
@@ -452,7 +449,7 @@
     ChangeNotes notes;
     try {
       notes = cd.notes();
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       String msg = "Error loading change";
       logger.atWarning().withCause(e).log(msg + " %s", cd.getId());
       I info = changeInfoSupplier.get();
@@ -491,8 +488,7 @@
 
   private <I extends ChangeInfo> I toChangeInfo(
       ChangeData cd, Optional<PatchSet.Id> limitToPsId, Supplier<I> changeInfoSupplier)
-      throws PatchListNotAvailableException, GpgException, OrmException, PermissionBackendException,
-          IOException {
+      throws PatchListNotAvailableException, GpgException, PermissionBackendException, IOException {
     try (Timer0.Context ignored = metrics.toChangeInfoLatency.start()) {
       return toChangeInfoImpl(cd, limitToPsId, changeInfoSupplier);
     }
@@ -500,8 +496,7 @@
 
   private <I extends ChangeInfo> I toChangeInfoImpl(
       ChangeData cd, Optional<PatchSet.Id> limitToPsId, Supplier<I> changeInfoSupplier)
-      throws PatchListNotAvailableException, GpgException, OrmException, PermissionBackendException,
-          IOException {
+      throws PatchListNotAvailableException, GpgException, PermissionBackendException, IOException {
     I out = changeInfoSupplier.get();
     CurrentUser user = userProvider.get();
 
@@ -510,7 +505,7 @@
       // If any problems were fixed, the ChangeData needs to be reloaded.
       for (ProblemInfo p : out.problems) {
         if (p.status == ProblemInfo.Status.FIXED) {
-          cd = changeDataFactory.create(cd.db(), cd.project(), cd.getId());
+          cd = changeDataFactory.create(cd.project(), cd.getId());
           break;
         }
       }
@@ -523,12 +518,12 @@
     out.assignee = in.getAssignee() != null ? accountLoader.get(in.getAssignee()) : null;
     out.hashtags = cd.hashtags();
     out.changeId = in.getKey().get();
-    if (in.getStatus().isOpen()) {
+    if (in.isNew()) {
       SubmitTypeRecord str = cd.submitTypeRecord();
       if (str.isOk()) {
         out.submitType = str.type;
       }
-      if (!has(SKIP_MERGEABLE)) {
+      if (!excludeMergeableInChangeInfo && !has(SKIP_MERGEABLE)) {
         out.mergeable = cd.isMergeable();
       }
       if (has(SUBMITTABLE)) {
@@ -549,6 +544,7 @@
     out.created = in.getCreatedOn();
     out.updated = in.getLastUpdatedOn();
     out._number = in.getId().get();
+    out.totalCommentCount = cd.totalCommentCount();
     out.unresolvedCommentCount = cd.unresolvedCommentCount();
 
     if (user.isIdentifiedUser()) {
@@ -559,7 +555,7 @@
       }
     }
 
-    if (in.getStatus().isOpen() && has(REVIEWED) && user.isIdentifiedUser()) {
+    if (in.isNew() && has(REVIEWED) && user.isIdentifiedUser()) {
       out.reviewed = cd.isReviewedBy(user.getAccountId()) ? true : null;
     }
 
@@ -572,7 +568,7 @@
       if (user.isIdentifiedUser()
           && (!limitToPsId.isPresent() || limitToPsId.get().equals(in.currentPatchSetId()))) {
         out.permittedLabels =
-            cd.change().getStatus() != Change.Status.ABANDONED
+            !cd.change().isAbandoned()
                 ? labelsJson.permittedLabels(user.getAccountId(), cd)
                 : ImmutableMap.of();
       }
@@ -651,7 +647,7 @@
     return reviewerMap;
   }
 
-  private Collection<ReviewerUpdateInfo> reviewerUpdates(ChangeData cd) throws OrmException {
+  private Collection<ReviewerUpdateInfo> reviewerUpdates(ChangeData cd) {
     List<ReviewerStatusUpdate> reviewerUpdates = cd.reviewerUpdates();
     List<ReviewerUpdateInfo> result = new ArrayList<>(reviewerUpdates.size());
     for (ReviewerStatusUpdate c : reviewerUpdates) {
@@ -669,7 +665,7 @@
     return SubmitRecord.allRecordsOK(cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT));
   }
 
-  private void setSubmitter(ChangeData cd, ChangeInfo out) throws OrmException {
+  private void setSubmitter(ChangeData cd, ChangeInfo out) {
     Optional<PatchSetApproval> s = cd.getSubmitApproval();
     if (!s.isPresent()) {
       return;
@@ -678,8 +674,8 @@
     out.submitter = accountLoader.get(s.get().getAccountId());
   }
 
-  private Collection<ChangeMessageInfo> messages(ChangeData cd) throws OrmException {
-    List<ChangeMessage> messages = cmUtil.byChange(db.get(), cd.notes());
+  private Collection<ChangeMessageInfo> messages(ChangeData cd) {
+    List<ChangeMessage> messages = cmUtil.byChange(cd.notes());
     if (messages.isEmpty()) {
       return Collections.emptyList();
     }
@@ -692,7 +688,7 @@
   }
 
   private Collection<AccountInfo> removableReviewers(ChangeData cd, ChangeInfo out)
-      throws PermissionBackendException, OrmException {
+      throws PermissionBackendException {
     // Although this is called removableReviewers, this method also determines
     // which CCs are removable.
     //
@@ -780,8 +776,8 @@
         .collect(toList());
   }
 
-  private Map<PatchSet.Id, PatchSet> loadPatchSets(ChangeData cd, Optional<PatchSet.Id> limitToPsId)
-      throws OrmException {
+  private Map<PatchSet.Id, PatchSet> loadPatchSets(
+      ChangeData cd, Optional<PatchSet.Id> limitToPsId) {
     Collection<PatchSet> src;
     if (has(ALL_REVISIONS) || has(MESSAGES)) {
       src = cd.patchSets();
@@ -790,12 +786,12 @@
       if (limitToPsId.isPresent()) {
         ps = cd.patchSet(limitToPsId.get());
         if (ps == null) {
-          throw new OrmException("missing patch set " + limitToPsId.get());
+          throw new StorageException("missing patch set " + limitToPsId.get());
         }
       } else {
         ps = cd.currentPatchSet();
         if (ps == null) {
-          throw new OrmException("missing current patch set for change " + cd.getId());
+          throw new StorageException("missing current patch set for change " + cd.getId());
         }
       }
       src = Collections.singletonList(ps);
@@ -812,9 +808,8 @@
    *     from either an index-backed or a database-backed {@link ChangeData} depending on {@code
    *     lazyload}.
    */
-  private PermissionBackend.ForChange permissionBackendForChange(CurrentUser user, ChangeData cd)
-      throws OrmException {
-    PermissionBackend.WithUser withUser = permissionBackend.user(user).database(db);
+  private PermissionBackend.ForChange permissionBackendForChange(CurrentUser user, ChangeData cd) {
+    PermissionBackend.WithUser withUser = permissionBackend.user(user);
     return lazyLoad
         ? withUser.change(cd)
         : withUser.indexedChange(cd, notesFactory.createFromIndexedChange(cd.change()));
diff --git a/java/com/google/gerrit/server/change/ChangeKindCache.java b/java/com/google/gerrit/server/change/ChangeKindCache.java
index 6baeefc..44da4d6 100644
--- a/java/com/google/gerrit/server/change/ChangeKindCache.java
+++ b/java/com/google/gerrit/server/change/ChangeKindCache.java
@@ -19,7 +19,6 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.query.change.ChangeData;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
@@ -39,7 +38,7 @@
       ObjectId prior,
       ObjectId next);
 
-  ChangeKind getChangeKind(ReviewDb db, Change change, PatchSet patch);
+  ChangeKind getChangeKind(Change change, PatchSet patch);
 
   ChangeKind getChangeKind(
       @Nullable RevWalk rw, @Nullable Config repoConfig, ChangeData cd, PatchSet patch);
diff --git a/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java b/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
index d96229e..a80f48b 100644
--- a/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
+++ b/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
@@ -23,23 +23,22 @@
 import com.google.common.collect.FluentIterable;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.cache.proto.Cache.ChangeKindKeyProto;
 import com.google.gerrit.server.cache.serialize.CacheSerializer;
 import com.google.gerrit.server.cache.serialize.EnumCacheSerializer;
-import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers;
-import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers.ObjectIdConverter;
+import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
 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.MergeUtil;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.name.Named;
@@ -80,7 +79,6 @@
     };
   }
 
-  @VisibleForTesting
   public static class NoCache implements ChangeKindCache {
     private final boolean useRecursiveMerge;
     private final ChangeData.Factory changeDataFactory;
@@ -114,8 +112,8 @@
     }
 
     @Override
-    public ChangeKind getChangeKind(ReviewDb db, Change change, PatchSet patch) {
-      return getChangeKindInternal(this, db, change, patch, changeDataFactory, repoManager);
+    public ChangeKind getChangeKind(Change change, PatchSet patch) {
+      return getChangeKindInternal(this, change, patch, changeDataFactory, repoManager);
     }
 
     @Override
@@ -146,7 +144,7 @@
       @Override
       public byte[] serialize(Key object) {
         ObjectIdConverter idConverter = ObjectIdConverter.create();
-        return ProtoCacheSerializers.toByteArray(
+        return Protos.toByteArray(
             ChangeKindKeyProto.newBuilder()
                 .setPrior(idConverter.toByteString(object.prior()))
                 .setNext(idConverter.toByteString(object.next()))
@@ -156,8 +154,7 @@
 
       @Override
       public Key deserialize(byte[] in) {
-        ChangeKindKeyProto proto =
-            ProtoCacheSerializers.parseUnchecked(ChangeKindKeyProto.parser(), in);
+        ChangeKindKeyProto proto = Protos.parseUnchecked(ChangeKindKeyProto.parser(), in);
         ObjectIdConverter idConverter = ObjectIdConverter.create();
         return create(
             idConverter.fromByteString(proto.getPrior()),
@@ -356,8 +353,8 @@
   }
 
   @Override
-  public ChangeKind getChangeKind(ReviewDb db, Change change, PatchSet patch) {
-    return getChangeKindInternal(this, db, change, patch, changeDataFactory, repoManager);
+  public ChangeKind getChangeKind(Change change, PatchSet patch) {
+    return getChangeKindInternal(this, change, patch, changeDataFactory, repoManager);
   }
 
   @Override
@@ -400,7 +397,7 @@
                   ObjectId.fromString(priorPs.getRevision().get()),
                   ObjectId.fromString(patch.getRevision().get()));
         }
-      } catch (OrmException e) {
+      } 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",
@@ -412,7 +409,6 @@
 
   private static ChangeKind getChangeKindInternal(
       ChangeKindCache cache,
-      ReviewDb db,
       Change change,
       PatchSet patch,
       ChangeData.Factory changeDataFactory,
@@ -426,7 +422,7 @@
           RevWalk rw = new RevWalk(repo)) {
         kind =
             getChangeKindInternal(
-                cache, rw, repo.getConfig(), changeDataFactory.create(db, change), patch);
+                cache, rw, repo.getConfig(), changeDataFactory.create(change), patch);
       } catch (IOException e) {
         // Do nothing; assume we have a complex change
         logger.atWarning().withCause(e).log(
diff --git a/java/com/google/gerrit/server/change/ChangeMessages.java b/java/com/google/gerrit/server/change/ChangeMessages.java
index 41b6855..6cd3726 100644
--- a/java/com/google/gerrit/server/change/ChangeMessages.java
+++ b/java/com/google/gerrit/server/change/ChangeMessages.java
@@ -25,9 +25,7 @@
   public String revertChangeDefaultMessage;
 
   public String reviewerCantSeeChange;
-  public String reviewerInactive;
   public String reviewerInvalid;
-  public String reviewerNotFoundUser;
   public String reviewerNotFoundUserOrGroup;
 
   public String groupIsNotAllowed;
diff --git a/java/com/google/gerrit/server/change/ChangeResource.java b/java/com/google/gerrit/server/change/ChangeResource.java
index d61d56f..98b728f 100644
--- a/java/com/google/gerrit/server/change/ChangeResource.java
+++ b/java/com/google/gerrit/server/change/ChangeResource.java
@@ -21,6 +21,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.common.hash.Hasher;
 import com.google.common.hash.Hashing;
+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;
@@ -29,7 +30,6 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.PatchSetUtil;
@@ -40,9 +40,7 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.TypeLiteral;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
@@ -71,7 +69,6 @@
 
   private static final String ZERO_ID_STRING = ObjectId.zeroId().name();
 
-  private final Provider<ReviewDb> db;
   private final AccountCache accountCache;
   private final ApprovalsUtil approvalUtil;
   private final PatchSetUtil patchSetUtil;
@@ -83,7 +80,6 @@
 
   @Inject
   ChangeResource(
-      Provider<ReviewDb> db,
       AccountCache accountCache,
       ApprovalsUtil approvalUtil,
       PatchSetUtil patchSetUtil,
@@ -92,7 +88,6 @@
       ProjectCache projectCache,
       @Assisted ChangeNotes notes,
       @Assisted CurrentUser user) {
-    this.db = db;
     this.accountCache = accountCache;
     this.approvalUtil = approvalUtil;
     this.patchSetUtil = patchSetUtil;
@@ -104,7 +99,7 @@
   }
 
   public PermissionBackend.ForChange permissions() {
-    return permissionBackend.user(user).database(db).change(notes);
+    return permissionBackend.user(user).change(notes);
   }
 
   public CurrentUser getUser() {
@@ -154,9 +149,7 @@
       accounts.add(getChange().getAssignee());
     }
     try {
-      patchSetUtil.byChange(db.get(), notes).stream()
-          .map(PatchSet::getUploader)
-          .forEach(accounts::add);
+      patchSetUtil.byChange(notes).stream().map(PatchSet::getUploader).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.
@@ -165,8 +158,8 @@
       // set of accounts that posted a message is too expensive. However everyone who posts a
       // message is automatically added as reviewer. Hence if we include removed reviewers we can
       // be sure that we have all accounts that posted messages on the change.
-      accounts.addAll(approvalUtil.getReviewers(db.get(), notes).all());
-    } catch (OrmException e) {
+      accounts.addAll(approvalUtil.getReviewers(notes).all());
+    } catch (StorageException e) {
       // This ETag will be invalidated if it loads next time.
     }
 
@@ -182,7 +175,7 @@
     ObjectId noteId;
     try {
       noteId = notes.loadRevision();
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       noteId = null; // This ETag will be invalidated if it loads next time.
     }
     hashObjectId(h, noteId, buf);
diff --git a/java/com/google/gerrit/server/change/ConsistencyChecker.java b/java/com/google/gerrit/server/change/ConsistencyChecker.java
index 128297f..80b7190 100644
--- a/java/com/google/gerrit/server/change/ConsistencyChecker.java
+++ b/java/com/google/gerrit/server/change/ConsistencyChecker.java
@@ -15,9 +15,10 @@
 package com.google.gerrit.server.change;
 
 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.reviewdb.server.ReviewDbUtil.intKeyOrdering;
 import static com.google.gerrit.server.ChangeUtil.PS_ID_ORDER;
+import static java.util.Comparator.comparing;
 import static java.util.Objects.requireNonNull;
 
 import com.google.auto.value.AutoValue;
@@ -29,16 +30,14 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.FixInput;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
 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.reviewdb.server.ReviewDb;
-import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
@@ -52,13 +51,11 @@
 import com.google.gerrit.server.plugincontext.PluginItemContext;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
-import com.google.gerrit.server.update.BatchUpdateReviewDb;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.RepoContext;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -67,8 +64,10 @@
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
+import java.util.TreeSet;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
@@ -114,7 +113,6 @@
   private final PatchSetUtil psUtil;
   private final Provider<CurrentUser> user;
   private final Provider<PersonIdent> serverIdent;
-  private final Provider<ReviewDb> db;
   private final RetryHelper retryHelper;
 
   private BatchUpdate.Factory updateFactory;
@@ -142,11 +140,9 @@
       PatchSetInserter.Factory patchSetInserterFactory,
       PatchSetUtil psUtil,
       Provider<CurrentUser> user,
-      Provider<ReviewDb> db,
       RetryHelper retryHelper) {
     this.accounts = accounts;
     this.accountPatchReviewStore = accountPatchReviewStore;
-    this.db = db;
     this.notesFactory = notesFactory;
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.patchSetInserterFactory = patchSetInserterFactory;
@@ -231,12 +227,12 @@
 
   private void checkCurrentPatchSetEntity() {
     try {
-      currPs = psUtil.current(db.get(), notes);
+      currPs = psUtil.current(notes);
       if (currPs == null) {
         problem(
             String.format("Current patch set %d not found", change().currentPatchSetId().get()));
       }
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       error("Failed to look up current patch set", e);
     }
   }
@@ -259,8 +255,8 @@
     List<PatchSet> all;
     try {
       // Iterate in descending order.
-      all = PS_ID_ORDER.sortedCopy(psUtil.byChange(db.get(), notes));
-    } catch (OrmException e) {
+      all = PS_ID_ORDER.sortedCopy(psUtil.byChange(notes));
+    } catch (StorageException e) {
       return error("Failed to look up patch sets", e);
     }
     patchSetsBySha = MultimapBuilder.hashKeys(all.size()).treeSetValues(PS_ID_ORDER).build();
@@ -365,28 +361,39 @@
   private ProblemInfo wrongChangeStatus(PatchSet.Id psId, RevCommit commit) {
     String refName = change().getDest().get();
     return problem(
-        String.format(
+        formatProblemMessage(
             "Patch set %d (%s) is merged into destination ref %s (%s), but change"
                 + " status is %s",
-            psId.get(), commit.name(), refName, tip.name(), change().getStatus()));
+            psId.get(), commit.name(), refName, tip.name()));
   }
 
   private void checkMergedBitMatchesStatus(PatchSet.Id psId, RevCommit commit, boolean merged) {
     String refName = change().getDest().get();
-    if (merged && change().getStatus() != Change.Status.MERGED) {
+    if (merged && !change().isMerged()) {
       ProblemInfo p = wrongChangeStatus(psId, commit);
       if (fix != null) {
         fixMerged(p);
       }
-    } else if (!merged && change().getStatus() == Change.Status.MERGED) {
+    } else if (!merged && change().isMerged()) {
       problem(
-          String.format(
+          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(), change().getStatus()));
+              currPs.getId().get(), commit.name(), refName, tip.name()));
     }
   }
 
+  private String formatProblemMessage(
+      String message, int psId, String commitName, String refName, String tipName) {
+    return String.format(
+        message,
+        psId,
+        commitName,
+        refName,
+        tipName,
+        ChangeUtil.status(change()).toUpperCase(Locale.US));
+  }
+
   private void checkExpectMergedAs() {
     ObjectId objId = parseObjectId(fix.expectMergedAs, "expected merged commit");
     RevCommit commit = parseCommit(objId, "expected merged commit");
@@ -414,13 +421,11 @@
         }
         try {
           Change c =
-              notesFactory
-                  .createChecked(db.get(), change().getProject(), psId.getParentKey())
-                  .getChange();
+              notesFactory.createChecked(change().getProject(), psId.getParentKey()).getChange();
           if (!c.getDest().equals(change().getDest())) {
             continue;
           }
-        } catch (OrmException e) {
+        } catch (StorageException e) {
           warn(e);
           // Include this patch set; should cause an error below, which is good.
         }
@@ -464,7 +469,10 @@
           problem(
               String.format(
                   "Multiple patch sets for expected merged commit %s: %s",
-                  commit.name(), intKeyOrdering().sortedCopy(thisCommitPsIds)));
+                  commit.name(),
+                  thisCommitPsIds.stream()
+                      .sorted(comparing(PatchSet.Id::get))
+                      .collect(toImmutableList())));
           break;
       }
     } catch (IOException e) {
@@ -534,21 +542,21 @@
           }
         }
 
+        bu.setNotify(NotifyResolver.Result.none());
         bu.addOp(
             notes.getChangeId(),
             inserter
                 .setValidate(false)
                 .setFireRevisionCreated(false)
-                .setNotify(NotifyHandling.NONE)
                 .setAllowClosed(true)
                 .setMessage("Patch set for merged commit inserted by consistency checker"));
         bu.addOp(notes.getChangeId(), new FixMergedOp(notFound));
         bu.execute();
       }
-      notes = notesFactory.createChecked(db.get(), inserter.getChange());
+      notes = notesFactory.createChecked(inserter.getChange());
       insertPatchSetProblem.status = Status.FIXED;
       insertPatchSetProblem.outcome = "Inserted as patch set " + psId.get();
-    } catch (OrmException | IOException | UpdateException | RestApiException e) {
+    } catch (StorageException | IOException | UpdateException | RestApiException e) {
       warn(e);
       for (ProblemInfo pi : currProblems) {
         pi.status = Status.FIX_FAILED;
@@ -566,7 +574,7 @@
     }
 
     @Override
-    public boolean updateChange(ChangeContext ctx) throws OrmException {
+    public boolean updateChange(ChangeContext ctx) {
       ctx.getChange().setStatus(Change.Status.MERGED);
       ctx.getUpdate(ctx.getChange().currentPatchSetId()).fixStatus(Change.Status.MERGED);
       p.status = Status.FIXED;
@@ -588,7 +596,7 @@
   }
 
   private BatchUpdate newBatchUpdate() {
-    return updateFactory.create(db.get(), change().getProject(), user.get(), TimeUtil.nowTs());
+    return updateFactory.create(change().getProject(), user.get(), TimeUtil.nowTs());
   }
 
   private void fixPatchSetRef(ProblemInfo p, PatchSet ps) {
@@ -664,18 +672,11 @@
     }
 
     @Override
-    public boolean updateChange(ChangeContext ctx)
-        throws OrmException, PatchSetInfoNotAvailableException {
+    public boolean updateChange(ChangeContext ctx) throws PatchSetInfoNotAvailableException {
       // Delete dangling key references.
-      ReviewDb db = BatchUpdateReviewDb.unwrap(ctx.getDb());
-      accountPatchReviewStore.run(s -> s.clearReviewed(psId), OrmException.class);
-      db.changeMessages().delete(db.changeMessages().byChange(psId.getParentKey()));
-      db.patchSetApprovals().delete(db.patchSetApprovals().byPatchSet(psId));
-      db.patchComments().delete(db.patchComments().byPatchSet(psId));
-      db.patchSets().deleteKeys(Collections.singleton(psId));
+      accountPatchReviewStore.run(s -> s.clearReviewed(psId));
 
-      // NoteDb requires no additional fiddling; setting the state to deleted is
-      // sufficient to filter everything else out.
+      // For NoteDb setting the state to deleted is sufficient to filter everything out.
       ctx.getUpdate(psId).setPatchSetState(PatchSetState.DELETED);
 
       p.status = Status.FIXED;
@@ -704,15 +705,15 @@
 
     @Override
     public boolean updateChange(ChangeContext ctx)
-        throws OrmException, PatchSetInfoNotAvailableException, NoPatchSetsWouldRemainException {
+        throws PatchSetInfoNotAvailableException, NoPatchSetsWouldRemainException {
       if (!toDelete.contains(ctx.getChange().currentPatchSetId())) {
         return false;
       }
-      Set<PatchSet.Id> all = new HashSet<>();
+      TreeSet<PatchSet.Id> all = new TreeSet<>(comparing(PatchSet.Id::get));
       // Doesn't make any assumptions about the order in which deletes happen
       // 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.getDb(), ctx.getNotes())) {
+      for (PatchSet ps : psUtil.byChange(ctx.getNotes())) {
         if (!toDelete.contains(ps.getId())) {
           all.add(ps.getId());
         }
@@ -720,9 +721,7 @@
       if (all.isEmpty()) {
         throw new NoPatchSetsWouldRemainException();
       }
-      PatchSet.Id latest = ReviewDbUtil.intKeyOrdering().max(all);
-      ctx.getChange()
-          .setCurrentPatchSet(patchSetInfoFactory.get(ctx.getDb(), ctx.getNotes(), latest));
+      ctx.getChange().setCurrentPatchSet(patchSetInfoFactory.get(ctx.getNotes(), all.last()));
       return true;
     }
   }
diff --git a/java/com/google/gerrit/server/change/DeleteChangeOp.java b/java/com/google/gerrit/server/change/DeleteChangeOp.java
index 461274b..ec20e87 100644
--- a/java/com/google/gerrit/server/change/DeleteChangeOp.java
+++ b/java/com/google/gerrit/server/change/DeleteChangeOp.java
@@ -14,26 +14,21 @@
 
 package com.google.gerrit.server.change;
 
-import static com.google.common.base.Preconditions.checkState;
-
 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.reviewdb.server.ReviewDb;
 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.BatchUpdateReviewDb;
 import com.google.gerrit.server.update.ChangeContext;
-import com.google.gerrit.server.update.Order;
 import com.google.gerrit.server.update.RepoContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
 import java.util.Collection;
 import java.util.Map;
@@ -42,40 +37,41 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 
 public class DeleteChangeOp implements BatchUpdateOp {
+  public interface Factory {
+    DeleteChangeOp create(Change.Id id);
+  }
+
   private final PatchSetUtil psUtil;
   private final StarredChangesUtil starredChangesUtil;
   private final PluginItemContext<AccountPatchReviewStore> accountPatchReviewStore;
   private final ChangeDeleted changeDeleted;
-
-  private Change.Id id;
+  private final Change.Id id;
 
   @Inject
   DeleteChangeOp(
       PatchSetUtil psUtil,
       StarredChangesUtil starredChangesUtil,
       PluginItemContext<AccountPatchReviewStore> accountPatchReviewStore,
-      ChangeDeleted changeDeleted) {
+      ChangeDeleted changeDeleted,
+      @Assisted Change.Id id) {
     this.psUtil = psUtil;
     this.starredChangesUtil = starredChangesUtil;
     this.accountPatchReviewStore = accountPatchReviewStore;
     this.changeDeleted = changeDeleted;
+    this.id = id;
   }
 
+  // The relative order of updateChange and updateRepo doesn't matter as long as all operations are
+  // executed in a single atomic BatchRefUpdate. Actually deleting the change refs first would not
+  // fail gracefully if the second delete fails, but fortunately that's not what happens.
   @Override
-  public boolean updateChange(ChangeContext ctx)
-      throws RestApiException, OrmException, IOException, NoSuchChangeException {
-    checkState(
-        ctx.getOrder() == Order.DB_BEFORE_REPO, "must use DeleteChangeOp with DB_BEFORE_REPO");
-    checkState(id == null, "cannot reuse DeleteChangeOp");
-
-    id = ctx.getChange().getId();
-    Collection<PatchSet> patchSets = psUtil.byChange(ctx.getDb(), ctx.getNotes());
+  public boolean updateChange(ChangeContext ctx) throws RestApiException, IOException {
+    Collection<PatchSet> patchSets = psUtil.byChange(ctx.getNotes());
 
     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, patchSets);
-    deleteChangeElementsFromDb(ctx, id);
+    cleanUpReferences(ctx, id);
 
     ctx.deleteChange();
     changeDeleted.fire(ctx.getChange(), ctx.getAccount(), ctx.getWhen());
@@ -84,8 +80,7 @@
 
   private void ensureDeletable(ChangeContext ctx, Change.Id id, Collection<PatchSet> patchSets)
       throws ResourceConflictException, MethodNotAllowedException, IOException {
-    Change.Status status = ctx.getChange().getStatus();
-    if (status == Change.Status.MERGED) {
+    if (ctx.getChange().isMerged()) {
       throw new MethodNotAllowedException("Deleting merged change " + id + " is not allowed");
     }
     for (PatchSet patchSet : patchSets) {
@@ -109,25 +104,8 @@
     return revWalk.isMergedInto(revWalk.parseCommit(objectId), revWalk.parseCommit(destId.get()));
   }
 
-  private void deleteChangeElementsFromDb(ChangeContext ctx, Change.Id id) throws OrmException {
-    // Only delete from ReviewDb here; deletion from NoteDb is handled in
-    // BatchUpdate.
-    //
-    // This is special. We want to delete exactly the rows that are present in
-    // the database, even when reading everything else from NoteDb, so we need
-    // to bypass the write-only wrapper.
-    ReviewDb db = BatchUpdateReviewDb.unwrap(ctx.getDb());
-    db.patchComments().delete(db.patchComments().byChange(id));
-    db.patchSetApprovals().delete(db.patchSetApprovals().byChange(id));
-    db.patchSets().delete(db.patchSets().byChange(id));
-    db.changeMessages().delete(db.changeMessages().byChange(id));
-  }
-
-  private void cleanUpReferences(ChangeContext ctx, Change.Id id, Collection<PatchSet> patchSets)
-      throws OrmException, NoSuchChangeException {
-    for (PatchSet ps : patchSets) {
-      accountPatchReviewStore.run(s -> s.clearReviewed(ps.getId()), OrmException.class);
-    }
+  private void cleanUpReferences(ChangeContext ctx, Change.Id id) throws NoSuchChangeException {
+    accountPatchReviewStore.run(s -> s.clearReviewed(id));
 
     // Non-atomic operation on Accounts table; not much we can do to make it
     // atomic.
diff --git a/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java b/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java
index ad4f4af..4b5572b 100644
--- a/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java
+++ b/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java
@@ -15,8 +15,6 @@
 package com.google.gerrit.server.change;
 
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -26,7 +24,6 @@
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.util.Collections;
@@ -35,31 +32,24 @@
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public interface Factory {
-    DeleteReviewerByEmailOp create(Address reviewer, DeleteReviewerInput input);
+    DeleteReviewerByEmailOp create(Address reviewer);
   }
 
   private final DeleteReviewerSender.Factory deleteReviewerSenderFactory;
-  private final NotifyUtil notifyUtil;
   private final Address reviewer;
-  private final DeleteReviewerInput input;
 
   private ChangeMessage changeMessage;
   private Change change;
 
   @Inject
   DeleteReviewerByEmailOp(
-      DeleteReviewerSender.Factory deleteReviewerSenderFactory,
-      NotifyUtil notifyUtil,
-      @Assisted Address reviewer,
-      @Assisted DeleteReviewerInput input) {
+      DeleteReviewerSender.Factory deleteReviewerSenderFactory, @Assisted Address reviewer) {
     this.deleteReviewerSenderFactory = deleteReviewerSenderFactory;
-    this.notifyUtil = notifyUtil;
     this.reviewer = reviewer;
-    this.input = input;
   }
 
   @Override
-  public boolean updateChange(ChangeContext ctx) throws OrmException {
+  public boolean updateChange(ChangeContext ctx) {
     change = ctx.getChange();
     PatchSet.Id psId = ctx.getChange().currentPatchSetId();
     String msg = "Removed reviewer " + reviewer;
@@ -78,24 +68,17 @@
 
   @Override
   public void postUpdate(Context ctx) {
-    if (input.notify == null) {
-      if (change.isWorkInProgress()) {
-        input.notify = NotifyHandling.NONE;
-      } else {
-        input.notify = NotifyHandling.ALL;
-      }
-    }
-    if (!NotifyUtil.shouldNotify(input.notify, input.notifyDetails)) {
-      return;
-    }
     try {
+      NotifyResolver.Result notify = ctx.getNotify(change.getId());
+      if (!notify.shouldNotify()) {
+        return;
+      }
       DeleteReviewerSender cm =
           deleteReviewerSenderFactory.create(ctx.getProject(), change.getId());
       cm.setFrom(ctx.getAccountId());
       cm.addReviewersByEmail(Collections.singleton(reviewer));
       cm.setChangeMessage(changeMessage.getMessage(), changeMessage.getWrittenOn());
-      cm.setNotify(input.notify);
-      cm.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
+      cm.setNotify(notify);
       cm.send();
     } catch (Exception err) {
       logger.atSevere().withCause(err).log("Cannot email update for change %s", change.getId());
diff --git a/java/com/google/gerrit/server/change/DeleteReviewerOp.java b/java/com/google/gerrit/server/change/DeleteReviewerOp.java
index 7915cb7..29458a8 100644
--- a/java/com/google/gerrit/server/change/DeleteReviewerOp.java
+++ b/java/com/google/gerrit/server/change/DeleteReviewerOp.java
@@ -18,6 +18,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
+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;
@@ -27,9 +28,7 @@
 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.server.ReviewDb;
-import com.google.gerrit.reviewdb.server.ReviewDbUtil;
+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;
@@ -38,16 +37,12 @@
 import com.google.gerrit.server.extensions.events.ReviewerDeleted;
 import com.google.gerrit.server.mail.send.DeleteReviewerSender;
 import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
-import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.RemoveReviewerControl;
 import com.google.gerrit.server.update.BatchUpdateOp;
-import com.google.gerrit.server.update.BatchUpdateReviewDb;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
@@ -72,8 +67,6 @@
   private final ReviewerDeleted reviewerDeleted;
   private final Provider<IdentifiedUser> user;
   private final DeleteReviewerSender.Factory deleteReviewerSenderFactory;
-  private final NotesMigration migration;
-  private final NotifyUtil notifyUtil;
   private final RemoveReviewerControl removeReviewerControl;
   private final ProjectCache projectCache;
 
@@ -95,8 +88,6 @@
       ReviewerDeleted reviewerDeleted,
       Provider<IdentifiedUser> user,
       DeleteReviewerSender.Factory deleteReviewerSenderFactory,
-      NotesMigration migration,
-      NotifyUtil notifyUtil,
       RemoveReviewerControl removeReviewerControl,
       ProjectCache projectCache,
       @Assisted AccountState reviewerAccount,
@@ -108,8 +99,6 @@
     this.reviewerDeleted = reviewerDeleted;
     this.user = user;
     this.deleteReviewerSenderFactory = deleteReviewerSenderFactory;
-    this.migration = migration;
-    this.notifyUtil = notifyUtil;
     this.removeReviewerControl = removeReviewerControl;
     this.projectCache = projectCache;
     this.reviewer = reviewerAccount;
@@ -118,17 +107,16 @@
 
   @Override
   public boolean updateChange(ChangeContext ctx)
-      throws AuthException, ResourceNotFoundException, OrmException, PermissionBackendException,
-          IOException {
+      throws AuthException, ResourceNotFoundException, PermissionBackendException, IOException {
     Account.Id reviewerId = reviewer.getAccount().getId();
     // 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);
 
-    if (!approvalsUtil.getReviewers(ctx.getDb(), ctx.getNotes()).all().contains(reviewerId)) {
+    if (!approvalsUtil.getReviewers(ctx.getNotes()).all().contains(reviewerId)) {
       throw new ResourceNotFoundException();
     }
     currChange = ctx.getChange();
-    currPs = psUtil.current(ctx.getDb(), ctx.getNotes());
+    currPs = psUtil.current(ctx.getNotes());
 
     LabelTypes labelTypes = projectCache.checkedGet(ctx.getProject()).getLabelTypes(ctx.getNotes());
     // removing a reviewer will remove all her votes
@@ -164,28 +152,33 @@
     } else {
       msg.append(".");
     }
-    ctx.getDb().patchSetApprovals().delete(del);
     ChangeUpdate update = ctx.getUpdate(currPs.getId());
     update.removeReviewer(reviewerId);
 
     changeMessage =
         ChangeMessagesUtil.newMessage(ctx, msg.toString(), ChangeMessagesUtil.TAG_DELETE_REVIEWER);
-    cmUtil.addChangeMessage(ctx.getDb(), update, changeMessage);
+    cmUtil.addChangeMessage(update, changeMessage);
 
     return true;
   }
 
   @Override
   public void postUpdate(Context ctx) {
-    if (input.notify == null) {
-      if (currChange.isWorkInProgress()) {
-        input.notify = oldApprovals.isEmpty() ? NotifyHandling.NONE : NotifyHandling.OWNER;
-      } else {
-        input.notify = NotifyHandling.ALL;
-      }
+    NotifyResolver.Result notify = ctx.getNotify(currChange.getId());
+    if (input.notify == null
+        && currChange.isWorkInProgress()
+        && !oldApprovals.isEmpty()
+        && notify.handling().compareTo(NotifyHandling.OWNER) < 0) {
+      // Override NotifyHandling from the context to notify owner if votes were removed on a WIP
+      // change.
+      notify = notify.withHandling(NotifyHandling.OWNER);
     }
-    if (NotifyUtil.shouldNotify(input.notify, input.notifyDetails)) {
-      emailReviewers(ctx.getProject(), currChange, changeMessage);
+    try {
+      if (notify.shouldNotify()) {
+        emailReviewers(ctx.getProject(), currChange, changeMessage, notify);
+      }
+    } catch (Exception err) {
+      logger.atSevere().withCause(err).log("Cannot email update for change %s", currChange.getId());
     }
     reviewerDeleted.fire(
         currChange,
@@ -195,30 +188,13 @@
         changeMessage.getMessage(),
         newApprovals,
         oldApprovals,
-        input.notify,
+        notify.handling(),
         ctx.getWhen());
   }
 
-  private Iterable<PatchSetApproval> approvals(ChangeContext ctx, Account.Id accountId)
-      throws OrmException {
-    Change.Id changeId = ctx.getNotes().getChangeId();
+  private Iterable<PatchSetApproval> approvals(ChangeContext ctx, Account.Id accountId) {
     Iterable<PatchSetApproval> approvals;
-    PrimaryStorage r = PrimaryStorage.of(ctx.getChange());
-
-    if (migration.readChanges() && r == PrimaryStorage.REVIEW_DB) {
-      // Because NoteDb and ReviewDb have different semantics for zero-value
-      // approvals, we must fall back to ReviewDb as the source of truth here.
-      ReviewDb db = ctx.getDb();
-
-      if (db instanceof BatchUpdateReviewDb) {
-        db = ((BatchUpdateReviewDb) db).unsafeGetDelegate();
-      }
-      db = ReviewDbUtil.unwrapDb(db);
-      approvals = db.patchSetApprovals().byChange(changeId);
-    } else {
-      approvals = approvalsUtil.byChange(ctx.getDb(), ctx.getNotes()).values();
-    }
-
+    approvals = approvalsUtil.byChange(ctx.getNotes()).values();
     return Iterables.filter(approvals, psa -> accountId.equals(psa.getAccountId()));
   }
 
@@ -230,22 +206,18 @@
   }
 
   private void emailReviewers(
-      Project.NameKey projectName, Change change, ChangeMessage changeMessage) {
+      NameKey projectName, Change change, ChangeMessage changeMessage, NotifyResolver.Result notify)
+      throws EmailException {
     Account.Id userId = user.get().getAccountId();
     if (userId.equals(reviewer.getAccount().getId())) {
       // The user knows they removed themselves, don't bother emailing them.
       return;
     }
-    try {
-      DeleteReviewerSender cm = deleteReviewerSenderFactory.create(projectName, change.getId());
-      cm.setFrom(userId);
-      cm.addReviewers(Collections.singleton(reviewer.getAccount().getId()));
-      cm.setChangeMessage(changeMessage.getMessage(), changeMessage.getWrittenOn());
-      cm.setNotify(input.notify);
-      cm.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
-      cm.send();
-    } catch (Exception err) {
-      logger.atSevere().withCause(err).log("Cannot email update for change %s", change.getId());
-    }
+    DeleteReviewerSender cm = deleteReviewerSenderFactory.create(projectName, change.getId());
+    cm.setFrom(userId);
+    cm.addReviewers(Collections.singleton(reviewer.getAccount().getId()));
+    cm.setChangeMessage(changeMessage.getMessage(), changeMessage.getWrittenOn());
+    cm.setNotify(notify);
+    cm.send();
   }
 }
diff --git a/java/com/google/gerrit/server/change/EmailReviewComments.java b/java/com/google/gerrit/server/change/EmailReviewComments.java
index 65bef70..8353501 100644
--- a/java/com/google/gerrit/server/change/EmailReviewComments.java
+++ b/java/com/google/gerrit/server/change/EmailReviewComments.java
@@ -16,16 +16,11 @@
 
 import static com.google.gerrit.server.CommentsUtil.COMMENT_ORDER;
 
-import com.google.common.collect.ListMultimap;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
-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.ChangeMessage;
 import com.google.gerrit.reviewdb.client.Comment;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.SendEmailExecutor;
@@ -35,11 +30,7 @@
 import com.google.gerrit.server.util.LabelVote;
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
 import com.google.inject.assistedinject.Assisted;
 import java.util.List;
 import java.util.concurrent.ExecutorService;
@@ -53,7 +44,6 @@
     // on the same set of inputs.
     /**
      * @param notify setting for handling notification.
-     * @param accountsToNotify detailed map of accounts to notify.
      * @param notes change notes.
      * @param patchSet patch set corresponding to the top-level op
      * @param user user the email should come from.
@@ -68,8 +58,7 @@
      * @return handle for sending email.
      */
     EmailReviewComments create(
-        NotifyHandling notify,
-        ListMultimap<RecipientType, Account.Id> accountsToNotify,
+        NotifyResolver.Result notify,
         ChangeNotes notes,
         PatchSet patchSet,
         IdentifiedUser user,
@@ -82,11 +71,9 @@
   private final ExecutorService sendEmailsExecutor;
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final CommentSender.Factory commentSenderFactory;
-  private final SchemaFactory<ReviewDb> schemaFactory;
   private final ThreadLocalRequestContext requestContext;
 
-  private final NotifyHandling notify;
-  private final ListMultimap<RecipientType, Account.Id> accountsToNotify;
+  private final NotifyResolver.Result notify;
   private final ChangeNotes notes;
   private final PatchSet patchSet;
   private final IdentifiedUser user;
@@ -94,17 +81,14 @@
   private final List<Comment> comments;
   private final String patchSetComment;
   private final List<LabelVote> labels;
-  private ReviewDb db;
 
   @Inject
   EmailReviewComments(
       @SendEmailExecutor ExecutorService executor,
       PatchSetInfoFactory patchSetInfoFactory,
       CommentSender.Factory commentSenderFactory,
-      SchemaFactory<ReviewDb> schemaFactory,
       ThreadLocalRequestContext requestContext,
-      @Assisted NotifyHandling notify,
-      @Assisted ListMultimap<RecipientType, Account.Id> accountsToNotify,
+      @Assisted NotifyResolver.Result notify,
       @Assisted ChangeNotes notes,
       @Assisted PatchSet patchSet,
       @Assisted IdentifiedUser user,
@@ -115,10 +99,8 @@
     this.sendEmailsExecutor = executor;
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.commentSenderFactory = commentSenderFactory;
-    this.schemaFactory = schemaFactory;
     this.requestContext = requestContext;
     this.notify = notify;
-    this.accountsToNotify = accountsToNotify;
     this.notes = notes;
     this.patchSet = patchSet;
     this.user = user;
@@ -137,7 +119,6 @@
   public void run() {
     RequestContext old = requestContext.setContext(this);
     try {
-
       CommentSender cm = commentSenderFactory.create(notes.getProjectName(), notes.getChangeId());
       cm.setFrom(user.getAccountId());
       cm.setPatchSet(patchSet, patchSetInfoFactory.get(notes.getProjectName(), patchSet));
@@ -146,16 +127,11 @@
       cm.setPatchSetComment(patchSetComment);
       cm.setLabels(labels);
       cm.setNotify(notify);
-      cm.setAccountsToNotify(accountsToNotify);
       cm.send();
     } catch (Exception e) {
       logger.atSevere().withCause(e).log("Cannot email comments for %s", patchSet.getId());
     } finally {
       requestContext.setContext(old);
-      if (db != null) {
-        db.close();
-        db = null;
-      }
     }
   }
 
@@ -168,21 +144,4 @@
   public CurrentUser getUser() {
     return user.getRealUser();
   }
-
-  @Override
-  public Provider<ReviewDb> getReviewDbProvider() {
-    return new Provider<ReviewDb>() {
-      @Override
-      public ReviewDb get() {
-        if (db == null) {
-          try {
-            db = schemaFactory.open();
-          } catch (OrmException e) {
-            throw new ProvisionException("Cannot open ReviewDb", e);
-          }
-        }
-        return db;
-      }
-    };
-  }
 }
diff --git a/java/com/google/gerrit/server/change/LabelsJson.java b/java/com/google/gerrit/server/change/LabelsJson.java
index 787e8ef..6fde5a5 100644
--- a/java/com/google/gerrit/server/change/LabelsJson.java
+++ b/java/com/google/gerrit/server/change/LabelsJson.java
@@ -41,11 +41,9 @@
 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.Account.Id;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
@@ -53,9 +51,7 @@
 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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 import java.sql.Timestamp;
 import java.util.ArrayList;
@@ -79,7 +75,6 @@
     LabelsJson create(Iterable<ListChangesOption> options);
   }
 
-  private final Provider<ReviewDb> db;
   private final ApprovalsUtil approvalsUtil;
   private final ChangeNotes.Factory notesFactory;
   private final PermissionBackend permissionBackend;
@@ -87,12 +82,10 @@
 
   @Inject
   LabelsJson(
-      Provider<ReviewDb> db,
       ApprovalsUtil approvalsUtil,
       ChangeNotes.Factory notesFactory,
       PermissionBackend permissionBackend,
       @Assisted Iterable<ListChangesOption> options) {
-    this.db = db;
     this.approvalsUtil = approvalsUtil;
     this.notesFactory = notesFactory;
     this.permissionBackend = permissionBackend;
@@ -106,14 +99,14 @@
    */
   Map<String, LabelInfo> labelsFor(
       AccountLoader accountLoader, ChangeData cd, boolean standard, boolean detailed)
-      throws OrmException, PermissionBackendException {
+      throws PermissionBackendException {
     if (!standard && !detailed) {
       return null;
     }
 
     LabelTypes labelTypes = cd.getLabelTypes();
     Map<String, LabelWithStatus> withStatus =
-        cd.change().getStatus() == Change.Status.MERGED
+        cd.change().isMerged()
             ? labelsForSubmittedChange(accountLoader, cd, labelTypes, standard, detailed)
             : labelsForUnsubmittedChange(accountLoader, cd, labelTypes, standard, detailed);
     return ImmutableMap.copyOf(Maps.transformValues(withStatus, LabelWithStatus::label));
@@ -121,8 +114,8 @@
 
   /** Returns all labels that the provided user has permission to vote on. */
   Map<String, Collection<String>> permittedLabels(Account.Id filterApprovalsBy, ChangeData cd)
-      throws OrmException, PermissionBackendException {
-    boolean isMerged = cd.change().getStatus() == Change.Status.MERGED;
+      throws PermissionBackendException {
+    boolean isMerged = cd.change().isMerged();
     LabelTypes labelTypes = cd.getLabelTypes();
     Map<String, LabelType> toCheck = new HashMap<>();
     for (SubmitRecord rec : submitRecords(cd)) {
@@ -200,7 +193,7 @@
       LabelTypes labelTypes,
       boolean standard,
       boolean detailed)
-      throws OrmException, PermissionBackendException {
+      throws PermissionBackendException {
     Map<String, LabelWithStatus> labels = initLabels(accountLoader, cd, labelTypes, standard);
     if (detailed) {
       setAllApprovals(accountLoader, cd, labels);
@@ -258,12 +251,10 @@
     }
   }
 
-  private Map<String, Short> currentLabels(Account.Id accountId, ChangeData cd)
-      throws OrmException {
+  private Map<String, Short> currentLabels(Account.Id accountId, ChangeData cd) {
     Map<String, Short> result = new HashMap<>();
     for (PatchSetApproval psa :
         approvalsUtil.byPatchSetUser(
-            db.get(),
             lazyLoad ? cd.notes() : notesFactory.createFromIndexedChange(cd.change()),
             cd.change().currentPatchSetId(),
             accountId,
@@ -280,7 +271,7 @@
       LabelTypes labelTypes,
       boolean standard,
       boolean detailed)
-      throws OrmException, PermissionBackendException {
+      throws PermissionBackendException {
     Set<Account.Id> allUsers = new HashSet<>();
     if (detailed) {
       // Users expect to see all reviewers on closed changes, even if they
@@ -293,7 +284,8 @@
     }
 
     Set<String> labelNames = new HashSet<>();
-    SetMultimap<Id, PatchSetApproval> current = MultimapBuilder.hashKeys().hashSetValues().build();
+    SetMultimap<Account.Id, PatchSetApproval> current =
+        MultimapBuilder.hashKeys().hashSetValues().build();
     for (PatchSetApproval a : cd.currentApprovals()) {
       allUsers.add(a.getAccountId());
       LabelType type = labelTypes.byLabel(a.getLabelId());
@@ -437,21 +429,22 @@
 
   private void setAllApprovals(
       AccountLoader accountLoader, ChangeData cd, Map<String, LabelWithStatus> labels)
-      throws OrmException, PermissionBackendException {
-    Change.Status status = cd.change().getStatus();
+      throws PermissionBackendException {
     checkState(
-        status != Change.Status.MERGED, "should not call setAllApprovals on %s change", status);
+        !cd.change().isMerged(),
+        "should not call setAllApprovals on %s change",
+        ChangeUtil.status(cd.change()));
 
     // Include a user in the output for this label if either:
     //  - They are an explicit reviewer.
     //  - They ever voted on this change.
-    Set<Id> allUsers = new HashSet<>();
+    Set<Account.Id> allUsers = new HashSet<>();
     allUsers.addAll(cd.reviewers().byState(ReviewerStateInternal.REVIEWER));
     for (PatchSetApproval psa : cd.approvals().values()) {
       allUsers.add(psa.getAccountId());
     }
 
-    Table<Id, String, PatchSetApproval> current =
+    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);
@@ -504,9 +497,8 @@
    *     from either an index-backed or a database-backed {@link ChangeData} depending on {@code
    *     lazyload}.
    */
-  private PermissionBackend.ForChange permissionBackendForChange(Account.Id user, ChangeData cd)
-      throws OrmException {
-    PermissionBackend.WithUser withUser = permissionBackend.absentUser(user).database(db);
+  private PermissionBackend.ForChange permissionBackendForChange(Account.Id user, ChangeData cd) {
+    PermissionBackend.WithUser withUser = permissionBackend.absentUser(user);
     return lazyLoad
         ? withUser.change(cd)
         : withUser.indexedChange(cd, notesFactory.createFromIndexedChange(cd.change()));
diff --git a/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java b/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
index 131f3a1..d408519 100644
--- a/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
+++ b/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
@@ -25,13 +25,13 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.common.util.concurrent.UncheckedExecutionException;
 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;
 import com.google.gerrit.server.cache.serialize.CacheSerializer;
-import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers;
-import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers.ObjectIdConverter;
+import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
 import com.google.gerrit.server.submit.SubmitDryRun;
@@ -143,7 +143,7 @@
       @Override
       public byte[] serialize(EntryKey object) {
         ObjectIdConverter idConverter = ObjectIdConverter.create();
-        return ProtoCacheSerializers.toByteArray(
+        return Protos.toByteArray(
             MergeabilityKeyProto.newBuilder()
                 .setCommit(idConverter.toByteString(object.getCommit()))
                 .setInto(idConverter.toByteString(object.getInto()))
@@ -154,8 +154,7 @@
 
       @Override
       public EntryKey deserialize(byte[] in) {
-        MergeabilityKeyProto proto =
-            ProtoCacheSerializers.parseUnchecked(MergeabilityKeyProto.parser(), in);
+        MergeabilityKeyProto proto = Protos.parseUnchecked(MergeabilityKeyProto.parser(), in);
         ObjectIdConverter idConverter = ObjectIdConverter.create();
         return new EntryKey(
             idConverter.fromByteString(proto.getCommit()),
@@ -208,7 +207,7 @@
               accepted.add(rw.parseCommit(key.into));
               accepted.addAll(Arrays.asList(rw.parseCommit(key.commit).getParents()));
               return submitDryRun.run(
-                  key.submitType, repo, rw, dest, key.into, key.commit, accepted);
+                  null, key.submitType, repo, rw, dest, key.into, key.commit, accepted);
             }
           });
     } catch (ExecutionException | UncheckedExecutionException e) {
diff --git a/java/com/google/gerrit/server/change/NotifyResolver.java b/java/com/google/gerrit/server/change/NotifyResolver.java
new file mode 100644
index 0000000..5b15684
--- /dev/null
+++ b/java/com/google/gerrit/server/change/NotifyResolver.java
@@ -0,0 +1,116 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.joining;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSetMultimap;
+import com.google.gerrit.common.Nullable;
+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;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+@Singleton
+public class NotifyResolver {
+  @AutoValue
+  public abstract static class Result {
+    public static Result none() {
+      return create(NotifyHandling.NONE);
+    }
+
+    public static Result all() {
+      return create(NotifyHandling.ALL);
+    }
+
+    public static Result create(NotifyHandling notifyHandling) {
+      return create(notifyHandling, ImmutableSetMultimap.of());
+    }
+
+    public static Result create(
+        NotifyHandling handling, ImmutableSetMultimap<RecipientType, Account.Id> recipients) {
+      return new AutoValue_NotifyResolver_Result(handling, recipients);
+    }
+
+    public abstract NotifyHandling handling();
+
+    public abstract ImmutableSetMultimap<RecipientType, Account.Id> accounts();
+
+    public Result withHandling(NotifyHandling notifyHandling) {
+      return create(notifyHandling, accounts());
+    }
+
+    public boolean shouldNotify() {
+      return !accounts().isEmpty() || handling().compareTo(NotifyHandling.NONE) > 0;
+    }
+  }
+
+  private final AccountResolver accountResolver;
+
+  @Inject
+  NotifyResolver(AccountResolver accountResolver) {
+    this.accountResolver = accountResolver;
+  }
+
+  public Result resolve(
+      NotifyHandling handling, @Nullable Map<RecipientType, NotifyInfo> notifyDetails)
+      throws BadRequestException, IOException, ConfigInvalidException {
+    requireNonNull(handling);
+    ImmutableSetMultimap.Builder<RecipientType, Account.Id> b = ImmutableSetMultimap.builder();
+    if (notifyDetails != null) {
+      for (Map.Entry<RecipientType, NotifyInfo> e : notifyDetails.entrySet()) {
+        b.putAll(e.getKey(), find(e.getValue().accounts));
+      }
+    }
+    return Result.create(handling, b.build());
+  }
+
+  private ImmutableList<Account.Id> find(@Nullable List<String> inputs)
+      throws BadRequestException, IOException, ConfigInvalidException {
+    if (inputs == null || inputs.isEmpty()) {
+      return ImmutableList.of();
+    }
+    ImmutableList.Builder<Account.Id> r = ImmutableList.builder();
+    List<String> problems = new ArrayList<>(inputs.size());
+    for (String nameOrEmail : inputs) {
+      try {
+        r.add(accountResolver.resolve(nameOrEmail).asUnique().getAccount().getId());
+      } catch (UnprocessableEntityException e) {
+        problems.add(e.getMessage());
+      }
+    }
+
+    if (!problems.isEmpty()) {
+      throw new BadRequestException(
+          "Some accounts that should be notified could not be resolved: "
+              + problems.stream().collect(joining("\n")));
+    }
+
+    return r.build();
+  }
+}
diff --git a/java/com/google/gerrit/server/change/NotifyUtil.java b/java/com/google/gerrit/server/change/NotifyUtil.java
deleted file mode 100644
index c29faee..0000000
--- a/java/com/google/gerrit/server/change/NotifyUtil.java
+++ /dev/null
@@ -1,117 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.change;
-
-import static java.util.stream.Collectors.joining;
-
-import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.ListMultimap;
-import com.google.common.collect.MultimapBuilder;
-import com.google.gerrit.common.Nullable;
-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.reviewdb.client.Account;
-import com.google.gerrit.server.account.AccountResolver;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-
-@Singleton
-public class NotifyUtil {
-  private final AccountResolver accountResolver;
-
-  @Inject
-  NotifyUtil(AccountResolver accountResolver) {
-    this.accountResolver = accountResolver;
-  }
-
-  public static boolean shouldNotify(
-      NotifyHandling notify, @Nullable Map<RecipientType, NotifyInfo> notifyDetails) {
-    if (!isNullOrEmpty(notifyDetails)) {
-      return true;
-    }
-
-    return notify.compareTo(NotifyHandling.NONE) > 0;
-  }
-
-  private static boolean isNullOrEmpty(@Nullable Map<RecipientType, NotifyInfo> notifyDetails) {
-    if (notifyDetails == null || notifyDetails.isEmpty()) {
-      return true;
-    }
-
-    for (NotifyInfo notifyInfo : notifyDetails.values()) {
-      if (!isEmpty(notifyInfo)) {
-        return false;
-      }
-    }
-
-    return true;
-  }
-
-  private static boolean isEmpty(NotifyInfo notifyInfo) {
-    return notifyInfo.accounts == null || notifyInfo.accounts.isEmpty();
-  }
-
-  public ListMultimap<RecipientType, Account.Id> resolveAccounts(
-      @Nullable Map<RecipientType, NotifyInfo> notifyDetails)
-      throws OrmException, BadRequestException, IOException, ConfigInvalidException {
-    if (isNullOrEmpty(notifyDetails)) {
-      return ImmutableListMultimap.of();
-    }
-
-    ListMultimap<RecipientType, Account.Id> m = null;
-    for (Entry<RecipientType, NotifyInfo> e : notifyDetails.entrySet()) {
-      List<String> accounts = e.getValue().accounts;
-      if (accounts != null) {
-        if (m == null) {
-          m = MultimapBuilder.hashKeys().arrayListValues().build();
-        }
-        m.putAll(e.getKey(), find(accounts));
-      }
-    }
-
-    return m != null ? m : ImmutableListMultimap.of();
-  }
-
-  private List<Account.Id> find(List<String> nameOrEmails)
-      throws OrmException, BadRequestException, IOException, ConfigInvalidException {
-    List<String> missing = new ArrayList<>(nameOrEmails.size());
-    List<Account.Id> r = new ArrayList<>(nameOrEmails.size());
-    for (String nameOrEmail : nameOrEmails) {
-      Account a = accountResolver.find(nameOrEmail);
-      if (a != null) {
-        r.add(a.getId());
-      } else {
-        missing.add(nameOrEmail);
-      }
-    }
-
-    if (!missing.isEmpty()) {
-      throw new BadRequestException(
-          "The following accounts that should be notified could not be resolved: "
-              + missing.stream().distinct().sorted().collect(joining(", ")));
-    }
-
-    return r;
-  }
-}
diff --git a/java/com/google/gerrit/server/change/PatchSetInserter.java b/java/com/google/gerrit/server/change/PatchSetInserter.java
index 24c4237..d3649f6 100644
--- a/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -19,20 +19,14 @@
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
 import static java.util.Objects.requireNonNull;
 
-import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.ListMultimap;
 import com.google.common.flogger.FluentLogger;
 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.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.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ApprovalCopier;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
@@ -55,7 +49,6 @@
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
 import com.google.gerrit.server.update.RepoContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
@@ -79,7 +72,6 @@
   private final ProjectCache projectCache;
   private final RevisionCreated revisionCreated;
   private final ApprovalsUtil approvalsUtil;
-  private final ApprovalCopier approvalCopier;
   private final ChangeMessagesUtil cmUtil;
   private final PatchSetUtil psUtil;
 
@@ -98,10 +90,8 @@
   private boolean checkAddPatchSetPermission = true;
   private List<String> groups = Collections.emptyList();
   private boolean fireRevisionCreated = true;
-  private NotifyHandling notify = NotifyHandling.ALL;
-  private ListMultimap<RecipientType, Account.Id> accountsToNotify = ImmutableListMultimap.of();
   private boolean allowClosed;
-  private boolean copyApprovals = true;
+  private boolean sendEmail = true;
 
   // Fields set during some phase of BatchUpdate.Op.
   private Change change;
@@ -114,7 +104,6 @@
   public PatchSetInserter(
       PermissionBackend permissionBackend,
       ApprovalsUtil approvalsUtil,
-      ApprovalCopier approvalCopier,
       ChangeMessagesUtil cmUtil,
       PatchSetInfoFactory patchSetInfoFactory,
       CommitValidators.Factory commitValidatorsFactory,
@@ -127,7 +116,6 @@
       @Assisted ObjectId commitId) {
     this.permissionBackend = permissionBackend;
     this.approvalsUtil = approvalsUtil;
-    this.approvalCopier = approvalCopier;
     this.cmUtil = cmUtil;
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.commitValidatorsFactory = commitValidatorsFactory;
@@ -176,24 +164,13 @@
     return this;
   }
 
-  public PatchSetInserter setNotify(NotifyHandling notify) {
-    this.notify = requireNonNull(notify);
-    return this;
-  }
-
-  public PatchSetInserter setAccountsToNotify(
-      ListMultimap<RecipientType, Account.Id> accountsToNotify) {
-    this.accountsToNotify = requireNonNull(accountsToNotify);
-    return this;
-  }
-
   public PatchSetInserter setAllowClosed(boolean allowClosed) {
     this.allowClosed = allowClosed;
     return this;
   }
 
-  public PatchSetInserter setCopyApprovals(boolean copyApprovals) {
-    this.copyApprovals = copyApprovals;
+  public PatchSetInserter setSendEmail(boolean sendEmail) {
+    this.sendEmail = sendEmail;
     return this;
   }
 
@@ -209,22 +186,18 @@
 
   @Override
   public void updateRepo(RepoContext ctx)
-      throws AuthException, ResourceConflictException, IOException, OrmException,
-          PermissionBackendException {
+      throws AuthException, ResourceConflictException, IOException, PermissionBackendException {
     validate(ctx);
     ctx.addRefUpdate(ObjectId.zeroId(), commitId, getPatchSetId().toRefName());
   }
 
   @Override
-  public boolean updateChange(ChangeContext ctx)
-      throws ResourceConflictException, OrmException, IOException {
-    ReviewDb db = ctx.getDb();
-
+  public boolean updateChange(ChangeContext ctx) throws ResourceConflictException, IOException {
     change = ctx.getChange();
     ChangeUpdate update = ctx.getUpdate(psId);
     update.setSubjectForCommit("Create patch set " + psId.get());
 
-    if (!change.getStatus().isOpen() && !allowClosed) {
+    if (!change.isNew() && !allowClosed) {
       throw new ResourceConflictException(
           String.format(
               "Cannot create new patch set of change %s because it is %s",
@@ -233,24 +206,17 @@
 
     List<String> newGroups = groups;
     if (newGroups.isEmpty()) {
-      PatchSet prevPs = psUtil.current(db, ctx.getNotes());
+      PatchSet prevPs = psUtil.current(ctx.getNotes());
       if (prevPs != null) {
         newGroups = prevPs.getGroups();
       }
     }
     patchSet =
         psUtil.insert(
-            db,
-            ctx.getRevWalk(),
-            ctx.getUpdate(psId),
-            psId,
-            commitId,
-            newGroups,
-            null,
-            description);
+            ctx.getRevWalk(), ctx.getUpdate(psId), psId, commitId, newGroups, null, description);
 
-    if (notify != NotifyHandling.NONE) {
-      oldReviewers = approvalsUtil.getReviewers(db, ctx.getNotes());
+    if (ctx.getNotify(change.getId()).handling() != NotifyHandling.NONE) {
+      oldReviewers = approvalsUtil.getReviewers(ctx.getNotes());
     }
 
     if (message != null) {
@@ -270,19 +236,17 @@
       change.setStatus(Change.Status.NEW);
     }
     change.setCurrentPatchSet(patchSetInfo);
-    if (copyApprovals) {
-      approvalCopier.copyInReviewDb(
-          db, ctx.getNotes(), patchSet, ctx.getRevWalk(), ctx.getRepoView().getConfig());
-    }
     if (changeMessage != null) {
-      cmUtil.addChangeMessage(db, update, changeMessage);
+      cmUtil.addChangeMessage(update, changeMessage);
     }
     return true;
   }
 
   @Override
-  public void postUpdate(Context ctx) throws OrmException {
-    if (notify != NotifyHandling.NONE || !accountsToNotify.isEmpty()) {
+  public void postUpdate(Context ctx) {
+    NotifyResolver.Result notify = ctx.getNotify(change.getId());
+    if (notify.shouldNotify() && sendEmail) {
+      requireNonNull(changeMessage);
       try {
         ReplacePatchSetSender cm = replacePatchSetFactory.create(ctx.getProject(), change.getId());
         cm.setFrom(ctx.getAccountId());
@@ -291,7 +255,6 @@
         cm.addReviewers(oldReviewers.byState(REVIEWER));
         cm.addExtraCC(oldReviewers.byState(CC));
         cm.setNotify(notify);
-        cm.setAccountsToNotify(accountsToNotify);
         cm.send();
       } catch (Exception err) {
         logger.atSevere().withCause(err).log(
@@ -305,17 +268,12 @@
   }
 
   private void validate(RepoContext ctx)
-      throws AuthException, ResourceConflictException, IOException, PermissionBackendException,
-          OrmException {
+      throws AuthException, ResourceConflictException, IOException, PermissionBackendException {
     // Not allowed to create a new patch set if the current patch set is locked.
     psUtil.checkPatchSetNotLocked(origNotes);
 
     if (checkAddPatchSetPermission) {
-      permissionBackend
-          .user(ctx.getUser())
-          .database(ctx.getDb())
-          .change(origNotes)
-          .check(ChangePermission.ADD_PATCH_SET);
+      permissionBackend.user(ctx.getUser()).change(origNotes).check(ChangePermission.ADD_PATCH_SET);
     }
     projectCache.checkedGet(ctx.getProject()).checkStatePermitsWrite();
     if (!validate) {
diff --git a/java/com/google/gerrit/server/change/PluginDefinedAttributesFactories.java b/java/com/google/gerrit/server/change/PluginDefinedAttributesFactories.java
new file mode 100644
index 0000000..9928125
--- /dev/null
+++ b/java/com/google/gerrit/server/change/PluginDefinedAttributesFactories.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.change;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static java.util.concurrent.TimeUnit.MINUTES;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.common.PluginDefinedInfo;
+import com.google.gerrit.extensions.registration.Extension;
+import com.google.gerrit.server.DynamicOptions.BeanProvider;
+import com.google.gerrit.server.query.change.ChangeData;
+import java.util.Objects;
+import java.util.stream.Stream;
+
+/** Static helpers for use by {@link PluginDefinedAttributesFactory} implementations. */
+public class PluginDefinedAttributesFactories {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  @Nullable
+  public static ImmutableList<PluginDefinedInfo> createAll(
+      ChangeData cd,
+      BeanProvider beanProvider,
+      Stream<Extension<ChangeAttributeFactory>> attrFactories) {
+    ImmutableList<PluginDefinedInfo> result =
+        attrFactories
+            .map(e -> tryCreate(cd, beanProvider, e.getPluginName(), e.get()))
+            .filter(Objects::nonNull)
+            .collect(toImmutableList());
+    return !result.isEmpty() ? result : null;
+  }
+
+  @Nullable
+  private static PluginDefinedInfo tryCreate(
+      ChangeData cd, BeanProvider beanProvider, String plugin, ChangeAttributeFactory attrFactory) {
+    PluginDefinedInfo pdi = null;
+    try {
+      pdi = attrFactory.create(cd, beanProvider, plugin);
+    } catch (RuntimeException ex) {
+      logger.atWarning().atMostEvery(1, MINUTES).withCause(ex).log(
+          "error populating attribute on change %s from plugin %s", cd.getId(), plugin);
+    }
+    if (pdi != null) {
+      pdi.name = plugin;
+    }
+    return pdi;
+  }
+
+  private PluginDefinedAttributesFactories() {}
+}
diff --git a/java/com/google/gerrit/server/change/PluginDefinedAttributesFactory.java b/java/com/google/gerrit/server/change/PluginDefinedAttributesFactory.java
new file mode 100644
index 0000000..08d6ce7
--- /dev/null
+++ b/java/com/google/gerrit/server/change/PluginDefinedAttributesFactory.java
@@ -0,0 +1,23 @@
+// 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.extensions.common.PluginDefinedInfo;
+import com.google.gerrit.server.query.change.ChangeData;
+import java.util.List;
+
+public interface PluginDefinedAttributesFactory {
+  List<PluginDefinedInfo> create(ChangeData cd);
+}
diff --git a/java/com/google/gerrit/server/change/PureRevert.java b/java/com/google/gerrit/server/change/PureRevert.java
index 766b82f..acb3dd7 100644
--- a/java/com/google/gerrit/server/change/PureRevert.java
+++ b/java/com/google/gerrit/server/change/PureRevert.java
@@ -14,116 +14,48 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.gerrit.common.Nullable;
-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.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.PatchSetUtil;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.git.PureRevertCache;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.util.List;
-import org.eclipse.jgit.diff.DiffEntry;
-import org.eclipse.jgit.diff.DiffFormatter;
+import java.util.Optional;
 import org.eclipse.jgit.errors.InvalidObjectIdException;
-import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.merge.ThreeWayMerger;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
 
+/** Can check if a change is a pure revert (= a revert with no further modifications). */
 @Singleton
 public class PureRevert {
-  private final MergeUtil.Factory mergeUtilFactory;
-  private final GitRepositoryManager repoManager;
-  private final ProjectCache projectCache;
-  private final ChangeNotes.Factory notesFactory;
-  private final Provider<ReviewDb> dbProvider;
-  private final PatchSetUtil psUtil;
+  private final PureRevertCache pureRevertCache;
 
   @Inject
-  PureRevert(
-      MergeUtil.Factory mergeUtilFactory,
-      GitRepositoryManager repoManager,
-      ProjectCache projectCache,
-      ChangeNotes.Factory notesFactory,
-      Provider<ReviewDb> dbProvider,
-      PatchSetUtil psUtil) {
-    this.mergeUtilFactory = mergeUtilFactory;
-    this.repoManager = repoManager;
-    this.projectCache = projectCache;
-    this.notesFactory = notesFactory;
-    this.dbProvider = dbProvider;
-    this.psUtil = psUtil;
+  PureRevert(PureRevertCache pureRevertCache) {
+    this.pureRevertCache = pureRevertCache;
   }
 
-  public PureRevertInfo get(ChangeNotes notes, @Nullable String claimedOriginal)
-      throws OrmException, IOException, BadRequestException, ResourceConflictException {
-    PatchSet currentPatchSet = psUtil.current(dbProvider.get(), notes);
+  public boolean get(ChangeNotes notes, Optional<String> claimedOriginal)
+      throws IOException, BadRequestException, ResourceConflictException {
+    PatchSet currentPatchSet = notes.getCurrentPatchSet();
     if (currentPatchSet == null) {
       throw new ResourceConflictException("current revision is missing");
     }
-
-    if (claimedOriginal == null) {
-      if (notes.getChange().getRevertOf() == null) {
-        throw new BadRequestException("no ID was provided and change isn't a revert");
-      }
-      PatchSet ps =
-          psUtil.current(
-              dbProvider.get(),
-              notesFactory.createChecked(
-                  dbProvider.get(), notes.getProjectName(), notes.getChange().getRevertOf()));
-      claimedOriginal = ps.getRevision().get();
+    if (!claimedOriginal.isPresent()) {
+      return pureRevertCache.isPureRevert(notes);
     }
 
-    try (Repository repo = repoManager.openRepository(notes.getProjectName());
-        ObjectInserter oi = repo.newObjectInserter();
-        RevWalk rw = new RevWalk(repo)) {
-      RevCommit claimedOriginalCommit;
-      try {
-        claimedOriginalCommit = rw.parseCommit(ObjectId.fromString(claimedOriginal));
-      } catch (InvalidObjectIdException | MissingObjectException e) {
-        throw new BadRequestException("invalid object ID", e);
-      }
-      if (claimedOriginalCommit.getParentCount() == 0) {
-        throw new BadRequestException("can't check against initial commit");
-      }
-      RevCommit claimedRevertCommit =
-          rw.parseCommit(ObjectId.fromString(currentPatchSet.getRevision().get()));
-      if (claimedRevertCommit.getParentCount() == 0) {
-        throw new BadRequestException("claimed revert has no parents");
-      }
-      // Rebase claimed revert onto claimed original
-      ThreeWayMerger merger =
-          mergeUtilFactory
-              .create(projectCache.checkedGet(notes.getProjectName()))
-              .newThreeWayMerger(oi, repo.getConfig());
-      merger.setBase(claimedRevertCommit.getParent(0));
-      boolean success = merger.merge(claimedRevertCommit, claimedOriginalCommit);
-      if (!success || merger.getResultTreeId() == null) {
-        // Merge conflict during rebase
-        return new PureRevertInfo(false);
-      }
-
-      // 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())) {
-        df.setReader(oi.newReader(), repo.getConfig());
-        List<DiffEntry> entries =
-            df.scan(claimedOriginalCommit.getParent(0), merger.getResultTreeId());
-        return new PureRevertInfo(entries.isEmpty());
-      }
+    ObjectId claimedOriginalObjectId;
+    try {
+      claimedOriginalObjectId = ObjectId.fromString(claimedOriginal.get());
+    } catch (InvalidObjectIdException e) {
+      throw new BadRequestException("invalid object ID", e);
     }
+
+    return pureRevertCache.isPureRevert(
+        notes.getProjectName(),
+        ObjectId.fromString(notes.getCurrentPatchSet().getRevision().get()),
+        claimedOriginalObjectId);
   }
 }
diff --git a/java/com/google/gerrit/server/change/RebaseChangeOp.java b/java/com/google/gerrit/server/change/RebaseChangeOp.java
index 1f216f0..fccda7c 100644
--- a/java/com/google/gerrit/server/change/RebaseChangeOp.java
+++ b/java/com/google/gerrit/server/change/RebaseChangeOp.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.base.Preconditions.checkState;
 
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.restapi.MergeConflictException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -26,6 +25,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.RebaseUtil.Base;
+import com.google.gerrit.server.git.GroupCollector;
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -37,7 +37,6 @@
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
 import com.google.gerrit.server.update.RepoContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
@@ -69,9 +68,9 @@
   private boolean validate = true;
   private boolean checkAddPatchSetPermission = true;
   private boolean forceContentMerge;
-  private boolean copyApprovals = true;
   private boolean detailedCommitMessage;
   private boolean postMessage = true;
+  private boolean sendEmail = true;
   private boolean matchAuthorToCommitterDate = false;
 
   private RevCommit rebasedCommit;
@@ -126,11 +125,6 @@
     return this;
   }
 
-  public RebaseChangeOp setCopyApprovals(boolean copyApprovals) {
-    this.copyApprovals = copyApprovals;
-    return this;
-  }
-
   public RebaseChangeOp setDetailedCommitMessage(boolean detailedCommitMessage) {
     this.detailedCommitMessage = detailedCommitMessage;
     return this;
@@ -141,6 +135,11 @@
     return this;
   }
 
+  public RebaseChangeOp setSendEmail(boolean sendEmail) {
+    this.sendEmail = sendEmail;
+    return this;
+  }
+
   public RebaseChangeOp setMatchAuthorToCommitterDate(boolean matchAuthorToCommitterDate) {
     this.matchAuthorToCommitterDate = matchAuthorToCommitterDate;
     return this;
@@ -149,7 +148,7 @@
   @Override
   public void updateRepo(RepoContext ctx)
       throws MergeConflictException, InvalidChangeOperationException, RestApiException, IOException,
-          OrmException, NoSuchChangeException, PermissionBackendException {
+          NoSuchChangeException, PermissionBackendException {
     // Ok that originalPatchSet was not read in a transaction, since we just
     // need its revision.
     RevId oldRev = originalPatchSet.getRevision();
@@ -178,18 +177,19 @@
             baseCommitId.name());
 
     rebasedPatchSetId =
-        ChangeUtil.nextPatchSetIdFromChangeRefsMap(
-            ctx.getRepoView().getRefs(originalPatchSet.getId().getParentKey().toRefPrefix()),
+        ChangeUtil.nextPatchSetIdFromChangeRefs(
+            ctx.getRepoView()
+                .getRefs(originalPatchSet.getId().getParentKey().toRefPrefix())
+                .keySet(),
             notes.getChange().currentPatchSetId());
     patchSetInserter =
         patchSetInserterFactory
             .create(notes, rebasedPatchSetId, rebasedCommit)
             .setDescription("Rebase")
-            .setNotify(NotifyHandling.NONE)
             .setFireRevisionCreated(fireRevisionCreated)
-            .setCopyApprovals(copyApprovals)
             .setCheckAddPatchSetPermission(checkAddPatchSetPermission)
-            .setValidate(validate);
+            .setValidate(validate)
+            .setSendEmail(sendEmail);
     if (postMessage) {
       patchSetInserter.setMessage(
           "Patch Set "
@@ -199,22 +199,27 @@
               + " was rebased");
     }
 
-    if (base != null) {
-      patchSetInserter.setGroups(base.patchSet().getGroups());
+    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());
+      } else {
+        // If the base is merged, start a new relation chain.
+        patchSetInserter.setGroups(GroupCollector.getDefaultGroups(rebasedCommit));
+      }
     }
     patchSetInserter.updateRepo(ctx);
   }
 
   @Override
-  public boolean updateChange(ChangeContext ctx)
-      throws ResourceConflictException, OrmException, IOException {
+  public boolean updateChange(ChangeContext ctx) throws ResourceConflictException, IOException {
     boolean ret = patchSetInserter.updateChange(ctx);
     rebasedPatchSet = patchSetInserter.getPatchSet();
     return ret;
   }
 
   @Override
-  public void postUpdate(Context ctx) throws OrmException {
+  public void postUpdate(Context ctx) {
     patchSetInserter.postUpdate(ctx);
   }
 
diff --git a/java/com/google/gerrit/server/change/RebaseUtil.java b/java/com/google/gerrit/server/change/RebaseUtil.java
index 22f98b8..a4cf5ba 100644
--- a/java/com/google/gerrit/server/change/RebaseUtil.java
+++ b/java/com/google/gerrit/server/change/RebaseUtil.java
@@ -17,20 +17,18 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
+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.Change.Status;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -46,18 +44,15 @@
 
   private final Provider<InternalChangeQuery> queryProvider;
   private final ChangeNotes.Factory notesFactory;
-  private final Provider<ReviewDb> dbProvider;
   private final PatchSetUtil psUtil;
 
   @Inject
   RebaseUtil(
       Provider<InternalChangeQuery> queryProvider,
       ChangeNotes.Factory notesFactory,
-      Provider<ReviewDb> dbProvider,
       PatchSetUtil psUtil) {
     this.queryProvider = queryProvider;
     this.notesFactory = notesFactory;
-    this.dbProvider = dbProvider;
     this.psUtil = psUtil;
   }
 
@@ -67,7 +62,7 @@
       return true;
     } catch (RestApiException e) {
       return false;
-    } catch (OrmException | IOException e) {
+    } catch (StorageException | IOException e) {
       logger.atWarning().withCause(e).log(
           "Error checking if patch set %s on %s can be rebased", patchSet.getId(), dest);
       return false;
@@ -88,9 +83,7 @@
     public abstract PatchSet patchSet();
   }
 
-  public Base parseBase(RevisionResource rsrc, String base) throws OrmException {
-    ReviewDb db = dbProvider.get();
-
+  public Base parseBase(RevisionResource rsrc, String base) {
     // Try parsing the base as a ref string.
     PatchSet.Id basePatchSetId = PatchSet.Id.fromRef(base);
     if (basePatchSetId != null) {
@@ -98,8 +91,7 @@
       ChangeNotes baseNotes = notesFor(rsrc, baseChangeId);
       if (baseNotes != null) {
         return Base.create(
-            notesFor(rsrc, basePatchSetId.getParentKey()),
-            psUtil.get(db, baseNotes, basePatchSetId));
+            notesFor(rsrc, basePatchSetId.getParentKey()), psUtil.get(baseNotes, basePatchSetId));
       }
     }
 
@@ -108,7 +100,7 @@
     if (baseChangeId != null) {
       ChangeNotes baseNotes = notesFor(rsrc, new Change.Id(baseChangeId));
       if (baseNotes != null) {
-        return Base.create(baseNotes, psUtil.current(db, baseNotes));
+        return Base.create(baseNotes, psUtil.current(baseNotes));
       }
     }
 
@@ -127,11 +119,11 @@
     return ret;
   }
 
-  private ChangeNotes notesFor(RevisionResource rsrc, Change.Id id) throws OrmException {
+  private ChangeNotes notesFor(RevisionResource rsrc, Change.Id id) {
     if (rsrc.getChange().getId().equals(id)) {
       return rsrc.getNotes();
     }
-    return notesFactory.createChecked(dbProvider.get(), rsrc.getProject(), id);
+    return notesFactory.createChecked(rsrc.getProject(), id);
   }
 
   /**
@@ -147,11 +139,10 @@
    * @return the commit onto which the patch set should be rebased.
    * @throws RestApiException if rebase is not possible.
    * @throws IOException if accessing the repository fails.
-   * @throws OrmException if accessing the database fails.
    */
   public ObjectId findBaseRevision(
       PatchSet patchSet, Branch.NameKey destBranch, Repository git, RevWalk rw)
-      throws RestApiException, IOException, OrmException {
+      throws RestApiException, IOException {
     String baseRev = null;
     RevCommit commit = rw.parseCommit(ObjectId.fromString(patchSet.getRevision().get()));
 
@@ -171,12 +162,12 @@
           continue;
         }
         Change depChange = cd.change();
-        if (depChange.getStatus() == Status.ABANDONED) {
+        if (depChange.isAbandoned()) {
           throw new ResourceConflictException(
               "Cannot rebase a change with an abandoned parent: " + depChange.getKey());
         }
 
-        if (depChange.getStatus().isOpen()) {
+        if (depChange.isNew()) {
           if (depPatchSet.getId().equals(depChange.currentPatchSetId())) {
             throw new ResourceConflictException(
                 "Change is already based on the latest patch set of the dependent change.");
diff --git a/java/com/google/gerrit/server/change/ReviewerAdder.java b/java/com/google/gerrit/server/change/ReviewerAdder.java
index fe6090f..a6ad559 100644
--- a/java/com/google/gerrit/server/change/ReviewerAdder.java
+++ b/java/com/google/gerrit/server/change/ReviewerAdder.java
@@ -24,23 +24,20 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Ordering;
 import com.google.common.collect.Streams;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.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.api.changes.RecipientType;
 import com.google.gerrit.extensions.api.changes.ReviewerInfo;
 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.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.mail.Address;
@@ -51,7 +48,6 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -63,7 +59,6 @@
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.mail.send.OutgoingEmailValidator;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -73,7 +68,6 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -89,6 +83,8 @@
 import org.eclipse.jgit.lib.Config;
 
 public class ReviewerAdder {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   public static final int DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK = 10;
   public static final int DEFAULT_MAX_REVIEWERS = 20;
 
@@ -150,8 +146,6 @@
   private final AccountLoader.Factory accountLoaderFactory;
   private final Config cfg;
   private final ReviewerJson json;
-  private final NotesMigration migration;
-  private final NotifyUtil notifyUtil;
   private final ProjectCache projectCache;
   private final Provider<AnonymousUser> anonymousProvider;
   private final AddReviewersOp.Factory addReviewersOpFactory;
@@ -166,8 +160,6 @@
       AccountLoader.Factory accountLoaderFactory,
       @GerritServerConfig Config cfg,
       ReviewerJson json,
-      NotesMigration migration,
-      NotifyUtil notifyUtil,
       ProjectCache projectCache,
       Provider<AnonymousUser> anonymousProvider,
       AddReviewersOp.Factory addReviewersOpFactory,
@@ -179,8 +171,6 @@
     this.accountLoaderFactory = accountLoaderFactory;
     this.cfg = cfg;
     this.json = json;
-    this.migration = migration;
-    this.notifyUtil = notifyUtil;
     this.projectCache = projectCache;
     this.anonymousProvider = anonymousProvider;
     this.addReviewersOpFactory = addReviewersOpFactory;
@@ -190,7 +180,6 @@
   /**
    * Prepare application of a single {@link AddReviewerInput}.
    *
-   * @param db database.
    * @param notes change notes.
    * @param user user performing the reviewer addition.
    * @param input input describing user or group to add as a reviewer.
@@ -198,48 +187,47 @@
    * @return handle describing the addition operation. If the {@code op} field is present, this
    *     operation may be added to a {@code BatchUpdate}. Otherwise, the {@code error} field
    *     contains information about an error that occurred
-   * @throws OrmException
    * @throws IOException
    * @throws PermissionBackendException
    * @throws ConfigInvalidException
    */
   public ReviewerAddition prepare(
-      ReviewDb db, ChangeNotes notes, CurrentUser user, AddReviewerInput input, boolean allowGroup)
-      throws OrmException, IOException, PermissionBackendException, ConfigInvalidException {
+      ChangeNotes notes, CurrentUser user, AddReviewerInput input, boolean allowGroup)
+      throws IOException, PermissionBackendException, ConfigInvalidException {
     requireNonNull(input.reviewer);
-    ListMultimap<RecipientType, Account.Id> accountsToNotify;
-    try {
-      accountsToNotify = notifyUtil.resolveAccounts(input.notifyDetails);
-    } catch (BadRequestException e) {
-      return fail(input, FailureType.OTHER, e.getMessage());
-    }
     boolean confirmed = input.confirmed();
     boolean allowByEmail =
         projectCache
             .checkedGet(notes.getProjectName())
             .is(BooleanProjectConfig.ENABLE_REVIEWER_BY_EMAIL);
 
-    ReviewerAddition byAccountId =
-        addByAccountId(db, input, notes, user, accountsToNotify, allowGroup, allowByEmail);
+    ReviewerAddition byAccountId = addByAccountId(input, notes, user);
 
     ReviewerAddition wholeGroup = null;
-    if (byAccountId == null || !byAccountId.exactMatchFound) {
-      wholeGroup =
-          addWholeGroup(
-              db, input, notes, user, accountsToNotify, confirmed, allowGroup, allowByEmail);
+    if (!byAccountId.exactMatchFound) {
+      wholeGroup = addWholeGroup(input, notes, user, confirmed, allowGroup, allowByEmail);
       if (wholeGroup != null && wholeGroup.exactMatchFound) {
         return wholeGroup;
       }
     }
 
-    if (byAccountId != null) {
+    if (wholeGroup != null
+        && byAccountId.failureType == FailureType.NOT_FOUND
+        && wholeGroup.failureType == FailureType.NOT_FOUND) {
+      return fail(
+          byAccountId.input,
+          FailureType.NOT_FOUND,
+          byAccountId.result.error + "\n" + wholeGroup.result.error);
+    }
+
+    if (byAccountId.failureType != FailureType.NOT_FOUND) {
       return byAccountId;
     }
     if (wholeGroup != null) {
       return wholeGroup;
     }
 
-    return addByEmail(db, input, notes, user, accountsToNotify);
+    return addByEmail(input, notes, user);
   }
 
   public ReviewerAddition ccCurrentUser(CurrentUser user, RevisionResource revision) {
@@ -249,58 +237,30 @@
         revision.getUser(),
         ImmutableSet.of(user.getAccountId()),
         null,
-        ImmutableListMultimap.of(),
         true);
   }
 
   @Nullable
   private ReviewerAddition addByAccountId(
-      ReviewDb db,
-      AddReviewerInput input,
-      ChangeNotes notes,
-      CurrentUser user,
-      ListMultimap<RecipientType, Account.Id> accountsToNotify,
-      boolean allowGroup,
-      boolean allowByEmail)
-      throws OrmException, PermissionBackendException, IOException, ConfigInvalidException {
+      AddReviewerInput input, ChangeNotes notes, CurrentUser user)
+      throws PermissionBackendException, IOException, ConfigInvalidException {
     IdentifiedUser reviewerUser;
     boolean exactMatchFound = false;
     try {
-      reviewerUser = accountResolver.parse(input.reviewer);
+      reviewerUser = accountResolver.resolve(input.reviewer).asUniqueUser();
       if (input.reviewer.equalsIgnoreCase(reviewerUser.getName())
           || input.reviewer.equals(String.valueOf(reviewerUser.getAccountId()))) {
         exactMatchFound = true;
       }
-    } catch (UnprocessableEntityException | AuthException e) {
-      // AuthException won't occur since the user is authenticated at this point.
-      if (!allowGroup && !allowByEmail) {
-        // Only return failure if we aren't going to try other interpretations.
-        return fail(
-            input,
-            FailureType.NOT_FOUND,
-            MessageFormat.format(ChangeMessages.get().reviewerNotFoundUser, input.reviewer));
-      }
-      return null;
+    } catch (UnprocessableEntityException e) {
+      // Caller might choose to ignore this NOT_FOUND result if they find another result e.g. by
+      // group, but if not, the error message will be useful.
+      return fail(input, FailureType.NOT_FOUND, e.getMessage());
     }
 
-    if (isValidReviewer(db, notes.getChange().getDest(), reviewerUser.getAccount())) {
+    if (isValidReviewer(notes.getChange().getDest(), reviewerUser.getAccount())) {
       return new ReviewerAddition(
-          input,
-          notes,
-          user,
-          ImmutableSet.of(reviewerUser.getAccountId()),
-          null,
-          accountsToNotify,
-          exactMatchFound);
-    }
-    if (!reviewerUser.getAccount().isActive()) {
-      if (allowByEmail && input.state() == CC) {
-        return null;
-      }
-      return fail(
-          input,
-          FailureType.OTHER,
-          MessageFormat.format(ChangeMessages.get().reviewerInactive, input.reviewer));
+          input, notes, user, ImmutableSet.of(reviewerUser.getAccountId()), null, exactMatchFound);
     }
     return fail(
         input,
@@ -310,11 +270,9 @@
 
   @Nullable
   private ReviewerAddition addWholeGroup(
-      ReviewDb db,
       AddReviewerInput input,
       ChangeNotes notes,
       CurrentUser user,
-      ListMultimap<RecipientType, Account.Id> accountsToNotify,
       boolean confirmed,
       boolean allowGroup,
       boolean allowByEmail)
@@ -357,6 +315,8 @@
     // reviewers
     int maxAllowed = cfg.getInt("addreviewer", "maxAllowed", DEFAULT_MAX_REVIEWERS);
     if (maxAllowed > 0 && members.size() > maxAllowed) {
+      logger.atFine().log(
+          "Adding %d group members is not allowed (maxAllowed = %d)", members.size(), maxAllowed);
       return fail(
           input,
           FailureType.OTHER,
@@ -367,6 +327,9 @@
     int maxWithoutConfirmation =
         cfg.getInt("addreviewer", "maxWithoutConfirmation", DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK);
     if (!confirmed && maxWithoutConfirmation > 0 && members.size() > maxWithoutConfirmation) {
+      logger.atFine().log(
+          "Adding %d group members as reviewer requires confirmation (maxWithoutConfirmation = %d)",
+          members.size(), maxWithoutConfirmation);
       return fail(
           input,
           FailureType.OTHER,
@@ -376,28 +339,19 @@
     }
 
     for (Account member : members) {
-      if (isValidReviewer(db, notes.getChange().getDest(), member)) {
+      if (isValidReviewer(notes.getChange().getDest(), member)) {
         reviewers.add(member.getId());
       }
     }
 
-    return new ReviewerAddition(input, notes, user, reviewers, null, accountsToNotify, true);
+    return new ReviewerAddition(input, notes, user, reviewers, null, true);
   }
 
   @Nullable
-  private ReviewerAddition addByEmail(
-      ReviewDb db,
-      AddReviewerInput input,
-      ChangeNotes notes,
-      CurrentUser user,
-      ListMultimap<RecipientType, Account.Id> accountsToNotify)
+  private ReviewerAddition addByEmail(AddReviewerInput input, ChangeNotes notes, CurrentUser user)
       throws PermissionBackendException {
     try {
-      permissionBackend
-          .user(anonymousProvider.get())
-          .database(db)
-          .change(notes)
-          .check(ChangePermission.READ);
+      permissionBackend.user(anonymousProvider.get()).change(notes).check(ChangePermission.READ);
     } catch (AuthException e) {
       return fail(
           input,
@@ -405,13 +359,6 @@
           MessageFormat.format(ChangeMessages.get().reviewerCantSeeChange, input.reviewer));
     }
 
-    if (!migration.readChanges()) {
-      // addByEmail depends on NoteDb.
-      return fail(
-          input,
-          FailureType.NOT_FOUND,
-          MessageFormat.format(ChangeMessages.get().reviewerNotFoundUser, input.reviewer));
-    }
     Address adr = Address.tryParse(input.reviewer);
     if (adr == null || !validator.isValid(adr.getEmail())) {
       return fail(
@@ -419,25 +366,16 @@
           FailureType.NOT_FOUND,
           MessageFormat.format(ChangeMessages.get().reviewerInvalid, input.reviewer));
     }
-    return new ReviewerAddition(
-        input, notes, user, null, ImmutableList.of(adr), accountsToNotify, true);
+    return new ReviewerAddition(input, notes, user, null, ImmutableList.of(adr), true);
   }
 
-  private boolean isValidReviewer(ReviewDb db, Branch.NameKey branch, Account member)
+  private boolean isValidReviewer(Branch.NameKey branch, Account member)
       throws PermissionBackendException {
-    if (!member.isActive()) {
-      return false;
-    }
-
     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())
-          .database(db)
-          .ref(branch)
-          .check(RefPermission.READ);
+      permissionBackend.absentUser(member.getId()).ref(branch).check(RefPermission.READ);
       return true;
     } catch (AuthException e) {
       return false;
@@ -483,7 +421,6 @@
         CurrentUser caller,
         @Nullable Iterable<Account.Id> reviewers,
         @Nullable Iterable<Address> reviewersByEmail,
-        ListMultimap<RecipientType, Account.Id> accountsToNotify,
         boolean exactMatchFound) {
       checkArgument(
           reviewers != null || reviewersByEmail != null,
@@ -498,9 +435,7 @@
       this.reviewersByEmail =
           reviewersByEmail == null ? ImmutableSet.of() : ImmutableSet.copyOf(reviewersByEmail);
       this.caller = caller.asIdentifiedUser();
-      op =
-          addReviewersOpFactory.create(
-              this.reviewers, this.reviewersByEmail, state(), input.notify, accountsToNotify);
+      op = addReviewersOpFactory.create(this.reviewers, this.reviewersByEmail, state());
       this.exactMatchFound = exactMatchFound;
     }
 
@@ -512,14 +447,14 @@
           : ImmutableSet.of();
     }
 
-    public void gatherResults(ChangeData cd) throws OrmException, PermissionBackendException {
+    public void gatherResults(ChangeData cd) throws PermissionBackendException {
       checkState(op != null, "addition did not result in an update op");
       checkState(op.getResult() != null, "op did not return a result");
 
       // Generate result details and fill AccountLoader. This occurs outside
       // the Op because the accounts are in a different table.
       AddReviewersOp.Result opResult = op.getResult();
-      if (migration.readChanges() && state() == CC) {
+      if (state() == CC) {
         result.ccs = Lists.newArrayListWithCapacity(opResult.addedCCs().size());
         for (Account.Id accountId : opResult.addedCCs()) {
           result.ccs.add(json.format(new ReviewerInfo(accountId.get()), accountId, cd));
@@ -569,12 +504,11 @@
   }
 
   public ReviewerAdditionList prepare(
-      ReviewDb db,
       ChangeNotes notes,
       CurrentUser user,
       Iterable<? extends AddReviewerInput> inputs,
       boolean allowGroup)
-      throws OrmException, IOException, PermissionBackendException, ConfigInvalidException {
+      throws IOException, PermissionBackendException, ConfigInvalidException {
     // Process CC ops before reviewer ops, so a user that appears in both lists ends up as a
     // reviewer; the last call to ChangeUpdate#putReviewer wins. This can happen if the caller
     // specifies the same string twice, or less obviously if they specify multiple groups with
@@ -586,11 +520,17 @@
         Streams.stream(inputs)
             .sorted(
                 comparing(
-                    i -> i.state(), Ordering.explicit(ReviewerState.CC, ReviewerState.REVIEWER)))
+                    AddReviewerInput::state,
+                    Ordering.explicit(ReviewerState.CC, ReviewerState.REVIEWER)))
             .collect(toImmutableList());
     List<ReviewerAddition> additions = new ArrayList<>();
     for (AddReviewerInput input : sorted) {
-      additions.add(prepare(db, notes, user, input, allowGroup));
+      ReviewerAddition addition = prepare(notes, user, input, allowGroup);
+      if (addition.op != null) {
+        // Assume any callers preparing a list of batch insertions are handling their own email.
+        addition.op.suppressEmail();
+      }
+      additions.add(addition);
     }
     return new ReviewerAdditionList(additions);
   }
@@ -616,7 +556,7 @@
     // We never call updateRepo on the addition ops, which is only ok because it's a no-op.
 
     public void updateChange(ChangeContext ctx, PatchSet patchSet)
-        throws OrmException, RestApiException, IOException {
+        throws RestApiException, IOException {
       for (ReviewerAddition addition : additions()) {
         addition.op.setPatchSet(patchSet);
         addition.op.updateChange(ctx);
diff --git a/java/com/google/gerrit/server/change/ReviewerJson.java b/java/com/google/gerrit/server/change/ReviewerJson.java
index ef2c926..2742bb9 100644
--- a/java/com/google/gerrit/server/change/ReviewerJson.java
+++ b/java/com/google/gerrit/server/change/ReviewerJson.java
@@ -27,7 +27,6 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.permissions.LabelPermission;
@@ -36,9 +35,7 @@
 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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.util.Collection;
 import java.util.List;
@@ -46,7 +43,6 @@
 
 @Singleton
 public class ReviewerJson {
-  private final Provider<ReviewDb> db;
   private final PermissionBackend permissionBackend;
   private final ChangeData.Factory changeDataFactory;
   private final ApprovalsUtil approvalsUtil;
@@ -55,13 +51,11 @@
 
   @Inject
   ReviewerJson(
-      Provider<ReviewDb> db,
       PermissionBackend permissionBackend,
       ChangeData.Factory changeDataFactory,
       ApprovalsUtil approvalsUtil,
       AccountLoader.Factory accountLoaderFactory,
       SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory) {
-    this.db = db;
     this.permissionBackend = permissionBackend;
     this.changeDataFactory = changeDataFactory;
     this.approvalsUtil = approvalsUtil;
@@ -70,13 +64,13 @@
   }
 
   public List<ReviewerInfo> format(Collection<ReviewerResource> rsrcs)
-      throws OrmException, PermissionBackendException {
+      throws PermissionBackendException {
     List<ReviewerInfo> infos = Lists.newArrayListWithCapacity(rsrcs.size());
     AccountLoader loader = accountLoaderFactory.create(true);
     ChangeData cd = null;
     for (ReviewerResource rsrc : rsrcs) {
       if (cd == null || !cd.getId().equals(rsrc.getChangeId())) {
-        cd = changeDataFactory.create(db.get(), rsrc.getChangeResource().getNotes());
+        cd = changeDataFactory.create(rsrc.getChangeResource().getNotes());
       }
       ReviewerInfo info;
       if (rsrc.isByEmail()) {
@@ -93,19 +87,18 @@
     return infos;
   }
 
-  public List<ReviewerInfo> format(ReviewerResource rsrc)
-      throws OrmException, PermissionBackendException {
-    return format(ImmutableList.<ReviewerResource>of(rsrc));
+  public List<ReviewerInfo> format(ReviewerResource rsrc) throws PermissionBackendException {
+    return format(ImmutableList.of(rsrc));
   }
 
   public ReviewerInfo format(ReviewerInfo out, Account.Id reviewerAccountId, ChangeData cd)
-      throws OrmException, PermissionBackendException {
+      throws PermissionBackendException {
     PatchSet.Id psId = cd.change().currentPatchSetId();
     return format(
         out,
         reviewerAccountId,
         cd,
-        approvalsUtil.byPatchSetUser(db.get(), cd.notes(), psId, reviewerAccountId, null, null));
+        approvalsUtil.byPatchSetUser(cd.notes(), psId, reviewerAccountId, null, null));
   }
 
   public ReviewerInfo format(
@@ -113,7 +106,7 @@
       Account.Id reviewerAccountId,
       ChangeData cd,
       Iterable<PatchSetApproval> approvals)
-      throws OrmException, PermissionBackendException {
+      throws PermissionBackendException {
     LabelTypes labelTypes = cd.getLabelTypes();
 
     out.approvals = new TreeMap<>(labelTypes.nameComparator());
@@ -128,8 +121,7 @@
     // do not exist in the DB.
     PatchSet ps = cd.currentPatchSet();
     if (ps != null) {
-      PermissionBackend.ForChange perm =
-          permissionBackend.absentUser(reviewerAccountId).database(db).change(cd);
+      PermissionBackend.ForChange perm = permissionBackend.absentUser(reviewerAccountId).change(cd);
 
       for (SubmitRecord rec : submitRuleEvaluator.evaluate(cd)) {
         if (rec.labels == null) {
diff --git a/java/com/google/gerrit/server/change/RevisionJson.java b/java/com/google/gerrit/server/change/RevisionJson.java
index b67028d..aa733cd 100644
--- a/java/com/google/gerrit/server/change/RevisionJson.java
+++ b/java/com/google/gerrit/server/change/RevisionJson.java
@@ -47,7 +47,6 @@
 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.PatchSet.Id;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
@@ -66,7 +65,6 @@
 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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
@@ -156,8 +154,7 @@
    * depending on the options provided when constructing this instance.
    */
   public RevisionInfo getRevisionInfo(ChangeData cd, PatchSet in)
-      throws PatchListNotAvailableException, GpgException, OrmException, IOException,
-          PermissionBackendException {
+      throws PatchListNotAvailableException, GpgException, IOException, PermissionBackendException {
     AccountLoader accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
     try (Repository repo = openRepoIfNecessary(cd.project());
         RevWalk rw = newRevWalk(repo)) {
@@ -212,10 +209,9 @@
       AccountLoader accountLoader,
       ChangeData cd,
       Map<PatchSet.Id, PatchSet> map,
-      Optional<Id> limitToPsId,
+      Optional<PatchSet.Id> limitToPsId,
       ChangeInfo changeInfo)
-      throws PatchListNotAvailableException, GpgException, OrmException, IOException,
-          PermissionBackendException {
+      throws PatchListNotAvailableException, GpgException, IOException, PermissionBackendException {
     Map<String, RevisionInfo> res = new LinkedHashMap<>();
     try (Repository repo = openRepoIfNecessary(cd.project());
         RevWalk rw = newRevWalk(repo)) {
@@ -240,7 +236,7 @@
   }
 
   private Map<String, FetchInfo> makeFetchMap(ChangeData cd, PatchSet in)
-      throws PermissionBackendException, OrmException, IOException {
+      throws PermissionBackendException, IOException {
     Map<String, FetchInfo> r = new LinkedHashMap<>();
     for (Extension<DownloadScheme> e : downloadSchemes) {
       String schemeName = e.getExportName();
@@ -276,8 +272,7 @@
       @Nullable RevWalk rw,
       boolean fillCommit,
       @Nullable ChangeInfo changeInfo)
-      throws PatchListNotAvailableException, GpgException, OrmException, IOException,
-          PermissionBackendException {
+      throws PatchListNotAvailableException, GpgException, IOException, PermissionBackendException {
     Change c = cd.change();
     RevisionInfo out = new RevisionInfo();
     out.isCurrent = in.getId().equals(c.currentPatchSetId());
@@ -351,14 +346,13 @@
    *     lazyload}.
    */
   private PermissionBackend.ForChange permissionBackendForChange(
-      PermissionBackend.WithUser withUser, ChangeData cd) throws OrmException {
+      PermissionBackend.WithUser withUser, ChangeData cd) {
     return lazyLoad
         ? withUser.change(cd)
         : withUser.indexedChange(cd, notesFactory.createFromIndexedChange(cd.change()));
   }
 
-  private boolean isWorldReadable(ChangeData cd)
-      throws OrmException, PermissionBackendException, IOException {
+  private boolean isWorldReadable(ChangeData cd) throws PermissionBackendException, IOException {
     try {
       permissionBackendForChange(permissionBackend.user(anonymous), cd)
           .check(ChangePermission.READ);
diff --git a/java/com/google/gerrit/server/change/RevisionResource.java b/java/com/google/gerrit/server/change/RevisionResource.java
index deb5022..caafe24 100644
--- a/java/com/google/gerrit/server/change/RevisionResource.java
+++ b/java/com/google/gerrit/server/change/RevisionResource.java
@@ -34,7 +34,7 @@
   public static final TypeLiteral<RestView<RevisionResource>> REVISION_KIND =
       new TypeLiteral<RestView<RevisionResource>>() {};
 
-  public static RevisionResource createNonCachable(ChangeResource change, PatchSet ps) {
+  public static RevisionResource createNonCacheable(ChangeResource change, PatchSet ps) {
     return new RevisionResource(change, ps, Optional.empty(), false);
   }
 
@@ -52,11 +52,11 @@
   }
 
   private RevisionResource(
-      ChangeResource change, PatchSet ps, Optional<ChangeEdit> edit, boolean cachable) {
+      ChangeResource change, PatchSet ps, Optional<ChangeEdit> edit, boolean cacheable) {
     this.change = change;
     this.ps = ps;
     this.edit = edit;
-    this.cacheable = cachable;
+    this.cacheable = cacheable;
   }
 
   public boolean isCacheable() {
diff --git a/java/com/google/gerrit/server/change/SetAssigneeOp.java b/java/com/google/gerrit/server/change/SetAssigneeOp.java
index f61e95f..8d350c3 100644
--- a/java/com/google/gerrit/server/change/SetAssigneeOp.java
+++ b/java/com/google/gerrit/server/change/SetAssigneeOp.java
@@ -32,7 +32,6 @@
 import com.google.gerrit.server.update.Context;
 import com.google.gerrit.server.validators.AssigneeValidationListener;
 import com.google.gerrit.server.validators.ValidationException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
@@ -74,7 +73,7 @@
   }
 
   @Override
-  public boolean updateChange(ChangeContext ctx) throws OrmException, RestApiException {
+  public boolean updateChange(ChangeContext ctx) throws RestApiException {
     change = ctx.getChange();
     if (newAssignee.getAccountId().equals(change.getAssignee())) {
       return false;
@@ -99,7 +98,7 @@
     return true;
   }
 
-  private void addMessage(ChangeContext ctx, ChangeUpdate update) throws OrmException {
+  private void addMessage(ChangeContext ctx, ChangeUpdate update) {
     StringBuilder msg = new StringBuilder();
     msg.append("Assignee ");
     if (oldAssignee == null) {
@@ -113,11 +112,11 @@
     }
     ChangeMessage cmsg =
         ChangeMessagesUtil.newMessage(ctx, msg.toString(), ChangeMessagesUtil.TAG_SET_ASSIGNEE);
-    cmUtil.addChangeMessage(ctx.getDb(), update, cmsg);
+    cmUtil.addChangeMessage(update, cmsg);
   }
 
   @Override
-  public void postUpdate(Context ctx) throws OrmException {
+  public void postUpdate(Context ctx) {
     try {
       SetAssigneeSender cm =
           setAssigneeSenderFactory.create(
diff --git a/java/com/google/gerrit/server/change/SetHashtagsOp.java b/java/com/google/gerrit/server/change/SetHashtagsOp.java
index d11b2df..abc4eee 100644
--- a/java/com/google/gerrit/server/change/SetHashtagsOp.java
+++ b/java/com/google/gerrit/server/change/SetHashtagsOp.java
@@ -32,14 +32,12 @@
 import com.google.gerrit.server.extensions.events.HashtagsEdited;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
 import com.google.gerrit.server.validators.HashtagValidationListener;
 import com.google.gerrit.server.validators.ValidationException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
@@ -52,7 +50,6 @@
     SetHashtagsOp create(HashtagsInput input);
   }
 
-  private final NotesMigration notesMigration;
   private final ChangeMessagesUtil cmUtil;
   private final PluginSetContext<HashtagValidationListener> validationListeners;
   private final HashtagsEdited hashtagsEdited;
@@ -67,12 +64,10 @@
 
   @Inject
   SetHashtagsOp(
-      NotesMigration notesMigration,
       ChangeMessagesUtil cmUtil,
       PluginSetContext<HashtagValidationListener> validationListeners,
       HashtagsEdited hashtagsEdited,
       @Assisted @Nullable HashtagsInput input) {
-    this.notesMigration = notesMigration;
     this.cmUtil = cmUtil;
     this.validationListeners = validationListeners;
     this.hashtagsEdited = hashtagsEdited;
@@ -86,11 +81,7 @@
 
   @Override
   public boolean updateChange(ChangeContext ctx)
-      throws AuthException, BadRequestException, MethodNotAllowedException, OrmException,
-          IOException {
-    if (!notesMigration.readChanges()) {
-      throw new MethodNotAllowedException("Cannot add hashtags; NoteDb is disabled");
-    }
+      throws AuthException, BadRequestException, MethodNotAllowedException, IOException {
     if (input == null || (input.add == null && input.remove == null)) {
       updatedHashtags = ImmutableSortedSet.of();
       return false;
@@ -125,13 +116,13 @@
     }
   }
 
-  private void addMessage(ChangeContext ctx, ChangeUpdate update) throws OrmException {
+  private void addMessage(ChangeContext ctx, ChangeUpdate update) {
     StringBuilder msg = new StringBuilder();
     appendHashtagMessage(msg, "added", toAdd);
     appendHashtagMessage(msg, "removed", toRemove);
     ChangeMessage cmsg =
         ChangeMessagesUtil.newMessage(ctx, msg.toString(), ChangeMessagesUtil.TAG_SET_HASHTAGS);
-    cmUtil.addChangeMessage(ctx.getDb(), update, cmsg);
+    cmUtil.addChangeMessage(update, cmsg);
   }
 
   private void appendHashtagMessage(StringBuilder b, String action, Set<String> hashtags) {
@@ -153,7 +144,7 @@
   }
 
   @Override
-  public void postUpdate(Context ctx) throws OrmException {
+  public void postUpdate(Context ctx) {
     if (updated() && fireEvent) {
       hashtagsEdited.fire(
           change, ctx.getAccount(), updatedHashtags, toAdd, toRemove, ctx.getWhen());
diff --git a/java/com/google/gerrit/server/change/SetPrivateOp.java b/java/com/google/gerrit/server/change/SetPrivateOp.java
index b7f444c..1600fd5 100644
--- a/java/com/google/gerrit/server/change/SetPrivateOp.java
+++ b/java/com/google/gerrit/server/change/SetPrivateOp.java
@@ -16,11 +16,13 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.Nullable;
+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;
 import com.google.gerrit.server.extensions.events.PrivateStateChanged;
 import com.google.gerrit.server.notedb.ChangeNotes;
@@ -28,7 +30,6 @@
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -44,7 +45,7 @@
   }
 
   public interface Factory {
-    SetPrivateOp create(ChangeMessagesUtil cmUtil, boolean isPrivate, @Nullable Input input);
+    SetPrivateOp create(boolean isPrivate, @Nullable Input input);
   }
 
   private final PrivateStateChanged privateStateChanged;
@@ -55,12 +56,13 @@
 
   private Change change;
   private PatchSet ps;
+  private boolean isNoOp;
 
   @Inject
   SetPrivateOp(
       PrivateStateChanged privateStateChanged,
       PatchSetUtil psUtil,
-      @Assisted ChangeMessagesUtil cmUtil,
+      ChangeMessagesUtil cmUtil,
       @Assisted boolean isPrivate,
       @Assisted @Nullable Input input) {
     this.privateStateChanged = privateStateChanged;
@@ -71,10 +73,21 @@
   }
 
   @Override
-  public boolean updateChange(ChangeContext ctx) throws ResourceConflictException, OrmException {
+  public boolean updateChange(ChangeContext ctx)
+      throws ResourceConflictException, BadRequestException {
     change = ctx.getChange();
+    if (ctx.getChange().isPrivate() == isPrivate) {
+      // No-op
+      isNoOp = true;
+      return false;
+    }
+
+    if (isPrivate && !change.isNew()) {
+      throw new BadRequestException(
+          String.format("cannot set %s change to private", ChangeUtil.status(change)));
+    }
     ChangeNotes notes = ctx.getNotes();
-    ps = psUtil.get(ctx.getDb(), notes, change.currentPatchSetId());
+    ps = psUtil.get(notes, change.currentPatchSetId());
     ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId());
     change.setPrivate(isPrivate);
     change.setLastUpdatedOn(ctx.getWhen());
@@ -85,10 +98,12 @@
 
   @Override
   public void postUpdate(Context ctx) {
-    privateStateChanged.fire(change, ps, ctx.getAccount(), ctx.getWhen());
+    if (!isNoOp) {
+      privateStateChanged.fire(change, ps, ctx.getAccount(), ctx.getWhen());
+    }
   }
 
-  private void addMessage(ChangeContext ctx, ChangeUpdate update) throws OrmException {
+  private void addMessage(ChangeContext ctx, ChangeUpdate update) {
     Change c = ctx.getChange();
     StringBuilder buf = new StringBuilder(c.isPrivate() ? "Set private" : "Unset private");
 
@@ -105,6 +120,6 @@
             c.isPrivate()
                 ? ChangeMessagesUtil.TAG_SET_PRIVATE
                 : ChangeMessagesUtil.TAG_UNSET_PRIVATE);
-    cmUtil.addChangeMessage(ctx.getDb(), update, cmsg);
+    cmUtil.addChangeMessage(update, cmsg);
   }
 }
diff --git a/java/com/google/gerrit/server/change/TestSubmitInput.java b/java/com/google/gerrit/server/change/TestSubmitInput.java
index b681bf8..bb85e66 100644
--- a/java/com/google/gerrit/server/change/TestSubmitInput.java
+++ b/java/com/google/gerrit/server/change/TestSubmitInput.java
@@ -1,3 +1,17 @@
+// 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.change;
 
 import com.google.common.annotations.VisibleForTesting;
diff --git a/java/com/google/gerrit/server/change/WalkSorter.java b/java/com/google/gerrit/server/change/WalkSorter.java
index 916a62b..5945a0c 100644
--- a/java/com/google/gerrit/server/change/WalkSorter.java
+++ b/java/com/google/gerrit/server/change/WalkSorter.java
@@ -24,11 +24,11 @@
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.Ordering;
 import com.google.common.flogger.FluentLogger;
+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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.ArrayDeque;
@@ -73,7 +73,7 @@
                 }
                 try {
                   return in.get(0).data().change().getProject();
-                } catch (OrmException e) {
+                } catch (StorageException e) {
                   throw new IllegalStateException(e);
                 }
               });
@@ -98,7 +98,7 @@
     return this;
   }
 
-  public Iterable<PatchSetData> sort(Iterable<ChangeData> in) throws OrmException, IOException {
+  public Iterable<PatchSetData> sort(Iterable<ChangeData> in) throws IOException {
     ListMultimap<Project.NameKey, ChangeData> byProject =
         MultimapBuilder.hashKeys().arrayListValues().build();
     for (ChangeData cd : in) {
@@ -114,7 +114,7 @@
   }
 
   private List<PatchSetData> sortProject(Project.NameKey project, Collection<ChangeData> in)
-      throws OrmException, IOException {
+      throws IOException {
     try (Repository repo = repoManager.openRepository(project);
         RevWalk rw = new RevWalk(repo)) {
       rw.setRetainBody(retainBody);
@@ -217,7 +217,7 @@
   }
 
   private ListMultimap<RevCommit, PatchSetData> byCommit(RevWalk rw, Collection<ChangeData> in)
-      throws OrmException, IOException {
+      throws IOException {
     ListMultimap<RevCommit, PatchSetData> byCommit =
         MultimapBuilder.hashKeys(in.size()).arrayListValues(1).build();
     for (ChangeData cd : in) {
diff --git a/java/com/google/gerrit/server/change/WorkInProgressOp.java b/java/com/google/gerrit/server/change/WorkInProgressOp.java
index 71cb4c4..f3f1a29 100644
--- a/java/com/google/gerrit/server/change/WorkInProgressOp.java
+++ b/java/com/google/gerrit/server/change/WorkInProgressOp.java
@@ -14,30 +14,21 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableListMultimap;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.restapi.AuthException;
 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.PatchSetUtil;
 import com.google.gerrit.server.extensions.events.WorkInProgressStateChanged;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
-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.ProjectPermission;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -59,42 +50,14 @@
     WorkInProgressOp create(boolean workInProgress, Input in);
   }
 
-  public static void checkPermissions(
-      PermissionBackend permissionBackend, CurrentUser user, Change change)
-      throws PermissionBackendException, AuthException {
-    if (!user.isIdentifiedUser()) {
-      throw new AuthException("Authentication required");
-    }
-
-    if (change.getOwner().equals(user.asIdentifiedUser().getAccountId())) {
-      return;
-    }
-
-    try {
-      permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
-      return;
-    } catch (AuthException e) {
-      // Skip.
-    }
-
-    try {
-      permissionBackend
-          .user(user)
-          .project(change.getProject())
-          .check(ProjectPermission.WRITE_CONFIG);
-    } catch (AuthException exp) {
-      throw new AuthException("not allowed to toggle work in progress", exp);
-    }
-  }
-
   private final ChangeMessagesUtil cmUtil;
   private final EmailReviewComments.Factory email;
   private final PatchSetUtil psUtil;
   private final boolean workInProgress;
   private final Input in;
-  private final NotifyHandling notify;
   private final WorkInProgressStateChanged stateChanged;
 
+  private boolean sendEmail = true;
   private Change change;
   private ChangeNotes notes;
   private PatchSet ps;
@@ -114,16 +77,17 @@
     this.stateChanged = stateChanged;
     this.workInProgress = workInProgress;
     this.in = in;
-    notify =
-        MoreObjects.firstNonNull(
-            in.notify, workInProgress ? NotifyHandling.NONE : NotifyHandling.ALL);
+  }
+
+  public void suppressEmail() {
+    this.sendEmail = false;
   }
 
   @Override
-  public boolean updateChange(ChangeContext ctx) throws OrmException {
+  public boolean updateChange(ChangeContext ctx) {
     change = ctx.getChange();
     notes = ctx.getNotes();
-    ps = psUtil.get(ctx.getDb(), ctx.getNotes(), change.currentPatchSetId());
+    ps = psUtil.get(ctx.getNotes(), change.currentPatchSetId());
     ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId());
     change.setWorkInProgress(workInProgress);
     if (!change.hasReviewStarted() && !workInProgress) {
@@ -135,7 +99,7 @@
     return true;
   }
 
-  private void addMessage(ChangeContext ctx, ChangeUpdate update) throws OrmException {
+  private void addMessage(ChangeContext ctx, ChangeUpdate update) {
     Change c = ctx.getChange();
     StringBuilder buf =
         new StringBuilder(c.isWorkInProgress() ? "Set Work In Progress" : "Set Ready For Review");
@@ -154,19 +118,21 @@
                 ? ChangeMessagesUtil.TAG_SET_WIP
                 : ChangeMessagesUtil.TAG_SET_READY);
 
-    cmUtil.addChangeMessage(ctx.getDb(), update, cmsg);
+    cmUtil.addChangeMessage(update, cmsg);
   }
 
   @Override
   public void postUpdate(Context ctx) {
     stateChanged.fire(change, ps, ctx.getAccount(), ctx.getWhen());
-    if (workInProgress || notify.ordinal() < NotifyHandling.OWNER_REVIEWERS.ordinal()) {
+    NotifyResolver.Result notify = ctx.getNotify(change.getId());
+    if (workInProgress
+        || notify.handling().compareTo(NotifyHandling.OWNER_REVIEWERS) < 0
+        || !sendEmail) {
       return;
     }
     email
         .create(
             notify,
-            ImmutableListMultimap.of(),
             notes,
             ps,
             ctx.getIdentifiedUser(),
diff --git a/java/com/google/gerrit/server/config/AuthConfig.java b/java/com/google/gerrit/server/config/AuthConfig.java
index d3f9186..de57d04 100644
--- a/java/com/google/gerrit/server/config/AuthConfig.java
+++ b/java/com/google/gerrit/server/config/AuthConfig.java
@@ -22,8 +22,8 @@
 import com.google.gerrit.extensions.client.GitBasicAuthPolicy;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.auth.openid.OpenIdProviderPattern;
-import com.google.gwtjsonrpc.server.SignedToken;
-import com.google.gwtjsonrpc.server.XsrfException;
+import com.google.gerrit.server.mail.SignedToken;
+import com.google.gerrit.server.mail.XsrfException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.ArrayList;
diff --git a/java/com/google/gerrit/server/config/CacheResource.java b/java/com/google/gerrit/server/config/CacheResource.java
index ffa7b5a..7a835b1 100644
--- a/java/com/google/gerrit/server/config/CacheResource.java
+++ b/java/com/google/gerrit/server/config/CacheResource.java
@@ -33,15 +33,7 @@
   }
 
   public CacheResource(String pluginName, String cacheName, Cache<?, ?> cache) {
-    this(
-        pluginName,
-        cacheName,
-        new Provider<Cache<?, ?>>() {
-          @Override
-          public Cache<?, ?> get() {
-            return cache;
-          }
-        });
+    this(pluginName, cacheName, () -> cache);
   }
 
   public String getName() {
diff --git a/java/com/google/gerrit/server/config/ChangeCleanupConfig.java b/java/com/google/gerrit/server/config/ChangeCleanupConfig.java
index f5c9fc2..4d41ed7 100644
--- a/java/com/google/gerrit/server/config/ChangeCleanupConfig.java
+++ b/java/com/google/gerrit/server/config/ChangeCleanupConfig.java
@@ -29,6 +29,7 @@
   private static String KEY_ABANDON_AFTER = "abandonAfter";
   private static String KEY_ABANDON_IF_MERGEABLE = "abandonIfMergeable";
   private static String KEY_ABANDON_MESSAGE = "abandonMessage";
+  private static String KEY_CLEANUP_ACCOUNT_PATCH_REVIEW = "cleanupAccountPatchReview";
   private static String DEFAULT_ABANDON_MESSAGE =
       "Auto-Abandoned due to inactivity, see "
           + "${URL}\n"
@@ -39,6 +40,7 @@
   private final Optional<Schedule> schedule;
   private final long abandonAfter;
   private final boolean abandonIfMergeable;
+  private final boolean cleanupAccountPatchReview;
   private final String abandonMessage;
 
   @Inject
@@ -47,6 +49,8 @@
     schedule = ScheduleConfig.createSchedule(cfg, SECTION);
     abandonAfter = readAbandonAfter(cfg);
     abandonIfMergeable = cfg.getBoolean(SECTION, null, KEY_ABANDON_IF_MERGEABLE, true);
+    cleanupAccountPatchReview =
+        cfg.getBoolean(SECTION, null, KEY_CLEANUP_ACCOUNT_PATCH_REVIEW, false);
     abandonMessage = readAbandonMessage(cfg);
   }
 
@@ -73,6 +77,10 @@
     return abandonIfMergeable;
   }
 
+  public boolean getCleanupAccountPatchReview() {
+    return cleanupAccountPatchReview;
+  }
+
   public String getAbandonMessage() {
     String docUrl =
         urlFormatter.get().getDocUrl("user-change-cleanup.html", "auto-abandon").orElse("");
diff --git a/java/com/google/gerrit/server/config/ConfigResource.java b/java/com/google/gerrit/server/config/ConfigResource.java
index ec0e0c2..f2b7c8e 100644
--- a/java/com/google/gerrit/server/config/ConfigResource.java
+++ b/java/com/google/gerrit/server/config/ConfigResource.java
@@ -14,11 +14,22 @@
 
 package com.google.gerrit.server.config;
 
+import com.google.gerrit.extensions.restapi.CacheControl;
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.inject.TypeLiteral;
+import java.util.concurrent.TimeUnit;
 
 public class ConfigResource implements RestResource {
   public static final TypeLiteral<RestView<ConfigResource>> CONFIG_KIND =
       new TypeLiteral<RestView<ConfigResource>>() {};
+
+  /**
+   * Default cache control that gets set on the 'Cache-Control' header for responses on this
+   * resource that are cacheable.
+   *
+   * <p>Not all resources are cacheable and in fact the vast majority might not be. Caching is a
+   * trade-off between the freshness of data and the number of QPS that the web UI sends.
+   */
+  public static CacheControl DEFAULT_CACHE_CONTROL = CacheControl.PRIVATE(300, TimeUnit.SECONDS);
 }
diff --git a/java/com/google/gerrit/server/config/ConfigUpdatedEvent.java b/java/com/google/gerrit/server/config/ConfigUpdatedEvent.java
index a7aff83..b37e489 100644
--- a/java/com/google/gerrit/server/config/ConfigUpdatedEvent.java
+++ b/java/com/google/gerrit/server/config/ConfigUpdatedEvent.java
@@ -37,7 +37,7 @@
  * (+ various overloaded versions of these)
  */
 public class ConfigUpdatedEvent {
-  public static final Multimap<UpdateResult, ConfigUpdateEntry> NO_UPDATES =
+  public static final ImmutableMultimap<UpdateResult, ConfigUpdateEntry> NO_UPDATES =
       new ImmutableMultimap.Builder<UpdateResult, ConfigUpdateEntry>().build();
   private final Config oldConfig;
   private final Config newConfig;
diff --git a/java/com/google/gerrit/server/config/DisableReverseDnsLookup.java b/java/com/google/gerrit/server/config/DisableReverseDnsLookup.java
deleted file mode 100644
index 336edeb..0000000
--- a/java/com/google/gerrit/server/config/DisableReverseDnsLookup.java
+++ /dev/null
@@ -1,24 +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.config;
-
-import static java.lang.annotation.RetentionPolicy.RUNTIME;
-
-import com.google.inject.BindingAnnotation;
-import java.lang.annotation.Retention;
-
-@Retention(RUNTIME)
-@BindingAnnotation
-public @interface DisableReverseDnsLookup {}
diff --git a/java/com/google/gerrit/server/config/DisableReverseDnsLookupProvider.java b/java/com/google/gerrit/server/config/DisableReverseDnsLookupProvider.java
deleted file mode 100644
index 87d6bac..0000000
--- a/java/com/google/gerrit/server/config/DisableReverseDnsLookupProvider.java
+++ /dev/null
@@ -1,33 +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.config;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import org.eclipse.jgit.lib.Config;
-
-public class DisableReverseDnsLookupProvider implements Provider<Boolean> {
-  private final boolean disableReverseDnsLookup;
-
-  @Inject
-  DisableReverseDnsLookupProvider(@GerritServerConfig Config config) {
-    disableReverseDnsLookup = config.getBoolean("gerrit", null, "disableReverseDnsLookup", false);
-  }
-
-  @Override
-  public Boolean get() {
-    return disableReverseDnsLookup;
-  }
-}
diff --git a/java/com/google/gerrit/server/config/EnableReverseDnsLookup.java b/java/com/google/gerrit/server/config/EnableReverseDnsLookup.java
new file mode 100644
index 0000000..ec57338
--- /dev/null
+++ b/java/com/google/gerrit/server/config/EnableReverseDnsLookup.java
@@ -0,0 +1,24 @@
+// 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.config;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+import java.lang.annotation.Retention;
+
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface EnableReverseDnsLookup {}
diff --git a/java/com/google/gerrit/server/config/EnableReverseDnsLookupProvider.java b/java/com/google/gerrit/server/config/EnableReverseDnsLookupProvider.java
new file mode 100644
index 0000000..71086a9
--- /dev/null
+++ b/java/com/google/gerrit/server/config/EnableReverseDnsLookupProvider.java
@@ -0,0 +1,33 @@
+// 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.config;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import org.eclipse.jgit.lib.Config;
+
+public class EnableReverseDnsLookupProvider implements Provider<Boolean> {
+  private final Boolean enableReverseDnsLookup;
+
+  @Inject
+  EnableReverseDnsLookupProvider(@GerritServerConfig Config config) {
+    enableReverseDnsLookup = config.getBoolean("gerrit", null, "enableReverseDnsLookup", false);
+  }
+
+  @Override
+  public Boolean get() {
+    return enableReverseDnsLookup;
+  }
+}
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 7882ec7..59a55ab 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.extensions.config.DownloadScheme;
 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.AccountIndexedListener;
 import com.google.gerrit.extensions.events.AgreementSignupListener;
 import com.google.gerrit.extensions.events.AssigneeChangedListener;
@@ -75,14 +76,13 @@
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.CmdLineParserModule;
 import com.google.gerrit.server.CreateGroupPermissionSyncer;
+import com.google.gerrit.server.DynamicOptions;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.account.AccountCacheImpl;
 import com.google.gerrit.server.account.AccountControl;
 import com.google.gerrit.server.account.AccountDeactivator;
 import com.google.gerrit.server.account.AccountExternalIdCreator;
 import com.google.gerrit.server.account.AccountManager;
-import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.account.AccountVisibilityProvider;
 import com.google.gerrit.server.account.CapabilityCollection;
 import com.google.gerrit.server.account.EmailExpander;
@@ -98,6 +98,7 @@
 import com.google.gerrit.server.cache.CacheRemovalListener;
 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.ChangeFinder;
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.ChangeKindCacheImpl;
@@ -116,6 +117,7 @@
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.git.MergedByPushOp;
 import com.google.gerrit.server.git.NotesBranchUtil;
+import com.google.gerrit.server.git.PureRevertCache;
 import com.google.gerrit.server.git.ReceivePackInitializer;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.TransferConfig;
@@ -160,8 +162,8 @@
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 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.gerrit.server.query.change.ConflictsCacheImpl;
+import com.google.gerrit.server.quota.QuotaEnforcer;
 import com.google.gerrit.server.restapi.change.SuggestReviewers;
 import com.google.gerrit.server.restapi.group.GroupModule;
 import com.google.gerrit.server.rules.DefaultSubmitRule;
@@ -213,7 +215,6 @@
     bind(IdGenerator.class);
     bind(RulesCache.class);
     bind(BlameCache.class).to(BlameCacheImpl.class);
-    bind(Sequences.class);
     install(authModule);
     install(AccountCacheImpl.module());
     install(BatchUpdate.module());
@@ -229,6 +230,7 @@
     install(SubmitStrategy.module());
     install(TagCache.module());
     install(OAuthTokenCache.module());
+    install(PureRevertCache.module());
 
     install(new AccessControlModule());
     install(new CmdLineParserModule());
@@ -237,7 +239,7 @@
     install(new GitModule());
     install(new GroupDbModule());
     install(new GroupModule());
-    install(new NoteDbModule(cfg));
+    install(new NoteDbModule());
     install(new PrologModule());
     install(new DefaultSubmitRule.Module());
     install(new IgnoreSelfApprovalRule.Module());
@@ -245,8 +247,6 @@
     install(new SshAddressesModule());
     install(ThreadLocalRequestContext.module());
 
-    bind(AccountResolver.class);
-
     factory(CapabilityCollection.Factory.class);
     factory(ChangeData.AssistedFactory.class);
     factory(ChangeJson.AssistedFactory.class);
@@ -283,8 +283,8 @@
     bind(SoyTofu.class).annotatedWith(MailTemplates.class).toProvider(MailSoyTofuProvider.class);
     bind(FromAddressGenerator.class).toProvider(FromAddressGeneratorProvider.class).in(SINGLETON);
     bind(Boolean.class)
-        .annotatedWith(DisableReverseDnsLookup.class)
-        .toProvider(DisableReverseDnsLookupProvider.class)
+        .annotatedWith(EnableReverseDnsLookup.class)
+        .toProvider(EnableReverseDnsLookupProvider.class)
         .in(SINGLETON);
 
     bind(PatchSetInfoFactory.class);
@@ -297,6 +297,7 @@
     DynamicMap.mapOf(binder(), new TypeLiteral<Cache<?, ?>>() {});
     DynamicSet.setOf(binder(), CacheRemovalListener.class);
     DynamicMap.mapOf(binder(), CapabilityDefinition.class);
+    DynamicMap.mapOf(binder(), PluginProjectPermissionDefinition.class);
     DynamicSet.setOf(binder(), GitReferenceUpdatedListener.class);
     DynamicSet.setOf(binder(), AssigneeChangedListener.class);
     DynamicSet.setOf(binder(), ChangeAbandonedListener.class);
@@ -378,6 +379,7 @@
     DynamicItem.itemOf(binder(), MergeSuperSetComputation.class);
     DynamicItem.itemOf(binder(), ProjectNameLockManager.class);
     DynamicSet.setOf(binder(), SubmitRule.class);
+    DynamicSet.setOf(binder(), QuotaEnforcer.class);
 
     DynamicMap.mapOf(binder(), MailFilter.class);
     bind(MailFilter.class).annotatedWith(Exports.named("ListMailFilter")).to(ListMailFilter.class);
@@ -388,9 +390,10 @@
     factory(UploadValidators.Factory.class);
     DynamicSet.setOf(binder(), UploadValidationListener.class);
 
+    DynamicMap.mapOf(binder(), DynamicOptions.DynamicBean.class);
     DynamicMap.mapOf(binder(), ChangeQueryBuilder.ChangeOperatorFactory.class);
     DynamicMap.mapOf(binder(), ChangeQueryBuilder.ChangeHasOperandFactory.class);
-    DynamicMap.mapOf(binder(), ChangeQueryProcessor.ChangeAttributeFactory.class);
+    DynamicSet.setOf(binder(), ChangeAttributeFactory.class);
 
     install(new GitwebConfig.LegacyModule(cfg));
 
diff --git a/java/com/google/gerrit/server/config/GerritOptions.java b/java/com/google/gerrit/server/config/GerritOptions.java
index 0192ddd..17b65c9 100644
--- a/java/com/google/gerrit/server/config/GerritOptions.java
+++ b/java/com/google/gerrit/server/config/GerritOptions.java
@@ -14,29 +14,21 @@
 
 package com.google.gerrit.server.config;
 
-import org.eclipse.jgit.lib.Config;
-
 public class GerritOptions {
   private final boolean headless;
   private final boolean slave;
-  private final boolean enableGwtUi;
   private final boolean forcePolyGerritDev;
 
-  public GerritOptions(Config cfg, boolean headless, boolean slave, boolean forcePolyGerritDev) {
-    this.slave = slave;
-    this.enableGwtUi = cfg.getBoolean("gerrit", null, "enableGwtUi", true);
-    this.forcePolyGerritDev = forcePolyGerritDev;
+  public GerritOptions(boolean headless, boolean slave, boolean forcePolyGerritDev) {
     this.headless = headless;
+    this.slave = slave;
+    this.forcePolyGerritDev = forcePolyGerritDev;
   }
 
   public boolean headless() {
     return headless;
   }
 
-  public boolean enableGwtUi() {
-    return !headless && enableGwtUi;
-  }
-
   public boolean enableMasterFeatures() {
     return !slave;
   }
diff --git a/java/com/google/gerrit/server/config/GerritRequestModule.java b/java/com/google/gerrit/server/config/GerritRequestModule.java
index 6a1103f..6732513 100644
--- a/java/com/google/gerrit/server/config/GerritRequestModule.java
+++ b/java/com/google/gerrit/server/config/GerritRequestModule.java
@@ -26,7 +26,6 @@
   @Override
   protected void configure() {
     bind(RequestCleanup.class).in(RequestScoped.class);
-    bind(RequestScopedReviewDbProvider.class);
     bind(IdentifiedUser.RequestFactory.class).in(SINGLETON);
   }
 }
diff --git a/java/com/google/gerrit/server/config/GerritServerConfigProvider.java b/java/com/google/gerrit/server/config/GerritServerConfigProvider.java
index 8df21da..952f7d3 100644
--- a/java/com/google/gerrit/server/config/GerritServerConfigProvider.java
+++ b/java/com/google/gerrit/server/config/GerritServerConfigProvider.java
@@ -18,7 +18,6 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.securestore.SecureStore;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -99,7 +98,7 @@
   private static void checkNoteDbConfig(FileBasedConfig noteDbConfig) {
     List<String> bad = new ArrayList<>();
     for (String section : noteDbConfig.getSections()) {
-      if (section.equals(NotesMigration.SECTION_NOTE_DB)) {
+      if (section.equals("noteDb")) {
         continue;
       }
       for (String subsection : noteDbConfig.getSubsections(section)) {
diff --git a/java/com/google/gerrit/server/config/GerritServerConfigReloader.java b/java/com/google/gerrit/server/config/GerritServerConfigReloader.java
index 09c10740..5ecf6ed 100644
--- a/java/com/google/gerrit/server/config/GerritServerConfigReloader.java
+++ b/java/com/google/gerrit/server/config/GerritServerConfigReloader.java
@@ -17,9 +17,9 @@
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.Multimap;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.server.config.ConfigUpdatedEvent.ConfigUpdateEntry;
 import com.google.gerrit.server.config.ConfigUpdatedEvent.UpdateResult;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -29,11 +29,12 @@
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final GerritServerConfigProvider configProvider;
-  private final DynamicSet<GerritConfigListener> configListeners;
+  private final PluginSetContext<GerritConfigListener> configListeners;
 
   @Inject
   GerritServerConfigReloader(
-      GerritServerConfigProvider configProvider, DynamicSet<GerritConfigListener> configListeners) {
+      GerritServerConfigProvider configProvider,
+      PluginSetContext<GerritConfigListener> configListeners) {
     this.configProvider = configProvider;
     this.configListeners = configListeners;
   }
@@ -53,9 +54,7 @@
   public Multimap<UpdateResult, ConfigUpdateEntry> fireUpdatedConfigEvent(
       ConfigUpdatedEvent event) {
     Multimap<UpdateResult, ConfigUpdateEntry> updates = ArrayListMultimap.create();
-    for (GerritConfigListener configListener : configListeners) {
-      updates.putAll(configListener.configUpdated(event));
-    }
+    configListeners.runEach(l -> updates.putAll(l.configUpdated(event)));
     return updates;
   }
 }
diff --git a/java/com/google/gerrit/server/config/GerritServerIdProvider.java b/java/com/google/gerrit/server/config/GerritServerIdProvider.java
index c609cc4..4898f55 100644
--- a/java/com/google/gerrit/server/config/GerritServerIdProvider.java
+++ b/java/com/google/gerrit/server/config/GerritServerIdProvider.java
@@ -48,9 +48,7 @@
 
     // We're not generally supposed to do work in provider constructors, but this is a bit of a
     // special case because we really need to have the ID available by the time the dbInjector
-    // is created. This even applies during MigrateToNoteDb, which otherwise would have been a
-    // reasonable place to do the ID generation. Fortunately, it's not much work, and it happens
-    // once.
+    // is created. Fortunately, it's not much work, and it happens once.
     id = generate();
     Config newCfg = readGerritConfig(sitePaths);
     newCfg.setString(SECTION, null, KEY, id);
diff --git a/java/com/google/gerrit/server/config/ProjectConfigEntry.java b/java/com/google/gerrit/server/config/ProjectConfigEntry.java
index 5515f0e..92ae10a 100644
--- a/java/com/google/gerrit/server/config/ProjectConfigEntry.java
+++ b/java/com/google/gerrit/server/config/ProjectConfigEntry.java
@@ -302,12 +302,16 @@
 
     private final GitRepositoryManager repoManager;
     private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
+    private final ProjectConfig.Factory projectConfigFactory;
 
     @Inject
     UpdateChecker(
-        GitRepositoryManager repoManager, DynamicMap<ProjectConfigEntry> pluginConfigEntries) {
+        GitRepositoryManager repoManager,
+        DynamicMap<ProjectConfigEntry> pluginConfigEntries,
+        ProjectConfig.Factory projectConfigFactory) {
       this.repoManager = repoManager;
       this.pluginConfigEntries = pluginConfigEntries;
+      this.projectConfigFactory = projectConfigFactory;
     }
 
     @Override
@@ -361,7 +365,7 @@
         return null;
       }
       try (Repository repo = repoManager.openRepository(p)) {
-        ProjectConfig pc = new ProjectConfig(p);
+        ProjectConfig pc = projectConfigFactory.create(p);
         pc.load(repo, id);
         return pc;
       }
diff --git a/java/com/google/gerrit/server/config/RequestScopedReviewDbProvider.java b/java/com/google/gerrit/server/config/RequestScopedReviewDbProvider.java
deleted file mode 100644
index fdb400b..0000000
--- a/java/com/google/gerrit/server/config/RequestScopedReviewDbProvider.java
+++ /dev/null
@@ -1,66 +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.server.config;
-
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.RequestCleanup;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
-import com.google.inject.servlet.RequestScoped;
-
-/** Provides {@link ReviewDb} database handle live only for this request. */
-@RequestScoped
-public class RequestScopedReviewDbProvider implements Provider<ReviewDb> {
-  private final SchemaFactory<ReviewDb> schema;
-  private final Provider<RequestCleanup> cleanup;
-  private ReviewDb db;
-
-  @Inject
-  public RequestScopedReviewDbProvider(
-      final SchemaFactory<ReviewDb> schema, Provider<RequestCleanup> cleanup) {
-    this.schema = schema;
-    this.cleanup = cleanup;
-  }
-
-  @SuppressWarnings("resource")
-  @Override
-  public ReviewDb get() {
-    if (db == null) {
-      ReviewDb c;
-      try {
-        c = schema.open();
-      } catch (OrmException e) {
-        throw new ProvisionException("Cannot open ReviewDb", e);
-      }
-      try {
-        cleanup
-            .get()
-            .add(
-                () -> {
-                  c.close();
-                  db = null;
-                });
-      } catch (Throwable e) {
-        c.close();
-        throw new ProvisionException("Cannot defer cleanup of ReviewDb", e);
-      }
-      db = c;
-    }
-    return db;
-  }
-}
diff --git a/java/com/google/gerrit/server/config/SitePaths.java b/java/com/google/gerrit/server/config/SitePaths.java
index 11ec50c..47b6336 100644
--- a/java/com/google/gerrit/server/config/SitePaths.java
+++ b/java/com/google/gerrit/server/config/SitePaths.java
@@ -43,7 +43,6 @@
   public final Path mail_dir;
   public final Path hooks_dir;
   public final Path static_dir;
-  public final Path themes_dir;
   public final Path index_dir;
 
   public final Path gerrit_sh;
@@ -67,8 +66,7 @@
   public final Path site_css;
   public final Path site_header;
   public final Path site_footer;
-  // For PolyGerrit UI only.
-  public final Path site_theme;
+  public final Path site_theme; // For PolyGerrit UI only.
   public final Path site_gitweb;
 
   /** {@code true} if {@link #site_path} has not been initialized. */
@@ -90,7 +88,6 @@
     mail_dir = etc_dir.resolve("mail");
     hooks_dir = p.resolve("hooks");
     static_dir = p.resolve("static");
-    themes_dir = p.resolve("themes");
     index_dir = p.resolve("index");
 
     gerrit_sh = bin_dir.resolve("gerrit.sh");
diff --git a/java/com/google/gerrit/server/config/SysExecutorModule.java b/java/com/google/gerrit/server/config/SysExecutorModule.java
index f552434..6c729d9 100644
--- a/java/com/google/gerrit/server/config/SysExecutorModule.java
+++ b/java/com/google/gerrit/server/config/SysExecutorModule.java
@@ -90,7 +90,7 @@
                     poolSize,
                     10,
                     TimeUnit.MINUTES,
-                    new ArrayBlockingQueue<Runnable>(poolSize),
+                    new ArrayBlockingQueue<>(poolSize),
                     new ThreadFactoryBuilder()
                         .setNameFormat("ChangeUpdate-%d")
                         .setDaemon(true)
diff --git a/java/com/google/gerrit/server/config/TrackingFootersProvider.java b/java/com/google/gerrit/server/config/TrackingFootersProvider.java
index ff1910d..2114b1a 100644
--- a/java/com/google/gerrit/server/config/TrackingFootersProvider.java
+++ b/java/com/google/gerrit/server/config/TrackingFootersProvider.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.config;
 
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.reviewdb.client.TrackingId;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -33,6 +32,8 @@
 public class TrackingFootersProvider implements Provider<TrackingFooters> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
+  private static final int MAX_LENGTH = 10;
+
   private static String TRACKING_ID_TAG = "trackingid";
   private static String FOOTER_TAG = "footer";
   private static String SYSTEM_TAG = "system";
@@ -59,11 +60,11 @@
         configValid = false;
         logger.atSevere().log(
             "Missing %s.%s.%s in gerrit.config", TRACKING_ID_TAG, name, SYSTEM_TAG);
-      } else if (system.length() > TrackingId.TRACKING_SYSTEM_MAX_CHAR) {
+      } else if (system.length() > MAX_LENGTH) {
         configValid = false;
         logger.atSevere().log(
             "String too long \"%s\" in gerrit.config %s.%s.%s (max %d char)",
-            system, TRACKING_ID_TAG, name, SYSTEM_TAG, TrackingId.TRACKING_SYSTEM_MAX_CHAR);
+            system, TRACKING_ID_TAG, name, SYSTEM_TAG, MAX_LENGTH);
       }
 
       String match = cfg.getString(TRACKING_ID_TAG, name, REGEX_TAG);
diff --git a/java/com/google/gerrit/server/documentation/MarkdownFormatterHeader.java b/java/com/google/gerrit/server/documentation/MarkdownFormatterHeader.java
index 00471fd..00f7ec1 100644
--- a/java/com/google/gerrit/server/documentation/MarkdownFormatterHeader.java
+++ b/java/com/google/gerrit/server/documentation/MarkdownFormatterHeader.java
@@ -18,7 +18,6 @@
 import com.vladsch.flexmark.ast.Node;
 import com.vladsch.flexmark.ext.anchorlink.AnchorLink;
 import com.vladsch.flexmark.ext.anchorlink.internal.AnchorLinkNodeRenderer;
-import com.vladsch.flexmark.html.CustomNodeRenderer;
 import com.vladsch.flexmark.html.HtmlRenderer;
 import com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension;
 import com.vladsch.flexmark.html.HtmlWriter;
@@ -57,25 +56,12 @@
 
     @Override
     public Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
-      return new HashSet<NodeRenderingHandler<? extends Node>>(
+      return new HashSet<>(
           Arrays.asList(
               new NodeRenderingHandler<>(
                   AnchorLink.class,
-                  new CustomNodeRenderer<AnchorLink>() {
-                    @Override
-                    public void render(
-                        AnchorLink node, NodeRendererContext context, HtmlWriter html) {
-                      HeadingNodeRenderer.this.render(node, context);
-                    }
-                  }),
-              new NodeRenderingHandler<>(
-                  Heading.class,
-                  new CustomNodeRenderer<Heading>() {
-                    @Override
-                    public void render(Heading node, NodeRendererContext context, HtmlWriter html) {
-                      HeadingNodeRenderer.this.render(node, context, html);
-                    }
-                  })));
+                  (node, context, html) -> HeadingNodeRenderer.this.render(node, context)),
+              new NodeRenderingHandler<>(Heading.class, HeadingNodeRenderer.this::render)));
     }
 
     void render(final AnchorLink node, final NodeRendererContext context) {
@@ -116,25 +102,15 @@
               .withAttr()
               .tagLine(
                   "h" + node.getLevel(),
-                  new Runnable() {
-                    @Override
-                    public void run() {
-                      html.srcPos(node.getText()).withAttr().tag("span");
-                      context.renderChildren(node);
-                      html.tag("/span");
-                    }
+                  () -> {
+                    html.srcPos(node.getText()).withAttr().tag("span");
+                    context.renderChildren(node);
+                    html.tag("/span");
                   });
         } else {
           html.srcPos(node.getText())
               .withAttr()
-              .tagLine(
-                  "h" + node.getLevel(),
-                  new Runnable() {
-                    @Override
-                    public void run() {
-                      context.renderChildren(node);
-                    }
-                  });
+              .tagLine("h" + node.getLevel(), () -> context.renderChildren(node));
         }
       } else {
         context.delegateRender();
diff --git a/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
index ce359a9..246a53f 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditModifier.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -23,7 +23,7 @@
 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.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
@@ -43,7 +43,6 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.util.CommitMessageUtil;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -52,6 +51,7 @@
 import java.util.List;
 import java.util.Optional;
 import java.util.TimeZone;
+import org.eclipse.jgit.dircache.InvalidPathException;
 import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.NullProgressMonitor;
@@ -80,7 +80,6 @@
 
   private final TimeZone tz;
   private final ChangeIndexer indexer;
-  private final Provider<ReviewDb> reviewDb;
   private final Provider<CurrentUser> currentUser;
   private final PermissionBackend permissionBackend;
   private final ChangeEditUtil changeEditUtil;
@@ -91,14 +90,12 @@
   ChangeEditModifier(
       @GerritPersonIdent PersonIdent gerritIdent,
       ChangeIndexer indexer,
-      Provider<ReviewDb> reviewDb,
       Provider<CurrentUser> currentUser,
       PermissionBackend permissionBackend,
       ChangeEditUtil changeEditUtil,
       PatchSetUtil patchSetUtil,
       ProjectCache projectCache) {
     this.indexer = indexer;
-    this.reviewDb = reviewDb;
     this.currentUser = currentUser;
     this.permissionBackend = permissionBackend;
     this.tz = gerritIdent.getTimeZone();
@@ -117,7 +114,7 @@
    * @throws PermissionBackendException
    */
   public void createEdit(Repository repository, ChangeNotes notes)
-      throws AuthException, IOException, InvalidChangeOperationException, OrmException,
+      throws AuthException, IOException, InvalidChangeOperationException,
           PermissionBackendException, ResourceConflictException {
     assertCanEdit(notes);
 
@@ -145,8 +142,8 @@
    * @throws PermissionBackendException
    */
   public void rebaseEdit(Repository repository, ChangeNotes notes)
-      throws AuthException, InvalidChangeOperationException, IOException, OrmException,
-          MergeConflictException, PermissionBackendException, ResourceConflictException {
+      throws AuthException, InvalidChangeOperationException, IOException, MergeConflictException,
+          PermissionBackendException, ResourceConflictException {
     assertCanEdit(notes);
 
     Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(notes);
@@ -168,7 +165,7 @@
   }
 
   private void rebase(Repository repository, ChangeEdit changeEdit, PatchSet currentPatchSet)
-      throws IOException, MergeConflictException, InvalidChangeOperationException, OrmException {
+      throws IOException, MergeConflictException, InvalidChangeOperationException {
     RevCommit currentEditCommit = changeEdit.getEditCommit();
     if (currentEditCommit.getParentCount() == 0) {
       throw new InvalidChangeOperationException(
@@ -210,7 +207,7 @@
    * @throws BadRequestException if the commit message is malformed
    */
   public void modifyMessage(Repository repository, ChangeNotes notes, String newCommitMessage)
-      throws AuthException, IOException, UnchangedCommitMessageException, OrmException,
+      throws AuthException, IOException, UnchangedCommitMessageException,
           PermissionBackendException, BadRequestException, ResourceConflictException {
     assertCanEdit(notes);
     newCommitMessage = CommitMessageUtil.checkAndSanitizeCommitMessage(newCommitMessage);
@@ -247,13 +244,14 @@
    * @param filePath the path of the file whose contents should be modified
    * @param newContent the new file content
    * @throws AuthException if the user isn't authenticated or not allowed to use change edits
+   * @throws BadRequestException if the user provided bad input (e.g. invalid file paths)
    * @throws InvalidChangeOperationException if the file already had the specified content
    * @throws PermissionBackendException
    * @throws ResourceConflictException if the project state does not permit the operation
    */
   public void modifyFile(
       Repository repository, ChangeNotes notes, String filePath, RawInput newContent)
-      throws AuthException, InvalidChangeOperationException, IOException, OrmException,
+      throws AuthException, BadRequestException, InvalidChangeOperationException, IOException,
           PermissionBackendException, ResourceConflictException {
     modifyTree(repository, notes, new ChangeFileContentModification(filePath, newContent));
   }
@@ -266,12 +264,13 @@
    * @param notes the {@link ChangeNotes} of the change whose change edit should be modified
    * @param file path of the file which should be deleted
    * @throws AuthException if the user isn't authenticated or not allowed to use change edits
+   * @throws BadRequestException if the user provided bad input (e.g. invalid file paths)
    * @throws InvalidChangeOperationException if the file does not exist
    * @throws PermissionBackendException
    * @throws ResourceConflictException if the project state does not permit the operation
    */
   public void deleteFile(Repository repository, ChangeNotes notes, String file)
-      throws AuthException, InvalidChangeOperationException, IOException, OrmException,
+      throws AuthException, BadRequestException, InvalidChangeOperationException, IOException,
           PermissionBackendException, ResourceConflictException {
     modifyTree(repository, notes, new DeleteFileModification(file));
   }
@@ -285,6 +284,7 @@
    * @param currentFilePath the current path/name of the file
    * @param newFilePath the desired path/name of the file
    * @throws AuthException if the user isn't authenticated or not allowed to use change edits
+   * @throws BadRequestException if the user provided bad input (e.g. invalid file paths)
    * @throws InvalidChangeOperationException if the file was already renamed to the specified new
    *     name
    * @throws PermissionBackendException
@@ -292,7 +292,7 @@
    */
   public void renameFile(
       Repository repository, ChangeNotes notes, String currentFilePath, String newFilePath)
-      throws AuthException, InvalidChangeOperationException, IOException, OrmException,
+      throws AuthException, BadRequestException, InvalidChangeOperationException, IOException,
           PermissionBackendException, ResourceConflictException {
     modifyTree(repository, notes, new RenameFileModification(currentFilePath, newFilePath));
   }
@@ -310,14 +310,14 @@
    * @throws PermissionBackendException
    */
   public void restoreFile(Repository repository, ChangeNotes notes, String file)
-      throws AuthException, InvalidChangeOperationException, IOException, OrmException,
+      throws AuthException, BadRequestException, InvalidChangeOperationException, IOException,
           PermissionBackendException, ResourceConflictException {
     modifyTree(repository, notes, new RestoreFileModification(file));
   }
 
   private void modifyTree(
       Repository repository, ChangeNotes notes, TreeModification treeModification)
-      throws AuthException, IOException, OrmException, InvalidChangeOperationException,
+      throws AuthException, BadRequestException, IOException, InvalidChangeOperationException,
           PermissionBackendException, ResourceConflictException {
     assertCanEdit(notes);
 
@@ -362,8 +362,8 @@
       ChangeNotes notes,
       PatchSet patchSet,
       List<TreeModification> treeModifications)
-      throws AuthException, IOException, InvalidChangeOperationException, MergeConflictException,
-          OrmException, PermissionBackendException, ResourceConflictException {
+      throws AuthException, BadRequestException, IOException, InvalidChangeOperationException,
+          MergeConflictException, PermissionBackendException, ResourceConflictException {
     assertCanEdit(notes);
 
     Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(notes);
@@ -394,27 +394,21 @@
   }
 
   private void assertCanEdit(ChangeNotes notes)
-      throws AuthException, PermissionBackendException, IOException, ResourceConflictException,
-          OrmException {
+      throws AuthException, PermissionBackendException, IOException, ResourceConflictException {
     if (!currentUser.get().isIdentifiedUser()) {
       throw new AuthException("Authentication required");
     }
 
     Change c = notes.getChange();
-    if (!c.getStatus().isOpen()) {
+    if (!c.isNew()) {
       throw new ResourceConflictException(
-          String.format(
-              "change %s is %s", c.getChangeId(), c.getStatus().toString().toLowerCase()));
+          String.format("change %s is %s", c.getChangeId(), ChangeUtil.status(c)));
     }
 
     // Not allowed to edit if the current patch set is locked.
     patchSetUtil.checkPatchSetNotLocked(notes);
     try {
-      permissionBackend
-          .currentUser()
-          .database(reviewDb)
-          .change(notes)
-          .check(ChangePermission.ADD_PATCH_SET);
+      permissionBackend.currentUser().change(notes).check(ChangePermission.ADD_PATCH_SET);
       projectCache.checkedGet(notes.getProjectName()).checkStatePermitsWrite();
     } catch (AuthException denied) {
       throw new AuthException("edit not permitted", denied);
@@ -450,14 +444,13 @@
     return changeEditUtil.byChange(notes);
   }
 
-  private PatchSet getBasePatchSet(Optional<ChangeEdit> optionalChangeEdit, ChangeNotes notes)
-      throws OrmException {
+  private PatchSet getBasePatchSet(Optional<ChangeEdit> optionalChangeEdit, ChangeNotes notes) {
     Optional<PatchSet> editBasePatchSet = optionalChangeEdit.map(ChangeEdit::getBasePatchSet);
     return editBasePatchSet.isPresent() ? editBasePatchSet.get() : lookupCurrentPatchSet(notes);
   }
 
-  private PatchSet lookupCurrentPatchSet(ChangeNotes notes) throws OrmException {
-    return patchSetUtil.current(reviewDb.get(), notes);
+  private PatchSet lookupCurrentPatchSet(ChangeNotes notes) {
+    return patchSetUtil.current(notes);
   }
 
   private static boolean isBasedOn(ChangeEdit changeEdit, PatchSet patchSet) {
@@ -480,10 +473,15 @@
 
   private static ObjectId createNewTree(
       Repository repository, RevCommit baseCommit, List<TreeModification> treeModifications)
-      throws IOException, InvalidChangeOperationException {
-    TreeCreator treeCreator = new TreeCreator(baseCommit);
-    treeCreator.addTreeModifications(treeModifications);
-    ObjectId newTreeId = treeCreator.createNewTreeAndGetId(repository);
+      throws BadRequestException, IOException, InvalidChangeOperationException {
+    ObjectId newTreeId;
+    try {
+      TreeCreator treeCreator = new TreeCreator(baseCommit);
+      treeCreator.addTreeModifications(treeModifications);
+      newTreeId = treeCreator.createNewTreeAndGetId(repository);
+    } catch (InvalidPathException e) {
+      throw new BadRequestException(e.getMessage());
+    }
 
     if (ObjectId.equals(newTreeId, baseCommit.getTree())) {
       throw new InvalidChangeOperationException("no changes were made");
@@ -543,7 +541,7 @@
       PatchSet basePatchSet,
       ObjectId newEditCommitId,
       Timestamp timestamp)
-      throws IOException, OrmException {
+      throws IOException {
     Change change = notes.getChange();
     String editRefName = getEditRefName(change, basePatchSet);
     updateReference(repository, editRefName, ObjectId.zeroId(), newEditCommitId, timestamp);
@@ -560,7 +558,7 @@
 
   private ChangeEdit updateEdit(
       Repository repository, ChangeEdit changeEdit, ObjectId newEditCommitId, Timestamp timestamp)
-      throws IOException, OrmException {
+      throws IOException {
     String editRefName = changeEdit.getRefName();
     RevCommit currentEditCommit = changeEdit.getEditCommit();
     updateReference(repository, editRefName, currentEditCommit, newEditCommitId, timestamp);
@@ -627,7 +625,7 @@
     return user.newRefLogIdent(timestamp, tz);
   }
 
-  private void reindex(Change change) throws IOException, OrmException {
-    indexer.index(reviewDb.get(), change);
+  private void reindex(Change change) {
+    indexer.index(change);
   }
 }
diff --git a/java/com/google/gerrit/server/edit/ChangeEditUtil.java b/java/com/google/gerrit/server/edit/ChangeEditUtil.java
index d5add76..cb8147c 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditUtil.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditUtil.java
@@ -16,23 +16,20 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 
-import com.google.common.collect.ListMultimap;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.api.changes.RecipientType;
+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.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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.ChangeKindCache;
+import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.change.PatchSetInserter;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.index.change.ChangeIndexer;
@@ -42,7 +39,6 @@
 import com.google.gerrit.server.update.RepoContext;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -69,7 +65,6 @@
   private final GitRepositoryManager gitManager;
   private final PatchSetInserter.Factory patchSetInserterFactory;
   private final ChangeIndexer indexer;
-  private final Provider<ReviewDb> db;
   private final Provider<CurrentUser> userProvider;
   private final ChangeKindCache changeKindCache;
   private final PatchSetUtil psUtil;
@@ -79,14 +74,12 @@
       GitRepositoryManager gitManager,
       PatchSetInserter.Factory patchSetInserterFactory,
       ChangeIndexer indexer,
-      Provider<ReviewDb> db,
       Provider<CurrentUser> userProvider,
       ChangeKindCache changeKindCache,
       PatchSetUtil psUtil) {
     this.gitManager = gitManager;
     this.patchSetInserterFactory = patchSetInserterFactory;
     this.indexer = indexer;
-    this.db = db;
     this.userProvider = userProvider;
     this.changeKindCache = changeKindCache;
     this.psUtil = psUtil;
@@ -152,9 +145,7 @@
    * @param edit change edit to publish
    * @param notify Notify handling that defines to whom email notifications should be sent after the
    *     change edit is published.
-   * @param accountsToNotify Accounts that should be notified after the change edit is published.
    * @throws IOException
-   * @throws OrmException
    * @throws UpdateException
    * @throws RestApiException
    */
@@ -162,10 +153,9 @@
       BatchUpdate.Factory updateFactory,
       ChangeNotes notes,
       CurrentUser user,
-      final ChangeEdit edit,
-      NotifyHandling notify,
-      ListMultimap<RecipientType, Account.Id> accountsToNotify)
-      throws IOException, OrmException, RestApiException, UpdateException {
+      ChangeEdit edit,
+      NotifyResolver.Result notify)
+      throws IOException, RestApiException, UpdateException {
     Change change = edit.getChange();
     try (Repository repo = gitManager.openRepository(change.getProject());
         ObjectInserter oi = repo.newObjectInserter();
@@ -178,11 +168,7 @@
 
       RevCommit squashed = squashEdit(rw, oi, edit.getEditCommit(), basePatchSet);
       PatchSet.Id psId = ChangeUtil.nextPatchSetId(repo, change.currentPatchSetId());
-      PatchSetInserter inserter =
-          patchSetInserterFactory
-              .create(notes, psId, squashed)
-              .setNotify(notify)
-              .setAccountsToNotify(accountsToNotify);
+      PatchSetInserter inserter = patchSetInserterFactory.create(notes, psId, squashed);
 
       StringBuilder message =
           new StringBuilder("Patch Set ").append(inserter.getPatchSetId().get()).append(": ");
@@ -201,9 +187,9 @@
             .append(".");
       }
 
-      try (BatchUpdate bu =
-          updateFactory.create(db.get(), change.getProject(), user, TimeUtil.nowTs())) {
+      try (BatchUpdate bu = updateFactory.create(change.getProject(), user, TimeUtil.nowTs())) {
         bu.setRepository(repo, rw, oi);
+        bu.setNotify(notify);
         bu.addOp(change.getId(), inserter.setMessage(message.toString()));
         bu.addOp(
             change.getId(),
@@ -223,14 +209,13 @@
    *
    * @param edit change edit to delete
    * @throws IOException
-   * @throws OrmException
    */
-  public void delete(ChangeEdit edit) throws IOException, OrmException {
+  public void delete(ChangeEdit edit) throws IOException {
     Change change = edit.getChange();
     try (Repository repo = gitManager.openRepository(change.getProject())) {
       deleteRef(repo, edit);
     }
-    indexer.index(db.get(), change);
+    indexer.index(change);
   }
 
   private PatchSet getBasePatchSet(ChangeNotes notes, Ref ref) throws IOException {
@@ -238,9 +223,8 @@
       int pos = ref.getName().lastIndexOf('/');
       checkArgument(pos > 0, "invalid edit ref: %s", ref.getName());
       String psId = ref.getName().substring(pos + 1);
-      return psUtil.get(
-          db.get(), notes, new PatchSet.Id(notes.getChange().getId(), Integer.parseInt(psId)));
-    } catch (OrmException | NumberFormatException e) {
+      return psUtil.get(notes, new PatchSet.Id(notes.getChange().getId(), Integer.parseInt(psId)));
+    } catch (StorageException | NumberFormatException e) {
       throw new IOException(e);
     }
   }
diff --git a/java/com/google/gerrit/server/events/EventBroker.java b/java/com/google/gerrit/server/events/EventBroker.java
index 94e9bb1..d5f548f 100644
--- a/java/com/google/gerrit/server/events/EventBroker.java
+++ b/java/com/google/gerrit/server/events/EventBroker.java
@@ -22,7 +22,6 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.ChangePermission;
@@ -35,9 +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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 /** Distributes Events to listeners if they are allowed to see them */
@@ -64,27 +61,22 @@
 
   protected final ChangeNotes.Factory notesFactory;
 
-  protected final Provider<ReviewDb> dbProvider;
-
   @Inject
   public EventBroker(
       PluginSetContext<UserScopedEventListener> listeners,
       PluginSetContext<EventListener> unrestrictedListeners,
       PermissionBackend permissionBackend,
       ProjectCache projectCache,
-      ChangeNotes.Factory notesFactory,
-      Provider<ReviewDb> dbProvider) {
+      ChangeNotes.Factory notesFactory) {
     this.listeners = listeners;
     this.unrestrictedListeners = unrestrictedListeners;
     this.permissionBackend = permissionBackend;
     this.projectCache = projectCache;
     this.notesFactory = notesFactory;
-    this.dbProvider = dbProvider;
   }
 
   @Override
-  public void postEvent(Change change, ChangeEvent event)
-      throws OrmException, PermissionBackendException {
+  public void postEvent(Change change, ChangeEvent event) throws PermissionBackendException {
     fireEvent(change, event);
   }
 
@@ -100,7 +92,7 @@
   }
 
   @Override
-  public void postEvent(Event event) throws OrmException, PermissionBackendException {
+  public void postEvent(Event event) throws PermissionBackendException {
     fireEvent(event);
   }
 
@@ -108,10 +100,9 @@
     unrestrictedListeners.runEach(l -> l.onEvent(event));
   }
 
-  protected void fireEvent(Change change, ChangeEvent event)
-      throws OrmException, PermissionBackendException {
+  protected void fireEvent(Change change, ChangeEvent event) throws PermissionBackendException {
     for (PluginSetEntryContext<UserScopedEventListener> c : listeners) {
-      CurrentUser user = c.call(l -> l.getUser());
+      CurrentUser user = c.call(UserScopedEventListener::getUser);
       if (isVisibleTo(change, user)) {
         c.run(l -> l.onEvent(event));
       }
@@ -121,7 +112,7 @@
 
   protected void fireEvent(Project.NameKey project, ProjectEvent event) {
     for (PluginSetEntryContext<UserScopedEventListener> c : listeners) {
-      CurrentUser user = c.call(l -> l.getUser());
+      CurrentUser user = c.call(UserScopedEventListener::getUser);
       if (isVisibleTo(project, user)) {
         c.run(l -> l.onEvent(event));
       }
@@ -132,7 +123,7 @@
   protected void fireEvent(Branch.NameKey branchName, RefEvent event)
       throws PermissionBackendException {
     for (PluginSetEntryContext<UserScopedEventListener> c : listeners) {
-      CurrentUser user = c.call(l -> l.getUser());
+      CurrentUser user = c.call(UserScopedEventListener::getUser);
       if (isVisibleTo(branchName, user)) {
         c.run(l -> l.onEvent(event));
       }
@@ -140,9 +131,9 @@
     fireEventForUnrestrictedListeners(event);
   }
 
-  protected void fireEvent(Event event) throws OrmException, PermissionBackendException {
+  protected void fireEvent(Event event) throws PermissionBackendException {
     for (PluginSetEntryContext<UserScopedEventListener> c : listeners) {
-      CurrentUser user = c.call(l -> l.getUser());
+      CurrentUser user = c.call(UserScopedEventListener::getUser);
       if (isVisibleTo(event, user)) {
         c.run(l -> l.onEvent(event));
       }
@@ -164,8 +155,7 @@
     }
   }
 
-  protected boolean isVisibleTo(Change change, CurrentUser user)
-      throws OrmException, PermissionBackendException {
+  protected boolean isVisibleTo(Change change, CurrentUser user) throws PermissionBackendException {
     if (change == null) {
       return false;
     }
@@ -173,12 +163,10 @@
     if (pe == null || !pe.statePermitsRead()) {
       return false;
     }
-    ReviewDb db = dbProvider.get();
     try {
       permissionBackend
           .user(user)
-          .change(notesFactory.createChecked(db, change))
-          .database(db)
+          .change(notesFactory.createChecked(change))
           .check(ChangePermission.READ);
       return true;
     } catch (AuthException e) {
@@ -201,18 +189,14 @@
     }
   }
 
-  protected boolean isVisibleTo(Event event, CurrentUser user)
-      throws OrmException, PermissionBackendException {
+  protected boolean isVisibleTo(Event event, CurrentUser user) throws PermissionBackendException {
     if (event instanceof RefEvent) {
       RefEvent refEvent = (RefEvent) event;
       String ref = refEvent.getRefName();
       if (PatchSet.isChangeRef(ref)) {
         Change.Id cid = PatchSet.Id.fromRef(ref).getParentKey();
         try {
-          Change change =
-              notesFactory
-                  .createChecked(dbProvider.get(), refEvent.getProjectNameKey(), cid)
-                  .getChange();
+          Change change = notesFactory.createChecked(refEvent.getProjectNameKey(), cid).getChange();
           return isVisibleTo(change, user);
         } catch (NoSuchChangeException e) {
           logger.atFine().log(
diff --git a/java/com/google/gerrit/server/events/EventDispatcher.java b/java/com/google/gerrit/server/events/EventDispatcher.java
index cbf547e..e6735f2 100644
--- a/java/com/google/gerrit/server/events/EventDispatcher.java
+++ b/java/com/google/gerrit/server/events/EventDispatcher.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 
 /** Interface for posting (dispatching) Events */
 public interface EventDispatcher {
@@ -27,10 +26,9 @@
    *
    * @param change The change that the event is related to
    * @param event The event to post
-   * @throws OrmException on failure to post the event due to DB error
    * @throws PermissionBackendException on failure of permission checks
    */
-  void postEvent(Change change, ChangeEvent event) throws OrmException, PermissionBackendException;
+  void postEvent(Change change, ChangeEvent event) throws PermissionBackendException;
 
   /**
    * Post a stream event that is related to a branch
@@ -56,8 +54,7 @@
    * specific postEvent methods for those use cases.
    *
    * @param event The event to post.
-   * @throws OrmException on failure to post the event due to DB error
    * @throws PermissionBackendException on failure of permission checks
    */
-  void postEvent(Event event) throws OrmException, PermissionBackendException;
+  void postEvent(Event event) throws PermissionBackendException;
 }
diff --git a/java/com/google/gerrit/server/events/EventFactory.java b/java/com/google/gerrit/server/events/EventFactory.java
index 2550efd..efd9bb9 100644
--- a/java/com/google/gerrit/server/events/EventFactory.java
+++ b/java/com/google/gerrit/server/events/EventFactory.java
@@ -24,6 +24,7 @@
 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.exceptions.StorageException;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.reviewdb.client.Account;
@@ -35,7 +36,6 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.account.AccountCache;
@@ -64,8 +64,6 @@
 import com.google.gerrit.server.patch.PatchListObjectTooLargeException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -94,7 +92,6 @@
   private final ApprovalsUtil approvalsUtil;
   private final ChangeKindCache changeKindCache;
   private final Provider<InternalChangeQuery> queryProvider;
-  private final SchemaFactory<ReviewDb> schema;
   private final IndexConfig indexConfig;
 
   @Inject
@@ -108,7 +105,6 @@
       ApprovalsUtil approvalsUtil,
       ChangeKindCache changeKindCache,
       Provider<InternalChangeQuery> queryProvider,
-      SchemaFactory<ReviewDb> schema,
       IndexConfig indexConfig) {
     this.accountCache = accountCache;
     this.urlFormatter = urlFormatter;
@@ -119,7 +115,6 @@
     this.approvalsUtil = approvalsUtil;
     this.changeKindCache = changeKindCache;
     this.queryProvider = queryProvider;
-    this.schema = schema;
     this.indexConfig = indexConfig;
   }
 
@@ -129,23 +124,7 @@
    * @param change
    * @return object suitable for serialization to JSON
    */
-  public ChangeAttribute asChangeAttribute(Change change, ChangeNotes notes) {
-    try (ReviewDb db = schema.open()) {
-      return asChangeAttribute(db, change, notes);
-    } catch (OrmException e) {
-      logger.atSevere().withCause(e).log("Cannot open database connection");
-      return new ChangeAttribute();
-    }
-  }
-
-  /**
-   * Create a ChangeAttribute for the given change suitable for serialization to JSON.
-   *
-   * @param db Review database
-   * @param change
-   * @return object suitable for serialization to JSON
-   */
-  public ChangeAttribute asChangeAttribute(ReviewDb db, Change change) {
+  public ChangeAttribute asChangeAttribute(Change change) {
     ChangeAttribute a = new ChangeAttribute();
     a.project = change.getProject().get();
     a.branch = change.getDest().getShortName();
@@ -154,7 +133,7 @@
     a.number = change.getId().get();
     a.subject = change.getSubject();
     try {
-      a.commitMessage = changeDataFactory.create(db, change).commitMessage();
+      a.commitMessage = changeDataFactory.create(change).commitMessage();
     } catch (Exception e) {
       logger.atSevere().withCause(e).log(
           "Error while getting full commit message for change %d", a.number);
@@ -172,14 +151,12 @@
   /**
    * Create a ChangeAttribute for the given change suitable for serialization to JSON.
    *
-   * @param db Review database
    * @param change
    * @param notes
    * @return object suitable for serialization to JSON
    */
-  public ChangeAttribute asChangeAttribute(ReviewDb db, Change change, ChangeNotes notes)
-      throws OrmException {
-    ChangeAttribute a = asChangeAttribute(db, change);
+  public ChangeAttribute asChangeAttribute(Change change, ChangeNotes notes) {
+    ChangeAttribute a = asChangeAttribute(change);
     Set<String> hashtags = notes.load().getHashtags();
     if (!hashtags.isEmpty()) {
       a.hashtags = new ArrayList<>(hashtags.size());
@@ -214,7 +191,7 @@
    */
   public void extend(ChangeAttribute a, Change change) {
     a.lastUpdated = change.getLastUpdatedOn().getTime() / 1000L;
-    a.open = change.getStatus().isOpen();
+    a.open = change.isNew();
   }
 
   /**
@@ -223,9 +200,8 @@
    * @param a
    * @param notes
    */
-  public void addAllReviewers(ReviewDb db, ChangeAttribute a, ChangeNotes notes)
-      throws OrmException {
-    Collection<Account.Id> reviewers = approvalsUtil.getReviewers(db, notes).all();
+  public void addAllReviewers(ChangeAttribute a, ChangeNotes notes) {
+    Collection<Account.Id> reviewers = approvalsUtil.getReviewers(notes).all();
     if (!reviewers.isEmpty()) {
       a.allReviewers = Lists.newArrayListWithCapacity(reviewers.size());
       for (Account.Id id : reviewers) {
@@ -295,7 +271,7 @@
     try {
       addDependsOn(rw, ca, change, currentPs);
       addNeededBy(rw, ca, change, currentPs);
-    } catch (OrmException | IOException e) {
+    } catch (StorageException | IOException e) {
       // Squash DB exceptions and leave dependency lists partially filled.
     }
     // Remove empty lists so a confusing label won't be displayed in the output.
@@ -308,7 +284,7 @@
   }
 
   private void addDependsOn(RevWalk rw, ChangeAttribute ca, Change change, PatchSet currentPs)
-      throws OrmException, IOException {
+      throws IOException {
     RevCommit commit = rw.parseCommit(ObjectId.fromString(currentPs.getRevision().get()));
     final List<String> parentNames = new ArrayList<>(commit.getParentCount());
     for (RevCommit p : commit.getParents()) {
@@ -341,7 +317,7 @@
   }
 
   private void addNeededBy(RevWalk rw, ChangeAttribute ca, Change change, PatchSet currentPs)
-      throws OrmException, IOException {
+      throws IOException {
     if (currentPs.getGroups().isEmpty()) {
       return;
     }
@@ -403,17 +379,15 @@
   }
 
   public void addPatchSets(
-      ReviewDb db,
       RevWalk revWalk,
       ChangeAttribute ca,
       Collection<PatchSet> ps,
       Map<PatchSet.Id, Collection<PatchSetApproval>> approvals,
       LabelTypes labelTypes) {
-    addPatchSets(db, revWalk, ca, ps, approvals, false, null, labelTypes);
+    addPatchSets(revWalk, ca, ps, approvals, false, null, labelTypes);
   }
 
   public void addPatchSets(
-      ReviewDb db,
       RevWalk revWalk,
       ChangeAttribute ca,
       Collection<PatchSet> ps,
@@ -424,7 +398,7 @@
     if (!ps.isEmpty()) {
       ca.patchSets = new ArrayList<>(ps.size());
       for (PatchSet p : ps) {
-        PatchSetAttribute psa = asPatchSetAttribute(db, revWalk, change, p);
+        PatchSetAttribute psa = asPatchSetAttribute(revWalk, change, p);
         if (approvals != null) {
           addApprovals(psa, p.getId(), approvals, labelTypes);
         }
@@ -484,28 +458,10 @@
   /**
    * Create a PatchSetAttribute for the given patchset suitable for serialization to JSON.
    *
-   * @param revWalk
    * @param patchSet
    * @return object suitable for serialization to JSON
    */
   public PatchSetAttribute asPatchSetAttribute(RevWalk revWalk, Change change, PatchSet patchSet) {
-    try (ReviewDb db = schema.open()) {
-      return asPatchSetAttribute(db, revWalk, change, patchSet);
-    } catch (OrmException e) {
-      logger.atSevere().withCause(e).log("Cannot open database connection");
-      return new PatchSetAttribute();
-    }
-  }
-
-  /**
-   * Create a PatchSetAttribute for the given patchset suitable for serialization to JSON.
-   *
-   * @param db Review database
-   * @param patchSet
-   * @return object suitable for serialization to JSON
-   */
-  public PatchSetAttribute asPatchSetAttribute(
-      ReviewDb db, RevWalk revWalk, Change change, PatchSet patchSet) {
     PatchSetAttribute p = new PatchSetAttribute();
     p.revision = patchSet.getRevision().get();
     p.number = patchSet.getPatchSetId();
@@ -537,8 +493,8 @@
           p.sizeInsertions += pe.getInsertions();
         }
       }
-      p.kind = changeKindCache.getChangeKind(db, change, patchSet);
-    } catch (IOException | OrmException e) {
+      p.kind = changeKindCache.getChangeKind(change, patchSet);
+    } catch (IOException | StorageException e) {
       logger.atSevere().withCause(e).log("Cannot load patch set data for %s", patchSet.getId());
     } catch (PatchListObjectTooLargeException e) {
       logger.atWarning().log("Cannot get size information for %s: %s", pId, e.getMessage());
@@ -550,7 +506,7 @@
 
   // TODO: The same method exists in PatchSetInfoFactory, find a common place
   // for it
-  private UserIdentity toUserIdentity(PersonIdent who) throws IOException, OrmException {
+  private UserIdentity toUserIdentity(PersonIdent who) throws IOException {
     UserIdentity u = new UserIdentity();
     u.setName(who.getName());
     u.setEmail(who.getEmailAddress());
diff --git a/java/com/google/gerrit/server/events/ReviewerAddedEvent.java b/java/com/google/gerrit/server/events/ReviewerAddedEvent.java
index 9644456..ea6bda3 100644
--- a/java/com/google/gerrit/server/events/ReviewerAddedEvent.java
+++ b/java/com/google/gerrit/server/events/ReviewerAddedEvent.java
@@ -21,6 +21,7 @@
 public class ReviewerAddedEvent extends PatchSetEvent {
   static final String TYPE = "reviewer-added";
   public Supplier<AccountAttribute> reviewer;
+  public Supplier<AccountAttribute> adder;
 
   public ReviewerAddedEvent(Change change) {
     super(TYPE, change);
diff --git a/java/com/google/gerrit/server/events/StreamEventsApiListener.java b/java/com/google/gerrit/server/events/StreamEventsApiListener.java
index 972266a..5b2c368 100644
--- a/java/com/google/gerrit/server/events/StreamEventsApiListener.java
+++ b/java/com/google/gerrit/server/events/StreamEventsApiListener.java
@@ -20,6 +20,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -45,28 +46,23 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.data.AccountAttribute;
 import com.google.gerrit.server.data.ApprovalAttribute;
 import com.google.gerrit.server.data.ChangeAttribute;
 import com.google.gerrit.server.data.PatchSetAttribute;
-import com.google.gerrit.server.data.RefUpdateAttribute;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.plugincontext.PluginItemContext;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.Map.Entry;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -117,7 +113,6 @@
   }
 
   private final PluginItemContext<EventDispatcher> dispatcher;
-  private final Provider<ReviewDb> db;
   private final EventFactory eventFactory;
   private final ProjectCache projectCache;
   private final GitRepositoryManager repoManager;
@@ -127,14 +122,12 @@
   @Inject
   StreamEventsApiListener(
       PluginItemContext<EventDispatcher> dispatcher,
-      Provider<ReviewDb> db,
       EventFactory eventFactory,
       ProjectCache projectCache,
       GitRepositoryManager repoManager,
       PatchSetUtil psUtil,
       ChangeNotes.Factory changeNotesFactory) {
     this.dispatcher = dispatcher;
-    this.db = db;
     this.eventFactory = eventFactory;
     this.projectCache = projectCache;
     this.repoManager = repoManager;
@@ -142,59 +135,53 @@
     this.changeNotesFactory = changeNotesFactory;
   }
 
-  private ChangeNotes getNotes(ChangeInfo info) throws OrmException {
+  private ChangeNotes getNotes(ChangeInfo info) {
     try {
       return changeNotesFactory.createChecked(new Change.Id(info._number));
     } catch (NoSuchChangeException e) {
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
   }
 
-  private PatchSet getPatchSet(ChangeNotes notes, RevisionInfo info) throws OrmException {
-    return psUtil.get(db.get(), notes, PatchSet.Id.fromRef(info.ref));
+  private PatchSet getPatchSet(ChangeNotes notes, RevisionInfo info) {
+    return psUtil.get(notes, PatchSet.Id.fromRef(info.ref));
   }
 
   private Supplier<ChangeAttribute> changeAttributeSupplier(Change change, ChangeNotes notes) {
     return Suppliers.memoize(
-        new Supplier<ChangeAttribute>() {
-          @Override
-          public ChangeAttribute get() {
+        () -> {
+          try {
             return eventFactory.asChangeAttribute(change, notes);
+          } catch (StorageException e) {
+            throw new RuntimeException(e);
           }
         });
   }
 
   private Supplier<AccountAttribute> accountAttributeSupplier(AccountInfo account) {
     return Suppliers.memoize(
-        new Supplier<AccountAttribute>() {
-          @Override
-          public AccountAttribute get() {
-            return account != null
+        () ->
+            account != null
                 ? eventFactory.asAccountAttribute(new Account.Id(account._accountId))
-                : null;
-          }
-        });
+                : null);
   }
 
   private Supplier<PatchSetAttribute> patchSetAttributeSupplier(
       final Change change, PatchSet patchSet) {
     return Suppliers.memoize(
-        new Supplier<PatchSetAttribute>() {
-          @Override
-          public PatchSetAttribute get() {
-            try (Repository repo = repoManager.openRepository(change.getProject());
-                RevWalk revWalk = new RevWalk(repo)) {
-              return eventFactory.asPatchSetAttribute(revWalk, change, patchSet);
-            } catch (IOException e) {
-              throw new RuntimeException(e);
-            }
+        () -> {
+          try (Repository repo = repoManager.openRepository(change.getProject());
+              RevWalk revWalk = new RevWalk(repo)) {
+            return eventFactory.asPatchSetAttribute(revWalk, change, patchSet);
+          } catch (IOException e) {
+            throw new RuntimeException(e);
           }
         });
   }
 
   private static Map<String, Short> convertApprovalsMap(Map<String, ApprovalInfo> approvals) {
     Map<String, Short> result = new HashMap<>();
-    for (Entry<String, ApprovalInfo> e : approvals.entrySet()) {
+    for (Map.Entry<String, ApprovalInfo> e : approvals.entrySet()) {
       Short value = e.getValue().value == null ? null : e.getValue().value.shortValue();
       result.put(e.getKey(), value);
     }
@@ -202,7 +189,7 @@
   }
 
   private ApprovalAttribute getApprovalAttribute(
-      LabelTypes labelTypes, Entry<String, Short> approval, Map<String, Short> oldApprovals) {
+      LabelTypes labelTypes, Map.Entry<String, Short> approval, Map<String, Short> oldApprovals) {
     ApprovalAttribute a = new ApprovalAttribute();
     a.type = approval.getKey();
 
@@ -227,21 +214,18 @@
       final Map<String, ApprovalInfo> oldApprovals) {
     final Map<String, Short> approvals = convertApprovalsMap(newApprovals);
     return Suppliers.memoize(
-        new Supplier<ApprovalAttribute[]>() {
-          @Override
-          public ApprovalAttribute[] get() {
-            LabelTypes labelTypes = projectCache.get(change.getProject()).getLabelTypes();
-            if (approvals.size() > 0) {
-              ApprovalAttribute[] r = new ApprovalAttribute[approvals.size()];
-              int i = 0;
-              for (Map.Entry<String, Short> approval : approvals.entrySet()) {
-                r[i++] =
-                    getApprovalAttribute(labelTypes, approval, convertApprovalsMap(oldApprovals));
-              }
-              return r;
+        () -> {
+          LabelTypes labelTypes = projectCache.get(change.getProject()).getLabelTypes();
+          if (approvals.size() > 0) {
+            ApprovalAttribute[] r = new ApprovalAttribute[approvals.size()];
+            int i = 0;
+            for (Map.Entry<String, Short> approval : approvals.entrySet()) {
+              r[i++] =
+                  getApprovalAttribute(labelTypes, approval, convertApprovalsMap(oldApprovals));
             }
-            return null;
+            return r;
           }
+          return null;
         });
   }
 
@@ -264,7 +248,7 @@
       event.oldAssignee = accountAttributeSupplier(ev.getOldAssignee());
 
       dispatcher.run(d -> d.postEvent(change, event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
@@ -281,7 +265,7 @@
       event.oldTopic = ev.getOldTopic();
 
       dispatcher.run(d -> d.postEvent(change, event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
@@ -299,7 +283,7 @@
       event.uploader = accountAttributeSupplier(ev.getWho());
 
       dispatcher.run(d -> d.postEvent(change, event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
@@ -311,7 +295,7 @@
       Change change = notes.getChange();
       ReviewerDeletedEvent event = new ReviewerDeletedEvent(change);
       event.change = changeAttributeSupplier(change, notes);
-      event.patchSet = patchSetAttributeSupplier(change, psUtil.current(db.get(), notes));
+      event.patchSet = patchSetAttributeSupplier(change, psUtil.current(notes));
       event.reviewer = accountAttributeSupplier(ev.getReviewer());
       event.remover = accountAttributeSupplier(ev.getWho());
       event.comment = ev.getComment();
@@ -319,7 +303,7 @@
           approvalsAttributeSupplier(change, ev.getNewApprovals(), ev.getOldApprovals());
 
       dispatcher.run(d -> d.postEvent(change, event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
@@ -332,12 +316,13 @@
       ReviewerAddedEvent event = new ReviewerAddedEvent(change);
 
       event.change = changeAttributeSupplier(change, notes);
-      event.patchSet = patchSetAttributeSupplier(change, psUtil.current(db.get(), notes));
+      event.patchSet = patchSetAttributeSupplier(change, psUtil.current(notes));
+      event.adder = accountAttributeSupplier(ev.getWho());
       for (AccountInfo reviewer : ev.getReviewers()) {
         event.reviewer = accountAttributeSupplier(reviewer);
         dispatcher.run(d -> d.postEvent(event));
       }
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
@@ -365,7 +350,7 @@
       event.removed = hashtagArray(ev.getRemovedHashtags());
 
       dispatcher.run(d -> d.postEvent(change, event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
@@ -379,15 +364,11 @@
     final Branch.NameKey refName = new Branch.NameKey(ev.getProjectName(), ev.getRefName());
     event.refUpdate =
         Suppliers.memoize(
-            new Supplier<RefUpdateAttribute>() {
-              @Override
-              public RefUpdateAttribute get() {
-                return eventFactory.asRefUpdateAttribute(
+            () ->
+                eventFactory.asRefUpdateAttribute(
                     ObjectId.fromString(ev.getOldObjectId()),
                     ObjectId.fromString(ev.getNewObjectId()),
-                    refName);
-              }
-            });
+                    refName));
     dispatcher.run(d -> d.postEvent(refName, event));
   }
 
@@ -406,7 +387,7 @@
       event.approvals = approvalsAttributeSupplier(change, ev.getApprovals(), ev.getOldApprovals());
 
       dispatcher.run(d -> d.postEvent(change, event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
@@ -420,11 +401,11 @@
 
       event.change = changeAttributeSupplier(change, notes);
       event.restorer = accountAttributeSupplier(ev.getWho());
-      event.patchSet = patchSetAttributeSupplier(change, psUtil.current(db.get(), notes));
+      event.patchSet = patchSetAttributeSupplier(change, psUtil.current(notes));
       event.reason = ev.getReason();
 
       dispatcher.run(d -> d.postEvent(change, event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
@@ -438,11 +419,11 @@
 
       event.change = changeAttributeSupplier(change, notes);
       event.submitter = accountAttributeSupplier(ev.getWho());
-      event.patchSet = patchSetAttributeSupplier(change, psUtil.current(db.get(), notes));
+      event.patchSet = patchSetAttributeSupplier(change, psUtil.current(notes));
       event.newRev = ev.getNewRevisionId();
 
       dispatcher.run(d -> d.postEvent(change, event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
@@ -456,11 +437,11 @@
 
       event.change = changeAttributeSupplier(change, notes);
       event.abandoner = accountAttributeSupplier(ev.getWho());
-      event.patchSet = patchSetAttributeSupplier(change, psUtil.current(db.get(), notes));
+      event.patchSet = patchSetAttributeSupplier(change, psUtil.current(notes));
       event.reason = ev.getReason();
 
       dispatcher.run(d -> d.postEvent(change, event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
@@ -478,7 +459,7 @@
       event.patchSet = patchSetAttributeSupplier(change, patchSet);
 
       dispatcher.run(d -> d.postEvent(change, event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
@@ -496,7 +477,7 @@
       event.patchSet = patchSetAttributeSupplier(change, patchSet);
 
       dispatcher.run(d -> d.postEvent(change, event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
@@ -509,14 +490,14 @@
       VoteDeletedEvent event = new VoteDeletedEvent(change);
 
       event.change = changeAttributeSupplier(change, notes);
-      event.patchSet = patchSetAttributeSupplier(change, psUtil.current(db.get(), notes));
+      event.patchSet = patchSetAttributeSupplier(change, psUtil.current(notes));
       event.comment = ev.getMessage();
       event.reviewer = accountAttributeSupplier(ev.getReviewer());
       event.remover = accountAttributeSupplier(ev.getWho());
       event.approvals = approvalsAttributeSupplier(change, ev.getApprovals(), ev.getOldApprovals());
 
       dispatcher.run(d -> d.postEvent(change, event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
@@ -532,7 +513,7 @@
       event.deleter = accountAttributeSupplier(ev.getWho());
 
       dispatcher.run(d -> d.postEvent(change, event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
diff --git a/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java b/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java
index 513a5de..fdce1da 100644
--- a/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java
+++ b/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+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;
@@ -22,7 +23,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.sql.Timestamp;
@@ -53,7 +53,7 @@
               util.accountInfo(oldAssignee),
               when);
       listeners.runEach(l -> l.onAssigneeChanged(event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Couldn't fire event");
     }
   }
diff --git a/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java b/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java
index 3d6700e..a8c08b9 100644
--- a/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java
+++ b/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+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;
@@ -28,7 +29,6 @@
 import com.google.gerrit.server.patch.PatchListObjectTooLargeException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -72,7 +72,7 @@
     } catch (PatchListNotAvailableException
         | GpgException
         | IOException
-        | OrmException
+        | StorageException
         | PermissionBackendException e) {
       logger.atSevere().withCause(e).log("Couldn't fire event");
     }
diff --git a/java/com/google/gerrit/server/extensions/events/ChangeDeleted.java b/java/com/google/gerrit/server/extensions/events/ChangeDeleted.java
index d9eb9f9..9e3e979 100644
--- a/java/com/google/gerrit/server/extensions/events/ChangeDeleted.java
+++ b/java/com/google/gerrit/server/extensions/events/ChangeDeleted.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+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;
@@ -22,7 +23,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.sql.Timestamp;
@@ -47,7 +47,7 @@
     try {
       Event event = new Event(util.changeInfo(change), util.accountInfo(deleter), when);
       listeners.runEach(l -> l.onChangeDeleted(event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Couldn't fire event");
     }
   }
diff --git a/java/com/google/gerrit/server/extensions/events/ChangeMerged.java b/java/com/google/gerrit/server/extensions/events/ChangeMerged.java
index 7b814ae..756f383 100644
--- a/java/com/google/gerrit/server/extensions/events/ChangeMerged.java
+++ b/java/com/google/gerrit/server/extensions/events/ChangeMerged.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+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;
@@ -28,7 +29,6 @@
 import com.google.gerrit.server.patch.PatchListObjectTooLargeException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -66,7 +66,7 @@
     } catch (PatchListNotAvailableException
         | GpgException
         | IOException
-        | OrmException
+        | StorageException
         | PermissionBackendException e) {
       logger.atSevere().withCause(e).log("Couldn't fire event");
     }
diff --git a/java/com/google/gerrit/server/extensions/events/ChangeRestored.java b/java/com/google/gerrit/server/extensions/events/ChangeRestored.java
index 81b04cd..e8bed56 100644
--- a/java/com/google/gerrit/server/extensions/events/ChangeRestored.java
+++ b/java/com/google/gerrit/server/extensions/events/ChangeRestored.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+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;
@@ -28,7 +29,6 @@
 import com.google.gerrit.server.patch.PatchListObjectTooLargeException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -66,7 +66,7 @@
     } catch (PatchListNotAvailableException
         | GpgException
         | IOException
-        | OrmException
+        | StorageException
         | PermissionBackendException e) {
       logger.atSevere().withCause(e).log("Couldn't fire event");
     }
diff --git a/java/com/google/gerrit/server/extensions/events/ChangeReverted.java b/java/com/google/gerrit/server/extensions/events/ChangeReverted.java
index ac7aac0..ccb17d5 100644
--- a/java/com/google/gerrit/server/extensions/events/ChangeReverted.java
+++ b/java/com/google/gerrit/server/extensions/events/ChangeReverted.java
@@ -15,12 +15,12 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.sql.Timestamp;
@@ -45,7 +45,7 @@
     try {
       Event event = new Event(util.changeInfo(change), util.changeInfo(revertChange), when);
       listeners.runEach(l -> l.onChangeReverted(event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Couldn't fire event");
     }
   }
diff --git a/java/com/google/gerrit/server/extensions/events/CommentAdded.java b/java/com/google/gerrit/server/extensions/events/CommentAdded.java
index e224540..ea9ae31 100644
--- a/java/com/google/gerrit/server/extensions/events/CommentAdded.java
+++ b/java/com/google/gerrit/server/extensions/events/CommentAdded.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+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.ApprovalInfo;
@@ -29,7 +30,6 @@
 import com.google.gerrit.server.patch.PatchListObjectTooLargeException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -76,7 +76,7 @@
     } catch (PatchListNotAvailableException
         | GpgException
         | IOException
-        | OrmException
+        | StorageException
         | PermissionBackendException e) {
       logger.atSevere().withCause(e).log("Couldn't fire event");
     }
diff --git a/java/com/google/gerrit/server/extensions/events/EventUtil.java b/java/com/google/gerrit/server/extensions/events/EventUtil.java
index 485ed50..d00eb31 100644
--- a/java/com/google/gerrit/server/extensions/events/EventUtil.java
+++ b/java/com/google/gerrit/server/extensions/events/EventUtil.java
@@ -25,7 +25,6 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.GpgException;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.change.ChangeJson;
@@ -33,9 +32,7 @@
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.sql.Timestamp;
@@ -63,7 +60,6 @@
   }
 
   private final ChangeData.Factory changeDataFactory;
-  private final Provider<ReviewDb> db;
   private final ChangeJson.Factory changeJsonFactory;
   private final RevisionJson.Factory revisionJsonFactory;
 
@@ -71,28 +67,24 @@
   EventUtil(
       ChangeJson.Factory changeJsonFactory,
       RevisionJson.Factory revisionJsonFactory,
-      ChangeData.Factory changeDataFactory,
-      Provider<ReviewDb> db) {
+      ChangeData.Factory changeDataFactory) {
     this.changeDataFactory = changeDataFactory;
-    this.db = db;
     this.changeJsonFactory = changeJsonFactory;
     this.revisionJsonFactory = revisionJsonFactory;
   }
 
-  public ChangeInfo changeInfo(Change change) throws OrmException {
+  public ChangeInfo changeInfo(Change change) {
     return changeJsonFactory.create(CHANGE_OPTIONS).format(change);
   }
 
   public RevisionInfo revisionInfo(Project project, PatchSet ps)
-      throws OrmException, PatchListNotAvailableException, GpgException, IOException,
-          PermissionBackendException {
+      throws PatchListNotAvailableException, GpgException, IOException, PermissionBackendException {
     return revisionInfo(project.getNameKey(), ps);
   }
 
   public RevisionInfo revisionInfo(Project.NameKey project, PatchSet ps)
-      throws OrmException, PatchListNotAvailableException, GpgException, IOException,
-          PermissionBackendException {
-    ChangeData cd = changeDataFactory.create(db.get(), project, ps.getId().getParentKey());
+      throws PatchListNotAvailableException, GpgException, IOException, PermissionBackendException {
+    ChangeData cd = changeDataFactory.create(project, ps.getId().getParentKey());
     return revisionJsonFactory.create(CHANGE_OPTIONS).getRevisionInfo(cd, ps);
   }
 
diff --git a/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java b/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java
index ca0edab..65f5b8b 100644
--- a/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java
+++ b/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.flogger.FluentLogger;
+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;
@@ -23,7 +24,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.sql.Timestamp;
@@ -58,7 +58,7 @@
           new Event(
               util.changeInfo(change), util.accountInfo(editor), hashtags, added, removed, when);
       listeners.runEach(l -> l.onHashtagsEdited(event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Couldn't fire event");
     }
   }
diff --git a/java/com/google/gerrit/server/extensions/events/PluginEvent.java b/java/com/google/gerrit/server/extensions/events/PluginEvent.java
index 8680ab1..60d27c9 100644
--- a/java/com/google/gerrit/server/extensions/events/PluginEvent.java
+++ b/java/com/google/gerrit/server/extensions/events/PluginEvent.java
@@ -15,16 +15,16 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.gerrit.extensions.events.PluginEventListener;
-import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 @Singleton
 public class PluginEvent {
-  private final DynamicSet<PluginEventListener> listeners;
+  private final PluginSetContext<PluginEventListener> listeners;
 
   @Inject
-  PluginEvent(DynamicSet<PluginEventListener> listeners) {
+  PluginEvent(PluginSetContext<PluginEventListener> listeners) {
     this.listeners = listeners;
   }
 
@@ -33,9 +33,7 @@
       return;
     }
     Event e = new Event(pluginName, type, data);
-    for (PluginEventListener l : listeners) {
-      l.onPluginEvent(e);
-    }
+    listeners.runEach(l -> l.onPluginEvent(e));
   }
 
   private static class Event extends AbstractNoNotifyEvent implements PluginEventListener.Event {
diff --git a/java/com/google/gerrit/server/extensions/events/PrivateStateChanged.java b/java/com/google/gerrit/server/extensions/events/PrivateStateChanged.java
index 358667f..49a6091 100644
--- a/java/com/google/gerrit/server/extensions/events/PrivateStateChanged.java
+++ b/java/com/google/gerrit/server/extensions/events/PrivateStateChanged.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+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;
@@ -27,7 +28,6 @@
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -58,7 +58,7 @@
               util.accountInfo(account),
               when);
       listeners.runEach(l -> l.onPrivateStateChanged(event));
-    } catch (OrmException
+    } catch (StorageException
         | PatchListNotAvailableException
         | GpgException
         | IOException
diff --git a/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java b/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java
index 8e5259c..f9f67f6 100644
--- a/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java
+++ b/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.Lists;
 import com.google.common.flogger.FluentLogger;
+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;
@@ -29,7 +30,6 @@
 import com.google.gerrit.server.patch.PatchListObjectTooLargeException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -73,7 +73,7 @@
     } catch (PatchListNotAvailableException
         | GpgException
         | IOException
-        | OrmException
+        | StorageException
         | PermissionBackendException e) {
       logger.atSevere().withCause(e).log("Couldn't fire event");
     }
diff --git a/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java b/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java
index 89c8f18..b92f3e6 100644
--- a/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java
+++ b/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+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.ApprovalInfo;
@@ -29,7 +30,6 @@
 import com.google.gerrit.server.patch.PatchListObjectTooLargeException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -80,7 +80,7 @@
     } catch (PatchListNotAvailableException
         | GpgException
         | IOException
-        | OrmException
+        | StorageException
         | PermissionBackendException e) {
       logger.atSevere().withCause(e).log("Couldn't fire event");
     }
diff --git a/java/com/google/gerrit/server/extensions/events/RevisionCreated.java b/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
index e043e9f..6fddcfe 100644
--- a/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
+++ b/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+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;
@@ -24,11 +25,11 @@
 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.change.NotifyResolver;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.patch.PatchListObjectTooLargeException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -46,7 +47,7 @@
             PatchSet patchSet,
             AccountState uploader,
             Timestamp when,
-            NotifyHandling notify) {}
+            NotifyResolver.Result notify) {}
       };
 
   private final PluginSetContext<RevisionCreatedListener> listeners;
@@ -68,7 +69,7 @@
       PatchSet patchSet,
       AccountState uploader,
       Timestamp when,
-      NotifyHandling notify) {
+      NotifyResolver.Result notify) {
     if (listeners.isEmpty()) {
       return;
     }
@@ -79,14 +80,14 @@
               util.revisionInfo(change.getProject(), patchSet),
               util.accountInfo(uploader),
               when,
-              notify);
+              notify.handling());
       listeners.runEach(l -> l.onRevisionCreated(event));
     } catch (PatchListObjectTooLargeException e) {
       logger.atWarning().log("Couldn't fire event: %s", e.getMessage());
     } catch (PatchListNotAvailableException
         | GpgException
         | IOException
-        | OrmException
+        | StorageException
         | PermissionBackendException e) {
       logger.atSevere().withCause(e).log("Couldn't fire event");
     }
diff --git a/java/com/google/gerrit/server/extensions/events/TopicEdited.java b/java/com/google/gerrit/server/extensions/events/TopicEdited.java
index 8568c0f..9e1ae44 100644
--- a/java/com/google/gerrit/server/extensions/events/TopicEdited.java
+++ b/java/com/google/gerrit/server/extensions/events/TopicEdited.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+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;
@@ -22,7 +23,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.sql.Timestamp;
@@ -48,7 +48,7 @@
       Event event =
           new Event(util.changeInfo(change), util.accountInfo(account), oldTopicName, when);
       listeners.runEach(l -> l.onTopicEdited(event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Couldn't fire event");
     }
   }
diff --git a/java/com/google/gerrit/server/extensions/events/VoteDeleted.java b/java/com/google/gerrit/server/extensions/events/VoteDeleted.java
index b750851..bd6873a 100644
--- a/java/com/google/gerrit/server/extensions/events/VoteDeleted.java
+++ b/java/com/google/gerrit/server/extensions/events/VoteDeleted.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+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.ApprovalInfo;
@@ -29,7 +30,6 @@
 import com.google.gerrit.server.patch.PatchListObjectTooLargeException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -80,7 +80,7 @@
     } catch (PatchListNotAvailableException
         | GpgException
         | IOException
-        | OrmException
+        | StorageException
         | PermissionBackendException e) {
       logger.atSevere().withCause(e).log("Couldn't fire event");
     }
diff --git a/java/com/google/gerrit/server/extensions/events/WorkInProgressStateChanged.java b/java/com/google/gerrit/server/extensions/events/WorkInProgressStateChanged.java
index 6273ad6..785d6fe 100644
--- a/java/com/google/gerrit/server/extensions/events/WorkInProgressStateChanged.java
+++ b/java/com/google/gerrit/server/extensions/events/WorkInProgressStateChanged.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+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;
@@ -27,7 +28,6 @@
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -59,7 +59,7 @@
               util.accountInfo(account),
               when);
       listeners.runEach(l -> l.onWorkInProgressStateChanged(event));
-    } catch (OrmException
+    } catch (StorageException
         | PatchListNotAvailableException
         | GpgException
         | IOException
diff --git a/java/com/google/gerrit/server/git/BanCommit.java b/java/com/google/gerrit/server/git/BanCommit.java
index aba4c53..d8aeece 100644
--- a/java/com/google/gerrit/server/git/BanCommit.java
+++ b/java/com/google/gerrit/server/git/BanCommit.java
@@ -18,6 +18,7 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 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;
diff --git a/java/com/google/gerrit/server/git/DefaultAdvertiseRefsHook.java b/java/com/google/gerrit/server/git/DefaultAdvertiseRefsHook.java
index ef5e65b..a8594e7 100644
--- a/java/com/google/gerrit/server/git/DefaultAdvertiseRefsHook.java
+++ b/java/com/google/gerrit/server/git/DefaultAdvertiseRefsHook.java
@@ -14,10 +14,14 @@
 
 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;
@@ -28,7 +32,6 @@
  * implements {@link org.eclipse.jgit.transport.AdvertiseRefsHook}.
  */
 public class DefaultAdvertiseRefsHook extends AbstractAdvertiseRefsHook {
-
   private final PermissionBackend.ForProject perm;
   private final PermissionBackend.RefFilterOptions opts;
 
@@ -42,9 +45,14 @@
   protected Map<String, Ref> getAdvertisedRefs(Repository repo, RevWalk revWalk)
       throws ServiceMayNotContinueException {
     try {
-      return perm.filter(repo.getAllRefs(), repo, opts);
-    } catch (PermissionBackendException e) {
-      throw new ServiceMayNotContinueException(e);
+      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 44b4921..5b9dffc 100644
--- a/java/com/google/gerrit/server/git/DefaultChangeReportFormatter.java
+++ b/java/com/google/gerrit/server/git/DefaultChangeReportFormatter.java
@@ -27,6 +27,7 @@
   private static final int SUBJECT_MAX_LENGTH = 80;
   private static final String SUBJECT_CROP_APPENDIX = "...";
   private static final int SUBJECT_CROP_RANGE = 10;
+  private static final String NEW_CHANGE_INDICATOR = " [NEW]";
 
   protected final DynamicItem<UrlFormatter> urlFormatter;
 
@@ -37,7 +38,7 @@
 
   @Override
   public String newChange(ChangeReportFormatter.Input input) {
-    return formatChangeUrl(input);
+    return formatChangeUrl(input) + NEW_CHANGE_INDICATOR;
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/git/GarbageCollection.java b/java/com/google/gerrit/server/git/GarbageCollection.java
index 3624695..75c9012 100644
--- a/java/com/google/gerrit/server/git/GarbageCollection.java
+++ b/java/com/google/gerrit/server/git/GarbageCollection.java
@@ -18,10 +18,10 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.GarbageCollectionResult;
 import com.google.gerrit.extensions.events.GarbageCollectorListener;
-import com.google.gerrit.extensions.registration.DynamicSet;
 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;
 import com.google.inject.Inject;
 import java.io.PrintWriter;
 import java.util.List;
@@ -43,7 +43,7 @@
   private final GitRepositoryManager repoManager;
   private final GarbageCollectionQueue gcQueue;
   private final GcConfig gcConfig;
-  private final DynamicSet<GarbageCollectorListener> listeners;
+  private final PluginSetContext<GarbageCollectorListener> listeners;
 
   public interface Factory {
     GarbageCollection create();
@@ -54,7 +54,7 @@
       GitRepositoryManager repoManager,
       GarbageCollectionQueue gcQueue,
       GcConfig config,
-      DynamicSet<GarbageCollectorListener> listeners) {
+      PluginSetContext<GarbageCollectorListener> listeners) {
     this.repoManager = repoManager;
     this.gcQueue = gcQueue;
     this.gcConfig = config;
@@ -113,13 +113,7 @@
       return;
     }
     Event event = new Event(p, statistics);
-    for (GarbageCollectorListener l : listeners) {
-      try {
-        l.onGarbageCollected(event);
-      } catch (RuntimeException e) {
-        logger.atWarning().withCause(e).log("Failure in GarbageCollectorListener");
-      }
-    }
+    listeners.runEach(l -> l.onGarbageCollected(event));
   }
 
   private static void logGcInfo(Project.NameKey projectName, String msg) {
diff --git a/java/com/google/gerrit/server/git/GroupCollector.java b/java/com/google/gerrit/server/git/GroupCollector.java
index bb65fa8..07c995f 100644
--- a/java/com/google/gerrit/server/git/GroupCollector.java
+++ b/java/com/google/gerrit/server/git/GroupCollector.java
@@ -30,11 +30,9 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gwtorm.server.OrmException;
 import java.util.ArrayDeque;
 import java.util.Collection;
 import java.util.Deque;
@@ -76,10 +74,6 @@
 public class GroupCollector {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  public static List<String> getDefaultGroups(PatchSet ps) {
-    return ImmutableList.of(ps.getRevision().get());
-  }
-
   public static List<String> getDefaultGroups(ObjectId commit) {
     return ImmutableList.of(commit.name());
   }
@@ -94,7 +88,7 @@
   }
 
   private interface Lookup {
-    List<String> lookup(PatchSet.Id psId) throws OrmException;
+    List<String> lookup(PatchSet.Id psId);
   }
 
   private final ListMultimap<ObjectId, PatchSet.Id> patchSetsBySha;
@@ -106,33 +100,16 @@
 
   public static GroupCollector create(
       ListMultimap<ObjectId, Ref> changeRefsById,
-      ReviewDb db,
       PatchSetUtil psUtil,
       ChangeNotes.Factory notesFactory,
       Project.NameKey project) {
     return new GroupCollector(
         transformRefs(changeRefsById),
-        new Lookup() {
-          @Override
-          public List<String> lookup(PatchSet.Id psId) throws OrmException {
-            // TODO(dborowitz): Reuse open repository from caller.
-            ChangeNotes notes = notesFactory.createChecked(db, project, psId.getParentKey());
-            PatchSet ps = psUtil.get(db, notes, psId);
-            return ps != null ? ps.getGroups() : null;
-          }
-        });
-  }
-
-  public static GroupCollector createForSchemaUpgradeOnly(
-      ListMultimap<ObjectId, Ref> changeRefsById, ReviewDb db) {
-    return new GroupCollector(
-        transformRefs(changeRefsById),
-        new Lookup() {
-          @Override
-          public List<String> lookup(PatchSet.Id psId) throws OrmException {
-            PatchSet ps = db.patchSets().get(psId);
-            return ps != null ? ps.getGroups() : null;
-          }
+        psId -> {
+          // TODO(dborowitz): Reuse open repository from caller.
+          ChangeNotes notes = notesFactory.createChecked(project, psId.getParentKey());
+          PatchSet ps = psUtil.get(notes, psId);
+          return ps != null ? ps.getGroups() : null;
         });
   }
 
@@ -154,12 +131,9 @@
       ListMultimap<PatchSet.Id, String> groupLookup) {
     this(
         patchSetsBySha,
-        new Lookup() {
-          @Override
-          public List<String> lookup(PatchSet.Id psId) {
-            List<String> groups = groupLookup.get(psId);
-            return !groups.isEmpty() ? groups : null;
-          }
+        psId -> {
+          List<String> groups = groupLookup.get(psId);
+          return !groups.isEmpty() ? groups : null;
         });
   }
 
@@ -223,7 +197,7 @@
     }
   }
 
-  public SortedSetMultimap<ObjectId, String> getGroups() throws OrmException {
+  public SortedSetMultimap<ObjectId, String> getGroups() {
     done = true;
     SortedSetMultimap<ObjectId, String> result =
         MultimapBuilder.hashKeys(groups.keySet().size()).treeSetValues().build();
@@ -249,8 +223,7 @@
     return id != null && patchSetsBySha.containsKey(id);
   }
 
-  private Set<String> resolveGroups(ObjectId forCommit, Collection<String> candidates)
-      throws OrmException {
+  private Set<String> resolveGroups(ObjectId forCommit, Collection<String> candidates) {
     Set<String> actual = Sets.newTreeSet();
     Set<String> done = Sets.newHashSetWithExpectedSize(candidates.size());
     Set<String> seen = Sets.newHashSetWithExpectedSize(candidates.size());
@@ -285,7 +258,7 @@
     }
   }
 
-  private Iterable<String> resolveGroup(ObjectId forCommit, String group) throws OrmException {
+  private Iterable<String> resolveGroup(ObjectId forCommit, String group) {
     ObjectId id = parseGroup(forCommit, group);
     if (id != null) {
       PatchSet.Id psId = Iterables.getFirst(patchSetsBySha.get(id), null);
diff --git a/java/com/google/gerrit/server/git/HookUtil.java b/java/com/google/gerrit/server/git/HookUtil.java
index 42d3f69..3bef7cc 100644
--- a/java/com/google/gerrit/server/git/HookUtil.java
+++ b/java/com/google/gerrit/server/git/HookUtil.java
@@ -14,10 +14,11 @@
 
 package com.google.gerrit.server.git;
 
+import static java.util.stream.Collectors.toMap;
+
 import java.io.IOException;
 import java.util.Map;
 import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.transport.BaseReceivePack;
 import org.eclipse.jgit.transport.ServiceMayNotContinueException;
 
@@ -31,6 +32,7 @@
    * @return map of refs that were advertised.
    * @throws ServiceMayNotContinueException if a problem occurred.
    */
+  @SuppressWarnings("deprecation")
   public static Map<String, Ref> ensureAllRefsAdvertised(BaseReceivePack rp)
       throws ServiceMayNotContinueException {
     Map<String, Ref> refs = rp.getAdvertisedRefs();
@@ -38,7 +40,9 @@
       return refs;
     }
     try {
-      refs = rp.getRepository().getRefDatabase().getRefs(RefDatabase.ALL);
+      refs =
+          rp.getRepository().getRefDatabase().getRefs().stream()
+              .collect(toMap(Ref::getName, r -> r));
     } catch (ServiceMayNotContinueException e) {
       throw e;
     } catch (IOException e) {
diff --git a/java/com/google/gerrit/server/git/LockFailureException.java b/java/com/google/gerrit/server/git/LockFailureException.java
deleted file mode 100644
index 503c0d9..0000000
--- a/java/com/google/gerrit/server/git/LockFailureException.java
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.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 {
-  private static final long serialVersionUID = 1L;
-
-  private final ImmutableList<String> refs;
-
-  public LockFailureException(String message, RefUpdate refUpdate) {
-    super(message);
-    refs = ImmutableList.of(refUpdate.getName());
-  }
-
-  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;
-  }
-}
diff --git a/java/com/google/gerrit/server/git/MergeUtil.java b/java/com/google/gerrit/server/git/MergeUtil.java
index 8922587..c1d8ddd 100644
--- a/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/java/com/google/gerrit/server/git/MergeUtil.java
@@ -30,8 +30,10 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.registration.Extension;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MergeConflictException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
@@ -42,9 +44,7 @@
 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.PatchSet.Id;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -57,9 +57,7 @@
 import com.google.gerrit.server.submit.IntegrationException;
 import com.google.gerrit.server.submit.MergeIdenticalTreeException;
 import com.google.gerrit.server.submit.MergeSorter;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 import java.io.IOException;
@@ -86,7 +84,6 @@
 import org.eclipse.jgit.errors.NoMergeBaseException;
 import org.eclipse.jgit.errors.NoMergeBaseException.MergeBaseFailureReason;
 import org.eclipse.jgit.errors.RevisionSyntaxException;
-import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
@@ -129,20 +126,31 @@
     }
 
     public String generate(
-        RevCommit original, RevCommit mergeTip, Branch.NameKey dest, String current) {
+        RevCommit original, RevCommit mergeTip, Branch.NameKey dest, String originalMessage) {
       requireNonNull(original.getRawBuffer());
       if (mergeTip != null) {
         requireNonNull(mergeTip.getRawBuffer());
       }
-      for (ChangeMessageModifier changeMessageModifier : changeMessageModifiers) {
+
+      int count = 0;
+      String current = originalMessage;
+      for (Extension<ChangeMessageModifier> ext : changeMessageModifiers.entries()) {
+        ChangeMessageModifier changeMessageModifier = ext.get();
+        String className = changeMessageModifier.getClass().getName();
         current = changeMessageModifier.onSubmit(current, original, mergeTip, dest);
-        requireNonNull(
-            current,
-            () ->
-                String.format(
-                    "%s.OnSubmit returned null instead of new commit message",
-                    changeMessageModifier.getClass().getName()));
+        checkState(
+            current != null,
+            "%s.onSubmit from plugin %s returned null instead of new commit message",
+            className,
+            ext.getPluginName());
+        count++;
+        logger.atFine().log(
+            "Invoked %s from plugin %s, message length now %d",
+            className, ext.getPluginName(), current.length());
       }
+      logger.atFine().log(
+          "Invoked %d ChangeMessageModifiers on message with original length %d",
+          count, originalMessage.length());
       return current;
     }
   }
@@ -163,7 +171,6 @@
     MergeUtil create(ProjectState project, boolean useContentMerge);
   }
 
-  private final Provider<ReviewDb> db;
   private final IdentifiedUser.GenericFactory identifiedUserFactory;
   private final DynamicItem<UrlFormatter> urlFormatter;
   private final ApprovalsUtil approvalsUtil;
@@ -175,7 +182,6 @@
   @AssistedInject
   MergeUtil(
       @GerritServerConfig Config serverConfig,
-      Provider<ReviewDb> db,
       IdentifiedUser.GenericFactory identifiedUserFactory,
       DynamicItem<UrlFormatter> urlFormatter,
       ApprovalsUtil approvalsUtil,
@@ -183,7 +189,6 @@
       @Assisted ProjectState project) {
     this(
         serverConfig,
-        db,
         identifiedUserFactory,
         urlFormatter,
         approvalsUtil,
@@ -195,14 +200,12 @@
   @AssistedInject
   MergeUtil(
       @GerritServerConfig Config serverConfig,
-      Provider<ReviewDb> db,
       IdentifiedUser.GenericFactory identifiedUserFactory,
       DynamicItem<UrlFormatter> urlFormatter,
       ApprovalsUtil approvalsUtil,
       @Assisted ProjectState project,
       PluggableCommitMessageGenerator commitMessageGenerator,
       @Assisted boolean useContentMerge) {
-    this.db = db;
     this.identifiedUserFactory = identifiedUserFactory;
     this.urlFormatter = urlFormatter;
     this.approvalsUtil = approvalsUtil;
@@ -234,7 +237,7 @@
     List<CodeReviewCommit> result = new ArrayList<>();
     try {
       result.addAll(mergeSorter.sort(toSort));
-    } catch (IOException | OrmException e) {
+    } catch (IOException | StorageException e) {
       throw new IntegrationException("Branch head sorting failed", e);
     }
     result.sort(CodeReviewCommit.ORDER);
@@ -351,8 +354,8 @@
       try {
         // TODO(dborowitz): Respect inCoreLimit here.
         buf = new TemporaryBuffer.LocalFile(null, 10 * 1024 * 1024);
-        fmt.formatMerge(buf, p, "BASE", oursNameFormatted, theirsNameFormatted, UTF_8.name());
-        buf.close();
+        fmt.formatMerge(buf, p, "BASE", oursNameFormatted, theirsNameFormatted, UTF_8);
+        buf.close(); // Flush file and close for writes, but leave available for reading.
 
         try (InputStream in = buf.openInputStream()) {
           resolved.put(entry.getKey(), ins.insert(Constants.OBJ_BLOB, buf.length(), in));
@@ -590,7 +593,7 @@
    * @return new message
    */
   public String createCommitMessageOnSubmit(
-      RevCommit n, RevCommit mergeTip, ChangeNotes notes, Id id) {
+      RevCommit n, RevCommit mergeTip, ChangeNotes notes, PatchSet.Id id) {
     return commitMessageGenerator.generate(
         n, mergeTip, notes.getChange().getDest(), createDetailedCommitMessage(n, notes, id));
   }
@@ -605,8 +608,8 @@
 
   private Iterable<PatchSetApproval> safeGetApprovals(ChangeNotes notes, PatchSet.Id psId) {
     try {
-      return approvalsUtil.byPatchSet(db.get(), notes, psId, null, null);
-    } catch (OrmException e) {
+      return approvalsUtil.byPatchSet(notes, psId, null, null);
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Can't read approval records for %s", psId);
       return Collections.emptyList();
     }
@@ -638,7 +641,7 @@
     }
 
     try (ObjectInserter ins = new InMemoryInserter(repo)) {
-      return newThreeWayMerger(ins, repo.getConfig()).merge(new AnyObjectId[] {mergeTip, toMerge});
+      return newThreeWayMerger(ins, repo.getConfig()).merge(mergeTip, toMerge);
     } catch (LargeObjectException e) {
       logger.atWarning().log("Cannot merge due to LargeObjectException: %s", toMerge.name());
       return false;
@@ -719,7 +722,7 @@
       throws IntegrationException {
     try {
       return !mergeSorter.sort(Collections.singleton(toMerge)).contains(toMerge);
-    } catch (IOException | OrmException e) {
+    } catch (IOException | StorageException e) {
       throw new IntegrationException("Branch head sorting failed", e);
     }
   }
@@ -736,7 +739,7 @@
       throws IntegrationException {
     ThreeWayMerger m = newThreeWayMerger(inserter, repoConfig);
     try {
-      if (m.merge(new AnyObjectId[] {mergeTip, n})) {
+      if (m.merge(mergeTip, n)) {
         return writeMergeCommit(
             author, committer, rw, inserter, destBranch, mergeTip, m.getResultTreeId(), n);
       }
diff --git a/java/com/google/gerrit/server/git/MergedByPushOp.java b/java/com/google/gerrit/server/git/MergedByPushOp.java
index 7f18e1b..3d64b82 100644
--- a/java/com/google/gerrit/server/git/MergedByPushOp.java
+++ b/java/com/google/gerrit/server/git/MergedByPushOp.java
@@ -35,12 +35,10 @@
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
 import com.google.gerrit.server.util.RequestScopePropagator;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
-import java.util.Collections;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
 import org.eclipse.jgit.lib.Constants;
@@ -112,7 +110,7 @@
   }
 
   @Override
-  public boolean updateChange(ChangeContext ctx) throws OrmException, IOException {
+  public boolean updateChange(ChangeContext ctx) throws IOException {
     change = ctx.getChange();
     correctBranch = refName.equals(change.getDest().get());
     if (!correctBranch) {
@@ -126,14 +124,13 @@
     } else {
       patchSet =
           requireNonNull(
-              psUtil.get(ctx.getDb(), ctx.getNotes(), psId),
+              psUtil.get(ctx.getNotes(), psId),
               () -> String.format("patch set %s not found", psId));
     }
     info = getPatchSetInfo(ctx);
 
     ChangeUpdate update = ctx.getUpdate(psId);
-    Change.Status status = change.getStatus();
-    if (status == Change.Status.MERGED) {
+    if (change.isMerged()) {
       return true;
     }
     change.setCurrentPatchSet(info);
@@ -161,13 +158,12 @@
     ChangeMessage msg =
         ChangeMessagesUtil.newMessage(
             psId, ctx.getUser(), ctx.getWhen(), msgBuf.toString(), ChangeMessagesUtil.TAG_MERGED);
-    cmUtil.addChangeMessage(ctx.getDb(), update, msg);
+    cmUtil.addChangeMessage(update, msg);
 
     PatchSetApproval submitter =
         ApprovalsUtil.newApproval(
             change.currentPatchSetId(), ctx.getUser(), LabelId.legacySubmit(), 1, ctx.getWhen());
     update.putApproval(submitter.getLabel(), submitter.getValue());
-    ctx.getDb().patchSetApprovals().upsert(Collections.singleton(submitter));
 
     return true;
   }
@@ -205,7 +201,7 @@
     changeMerged.fire(change, patchSet, ctx.getAccount(), mergeResultRevId, ctx.getWhen());
   }
 
-  private PatchSetInfo getPatchSetInfo(ChangeContext ctx) throws IOException, OrmException {
+  private PatchSetInfo getPatchSetInfo(ChangeContext ctx) throws IOException {
     RevWalk rw = ctx.getRevWalk();
     RevCommit commit =
         rw.parseCommit(ObjectId.fromString(requireNonNull(patchSet).getRevision().get()));
diff --git a/java/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManager.java b/java/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManager.java
index 6fafe4e..01e85cf 100644
--- a/java/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManager.java
+++ b/java/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManager.java
@@ -17,7 +17,7 @@
 import static com.google.common.base.Preconditions.checkState;
 
 import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.reviewdb.client.Project.NameKey;
+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;
@@ -54,7 +54,7 @@
   }
 
   @Override
-  public Path getBasePath(NameKey name) {
+  public Path getBasePath(Project.NameKey name) {
     Path alternateBasePath = config.getBasePath(name);
     return alternateBasePath != null ? alternateBasePath : super.getBasePath(name);
   }
diff --git a/java/com/google/gerrit/server/git/NotesBranchUtil.java b/java/com/google/gerrit/server/git/NotesBranchUtil.java
index 24b3727..1636b85 100644
--- a/java/com/google/gerrit/server/git/NotesBranchUtil.java
+++ b/java/com/google/gerrit/server/git/NotesBranchUtil.java
@@ -16,10 +16,11 @@
 
 import static com.google.common.base.MoreObjects.firstNonNull;
 
+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.gerrit.server.update.RefUpdateUtil;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
diff --git a/java/com/google/gerrit/server/git/PerThreadRequestScope.java b/java/com/google/gerrit/server/git/PerThreadRequestScope.java
index a4719a9..b7db542 100644
--- a/java/com/google/gerrit/server/git/PerThreadRequestScope.java
+++ b/java/com/google/gerrit/server/git/PerThreadRequestScope.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.gerrit.server.config.RequestScopedReviewDbProvider;
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestScopePropagator;
@@ -52,10 +51,8 @@
 
   public static class Propagator extends ThreadLocalRequestScopePropagator<Context> {
     @Inject
-    Propagator(
-        ThreadLocalRequestContext local,
-        Provider<RequestScopedReviewDbProvider> dbProviderProvider) {
-      super(REQUEST, current, local, dbProviderProvider);
+    Propagator(ThreadLocalRequestContext local) {
+      super(REQUEST, current, local);
     }
 
     @Override
diff --git a/java/com/google/gerrit/server/git/PureRevertCache.java b/java/com/google/gerrit/server/git/PureRevertCache.java
new file mode 100644
index 0000000..16ac87f
--- /dev/null
+++ b/java/com/google/gerrit/server/git/PureRevertCache.java
@@ -0,0 +1,200 @@
+// 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.annotations.VisibleForTesting;
+import com.google.common.base.Throwables;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+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.TraceContext;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+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;
+import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.errors.InvalidObjectIdException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.merge.ThreeWayMerger;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+/** Computes and caches if a change is a pure revert of another change. */
+@Singleton
+public class PureRevertCache {
+  private static final String ID_CACHE = "pure_revert";
+
+  public static Module module() {
+    return new CacheModule() {
+      @Override
+      protected void configure() {
+        persist(ID_CACHE, Cache.PureRevertKeyProto.class, Boolean.class)
+            .maximumWeight(100)
+            .loader(Loader.class)
+            .version(1)
+            .keySerializer(new ProtobufSerializer<>(Cache.PureRevertKeyProto.parser()))
+            .valueSerializer(BooleanCacheSerializer.INSTANCE);
+      }
+    };
+  }
+
+  private final LoadingCache<PureRevertKeyProto, Boolean> cache;
+  private final ChangeNotes.Factory notesFactory;
+
+  @Inject
+  PureRevertCache(
+      @Named(ID_CACHE) LoadingCache<PureRevertKeyProto, Boolean> cache,
+      ChangeNotes.Factory notesFactory) {
+    this.cache = cache;
+    this.notesFactory = notesFactory;
+  }
+
+  /**
+   * Returns {@code true} if {@code claimedRevert} is a pure (clean) revert of the change that is
+   * referenced in {@link Change#getRevertOf()}.
+   *
+   * @return {@code true} if {@code claimedRevert} is a pure (clean) revert.
+   * @throws IOException if there was a problem with the storage layer
+   * @throws BadRequestException if there is a problem with the provided {@link ChangeNotes}
+   */
+  public boolean isPureRevert(ChangeNotes claimedRevert) throws IOException, BadRequestException {
+    if (claimedRevert.getChange().getRevertOf() == null) {
+      throw new BadRequestException("revertOf not set");
+    }
+    ChangeNotes claimedOriginal =
+        notesFactory.createChecked(
+            claimedRevert.getProjectName(), claimedRevert.getChange().getRevertOf());
+    return isPureRevert(
+        claimedRevert.getProjectName(),
+        ObjectId.fromString(claimedRevert.getCurrentPatchSet().getRevision().get()),
+        ObjectId.fromString(claimedOriginal.getCurrentPatchSet().getRevision().get()));
+  }
+
+  /**
+   * Returns {@code true} if {@code claimedRevert} is a pure (clean) revert of {@code
+   * claimedOriginal}.
+   *
+   * @return {@code true} if {@code claimedRevert} is a pure (clean) revert of {@code
+   *     claimedOriginal}.
+   * @throws IOException if there was a problem with the storage layer
+   * @throws BadRequestException if there is a problem with the provided {@link ObjectId}s
+   */
+  public boolean isPureRevert(
+      Project.NameKey project, ObjectId claimedRevert, ObjectId claimedOriginal)
+      throws IOException, BadRequestException {
+    try {
+      return cache.get(key(project, claimedRevert, claimedOriginal));
+    } catch (ExecutionException e) {
+      Throwables.throwIfInstanceOf(e.getCause(), BadRequestException.class);
+      throw new IOException(e);
+    }
+  }
+
+  @VisibleForTesting
+  static PureRevertKeyProto key(
+      Project.NameKey project, ObjectId claimedRevert, ObjectId claimedOriginal) {
+    ByteString original = ObjectIdConverter.create().toByteString(claimedOriginal);
+    ByteString revert = ObjectIdConverter.create().toByteString(claimedRevert);
+    return PureRevertKeyProto.newBuilder()
+        .setProject(project.get())
+        .setClaimedOriginal(original)
+        .setClaimedRevert(revert)
+        .build();
+  }
+
+  static class Loader extends CacheLoader<PureRevertKeyProto, Boolean> {
+    private final GitRepositoryManager repoManager;
+    private final MergeUtil.Factory mergeUtilFactory;
+    private final ProjectCache projectCache;
+
+    @Inject
+    Loader(
+        GitRepositoryManager repoManager,
+        MergeUtil.Factory mergeUtilFactory,
+        ProjectCache projectCache) {
+      this.repoManager = repoManager;
+      this.mergeUtilFactory = mergeUtilFactory;
+      this.projectCache = projectCache;
+    }
+
+    @Override
+    public Boolean load(PureRevertKeyProto key) throws BadRequestException, IOException {
+      try (TraceContext.TraceTimer ignored =
+          TraceContext.newTimer("Loading pure revert for %s", key)) {
+        ObjectId original = ObjectIdConverter.create().fromByteString(key.getClaimedOriginal());
+        ObjectId revert = ObjectIdConverter.create().fromByteString(key.getClaimedRevert());
+        Project.NameKey project = new Project.NameKey(key.getProject());
+
+        try (Repository repo = repoManager.openRepository(project);
+            ObjectInserter oi = repo.newObjectInserter();
+            RevWalk rw = new RevWalk(repo)) {
+          RevCommit claimedOriginalCommit;
+          try {
+            claimedOriginalCommit = rw.parseCommit(original);
+          } catch (InvalidObjectIdException | MissingObjectException e) {
+            throw new BadRequestException("invalid object ID", e);
+          }
+          if (claimedOriginalCommit.getParentCount() == 0) {
+            throw new BadRequestException("can't check against initial commit");
+          }
+          RevCommit claimedRevertCommit = rw.parseCommit(revert);
+          if (claimedRevertCommit.getParentCount() == 0) {
+            return false;
+          }
+          // Rebase claimed revert onto claimed original
+          ThreeWayMerger merger =
+              mergeUtilFactory
+                  .create(projectCache.checkedGet(project))
+                  .newThreeWayMerger(oi, repo.getConfig());
+          merger.setBase(claimedRevertCommit.getParent(0));
+          boolean success = merger.merge(claimedRevertCommit, claimedOriginalCommit);
+          if (!success || merger.getResultTreeId() == null) {
+            // Merge conflict during rebase
+            return false;
+          }
+
+          // 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())) {
+            df.setReader(oi.newReader(), repo.getConfig());
+            List<DiffEntry> entries =
+                df.scan(claimedOriginalCommit.getParent(0), merger.getResultTreeId());
+            return entries.isEmpty();
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java b/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
index d5e19cc..d7f8982 100644
--- a/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
+++ b/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
@@ -19,12 +19,12 @@
 import com.google.common.cache.LoadingCache;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.UsedAt;
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.index.change.ChangeField;
@@ -45,6 +45,13 @@
 import java.util.List;
 import java.util.concurrent.ExecutionException;
 
+/**
+ * Cache based on an index query of the most recent changes. The number of cached items depends on
+ * the index implementation and configuration.
+ *
+ * <p>This cache is intended to be used when filtering references. By design it returns only a
+ * fraction of all changes. These are the changes that were modified last.
+ */
 @Singleton
 public class SearchingChangeCacheImpl implements GitReferenceUpdatedListener {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -65,8 +72,7 @@
     @Override
     protected void configure() {
       if (slave) {
-        bind(SearchingChangeCacheImpl.class)
-            .toProvider(Providers.<SearchingChangeCacheImpl>of(null));
+        bind(SearchingChangeCacheImpl.class).toProvider(Providers.of(null));
       } else {
         cache(ID_CACHE, Project.NameKey.class, new TypeLiteral<List<CachedChange>>() {})
             .maximumWeight(0)
@@ -80,7 +86,8 @@
   }
 
   @AutoValue
-  abstract static class CachedChange {
+  @UsedAt(UsedAt.Project.GOOGLE)
+  public abstract static class CachedChange {
     // Subset of fields in ChangeData, specifically fields needed to serve
     // VisibleRefFilter without touching the database. More can be added as
     // necessary.
@@ -107,16 +114,15 @@
    * <p>Returned changes only include the {@code Change} object (with id, branch) and the reviewers.
    * Additional stored fields are not loaded from the index.
    *
-   * @param db database handle to populate missing change data (probably unused).
    * @param project project to read.
    * @return list of known changes; empty if no changes.
    */
-  public List<ChangeData> getChangeData(ReviewDb db, Project.NameKey project) {
+  public List<ChangeData> getChangeData(Project.NameKey project) {
     try {
       List<CachedChange> cached = cache.get(project);
       List<ChangeData> cds = new ArrayList<>(cached.size());
       for (CachedChange cc : cached) {
-        ChangeData cd = changeDataFactory.create(db, cc.change());
+        ChangeData cd = changeDataFactory.create(cc.change());
         cd.setReviewers(cc.reviewers());
         cds.add(cd);
       }
diff --git a/java/com/google/gerrit/server/git/TagMatcher.java b/java/com/google/gerrit/server/git/TagMatcher.java
index 58b4b8b..f003b6f 100644
--- a/java/com/google/gerrit/server/git/TagMatcher.java
+++ b/java/com/google/gerrit/server/git/TagMatcher.java
@@ -51,13 +51,8 @@
     this.updated = updated;
   }
 
-  public boolean isReachable(Ref tagRef) {
-    try {
-      tagRef = db.getRefDatabase().peel(tagRef);
-    } catch (IOException e) {
-      // Ignore
-    }
-
+  public boolean isReachable(Ref tagRef) throws IOException {
+    tagRef = db.getRefDatabase().peel(tagRef);
     ObjectId tagObj = tagRef.getPeeledObjectId();
     if (tagObj == null) {
       tagObj = tagRef.getObjectId();
diff --git a/java/com/google/gerrit/server/git/TagSet.java b/java/com/google/gerrit/server/git/TagSet.java
index ce8814f..57637c89 100644
--- a/java/com/google/gerrit/server/git/TagSet.java
+++ b/java/com/google/gerrit/server/git/TagSet.java
@@ -24,7 +24,7 @@
 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.cache.serialize.ProtoCacheSerializers.ObjectIdConverter;
+import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
 import com.google.protobuf.ByteString;
 import java.io.IOException;
 import java.util.BitSet;
diff --git a/java/com/google/gerrit/server/git/TagSetHolder.java b/java/com/google/gerrit/server/git/TagSetHolder.java
index 4c0c035..194283e 100644
--- a/java/com/google/gerrit/server/git/TagSetHolder.java
+++ b/java/com/google/gerrit/server/git/TagSetHolder.java
@@ -17,10 +17,10 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.gerrit.common.Nullable;
+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 com.google.gerrit.server.cache.serialize.ProtoCacheSerializers;
 import java.util.Collection;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
@@ -111,13 +111,12 @@
       if (tags != null) {
         b.setTags(tags.toProto());
       }
-      return ProtoCacheSerializers.toByteArray(b.build());
+      return Protos.toByteArray(b.build());
     }
 
     @Override
     public TagSetHolder deserialize(byte[] in) {
-      TagSetHolderProto proto =
-          ProtoCacheSerializers.parseUnchecked(TagSetHolderProto.parser(), in);
+      TagSetHolderProto proto = Protos.parseUnchecked(TagSetHolderProto.parser(), in);
       TagSetHolder holder = new TagSetHolder(new Project.NameKey(proto.getProjectName()));
       if (proto.hasTags()) {
         holder.tags = TagSet.fromProto(proto.getTags());
diff --git a/java/com/google/gerrit/server/git/UserConfigSections.java b/java/com/google/gerrit/server/git/UserConfigSections.java
index 859e40d..0ef908c 100644
--- a/java/com/google/gerrit/server/git/UserConfigSections.java
+++ b/java/com/google/gerrit/server/git/UserConfigSections.java
@@ -25,9 +25,6 @@
   public static final String KEY_URL = "url";
   public static final String KEY_TARGET = "target";
   public static final String KEY_ID = "id";
-  public static final String URL_ALIAS = "urlAlias";
-  public static final String KEY_MATCH = "match";
-  public static final String KEY_TOKEN = "token";
 
   /** The table column user preferences. */
   public static final String CHANGE_TABLE = "changeTable";
diff --git a/java/com/google/gerrit/server/git/WorkQueue.java b/java/com/google/gerrit/server/git/WorkQueue.java
index 2d97d04f..4522b0d 100644
--- a/java/com/google/gerrit/server/git/WorkQueue.java
+++ b/java/com/google/gerrit/server/git/WorkQueue.java
@@ -17,7 +17,6 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.CaseFormat;
-import com.google.common.base.Supplier;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
@@ -86,12 +85,8 @@
   }
 
   private static final UncaughtExceptionHandler LOG_UNCAUGHT_EXCEPTION =
-      new UncaughtExceptionHandler() {
-        @Override
-        public void uncaughtException(Thread t, Throwable e) {
+      (t, e) ->
           logger.atSevere().withCause(e).log("WorkQueue thread %s threw exception", t.getName());
-        }
-      };
 
   private final ScheduledExecutorService defaultQueue;
   private final IdGenerator idGenerator;
@@ -360,75 +355,44 @@
           new Description("Maximum allowed number of threads in the pool")
               .setGauge()
               .setUnit("threads"),
-          new Supplier<Long>() {
-            @Override
-            public Long get() {
-              return (long) getMaximumPoolSize();
-            }
-          });
+          () -> (long) getMaximumPoolSize());
       metrics.newCallbackMetric(
           getMetricName(queueName, "pool_size"),
           Long.class,
           new Description("Current number of threads in the pool").setGauge().setUnit("threads"),
-          new Supplier<Long>() {
-            @Override
-            public Long get() {
-              return (long) getPoolSize();
-            }
-          });
+          () -> (long) getPoolSize());
       metrics.newCallbackMetric(
           getMetricName(queueName, "active_threads"),
           Long.class,
           new Description("Number number of threads that are actively executing tasks")
               .setGauge()
               .setUnit("threads"),
-          new Supplier<Long>() {
-            @Override
-            public Long get() {
-              return (long) getActiveCount();
-            }
-          });
+          () -> (long) getActiveCount());
       metrics.newCallbackMetric(
           getMetricName(queueName, "scheduled_tasks"),
           Integer.class,
           new Description("Number of scheduled tasks in the queue").setGauge().setUnit("tasks"),
-          new Supplier<Integer>() {
-            @Override
-            public Integer get() {
-              return getQueue().size();
-            }
-          });
+          () -> getQueue().size());
       metrics.newCallbackMetric(
           getMetricName(queueName, "total_scheduled_tasks_count"),
           Long.class,
           new Description("Total number of tasks that have been scheduled for execution")
               .setCumulative()
               .setUnit("tasks"),
-          new Supplier<Long>() {
-            @Override
-            public Long get() {
-              return (long) getTaskCount();
-            }
-          });
+          this::getTaskCount);
       metrics.newCallbackMetric(
           getMetricName(queueName, "total_completed_tasks_count"),
           Long.class,
           new Description("Total number of tasks that have completed execution")
               .setCumulative()
               .setUnit("tasks"),
-          new Supplier<Long>() {
-            @Override
-            public Long get() {
-              return (long) getCompletedTaskCount();
-            }
-          });
+          this::getCompletedTaskCount);
     }
 
     private String getMetricName(String queueName, String metricName) {
       String name =
           CaseFormat.UPPER_CAMEL.to(
-              CaseFormat.LOWER_UNDERSCORE,
-              queueName.replaceFirst("SSH", "Ssh").replaceAll("-", ""));
+              CaseFormat.LOWER_UNDERSCORE, queueName.replaceFirst("SSH", "Ssh").replace("-", ""));
       return metrics.sanitizeMetricName(String.format("queue/%s/%s", name, metricName));
     }
 
@@ -674,7 +638,7 @@
                 for (Field innerField : innerObj.getClass().getDeclaredFields()) {
                   if (innerField.getType().isAssignableFrom(Callable.class)) {
                     innerField.setAccessible(true);
-                    return ((Callable<?>) innerField.get(innerObj)).toString();
+                    return innerField.get(innerObj).toString();
                   }
                 }
               }
diff --git a/java/com/google/gerrit/server/git/meta/MetaDataUpdate.java b/java/com/google/gerrit/server/git/meta/MetaDataUpdate.java
index bbe0c62..97beefd 100644
--- a/java/com/google/gerrit/server/git/meta/MetaDataUpdate.java
+++ b/java/com/google/gerrit/server/git/meta/MetaDataUpdate.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -33,21 +34,22 @@
 
 /** Helps with the updating of a {@link VersionedMetaData}. */
 public class MetaDataUpdate implements AutoCloseable {
+  @Singleton
   public static class User {
     private final InternalFactory factory;
     private final GitRepositoryManager mgr;
-    private final PersonIdent serverIdent;
+    private final Provider<PersonIdent> serverIdentProvider;
     private final Provider<IdentifiedUser> identifiedUser;
 
     @Inject
     User(
         InternalFactory factory,
         GitRepositoryManager mgr,
-        @GerritPersonIdent PersonIdent serverIdent,
+        @GerritPersonIdent Provider<PersonIdent> serverIdentProvider,
         Provider<IdentifiedUser> identifiedUser) {
       this.factory = factory;
       this.mgr = mgr;
-      this.serverIdent = serverIdent;
+      this.serverIdentProvider = serverIdentProvider;
       this.identifiedUser = identifiedUser;
     }
 
@@ -126,29 +128,31 @@
     public MetaDataUpdate create(
         Project.NameKey name, Repository repository, IdentifiedUser user, BatchRefUpdate batch) {
       MetaDataUpdate md = factory.create(name, repository, batch);
-      md.getCommitBuilder().setCommitter(serverIdent);
+      md.getCommitBuilder().setCommitter(serverIdentProvider.get());
       md.setAuthor(user);
       return md;
     }
 
     private PersonIdent createPersonIdent(IdentifiedUser user) {
+      PersonIdent serverIdent = serverIdentProvider.get();
       return user.newCommitterIdent(serverIdent.getWhen(), serverIdent.getTimeZone());
     }
   }
 
+  @Singleton
   public static class Server {
     private final InternalFactory factory;
     private final GitRepositoryManager mgr;
-    private final PersonIdent serverIdent;
+    private final Provider<PersonIdent> serverIdentProvider;
 
     @Inject
     Server(
         InternalFactory factory,
         GitRepositoryManager mgr,
-        @GerritPersonIdent PersonIdent serverIdent) {
+        @GerritPersonIdent Provider<PersonIdent> serverIdentProvider) {
       this.factory = factory;
       this.mgr = mgr;
-      this.serverIdent = serverIdent;
+      this.serverIdentProvider = serverIdentProvider;
     }
 
     public MetaDataUpdate create(Project.NameKey name)
@@ -162,6 +166,7 @@
       Repository repo = mgr.openRepository(name);
       MetaDataUpdate md = factory.create(name, repo, batch);
       md.setCloseRepository(true);
+      PersonIdent serverIdent = serverIdentProvider.get();
       md.getCommitBuilder().setAuthor(serverIdent);
       md.getCommitBuilder().setCommitter(serverIdent);
       return md;
diff --git a/java/com/google/gerrit/server/git/meta/VersionedMetaData.java b/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
index 2e79a1a..f109570 100644
--- a/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
+++ b/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
@@ -18,8 +18,8 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.git.LockFailureException;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.git.LockFailureException;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import java.io.BufferedReader;
@@ -461,7 +461,12 @@
   }
 
   protected Config readConfig(String fileName) throws IOException, ConfigInvalidException {
-    Config rc = new Config();
+    return readConfig(fileName, null);
+  }
+
+  protected Config readConfig(String fileName, Config baseConfig)
+      throws IOException, ConfigInvalidException {
+    Config rc = new Config(baseConfig);
     String text = readUTF8(fileName);
     if (!text.isEmpty()) {
       try {
diff --git a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
index 882f208..da2887f 100644
--- a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
@@ -14,10 +14,12 @@
 
 package com.google.gerrit.server.git.receive;
 
+import static com.google.gerrit.server.quota.QuotaGroupDefinitions.REPOSITORY_SIZE_GROUP;
 import static java.util.concurrent.TimeUnit.NANOSECONDS;
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.common.data.Capable;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -31,7 +33,6 @@
 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.UsedAt;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.ReceiveCommitsExecutor;
@@ -46,6 +47,9 @@
 import com.google.gerrit.server.project.ContributorAgreementsChecker;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gerrit.server.quota.QuotaBackend;
+import com.google.gerrit.server.quota.QuotaException;
+import com.google.gerrit.server.quota.QuotaResponse;
 import com.google.gerrit.server.util.MagicBranch;
 import com.google.gerrit.server.util.RequestScopePropagator;
 import com.google.inject.Inject;
@@ -96,6 +100,7 @@
   public static class Module extends PrivateModule {
     @Override
     public void configure() {
+      install(new FactoryModuleBuilder().build(LazyPostReceiveHookChain.Factory.class));
       install(new FactoryModuleBuilder().build(AsyncReceiveCommits.Factory.class));
       expose(AsyncReceiveCommits.Factory.class);
       // Don't expose the binding for ReceiveCommits.Factory. All callers should
@@ -115,17 +120,25 @@
 
   private class Worker implements ProjectRunnable {
     final MultiProgressMonitor progress;
+    final String name;
 
     private final Collection<ReceiveCommand> commands;
 
-    private Worker(Collection<ReceiveCommand> commands) {
+    private Worker(Collection<ReceiveCommand> commands, String name) {
       this.commands = commands;
+      this.name = name;
       progress = new MultiProgressMonitor(new MessageSenderOutputStream(), "Processing changes");
     }
 
     @Override
     public void run() {
-      receiveCommits.processCommands(commands, progress);
+      String oldName = Thread.currentThread().getName();
+      Thread.currentThread().setName(oldName + "-for-" + name);
+      try {
+        receiveCommits.processCommands(commands, progress);
+      } finally {
+        Thread.currentThread().setName(oldName);
+      }
     }
 
     @Override
@@ -175,29 +188,45 @@
     }
   }
 
+  private enum PushType {
+    CREATE_REPLACE,
+    NORMAL,
+    AUTOCLOSE,
+  }
+
   @Singleton
   private static class Metrics {
-    private final Histogram1<ResultChangeIds.Key> changes;
-    private final Timer1<String> latencyPerChange;
+    private final Histogram1<PushType> changes;
+    private final Timer1<PushType> latencyPerChange;
+    private final Timer1<PushType> latencyPerPush;
     private final Counter0 timeouts;
 
     @Inject
     Metrics(MetricMaker metricMaker) {
       changes =
           metricMaker.newHistogram(
-              "receivecommits/changes",
+              "receivecommits/changes_per_push",
               new Description("number of changes uploaded in a single push.").setCumulative(),
-              Field.ofEnum(
-                  ResultChangeIds.Key.class,
-                  "type",
-                  "type of update (replace, create, autoclose)"));
+              Field.ofEnum(PushType.class, "type", "type of push (create/replace, autoclose)"));
+
       latencyPerChange =
           metricMaker.newTimer(
-              "receivecommits/latency",
-              new Description("average delay per updated change")
+              "receivecommits/latency_per_push_per_change",
+              new Description(
+                      "Processing delay per push divided by the number of changes in said push. "
+                          + "(Only includes pushes which contain changes.)")
                   .setUnit(Units.MILLISECONDS)
                   .setCumulative(),
-              Field.ofString("type", "type of update (create/replace, autoclose)"));
+              Field.ofEnum(PushType.class, "type", "type of push (create/replace, autoclose)"));
+
+      latencyPerPush =
+          metricMaker.newTimer(
+              "receivecommits/latency_per_push",
+              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)"));
 
       timeouts =
           metricMaker.newCounter(
@@ -229,9 +258,10 @@
       RequestScopePropagator scopePropagator,
       ReceiveConfig receiveConfig,
       TransferConfig transferConfig,
-      Provider<LazyPostReceiveHookChain> lazyPostReceive,
+      LazyPostReceiveHookChain.Factory lazyPostReceive,
       ContributorAgreementsChecker contributorAgreements,
       Metrics metrics,
+      QuotaBackend quotaBackend,
       @Named(TIMEOUT_NAME) long timeoutMillis,
       @Assisted ProjectState projectState,
       @Assisted IdentifiedUser user,
@@ -260,7 +290,7 @@
     receivePack.setRefFilter(new ReceiveRefFilter());
     receivePack.setAllowPushOptions(true);
     receivePack.setPreReceiveHook(this);
-    receivePack.setPostReceiveHook(lazyPostReceive.get());
+    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.
@@ -287,6 +317,17 @@
         factory.create(
             projectState, user, receivePack, allRefsWatcher, messageSender, resultChangeIds);
     receiveCommits.init();
+    QuotaResponse.Aggregated availableTokens =
+        quotaBackend.user(user).project(projectName).availableTokens(REPOSITORY_SIZE_GROUP);
+    try {
+      availableTokens.throwOnError();
+    } catch (QuotaException e) {
+      logger.atWarning().withCause(e).log(
+          "Quota %s availableTokens request failed for project %s",
+          REPOSITORY_SIZE_GROUP, projectName);
+      throw new RuntimeException(e);
+    }
+    availableTokens.availableTokens().ifPresent(v -> receivePack.setMaxObjectSizeLimit(v));
   }
 
   /** Determine if the user can upload commits. */
@@ -318,7 +359,7 @@
     }
 
     long startNanos = System.nanoTime();
-    Worker w = new Worker(commands);
+    Worker w = new Worker(commands, Thread.currentThread().getName());
     try {
       w.progress.waitFor(
           executor.submit(scopePropagator.wrap(w)), timeoutMillis, TimeUnit.MILLISECONDS);
@@ -341,20 +382,30 @@
 
     long deltaNanos = System.nanoTime() - startNanos;
     int totalChanges = 0;
-    for (ResultChangeIds.Key key : ResultChangeIds.Key.values()) {
-      List<Change.Id> ids = resultChangeIds.get(key);
-      metrics.changes.record(key, ids.size());
-      totalChanges += ids.size();
+
+    PushType pushType;
+    if (resultChangeIds.isMagicPush()) {
+      pushType = PushType.CREATE_REPLACE;
+      List<Change.Id> created = resultChangeIds.get(ResultChangeIds.Key.CREATED);
+      List<Change.Id> replaced = resultChangeIds.get(ResultChangeIds.Key.REPLACED);
+      metrics.changes.record(pushType, created.size() + replaced.size());
+      totalChanges = replaced.size() + created.size();
+    } else {
+      List<Change.Id> autoclosed = resultChangeIds.get(ResultChangeIds.Key.AUTOCLOSED);
+      if (!autoclosed.isEmpty()) {
+        pushType = PushType.AUTOCLOSE;
+        metrics.changes.record(pushType, autoclosed.size());
+        totalChanges = autoclosed.size();
+      } else {
+        pushType = PushType.NORMAL;
+      }
     }
 
     if (totalChanges > 0) {
-      metrics.latencyPerChange.record(
-          resultChangeIds.get(ResultChangeIds.Key.AUTOCLOSED).isEmpty()
-              ? "CREATE_REPLACE"
-              : ResultChangeIds.Key.AUTOCLOSED.name(),
-          deltaNanos / totalChanges,
-          NANOSECONDS);
+      metrics.latencyPerChange.record(pushType, deltaNanos / totalChanges, NANOSECONDS);
     }
+
+    metrics.latencyPerPush.record(pushType, deltaNanos, NANOSECONDS);
   }
 
   /** Returns the Change.Ids that were processed in onPreReceive */
diff --git a/java/com/google/gerrit/server/git/receive/BUILD b/java/com/google/gerrit/server/git/receive/BUILD
index 6a51474..b1bf933 100644
--- a/java/com/google/gerrit/server/git/receive/BUILD
+++ b/java/com/google/gerrit/server/git/receive/BUILD
@@ -7,6 +7,7 @@
     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/metrics",
         "//java/com/google/gerrit/reviewdb:server",
@@ -16,7 +17,6 @@
         "//java/com/google/gerrit/util/cli",
         "//lib:args4j",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
         "//lib/flogger:api",
diff --git a/java/com/google/gerrit/server/git/receive/HackPushNegotiateHook.java b/java/com/google/gerrit/server/git/receive/HackPushNegotiateHook.java
index bf3d270..36d0eb7 100644
--- a/java/com/google/gerrit/server/git/receive/HackPushNegotiateHook.java
+++ b/java/com/google/gerrit/server/git/receive/HackPushNegotiateHook.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.git.receive;
 
-import static org.eclipse.jgit.lib.RefDatabase.ALL;
+import static java.util.stream.Collectors.toMap;
 
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
@@ -73,12 +73,15 @@
     throw new UnsupportedOperationException("HackPushNegotiateHook cannot be used for UploadPack");
   }
 
+  @SuppressWarnings("deprecation")
   @Override
   public void advertiseRefs(BaseReceivePack rp) throws ServiceMayNotContinueException {
     Map<String, Ref> r = rp.getAdvertisedRefs();
     if (r == null) {
       try {
-        r = rp.getRepository().getRefDatabase().getRefs(ALL);
+        r =
+            rp.getRepository().getRefDatabase().getRefs().stream()
+                .collect(toMap(Ref::getName, x -> x));
       } catch (ServiceMayNotContinueException e) {
         throw e;
       } catch (IOException e) {
@@ -100,6 +103,7 @@
     }
 
     // Scan history until the advertisement is full.
+    @SuppressWarnings("deprecation")
     RevWalk rw = rp.getRevWalk();
     rw.reset();
     try {
diff --git a/java/com/google/gerrit/server/git/receive/LazyPostReceiveHookChain.java b/java/com/google/gerrit/server/git/receive/LazyPostReceiveHookChain.java
index 7adb21b..17d4a38 100644
--- a/java/com/google/gerrit/server/git/receive/LazyPostReceiveHookChain.java
+++ b/java/com/google/gerrit/server/git/receive/LazyPostReceiveHookChain.java
@@ -14,25 +14,85 @@
 
 package com.google.gerrit.server.git.receive;
 
-import com.google.gerrit.extensions.registration.DynamicSet;
+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.server.CurrentUser;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
+import com.google.gerrit.server.quota.QuotaBackend;
+import com.google.gerrit.server.quota.QuotaResponse;
 import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
 import java.util.Collection;
 import org.eclipse.jgit.transport.PostReceiveHook;
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.transport.ReceivePack;
 
-class LazyPostReceiveHookChain implements PostReceiveHook {
-  private final DynamicSet<PostReceiveHook> hooks;
+/**
+ * Class is responsible for calling all registered post-receive hooks. In addition, in case when
+ * repository size quota is defined, it requests tokens (pack size) that were received. This is the
+ * final step of enforcing repository size quota that deducts token from available tokens.
+ */
+public class LazyPostReceiveHookChain implements PostReceiveHook {
+  interface Factory {
+    LazyPostReceiveHookChain create(CurrentUser user, Project.NameKey project);
+  }
+
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final PluginSetContext<PostReceiveHook> hooks;
+  private final QuotaBackend quotaBackend;
+  private final CurrentUser user;
+  private final Project.NameKey project;
 
   @Inject
-  LazyPostReceiveHookChain(DynamicSet<PostReceiveHook> hooks) {
+  LazyPostReceiveHookChain(
+      PluginSetContext<PostReceiveHook> hooks,
+      QuotaBackend quotaBackend,
+      @Assisted CurrentUser user,
+      @Assisted Project.NameKey project) {
     this.hooks = hooks;
+    this.quotaBackend = quotaBackend;
+    this.user = user;
+    this.project = project;
   }
 
   @Override
   public void onPostReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
-    for (PostReceiveHook h : hooks) {
-      h.onPostReceive(rp, commands);
+    hooks.runEach(h -> h.onPostReceive(rp, commands));
+    if (affectsSize(rp, commands)) {
+      QuotaResponse.Aggregated a =
+          quotaBackend
+              .user(user)
+              .project(project)
+              .requestTokens(REPOSITORY_SIZE_GROUP, rp.getPackSize());
+      if (a.hasError()) {
+        String msg =
+            String.format(
+                "%s request failed for project %s with [%s]",
+                REPOSITORY_SIZE_GROUP, project, a.errorMessage());
+        logger.atWarning().log(msg);
+        throw new RuntimeException(msg);
+      }
     }
   }
+
+  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;
+  }
 }
diff --git a/java/com/google/gerrit/server/git/receive/MessageSender.java b/java/com/google/gerrit/server/git/receive/MessageSender.java
index 1f66570..4fa5451 100644
--- a/java/com/google/gerrit/server/git/receive/MessageSender.java
+++ b/java/com/google/gerrit/server/git/receive/MessageSender.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.git.receive;
 
-import com.google.gerrit.server.UsedAt;
+import com.google.gerrit.common.UsedAt;
 
 /**
  * Interface used by {@link ReceiveCommits} for send messages over the wire during {@code
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 414f63a..b8181b4 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -18,6 +18,7 @@
 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.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;
@@ -30,7 +31,6 @@
 import static com.google.gerrit.server.git.validators.CommitValidators.NEW_PATCHSET_PATTERN;
 import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.Comparator.comparingInt;
 import static java.util.Objects.requireNonNull;
 import static java.util.stream.Collectors.joining;
 import static java.util.stream.Collectors.toList;
@@ -49,6 +49,7 @@
 import com.google.common.collect.HashBiMap;
 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.collect.LinkedListMultimap;
 import com.google.common.collect.ListMultimap;
@@ -63,6 +64,7 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
+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.RecipientType;
@@ -86,16 +88,16 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CreateGroupPermissionSyncer;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
-import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.change.ChangeInserter;
+import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.change.SetHashtagsOp;
+import com.google.gerrit.server.change.SetPrivateOp;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.PluginConfig;
@@ -111,7 +113,6 @@
 import com.google.gerrit.server.git.ReceivePackInitializer;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.ValidationError;
-import com.google.gerrit.server.git.receive.ResultChangeIds.Key;
 import com.google.gerrit.server.git.validators.CommitValidationMessage;
 import com.google.gerrit.server.git.validators.RefOperationValidationException;
 import com.google.gerrit.server.git.validators.RefOperationValidators;
@@ -121,7 +122,7 @@
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.mail.MailUtil.MailRecipients;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.notedb.Sequences;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.GlobalPermission;
@@ -158,7 +159,6 @@
 import com.google.gerrit.server.util.RequestScopePropagator;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.util.cli.CmdLineParser;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
@@ -175,14 +175,17 @@
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
+import java.util.TreeSet;
 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;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -303,7 +306,6 @@
   private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
   private final PluginSetContext<ReceivePackInitializer> initializers;
   private final MergedByPushOp.Factory mergedByPushOpFactory;
-  private final NotesMigration notesMigration;
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final PatchSetUtil psUtil;
   private final PermissionBackend permissionBackend;
@@ -316,11 +318,12 @@
   private final ReplaceOp.Factory replaceOpFactory;
   private final RetryHelper retryHelper;
   private final RequestScopePropagator requestScopePropagator;
-  private final ReviewDb db;
   private final Sequences seq;
   private final SetHashtagsOp.Factory hashtagsFactory;
   private final SubmoduleOp.Factory subOpFactory;
   private final TagCache tagCache;
+  private final ProjectConfig.Factory projectConfigFactory;
+  private final SetPrivateOp.Factory setPrivateOpFactory;
 
   // Assisted injected fields.
   private final AllRefsWatcher allRefsWatcher;
@@ -365,6 +368,7 @@
       AccountResolver accountResolver,
       AllProjectsName allProjectsName,
       BatchUpdate.Factory batchUpdateFactory,
+      ProjectConfig.Factory projectConfigFactory,
       @GerritServerConfig Config cfg,
       ChangeEditUtil editUtil,
       ChangeIndexer indexer,
@@ -378,7 +382,6 @@
       DynamicMap<ProjectConfigEntry> pluginConfigEntries,
       PluginSetContext<ReceivePackInitializer> initializers,
       MergedByPushOp.Factory mergedByPushOpFactory,
-      NotesMigration notesMigration,
       PatchSetInfoFactory patchSetInfoFactory,
       PatchSetUtil psUtil,
       PermissionBackend permissionBackend,
@@ -391,11 +394,11 @@
       ReplaceOp.Factory replaceOpFactory,
       RetryHelper retryHelper,
       RequestScopePropagator requestScopePropagator,
-      ReviewDb db,
       Sequences seq,
       SetHashtagsOp.Factory hashtagsFactory,
       SubmoduleOp.Factory subOpFactory,
       TagCache tagCache,
+      SetPrivateOp.Factory setPrivateOpFactory,
       @Assisted ProjectState projectState,
       @Assisted IdentifiedUser user,
       @Assisted ReceivePack rp,
@@ -412,7 +415,6 @@
     this.commitValidatorFactory = commitValidatorFactory;
     this.createRefControl = createRefControl;
     this.createGroupPermissionSyncer = createGroupPermissionSyncer;
-    this.db = db;
     this.editUtil = editUtil;
     this.hashtagsFactory = hashtagsFactory;
     this.indexer = indexer;
@@ -420,7 +422,6 @@
     this.mergeOpProvider = mergeOpProvider;
     this.mergedByPushOpFactory = mergedByPushOpFactory;
     this.notesFactory = notesFactory;
-    this.notesMigration = notesMigration;
     this.optionParserFactory = optionParserFactory;
     this.ormProvider = ormProvider;
     this.patchSetInfoFactory = patchSetInfoFactory;
@@ -437,6 +438,8 @@
     this.seq = seq;
     this.subOpFactory = subOpFactory;
     this.tagCache = tagCache;
+    this.projectConfigFactory = projectConfigFactory;
+    this.setPrivateOpFactory = setPrivateOpFactory;
 
     // Assisted injected fields.
     this.allRefsWatcher = allRefsWatcher;
@@ -530,6 +533,8 @@
             (tagName, traceId) -> addMessage(tagName + ": " + traceId))) {
       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());
@@ -607,14 +612,10 @@
       newProgress.end();
       replaceProgress.end();
       queueSuccessMessages(newChanges);
-      refsPublishDeprecationWarning();
-    }
-  }
 
-  private void refsPublishDeprecationWarning() {
-    // TODO(xchangcheng): remove after migrating tools which are using this magic branch.
-    if (magicBranch != null && magicBranch.publish) {
-      addMessage("Pushing to refs/publish/* is deprecated, use refs/for/* instead.");
+      logger.atFine().log(
+          "Command results: %s",
+          lazy(() -> commands.stream().map(ReceiveCommits::commandToString).collect(joining(","))));
     }
   }
 
@@ -631,17 +632,18 @@
 
   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(
-                db, project.getNameKey(), user.materializedCopy(), TimeUtil.nowTs());
+                project.getNameKey(), user.materializedCopy(), TimeUtil.nowTs());
         ObjectInserter ins = repo.newObjectInserter();
         ObjectReader reader = ins.newReader();
         RevWalk rw = new RevWalk(reader)) {
-      bu.setRepository(repo, rw, ins).updateChangesInParallel();
+      bu.setRepository(repo, rw, ins);
       bu.setRefLogMessage("push");
 
       int added = 0;
@@ -686,7 +688,7 @@
     // Update superproject gitlinks if required.
     if (!branches.isEmpty()) {
       try (MergeOpRepoManager orm = ormProvider.get()) {
-        orm.setContext(db, TimeUtil.nowTs(), user);
+        orm.setContext(TimeUtil.nowTs(), user, NotifyResolver.Result.none());
         SubmoduleOp op = subOpFactory.create(branches, orm);
         op.updateSuperProjects();
       } catch (SubmoduleException e) {
@@ -697,13 +699,48 @@
 
   /** Appends messages for successful change creation/updates. */
   private void queueSuccessMessages(List<CreateRequest> newChanges) {
-    List<CreateRequest> created =
-        newChanges.stream().filter(r -> r.change != null).collect(toList());
-    List<ReplaceRequest> updated =
+    // adjacency list for commit => parent
+    Map<String, String> adjList = new HashMap<>();
+    for (CreateRequest cr : newChanges) {
+      String parent = cr.commit.getParentCount() == 0 ? null : cr.commit.getParent(0).name();
+      adjList.put(cr.commit.name(), parent);
+    }
+    for (ReplaceRequest rr : replaceByChange.values()) {
+      String parent = null;
+      if (rr.revCommit != null) {
+        parent = rr.revCommit.getParentCount() == 0 ? null : rr.revCommit.getParent(0).name();
+      }
+      adjList.put(rr.newCommitId.name(), parent);
+    }
+
+    // get commits that are not parents
+    Set<String> leafs = new TreeSet<>(adjList.keySet());
+    leafs.removeAll(adjList.values());
+    // go backwards from the last commit to its parent(s)
+    Set<String> ordered = new LinkedHashSet<>();
+    for (String leaf : leafs) {
+      if (ordered.contains(leaf)) {
+        continue;
+      }
+      while (leaf != null) {
+        if (!ordered.contains(leaf)) {
+          ordered.add(leaf);
+        }
+        leaf = adjList.get(leaf);
+      }
+    }
+    // reverse the order to start with earliest commit
+    List<String> orderedCommits = new ArrayList<>(ordered);
+    Collections.reverse(orderedCommits);
+
+    Map<String, CreateRequest> created =
+        newChanges.stream()
+            .filter(r -> r.change != null)
+            .collect(Collectors.toMap(r -> r.commit.name(), r -> r));
+    Map<String, ReplaceRequest> updated =
         replaceByChange.values().stream()
             .filter(r -> r.inputCommand.getResult() == OK)
-            .sorted(comparingInt(r -> r.notes.getChangeId().get()))
-            .collect(toList());
+            .collect(Collectors.toMap(r -> r.newCommitId.name(), r -> r));
 
     if (created.isEmpty() && updated.isEmpty()) {
       return;
@@ -711,63 +748,68 @@
 
     addMessage("");
     addMessage("SUCCESS");
+    addMessage("");
 
-    if (!created.isEmpty()) {
-      addMessage("");
-      addMessage("New Changes:");
-      for (CreateRequest c : created) {
-        addMessage(
-            changeFormatter.newChange(
-                ChangeReportFormatter.Input.builder().setChange(c.change).build()));
-      }
-    }
-
+    boolean edit = false;
+    Boolean isPrivate = null;
+    Boolean wip = null;
     if (!updated.isEmpty()) {
-      addMessage("");
-      addMessage("Updated Changes:");
-      boolean edit = magicBranch != null && (magicBranch.edit || magicBranch.draft);
-      for (ReplaceRequest u : updated) {
-        String subject;
-        Change change = u.notes.getChange();
-        if (edit) {
-          try {
-            subject = receivePack.getRevWalk().parseCommit(u.newCommitId).getShortMessage();
-          } catch (IOException e) {
-            // Log and fall back to original change subject
-            logger.atWarning().withCause(e).log("failed to get subject for edit patch set");
-            subject = change.getSubject();
-          }
-        } else {
-          subject = u.info.getSubject();
+      edit = magicBranch != null && (magicBranch.edit || magicBranch.draft);
+      if (magicBranch != null) {
+        if (magicBranch.isPrivate) {
+          isPrivate = true;
+        } else if (magicBranch.removePrivate) {
+          isPrivate = false;
         }
-
-        boolean isPrivate = change.isPrivate();
-        boolean wip = change.isWorkInProgress();
-        if (magicBranch != null) {
-          if (magicBranch.isPrivate) {
-            isPrivate = true;
-          } else if (magicBranch.removePrivate) {
-            isPrivate = false;
-          }
-          if (magicBranch.workInProgress) {
-            wip = true;
-          } else if (magicBranch.ready) {
-            wip = false;
-          }
+        if (magicBranch.workInProgress) {
+          wip = true;
+        } else if (magicBranch.ready) {
+          wip = false;
         }
-
-        ChangeReportFormatter.Input input =
-            ChangeReportFormatter.Input.builder()
-                .setChange(change)
-                .setSubject(subject)
-                .setIsEdit(edit)
-                .setIsPrivate(isPrivate)
-                .setIsWorkInProgress(wip)
-                .build();
-        addMessage(changeFormatter.changeUpdated(input));
       }
-      addMessage("");
     }
+
+    for (String commit : orderedCommits) {
+      if (created.get(commit) != null) {
+        addCreatedMessage(created.get(commit));
+      } else if (updated.get(commit) != null) {
+        addReplacedMessage(updated.get(commit), edit, isPrivate, wip);
+      }
+    }
+    addMessage("");
+  }
+
+  private void addCreatedMessage(CreateRequest c) {
+    addMessage(
+        changeFormatter.newChange(
+            ChangeReportFormatter.Input.builder().setChange(c.change).build()));
+  }
+
+  private void addReplacedMessage(ReplaceRequest u, boolean edit, Boolean isPrivate, Boolean wip) {
+    String subject;
+    if (edit) {
+      subject =
+          u.revCommit == null ? u.notes.getChange().getSubject() : u.revCommit.getShortMessage();
+    } else {
+      subject = u.info.getSubject();
+    }
+
+    if (isPrivate == null) {
+      isPrivate = u.notes.getChange().isPrivate();
+    }
+    if (wip == null) {
+      wip = u.notes.getChange().isWorkInProgress();
+    }
+
+    ChangeReportFormatter.Input input =
+        ChangeReportFormatter.Input.builder()
+            .setChange(u.notes.getChange())
+            .setSubject(subject)
+            .setIsEdit(edit)
+            .setIsPrivate(isPrivate)
+            .setIsWorkInProgress(wip)
+            .build();
+    addMessage(changeFormatter.changeUpdated(input));
   }
 
   private void insertChangesAndPatchSets(List<CreateRequest> newChanges, Task replaceProgress) {
@@ -783,15 +825,21 @@
 
     try (BatchUpdate bu =
             batchUpdateFactory.create(
-                db, project.getNameKey(), user.materializedCopy(), TimeUtil.nowTs());
+                project.getNameKey(), user.materializedCopy(), TimeUtil.nowTs());
         ObjectInserter ins = repo.newObjectInserter();
         ObjectReader reader = ins.newReader();
         RevWalk rw = new RevWalk(reader)) {
-      bu.setRepository(repo, rw, ins).updateChangesInParallel();
+      bu.setRepository(repo, rw, ins);
       bu.setRefLogMessage("push");
+      if (magicBranch != null) {
+        bu.setNotify(magicBranch.getNotifyForNewChange());
+      }
 
       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));
+        }
         replace.addOps(bu, replaceProgress);
       }
 
@@ -811,8 +859,9 @@
       }
 
       replaceByChange.values().stream()
-          .forEach(req -> resultChangeIds.add(Key.REPLACED, req.ontoChange));
-      newChanges.stream().forEach(req -> resultChangeIds.add(Key.CREATED, req.changeId));
+          .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);
@@ -833,7 +882,7 @@
     } catch (ResourceConflictException e) {
       addError(e.getMessage());
       reject(magicBranchCmd, "conflict");
-    } catch (BadRequestException | UnprocessableEntityException e) {
+    } catch (BadRequestException | UnprocessableEntityException | AuthException e) {
       logger.atFine().withCause(e).log("Rejecting due to client error");
       reject(magicBranchCmd, e.getMessage());
     } catch (RestApiException | IOException e) {
@@ -848,7 +897,7 @@
         addError(e.getMessage());
         reject(magicBranchCmd, "conflict");
       } catch (RestApiException
-          | OrmException
+          | StorageException
           | UpdateException
           | IOException
           | ConfigInvalidException
@@ -1046,7 +1095,7 @@
       case UPDATE:
       case UPDATE_NONFASTFORWARD:
         try {
-          ProjectConfig cfg = new ProjectConfig(project.getNameKey());
+          ProjectConfig cfg = projectConfigFactory.create(project.getNameKey());
           cfg.load(project.getNameKey(), receivePack.getRevWalk(), cmd.getNewId());
           if (!cfg.getValidationErrors().isEmpty()) {
             addError("Invalid project configuration:");
@@ -1315,7 +1364,6 @@
     boolean deprecatedTopicSeen;
     final ReceiveCommand cmd;
     final LabelTypes labelTypes;
-    final NotesMigration notesMigration;
     private final boolean defaultPublishComments;
     Branch.NameKey dest;
     PermissionBackend.ForRef perm;
@@ -1343,8 +1391,6 @@
                 + "for new changes and '--edit' for existing changes")
     boolean draft;
 
-    boolean publish;
-
     @Option(name = "--private", usage = "mark new/updated change as private")
     boolean isPrivate;
 
@@ -1387,7 +1433,7 @@
             "Notify handling that defines to whom email notifications "
                 + "should be sent. Allowed values are NONE, OWNER, "
                 + "OWNER_REVIEWERS, ALL. If not set, the default is ALL.")
-    private NotifyHandling notify;
+    private NotifyHandling notifyHandling;
 
     @Option(
         name = "--notify-to",
@@ -1463,10 +1509,7 @@
         aliases = {"-t"},
         metaVar = "HASHTAG",
         usage = "add hashtag to changes")
-    void addHashtag(String token) throws CmdLineException {
-      if (!notesMigration.readChanges()) {
-        throw cmdLineParser.reject("cannot add hashtags; noteDb is disabled");
-      }
+    void addHashtag(String token) {
       String hashtag = cleanupHashtag(token);
       if (!hashtag.isEmpty()) {
         hashtags.add(hashtag);
@@ -1474,17 +1517,11 @@
       // TODO(dpursehouse): validate hashtags
     }
 
-    MagicBranchInput(
-        IdentifiedUser user,
-        ReceiveCommand cmd,
-        LabelTypes labelTypes,
-        NotesMigration notesMigration) {
+    MagicBranchInput(IdentifiedUser user, ReceiveCommand cmd, LabelTypes labelTypes) {
       this.deprecatedTopicSeen = false;
       this.cmd = cmd;
       this.draft = cmd.getRefName().startsWith(MagicBranch.NEW_DRAFT_CHANGE);
-      this.publish = cmd.getRefName().startsWith(MagicBranch.NEW_PUBLISH_CHANGE);
       this.labelTypes = labelTypes;
-      this.notesMigration = notesMigration;
       GeneralPreferencesInfo prefs = user.state().getGeneralPreferences();
       this.defaultPublishComments =
           prefs != null
@@ -1526,15 +1563,6 @@
           .collect(toImmutableSet());
     }
 
-    ListMultimap<RecipientType, Account.Id> getAccountsToNotify() {
-      ListMultimap<RecipientType, Account.Id> accountsToNotify =
-          MultimapBuilder.hashKeys().arrayListValues().build();
-      accountsToNotify.putAll(RecipientType.TO, notifyTo);
-      accountsToNotify.putAll(RecipientType.CC, notifyCc);
-      accountsToNotify.putAll(RecipientType.BCC, notifyBcc);
-      return accountsToNotify;
-    }
-
     boolean shouldPublishComments() {
       if (publishComments) {
         return true;
@@ -1594,19 +1622,20 @@
       return ref.substring(0, split);
     }
 
-    NotifyHandling getNotify() {
-      if (notify != null) {
-        return notify;
-      }
-      if (workInProgress) {
-        return NotifyHandling.OWNER;
-      }
-      return NotifyHandling.ALL;
+    NotifyResolver.Result getNotifyForNewChange() {
+      return NotifyResolver.Result.create(
+          firstNonNull(notifyHandling, workInProgress ? NotifyHandling.OWNER : NotifyHandling.ALL),
+          ImmutableSetMultimap.<RecipientType, Account.Id>builder()
+              .putAll(RecipientType.TO, notifyTo)
+              .putAll(RecipientType.CC, notifyCc)
+              .putAll(RecipientType.BCC, notifyBcc)
+              .build());
     }
 
-    NotifyHandling getNotify(ChangeNotes notes) {
-      if (notify != null) {
-        return notify;
+    NotifyHandling getNotifyHandling(ChangeNotes notes) {
+      requireNonNull(notes);
+      if (notifyHandling != null) {
+        return notifyHandling;
       }
       if (workInProgress || (!ready && notes.getChange().isWorkInProgress())) {
         return NotifyHandling.OWNER;
@@ -1623,7 +1652,7 @@
    */
   private void parseMagicBranch(ReceiveCommand cmd) throws PermissionBackendException {
     logger.atFine().log("Found magic branch %s", cmd.getRefName());
-    MagicBranchInput magicBranch = new MagicBranchInput(user, cmd, labelTypes, notesMigration);
+    MagicBranchInput magicBranch = new MagicBranchInput(user, cmd, labelTypes);
 
     String ref;
     magicBranch.cmdLineParser = optionParserFactory.create(magicBranch);
@@ -1656,6 +1685,11 @@
       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)) {
@@ -1739,9 +1773,10 @@
           reject(cmd, "cannot use merged with base");
           return;
         }
-        RevCommit branchTip = readBranchTip(cmd, magicBranch.dest);
+        RevCommit branchTip = readBranchTip(magicBranch.dest);
         if (branchTip == null) {
-          return; // readBranchTip already rejected cmd.
+          reject(cmd, magicBranch.dest.get() + " not found");
+          return;
         }
         if (!walk.isMergedInto(tip, branchTip)) {
           reject(cmd, "not merged into branch");
@@ -1779,12 +1814,21 @@
           }
         }
       } else if (newChangeForAllNotInTarget) {
-        RevCommit branchTip = readBranchTip(cmd, magicBranch.dest);
-        if (branchTip == null) {
-          return; // readBranchTip already rejected cmd.
+        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;
+          }
         }
-        magicBranch.baseCommit = Collections.singletonList(branchTip);
-        logger.atFine().log("Set baseCommit = %s", magicBranch.baseCommit.get(0).name());
       }
     } catch (IOException ex) {
       logger.atWarning().withCause(ex).log(
@@ -1796,12 +1840,13 @@
     if (magicBranch.deprecatedTopicSeen) {
       messages.add(
           new ValidationMessage(
-              "WARNING: deprecated topic syntax. Use %topic=TOPIC instead", false));
+              "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);
     }
   }
 
@@ -1849,17 +1894,18 @@
 
   private static String readHEAD(Repository repo) {
     try {
-      return repo.getFullBranch();
+      String head = repo.getFullBranch();
+      logger.atFine().log("HEAD = %s", head);
+      return head;
     } catch (IOException e) {
       logger.atSevere().withCause(e).log("Cannot read HEAD symref");
       return null;
     }
   }
 
-  private RevCommit readBranchTip(ReceiveCommand cmd, Branch.NameKey branch) throws IOException {
+  private RevCommit readBranchTip(Branch.NameKey branch) throws IOException {
     Ref r = allRefs().get(branch.get());
     if (r == null) {
-      reject(cmd, branch.get() + " not found");
       return null;
     }
     return receivePack.getRevWalk().parseCommit(r.getObjectId());
@@ -1885,12 +1931,12 @@
 
     Change changeEnt;
     try {
-      changeEnt = notesFactory.createChecked(db, project.getNameKey(), changeId).getChange();
+      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 (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Cannot lookup existing change %s", changeId);
       reject(cmd, "database error");
       return;
@@ -1925,7 +1971,7 @@
    */
   private boolean requestReplace(
       ReceiveCommand cmd, boolean checkMergedInto, Change change, RevCommit newCommit) {
-    if (change.getStatus().isClosed()) {
+    if (change.isClosed()) {
       reject(
           cmd,
           changeFormatter.changeClosed(
@@ -1965,7 +2011,7 @@
 
     ListMultimap<ObjectId, Ref> existing = changeRefsById();
     GroupCollector groupCollector =
-        GroupCollector.create(changeRefsById(), db, psUtil, notesFactory, project.getNameKey());
+        GroupCollector.create(changeRefsById(), psUtil, notesFactory, project.getNameKey());
 
     BranchCommitValidator validator =
         commitValidatorFactory.create(projectState, magicBranch.dest, user);
@@ -2189,7 +2235,7 @@
       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 (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Cannot query database to locate prior changes");
       reject(magicBranch.cmd, "database error");
       return Collections.emptyList();
@@ -2219,17 +2265,17 @@
         update.groups = ImmutableList.copyOf((groups.get(update.commit)));
       }
       logger.atFine().log("Finished updating groups from GroupCollector");
-    } catch (OrmException e) {
+    } 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) throws OrmException {
+  private boolean foundInExistingRef(Collection<Ref> existingRefs) {
     for (Ref ref : existingRefs) {
       ChangeNotes notes =
-          notesFactory.create(db, project.getNameKey(), Change.Id.fromRef(ref.getName()));
+          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());
@@ -2346,11 +2392,11 @@
     }
   }
 
-  private ChangeLookup lookupByChangeKey(RevCommit c, Change.Key key) throws OrmException {
+  private ChangeLookup lookupByChangeKey(RevCommit c, Change.Key key) {
     return new ChangeLookup(c, key, queryProvider.get().byBranchKey(magicBranch.dest, key));
   }
 
-  private ChangeLookup lookupByCommit(RevCommit c) throws OrmException {
+  private ChangeLookup lookupByCommit(RevCommit c) {
     return new ChangeLookup(
         c, null, queryProvider.get().byBranchCommit(magicBranch.dest, c.getName()));
   }
@@ -2433,14 +2479,13 @@
           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())
-                .setNotify(magicBranch.getNotify())
-                .setAccountsToNotify(magicBranch.getAccountsToNotify())
                 .setRequestScopePropagator(requestScopePropagator)
                 .setSendMail(true)
                 .setPatchSetDescription(magicBranch.message));
@@ -2478,7 +2523,7 @@
   }
 
   private void submit(Collection<CreateRequest> create, Collection<ReplaceRequest> replace)
-      throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException,
+      throws RestApiException, UpdateException, IOException, ConfigInvalidException,
           PermissionBackendException {
     Map<ObjectId, Change> bySha = Maps.newHashMapWithExpectedSize(create.size() + replace.size());
     for (CreateRequest r : create) {
@@ -2499,7 +2544,7 @@
     logger.atFine().log(
         "Processing submit with tip change %s (%s)", tipChange.getId(), magicBranch.cmd.getNewId());
     try (MergeOp op = mergeOpProvider.get()) {
-      op.merge(db, tipChange, user, false, new SubmitInput(), false);
+      op.merge(tipChange, user, false, new SubmitInput(), false);
     }
   }
 
@@ -2511,7 +2556,7 @@
           req.validateNewPatchSet();
         }
       }
-    } catch (OrmException err) {
+    } catch (StorageException err) {
       logger.atSevere().withCause(err).log(
           "Cannot read database before replacement for project %s", project.getName());
       rejectRemainingRequests(replaceByChange.values(), "internal server error");
@@ -2535,10 +2580,10 @@
     }
   }
 
-  private void readChangesForReplace() throws OrmException {
+  private void readChangesForReplace() {
     Collection<ChangeNotes> allNotes =
         notesFactory.create(
-            db, replaceByChange.values().stream().map(r -> r.ontoChange).collect(toList()));
+            replaceByChange.values().stream().map(r -> r.ontoChange).collect(toList()));
     for (ChangeNotes notes : allNotes) {
       replaceByChange.get(notes.getChangeId()).notes = notes;
     }
@@ -2550,6 +2595,7 @@
     final ObjectId newCommitId;
     final ReceiveCommand inputCommand;
     final boolean checkMergedInto;
+    RevCommit revCommit;
     ChangeNotes notes;
     BiMap<RevCommit, PatchSet.Id> revisions;
     PatchSet.Id psId;
@@ -2567,6 +2613,11 @@
       this.inputCommand = requireNonNull(cmd);
       this.checkMergedInto = checkMergedInto;
 
+      try {
+        revCommit = receivePack.getRevWalk().parseCommit(newCommitId);
+      } catch (IOException e) {
+        revCommit = null;
+      }
       revisions = HashBiMap.create();
       for (Ref ref : refs(toChange)) {
         try {
@@ -2593,10 +2644,9 @@
      *
      * @return whether the new commit is valid
      * @throws IOException
-     * @throws OrmException
      * @throws PermissionBackendException
      */
-    boolean validateNewPatchSet() throws IOException, OrmException, PermissionBackendException {
+    boolean validateNewPatchSet() throws IOException, PermissionBackendException {
       if (!validateNewPatchSetNoteDb()) {
         return false;
       }
@@ -2617,8 +2667,7 @@
       return true;
     }
 
-    boolean validateNewPatchSetForAutoClose()
-        throws IOException, OrmException, PermissionBackendException {
+    boolean validateNewPatchSetForAutoClose() throws IOException, PermissionBackendException {
       if (!validateNewPatchSetNoteDb()) {
         return false;
       }
@@ -2628,8 +2677,7 @@
     }
 
     /** Validates the new PS against permissions and notedb status. */
-    private boolean validateNewPatchSetNoteDb()
-        throws IOException, OrmException, PermissionBackendException {
+    private boolean validateNewPatchSetNoteDb() throws IOException, PermissionBackendException {
       if (notes == null) {
         reject(inputCommand, "change " + ontoChange + " not found");
         return false;
@@ -2651,13 +2699,13 @@
       }
 
       try {
-        permissions.change(notes).database(db).check(ChangePermission.ADD_PATCH_SET);
+        permissions.change(notes).check(ChangePermission.ADD_PATCH_SET);
       } catch (AuthException no) {
         reject(inputCommand, "cannot add patch set to " + ontoChange + ".");
         return false;
       }
 
-      if (change.getStatus().isClosed()) {
+      if (change.isClosed()) {
         reject(inputCommand, "change " + ontoChange + " closed");
         return false;
       } else if (revisions.containsKey(newCommit)) {
@@ -2711,10 +2759,13 @@
 
     /** prints a warning if the new PS has the same tree as the previous commit. */
     private void sameTreeWarning() throws IOException {
-      RevCommit newCommit = receivePack.getRevWalk().parseCommit(newCommitId);
+      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);
@@ -2789,7 +2840,7 @@
     }
 
     /** Updates 'this' to add a new patchset. */
-    private void newPatchSet() throws IOException, OrmException {
+    private void newPatchSet() throws IOException {
       RevCommit newCommit = receivePack.getRevWalk().parseCommit(newCommitId);
       psId =
           ChangeUtil.nextPatchSetIdFromAllRefsMap(allRefs(), notes.getChange().currentPatchSetId());
@@ -2854,8 +2905,8 @@
           psId.getParentKey(),
           new BatchUpdateOp() {
             @Override
-            public boolean updateChange(ChangeContext ctx) throws OrmException {
-              PatchSet ps = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
+            public boolean updateChange(ChangeContext ctx) {
+              PatchSet ps = psUtil.get(ctx.getNotes(), psId);
               List<String> oldGroups = ps.getGroups();
               if (oldGroups == null) {
                 if (groups == null) {
@@ -2864,7 +2915,7 @@
               } else if (sameGroups(oldGroups, groups)) {
                 return false;
               }
-              psUtil.setGroups(ctx.getDb(), ctx.getUpdate(psId), ps, groups);
+              psUtil.setGroups(ctx.getUpdate(psId), ps, groups);
               return true;
             }
           });
@@ -3087,11 +3138,11 @@
       retryHelper.execute(
           updateFactory -> {
             try (BatchUpdate bu =
-                    updateFactory.create(db, projectState.getNameKey(), user, TimeUtil.nowTs());
+                    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).updateChangesInParallel();
+              bu.setRepository(repo, rw, ins);
               // TODO(dborowitz): Teach BatchUpdate to ignore missing changes.
 
               RevCommit newTip = rw.parseCommit(cmd.getNewId());
@@ -3119,6 +3170,7 @@
                   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(
@@ -3152,6 +3204,7 @@
                   continue;
                 }
                 req.addOps(bu, null);
+                bu.addOp(id, setPrivateOpFactory.create(false, null));
                 bu.addOp(
                     id,
                     mergedByPushOpFactory
@@ -3165,7 +3218,7 @@
                   "Auto-closing %s changes with existing patch sets and %s with new patch sets",
                   existingPatchSets, newPatchSets);
               bu.execute();
-            } catch (IOException | OrmException | PermissionBackendException e) {
+            } catch (IOException | StorageException | PermissionBackendException e) {
               logger.atSevere().withCause(e).log("Failed to auto-close changes");
               return null;
             }
@@ -3173,7 +3226,7 @@
             // 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(Key.AUTOCLOSED, id));
+            ids.stream().forEach(id -> resultChangeIds.add(ResultChangeIds.Key.AUTOCLOSED, id));
 
             return null;
           },
@@ -3189,26 +3242,25 @@
     }
   }
 
-  private Optional<ChangeNotes> getChangeNotes(Change.Id changeId) throws OrmException {
+  private Optional<ChangeNotes> getChangeNotes(Change.Id changeId) {
     try {
-      return Optional.of(notesFactory.createChecked(db, project.getNameKey(), changeId));
+      return Optional.of(notesFactory.createChecked(project.getNameKey(), changeId));
     } catch (NoSuchChangeException e) {
       return Optional.empty();
     }
   }
 
-  private <T> T executeIndexQuery(Action<T> action) throws OrmException {
+  private <T> T executeIndexQuery(Action<T> action) {
     try {
-      return retryHelper.execute(ActionType.INDEX_QUERY, action, OrmException.class::isInstance);
+      return retryHelper.execute(
+          ActionType.INDEX_QUERY, action, StorageException.class::isInstance);
     } catch (Exception e) {
       Throwables.throwIfUnchecked(e);
-      Throwables.throwIfInstanceOf(e, OrmException.class);
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
   }
 
-  private Map<Change.Key, ChangeNotes> openChangesByKeyByBranch(Branch.NameKey branch)
-      throws OrmException {
+  private Map<Change.Key, ChangeNotes> openChangesByKeyByBranch(Branch.NameKey branch) {
     Map<Change.Key, ChangeNotes> r = new HashMap<>();
     for (ChangeData cd : queryProvider.get().byBranchOpen(branch)) {
       try {
@@ -3250,4 +3302,15 @@
   private static boolean isConfig(ReceiveCommand cmd) {
     return cmd.getRefName().equals(RefNames.REFS_CONFIG);
   }
+
+  private static String commandToString(ReceiveCommand cmd) {
+    StringBuilder b = new StringBuilder();
+    b.append(cmd);
+    b.append("  (").append(cmd.getResult());
+    if (cmd.getMessage() != null) {
+      b.append(": ").append(cmd.getMessage());
+    }
+    b.append(")\n");
+    return b.toString();
+  }
 }
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java b/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java
index 8cbcc88..cfe06ea 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java
@@ -19,6 +19,7 @@
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
+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;
@@ -27,7 +28,6 @@
 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.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 import java.util.Collections;
 import java.util.Map;
@@ -66,6 +66,7 @@
         "ReceiveCommitsAdvertiseRefsHook cannot be used for UploadPack");
   }
 
+  @SuppressWarnings("deprecation")
   @Override
   public void advertiseRefs(BaseReceivePack rp) throws ServiceMayNotContinueException {
     Result r = advertiseRefs(HookUtil.ensureAllRefsAdvertised(rp));
@@ -118,7 +119,7 @@
         }
       }
       return r;
-    } catch (OrmException err) {
+    } catch (StorageException err) {
       logger.atSevere().withCause(err).log("Cannot list open changes of %s", projectName);
       return Collections.emptySet();
     }
diff --git a/java/com/google/gerrit/server/git/receive/ReplaceOp.java b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
index af4eab0..4d0d2c3 100644
--- a/java/com/google/gerrit/server/git/receive/ReplaceOp.java
+++ b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
@@ -25,7 +25,6 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.Streams;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
@@ -43,7 +42,6 @@
 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.ApprovalCopier;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.CommentsUtil;
@@ -53,6 +51,7 @@
 import com.google.gerrit.server.change.AddReviewersOp;
 import com.google.gerrit.server.change.ChangeKindCache;
 import com.google.gerrit.server.change.EmailReviewComments;
+import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.change.ReviewerAdder;
 import com.google.gerrit.server.change.ReviewerAdder.InternalAddReviewerInput;
 import com.google.gerrit.server.change.ReviewerAdder.ReviewerAddition;
@@ -75,7 +74,6 @@
 import com.google.gerrit.server.update.Context;
 import com.google.gerrit.server.update.RepoContext;
 import com.google.gerrit.server.util.RequestScopePropagator;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.util.Providers;
@@ -117,7 +115,6 @@
   private static final String CHANGE_IS_CLOSED = "change is closed";
 
   private final AccountResolver accountResolver;
-  private final ApprovalCopier approvalCopier;
   private final ApprovalsUtil approvalsUtil;
   private final ChangeData.Factory changeDataFactory;
   private final ChangeKindCache changeKindCache;
@@ -164,7 +161,6 @@
   @Inject
   ReplaceOp(
       AccountResolver accountResolver,
-      ApprovalCopier approvalCopier,
       ApprovalsUtil approvalsUtil,
       ChangeData.Factory changeDataFactory,
       ChangeKindCache changeKindCache,
@@ -193,7 +189,6 @@
       @Assisted @Nullable MagicBranchInput magicBranch,
       @Assisted @Nullable PushCertificate pushCertificate) {
     this.accountResolver = accountResolver;
-    this.approvalCopier = approvalCopier;
     this.approvalsUtil = approvalsUtil;
     this.changeDataFactory = changeDataFactory;
     this.changeKindCache = changeKindCache;
@@ -251,20 +246,19 @@
 
   @Override
   public boolean updateChange(ChangeContext ctx)
-      throws RestApiException, OrmException, IOException, PermissionBackendException,
-          ConfigInvalidException {
+      throws RestApiException, IOException, PermissionBackendException, ConfigInvalidException {
     notes = ctx.getNotes();
     Change change = notes.getChange();
-    if (change == null || change.getStatus().isClosed()) {
+    if (change == null || change.isClosed()) {
       rejectMessage = CHANGE_IS_CLOSED;
       return false;
     }
     if (groups.isEmpty()) {
-      PatchSet prevPs = psUtil.current(ctx.getDb(), notes);
-      groups = prevPs != null ? prevPs.getGroups() : ImmutableList.<String>of();
+      PatchSet prevPs = psUtil.current(notes);
+      groups = prevPs != null ? prevPs.getGroups() : ImmutableList.of();
     }
 
-    ChangeData cd = changeDataFactory.create(ctx.getDb(), ctx.getNotes());
+    ChangeData cd = changeDataFactory.create(ctx.getNotes());
     oldRecipients = getRecipientsFromReviewers(cd.reviewers());
 
     ChangeUpdate update = ctx.getUpdate(patchSetId);
@@ -310,7 +304,6 @@
 
     newPatchSet =
         psUtil.insert(
-            ctx.getDb(),
             ctx.getRevWalk(),
             update,
             patchSetId,
@@ -321,25 +314,11 @@
 
     update.setPsDescription(psDescription);
     MailRecipients fromFooters = getRecipientsFromFooters(accountResolver, commit.getFooterLines());
-    Iterable<PatchSetApproval> newApprovals =
-        approvalsUtil.addApprovalsForNewPatchSet(
-            ctx.getDb(),
-            update,
-            projectState.getLabelTypes(),
-            newPatchSet,
-            ctx.getUser(),
-            approvals);
-    approvalCopier.copyInReviewDb(
-        ctx.getDb(),
-        ctx.getNotes(),
-        newPatchSet,
-        ctx.getRevWalk(),
-        ctx.getRepoView().getConfig(),
-        newApprovals);
+    approvalsUtil.addApprovalsForNewPatchSet(
+        update, projectState.getLabelTypes(), newPatchSet, ctx.getUser(), approvals);
 
     reviewerAdditions =
         reviewerAdder.prepare(
-            ctx.getDb(),
             ctx.getNotes(),
             ctx.getUser(),
             getReviewerInputs(magicBranch, fromFooters, ctx.getChange(), info),
@@ -358,7 +337,7 @@
     }
 
     msg = createChangeMessage(ctx, reviewMessage);
-    cmUtil.addChangeMessage(ctx.getDb(), update, msg);
+    cmUtil.addChangeMessage(update, msg);
 
     if (mergedByPushOp == null) {
       resetChange(ctx);
@@ -411,7 +390,7 @@
   }
 
   private ChangeMessage createChangeMessage(ChangeContext ctx, String reviewMessage)
-      throws OrmException, IOException {
+      throws IOException {
     String approvalMessage =
         ApprovalsUtil.renderMessageWithApprovals(
             patchSetId.get(), approvals, scanLabels(ctx, approvals));
@@ -465,13 +444,12 @@
   }
 
   private Map<String, PatchSetApproval> scanLabels(ChangeContext ctx, Map<String, Short> approvals)
-      throws OrmException, IOException {
+      throws IOException {
     Map<String, PatchSetApproval> current = new HashMap<>();
     // We optimize here and only retrieve current when approvals provided
     if (!approvals.isEmpty()) {
       for (PatchSetApproval a :
           approvalsUtil.byPatchSetUser(
-              ctx.getDb(),
               ctx.getNotes(),
               priorPatchSetId,
               ctx.getAccountId(),
@@ -510,10 +488,9 @@
     }
   }
 
-  private List<Comment> publishComments(ChangeContext ctx, boolean workInProgress)
-      throws OrmException {
+  private List<Comment> publishComments(ChangeContext ctx, boolean workInProgress) {
     List<Comment> comments =
-        commentsUtil.draftByChangeAuthor(ctx.getDb(), ctx.getNotes(), ctx.getUser().getAccountId());
+        commentsUtil.draftByChangeAuthor(ctx.getNotes(), ctx.getUser().getAccountId());
     publishCommentUtil.publish(
         ctx, patchSetId, comments, ChangeMessagesUtil.uploadedPatchSetTag(workInProgress));
     return comments;
@@ -533,13 +510,11 @@
       }
     }
 
-    NotifyHandling notify = magicBranch != null ? magicBranch.getNotify(notes) : NotifyHandling.ALL;
-
+    NotifyResolver.Result notify = ctx.getNotify(notes.getChangeId());
     if (shouldPublishComments()) {
       emailCommentsFactory
           .create(
               notify,
-              magicBranch != null ? magicBranch.getAccountsToNotify() : ImmutableListMultimap.of(),
               notes,
               newPatchSet,
               ctx.getUser().asIdentifiedUser(),
@@ -576,10 +551,7 @@
         cm.setFrom(ctx.getAccount().getAccount().getId());
         cm.setPatchSet(newPatchSet, info);
         cm.setChangeMessage(msg.getMessage(), ctx.getWhen());
-        if (magicBranch != null) {
-          cm.setNotify(magicBranch.getNotify(notes));
-          cm.setAccountsToNotify(magicBranch.getAccountsToNotify());
-        }
+        cm.setNotify(ctx.getNotify(notes.getChangeId()));
         cm.addReviewers(
             Streams.concat(
                     oldRecipients.getReviewers().stream(),
diff --git a/java/com/google/gerrit/server/git/receive/ResultChangeIds.java b/java/com/google/gerrit/server/git/receive/ResultChangeIds.java
index bbf8d95..e326141 100644
--- a/java/com/google/gerrit/server/git/receive/ResultChangeIds.java
+++ b/java/com/google/gerrit/server/git/receive/ResultChangeIds.java
@@ -33,6 +33,7 @@
     AUTOCLOSED,
   }
 
+  private boolean isMagicPush;
   private final Map<Key, List<Change.Id>> ids;
 
   ResultChangeIds() {
@@ -43,16 +44,24 @@
   }
 
   /** Record a change ID update as having completed. Thread-safe. */
-  public void add(Key key, Change.Id id) {
-    synchronized (this) {
-      ids.get(key).add(id);
-    }
+  public synchronized void add(Key key, Change.Id id) {
+    ids.get(key).add(id);
   }
 
-  /** Returns change IDs of the given type for which the BatchUpdate succeeded. Thread-safe. */
-  public List<Change.Id> get(Key key) {
-    synchronized (this) {
-      return ImmutableList.copyOf(ids.get(key));
-    }
+  /** Indicate that the ReceiveCommits call involved a magic branch. */
+  public synchronized void setMagicPush(boolean magic) {
+    isMagicPush = magic;
+  }
+
+  public synchronized boolean isMagicPush() {
+    return isMagicPush;
+  }
+
+  /**
+   * Returns change IDs of the given type for which the BatchUpdate succeeded, or empty list if
+   * there are none. Thread-safe.
+   */
+  public synchronized List<Change.Id> get(Key key) {
+    return ImmutableList.copyOf(ids.get(key));
   }
 }
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidators.java b/java/com/google/gerrit/server/git/validators/CommitValidators.java
index ceb6249..3c36273 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -21,9 +21,7 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Splitter;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.Nullable;
@@ -33,7 +31,6 @@
 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.Branch.NameKey;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.GerritPersonIdent;
@@ -99,6 +96,7 @@
     private final AccountValidator accountValidator;
     private final String installCommitMsgHookCommand;
     private final ProjectCache projectCache;
+    private final ProjectConfig.Factory projectConfigFactory;
 
     @Inject
     Factory(
@@ -111,7 +109,8 @@
         AllProjectsName allProjects,
         ExternalIdsConsistencyChecker externalIdsConsistencyChecker,
         AccountValidator accountValidator,
-        ProjectCache projectCache) {
+        ProjectCache projectCache,
+        ProjectConfig.Factory projectConfigFactory) {
       this.gerritIdent = gerritIdent;
       this.urlFormatter = urlFormatter;
       this.pluginValidators = pluginValidators;
@@ -123,6 +122,7 @@
       this.installCommitMsgHookCommand =
           cfg != null ? cfg.getString("gerrit", null, "installCommitMsgHookCommand") : null;
       this.projectCache = projectCache;
+      this.projectConfigFactory = projectConfigFactory;
     }
 
     public CommitValidators forReceiveCommits(
@@ -152,7 +152,7 @@
                   installCommitMsgHookCommand,
                   sshInfo,
                   change),
-              new ConfigValidator(branch, user, rw, allUsers, allProjects),
+              new ConfigValidator(projectConfigFactory, branch, user, rw, allUsers, allProjects),
               new BannedCommitsValidator(rejectCommits),
               new PluginCommitValidationListener(pluginValidators, skipValidation),
               new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker),
@@ -162,7 +162,7 @@
 
     public CommitValidators forGerritCommits(
         PermissionBackend.ForProject forProject,
-        NameKey branch,
+        Branch.NameKey branch,
         IdentifiedUser user,
         SshInfo sshInfo,
         RevWalk rw,
@@ -184,7 +184,7 @@
                   installCommitMsgHookCommand,
                   sshInfo,
                   change),
-              new ConfigValidator(branch, user, rw, allUsers, allProjects),
+              new ConfigValidator(projectConfigFactory, branch, user, rw, allUsers, allProjects),
               new PluginCommitValidationListener(pluginValidators),
               new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker),
               new AccountCommitValidator(repoManager, allUsers, accountValidator),
@@ -245,6 +245,7 @@
     private static final String MISSING_CHANGE_ID_MSG = "missing Change-Id in message footer";
     private static final String MISSING_SUBJECT_MSG =
         "missing subject; Change-Id must be in message footer";
+    private static final String CHANGE_ID_ABOVE_FOOTER_MSG = "Change-Id must be in message footer";
     private static final String MULTIPLE_CHANGE_ID_MSG =
         "multiple Change-Id lines in message footer";
     private static final String INVALID_CHANGE_ID_MSG =
@@ -294,8 +295,20 @@
             && CHANGE_ID.matcher(shortMsg.substring(CHANGE_ID_PREFIX.length()).trim()).matches()) {
           throw new CommitValidationException(MISSING_SUBJECT_MSG);
         }
+        if (commit.getFullMessage().contains("\n" + CHANGE_ID_PREFIX)) {
+          messages.add(
+              new CommitValidationMessage(
+                  CHANGE_ID_ABOVE_FOOTER_MSG
+                      + "\n"
+                      + "\n"
+                      + "Hint: run\n"
+                      + "  git commit --amend\n"
+                      + "and move 'Change-Id: Ixxx..' to the bottom on a separate line\n",
+                  Type.ERROR));
+          throw new CommitValidationException(CHANGE_ID_ABOVE_FOOTER_MSG, messages);
+        }
         if (projectState.is(BooleanProjectConfig.REQUIRE_CHANGE_ID)) {
-          messages.add(getMissingChangeIdErrorMsg(MISSING_CHANGE_ID_MSG, commit));
+          messages.add(getMissingChangeIdErrorMsg(MISSING_CHANGE_ID_MSG));
           throw new CommitValidationException(MISSING_CHANGE_ID_MSG, messages);
         }
       } else if (idList.size() > 1) {
@@ -305,7 +318,7 @@
         // Reject Change-Ids with wrong format and invalid placeholder ID from
         // Egit (I0000000000000000000000000000000000000000).
         if (!CHANGE_ID.matcher(v).matches() || v.matches("^I00*$")) {
-          messages.add(getMissingChangeIdErrorMsg(INVALID_CHANGE_ID_MSG, receiveEvent.commit));
+          messages.add(getMissingChangeIdErrorMsg(INVALID_CHANGE_ID_MSG));
           throw new CommitValidationException(INVALID_CHANGE_ID_MSG, messages);
         }
         if (change != null && !v.equals(change.getKey().get())) {
@@ -321,31 +334,17 @@
           || NEW_PATCHSET_PATTERN.matcher(event.command.getRefName()).matches();
     }
 
-    private CommitValidationMessage getMissingChangeIdErrorMsg(String errMsg, RevCommit c) {
-      StringBuilder sb = new StringBuilder();
-      sb.append(errMsg).append("\n");
-
-      boolean hinted = false;
-      if (c.getFullMessage().contains(CHANGE_ID_PREFIX)) {
-        String lastLine = Iterables.getLast(Splitter.on('\n').split(c.getFullMessage()), "");
-        if (!lastLine.contains(CHANGE_ID_PREFIX)) {
-          hinted = true;
-          sb.append("\n")
-              .append("Hint: run\n")
-              .append("  git commit --amend\n")
-              .append("and move 'Change-Id: Ixxx..' to the bottom on a separate line\n");
-        }
-      }
-
-      // Print only one hint to avoid overwhelming the user.
-      if (!hinted) {
-        sb.append("\nHint: to automatically insert a Change-Id, install the hook:\n")
-            .append(getCommitMessageHookInstallationHint())
-            .append("\n")
-            .append("and then amend the commit:\n")
-            .append("  git commit --amend --no-edit\n");
-      }
-      return new CommitValidationMessage(sb.toString(), Type.ERROR);
+    private CommitValidationMessage getMissingChangeIdErrorMsg(String errMsg) {
+      return new CommitValidationMessage(
+          errMsg
+              + "\n"
+              + "\nHint: to automatically insert a Change-Id, install the hook:\n"
+              + getCommitMessageHookInstallationHint()
+              + "\n"
+              + "and then amend the commit:\n"
+              + "  git commit --amend --no-edit\n"
+              + "Finally, push your changes again\n",
+          Type.ERROR);
     }
 
     private String getCommitMessageHookInstallationHint() {
@@ -390,6 +389,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 IdentifiedUser user;
     private final RevWalk rw;
@@ -397,11 +397,13 @@
     private final AllProjectsName allProjects;
 
     public ConfigValidator(
+        ProjectConfig.Factory projectConfigFactory,
         Branch.NameKey branch,
         IdentifiedUser user,
         RevWalk rw,
         AllUsersName allUsers,
         AllProjectsName allProjects) {
+      this.projectConfigFactory = projectConfigFactory;
       this.branch = branch;
       this.user = user;
       this.rw = rw;
@@ -416,7 +418,7 @@
         List<CommitValidationMessage> messages = new ArrayList<>();
 
         try {
-          ProjectConfig cfg = new ProjectConfig(receiveEvent.project.getNameKey());
+          ProjectConfig cfg = projectConfigFactory.create(receiveEvent.project.getNameKey());
           cfg.load(rw, receiveEvent.command.getNewId());
           if (!cfg.getValidationErrors().isEmpty()) {
             addError("Invalid project configuration:", messages);
diff --git a/java/com/google/gerrit/server/git/validators/MergeValidators.java b/java/com/google/gerrit/server/git/validators/MergeValidators.java
index 425ab3f..75be8f3 100644
--- a/java/com/google/gerrit/server/git/validators/MergeValidators.java
+++ b/java/com/google/gerrit/server/git/validators/MergeValidators.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
+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;
@@ -26,7 +27,6 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountProperties;
 import com.google.gerrit.server.config.AllProjectsName;
@@ -44,9 +44,7 @@
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import java.io.IOException;
 import java.util.List;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -127,6 +125,7 @@
     private final ProjectCache projectCache;
     private final PermissionBackend permissionBackend;
     private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
+    private final ProjectConfig.Factory projectConfigFactory;
     private final boolean allowProjectOwnersToChangeParent;
 
     public interface Factory {
@@ -140,12 +139,14 @@
         ProjectCache projectCache,
         PermissionBackend permissionBackend,
         DynamicMap<ProjectConfigEntry> pluginConfigEntries,
+        ProjectConfig.Factory projectConfigFactory,
         @GerritServerConfig Config config) {
       this.allProjectsName = allProjectsName;
       this.allUsersName = allUsersName;
       this.projectCache = projectCache;
       this.permissionBackend = permissionBackend;
       this.pluginConfigEntries = pluginConfigEntries;
+      this.projectConfigFactory = projectConfigFactory;
       this.allowProjectOwnersToChangeParent =
           config.getBoolean("receive", "allowProjectOwnersToChangeParent", false);
     }
@@ -162,7 +163,7 @@
       if (RefNames.REFS_CONFIG.equals(destBranch.get())) {
         final Project.NameKey newParent;
         try {
-          ProjectConfig cfg = new ProjectConfig(destProject.getNameKey());
+          ProjectConfig cfg = projectConfigFactory.create(destProject.getNameKey());
           cfg.load(destProject.getNameKey(), repo, commit);
           newParent = cfg.getProject().getParent(allProjectsName);
           final Project.NameKey oldParent = destProject.getProject().getParent(allProjectsName);
@@ -265,18 +266,15 @@
       AccountMergeValidator create();
     }
 
-    private final Provider<ReviewDb> dbProvider;
     private final AllUsersName allUsersName;
     private final ChangeData.Factory changeDataFactory;
     private final AccountValidator accountValidator;
 
     @Inject
     public AccountMergeValidator(
-        Provider<ReviewDb> dbProvider,
         AllUsersName allUsersName,
         ChangeData.Factory changeDataFactory,
         AccountValidator accountValidator) {
-      this.dbProvider = dbProvider;
       this.allUsersName = allUsersName;
       this.changeDataFactory = changeDataFactory;
       this.accountValidator = accountValidator;
@@ -298,12 +296,12 @@
 
       ChangeData cd =
           changeDataFactory.create(
-              dbProvider.get(), destProject.getProject().getNameKey(), patchSetId.getParentKey());
+              destProject.getProject().getNameKey(), patchSetId.getParentKey());
       try {
         if (!cd.currentFilePaths().contains(AccountProperties.ACCOUNT_CONFIG)) {
           return;
         }
-      } catch (IOException | OrmException e) {
+      } catch (StorageException e) {
         logger.atSevere().withCause(e).log("Cannot validate account update");
         throw new MergeValidationException("account validation unavailable");
       }
diff --git a/java/com/google/gerrit/server/git/validators/RefOperationValidators.java b/java/com/google/gerrit/server/git/validators/RefOperationValidators.java
index 4eb317c..3c0208c 100644
--- a/java/com/google/gerrit/server/git/validators/RefOperationValidators.java
+++ b/java/com/google/gerrit/server/git/validators/RefOperationValidators.java
@@ -77,7 +77,8 @@
     boolean withException = false;
     try {
       messages.addAll(
-          new DisallowCreationAndDeletionOfUserBranches(perm, allUsersName).onRefOperation(event));
+          new DisallowCreationAndDeletionOfGerritMaintainedBranches(perm, allUsersName)
+              .onRefOperation(event));
       refOperationValidationListeners.runEach(
           l -> l.onRefOperation(event), ValidationException.class);
     } catch (ValidationException e) {
@@ -110,12 +111,12 @@
     }
   }
 
-  private static class DisallowCreationAndDeletionOfUserBranches
+  private static class DisallowCreationAndDeletionOfGerritMaintainedBranches
       implements RefOperationValidationListener {
     private final PermissionBackend.WithUser perm;
     private final AllUsersName allUsersName;
 
-    DisallowCreationAndDeletionOfUserBranches(
+    DisallowCreationAndDeletionOfGerritMaintainedBranches(
         PermissionBackend.WithUser perm, AllUsersName allUsersName) {
       this.perm = perm;
       this.allUsersName = allUsersName;
diff --git a/java/com/google/gerrit/server/group/GroupAuditService.java b/java/com/google/gerrit/server/group/GroupAuditService.java
index 4b851ea..4c02ada 100644
--- a/java/com/google/gerrit/server/group/GroupAuditService.java
+++ b/java/com/google/gerrit/server/group/GroupAuditService.java
@@ -16,7 +16,6 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Account.Id;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.AuditEvent;
 import java.sql.Timestamp;
@@ -27,7 +26,7 @@
   void dispatchAddMembers(
       Account.Id actor,
       AccountGroup.UUID updatedGroup,
-      ImmutableSet<Id> addedMembers,
+      ImmutableSet<Account.Id> addedMembers,
       Timestamp addedOn);
 
   void dispatchDeleteMembers(
diff --git a/java/com/google/gerrit/server/group/db/AuditLogReader.java b/java/com/google/gerrit/server/group/db/AuditLogReader.java
index 61fdb60..106ee6b 100644
--- a/java/com/google/gerrit/server/group/db/AuditLogReader.java
+++ b/java/com/google/gerrit/server/group/db/AuditLogReader.java
@@ -59,8 +59,8 @@
   }
 
   // Having separate methods for reading the two types of audit records mirrors the split in
-  // ReviewDb. Once ReviewDb is gone, the audit record interface becomes more flexible and we can
-  // revisit this, e.g. to do only a single walk, or even change the record types.
+  // ReviewDb. Now that ReviewDb is gone, the audit record interface is more flexible and this may
+  // be changed, e.g. to do only a single walk, or even change the record types.
 
   public ImmutableList<AccountGroupMemberAudit> getMembersAudit(
       Repository allUsersRepo, AccountGroup.UUID uuid) throws IOException, ConfigInvalidException {
@@ -134,8 +134,8 @@
   private Optional<ParsedCommit> parse(AccountGroup.UUID uuid, RevCommit c) {
     Optional<Account.Id> authorId = NoteDbUtil.parseIdent(c.getAuthorIdent(), serverId);
     if (!authorId.isPresent()) {
-      // Only report audit events from identified users, since this is a non-nullable field in
-      // ReviewDb. May be revisited after groups are fully migrated to NoteDb.
+      // Only report audit events from identified users, since this was a non-nullable field in
+      // ReviewDb. May be revisited.
       return Optional.empty();
     }
 
diff --git a/java/com/google/gerrit/server/group/db/GroupConfig.java b/java/com/google/gerrit/server/group/db/GroupConfig.java
index 66230ea..903b9c0 100644
--- a/java/com/google/gerrit/server/group/db/GroupConfig.java
+++ b/java/com/google/gerrit/server/group/db/GroupConfig.java
@@ -24,6 +24,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Streams;
+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;
@@ -32,7 +33,6 @@
 import com.google.gerrit.server.git.meta.VersionedMetaData;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.Arrays;
@@ -57,13 +57,13 @@
  * AccountGroup.UUID)} or {@link #loadForGroupSnapshot(Project.NameKey, Repository,
  * AccountGroup.UUID, ObjectId)}.
  *
- * <p><strong>Note: </strong>Any modification (group creation or update) only becomes permanent (and
+ * <p><strong>Note:</strong> Any modification (group creation or update) only becomes permanent (and
  * hence written to NoteDb) if {@link #commit(MetaDataUpdate)} is called.
  *
- * <p><strong>Warning: </strong>This class is a low-level API for groups in NoteDb. Most code which
+ * <p><strong>Warning:</strong> This class is a low-level API for groups in NoteDb. Most code which
  * deals with internal Gerrit groups should use {@link Groups} or {@link GroupsUpdate} instead.
  *
- * <p><em>Internal details</em>
+ * <h2>Internal details</h2>
  *
  * <p>Each group is represented by a commit on a branch as defined by {@link
  * RefNames#refsGroups(AccountGroup.UUID)}. Previous versions of the group exist as older commits on
@@ -99,7 +99,7 @@
    * {@link #setGroupUpdate(InternalGroupUpdate, AuditLogFormatter)} on the returned {@code
    * GroupConfig}.
    *
-   * <p><strong>Note: </strong>The returned {@code GroupConfig} has to be committed via {@link
+   * <p><strong>Note:</strong> The returned {@code GroupConfig} has to be committed via {@link
    * #commit(MetaDataUpdate)} in order to create the group for real.
    *
    * @param projectName the name of the project which holds the NoteDb commits for groups
@@ -110,11 +110,11 @@
    * @throws IOException if the repository can't be accessed for some reason
    * @throws ConfigInvalidException if a group with the same UUID already exists but can't be read
    *     due to an invalid format
-   * @throws OrmDuplicateKeyException if a group with the same UUID already exists
+   * @throws DuplicateKeyException if a group with the same UUID already exists
    */
   public static GroupConfig createForNewGroup(
       Project.NameKey projectName, Repository repository, InternalGroupCreation groupCreation)
-      throws IOException, ConfigInvalidException, OrmDuplicateKeyException {
+      throws IOException, ConfigInvalidException, DuplicateKeyException {
     GroupConfig groupConfig = new GroupConfig(groupCreation.getGroupUUID());
     groupConfig.load(projectName, repository);
     groupConfig.setGroupCreation(groupCreation);
@@ -216,7 +216,7 @@
    * <p>If the group is newly created, the {@code InternalGroupUpdate} can be used to specify
    * optional properties.
    *
-   * <p><strong>Note: </strong>This method doesn't perform the update. It only contains the
+   * <p><strong>Note:</strong> This method doesn't perform the update. It only contains the
    * instructions for the update. To apply the update for real and write the result back to NoteDb,
    * call {@link #commit(MetaDataUpdate)} on this {@code GroupConfig}.
    *
@@ -233,7 +233,7 @@
   /**
    * Allows the new name of a group to be empty during creation or update.
    *
-   * <p><strong>Note: </strong>This method exists only to support the migration of legacy groups
+   * <p><strong>Note:</strong> This method exists only to support the migration of legacy groups
    * which don't always necessarily have a name. Nowadays, we enforce that groups always have names.
    * When we remove the migration code, we can probably remove this method as well.
    */
@@ -241,11 +241,10 @@
     this.allowSaveEmptyName = true;
   }
 
-  private void setGroupCreation(InternalGroupCreation groupCreation)
-      throws OrmDuplicateKeyException {
+  private void setGroupCreation(InternalGroupCreation groupCreation) throws DuplicateKeyException {
     checkLoaded();
     if (loadedGroup.isPresent()) {
-      throw new OrmDuplicateKeyException(String.format("Group %s already exists", groupUuid.get()));
+      throw new DuplicateKeyException(String.format("Group %s already exists", groupUuid.get()));
     }
 
     this.groupCreation = Optional.of(groupCreation);
diff --git a/java/com/google/gerrit/server/group/db/GroupConfigEntry.java b/java/com/google/gerrit/server/group/db/GroupConfigEntry.java
index eff3458..f7a104d 100644
--- a/java/com/google/gerrit/server/group/db/GroupConfigEntry.java
+++ b/java/com/google/gerrit/server/group/db/GroupConfigEntry.java
@@ -25,7 +25,7 @@
  *
  * <p>Each property knows how to read and write its value from/to a JGit {@link Config} file.
  *
- * <p><strong>Warning: </strong>This class is a low-level API for properties of groups in NoteDb. It
+ * <p><strong>Warning:</strong> This class is a low-level API for properties of groups in NoteDb. It
  * may only be used by {@link GroupConfig}. Other classes should use {@link InternalGroupUpdate} to
  * modify the properties of a group.
  */
diff --git a/java/com/google/gerrit/server/group/db/GroupNameNotes.java b/java/com/google/gerrit/server/group/db/GroupNameNotes.java
index e95e91f..eda7153 100644
--- a/java/com/google/gerrit/server/group/db/GroupNameNotes.java
+++ b/java/com/google/gerrit/server/group/db/GroupNameNotes.java
@@ -29,11 +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.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.server.git.meta.VersionedMetaData;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
 import java.io.IOException;
 import java.util.Collection;
 import java.util.Map;
@@ -114,7 +114,7 @@
    * @throws IOException if the repository can't be accessed for some reason
    * @throws ConfigInvalidException if the note for the specified group doesn't exist or is in an
    *     invalid state
-   * @throws OrmDuplicateKeyException if a group with the new name already exists
+   * @throws DuplicateKeyException if a group with the new name already exists
    */
   public static GroupNameNotes forRename(
       Project.NameKey projectName,
@@ -122,7 +122,7 @@
       AccountGroup.UUID groupUuid,
       AccountGroup.NameKey oldName,
       AccountGroup.NameKey newName)
-      throws IOException, ConfigInvalidException, OrmDuplicateKeyException {
+      throws IOException, ConfigInvalidException, DuplicateKeyException {
     requireNonNull(oldName);
     requireNonNull(newName);
 
@@ -146,14 +146,14 @@
    * @return an instance of {@code GroupNameNotes} configured for a specific group creation
    * @throws IOException if the repository can't be accessed for some reason
    * @throws ConfigInvalidException in no case so far
-   * @throws OrmDuplicateKeyException if a group with the new name already exists
+   * @throws DuplicateKeyException if a group with the new name already exists
    */
   public static GroupNameNotes forNewGroup(
       Project.NameKey projectName,
       Repository repository,
       AccountGroup.UUID groupUuid,
       AccountGroup.NameKey groupName)
-      throws IOException, ConfigInvalidException, OrmDuplicateKeyException {
+      throws IOException, ConfigInvalidException, DuplicateKeyException {
     requireNonNull(groupName);
 
     GroupNameNotes groupNameNotes = new GroupNameNotes(groupUuid, null, groupName);
@@ -363,9 +363,9 @@
     }
   }
 
-  private void ensureNewNameIsNotUsed() throws OrmDuplicateKeyException {
+  private void ensureNewNameIsNotUsed() throws DuplicateKeyException {
     if (newGroupName.isPresent() && nameConflicting) {
-      throw new OrmDuplicateKeyException(
+      throw new DuplicateKeyException(
           String.format("Name '%s' is already used", newGroupName.get().get()));
     }
   }
diff --git a/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java b/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java
index 58b55ee..c3ca60b 100644
--- a/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java
+++ b/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java
@@ -87,8 +87,12 @@
 
     BiMap<AccountGroup.UUID, String> uuidNameBiMap = HashBiMap.create();
 
-    // Get all refs in an attempt to avoid seeing half committed group updates.
-    Map<String, Ref> refs = allUsersRepo.getAllRefs();
+    // Get group refs and group names ref using the most atomic API available, in an attempt to
+    // avoid seeing half-committed group updates.
+    List<Ref> refs =
+        allUsersRepo
+            .getRefDatabase()
+            .getRefsByPrefix(RefNames.REFS_GROUPS, RefNames.REFS_GROUPNAMES);
     readGroups(allUsersRepo, refs, result);
     readGroupNames(allUsersRepo, refs, result, uuidNameBiMap);
     // The sequential IDs are not keys in NoteDb, so no need to check them.
@@ -103,22 +107,21 @@
     return result;
   }
 
-  private void readGroups(Repository allUsersRepo, Map<String, Ref> refs, Result result)
+  private void readGroups(Repository allUsersRepo, List<Ref> refs, Result result)
       throws IOException {
-    for (Map.Entry<String, Ref> entry : refs.entrySet()) {
-      if (!entry.getKey().startsWith(RefNames.REFS_GROUPS)) {
+    for (Ref ref : refs) {
+      if (!ref.getName().startsWith(RefNames.REFS_GROUPS)) {
         continue;
       }
 
-      AccountGroup.UUID uuid = AccountGroup.UUID.fromRef(entry.getKey());
+      AccountGroup.UUID uuid = AccountGroup.UUID.fromRef(ref.getName());
       if (uuid == null) {
-        result.problems.add(error("null UUID from %s", entry.getKey()));
+        result.problems.add(error("null UUID from %s", ref.getName()));
         continue;
       }
       try {
         GroupConfig cfg =
-            GroupConfig.loadForGroupSnapshot(
-                allUsersName, allUsersRepo, uuid, entry.getValue().getObjectId());
+            GroupConfig.loadForGroupSnapshot(allUsersName, allUsersRepo, uuid, ref.getObjectId());
         result.uuidToGroupMap.put(uuid, cfg.getLoadedGroup().get());
       } catch (ConfigInvalidException e) {
         result.problems.add(error("group %s does not parse: %s", uuid, e.getMessage()));
@@ -128,16 +131,18 @@
 
   private void readGroupNames(
       Repository repo,
-      Map<String, Ref> refs,
+      List<Ref> refs,
       Result result,
       BiMap<AccountGroup.UUID, String> uuidNameBiMap)
       throws IOException {
-    Ref ref = refs.get(RefNames.REFS_GROUPNAMES);
-    if (ref == null) {
+    Optional<Ref> maybeRef =
+        refs.stream().filter(r -> r.getName().equals(RefNames.REFS_GROUPNAMES)).findFirst();
+    if (!maybeRef.isPresent()) {
       String msg = String.format("ref %s does not exist", RefNames.REFS_GROUPNAMES);
       result.problems.add(error(msg));
       return;
     }
+    Ref ref = maybeRef.get();
 
     try (RevWalk rw = new RevWalk(repo)) {
       RevCommit c = rw.parseCommit(ref.getObjectId());
diff --git a/java/com/google/gerrit/server/group/db/GroupsUpdate.java b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
index ad3813a6..b450ab8 100644
--- a/java/com/google/gerrit/server/group/db/GroupsUpdate.java
+++ b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
@@ -21,7 +21,10 @@
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+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;
@@ -35,20 +38,17 @@
 import com.google.gerrit.server.config.GerritServerId;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.LockFailureException;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 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.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
-import com.google.gerrit.server.update.RefUpdateUtil;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
-import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.Objects;
@@ -78,13 +78,24 @@
      * modifications executed by it. For NoteDb, this identity is used as author and committer for
      * all related commits.
      *
-     * <p><strong>Note</strong>: Please use this method with care and rather consider to use the
-     * correct annotation on the provider of a {@code GroupsUpdate} instead.
+     * <p><strong>Note</strong>: Please use this method with care and consider using the {@link
+     * com.google.gerrit.server.UserInitiated} annotation on the provider of a {@code GroupsUpdate}
+     * instead.
      *
-     * @param currentUser the user to which modifications should be attributed, or {@code null} if
-     *     the Gerrit server identity should be used
+     * @param currentUser the user to which modifications should be attributed
      */
-    GroupsUpdate create(@Nullable IdentifiedUser currentUser);
+    GroupsUpdate create(IdentifiedUser currentUser);
+
+    /**
+     * Creates a {@code GroupsUpdate} which uses the server identity to mark database modifications
+     * executed by it. For NoteDb, this identity is used as author and committer for all related
+     * commits.
+     *
+     * <p><strong>Note</strong>: Please use this method with care and consider using the {@link
+     * com.google.gerrit.server.ServerInitiated} annotation on the provider of a {@code
+     * GroupsUpdate} instead.
+     */
+    GroupsUpdate createWithServerIdent();
   }
 
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -96,14 +107,48 @@
   private final Provider<GroupIndexer> indexer;
   private final GroupAuditService groupAuditService;
   private final RenameGroupOp.Factory renameGroupOpFactory;
-  @Nullable private final IdentifiedUser currentUser;
+  private final Optional<IdentifiedUser> currentUser;
   private final AuditLogFormatter auditLogFormatter;
   private final PersonIdent authorIdent;
   private final MetaDataUpdateFactory metaDataUpdateFactory;
   private final GitReferenceUpdated gitRefUpdated;
   private final RetryHelper retryHelper;
 
-  @Inject
+  @AssistedInject
+  GroupsUpdate(
+      GitRepositoryManager repoManager,
+      AllUsersName allUsersName,
+      GroupBackend groupBackend,
+      GroupCache groupCache,
+      GroupIncludeCache groupIncludeCache,
+      Provider<GroupIndexer> indexer,
+      GroupAuditService auditService,
+      AccountCache accountCache,
+      RenameGroupOp.Factory renameGroupOpFactory,
+      @GerritServerId String serverId,
+      @GerritPersonIdent PersonIdent serverIdent,
+      MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory,
+      GitReferenceUpdated gitRefUpdated,
+      RetryHelper retryHelper) {
+    this(
+        repoManager,
+        allUsersName,
+        groupBackend,
+        groupCache,
+        groupIncludeCache,
+        indexer,
+        auditService,
+        accountCache,
+        renameGroupOpFactory,
+        serverId,
+        serverIdent,
+        metaDataUpdateInternalFactory,
+        gitRefUpdated,
+        retryHelper,
+        Optional.empty());
+  }
+
+  @AssistedInject
   GroupsUpdate(
       GitRepositoryManager repoManager,
       AllUsersName allUsersName,
@@ -119,7 +164,41 @@
       MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory,
       GitReferenceUpdated gitRefUpdated,
       RetryHelper retryHelper,
-      @Assisted @Nullable IdentifiedUser currentUser) {
+      @Assisted IdentifiedUser currentUser) {
+    this(
+        repoManager,
+        allUsersName,
+        groupBackend,
+        groupCache,
+        groupIncludeCache,
+        indexer,
+        auditService,
+        accountCache,
+        renameGroupOpFactory,
+        serverId,
+        serverIdent,
+        metaDataUpdateInternalFactory,
+        gitRefUpdated,
+        retryHelper,
+        Optional.of(currentUser));
+  }
+
+  private GroupsUpdate(
+      GitRepositoryManager repoManager,
+      AllUsersName allUsersName,
+      GroupBackend groupBackend,
+      GroupCache groupCache,
+      GroupIncludeCache groupIncludeCache,
+      Provider<GroupIndexer> indexer,
+      GroupAuditService auditService,
+      AccountCache accountCache,
+      RenameGroupOp.Factory renameGroupOpFactory,
+      @GerritServerId String serverId,
+      @GerritPersonIdent PersonIdent serverIdent,
+      MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory,
+      GitReferenceUpdated gitRefUpdated,
+      RetryHelper retryHelper,
+      Optional<IdentifiedUser> currentUser) {
     this.repoManager = repoManager;
     this.allUsersName = allUsersName;
     this.groupCache = groupCache;
@@ -140,7 +219,7 @@
 
   private static MetaDataUpdateFactory getMetaDataUpdateFactory(
       MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory,
-      @Nullable IdentifiedUser currentUser,
+      Optional<IdentifiedUser> currentUser,
       PersonIdent serverIdent,
       AuditLogFormatter auditLogFormatter) {
     return (projectName, repository, batchRefUpdate) -> {
@@ -148,10 +227,10 @@
           metaDataUpdateInternalFactory.create(projectName, repository, batchRefUpdate);
       metaDataUpdate.getCommitBuilder().setCommitter(serverIdent);
       PersonIdent authorIdent;
-      if (currentUser != null) {
-        metaDataUpdate.setAuthor(currentUser);
+      if (currentUser.isPresent()) {
+        metaDataUpdate.setAuthor(currentUser.get());
         authorIdent =
-            auditLogFormatter.getParsableAuthorIdent(currentUser.getAccount(), serverIdent);
+            auditLogFormatter.getParsableAuthorIdent(currentUser.get().getAccount(), serverIdent);
       } else {
         authorIdent = serverIdent;
       }
@@ -161,8 +240,8 @@
   }
 
   private static PersonIdent getAuthorIdent(
-      PersonIdent serverIdent, @Nullable IdentifiedUser currentUser) {
-    return currentUser != null ? createPersonIdent(serverIdent, currentUser) : serverIdent;
+      PersonIdent serverIdent, Optional<IdentifiedUser> currentUser) {
+    return currentUser.map(user -> createPersonIdent(serverIdent, user)).orElse(serverIdent);
   }
 
   private static PersonIdent createPersonIdent(PersonIdent ident, IdentifiedUser user) {
@@ -177,13 +256,13 @@
    * @param groupUpdate an {@code InternalGroupUpdate} which specifies optional properties of the
    *     group. If this {@code InternalGroupUpdate} updates a property which was already specified
    *     by the {@code InternalGroupCreation}, the value of this {@code InternalGroupUpdate} wins.
-   * @throws OrmDuplicateKeyException if a group with the chosen name already exists
+   * @throws DuplicateKeyException if a group with the chosen name already exists
    * @throws IOException if indexing fails, or an error occurs while reading/writing from/to NoteDb
    * @return the created {@code InternalGroup}
    */
   public InternalGroup createGroup(
       InternalGroupCreation groupCreation, InternalGroupUpdate groupUpdate)
-      throws OrmDuplicateKeyException, IOException, ConfigInvalidException {
+      throws DuplicateKeyException, IOException, ConfigInvalidException {
     try (TraceTimer timer =
         TraceContext.newTimer(
             "Creating group '%s'", groupUpdate.getName().orElseGet(groupCreation::getNameKey))) {
@@ -200,12 +279,12 @@
    * @param groupUuid the UUID of the group to update
    * @param groupUpdate an {@code InternalGroupUpdate} which indicates the desired updates on the
    *     group
-   * @throws OrmDuplicateKeyException if the new name of the group is used by another group
+   * @throws DuplicateKeyException if the new name of the group is used by another group
    * @throws IOException if indexing fails, or an error occurs while reading/writing from/to NoteDb
    * @throws NoSuchGroupException if the specified group doesn't exist
    */
   public void updateGroup(AccountGroup.UUID groupUuid, InternalGroupUpdate groupUpdate)
-      throws OrmDuplicateKeyException, IOException, NoSuchGroupException, ConfigInvalidException {
+      throws DuplicateKeyException, IOException, NoSuchGroupException, ConfigInvalidException {
     try (TraceTimer timer = TraceContext.newTimer("Updating group %s", groupUuid)) {
       Optional<Timestamp> updatedOn = groupUpdate.getUpdatedOn();
       if (!updatedOn.isPresent()) {
@@ -222,7 +301,7 @@
 
   private InternalGroup createGroupInNoteDbWithRetry(
       InternalGroupCreation groupCreation, InternalGroupUpdate groupUpdate)
-      throws IOException, ConfigInvalidException, OrmDuplicateKeyException {
+      throws IOException, ConfigInvalidException, DuplicateKeyException {
     try {
       return retryHelper.execute(
           RetryHelper.ActionType.GROUP_UPDATE,
@@ -232,7 +311,7 @@
       Throwables.throwIfUnchecked(e);
       Throwables.throwIfInstanceOf(e, IOException.class);
       Throwables.throwIfInstanceOf(e, ConfigInvalidException.class);
-      Throwables.throwIfInstanceOf(e, OrmDuplicateKeyException.class);
+      Throwables.throwIfInstanceOf(e, DuplicateKeyException.class);
       throw new IOException(e);
     }
   }
@@ -240,7 +319,7 @@
   @VisibleForTesting
   public InternalGroup createGroupInNoteDb(
       InternalGroupCreation groupCreation, InternalGroupUpdate groupUpdate)
-      throws IOException, ConfigInvalidException, OrmDuplicateKeyException {
+      throws IOException, ConfigInvalidException, DuplicateKeyException {
     try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
       AccountGroup.NameKey groupName = groupUpdate.getName().orElseGet(groupCreation::getNameKey);
       GroupNameNotes groupNameNotes =
@@ -262,7 +341,7 @@
 
   private UpdateResult updateGroupInNoteDbWithRetry(
       AccountGroup.UUID groupUuid, InternalGroupUpdate groupUpdate)
-      throws IOException, ConfigInvalidException, OrmDuplicateKeyException, NoSuchGroupException {
+      throws IOException, ConfigInvalidException, DuplicateKeyException, NoSuchGroupException {
     try {
       return retryHelper.execute(
           RetryHelper.ActionType.GROUP_UPDATE,
@@ -272,7 +351,7 @@
       Throwables.throwIfUnchecked(e);
       Throwables.throwIfInstanceOf(e, IOException.class);
       Throwables.throwIfInstanceOf(e, ConfigInvalidException.class);
-      Throwables.throwIfInstanceOf(e, OrmDuplicateKeyException.class);
+      Throwables.throwIfInstanceOf(e, DuplicateKeyException.class);
       Throwables.throwIfInstanceOf(e, NoSuchGroupException.class);
       throw new IOException(e);
     }
@@ -281,7 +360,7 @@
   @VisibleForTesting
   public UpdateResult updateGroupInNoteDb(
       AccountGroup.UUID groupUuid, InternalGroupUpdate groupUpdate)
-      throws IOException, ConfigInvalidException, OrmDuplicateKeyException, NoSuchGroupException {
+      throws IOException, ConfigInvalidException, DuplicateKeyException, NoSuchGroupException {
     try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
       GroupConfig groupConfig = GroupConfig.loadForGroup(allUsersName, allUsersRepo, groupUuid);
       groupConfig.setGroupUpdate(groupUpdate, auditLogFormatter);
@@ -353,10 +432,10 @@
 
     RefUpdateUtil.executeChecked(batchRefUpdate, allUsersRepo);
     gitRefUpdated.fire(
-        allUsersName, batchRefUpdate, currentUser != null ? currentUser.state() : null);
+        allUsersName, batchRefUpdate, currentUser.map(user -> user.state()).orElse(null));
   }
 
-  private void evictCachesOnGroupCreation(InternalGroup createdGroup) throws IOException {
+  private void evictCachesOnGroupCreation(InternalGroup createdGroup) {
     logger.atFine().log("evict caches on creation of group %s", createdGroup.getGroupUUID());
     // By UUID is used for the index and hence should be evicted before refreshing the index.
     groupCache.evict(createdGroup.getGroupUUID());
@@ -369,7 +448,7 @@
     createdGroup.getSubgroups().forEach(groupIncludeCache::evictParentGroupsOf);
   }
 
-  private void evictCachesOnGroupUpdate(UpdateResult result) throws IOException {
+  private void evictCachesOnGroupUpdate(UpdateResult result) {
     logger.atFine().log("evict caches on update of group %s", result.getGroupUuid());
     // By UUID is used for the index and hence should be evicted before refreshing the index.
     groupCache.evict(result.getGroupUuid());
@@ -403,20 +482,20 @@
   }
 
   private void dispatchAuditEventsOnGroupCreation(InternalGroup createdGroup) {
-    if (currentUser == null) {
+    if (!currentUser.isPresent()) {
       return;
     }
 
     if (!createdGroup.getMembers().isEmpty()) {
       groupAuditService.dispatchAddMembers(
-          currentUser.getAccountId(),
+          currentUser.get().getAccountId(),
           createdGroup.getGroupUUID(),
           createdGroup.getMembers(),
           createdGroup.getCreatedOn());
     }
     if (!createdGroup.getSubgroups().isEmpty()) {
       groupAuditService.dispatchAddSubgroups(
-          currentUser.getAccountId(),
+          currentUser.get().getAccountId(),
           createdGroup.getGroupUUID(),
           createdGroup.getSubgroups(),
           createdGroup.getCreatedOn());
@@ -424,25 +503,34 @@
   }
 
   private void dispatchAuditEventsOnGroupUpdate(UpdateResult result, Timestamp updatedOn) {
-    if (currentUser == null) {
+    if (!currentUser.isPresent()) {
       return;
     }
 
     if (!result.getAddedMembers().isEmpty()) {
       groupAuditService.dispatchAddMembers(
-          currentUser.getAccountId(), result.getGroupUuid(), result.getAddedMembers(), updatedOn);
+          currentUser.get().getAccountId(),
+          result.getGroupUuid(),
+          result.getAddedMembers(),
+          updatedOn);
     }
     if (!result.getDeletedMembers().isEmpty()) {
       groupAuditService.dispatchDeleteMembers(
-          currentUser.getAccountId(), result.getGroupUuid(), result.getDeletedMembers(), updatedOn);
+          currentUser.get().getAccountId(),
+          result.getGroupUuid(),
+          result.getDeletedMembers(),
+          updatedOn);
     }
     if (!result.getAddedSubgroups().isEmpty()) {
       groupAuditService.dispatchAddSubgroups(
-          currentUser.getAccountId(), result.getGroupUuid(), result.getAddedSubgroups(), updatedOn);
+          currentUser.get().getAccountId(),
+          result.getGroupUuid(),
+          result.getAddedSubgroups(),
+          updatedOn);
     }
     if (!result.getDeletedSubgroups().isEmpty()) {
       groupAuditService.dispatchDeleteSubgroups(
-          currentUser.getAccountId(),
+          currentUser.get().getAccountId(),
           result.getGroupUuid(),
           result.getDeletedSubgroups(),
           updatedOn);
diff --git a/java/com/google/gerrit/server/group/db/RenameGroupOp.java b/java/com/google/gerrit/server/group/db/RenameGroupOp.java
index eada57d..e002192 100644
--- a/java/com/google/gerrit/server/group/db/RenameGroupOp.java
+++ b/java/com/google/gerrit/server/group/db/RenameGroupOp.java
@@ -49,6 +49,7 @@
 
   private final ProjectCache projectCache;
   private final MetaDataUpdate.Server metaDataUpdateFactory;
+  private final ProjectConfig.Factory projectConfigFactory;
 
   private final PersonIdent author;
   private final AccountGroup.UUID uuid;
@@ -63,6 +64,7 @@
       WorkQueue workQueue,
       ProjectCache projectCache,
       MetaDataUpdate.Server metaDataUpdateFactory,
+      ProjectConfig.Factory projectConfigFactory,
       @Assisted("author") PersonIdent author,
       @Assisted AccountGroup.UUID uuid,
       @Assisted("oldName") String oldName,
@@ -70,6 +72,7 @@
     super(workQueue);
     this.projectCache = projectCache;
     this.metaDataUpdateFactory = metaDataUpdateFactory;
+    this.projectConfigFactory = projectConfigFactory;
 
     this.author = author;
     this.uuid = uuid;
@@ -109,7 +112,7 @@
   private void rename(MetaDataUpdate md) throws IOException, ConfigInvalidException {
     boolean success = false;
     for (int attempts = 0; !success && attempts < MAX_TRIES; attempts++) {
-      ProjectConfig config = ProjectConfig.read(md);
+      ProjectConfig config = projectConfigFactory.read(md);
 
       // The group isn't referenced, or its name has been fixed already.
       //
diff --git a/java/com/google/gerrit/server/group/db/testing/GroupTestUtil.java b/java/com/google/gerrit/server/group/db/testing/GroupTestUtil.java
index 5a0d28c..fa06281 100644
--- a/java/com/google/gerrit/server/group/db/testing/GroupTestUtil.java
+++ b/java/com/google/gerrit/server/group/db/testing/GroupTestUtil.java
@@ -45,8 +45,8 @@
       String fileName,
       String contents)
       throws Exception {
-    try (RevWalk rw = new RevWalk(allUsersRepo)) {
-      TestRepository<Repository> testRepository = new TestRepository<>(allUsersRepo, rw);
+    try (RevWalk rw = new RevWalk(allUsersRepo);
+        TestRepository<Repository> testRepository = new TestRepository<>(allUsersRepo, rw)) {
       TestRepository<Repository>.CommitBuilder builder =
           testRepository
               .branch(refName)
diff --git a/java/com/google/gerrit/server/group/testing/InternalGroupSubject.java b/java/com/google/gerrit/server/group/testing/InternalGroupSubject.java
index f0ab638..2f91394 100644
--- a/java/com/google/gerrit/server/group/testing/InternalGroupSubject.java
+++ b/java/com/google/gerrit/server/group/testing/InternalGroupSubject.java
@@ -23,7 +23,6 @@
 import com.google.common.truth.IterableSubject;
 import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.group.InternalGroup;
 import java.sql.Timestamp;
@@ -32,7 +31,11 @@
 public class InternalGroupSubject extends Subject<InternalGroupSubject, InternalGroup> {
 
   public static InternalGroupSubject assertThat(InternalGroup group) {
-    return assertAbout(InternalGroupSubject::new).that(group);
+    return assertAbout(internalGroups()).that(group);
+  }
+
+  public static Subject.Factory<InternalGroupSubject, InternalGroup> internalGroups() {
+    return InternalGroupSubject::new;
   }
 
   private InternalGroupSubject(FailureMetadata metadata, InternalGroup actual) {
@@ -42,66 +45,66 @@
   public ComparableSubject<?, AccountGroup.UUID> groupUuid() {
     isNotNull();
     InternalGroup group = actual();
-    return Truth.assertThat(group.getGroupUUID()).named("groupUuid");
+    return check("groupUuid()").that(group.getGroupUUID());
   }
 
   public ComparableSubject<?, AccountGroup.NameKey> nameKey() {
     isNotNull();
     InternalGroup group = actual();
-    return Truth.assertThat(group.getNameKey()).named("nameKey");
+    return check("nameKey()").that(group.getNameKey());
   }
 
   public StringSubject name() {
     isNotNull();
     InternalGroup group = actual();
-    return Truth.assertThat(group.getName()).named("name");
+    return check("name()").that(group.getName());
   }
 
-  public DefaultSubject id() {
+  public Subject<DefaultSubject, Object> id() {
     isNotNull();
     InternalGroup group = actual();
-    return Truth.assertThat(group.getId()).named("id");
+    return check("id()").that(group.getId());
   }
 
   public StringSubject description() {
     isNotNull();
     InternalGroup group = actual();
-    return Truth.assertThat(group.getDescription()).named("description");
+    return check("description()").that(group.getDescription());
   }
 
   public ComparableSubject<?, AccountGroup.UUID> ownerGroupUuid() {
     isNotNull();
     InternalGroup group = actual();
-    return Truth.assertThat(group.getOwnerGroupUUID()).named("ownerGroupUuid");
+    return check("ownerGroupUuid()").that(group.getOwnerGroupUUID());
   }
 
   public BooleanSubject visibleToAll() {
     isNotNull();
     InternalGroup group = actual();
-    return Truth.assertThat(group.isVisibleToAll()).named("visibleToAll");
+    return check("visibleToAll()").that(group.isVisibleToAll());
   }
 
   public ComparableSubject<?, Timestamp> createdOn() {
     isNotNull();
     InternalGroup group = actual();
-    return Truth.assertThat(group.getCreatedOn()).named("createdOn");
+    return check("createdOn()").that(group.getCreatedOn());
   }
 
   public IterableSubject members() {
     isNotNull();
     InternalGroup group = actual();
-    return Truth.assertThat(group.getMembers()).named("members");
+    return check("members()").that(group.getMembers());
   }
 
   public IterableSubject subgroups() {
     isNotNull();
     InternalGroup group = actual();
-    return Truth.assertThat(group.getSubgroups()).named("subgroups");
+    return check("subgroups()").that(group.getSubgroups());
   }
 
   public ComparableSubject<?, ObjectId> refState() {
     isNotNull();
     InternalGroup group = actual();
-    return Truth.assertThat(group.getRefState()).named("refState");
+    return check("refState()").that(group.getRefState());
   }
 }
diff --git a/java/com/google/gerrit/server/index/IndexModule.java b/java/com/google/gerrit/server/index/IndexModule.java
index 3c9538c..e7b892d 100644
--- a/java/com/google/gerrit/server/index/IndexModule.java
+++ b/java/com/google/gerrit/server/index/IndexModule.java
@@ -22,6 +22,7 @@
 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;
@@ -76,7 +77,7 @@
   }
 
   public static final ImmutableCollection<SchemaDefinitions<?>> ALL_SCHEMA_DEFS =
-      ImmutableList.<SchemaDefinitions<?>>of(
+      ImmutableList.of(
           AccountSchemaDefinitions.INSTANCE,
           ChangeSchemaDefinitions.INSTANCE,
           GroupSchemaDefinitions.INSTANCE,
@@ -85,7 +86,19 @@
   /** Type of secondary index. */
   public static IndexType getIndexType(Injector injector) {
     Config cfg = injector.getInstance(Key.get(Config.class, GerritServerConfig.class));
-    return cfg.getEnum("index", null, "type", IndexType.LUCENE);
+    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();
   }
 
   private final int threads;
@@ -125,7 +138,7 @@
     factory(ChangeIndexer.Factory.class);
 
     bind(GroupIndexRewriter.class);
-    bind(GroupIndexCollection.class);
+    // GroupIndexCollection is already bound very high up in SchemaModule.
     listener().to(GroupIndexCollection.class);
     factory(GroupIndexerImpl.Factory.class);
 
@@ -160,7 +173,7 @@
     }
 
     Collection<IndexDefinition<?, ?, ?>> result =
-        ImmutableList.<IndexDefinition<?, ?, ?>>of(accounts, groups, changes, projects);
+        ImmutableList.of(accounts, groups, changes, projects);
     Set<String> expected =
         FluentIterable.from(ALL_SCHEMA_DEFS).transform(SchemaDefinitions::getName).toSet();
     Set<String> actual = FluentIterable.from(result).transform(IndexDefinition::getName).toSet();
diff --git a/java/com/google/gerrit/server/index/IndexUtils.java b/java/com/google/gerrit/server/index/IndexUtils.java
index 9836e82..4b5cd49 100644
--- a/java/com/google/gerrit/server/index/IndexUtils.java
+++ b/java/com/google/gerrit/server/index/IndexUtils.java
@@ -18,10 +18,10 @@
 import static com.google.gerrit.server.index.change.ChangeField.LEGACY_ID;
 import static com.google.gerrit.server.index.change.ChangeField.PROJECT;
 
-import com.google.common.base.Function;
 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.QueryOptions;
 import com.google.gerrit.index.project.ProjectField;
 import com.google.gerrit.server.CurrentUser;
@@ -31,44 +31,28 @@
 import com.google.gerrit.server.query.change.SingleGroupUser;
 import java.io.IOException;
 import java.util.Set;
-import java.util.concurrent.ExecutionException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
 public final class IndexUtils {
   public static final ImmutableMap<String, String> CUSTOM_CHAR_MAPPING =
       ImmutableMap.of("_", " ", ".", " ");
 
-  public static final Function<Exception, IOException> MAPPER =
-      new Function<Exception, IOException>() {
-        @Override
-        public IOException apply(Exception in) {
-          if (in instanceof IOException) {
-            return (IOException) in;
-          } else if (in instanceof ExecutionException && in.getCause() instanceof IOException) {
-            return (IOException) in.getCause();
-          } else {
-            return new IOException(in);
-          }
-        }
-      };
-
-  public static void setReady(SitePaths sitePaths, String name, int version, boolean ready)
-      throws IOException {
+  public static void setReady(SitePaths sitePaths, String name, int version, boolean ready) {
     try {
       GerritIndexStatus cfg = new GerritIndexStatus(sitePaths);
       cfg.setReady(name, version, ready);
       cfg.save();
-    } catch (ConfigInvalidException e) {
-      throw new IOException(e);
+    } catch (ConfigInvalidException | IOException e) {
+      throw new StorageException(e);
     }
   }
 
-  public static boolean getReady(SitePaths sitePaths, String name, int version) throws IOException {
+  public static boolean getReady(SitePaths sitePaths, String name, int version) {
     try {
       GerritIndexStatus cfg = new GerritIndexStatus(sitePaths);
       return cfg.getReady(name, version);
-    } catch (ConfigInvalidException e) {
-      throw new IOException(e);
+    } catch (ConfigInvalidException | IOException e) {
+      throw new StorageException(e);
     }
   }
 
diff --git a/java/com/google/gerrit/server/index/OnlineReindexer.java b/java/com/google/gerrit/server/index/OnlineReindexer.java
index ec0e1d4..37677bdd 100644
--- a/java/com/google/gerrit/server/index/OnlineReindexer.java
+++ b/java/com/google/gerrit/server/index/OnlineReindexer.java
@@ -18,12 +18,11 @@
 
 import com.google.common.collect.Lists;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.index.Index;
 import com.google.gerrit.index.IndexCollection;
 import com.google.gerrit.index.IndexDefinition;
 import com.google.gerrit.index.SiteIndexer;
-import java.io.IOException;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicBoolean;
 
@@ -35,7 +34,7 @@
   private final SiteIndexer<K, V, I> batchIndexer;
   private final int oldVersion;
   private final int newVersion;
-  private final DynamicSet<OnlineUpgradeListener> listeners;
+  private final PluginSetContext<OnlineUpgradeListener> listeners;
   private I index;
   private final AtomicBoolean running = new AtomicBoolean();
 
@@ -43,7 +42,7 @@
       IndexDefinition<K, V, I> def,
       int oldVersion,
       int newVersion,
-      DynamicSet<OnlineUpgradeListener> listeners) {
+      PluginSetContext<OnlineUpgradeListener> listeners) {
     this.name = def.getName();
     this.indexes = def.getIndexCollection();
     this.batchIndexer = def.getSiteIndexer();
@@ -62,15 +61,13 @@
               try {
                 reindex();
                 ok = true;
-              } catch (IOException e) {
+              } catch (RuntimeException e) {
                 logger.atSevere().withCause(e).log(
                     "Online reindex of %s schema version %s failed", name, version(index));
               } finally {
                 running.set(false);
                 if (!ok) {
-                  for (OnlineUpgradeListener listener : listeners) {
-                    listener.onFailure(name, oldVersion, newVersion);
-                  }
+                  listeners.runEach(listener -> listener.onFailure(name, oldVersion, newVersion));
                 }
               }
             }
@@ -93,10 +90,8 @@
     return i.getSchema().getVersion();
   }
 
-  private void reindex() throws IOException {
-    for (OnlineUpgradeListener listener : listeners) {
-      listener.onStart(name, oldVersion, newVersion);
-    }
+  private void reindex() {
+    listeners.runEach(listener -> listener.onStart(name, oldVersion, newVersion));
     index =
         requireNonNull(
             indexes.getWriteIndex(newVersion),
@@ -118,19 +113,13 @@
     }
     logger.atInfo().log("Reindex %s to version %s complete", name, version(index));
     activateIndex();
-    for (OnlineUpgradeListener listener : listeners) {
-      listener.onSuccess(name, oldVersion, newVersion);
-    }
+    listeners.runEach(listener -> listener.onSuccess(name, oldVersion, newVersion));
   }
 
   public void activateIndex() {
     indexes.setSearchIndex(index);
     logger.atInfo().log("Using %s schema version %s", name, version(index));
-    try {
-      index.markReady(true);
-    } catch (IOException e) {
-      logger.atWarning().log("Error activating new %s schema version %s", name, version(index));
-    }
+    index.markReady(true);
 
     List<I> toRemove = Lists.newArrayListWithExpectedSize(1);
     for (I i : indexes.getWriteIndexes()) {
@@ -139,12 +128,8 @@
       }
     }
     for (I i : toRemove) {
-      try {
-        i.markReady(false);
-        indexes.removeWriteIndex(version(i));
-      } catch (IOException e) {
-        logger.atWarning().log("Error deactivating old %s schema version %s", name, version(i));
-      }
+      i.markReady(false);
+      indexes.removeWriteIndex(version(i));
     }
   }
 }
diff --git a/java/com/google/gerrit/server/index/VersionManager.java b/java/com/google/gerrit/server/index/VersionManager.java
index f37472c..3417379 100644
--- a/java/com/google/gerrit/server/index/VersionManager.java
+++ b/java/com/google/gerrit/server/index/VersionManager.java
@@ -21,13 +21,13 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.gerrit.extensions.events.LifecycleListener;
-import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.index.Index;
 import com.google.gerrit.index.IndexCollection;
 import com.google.gerrit.index.IndexDefinition;
 import com.google.gerrit.index.IndexDefinition.IndexFactory;
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.inject.ProvisionException;
 import java.io.IOException;
 import java.util.Collection;
@@ -61,7 +61,7 @@
   protected final String runReindexMsg;
   protected final SitePaths sitePaths;
 
-  private final DynamicSet<OnlineUpgradeListener> listeners;
+  private final PluginSetContext<OnlineUpgradeListener> listeners;
 
   // The following fields must be accessed synchronized on this.
   protected final Map<String, IndexDefinition<?, ?, ?>> defs;
@@ -69,7 +69,7 @@
 
   protected VersionManager(
       SitePaths sitePaths,
-      DynamicSet<OnlineUpgradeListener> listeners,
+      PluginSetContext<OnlineUpgradeListener> listeners,
       Collection<IndexDefinition<?, ?, ?>> defs,
       boolean onlineUpgrade) {
     this.sitePaths = sitePaths;
diff --git a/java/com/google/gerrit/server/index/account/AccountIndexer.java b/java/com/google/gerrit/server/index/account/AccountIndexer.java
index 91fa1d9..7f4f295 100644
--- a/java/com/google/gerrit/server/index/account/AccountIndexer.java
+++ b/java/com/google/gerrit/server/index/account/AccountIndexer.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.index.account;
 
 import com.google.gerrit.reviewdb.client.Account;
-import java.io.IOException;
 
 public interface AccountIndexer {
 
@@ -24,7 +23,7 @@
    *
    * @param id account id to index.
    */
-  void index(Account.Id id) throws IOException;
+  void index(Account.Id id);
 
   /**
    * Synchronously reindex an account if it is stale.
@@ -32,5 +31,5 @@
    * @param id account id to index.
    * @return whether the account was reindexed
    */
-  boolean reindexIfStale(Account.Id id) throws IOException;
+  boolean reindexIfStale(Account.Id id);
 }
diff --git a/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java b/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
index e1b0edd..1eaac7a 100644
--- a/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
+++ b/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
@@ -17,6 +17,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+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;
@@ -74,7 +75,7 @@
   }
 
   @Override
-  public void index(Account.Id id) throws IOException {
+  public void index(Account.Id id) {
     byIdCache.evict(id);
     Optional<AccountState> accountState = byIdCache.get(id);
 
@@ -104,10 +105,14 @@
   }
 
   @Override
-  public boolean reindexIfStale(Account.Id id) throws IOException {
-    if (stalenessChecker.isStale(id)) {
-      index(id);
-      return true;
+  public boolean reindexIfStale(Account.Id id) {
+    try {
+      if (stalenessChecker.isStale(id)) {
+        index(id);
+        return true;
+      }
+    } catch (IOException e) {
+      throw new StorageException(e);
     }
     return false;
   }
@@ -121,6 +126,6 @@
       return indexes.getWriteIndexes();
     }
 
-    return index != null ? Collections.singleton(index) : ImmutableSet.<AccountIndex>of();
+    return index != null ? Collections.singleton(index) : ImmutableSet.of();
   }
 }
diff --git a/java/com/google/gerrit/server/index/account/IndexedAccountQuery.java b/java/com/google/gerrit/server/index/account/IndexedAccountQuery.java
index 644f1eb..8b9fa27 100644
--- a/java/com/google/gerrit/server/index/account/IndexedAccountQuery.java
+++ b/java/com/google/gerrit/server/index/account/IndexedAccountQuery.java
@@ -17,15 +17,14 @@
 import static com.google.common.base.Preconditions.checkState;
 
 import com.google.gerrit.index.Index;
-import com.google.gerrit.index.IndexedQuery;
 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.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;
-import com.google.gwtorm.server.OrmException;
 
 public class IndexedAccountQuery extends IndexedQuery<Account.Id, AccountState>
     implements DataSource<AccountState>, Matchable<AccountState> {
@@ -37,7 +36,7 @@
   }
 
   @Override
-  public boolean match(AccountState accountState) throws OrmException {
+  public boolean match(AccountState accountState) {
     Predicate<AccountState> pred = getChild(0);
     checkState(
         pred.isMatchable(),
diff --git a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
index 927440e..7082e08 100644
--- a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
+++ b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
@@ -29,7 +29,6 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MultiProgressMonitor;
 import com.google.gerrit.server.git.MultiProgressMonitor.Task;
@@ -39,7 +38,6 @@
 import com.google.gerrit.server.notedb.ChangeNotes.Factory.ChangeNotesResult;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -59,7 +57,6 @@
 public class AllChangesIndexer extends SiteIndexer<Change.Id, ChangeData, ChangeIndex> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  private final SchemaFactory<ReviewDb> schemaFactory;
   private final ChangeData.Factory changeDataFactory;
   private final GitRepositoryManager repoManager;
   private final ListeningExecutorService executor;
@@ -69,14 +66,12 @@
 
   @Inject
   AllChangesIndexer(
-      SchemaFactory<ReviewDb> schemaFactory,
       ChangeData.Factory changeDataFactory,
       GitRepositoryManager repoManager,
       @IndexExecutor(BATCH) ListeningExecutorService executor,
       ChangeIndexer.Factory indexerFactory,
       ChangeNotes.Factory notesFactory,
       ProjectCache projectCache) {
-    this.schemaFactory = schemaFactory;
     this.changeDataFactory = changeDataFactory;
     this.repoManager = repoManager;
     this.executor = executor;
@@ -216,8 +211,7 @@
 
     @Override
     public Void call() throws Exception {
-      try (Repository repo = repoManager.openRepository(project);
-          ReviewDb db = schemaFactory.open()) {
+      try (Repository repo = repoManager.openRepository(project)) {
         OnlineReindexMode.begin();
 
         // Order of scanning changes is undefined. This is ok if we assume that packfile locality is
@@ -225,7 +219,7 @@
         // It does mean that reindexing after invalidating the DiffSummary cache will be expensive,
         // but the goal is to invalidate that cache as infrequently as we possibly can. And besides,
         // we don't have concrete proof that improving packfile locality would help.
-        notesFactory.scan(repo, db, project).forEach(r -> index(db, r));
+        notesFactory.scan(repo, project).forEach(r -> index(r));
       } catch (RepositoryNotFoundException rnfe) {
         logger.atSevere().log(rnfe.getMessage());
       } finally {
@@ -234,13 +228,13 @@
       return null;
     }
 
-    private void index(ReviewDb db, ChangeNotesResult r) {
+    private void index(ChangeNotesResult r) {
       if (r.error().isPresent()) {
         fail("Failed to read change " + r.id() + " for indexing", true, r.error().get());
         return;
       }
       try {
-        indexer.index(changeDataFactory.create(db, r.notes()));
+        indexer.index(changeDataFactory.create(r.notes()));
         done.update(1);
         verboseWriter.println("Reindexed change " + r.id());
       } catch (RejectedExecutionException e) {
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index da807e7..394761b 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.index.change;
 
 import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.gerrit.index.FieldDef.exact;
 import static com.google.gerrit.index.FieldDef.fullText;
 import static com.google.gerrit.index.FieldDef.intRange;
@@ -22,10 +23,8 @@
 import static com.google.gerrit.index.FieldDef.prefix;
 import static com.google.gerrit.index.FieldDef.storedOnly;
 import static com.google.gerrit.index.FieldDef.timestamp;
-import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.APPROVAL_CODEC;
-import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.CHANGE_CODEC;
-import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.PATCH_SET_CODEC;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.joining;
 import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toSet;
 
@@ -39,13 +38,16 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Table;
 import com.google.common.flogger.FluentLogger;
+import com.google.common.io.Files;
 import com.google.common.primitives.Longs;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitRequirement;
 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;
@@ -53,14 +55,16 @@
 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.OutputFormat;
+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;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.index.change.StalenessChecker.RefStatePattern;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.notedb.RobotCommentNotes;
 import com.google.gerrit.server.project.SubmitRuleOptions;
@@ -68,11 +72,6 @@
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gerrit.server.query.change.ChangeStatusPredicate;
 import com.google.gson.Gson;
-import com.google.gwtorm.protobuf.ProtobufCodec;
-import com.google.gwtorm.server.OrmException;
-import com.google.protobuf.CodedOutputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -153,13 +152,8 @@
       exact(ChangeQueryBuilder.FIELD_FILE)
           .buildRepeatable(cd -> firstNonNull(cd.currentFilePaths(), ImmutableList.of()));
 
-  public static Set<String> getFileParts(ChangeData cd) throws OrmException {
-    List<String> paths;
-    try {
-      paths = cd.currentFilePaths();
-    } catch (IOException e) {
-      throw new OrmException(e);
-    }
+  public static Set<String> getFileParts(ChangeData cd) {
+    List<String> paths = cd.currentFilePaths();
 
     Splitter s = Splitter.on('/').omitEmptyStrings();
     Set<String> r = new HashSet<>();
@@ -186,6 +180,89 @@
   public static final FieldDef<ChangeData, Iterable<String>> FILE_PART =
       exact(ChangeQueryBuilder.FIELD_FILEPART).buildRepeatable(ChangeField::getFileParts);
 
+  /** File extensions of each file modified in the current patch set. */
+  public static final FieldDef<ChangeData, Iterable<String>> EXTENSION =
+      exact(ChangeQueryBuilder.FIELD_EXTENSION).buildRepeatable(ChangeField::getExtensions);
+
+  public static Set<String> getExtensions(ChangeData cd) {
+    return extensions(cd).collect(toSet());
+  }
+
+  /**
+   * File extensions of each file modified in the current patch set as a sorted list. The purpose of
+   * this field is to allow matching changes that only touch files with certain file extensions.
+   */
+  public static final FieldDef<ChangeData, String> ONLY_EXTENSIONS =
+      exact(ChangeQueryBuilder.FIELD_ONLY_EXTENSIONS).build(ChangeField::getAllExtensionsAsList);
+
+  public static String getAllExtensionsAsList(ChangeData cd) {
+    return extensions(cd).distinct().sorted().collect(joining(","));
+  }
+
+  /**
+   * Returns a stream with all file extensions that are used by files in the given change. A file
+   * extension is defined as the portion of the filename following the final `.`. Files with no `.`
+   * in their name have no extension. For them an empty string is returned as part of the stream.
+   *
+   * <p>If the change contains multiple files with the same extension the extension is returned
+   * multiple times in the stream (once per file).
+   */
+  private static Stream<String> extensions(ChangeData cd) {
+    return cd.currentFilePaths().stream()
+        // Use case-insensitive file extensions even though other file fields are case-sensitive.
+        // If we want to find "all Java files", we want to match both .java and .JAVA, even if we
+        // normally care about case sensitivity. (Whether we should change the existing file/path
+        // predicates to be case insensitive is a separate question.)
+        .map(f -> Files.getFileExtension(f).toLowerCase(Locale.US));
+  }
+
+  /** Footers from the commit message of the current patch set. */
+  public static final FieldDef<ChangeData, Iterable<String>> FOOTER =
+      exact(ChangeQueryBuilder.FIELD_FOOTER).buildRepeatable(ChangeField::getFooters);
+
+  public static Set<String> getFooters(ChangeData cd) {
+    return cd.commitFooters().stream()
+        .map(f -> f.toString().toLowerCase(Locale.US))
+        .collect(toSet());
+  }
+
+  /** Folders that are touched by the current patch set. */
+  public static final FieldDef<ChangeData, Iterable<String>> DIRECTORY =
+      exact(ChangeQueryBuilder.FIELD_DIRECTORY).buildRepeatable(ChangeField::getDirectories);
+
+  public static Set<String> getDirectories(ChangeData cd) {
+    List<String> paths = cd.currentFilePaths();
+
+    Splitter s = Splitter.on('/').omitEmptyStrings();
+    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)) {
+        if (nextPart != null) {
+          r.add(nextPart);
+
+          if (directory.length() > 0) {
+            directory.append("/");
+          }
+          directory.append(nextPart);
+
+          String intermediateDir = directory.toString();
+          int i = intermediateDir.indexOf('/');
+          while (i >= 0) {
+            r.add(intermediateDir);
+            intermediateDir = intermediateDir.substring(i + 1);
+            i = intermediateDir.indexOf('/');
+          }
+        }
+        nextPart = part;
+      }
+    }
+    return r;
+  }
+
   /** Owner/creator of the change. */
   public static final FieldDef<ChangeData, Integer> OWNER =
       integer(ChangeQueryBuilder.FIELD_OWNER).build(changeGetter(c -> c.getOwner().get()));
@@ -298,7 +375,7 @@
         continue;
       }
 
-      Long l = Longs.tryParse(v.substring(i2 + 1, v.length()));
+      Long l = Longs.tryParse(v.substring(i2 + 1));
       if (l == null) {
         logger.atWarning().log(
             "Failed to parse timestamp of reviewer field from change %s: %s", changeId.get(), v);
@@ -351,7 +428,7 @@
         continue;
       }
 
-      Long l = Longs.tryParse(v.substring(i2 + 1, v.length()));
+      Long l = Longs.tryParse(v.substring(i2 + 1));
       if (l == null) {
         logger.atWarning().log(
             "Failed to parse timestamp of reviewer by email field from change %s: %s",
@@ -373,7 +450,7 @@
   public static final FieldDef<ChangeData, Iterable<String>> EXACT_COMMIT =
       exact(ChangeQueryBuilder.FIELD_EXACTCOMMIT).buildRepeatable(ChangeField::getRevisions);
 
-  private static Set<String> getRevisions(ChangeData cd) throws OrmException {
+  private static Set<String> getRevisions(ChangeData cd) {
     Set<String> revisions = new HashSet<>();
     for (PatchSet ps : cd.patchSets()) {
       if (ps.getRevision() != null) {
@@ -392,7 +469,7 @@
   public static final FieldDef<ChangeData, Iterable<String>> LABEL =
       exact("label2").buildRepeatable(cd -> getLabels(cd, true));
 
-  private static Iterable<String> getLabels(ChangeData cd, boolean owners) throws OrmException {
+  private static Iterable<String> getLabels(ChangeData cd, boolean owners) {
     Set<String> allApprovals = new HashSet<>();
     Set<String> distinctApprovals = new HashSet<>();
     for (PatchSetApproval a : cd.currentApprovals()) {
@@ -409,20 +486,19 @@
     return allApprovals;
   }
 
-  public static Set<String> getAuthorParts(ChangeData cd) throws OrmException, IOException {
+  public static Set<String> getAuthorParts(ChangeData cd) {
     return SchemaUtil.getPersonParts(cd.getAuthor());
   }
 
-  public static Set<String> getAuthorNameAndEmail(ChangeData cd) throws OrmException, IOException {
+  public static Set<String> getAuthorNameAndEmail(ChangeData cd) {
     return getNameAndEmail(cd.getAuthor());
   }
 
-  public static Set<String> getCommitterParts(ChangeData cd) throws OrmException, IOException {
+  public static Set<String> getCommitterParts(ChangeData cd) {
     return SchemaUtil.getPersonParts(cd.getCommitter());
   }
 
-  public static Set<String> getCommitterNameAndEmail(ChangeData cd)
-      throws OrmException, IOException {
+  public static Set<String> getCommitterNameAndEmail(ChangeData cd) {
     return getNameAndEmail(cd.getCommitter());
   }
 
@@ -469,12 +545,14 @@
 
   /** Serialized change object, used for pre-populating results. */
   public static final FieldDef<ChangeData, byte[]> CHANGE =
-      storedOnly("_change").build(changeGetter(CHANGE_CODEC::encodeToByteArray));
+      storedOnly("_change")
+          .build(changeGetter(change -> toProto(ChangeProtoConverter.INSTANCE, change)));
 
   /** Serialized approvals for the current patch set, used for pre-populating results. */
   public static final FieldDef<ChangeData, Iterable<byte[]>> APPROVAL =
       storedOnly("_approval")
-          .buildRepeatable(cd -> toProtos(APPROVAL_CODEC, cd.currentApprovals()));
+          .buildRepeatable(
+              cd -> toProtos(PatchSetApprovalProtoConverter.INSTANCE, cd.currentApprovals()));
 
   public static String formatLabel(String label, int value) {
     return formatLabel(label, value, null);
@@ -508,11 +586,15 @@
                           cd.messages().stream().map(ChangeMessage::getMessage))
                       .collect(toSet()));
 
-  /** Number of unresolved comments of the change. */
+  /** Number of unresolved comment threads of the change, including robot comments. */
   public static final FieldDef<ChangeData, Integer> UNRESOLVED_COMMENT_COUNT =
       intRange(ChangeQueryBuilder.FIELD_UNRESOLVED_COMMENT_COUNT)
           .build(ChangeData::unresolvedCommentCount);
 
+  /** Total number of published inline comments of the change, including robot comments. */
+  public static final FieldDef<ChangeData, Integer> TOTAL_COMMENT_COUNT =
+      intRange("total_comments").build(ChangeData::totalCommentCount);
+
   /** Whether the change is mergeable. */
   public static final FieldDef<ChangeData, String> MERGEABLE =
       exact(ChangeQueryBuilder.FIELD_MERGEABLE)
@@ -592,7 +674,8 @@
 
   /** Serialized patch set object, used for pre-populating results. */
   public static final FieldDef<ChangeData, Iterable<byte[]>> PATCH_SET =
-      storedOnly("_patch_set").buildRepeatable(cd -> toProtos(PATCH_SET_CODEC, cd.patchSets()));
+      storedOnly("_patch_set")
+          .buildRepeatable(cd -> toProtos(PatchSetProtoConverter.INSTANCE, cd.patchSets()));
 
   /** Users who have edits on this change. */
   public static final FieldDef<ChangeData, Iterable<Integer>> EDITBY =
@@ -751,7 +834,7 @@
     return storedSubmitRecords(cd.submitRecords(opts));
   }
 
-  public static List<String> formatSubmitRecordValues(ChangeData cd) throws OrmException {
+  public static List<String> formatSubmitRecordValues(ChangeData cd) {
     return formatSubmitRecordValues(
         cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT), cd.change().getOwner());
   }
@@ -798,19 +881,17 @@
                     .values()
                     .forEach(r -> result.add(RefState.of(r.ref()).toByteArray(allUsers(cd))));
 
-                if (PrimaryStorage.of(cd.change()) == PrimaryStorage.NOTE_DB) {
-                  ChangeNotes notes = cd.notes();
-                  result.add(
-                      RefState.create(notes.getRefName(), notes.getMetaId()).toByteArray(project));
-                  notes.getRobotComments(); // Force loading robot comments.
-                  RobotCommentNotes robotNotes = notes.getRobotCommentNotes();
-                  result.add(
-                      RefState.create(robotNotes.getRefName(), robotNotes.getMetaId())
-                          .toByteArray(project));
-                  cd.draftRefs()
-                      .values()
-                      .forEach(r -> result.add(RefState.of(r).toByteArray(allUsers(cd))));
-                }
+                ChangeNotes notes = cd.notes();
+                result.add(
+                    RefState.create(notes.getRefName(), notes.getMetaId()).toByteArray(project));
+                notes.getRobotComments(); // Force loading robot comments.
+                RobotCommentNotes robotNotes = notes.getRobotCommentNotes();
+                result.add(
+                    RefState.create(robotNotes.getRefName(), robotNotes.getMetaId())
+                        .toByteArray(project));
+                cd.draftRefs()
+                    .values()
+                    .forEach(r -> result.add(RefState.of(r).toByteArray(allUsers(cd))));
 
                 return result;
               });
@@ -835,15 +916,13 @@
                 result.add(
                     RefStatePattern.create(RefNames.refsStarredChangesPrefix(id) + "*")
                         .toByteArray(allUsers(cd)));
-                if (PrimaryStorage.of(cd.change()) == PrimaryStorage.NOTE_DB) {
-                  result.add(
-                      RefStatePattern.create(RefNames.refsDraftCommentsPrefix(id) + "*")
-                          .toByteArray(allUsers(cd)));
-                }
+                result.add(
+                    RefStatePattern.create(RefNames.refsDraftCommentsPrefix(id) + "*")
+                        .toByteArray(allUsers(cd)));
                 return result;
               });
 
-  private static String getTopic(ChangeData cd) throws OrmException {
+  private static String getTopic(ChangeData cd) {
     Change c = cd.change();
     if (c == null) {
       return null;
@@ -851,22 +930,12 @@
     return firstNonNull(c.getTopic(), "");
   }
 
-  private static <T> List<byte[]> toProtos(ProtobufCodec<T> codec, Collection<T> objs)
-      throws OrmException {
-    List<byte[]> result = Lists.newArrayListWithCapacity(objs.size());
-    ByteArrayOutputStream out = new ByteArrayOutputStream(256);
-    try {
-      for (T obj : objs) {
-        out.reset();
-        CodedOutputStream cos = CodedOutputStream.newInstance(out);
-        codec.encode(obj, cos);
-        cos.flush();
-        result.add(out.toByteArray());
-      }
-    } catch (IOException e) {
-      throw new OrmException(e);
-    }
-    return result;
+  private static <T> List<byte[]> toProtos(ProtoConverter<?, T> converter, Collection<T> objects) {
+    return objects.stream().map(object -> toProto(converter, object)).collect(toImmutableList());
+  }
+
+  private static <T> byte[] toProto(ProtoConverter<?, T> converter, T object) {
+    return Protos.toByteArray(converter.toProto(object));
   }
 
   private static <T> FieldDef.Getter<ChangeData, T> changeGetter(Function<Change, T> func) {
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexer.java b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
index 5e75c48..f5a96dd 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndexer.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
@@ -18,7 +18,6 @@
 
 import com.google.common.base.Objects;
 import com.google.common.flogger.FluentLogger;
-import com.google.common.util.concurrent.Atomics;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
@@ -27,29 +26,17 @@
 import com.google.gerrit.index.Index;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.index.IndexExecutor;
-import com.google.gerrit.server.index.IndexUtils;
 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.NotesMigration;
 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;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.OutOfScopeException;
-import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
-import com.google.inject.util.Providers;
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -58,7 +45,6 @@
 import java.util.concurrent.Callable;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Future;
-import java.util.concurrent.atomic.AtomicReference;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
 
@@ -77,21 +63,8 @@
     ChangeIndexer create(ListeningExecutorService executor, ChangeIndexCollection indexes);
   }
 
-  @SuppressWarnings("deprecation")
-  public static com.google.common.util.concurrent.CheckedFuture<?, IOException> allAsList(
-      List<? extends ListenableFuture<?>> futures) {
-    // allAsList propagates the first seen exception, wrapped in
-    // ExecutionException, so we can reuse the same mapper as for a single
-    // future. Assume the actual contents of the exception are not useful to
-    // callers. All exceptions are already logged by IndexTask.
-    return Futures.makeChecked(Futures.allAsList(futures), IndexUtils.MAPPER);
-  }
-
   @Nullable private final ChangeIndexCollection indexes;
   @Nullable private final ChangeIndex index;
-  private final SchemaFactory<ReviewDb> schemaFactory;
-  private final NotesMigration notesMigration;
-  private final ChangeNotes.Factory changeNotesFactory;
   private final ChangeData.Factory changeDataFactory;
   private final ThreadLocalRequestContext context;
   private final ListeningExecutorService batchExecutor;
@@ -108,9 +81,6 @@
   @AssistedInject
   ChangeIndexer(
       @GerritServerConfig Config cfg,
-      SchemaFactory<ReviewDb> schemaFactory,
-      NotesMigration notesMigration,
-      ChangeNotes.Factory changeNotesFactory,
       ChangeData.Factory changeDataFactory,
       ThreadLocalRequestContext context,
       PluginSetContext<ChangeIndexedListener> indexedListeners,
@@ -119,9 +89,6 @@
       @Assisted ListeningExecutorService executor,
       @Assisted ChangeIndex index) {
     this.executor = executor;
-    this.schemaFactory = schemaFactory;
-    this.notesMigration = notesMigration;
-    this.changeNotesFactory = changeNotesFactory;
     this.changeDataFactory = changeDataFactory;
     this.context = context;
     this.indexedListeners = indexedListeners;
@@ -134,10 +101,7 @@
 
   @AssistedInject
   ChangeIndexer(
-      SchemaFactory<ReviewDb> schemaFactory,
       @GerritServerConfig Config cfg,
-      NotesMigration notesMigration,
-      ChangeNotes.Factory changeNotesFactory,
       ChangeData.Factory changeDataFactory,
       ThreadLocalRequestContext context,
       PluginSetContext<ChangeIndexedListener> indexedListeners,
@@ -146,9 +110,6 @@
       @Assisted ListeningExecutorService executor,
       @Assisted ChangeIndexCollection indexes) {
     this.executor = executor;
-    this.schemaFactory = schemaFactory;
-    this.notesMigration = notesMigration;
-    this.changeNotesFactory = changeNotesFactory;
     this.changeDataFactory = changeDataFactory;
     this.context = context;
     this.indexedListeners = indexedListeners;
@@ -169,14 +130,12 @@
    * @param id change to index.
    * @return future for the indexing task.
    */
-  @SuppressWarnings("deprecation")
-  public com.google.common.util.concurrent.CheckedFuture<?, IOException> indexAsync(
-      Project.NameKey project, Change.Id id) {
+  public ListenableFuture<?> indexAsync(Project.NameKey project, Change.Id id) {
     IndexTask task = new IndexTask(project, id);
     if (queuedIndexTasks.add(task)) {
       return submit(task);
     }
-    return Futures.immediateCheckedFuture(null);
+    return Futures.immediateFuture(null);
   }
 
   /**
@@ -185,14 +144,12 @@
    * @param ids changes to index.
    * @return future for completing indexing of all changes.
    */
-  @SuppressWarnings("deprecation")
-  public com.google.common.util.concurrent.CheckedFuture<?, IOException> indexAsync(
-      Project.NameKey project, Collection<Change.Id> ids) {
+  public ListenableFuture<?> indexAsync(Project.NameKey project, Collection<Change.Id> ids) {
     List<ListenableFuture<?>> futures = new ArrayList<>(ids.size());
     for (Change.Id id : ids) {
       futures.add(indexAsync(project, id));
     }
-    return allAsList(futures);
+    return Futures.allAsList(futures);
   }
 
   /**
@@ -200,7 +157,7 @@
    *
    * @param cd change to index.
    */
-  public void index(ChangeData cd) throws IOException {
+  public void index(ChangeData cd) {
     indexImpl(cd);
 
     // Always double-check whether the change might be stale immediately after
@@ -224,7 +181,7 @@
     autoReindexIfStale(cd);
   }
 
-  private void indexImpl(ChangeData cd) throws IOException {
+  private void indexImpl(ChangeData cd) {
     logger.atFine().log("Replace change %d in index.", cd.getId().get());
     for (Index<?, ChangeData> i : getWriteIndexes()) {
       try (TraceTimer traceTimer =
@@ -248,23 +205,20 @@
   /**
    * Synchronously index a change.
    *
-   * @param db review database.
    * @param change change to index.
    */
-  public void index(ReviewDb db, Change change) throws IOException, OrmException {
-    index(newChangeData(db, change));
+  public void index(Change change) {
+    index(changeDataFactory.create(change));
   }
 
   /**
    * Synchronously index a change.
    *
-   * @param db review database.
    * @param project the project to which the change belongs.
    * @param changeId ID of the change to index.
    */
-  public void index(ReviewDb db, Project.NameKey project, Change.Id changeId)
-      throws IOException, OrmException {
-    index(newChangeData(db, project, changeId));
+  public void index(Project.NameKey project, Change.Id changeId) {
+    index(changeDataFactory.create(project, changeId));
   }
 
   /**
@@ -273,8 +227,7 @@
    * @param id change to delete.
    * @return future for the deleting task.
    */
-  @SuppressWarnings("deprecation")
-  public com.google.common.util.concurrent.CheckedFuture<?, IOException> deleteAsync(Change.Id id) {
+  public ListenableFuture<?> deleteAsync(Change.Id id) {
     return submit(new DeleteTask(id));
   }
 
@@ -283,7 +236,7 @@
    *
    * @param id change ID to delete.
    */
-  public void delete(Change.Id id) throws IOException {
+  public void delete(Change.Id id) {
     new DeleteTask(id).call();
   }
 
@@ -297,14 +250,12 @@
    * @param id ID of the change to index.
    * @return future for reindexing the change; returns true if the change was stale.
    */
-  @SuppressWarnings("deprecation")
-  public com.google.common.util.concurrent.CheckedFuture<Boolean, IOException> reindexIfStale(
-      Project.NameKey project, Change.Id id) {
+  public ListenableFuture<Boolean> reindexIfStale(Project.NameKey project, Change.Id id) {
     ReindexIfStaleTask task = new ReindexIfStaleTask(project, id);
     if (queuedReindexIfStaleTasks.add(task)) {
       return submit(task, batchExecutor);
     }
-    return Futures.immediateCheckedFuture(false);
+    return Futures.immediateFuture(false);
   }
 
   private void autoReindexIfStale(ChangeData cd) {
@@ -323,17 +274,13 @@
     return indexes != null ? indexes.getWriteIndexes() : Collections.singleton(index);
   }
 
-  @SuppressWarnings("deprecation")
-  private <T> com.google.common.util.concurrent.CheckedFuture<T, IOException> submit(
-      Callable<T> task) {
+  private <T> ListenableFuture<T> submit(Callable<T> task) {
     return submit(task, executor);
   }
 
-  @SuppressWarnings("deprecation")
-  private static <T> com.google.common.util.concurrent.CheckedFuture<T, IOException> submit(
+  private static <T> ListenableFuture<T> submit(
       Callable<T> task, ListeningExecutorService executor) {
-    return Futures.makeChecked(
-        Futures.nonCancellationPropagating(executor.submit(task)), IndexUtils.MAPPER);
+    return Futures.nonCancellationPropagating(executor.submit(task));
   }
 
   private abstract class AbstractIndexTask<T> implements Callable<T> {
@@ -345,7 +292,7 @@
       this.id = id;
     }
 
-    protected abstract T callImpl(Provider<ReviewDb> db) throws Exception;
+    protected abstract T callImpl() throws Exception;
 
     protected abstract void remove();
 
@@ -355,37 +302,15 @@
     @Override
     public final T call() throws Exception {
       try {
-        final AtomicReference<Provider<ReviewDb>> dbRef = Atomics.newReference();
         RequestContext newCtx =
-            new RequestContext() {
-              @Override
-              public Provider<ReviewDb> getReviewDbProvider() {
-                Provider<ReviewDb> db = dbRef.get();
-                if (db == null) {
-                  try {
-                    db = Providers.of(schemaFactory.open());
-                  } catch (OrmException e) {
-                    throw new ProvisionException("error opening ReviewDb", e);
-                  }
-                  dbRef.set(db);
-                }
-                return db;
-              }
-
-              @Override
-              public CurrentUser getUser() {
-                throw new OutOfScopeException("No user during ChangeIndexer");
-              }
+            () -> {
+              throw new OutOfScopeException("No user during ChangeIndexer");
             };
         RequestContext oldCtx = context.setContext(newCtx);
         try {
-          return callImpl(newCtx.getReviewDbProvider());
+          return callImpl();
         } finally {
           context.setContext(oldCtx);
-          Provider<ReviewDb> db = dbRef.get();
-          if (db != null) {
-            db.get().close();
-          }
         }
       } catch (Exception e) {
         logger.atSevere().withCause(e).log("Failed to execute %s", this);
@@ -400,9 +325,9 @@
     }
 
     @Override
-    public Void callImpl(Provider<ReviewDb> db) throws Exception {
+    public Void callImpl() throws Exception {
       remove();
-      ChangeData cd = newChangeData(db.get(), project, id);
+      ChangeData cd = changeDataFactory.create(project, id);
       index(cd);
       return null;
     }
@@ -432,7 +357,7 @@
     }
   }
 
-  // Not AbstractIndexTask as it doesn't need ReviewDb.
+  // Not AbstractIndexTask as it doesn't need a request context.
   private class DeleteTask implements Callable<Void> {
     private final Change.Id id;
 
@@ -441,7 +366,7 @@
     }
 
     @Override
-    public Void call() throws IOException {
+    public Void call() {
       logger.atFine().log("Delete change %d from index.", id.get());
       // Don't bother setting a RequestContext to provide the DB.
       // Implementations should not need to access the DB in order to delete a
@@ -464,15 +389,13 @@
     }
 
     @Override
-    public Boolean callImpl(Provider<ReviewDb> db) throws Exception {
+    public Boolean callImpl() throws Exception {
       remove();
       try {
         if (stalenessChecker.isStale(id)) {
-          indexImpl(newChangeData(db.get(), project, id));
+          indexImpl(changeDataFactory.create(project, id));
           return true;
         }
-      } catch (NoSuchChangeException nsce) {
-        logger.atFine().log("Change %s was deleted, aborting reindexing the change.", id.get());
       } catch (Exception e) {
         if (!isCausedByRepositoryNotFoundException(e)) {
           throw e;
@@ -518,26 +441,4 @@
     }
     return false;
   }
-
-  // Avoid auto-rebuilding when reindexing if reading is disabled. This just
-  // increases contention on the meta ref from a background indexing thread
-  // with little benefit. The next actual write to the entity may still incur a
-  // less-contentious rebuild.
-  private ChangeData newChangeData(ReviewDb db, Change change) throws OrmException {
-    if (!notesMigration.readChanges()) {
-      ChangeNotes notes = changeNotesFactory.createWithAutoRebuildingDisabled(change, null);
-      return changeDataFactory.create(db, notes);
-    }
-    return changeDataFactory.create(db, change);
-  }
-
-  private ChangeData newChangeData(ReviewDb db, Project.NameKey project, Change.Id changeId)
-      throws OrmException {
-    if (!notesMigration.readChanges()) {
-      ChangeNotes notes =
-          changeNotesFactory.createWithAutoRebuildingDisabled(db, project, changeId);
-      return changeDataFactory.create(db, notes);
-    }
-    return changeDataFactory.create(db, project, changeId);
-  }
 }
diff --git a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index 2000cd1..cde6a64 100644
--- a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -99,7 +99,20 @@
   @Deprecated static final Schema<ChangeData> V49 = schema(V48);
 
   // Bump Lucene version requires reindexing
-  static final Schema<ChangeData> V50 = schema(V49);
+  @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);
+
+  // The computation of the 'extension' field is changed, hence reindexing is required.
+  static final Schema<ChangeData> V56 = schema(V55);
 
   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 f6cee6d..9be93f7 100644
--- a/java/com/google/gerrit/server/index/change/DummyChangeIndex.java
+++ b/java/com/google/gerrit/server/index/change/DummyChangeIndex.java
@@ -20,7 +20,6 @@
 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.io.IOException;
 
 public class DummyChangeIndex implements ChangeIndex {
   @Override
@@ -32,13 +31,13 @@
   public void close() {}
 
   @Override
-  public void replace(ChangeData cd) throws IOException {}
+  public void replace(ChangeData cd) {}
 
   @Override
-  public void delete(Change.Id id) throws IOException {}
+  public void delete(Change.Id id) {}
 
   @Override
-  public void deleteAll() throws IOException {}
+  public void deleteAll() {}
 
   @Override
   public ChangeDataSource getSource(Predicate<ChangeData> p, QueryOptions opts) {
@@ -46,7 +45,7 @@
   }
 
   @Override
-  public void markReady(boolean ready) throws IOException {}
+  public void markReady(boolean ready) {}
 
   public int getMaxLimit() {
     return Integer.MAX_VALUE;
diff --git a/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java b/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
index 66f8df2..ed09eed 100644
--- a/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
+++ b/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
@@ -19,25 +19,24 @@
 import static com.google.gerrit.server.index.change.ChangeField.PROJECT;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.index.IndexConfig;
-import com.google.gerrit.index.IndexedQuery;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.query.DataSource;
 import com.google.gerrit.index.query.IndexPredicate;
+import com.google.gerrit.index.query.IndexedQuery;
 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.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 com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
-import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -52,7 +51,7 @@
 public class IndexedChangeQuery extends IndexedQuery<Change.Id, ChangeData>
     implements ChangeDataSource, Matchable<ChangeData> {
   public static QueryOptions oneResult() {
-    return createOptions(IndexConfig.createDefault(), 0, 1, ImmutableSet.<String>of());
+    return createOptions(IndexConfig.createDefault(), 0, 1, ImmutableSet.of());
   }
 
   public static QueryOptions createOptions(
@@ -81,7 +80,7 @@
   }
 
   @Override
-  public ResultSet<ChangeData> read() throws OrmException {
+  public ResultSet<ChangeData> read() {
     final DataSource<ChangeData> currSource = source;
     final ResultSet<ChangeData> rs = currSource.read();
 
@@ -98,8 +97,8 @@
       }
 
       @Override
-      public List<ChangeData> toList() {
-        List<ChangeData> r = rs.toList();
+      public ImmutableList<ChangeData> toList() {
+        ImmutableList<ChangeData> r = rs.toList();
         for (ChangeData cd : r) {
           fromSource.put(cd, currSource);
         }
@@ -114,7 +113,7 @@
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
+  public boolean match(ChangeData cd) {
     if (source != null && fromSource.get(cd) == source) {
       return true;
     }
diff --git a/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java b/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
index be4f38c..fd12345 100644
--- a/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
+++ b/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
@@ -28,7 +28,6 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -41,7 +40,6 @@
 import com.google.gerrit.server.util.ManualRequestContext;
 import com.google.gerrit.server.util.OneOffRequestContext;
 import com.google.gerrit.server.util.RequestContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -98,12 +96,8 @@
     if (allUsersName.get().equals(event.getProjectName())) {
       Account.Id accountId = Account.Id.fromRef(event.getRefName());
       if (accountId != null && !event.getRefName().startsWith(RefNames.REFS_STARRED_CHANGES)) {
-        try {
-          accountCache.evict(accountId);
-          indexer.get().index(accountId);
-        } catch (IOException e) {
-          logger.atSevere().withCause(e).log("Reindex account %s failed.", accountId);
-        }
+        accountCache.evict(accountId);
+        indexer.get().index(accountId);
       }
     }
 
@@ -164,7 +158,7 @@
     }
 
     @Override
-    protected List<Change> impl(RequestContext ctx) throws OrmException {
+    protected List<Change> impl(RequestContext ctx) {
       String ref = event.getRefName();
       Project.NameKey project = new Project.NameKey(event.getProjectName());
       if (ref.equals(RefNames.REFS_CONFIG)) {
@@ -194,16 +188,13 @@
     }
 
     @Override
-    protected Void impl(RequestContext ctx) throws OrmException, IOException {
+    protected Void impl(RequestContext ctx) throws IOException {
       // Reload change, as some time may have passed since GetChanges.
-      ReviewDb db = ctx.getReviewDbProvider().get();
       remove();
       try {
         Change c =
-            notesFactory
-                .createChecked(db, new Project.NameKey(event.getProjectName()), id)
-                .getChange();
-        indexerFactory.create(executor, indexes).index(db, 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);
       }
diff --git a/java/com/google/gerrit/server/index/change/StalenessChecker.java b/java/com/google/gerrit/server/index/change/StalenessChecker.java
index cf7db6f..338cf3d 100644
--- a/java/com/google/gerrit/server/index/change/StalenessChecker.java
+++ b/java/com/google/gerrit/server/index/change/StalenessChecker.java
@@ -16,7 +16,6 @@
 
 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.joining;
 
 import com.google.auto.value.AutoValue;
@@ -29,21 +28,15 @@
 import com.google.common.collect.Sets;
 import com.google.common.collect.Streams;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.UsedAt;
 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.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.UsedAt;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.List;
@@ -66,21 +59,16 @@
   private final ChangeIndexCollection indexes;
   private final GitRepositoryManager repoManager;
   private final IndexConfig indexConfig;
-  private final Provider<ReviewDb> db;
 
   @Inject
   StalenessChecker(
-      ChangeIndexCollection indexes,
-      GitRepositoryManager repoManager,
-      IndexConfig indexConfig,
-      Provider<ReviewDb> db) {
+      ChangeIndexCollection indexes, GitRepositoryManager repoManager, IndexConfig indexConfig) {
     this.indexes = indexes;
     this.repoManager = repoManager;
     this.indexConfig = indexConfig;
-    this.db = db;
   }
 
-  public boolean isStale(Change.Id id) throws IOException, OrmException {
+  public boolean isStale(Change.Id id) {
     ChangeIndex i = indexes.getSearchIndex();
     if (i == null) {
       return false; // No index; caller couldn't do anything if it is stale.
@@ -96,25 +84,16 @@
       return true; // Not in index, but caller wants it to be.
     }
     ChangeData cd = result.get();
-    return isStale(
-        repoManager,
-        id,
-        cd.change(),
-        ChangeNotes.readOneReviewDbChange(db.get(), id),
-        parseStates(cd),
-        parsePatterns(cd));
+    return isStale(repoManager, id, parseStates(cd), parsePatterns(cd));
   }
 
   @UsedAt(UsedAt.Project.GOOGLE)
   public static boolean isStale(
       GitRepositoryManager repoManager,
       Change.Id id,
-      Change indexChange,
-      @Nullable Change reviewDbChange,
       SetMultimap<Project.NameKey, RefState> states,
       ListMultimap<Project.NameKey, RefStatePattern> patterns) {
-    return reviewDbChangeIsStale(indexChange, reviewDbChange)
-        || refsAreStale(repoManager, id, states, patterns);
+    return refsAreStale(repoManager, id, states, patterns);
   }
 
   @VisibleForTesting
@@ -134,31 +113,6 @@
     return false;
   }
 
-  @VisibleForTesting
-  static boolean reviewDbChangeIsStale(Change indexChange, @Nullable Change reviewDbChange) {
-    requireNonNull(indexChange);
-    PrimaryStorage storageFromIndex = PrimaryStorage.of(indexChange);
-    PrimaryStorage storageFromReviewDb = PrimaryStorage.of(reviewDbChange);
-    if (reviewDbChange == null) {
-      if (storageFromIndex == PrimaryStorage.REVIEW_DB) {
-        return true; // Index says it should have been in ReviewDb, but it wasn't.
-      }
-      return false; // Not in ReviewDb, but that's ok.
-    }
-    checkArgument(
-        indexChange.getId().equals(reviewDbChange.getId()),
-        "mismatched change ID: %s != %s",
-        indexChange.getId(),
-        reviewDbChange.getId());
-    if (storageFromIndex != storageFromReviewDb) {
-      return true; // Primary storage differs, definitely stale.
-    }
-    if (storageFromReviewDb != PrimaryStorage.REVIEW_DB) {
-      return false; // Not a ReviewDb change, don't check rowVersion.
-    }
-    return reviewDbChange.getRowVersion() != indexChange.getRowVersion();
-  }
-
   private SetMultimap<Project.NameKey, RefState> parseStates(ChangeData cd) {
     return RefState.parseStates(cd.getRefStates());
   }
diff --git a/java/com/google/gerrit/server/index/group/GroupIndexer.java b/java/com/google/gerrit/server/index/group/GroupIndexer.java
index 503fd6b..5d9232e 100644
--- a/java/com/google/gerrit/server/index/group/GroupIndexer.java
+++ b/java/com/google/gerrit/server/index/group/GroupIndexer.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.index.group;
 
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import java.io.IOException;
 
 public interface GroupIndexer {
 
@@ -24,7 +23,7 @@
    *
    * @param uuid group UUID to index.
    */
-  void index(AccountGroup.UUID uuid) throws IOException;
+  void index(AccountGroup.UUID uuid);
 
   /**
    * Synchronously reindex a group if it is stale.
@@ -32,5 +31,5 @@
    * @param uuid group UUID to index.
    * @return whether the group was reindexed
    */
-  boolean reindexIfStale(AccountGroup.UUID uuid) throws IOException;
+  boolean reindexIfStale(AccountGroup.UUID uuid);
 }
diff --git a/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java b/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
index a9124e1..5982de7 100644
--- a/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
+++ b/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
@@ -17,6 +17,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+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;
@@ -74,7 +75,7 @@
   }
 
   @Override
-  public void index(AccountGroup.UUID uuid) throws IOException {
+  public void index(AccountGroup.UUID uuid) {
     // Evict the cache to get an up-to-date value for sure.
     groupCache.evict(uuid);
     Optional<InternalGroup> internalGroup = groupCache.get(uuid);
@@ -104,10 +105,14 @@
   }
 
   @Override
-  public boolean reindexIfStale(AccountGroup.UUID uuid) throws IOException {
-    if (stalenessChecker.isStale(uuid)) {
-      index(uuid);
-      return true;
+  public boolean reindexIfStale(AccountGroup.UUID uuid) {
+    try {
+      if (stalenessChecker.isStale(uuid)) {
+        index(uuid);
+        return true;
+      }
+    } catch (IOException e) {
+      throw new StorageException(e);
     }
     return false;
   }
diff --git a/java/com/google/gerrit/server/index/group/IndexedGroupQuery.java b/java/com/google/gerrit/server/index/group/IndexedGroupQuery.java
index 79f25c0..32393b06 100644
--- a/java/com/google/gerrit/server/index/group/IndexedGroupQuery.java
+++ b/java/com/google/gerrit/server/index/group/IndexedGroupQuery.java
@@ -16,9 +16,9 @@
 
 import com.google.gerrit.index.Index;
 import com.google.gerrit.index.IndexConfig;
-import com.google.gerrit.index.IndexedQuery;
 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.AccountGroup;
diff --git a/java/com/google/gerrit/server/index/project/ProjectIndexerImpl.java b/java/com/google/gerrit/server/index/project/ProjectIndexerImpl.java
index 1a74eca..6d6f78d 100644
--- a/java/com/google/gerrit/server/index/project/ProjectIndexerImpl.java
+++ b/java/com/google/gerrit/server/index/project/ProjectIndexerImpl.java
@@ -30,7 +30,6 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
-import java.io.IOException;
 import java.util.Collection;
 import java.util.Collections;
 
@@ -71,7 +70,7 @@
   }
 
   @Override
-  public void index(Project.NameKey nameKey) throws IOException {
+  public void index(Project.NameKey nameKey) {
     ProjectState projectState = projectCache.get(nameKey);
     if (projectState != null) {
       logger.atFine().log("Replace project %s in index", nameKey.get());
diff --git a/java/com/google/gerrit/server/index/project/StalenessChecker.java b/java/com/google/gerrit/server/index/project/StalenessChecker.java
index 733e486..dc5ebc6 100644
--- a/java/com/google/gerrit/server/index/project/StalenessChecker.java
+++ b/java/com/google/gerrit/server/index/project/StalenessChecker.java
@@ -29,7 +29,6 @@
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
-import java.io.IOException;
 import java.util.Optional;
 
 public class StalenessChecker {
@@ -48,7 +47,7 @@
     this.indexConfig = indexConfig;
   }
 
-  public boolean isStale(Project.NameKey project) throws IOException {
+  public boolean isStale(Project.NameKey project) {
     ProjectData projectData = projectCache.get(project).toProjectData();
     ProjectIndex i = indexes.getSearchIndex();
     if (i == null) {
diff --git a/java/com/google/gerrit/server/ioutil/BUILD b/java/com/google/gerrit/server/ioutil/BUILD
index 5fcf0bf..ed58d5b 100644
--- a/java/com/google/gerrit/server/ioutil/BUILD
+++ b/java/com/google/gerrit/server/ioutil/BUILD
@@ -5,7 +5,7 @@
     srcs = glob(["**/*.java"]),
     visibility = ["//visibility:public"],
     deps = [
-        "//java/com/google/gerrit/reviewdb:client",
+        "//java/com/google/gerrit/reviewdb:server",
         "//lib:automaton",
         "//lib:guava",
         "//lib/jgit/org.eclipse.jgit.archive:jgit-archive",
diff --git a/java/com/google/gerrit/server/ioutil/HostPlatform.java b/java/com/google/gerrit/server/ioutil/HostPlatform.java
index 5afda3c..39e9c07 100644
--- a/java/com/google/gerrit/server/ioutil/HostPlatform.java
+++ b/java/com/google/gerrit/server/ioutil/HostPlatform.java
@@ -33,12 +33,7 @@
   private static boolean compute(String platform) {
     final String osDotName =
         AccessController.doPrivileged(
-            new PrivilegedAction<String>() {
-              @Override
-              public String run() {
-                return System.getProperty("os.name");
-              }
-            });
+            (PrivilegedAction<String>) () -> System.getProperty("os.name"));
     return osDotName != null && osDotName.toLowerCase().contains(platform);
   }
 
diff --git a/java/com/google/gerrit/server/mail/MailUtil.java b/java/com/google/gerrit/server/mail/MailUtil.java
index 507b53f..4afcc7b 100644
--- a/java/com/google/gerrit/server/mail/MailUtil.java
+++ b/java/com/google/gerrit/server/mail/MailUtil.java
@@ -18,17 +18,17 @@
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
 
 import com.google.gerrit.common.FooterConstants;
-import com.google.gerrit.common.errors.NoSuchAccountException;
+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 com.google.gwtorm.server.OrmException;
 import java.io.IOException;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 import java.util.regex.Pattern;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.revwalk.FooterKey;
 import org.eclipse.jgit.revwalk.FooterLine;
 
@@ -36,7 +36,7 @@
 
   public static MailRecipients getRecipientsFromFooters(
       AccountResolver accountResolver, List<FooterLine> footerLines)
-      throws OrmException, IOException {
+      throws IOException, ConfigInvalidException {
     MailRecipients recipients = new MailRecipients();
     for (FooterLine footerLine : footerLines) {
       try {
@@ -45,7 +45,7 @@
         } else if (footerLine.matches(FooterKey.CC)) {
           recipients.cc.add(toAccountId(accountResolver, footerLine.getValue().trim()));
         }
-      } catch (NoSuchAccountException e) {
+      } catch (UnprocessableEntityException e) {
         continue;
       }
     }
@@ -59,13 +59,10 @@
     return recipients;
   }
 
+  @SuppressWarnings("deprecation")
   private static Account.Id toAccountId(AccountResolver accountResolver, String nameOrEmail)
-      throws OrmException, NoSuchAccountException, IOException {
-    Account a = accountResolver.findByNameOrEmail(nameOrEmail);
-    if (a == null) {
-      throw new NoSuchAccountException("\"" + nameOrEmail + "\" is not registered");
-    }
-    return a.getId();
+      throws UnprocessableEntityException, IOException, ConfigInvalidException {
+    return accountResolver.resolveByNameOrEmail(nameOrEmail).asUnique().getAccount().getId();
   }
 
   private static boolean isReviewer(FooterLine candidateFooterLine) {
@@ -124,7 +121,7 @@
       return Pattern.compile(".*");
     }
 
-    StringBuilder sb = new StringBuilder("");
+    StringBuilder sb = new StringBuilder();
     for (String domain : domains) {
       String quoted = "\\Q" + domain.replace("\\E", "\\E\\\\E\\Q") + "\\E|";
       sb.append(quoted.replace("*", "\\E.*\\Q"));
diff --git a/java/com/google/gerrit/server/mail/SignedToken.java b/java/com/google/gerrit/server/mail/SignedToken.java
new file mode 100644
index 0000000..436b854
--- /dev/null
+++ b/java/com/google/gerrit/server/mail/SignedToken.java
@@ -0,0 +1,213 @@
+// 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.gerrit.server.mail;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import javax.crypto.Mac;
+import javax.crypto.ShortBufferException;
+import javax.crypto.spec.SecretKeySpec;
+import org.apache.commons.codec.binary.Base64;
+
+/**
+ * Utility function to compute and verify XSRF tokens.
+ *
+ * <p>{@link SignedTokenEmailTokenVerifier} uses this class to verify tokens appearing in the custom
+ * <code>xsrfKey
+ * </code> JSON request property. The tokens protect against cross-site request forgery by depending
+ * upon the browser's security model. The classic browser security model prohibits a script from
+ * site A from reading any data received from site B. By sending unforgeable tokens from the server
+ * and asking the client to return them to us, the client script must have had read access to the
+ * token at some point and is therefore also from our server.
+ */
+public class SignedToken {
+  private static final int INT_SZ = 4;
+  private static final String MAC_ALG = "HmacSHA1";
+
+  /**
+   * Generate a random key for use with the XSRF library.
+   *
+   * @return a new private key, base 64 encoded.
+   */
+  public static String generateRandomKey() {
+    final byte[] r = new byte[26];
+    new SecureRandom().nextBytes(r);
+    return encodeBase64(r);
+  }
+
+  private final int maxAge;
+  private final SecretKeySpec key;
+  private final SecureRandom rng;
+  private final int tokenLength;
+
+  /**
+   * Create a new utility, using the specific key.
+   *
+   * @param age the number of seconds a token may remain valid.
+   * @param keyBase64 base 64 encoded representation of the key.
+   * @throws XsrfException the JVM doesn't support the necessary algorithms.
+   */
+  public SignedToken(final int age, final String keyBase64) throws XsrfException {
+    maxAge = age > 5 ? age / 5 : age;
+    key = new SecretKeySpec(decodeBase64(keyBase64), MAC_ALG);
+    rng = new SecureRandom();
+    tokenLength = 2 * INT_SZ + newMac().getMacLength();
+  }
+
+  /**
+   * Generate a new signed token.
+   *
+   * @param text the text string to sign. Typically this should be some user-specific string, to
+   *     prevent replay attacks. The text must be safe to appear in whatever context the token
+   *     itself will appear, as the text is included on the end of the token.
+   * @return the signed token. The text passed in <code>text</code> will appear after the first ','
+   *     in the returned token string.
+   * @throws XsrfException the JVM doesn't support the necessary algorithms.
+   */
+  String newToken(final String text) throws XsrfException {
+    final int q = rng.nextInt();
+    final byte[] buf = new byte[tokenLength];
+    encodeInt(buf, 0, q);
+    encodeInt(buf, INT_SZ, now() ^ q);
+    computeToken(buf, text);
+    return encodeBase64(buf) + '$' + text;
+  }
+
+  /**
+   * Validate a returned token.
+   *
+   * @param tokenString a token string previously created by this class.
+   * @param text text that must have been used during {@link #newToken(String)} in order for the
+   *     token to be valid. If null the text will be taken from the token string itself.
+   * @return true if the token is valid; false if the token is null, the empty string, has expired,
+   *     does not match the text supplied, or is a forged token.
+   * @throws XsrfException the JVM doesn't support the necessary algorithms to generate a token.
+   *     XSRF services are simply not available.
+   */
+  ValidToken checkToken(final String tokenString, final String text) throws XsrfException {
+    if (tokenString == null || tokenString.length() == 0) {
+      return null;
+    }
+
+    final int s = tokenString.indexOf('$');
+    if (s <= 0) {
+      return null;
+    }
+
+    final String recvText = tokenString.substring(s + 1);
+    final byte[] in;
+    try {
+      in = decodeBase64(tokenString.substring(0, s));
+    } catch (RuntimeException e) {
+      return null;
+    }
+    if (in.length != tokenLength) {
+      return null;
+    }
+
+    final int q = decodeInt(in, 0);
+    final int c = decodeInt(in, INT_SZ) ^ q;
+    final int n = now();
+    if (maxAge > 0 && Math.abs(c - n) > maxAge) {
+      return null;
+    }
+
+    final byte[] gen = new byte[tokenLength];
+    System.arraycopy(in, 0, gen, 0, 2 * INT_SZ);
+    computeToken(gen, text != null ? text : recvText);
+    if (!Arrays.equals(gen, in)) {
+      return null;
+    }
+
+    return new ValidToken(maxAge > 0 && c + (maxAge >> 1) <= n, recvText);
+  }
+
+  private void computeToken(final byte[] buf, final String text) throws XsrfException {
+    final Mac m = newMac();
+    m.update(buf, 0, 2 * INT_SZ);
+    m.update(toBytes(text));
+    try {
+      m.doFinal(buf, 2 * INT_SZ);
+    } catch (ShortBufferException e) {
+      throw new XsrfException("Unexpected token overflow", e);
+    }
+  }
+
+  private Mac newMac() throws XsrfException {
+    try {
+      final Mac m = Mac.getInstance(MAC_ALG);
+      m.init(key);
+      return m;
+    } catch (NoSuchAlgorithmException e) {
+      throw new XsrfException(MAC_ALG + " not supported", e);
+    } catch (InvalidKeyException e) {
+      throw new XsrfException("Invalid private key", e);
+    }
+  }
+
+  private static int now() {
+    return (int) (System.currentTimeMillis() / 5000L);
+  }
+
+  private static byte[] decodeBase64(final String s) {
+    return Base64.decodeBase64(toBytes(s));
+  }
+
+  private static String encodeBase64(final byte[] buf) {
+    return toString(Base64.encodeBase64(buf));
+  }
+
+  private static void encodeInt(final byte[] buf, final int o, final int v) {
+    int _v = v;
+    buf[o + 3] = (byte) _v;
+    _v >>>= 8;
+
+    buf[o + 2] = (byte) _v;
+    _v >>>= 8;
+
+    buf[o + 1] = (byte) _v;
+    _v >>>= 8;
+
+    buf[o] = (byte) _v;
+  }
+
+  private static int decodeInt(final byte[] buf, final int o) {
+    int r = buf[o] << 8;
+
+    r |= buf[o + 1] & 0xff;
+    r <<= 8;
+
+    r |= buf[o + 2] & 0xff;
+    return (r << 8) | (buf[o + 3] & 0xff);
+  }
+
+  private static byte[] toBytes(final String s) {
+    final byte[] r = new byte[s.length()];
+    for (int k = r.length - 1; k >= 0; k--) {
+      r[k] = (byte) s.charAt(k);
+    }
+    return r;
+  }
+
+  private static String toString(final byte[] b) {
+    final StringBuilder r = new StringBuilder(b.length);
+    for (int i = 0; i < b.length; i++) {
+      r.append((char) b[i]);
+    }
+    return r.toString();
+  }
+}
diff --git a/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java b/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java
index 66fe07e..af492f1 100644
--- a/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java
+++ b/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java
@@ -20,9 +20,6 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.mail.send.RegisterNewEmailSender;
-import com.google.gwtjsonrpc.server.SignedToken;
-import com.google.gwtjsonrpc.server.ValidToken;
-import com.google.gwtjsonrpc.server.XsrfException;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
diff --git a/java/com/google/gerrit/server/mail/ValidToken.java b/java/com/google/gerrit/server/mail/ValidToken.java
new file mode 100644
index 0000000..19d6cb0
--- /dev/null
+++ b/java/com/google/gerrit/server/mail/ValidToken.java
@@ -0,0 +1,36 @@
+// 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.gerrit.server.mail;
+
+/** A validated token from {@link SignedToken#checkToken(String, String)} */
+class ValidToken {
+  private final boolean refresh;
+  private final String data;
+
+  ValidToken(final boolean ref, final String d) {
+    refresh = ref;
+    data = d;
+  }
+
+  /** The text protected by the token's encryption key. */
+  String getData() {
+    return data;
+  }
+
+  /** True if the token's life span is almost half-over and should be renewed. */
+  boolean needsRefresh() {
+    return refresh;
+  }
+}
diff --git a/java/com/google/gerrit/server/mail/XsrfException.java b/java/com/google/gerrit/server/mail/XsrfException.java
new file mode 100644
index 0000000..eda17c3
--- /dev/null
+++ b/java/com/google/gerrit/server/mail/XsrfException.java
@@ -0,0 +1,24 @@
+// 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.gerrit.server.mail;
+
+/** Indicates the requested method is not known. */
+public class XsrfException extends Exception {
+  private static final long serialVersionUID = 1L;
+
+  XsrfException(final String message, final Throwable why) {
+    super(message, why);
+  }
+}
diff --git a/java/com/google/gerrit/server/mail/receive/MailProcessor.java b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
index 15a71e0..b1acab9 100644
--- a/java/com/google/gerrit/server/mail/receive/MailProcessor.java
+++ b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -17,10 +17,9 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.Strings;
-import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicMap;
@@ -66,7 +65,6 @@
 import com.google.gerrit.server.util.ManualRequestContext;
 import com.google.gerrit.server.util.OneOffRequestContext;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -149,7 +147,7 @@
   }
 
   private void processImpl(BatchUpdate.Factory buf, MailMessage message)
-      throws OrmException, UpdateException, RestApiException, IOException {
+      throws UpdateException, RestApiException, IOException {
     for (Extension<MailFilter> filter : mailFilters) {
       if (!filter.getProvider().get().shouldProcessMessage(message)) {
         logger.atWarning().log(
@@ -210,7 +208,7 @@
 
   private void persistComments(
       BatchUpdate.Factory buf, MailMessage message, MailMetadata metadata, Account.Id sender)
-      throws OrmException, UpdateException, RestApiException {
+      throws UpdateException, RestApiException {
     try (ManualRequestContext ctx = oneOffRequestContext.openAs(sender)) {
       List<ChangeData> changeDataList =
           queryProvider.get().byLegacyChangeId(new Change.Id(metadata.changeNumber));
@@ -262,7 +260,7 @@
       }
 
       Op o = new Op(new PatchSet.Id(cd.getId(), metadata.patchSet), parsedComments, message.id());
-      BatchUpdate batchUpdate = buf.create(cd.db(), project, ctx.getUser(), TimeUtil.nowTs());
+      BatchUpdate batchUpdate = buf.create(project, ctx.getUser(), TimeUtil.nowTs());
       batchUpdate.addOp(cd.getId(), o);
       batchUpdate.execute();
     }
@@ -285,15 +283,15 @@
 
     @Override
     public boolean updateChange(ChangeContext ctx)
-        throws OrmException, UnprocessableEntityException, PatchListNotAvailableException {
-      patchSet = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
+        throws UnprocessableEntityException, PatchListNotAvailableException {
+      patchSet = psUtil.get(ctx.getNotes(), psId);
       notes = ctx.getNotes();
       if (patchSet == null) {
-        throw new OrmException("patch set not found: " + psId);
+        throw new StorageException("patch set not found: " + psId);
       }
 
       changeMessage = generateChangeMessage(ctx);
-      changeMessagesUtil.addChangeMessage(ctx.getDb(), ctx.getUpdate(psId), changeMessage);
+      changeMessagesUtil.addChangeMessage(ctx.getUpdate(psId), changeMessage);
 
       comments = new ArrayList<>();
       for (MailComment c : parsedComments) {
@@ -304,10 +302,7 @@
             persistentCommentFromMailComment(ctx, c, targetPatchSetForComment(ctx, c, patchSet)));
       }
       commentsUtil.putComments(
-          ctx.getDb(),
-          ctx.getUpdate(ctx.getChange().currentPatchSetId()),
-          Status.PUBLISHED,
-          comments);
+          ctx.getUpdate(ctx.getChange().currentPatchSetId()), Status.PUBLISHED, comments);
 
       return true;
     }
@@ -321,8 +316,7 @@
       // Send email notifications
       outgoingMailFactory
           .create(
-              NotifyHandling.ALL,
-              ArrayListMultimap.create(),
+              ctx.getNotify(notes.getChangeId()),
               notes,
               patchSet,
               ctx.getUser().asIdentifiedUser(),
@@ -335,12 +329,7 @@
       Map<String, Short> approvals = new HashMap<>();
       approvalsUtil
           .byPatchSetUser(
-              ctx.getDb(),
-              notes,
-              psId,
-              ctx.getAccountId(),
-              ctx.getRevWalk(),
-              ctx.getRepoView().getConfig())
+              notes, psId, ctx.getAccountId(), ctx.getRevWalk(), ctx.getRepoView().getConfig())
           .forEach(a -> approvals.put(a.getLabel(), a.getValue()));
       // Fire Gerrit event. Note that approvals can't be granted via email, so old and new approvals
       // are always the same here.
@@ -369,10 +358,9 @@
     }
 
     private PatchSet targetPatchSetForComment(
-        ChangeContext ctx, MailComment mailComment, PatchSet current) throws OrmException {
+        ChangeContext ctx, MailComment mailComment, PatchSet current) {
       if (mailComment.getInReplyTo() != null) {
         return psUtil.get(
-            ctx.getDb(),
             ctx.getNotes(),
             new PatchSet.Id(ctx.getChange().getId(), mailComment.getInReplyTo().key.patchSetId));
       }
@@ -381,7 +369,7 @@
 
     private Comment persistentCommentFromMailComment(
         ChangeContext ctx, MailComment mailComment, PatchSet patchSetForComment)
-        throws OrmException, UnprocessableEntityException, PatchListNotAvailableException {
+        throws UnprocessableEntityException, PatchListNotAvailableException {
       String fileName;
       // The patch set that this comment is based on is different if this
       // comment was sent in reply to a comment on a previous patch set.
@@ -424,7 +412,7 @@
     return "(" + numComments + (numComments > 1 ? " comments)" : " comment)");
   }
 
-  private Set<String> existingMessageIds(ChangeData cd) throws OrmException {
+  private Set<String> existingMessageIds(ChangeData cd) {
     Set<String> existingMessageIds = new HashSet<>();
     cd.messages().stream()
         .forEach(
diff --git a/java/com/google/gerrit/server/mail/send/AbandonedSender.java b/java/com/google/gerrit/server/mail/send/AbandonedSender.java
index 05dd542..1017965 100644
--- a/java/com/google/gerrit/server/mail/send/AbandonedSender.java
+++ b/java/com/google/gerrit/server/mail/send/AbandonedSender.java
@@ -14,11 +14,10 @@
 
 package com.google.gerrit.server.mail.send;
 
-import com.google.gerrit.common.errors.EmailException;
+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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -31,8 +30,7 @@
 
   @Inject
   public AbandonedSender(
-      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id)
-      throws OrmException {
+      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id) {
     super(ea, "abandon", ChangeEmail.newChangeData(ea, project, id));
   }
 
diff --git a/java/com/google/gerrit/server/mail/send/AddKeySender.java b/java/com/google/gerrit/server/mail/send/AddKeySender.java
index 7701c18..6a4918c 100644
--- a/java/com/google/gerrit/server/mail/send/AddKeySender.java
+++ b/java/com/google/gerrit/server/mail/send/AddKeySender.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.mail.send;
 
 import com.google.common.base.Joiner;
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.IdentifiedUser;
diff --git a/java/com/google/gerrit/server/mail/send/AddReviewerSender.java b/java/com/google/gerrit/server/mail/send/AddReviewerSender.java
index cb70106..22abd9c 100644
--- a/java/com/google/gerrit/server/mail/send/AddReviewerSender.java
+++ b/java/com/google/gerrit/server/mail/send/AddReviewerSender.java
@@ -14,10 +14,9 @@
 
 package com.google.gerrit.server.mail.send;
 
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -29,8 +28,7 @@
 
   @Inject
   public AddReviewerSender(
-      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id)
-      throws OrmException {
+      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id) {
     super(ea, newChangeData(ea, project, id));
   }
 
diff --git a/java/com/google/gerrit/server/mail/send/ChangeEmail.java b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
index a3000eb..4888e65 100644
--- a/java/com/google/gerrit/server/mail/send/ChangeEmail.java
+++ b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
@@ -15,10 +15,13 @@
 package com.google.gerrit.server.mail.send;
 
 import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.errors.EmailException;
+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;
@@ -43,9 +46,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
-import com.google.template.soy.data.SoyListData;
-import com.google.template.soy.data.SoyMapData;
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.text.MessageFormat;
@@ -69,7 +69,7 @@
 
   protected static ChangeData newChangeData(
       EmailArguments ea, Project.NameKey project, Change.Id id) {
-    return ea.changeDataFactory.create(ea.db.get(), project, id);
+    return ea.changeDataFactory.create(project, id);
   }
 
   protected final Change change;
@@ -84,7 +84,7 @@
   protected Set<Account.Id> authors;
   protected boolean emailOnlyAuthors;
 
-  protected ChangeEmail(EmailArguments ea, String mc, ChangeData cd) throws OrmException {
+  protected ChangeEmail(EmailArguments ea, String mc, ChangeData cd) {
     super(ea, mc, cd.change().getDest());
     changeData = cd;
     change = cd.change();
@@ -150,7 +150,7 @@
     if (patchSet == null) {
       try {
         patchSet = changeData.currentPatchSet();
-      } catch (OrmException err) {
+      } catch (StorageException err) {
         patchSet = null;
       }
     }
@@ -159,9 +159,8 @@
       setHeader(MailHeader.PATCH_SET.fieldName(), patchSet.getPatchSetId() + "");
       if (patchSetInfo == null) {
         try {
-          patchSetInfo =
-              args.patchSetInfoFactory.get(args.db.get(), changeData.notes(), patchSet.getId());
-        } catch (PatchSetInfoNotAvailableException | OrmException err) {
+          patchSetInfo = args.patchSetInfoFactory.get(changeData.notes(), patchSet.getId());
+        } catch (PatchSetInfoNotAvailableException | StorageException err) {
           patchSetInfo = null;
         }
       }
@@ -170,7 +169,7 @@
 
     try {
       stars = changeData.stars();
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       throw new EmailException("Failed to load stars for change " + change.getChangeId(), e);
     }
 
@@ -185,14 +184,14 @@
     setChangeUrlHeader();
     setCommitIdHeader();
 
-    if (notify.ordinal() >= NotifyHandling.OWNER_REVIEWERS.ordinal()) {
+    if (notify.handling().compareTo(NotifyHandling.OWNER_REVIEWERS) >= 0) {
       try {
         addByEmail(
             RecipientType.CC, changeData.reviewersByEmail().byState(ReviewerStateInternal.CC));
         addByEmail(
             RecipientType.CC,
             changeData.reviewersByEmail().byState(ReviewerStateInternal.REVIEWER));
-      } catch (OrmException e) {
+      } catch (StorageException e) {
         throw new EmailException("Failed to add unregistered CCs " + change.getChangeId(), e);
       }
     }
@@ -295,10 +294,8 @@
       ps = patchSet;
     } else {
       try {
-        ps =
-            args.patchSetUtil.get(
-                changeData.db(), changeData.notes(), new PatchSet.Id(change.getId(), patchSetId));
-      } catch (OrmException e) {
+        ps = args.patchSetUtil.get(changeData.notes(), new PatchSet.Id(change.getId(), patchSetId));
+      } catch (StorageException e) {
         throw new PatchListNotAvailableException("Failed to get patchSet", e);
       }
     }
@@ -327,7 +324,7 @@
 
   /** BCC any user who has starred this change. */
   protected void bccStarredBy() {
-    if (!NotifyHandling.ALL.equals(notify)) {
+    if (!NotifyHandling.ALL.equals(notify.handling())) {
       return;
     }
 
@@ -347,9 +344,8 @@
   }
 
   @Override
-  protected final Watchers getWatchers(NotifyType type, boolean includeWatchersFromNotifyConfig)
-      throws OrmException {
-    if (!NotifyHandling.ALL.equals(notify)) {
+  protected final Watchers getWatchers(NotifyType type, boolean includeWatchersFromNotifyConfig) {
+    if (!NotifyHandling.ALL.equals(notify.handling())) {
       return new Watchers();
     }
 
@@ -359,7 +355,8 @@
 
   /** Any user who has published comments on this change. */
   protected void ccAllApprovals() {
-    if (!NotifyHandling.ALL.equals(notify) && !NotifyHandling.OWNER_REVIEWERS.equals(notify)) {
+    if (!NotifyHandling.ALL.equals(notify.handling())
+        && !NotifyHandling.OWNER_REVIEWERS.equals(notify.handling())) {
       return;
     }
 
@@ -367,14 +364,15 @@
       for (Account.Id id : changeData.reviewers().all()) {
         add(RecipientType.CC, id);
       }
-    } catch (OrmException err) {
+    } catch (StorageException err) {
       logger.atWarning().withCause(err).log("Cannot CC users that reviewed updated change");
     }
   }
 
   /** Users who have non-zero approval codes on the change. */
   protected void ccExistingReviewers() {
-    if (!NotifyHandling.ALL.equals(notify) && !NotifyHandling.OWNER_REVIEWERS.equals(notify)) {
+    if (!NotifyHandling.ALL.equals(notify.handling())
+        && !NotifyHandling.OWNER_REVIEWERS.equals(notify.handling())) {
       return;
     }
 
@@ -382,7 +380,7 @@
       for (Account.Id id : changeData.reviewers().byState(ReviewerStateInternal.REVIEWER)) {
         add(RecipientType.CC, id);
       }
-    } catch (OrmException err) {
+    } catch (StorageException err) {
       logger.atWarning().withCause(err).log("Cannot CC users that commented on updated change");
     }
   }
@@ -400,11 +398,7 @@
       return false;
     }
     try {
-      args.permissionBackend
-          .absentUser(to)
-          .change(changeData)
-          .database(args.db)
-          .check(ChangePermission.READ);
+      args.permissionBackend.absentUser(to).change(changeData).check(ChangePermission.READ);
       return true;
     } catch (AuthException e) {
       return false;
@@ -415,7 +409,7 @@
   protected Set<Account.Id> getAuthors() {
     Set<Account.Id> authors = new HashSet<>();
 
-    switch (notify) {
+    switch (notify.handling()) {
       case NONE:
         break;
       case ALL:
@@ -512,7 +506,7 @@
       for (Account.Id who : changeData.reviewers().byState(state)) {
         reviewers.add(getNameEmailFor(who));
       }
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atWarning().withCause(e).log("Cannot get change reviewers");
     }
     return reviewers;
@@ -566,15 +560,15 @@
   }
 
   /**
-   * Generate a Soy list of maps representing each line of the unified diff. The line maps will have
-   * a 'type' key which maps to one of 'common', 'add' or 'remove' and a 'text' key which maps to
-   * the line's content.
+   * Generate a list of maps representing each line of the unified diff. The line maps will have a
+   * 'type' key which maps to one of 'common', 'add' or 'remove' and a 'text' key which maps to the
+   * line's content.
    */
-  private SoyListData getDiffTemplateData() {
-    SoyListData result = new SoyListData();
+  private ImmutableList<ImmutableMap<String, String>> getDiffTemplateData() {
+    ImmutableList.Builder<ImmutableMap<String, String>> result = ImmutableList.builder();
     Splitter lineSplitter = Splitter.on(System.getProperty("line.separator"));
     for (String diffLine : lineSplitter.split(getUnifiedDiff())) {
-      SoyMapData lineData = new SoyMapData();
+      ImmutableMap.Builder<String, String> lineData = ImmutableMap.builder();
       lineData.put("text", diffLine);
 
       // Skip empty lines and lines that look like diff headers.
@@ -593,8 +587,8 @@
             break;
         }
       }
-      result.add(lineData);
+      result.add(lineData.build());
     }
-    return result;
+    return result.build();
   }
 }
diff --git a/java/com/google/gerrit/server/mail/send/CommentSender.java b/java/com/google/gerrit/server/mail/send/CommentSender.java
index 90f43d8..7b11ce6 100644
--- a/java/com/google/gerrit/server/mail/send/CommentSender.java
+++ b/java/com/google/gerrit/server/mail/send/CommentSender.java
@@ -20,8 +20,9 @@
 import com.google.common.collect.Ordering;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.FilenameComparator;
-import com.google.gerrit.common.errors.EmailException;
-import com.google.gerrit.common.errors.NoSuchEntityException;
+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;
@@ -41,7 +42,6 @@
 import com.google.gerrit.server.patch.PatchListObjectTooLargeException;
 import com.google.gerrit.server.util.LabelVote;
 import com.google.gwtorm.client.KeyUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
@@ -117,8 +117,7 @@
       CommentsUtil commentsUtil,
       @GerritServerConfig Config cfg,
       @Assisted Project.NameKey project,
-      @Assisted Change.Id id)
-      throws OrmException {
+      @Assisted Change.Id id) {
     super(ea, "comment", newChangeData(ea, project, id));
     this.commentsUtil = commentsUtil;
     this.incomingEmailEnabled =
@@ -127,7 +126,7 @@
     this.replyToAddress = cfg.getString("sendemail", null, "replyToAddress");
   }
 
-  public void setComments(List<Comment> comments) throws OrmException {
+  public void setComments(List<Comment> comments) {
     inlineComments = comments;
 
     Set<String> paths = new HashSet<>();
@@ -151,10 +150,10 @@
   protected void init() throws EmailException {
     super.init();
 
-    if (notify.compareTo(NotifyHandling.OWNER_REVIEWERS) >= 0) {
+    if (notify.handling().compareTo(NotifyHandling.OWNER_REVIEWERS) >= 0) {
       ccAllApprovals();
     }
-    if (notify.compareTo(NotifyHandling.ALL) >= 0) {
+    if (notify.handling().compareTo(NotifyHandling.ALL) >= 0) {
       bccStarredBy();
       includeWatchers(NotifyType.ALL_COMMENTS, !change.isWorkInProgress() && !change.isPrivate());
     }
@@ -313,8 +312,8 @@
 
     Comment.Key key = new Comment.Key(child.parentUuid, child.key.filename, child.key.patchSetId);
     try {
-      return commentsUtil.getPublished(args.db.get(), changeData.notes(), key);
-    } catch (OrmException e) {
+      return commentsUtil.getPublished(changeData.notes(), key);
+    } catch (StorageException e) {
       logger.atWarning().log("Could not find the parent of this comment: %s", child);
       return Optional.empty();
     }
diff --git a/java/com/google/gerrit/server/mail/send/CreateChangeSender.java b/java/com/google/gerrit/server/mail/send/CreateChangeSender.java
index fc9c14a..9895e07 100644
--- a/java/com/google/gerrit/server/mail/send/CreateChangeSender.java
+++ b/java/com/google/gerrit/server/mail/send/CreateChangeSender.java
@@ -15,7 +15,8 @@
 package com.google.gerrit.server.mail.send;
 
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.errors.EmailException;
+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;
@@ -24,7 +25,6 @@
 import com.google.gerrit.server.mail.send.ProjectWatch.Watchers;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.RefPermission;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.util.stream.StreamSupport;
@@ -44,8 +44,7 @@
       EmailArguments ea,
       PermissionBackend permissionBackend,
       @Assisted Project.NameKey project,
-      @Assisted Change.Id id)
-      throws OrmException {
+      @Assisted Change.Id id) {
     super(ea, newChangeData(ea, project, id));
     this.permissionBackend = permissionBackend;
   }
@@ -66,7 +65,7 @@
       add(RecipientType.TO, matching.to);
       add(RecipientType.CC, matching.cc);
       add(RecipientType.BCC, matching.bcc);
-    } catch (OrmException err) {
+    } catch (StorageException err) {
       // Just don't CC everyone. Better to send a partial message to those
       // we already have queued up then to fail deliver entirely to people
       // who have a lower interest in the change.
diff --git a/java/com/google/gerrit/server/mail/send/DeleteKeySender.java b/java/com/google/gerrit/server/mail/send/DeleteKeySender.java
index 31e4e77..39e21a5 100644
--- a/java/com/google/gerrit/server/mail/send/DeleteKeySender.java
+++ b/java/com/google/gerrit/server/mail/send/DeleteKeySender.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.mail.send;
 
 import com.google.common.base.Joiner;
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.IdentifiedUser;
diff --git a/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java b/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java
index 576d506..f941acc 100644
--- a/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java
+++ b/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java
@@ -14,14 +14,13 @@
 
 package com.google.gerrit.server.mail.send;
 
-import com.google.gerrit.common.errors.EmailException;
+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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.util.ArrayList;
@@ -42,8 +41,7 @@
 
   @Inject
   public DeleteReviewerSender(
-      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id)
-      throws OrmException {
+      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id) {
     super(ea, "deleteReviewer", newChangeData(ea, project, id));
   }
 
diff --git a/java/com/google/gerrit/server/mail/send/DeleteVoteSender.java b/java/com/google/gerrit/server/mail/send/DeleteVoteSender.java
index 0c81293..195d53d 100644
--- a/java/com/google/gerrit/server/mail/send/DeleteVoteSender.java
+++ b/java/com/google/gerrit/server/mail/send/DeleteVoteSender.java
@@ -14,11 +14,10 @@
 
 package com.google.gerrit.server.mail.send;
 
-import com.google.gerrit.common.errors.EmailException;
+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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -31,8 +30,7 @@
 
   @Inject
   protected DeleteVoteSender(
-      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id)
-      throws OrmException {
+      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id) {
     super(ea, "deleteVote", newChangeData(ea, project, id));
   }
 
diff --git a/java/com/google/gerrit/server/mail/send/EmailArguments.java b/java/com/google/gerrit/server/mail/send/EmailArguments.java
index 41a464e..fe2f74b 100644
--- a/java/com/google/gerrit/server/mail/send/EmailArguments.java
+++ b/java/com/google/gerrit/server/mail/send/EmailArguments.java
@@ -14,16 +14,15 @@
 
 package com.google.gerrit.server.mail.send;
 
+import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.GerritPersonIdentProvider;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.IdentifiedUser.GenericFactory;
 import com.google.gerrit.server.PatchSetUtil;
-import com.google.gerrit.server.UsedAt;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.config.AllProjectsName;
@@ -75,7 +74,6 @@
   final SitePaths site;
 
   final ChangeQueryBuilder queryBuilder;
-  final Provider<ReviewDb> db;
   final ChangeData.Factory changeDataFactory;
   final SoyTofu soyTofu;
   final EmailSettings settings;
@@ -106,7 +104,6 @@
       DynamicItem<UrlFormatter> urlFormatter,
       AllProjectsName allProjectsName,
       ChangeQueryBuilder queryBuilder,
-      Provider<ReviewDb> db,
       ChangeData.Factory changeDataFactory,
       @MailTemplates SoyTofu soyTofu,
       EmailSettings settings,
@@ -136,7 +133,6 @@
     this.urlFormatter = urlFormatter;
     this.allProjectsName = allProjectsName;
     this.queryBuilder = queryBuilder;
-    this.db = db;
     this.changeDataFactory = changeDataFactory;
     this.soyTofu = soyTofu;
     this.settings = settings;
diff --git a/java/com/google/gerrit/server/mail/send/EmailSender.java b/java/com/google/gerrit/server/mail/send/EmailSender.java
index ce4964d..9b3a1f7 100644
--- a/java/com/google/gerrit/server/mail/send/EmailSender.java
+++ b/java/com/google/gerrit/server/mail/send/EmailSender.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.mail.send;
 
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.mail.EmailHeader;
 import java.util.Collection;
diff --git a/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java b/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java
index 07e165c..ca332ff 100644
--- a/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java
+++ b/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.mail.send;
 
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.IdentifiedUser;
diff --git a/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java b/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java
index 99edc04..b5d384d 100644
--- a/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java
+++ b/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java
@@ -16,7 +16,7 @@
 
 import static java.util.Objects.requireNonNull;
 
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.mail.MailHeader;
diff --git a/java/com/google/gerrit/server/mail/send/MergedSender.java b/java/com/google/gerrit/server/mail/send/MergedSender.java
index 34f959c..cbc4117 100644
--- a/java/com/google/gerrit/server/mail/send/MergedSender.java
+++ b/java/com/google/gerrit/server/mail/send/MergedSender.java
@@ -19,13 +19,13 @@
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.LabelValue;
-import com.google.gerrit.common.errors.EmailException;
+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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -38,8 +38,8 @@
   private final LabelTypes labelTypes;
 
   @Inject
-  public MergedSender(EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id)
-      throws OrmException {
+  public MergedSender(
+      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id) {
     super(ea, "merged", newChangeData(ea, project, id));
     labelTypes = changeData.getLabelTypes();
   }
@@ -69,8 +69,7 @@
       Table<Account.Id, String, PatchSetApproval> pos = HashBasedTable.create();
       Table<Account.Id, String, PatchSetApproval> neg = HashBasedTable.create();
       for (PatchSetApproval ca :
-          args.approvalsUtil.byPatchSet(
-              args.db.get(), changeData.notes(), patchSet.getId(), null, null)) {
+          args.approvalsUtil.byPatchSet(changeData.notes(), patchSet.getId(), null, null)) {
         LabelType lt = labelTypes.byLabel(ca.getLabelId());
         if (lt == null) {
           continue;
@@ -83,7 +82,7 @@
       }
 
       return format("Approvals", pos) + format("Objections", neg);
-    } catch (OrmException err) {
+    } catch (StorageException err) {
       // Don't list the approvals
     }
     return "";
diff --git a/java/com/google/gerrit/server/mail/send/NewChangeSender.java b/java/com/google/gerrit/server/mail/send/NewChangeSender.java
index 917094e..a31596b 100644
--- a/java/com/google/gerrit/server/mail/send/NewChangeSender.java
+++ b/java/com/google/gerrit/server/mail/send/NewChangeSender.java
@@ -14,12 +14,11 @@
 
 package com.google.gerrit.server.mail.send;
 
-import com.google.gerrit.common.errors.EmailException;
+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 com.google.gwtorm.server.OrmException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
@@ -33,7 +32,7 @@
   private final Set<Account.Id> extraCC = new HashSet<>();
   private final Set<Address> extraCCByEmail = new HashSet<>();
 
-  protected NewChangeSender(EmailArguments ea, ChangeData cd) throws OrmException {
+  protected NewChangeSender(EmailArguments ea, ChangeData cd) {
     super(ea, "newchange", cd);
   }
 
@@ -61,7 +60,7 @@
     setHeader("Message-ID", threadId);
     setHeader("References", threadId);
 
-    switch (notify) {
+    switch (notify.handling()) {
       case NONE:
       case OWNER:
         break;
diff --git a/java/com/google/gerrit/server/mail/send/NotificationEmail.java b/java/com/google/gerrit/server/mail/send/NotificationEmail.java
index be593ff..9952ba6 100644
--- a/java/com/google/gerrit/server/mail/send/NotificationEmail.java
+++ b/java/com/google/gerrit/server/mail/send/NotificationEmail.java
@@ -17,7 +17,8 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.Iterables;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.errors.EmailException;
+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;
@@ -25,7 +26,6 @@
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.server.mail.send.ProjectWatch.Watchers;
-import com.google.gwtorm.server.OrmException;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -68,7 +68,7 @@
       add(RecipientType.TO, matching.to);
       add(RecipientType.CC, matching.cc);
       add(RecipientType.BCC, matching.bcc);
-    } catch (OrmException err) {
+    } catch (StorageException err) {
       // Just don't CC everyone. Better to send a partial message to those
       // we already have queued up then to fail deliver entirely to people
       // who have a lower interest in the change.
@@ -77,8 +77,7 @@
   }
 
   /** Returns all watchers that are relevant */
-  protected abstract Watchers getWatchers(NotifyType type, boolean includeWatchersFromNotifyConfig)
-      throws OrmException;
+  protected abstract Watchers getWatchers(NotifyType type, boolean includeWatchersFromNotifyConfig);
 
   /** Add users or email addresses to the TO, CC, or BCC list. */
   protected void add(RecipientType type, Watchers.List list) {
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index 400bc4f..e23d6f8 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -18,12 +18,9 @@
 import static com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy.DISABLED;
 import static java.util.Objects.requireNonNull;
 
-import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.errors.EmailException;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailFormat;
@@ -34,6 +31,7 @@
 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.change.NotifyResolver;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.validators.OutgoingEmailValidationListener;
 import com.google.gerrit.server.validators.ValidationException;
@@ -66,13 +64,12 @@
   private Address smtpFromAddress;
   private StringBuilder textBody;
   private StringBuilder htmlBody;
-  private ListMultimap<RecipientType, Account.Id> accountsToNotify = ImmutableListMultimap.of();
   protected Map<String, Object> soyContext;
   protected Map<String, Object> soyContextEmailData;
   protected List<String> footers;
   protected final EmailArguments args;
   protected Account.Id fromId;
-  protected NotifyHandling notify = NotifyHandling.ALL;
+  protected NotifyResolver.Result notify = NotifyResolver.Result.all();
 
   protected OutgoingEmail(EmailArguments ea, String mc) {
     args = ea;
@@ -84,14 +81,10 @@
     fromId = id;
   }
 
-  public void setNotify(NotifyHandling notify) {
+  public void setNotify(NotifyResolver.Result notify) {
     this.notify = requireNonNull(notify);
   }
 
-  public void setAccountsToNotify(ListMultimap<RecipientType, Account.Id> accountsToNotify) {
-    this.accountsToNotify = requireNonNull(accountsToNotify);
-  }
-
   /**
    * Format and enqueue the message for delivery.
    *
@@ -106,7 +99,7 @@
       return;
     }
 
-    if (NotifyHandling.NONE.equals(notify) && accountsToNotify.isEmpty()) {
+    if (!notify.shouldNotify()) {
       logger.atFine().log("Not sending '%s': Notify handling is NONE", messageClass);
       return;
     }
@@ -133,7 +126,7 @@
             // on their behalf to others.
             //
             add(RecipientType.CC, fromId);
-          } else if (!accountsToNotify.containsValue(fromId) && rcptTo.remove(fromId)) {
+          } else if (!notify.accounts().containsValue(fromId) && rcptTo.remove(fromId)) {
             // If they don't want a copy, but we queued one up anyway,
             // drop them from the recipient lists.
             //
@@ -253,8 +246,8 @@
     setHeader(FieldName.MESSAGE_ID, "");
     setHeader(MailHeader.AUTO_SUBMITTED.fieldName(), "auto-generated");
 
-    for (RecipientType recipientType : accountsToNotify.keySet()) {
-      add(recipientType, accountsToNotify.get(recipientType));
+    for (RecipientType recipientType : notify.accounts().keySet()) {
+      add(recipientType, notify.accounts().get(recipientType));
     }
 
     setHeader(MailHeader.MESSAGE_TYPE.fieldName(), messageClass);
@@ -423,7 +416,7 @@
       return false;
     }
 
-    if ((accountsToNotify == null || accountsToNotify.isEmpty())
+    if (notify.accounts().isEmpty()
         && smtpRcptTo.size() == 1
         && rcptTo.size() == 1
         && rcptTo.contains(fromId)) {
diff --git a/java/com/google/gerrit/server/mail/send/ProjectWatch.java b/java/com/google/gerrit/server/mail/send/ProjectWatch.java
index 9a86fdb..fd006c2 100644
--- a/java/com/google/gerrit/server/mail/send/ProjectWatch.java
+++ b/java/com/google/gerrit/server/mail/send/ProjectWatch.java
@@ -35,7 +35,6 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gerrit.server.query.change.SingleGroupUser;
-import com.google.gwtorm.server.OrmException;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -62,8 +61,7 @@
   }
 
   /** Returns all watchers that are relevant */
-  public final Watchers getWatchers(NotifyType type, boolean includeWatchersFromNotifyConfig)
-      throws OrmException {
+  public final Watchers getWatchers(NotifyType type, boolean includeWatchersFromNotifyConfig) {
     Watchers matching = new Watchers();
     Set<Account.Id> projectWatchers = new HashSet<>();
 
@@ -148,7 +146,7 @@
     }
   }
 
-  private void add(Watchers matching, NotifyConfig nc) throws OrmException, QueryParseException {
+  private void add(Watchers matching, NotifyConfig nc) throws QueryParseException {
     for (GroupReference ref : nc.getGroups()) {
       CurrentUser user = new SingleGroupUser(ref.getUUID());
       if (filterMatch(user, nc.getFilter())) {
@@ -202,8 +200,7 @@
       Account.Id accountId,
       ProjectWatchKey key,
       Set<NotifyType> watchedTypes,
-      NotifyType type)
-      throws OrmException {
+      NotifyType type) {
     IdentifiedUser user = args.identifiedUserFactory.create(accountId);
 
     try {
@@ -221,8 +218,7 @@
     return false;
   }
 
-  private boolean filterMatch(CurrentUser user, String filter)
-      throws OrmException, QueryParseException {
+  private boolean filterMatch(CurrentUser user, String filter) throws QueryParseException {
     ChangeQueryBuilder qb;
     Predicate<ChangeData> p = null;
 
diff --git a/java/com/google/gerrit/server/mail/send/RegisterNewEmailSender.java b/java/com/google/gerrit/server/mail/send/RegisterNewEmailSender.java
index 3bca00c..436736b 100644
--- a/java/com/google/gerrit/server/mail/send/RegisterNewEmailSender.java
+++ b/java/com/google/gerrit/server/mail/send/RegisterNewEmailSender.java
@@ -16,7 +16,7 @@
 
 import static java.util.Objects.requireNonNull;
 
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.IdentifiedUser;
diff --git a/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java b/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java
index 2398b82..30bcdeb 100644
--- a/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java
+++ b/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java
@@ -14,14 +14,13 @@
 
 package com.google.gerrit.server.mail.send;
 
-import com.google.gerrit.common.errors.EmailException;
+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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.util.ArrayList;
@@ -41,8 +40,7 @@
 
   @Inject
   public ReplacePatchSetSender(
-      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id)
-      throws OrmException {
+      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id) {
     super(ea, "newpatchset", newChangeData(ea, project, id));
   }
 
@@ -63,7 +61,8 @@
       //
       reviewers.remove(fromId);
     }
-    if (notify == NotifyHandling.ALL || notify == NotifyHandling.OWNER_REVIEWERS) {
+    if (notify.handling() == NotifyHandling.ALL
+        || notify.handling() == NotifyHandling.OWNER_REVIEWERS) {
       add(RecipientType.TO, reviewers);
       add(RecipientType.CC, extraCC);
     }
diff --git a/java/com/google/gerrit/server/mail/send/ReplyToChangeSender.java b/java/com/google/gerrit/server/mail/send/ReplyToChangeSender.java
index 61e9d1d..960c3a8 100644
--- a/java/com/google/gerrit/server/mail/send/ReplyToChangeSender.java
+++ b/java/com/google/gerrit/server/mail/send/ReplyToChangeSender.java
@@ -14,12 +14,11 @@
 
 package com.google.gerrit.server.mail.send;
 
-import com.google.gerrit.common.errors.EmailException;
+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;
-import com.google.gwtorm.server.OrmException;
 
 /** Alert a user to a reply to a change, usually commentary made during review. */
 public abstract class ReplyToChangeSender extends ChangeEmail {
@@ -27,7 +26,7 @@
     T create(Project.NameKey project, Change.Id id);
   }
 
-  protected ReplyToChangeSender(EmailArguments ea, String mc, ChangeData cd) throws OrmException {
+  protected ReplyToChangeSender(EmailArguments ea, String mc, ChangeData cd) {
     super(ea, mc, cd);
   }
 
diff --git a/java/com/google/gerrit/server/mail/send/RestoredSender.java b/java/com/google/gerrit/server/mail/send/RestoredSender.java
index d7f8eb5..0d998aa 100644
--- a/java/com/google/gerrit/server/mail/send/RestoredSender.java
+++ b/java/com/google/gerrit/server/mail/send/RestoredSender.java
@@ -14,11 +14,10 @@
 
 package com.google.gerrit.server.mail.send;
 
-import com.google.gerrit.common.errors.EmailException;
+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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -31,8 +30,7 @@
 
   @Inject
   public RestoredSender(
-      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id)
-      throws OrmException {
+      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id) {
     super(ea, "restore", ChangeEmail.newChangeData(ea, project, id));
   }
 
diff --git a/java/com/google/gerrit/server/mail/send/RevertedSender.java b/java/com/google/gerrit/server/mail/send/RevertedSender.java
index 21703a3..48b5d99 100644
--- a/java/com/google/gerrit/server/mail/send/RevertedSender.java
+++ b/java/com/google/gerrit/server/mail/send/RevertedSender.java
@@ -14,11 +14,10 @@
 
 package com.google.gerrit.server.mail.send;
 
-import com.google.gerrit.common.errors.EmailException;
+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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -30,8 +29,7 @@
 
   @Inject
   public RevertedSender(
-      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id)
-      throws OrmException {
+      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id) {
     super(ea, "revert", ChangeEmail.newChangeData(ea, project, id));
   }
 
diff --git a/java/com/google/gerrit/server/mail/send/SetAssigneeSender.java b/java/com/google/gerrit/server/mail/send/SetAssigneeSender.java
index 9708b1b..a120769 100644
--- a/java/com/google/gerrit/server/mail/send/SetAssigneeSender.java
+++ b/java/com/google/gerrit/server/mail/send/SetAssigneeSender.java
@@ -14,12 +14,11 @@
 
 package com.google.gerrit.server.mail.send;
 
-import com.google.gerrit.common.errors.EmailException;
+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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -35,8 +34,7 @@
       EmailArguments ea,
       @Assisted Project.NameKey project,
       @Assisted Change.Id id,
-      @Assisted Account.Id assignee)
-      throws OrmException {
+      @Assisted Account.Id assignee) {
     super(ea, "setassignee", newChangeData(ea, project, id));
     this.assignee = assignee;
   }
diff --git a/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java b/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
index 8615c04..3f103fc 100644
--- a/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
+++ b/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
@@ -20,7 +20,7 @@
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.Version;
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.mail.EmailHeader;
 import com.google.gerrit.server.config.ConfigUtil;
@@ -170,7 +170,7 @@
       throw new EmailException("Sending email is disabled");
     }
 
-    StringBuffer rejected = new StringBuffer();
+    StringBuilder rejected = new StringBuilder();
     try {
       final SMTPClient client = open();
       try {
@@ -207,10 +207,11 @@
              */
             throw new EmailException(
                 rejected
-                    + "Server "
-                    + smtpHost
-                    + " rejected DATA command: "
-                    + client.getReplyString());
+                    .append("Server ")
+                    .append(smtpHost)
+                    .append(" rejected DATA command: ")
+                    .append(client.getReplyString())
+                    .toString());
           }
 
           render(messageDataWriter, callerHeaders, textBody, htmlBody);
diff --git a/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java b/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
index 72f1ba6..1c6057d 100644
--- a/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
@@ -17,24 +17,22 @@
 import static com.google.gerrit.server.notedb.NoteDbTable.CHANGES;
 import static java.util.Objects.requireNonNull;
 
-import com.google.auto.value.AutoValue;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.UsedAt;
+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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
-import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
-import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder;
 import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
+import java.util.concurrent.atomic.AtomicBoolean;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
@@ -44,92 +42,84 @@
 public abstract class AbstractChangeNotes<T> {
   @VisibleForTesting
   @Singleton
+  @UsedAt(UsedAt.Project.PLUGIN_CHECKS)
   public static class Args {
-    final GitRepositoryManager repoManager;
-    final NotesMigration migration;
-    final AllUsersName allUsers;
-    final ChangeNoteJson changeNoteJson;
-    final LegacyChangeNoteRead legacyChangeNoteRead;
-    final NoteDbMetrics metrics;
-    final Provider<ReviewDb> db;
+    // TODO(dborowitz): Some less smelly way of disabling NoteDb in tests.
+    public final AtomicBoolean failOnLoadForTest;
+    public final ChangeNoteJson changeNoteJson;
+    public final GitRepositoryManager repoManager;
+    public final AllUsersName allUsers;
+    public final LegacyChangeNoteRead legacyChangeNoteRead;
+    public final NoteDbMetrics metrics;
 
     // Providers required to avoid dependency cycles.
 
-    // ChangeRebuilder -> ChangeNotes.Factory -> Args
-    final Provider<ChangeRebuilder> rebuilder;
-
     // ChangeNoteCache -> Args
-    final Provider<ChangeNotesCache> cache;
+    public final Provider<ChangeNotesCache> cache;
 
     @Inject
     Args(
         GitRepositoryManager repoManager,
-        NotesMigration migration,
         AllUsersName allUsers,
         ChangeNoteJson changeNoteJson,
         LegacyChangeNoteRead legacyChangeNoteRead,
         NoteDbMetrics metrics,
-        Provider<ReviewDb> db,
-        Provider<ChangeRebuilder> rebuilder,
         Provider<ChangeNotesCache> cache) {
+      this.failOnLoadForTest = new AtomicBoolean();
       this.repoManager = repoManager;
-      this.migration = migration;
       this.allUsers = allUsers;
       this.legacyChangeNoteRead = legacyChangeNoteRead;
       this.changeNoteJson = changeNoteJson;
       this.metrics = metrics;
-      this.db = db;
-      this.rebuilder = rebuilder;
       this.cache = cache;
     }
   }
 
-  @AutoValue
-  public abstract static class LoadHandle implements AutoCloseable {
-    public static LoadHandle create(ChangeNotesRevWalk walk, ObjectId id) {
+  public static class LoadHandle implements AutoCloseable {
+    private final Repository repo;
+    private final ObjectId id;
+    private ChangeNotesRevWalk rw;
+
+    private LoadHandle(Repository repo, @Nullable ObjectId id) {
+      this.repo = requireNonNull(repo);
+
       if (ObjectId.zeroId().equals(id)) {
         id = null;
       } else if (id != null) {
         id = id.copy();
       }
-      return new AutoValue_AbstractChangeNotes_LoadHandle(requireNonNull(walk), id);
+      this.id = id;
     }
 
-    public static LoadHandle missing() {
-      return new AutoValue_AbstractChangeNotes_LoadHandle(null, null);
+    public ChangeNotesRevWalk walk() {
+      if (rw == null) {
+        rw = ChangeNotesCommit.newRevWalk(repo);
+      }
+      return rw;
     }
 
     @Nullable
-    public abstract ChangeNotesRevWalk walk();
-
-    @Nullable
-    public abstract ObjectId id();
+    public ObjectId id() {
+      return id;
+    }
 
     @Override
     public void close() {
-      if (walk() != null) {
-        walk().close();
+      if (rw != null) {
+        rw.close();
       }
     }
   }
 
   protected final Args args;
-  protected final PrimaryStorage primaryStorage;
-  protected final boolean autoRebuild;
   private final Change.Id changeId;
 
   private ObjectId revision;
   private boolean loaded;
 
-  AbstractChangeNotes(
-      Args args, Change.Id changeId, @Nullable PrimaryStorage primaryStorage, boolean autoRebuild) {
+  protected AbstractChangeNotes(Args args, Change.Id changeId) {
     this.args = requireNonNull(args);
     this.changeId = requireNonNull(changeId);
-    this.primaryStorage = primaryStorage;
-    this.autoRebuild =
-        primaryStorage == PrimaryStorage.REVIEW_DB
-            && !args.migration.disableChangeReviewDb()
-            && autoRebuild;
   }
 
   public Change.Id getChangeId() {
@@ -141,40 +131,24 @@
     return revision;
   }
 
-  public T load() throws OrmException {
+  public T load() {
     if (loaded) {
       return self();
     }
 
-    boolean read = args.migration.readChanges();
-    if (!read && primaryStorage == PrimaryStorage.NOTE_DB) {
-      throw new OrmException("NoteDb is required to read change " + changeId);
-    }
-    boolean readOrWrite = read || args.migration.rawWriteChangesSetting();
-    if (!readOrWrite) {
-      // Don't even open the repo if we neither write to nor read from NoteDb. It's possible that
-      // there is some garbage in the noteDbState field and/or the repo, but at this point NoteDb is
-      // completely off so it's none of our business.
-      loadDefaults();
-      return self();
-    }
-    if (args.migration.failOnLoadForTest()) {
-      throw new OrmException("Reading from NoteDb is disabled");
+    if (args.failOnLoadForTest.get()) {
+      throw new StorageException("Reading from NoteDb is disabled");
     }
     try (Timer1.Context 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.
         LoadHandle handle = openHandle(repo)) {
-      if (read) {
-        revision = handle.id();
-        onLoad(handle);
-      } else {
-        loadDefaults();
-      }
+      revision = handle.id();
+      onLoad(handle);
       loaded = true;
     } catch (ConfigInvalidException | IOException e) {
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
     return self();
   }
@@ -199,25 +173,23 @@
   }
 
   protected LoadHandle openHandle(Repository repo, ObjectId id) {
-    return LoadHandle.create(ChangeNotesCommit.newRevWalk(repo), id);
+    return new LoadHandle(repo, id);
   }
 
-  public T reload() throws NoSuchChangeException, OrmException {
+  public T reload() {
     loaded = false;
     return load();
   }
 
-  public ObjectId loadRevision() throws OrmException {
+  public ObjectId loadRevision() {
     if (loaded) {
       return getRevision();
-    } else if (!args.migration.readChanges()) {
-      return null;
     }
     try (Repository repo = args.repoManager.openRepository(getProjectName())) {
       Ref ref = repo.getRefDatabase().exactRef(getRefName());
       return ref != null ? ref.getObjectId() : null;
     } catch (IOException e) {
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
   }
 
diff --git a/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java b/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
index e0cc771..91469f8 100644
--- a/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
@@ -27,12 +27,9 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.InternalUser;
-import com.google.gwtorm.server.OrmException;
 import java.io.IOException;
-import java.sql.Timestamp;
 import java.util.Date;
 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;
@@ -44,13 +41,11 @@
 public abstract class AbstractChangeUpdate {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  protected final NotesMigration migration;
   protected final ChangeNoteUtil noteUtil;
   protected final Account.Id accountId;
   protected final Account.Id realAccountId;
   protected final PersonIdent authorIdent;
   protected final Date when;
-  private final long readOnlySkewMs;
 
   @Nullable private final ChangeNotes notes;
   private final Change change;
@@ -61,14 +56,11 @@
   protected boolean rootOnly;
 
   protected AbstractChangeUpdate(
-      Config cfg,
-      NotesMigration migration,
       ChangeNotes notes,
       CurrentUser user,
       PersonIdent serverIdent,
       ChangeNoteUtil noteUtil,
       Date when) {
-    this.migration = migration;
     this.noteUtil = noteUtil;
     this.serverIdent = new PersonIdent(serverIdent, when);
     this.notes = notes;
@@ -78,12 +70,9 @@
     this.realAccountId = realAccountId != null ? realAccountId : accountId;
     this.authorIdent = ident(noteUtil, serverIdent, user, when);
     this.when = when;
-    this.readOnlySkewMs = NoteDbChangeState.getReadOnlySkew(cfg);
   }
 
   protected AbstractChangeUpdate(
-      Config cfg,
-      NotesMigration migration,
       ChangeNoteUtil noteUtil,
       PersonIdent serverIdent,
       @Nullable ChangeNotes notes,
@@ -95,7 +84,6 @@
     checkArgument(
         (notes != null && change == null) || (notes == null && change != null),
         "exactly one of notes or change required");
-    this.migration = migration;
     this.noteUtil = noteUtil;
     this.serverIdent = new PersonIdent(serverIdent, when);
     this.notes = notes;
@@ -104,7 +92,6 @@
     this.realAccountId = realAccountId;
     this.authorIdent = authorIdent;
     this.when = when;
-    this.readOnlySkewMs = NoteDbChangeState.getReadOnlySkew(cfg);
   }
 
   private static void checkUserType(CurrentUser user) {
@@ -206,21 +193,14 @@
    * @return commit ID produced by inserting this update's commit, or null if this update is a no-op
    *     and should be skipped. The zero ID is a valid return value, and indicates the ref should be
    *     deleted.
-   * @throws OrmException if a Gerrit-level error occurred.
    * @throws IOException if a lower-level error occurred.
    */
-  final ObjectId apply(RevWalk rw, ObjectInserter ins, ObjectId curr)
-      throws OrmException, IOException {
+  final ObjectId apply(RevWalk rw, ObjectInserter ins, ObjectId curr) throws IOException {
     if (isEmpty()) {
       return null;
     }
 
-    // Allow this method to proceed even if migration.failChangeWrites() = true.
-    // This may be used by an auto-rebuilding step that the caller does not plan
-    // to actually store.
-
     checkArgument(rw.getObjectReader().getCreatedFromInserter() == ins);
-    checkNotReadOnly();
 
     logger.atFinest().log(
         "%s for change %s of project %s in %s (NoteDb)",
@@ -253,18 +233,6 @@
     return result;
   }
 
-  protected void checkNotReadOnly() throws OrmException {
-    ChangeNotes notes = getNotes();
-    if (notes == null) {
-      // Can only happen during ChangeRebuilder, which will never include a read-only lease.
-      return;
-    }
-    Timestamp until = notes.getReadOnlyUntil();
-    if (until != null && NoteDbChangeState.timeForReadOnlyCheck(readOnlySkewMs).before(until)) {
-      throw new OrmException("change " + notes.getChangeId() + " is read-only until " + until);
-    }
-  }
-
   /**
    * Create a commit containing the contents of this update.
    *
@@ -275,11 +243,10 @@
    *     indicates to the caller that it should be copied from the parent commit. To indicate that
    *     this update is a no-op (but this could not be determined by {@link #isEmpty()}), return the
    *     sentinel {@link #NO_OP_UPDATE}.
-   * @throws OrmException if a Gerrit-level error occurred.
    * @throws IOException if a lower-level error occurred.
    */
   protected abstract CommitBuilder applyImpl(RevWalk rw, ObjectInserter ins, ObjectId curr)
-      throws OrmException, IOException;
+      throws IOException;
 
   protected static final CommitBuilder NO_OP_UPDATE = new CommitBuilder();
 
diff --git a/java/com/google/gerrit/server/notedb/ChangeBundle.java b/java/com/google/gerrit/server/notedb/ChangeBundle.java
deleted file mode 100644
index 058e5e5..0000000
--- a/java/com/google/gerrit/server/notedb/ChangeBundle.java
+++ /dev/null
@@ -1,973 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb;
-
-import static com.google.common.base.MoreObjects.firstNonNull;
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.collect.ImmutableList.toImmutableList;
-import static com.google.common.collect.ImmutableSortedMap.toImmutableSortedMap;
-import static com.google.gerrit.reviewdb.server.ReviewDbUtil.checkColumns;
-import static com.google.gerrit.reviewdb.server.ReviewDbUtil.intKeyOrdering;
-import static com.google.gerrit.server.notedb.ChangeBundle.Source.NOTE_DB;
-import static com.google.gerrit.server.notedb.ChangeBundle.Source.REVIEW_DB;
-import static com.google.gerrit.server.util.time.TimeUtil.truncateToSecond;
-import static java.util.Comparator.comparing;
-import static java.util.Comparator.naturalOrder;
-import static java.util.Comparator.nullsFirst;
-import static java.util.Objects.requireNonNull;
-import static java.util.stream.Collectors.toList;
-
-import com.google.auto.value.AutoValue;
-import com.google.common.base.CharMatcher;
-import com.google.common.base.Function;
-import com.google.common.base.Predicate;
-import com.google.common.base.Predicates;
-import com.google.common.base.Strings;
-import com.google.common.collect.Collections2;
-import com.google.common.collect.ImmutableCollection;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSortedMap;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.LinkedListMultimap;
-import com.google.common.collect.ListMultimap;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Ordering;
-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.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSet.Id;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.CommentsUtil;
-import com.google.gerrit.server.ReviewerSet;
-import com.google.gerrit.server.notedb.rebuild.ChangeRebuilderImpl;
-import com.google.gwtorm.client.Column;
-import com.google.gwtorm.server.OrmException;
-import java.lang.reflect.Field;
-import java.sql.Timestamp;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Comparator;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-
-/**
- * A bundle of all entities rooted at a single {@link Change} entity.
- *
- * <p>See the {@link Change} Javadoc for a depiction of this tree. Bundles may be compared using
- * {@link #differencesFrom(ChangeBundle)}, which normalizes out the minor implementation differences
- * between ReviewDb and NoteDb.
- */
-public class ChangeBundle {
-  public enum Source {
-    REVIEW_DB,
-    NOTE_DB;
-  }
-
-  public static ChangeBundle fromNotes(CommentsUtil commentsUtil, ChangeNotes notes)
-      throws OrmException {
-    return new ChangeBundle(
-        notes.getChange(),
-        notes.getChangeMessages(),
-        notes.getPatchSets().values(),
-        notes.getApprovals().values(),
-        Iterables.concat(
-            CommentsUtil.toPatchLineComments(
-                notes.getChangeId(),
-                PatchLineComment.Status.DRAFT,
-                commentsUtil.draftByChange(null, notes)),
-            CommentsUtil.toPatchLineComments(
-                notes.getChangeId(),
-                PatchLineComment.Status.PUBLISHED,
-                commentsUtil.publishedByChange(null, notes))),
-        notes.getReviewers(),
-        Source.NOTE_DB);
-  }
-
-  private static ImmutableSortedMap<ChangeMessage.Key, ChangeMessage> changeMessageMap(
-      Collection<ChangeMessage> in) {
-    return in.stream()
-        .collect(
-            toImmutableSortedMap(
-                comparing((ChangeMessage.Key k) -> k.getParentKey().get())
-                    .thenComparing(k -> k.get()),
-                cm -> cm.getKey(),
-                cm -> cm));
-  }
-
-  // Unlike the *Map comparators, which are intended to make key lists diffable,
-  // this comparator sorts first on timestamp, then on every other field.
-  private static final Comparator<ChangeMessage> CHANGE_MESSAGE_COMPARATOR =
-      comparing(ChangeMessage::getWrittenOn)
-          .thenComparing(m -> m.getKey().getParentKey().get())
-          .thenComparing(
-              m -> m.getPatchSetId() != null ? m.getPatchSetId().get() : null,
-              nullsFirst(naturalOrder()))
-          .thenComparing(ChangeMessage::getAuthor, intKeyOrdering())
-          .thenComparing(ChangeMessage::getMessage, nullsFirst(naturalOrder()));
-
-  private static ImmutableList<ChangeMessage> changeMessageList(Iterable<ChangeMessage> in) {
-    return Streams.stream(in).sorted(CHANGE_MESSAGE_COMPARATOR).collect(toImmutableList());
-  }
-
-  private static ImmutableSortedMap<Id, PatchSet> patchSetMap(Iterable<PatchSet> in) {
-    return Streams.stream(in)
-        .collect(toImmutableSortedMap(patchSetIdComparator(), PatchSet::getId, ps -> ps));
-  }
-
-  private static ImmutableSortedMap<PatchSetApproval.Key, PatchSetApproval> patchSetApprovalMap(
-      Iterable<PatchSetApproval> in) {
-    return Streams.stream(in)
-        .collect(
-            toImmutableSortedMap(
-                comparing(PatchSetApproval.Key::getParentKey, patchSetIdComparator())
-                    .thenComparing(PatchSetApproval.Key::getAccountId, intKeyOrdering())
-                    .thenComparing(PatchSetApproval.Key::getLabelId),
-                PatchSetApproval::getKey,
-                a -> a));
-  }
-
-  private static ImmutableSortedMap<PatchLineComment.Key, PatchLineComment> patchLineCommentMap(
-      Iterable<PatchLineComment> in) {
-    return Streams.stream(in)
-        .collect(
-            toImmutableSortedMap(
-                comparing(
-                        (PatchLineComment.Key k) -> k.getParentKey().getParentKey(),
-                        patchSetIdComparator())
-                    .thenComparing(PatchLineComment.Key::getParentKey)
-                    .thenComparing(PatchLineComment.Key::get),
-                PatchLineComment::getKey,
-                c -> c));
-  }
-
-  private static Comparator<PatchSet.Id> patchSetIdComparator() {
-    return comparing((PatchSet.Id id) -> id.getParentKey().get()).thenComparing(id -> id.get());
-  }
-
-  static {
-    // Initialization-time checks that the column set hasn't changed since the
-    // last time this file was updated.
-    checkColumns(Change.Id.class, 1);
-
-    checkColumns(
-        Change.class, 1, 2, 3, 4, 5, 7, 8, 10, 12, 13, 14, 17, 18, 19, 20, 21, 22, 23, 101);
-    checkColumns(ChangeMessage.Key.class, 1, 2);
-    checkColumns(ChangeMessage.class, 1, 2, 3, 4, 5, 6, 7);
-    checkColumns(PatchSet.Id.class, 1, 2);
-    checkColumns(PatchSet.class, 1, 2, 3, 4, 6, 8, 9);
-    checkColumns(PatchSetApproval.Key.class, 1, 2, 3);
-    checkColumns(PatchSetApproval.class, 1, 2, 3, 6, 7, 8);
-    checkColumns(PatchLineComment.Key.class, 1, 2);
-    checkColumns(PatchLineComment.class, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);
-  }
-
-  private final Change change;
-  private final ImmutableList<ChangeMessage> changeMessages;
-  private final ImmutableSortedMap<PatchSet.Id, PatchSet> patchSets;
-  private final ImmutableMap<PatchSetApproval.Key, PatchSetApproval> patchSetApprovals;
-  private final ImmutableMap<PatchLineComment.Key, PatchLineComment> patchLineComments;
-  private final ReviewerSet reviewers;
-  private final Source source;
-
-  public ChangeBundle(
-      Change change,
-      Iterable<ChangeMessage> changeMessages,
-      Iterable<PatchSet> patchSets,
-      Iterable<PatchSetApproval> patchSetApprovals,
-      Iterable<PatchLineComment> patchLineComments,
-      ReviewerSet reviewers,
-      Source source) {
-    this.change = requireNonNull(change);
-    this.changeMessages = changeMessageList(changeMessages);
-    this.patchSets = ImmutableSortedMap.copyOfSorted(patchSetMap(patchSets));
-    this.patchSetApprovals = ImmutableMap.copyOf(patchSetApprovalMap(patchSetApprovals));
-    this.patchLineComments = ImmutableMap.copyOf(patchLineCommentMap(patchLineComments));
-    this.reviewers = requireNonNull(reviewers);
-    this.source = requireNonNull(source);
-
-    for (ChangeMessage m : this.changeMessages) {
-      checkArgument(m.getKey().getParentKey().equals(change.getId()));
-    }
-    for (PatchSet.Id id : this.patchSets.keySet()) {
-      checkArgument(id.getParentKey().equals(change.getId()));
-    }
-    for (PatchSetApproval.Key k : this.patchSetApprovals.keySet()) {
-      checkArgument(k.getParentKey().getParentKey().equals(change.getId()));
-    }
-    for (PatchLineComment.Key k : this.patchLineComments.keySet()) {
-      checkArgument(k.getParentKey().getParentKey().getParentKey().equals(change.getId()));
-    }
-  }
-
-  public Change getChange() {
-    return change;
-  }
-
-  public ImmutableCollection<ChangeMessage> getChangeMessages() {
-    return changeMessages;
-  }
-
-  public ImmutableCollection<PatchSet> getPatchSets() {
-    return patchSets.values();
-  }
-
-  public ImmutableCollection<PatchSetApproval> getPatchSetApprovals() {
-    return patchSetApprovals.values();
-  }
-
-  public ImmutableCollection<PatchLineComment> getPatchLineComments() {
-    return patchLineComments.values();
-  }
-
-  public ReviewerSet getReviewers() {
-    return reviewers;
-  }
-
-  public Source getSource() {
-    return source;
-  }
-
-  public ImmutableList<String> differencesFrom(ChangeBundle o) {
-    List<String> diffs = new ArrayList<>();
-    diffChanges(diffs, this, o);
-    diffChangeMessages(diffs, this, o);
-    diffPatchSets(diffs, this, o);
-    diffPatchSetApprovals(diffs, this, o);
-    diffReviewers(diffs, this, o);
-    diffPatchLineComments(diffs, this, o);
-    return ImmutableList.copyOf(diffs);
-  }
-
-  private Timestamp getFirstPatchSetTime() {
-    if (patchSets.isEmpty()) {
-      return change.getCreatedOn();
-    }
-    return patchSets.firstEntry().getValue().getCreatedOn();
-  }
-
-  private Timestamp getLatestTimestamp() {
-    Ordering<Timestamp> o = Ordering.natural().nullsFirst();
-    Timestamp ts = null;
-    for (ChangeMessage cm : filterChangeMessages()) {
-      ts = o.max(ts, cm.getWrittenOn());
-    }
-    for (PatchSet ps : getPatchSets()) {
-      ts = o.max(ts, ps.getCreatedOn());
-    }
-    for (PatchSetApproval psa : filterPatchSetApprovals().values()) {
-      ts = o.max(ts, psa.getGranted());
-    }
-    for (PatchLineComment plc : filterPatchLineComments().values()) {
-      // Ignore draft comments, as they do not show up in the change meta graph.
-      if (plc.getStatus() != PatchLineComment.Status.DRAFT) {
-        ts = o.max(ts, plc.getWrittenOn());
-      }
-    }
-    return firstNonNull(ts, change.getLastUpdatedOn());
-  }
-
-  private Map<PatchSetApproval.Key, PatchSetApproval> filterPatchSetApprovals() {
-    return limitToValidPatchSets(patchSetApprovals, PatchSetApproval.Key::getParentKey);
-  }
-
-  private Map<PatchLineComment.Key, PatchLineComment> filterPatchLineComments() {
-    return limitToValidPatchSets(patchLineComments, k -> k.getParentKey().getParentKey());
-  }
-
-  private <K, V> Map<K, V> limitToValidPatchSets(Map<K, V> in, Function<K, PatchSet.Id> func) {
-    return Maps.filterKeys(in, Predicates.compose(validPatchSetPredicate(), func));
-  }
-
-  private Predicate<PatchSet.Id> validPatchSetPredicate() {
-    return patchSets::containsKey;
-  }
-
-  private Collection<ChangeMessage> filterChangeMessages() {
-    final Predicate<PatchSet.Id> validPatchSet = validPatchSetPredicate();
-    return Collections2.filter(
-        changeMessages,
-        m -> {
-          PatchSet.Id psId = m.getPatchSetId();
-          if (psId == null) {
-            return true;
-          }
-          return validPatchSet.apply(psId);
-        });
-  }
-
-  private static void diffChanges(List<String> diffs, ChangeBundle bundleA, ChangeBundle bundleB) {
-    Change a = bundleA.change;
-    Change b = bundleB.change;
-    String desc = a.getId().equals(b.getId()) ? describe(a.getId()) : "Changes";
-
-    boolean excludeCreatedOn = false;
-    boolean excludeCurrentPatchSetId = false;
-    boolean excludeTopic = false;
-    Timestamp aCreated = a.getCreatedOn();
-    Timestamp bCreated = b.getCreatedOn();
-    Timestamp aUpdated = a.getLastUpdatedOn();
-    Timestamp bUpdated = b.getLastUpdatedOn();
-
-    boolean excludeSubject = false;
-    boolean excludeOrigSubj = false;
-    // Subject is not technically a nullable field, but we observed some null
-    // subjects in the wild on googlesource.com, so treat null as empty.
-    String aSubj = Strings.nullToEmpty(a.getSubject());
-    String bSubj = Strings.nullToEmpty(b.getSubject());
-
-    // Allow created timestamp in NoteDb to be any of:
-    //  - The created timestamp of the change.
-    //  - The timestamp of the first remaining patch set.
-    //  - The last updated timestamp, if it is less than the created timestamp.
-    //
-    // Ignore subject if the NoteDb subject starts with the ReviewDb subject.
-    // The NoteDb subject is read directly from the commit, whereas the ReviewDb
-    // subject historically may have been truncated to fit in a SQL varchar
-    // column.
-    //
-    // Ignore original subject on the ReviewDb side when comparing to NoteDb.
-    // This field may have any number of values:
-    //  - It may be null, if the change has had no new patch sets pushed since
-    //    migrating to schema 103.
-    //  - It may match the first patch set subject, if the change was created
-    //    after migrating to schema 103.
-    //  - It may match the subject of the first patch set that was pushed after
-    //    the migration to schema 103, even though that is neither the subject
-    //    of the first patch set nor the subject of the last patch set. (See
-    //    Change#setCurrentPatchSet as of 43b10f86 for this behavior.) This
-    //    subject of an intermediate patch set is not available to the
-    //    ChangeBundle; we would have to get the subject from the repo, which is
-    //    inconvenient at this point.
-    //
-    // Ignore original subject on the ReviewDb side if it equals the subject of
-    // the current patch set.
-    //
-    // For all of the above subject comparisons, first trim any leading spaces
-    // from the NoteDb strings. (We actually do represent the leading spaces
-    // faithfully during conversion, but JGit's FooterLine parser trims them
-    // when reading.)
-    //
-    // Ignore empty topic on the ReviewDb side if it is null on the NoteDb side.
-    //
-    // Ignore currentPatchSetId on NoteDb side if ReviewDb does not point to a
-    // valid patch set.
-    //
-    // Use max timestamp of all ReviewDb entities when comparing with NoteDb.
-    if (bundleA.source == REVIEW_DB && bundleB.source == NOTE_DB) {
-      boolean createdOnMatchesFirstPs =
-          !timestampsDiffer(bundleA, bundleA.getFirstPatchSetTime(), bundleB, bCreated);
-      boolean createdOnMatchesLastUpdatedOn =
-          !timestampsDiffer(bundleA, aUpdated, bundleB, bCreated);
-      boolean createdAfterUpdated = aCreated.compareTo(aUpdated) > 0;
-      excludeCreatedOn =
-          createdOnMatchesFirstPs || (createdAfterUpdated && createdOnMatchesLastUpdatedOn);
-
-      aSubj = cleanReviewDbSubject(aSubj);
-      bSubj = cleanNoteDbSubject(bSubj);
-      excludeCurrentPatchSetId = !bundleA.validPatchSetPredicate().apply(a.currentPatchSetId());
-      excludeSubject = bSubj.startsWith(aSubj) || excludeCurrentPatchSetId;
-      excludeOrigSubj = true;
-      String aTopic = trimOrNull(a.getTopic());
-      excludeTopic =
-          Objects.equals(aTopic, b.getTopic()) || ("".equals(aTopic) && b.getTopic() == null);
-      aUpdated = bundleA.getLatestTimestamp();
-    } else if (bundleA.source == NOTE_DB && bundleB.source == REVIEW_DB) {
-      boolean createdOnMatchesFirstPs =
-          !timestampsDiffer(bundleA, aCreated, bundleB, bundleB.getFirstPatchSetTime());
-      boolean createdOnMatchesLastUpdatedOn =
-          !timestampsDiffer(bundleA, aCreated, bundleB, bUpdated);
-      boolean createdAfterUpdated = bCreated.compareTo(bUpdated) > 0;
-      excludeCreatedOn =
-          createdOnMatchesFirstPs || (createdAfterUpdated && createdOnMatchesLastUpdatedOn);
-
-      aSubj = cleanNoteDbSubject(aSubj);
-      bSubj = cleanReviewDbSubject(bSubj);
-      excludeCurrentPatchSetId = !bundleB.validPatchSetPredicate().apply(b.currentPatchSetId());
-      excludeSubject = aSubj.startsWith(bSubj) || excludeCurrentPatchSetId;
-      excludeOrigSubj = true;
-      String bTopic = trimOrNull(b.getTopic());
-      excludeTopic =
-          Objects.equals(bTopic, a.getTopic()) || (a.getTopic() == null && "".equals(bTopic));
-      bUpdated = bundleB.getLatestTimestamp();
-    }
-
-    String subjectField = "subject";
-    String updatedField = "lastUpdatedOn";
-    List<String> exclude =
-        Lists.newArrayList(subjectField, updatedField, "noteDbState", "rowVersion");
-    if (excludeCreatedOn) {
-      exclude.add("createdOn");
-    }
-    if (excludeCurrentPatchSetId) {
-      exclude.add("currentPatchSetId");
-    }
-    if (excludeOrigSubj) {
-      exclude.add("originalSubject");
-    }
-    if (excludeTopic) {
-      exclude.add("topic");
-    }
-    diffColumnsExcluding(diffs, Change.class, desc, bundleA, a, bundleB, b, exclude);
-
-    // Allow last updated timestamps to either be exactly equal (within slop),
-    // or the NoteDb timestamp to be equal to the latest entity timestamp in the
-    // whole ReviewDb bundle (within slop).
-    if (timestampsDiffer(bundleA, a.getLastUpdatedOn(), bundleB, b.getLastUpdatedOn())) {
-      diffTimestamps(
-          diffs, desc, bundleA, aUpdated, bundleB, bUpdated, "effective last updated time");
-    }
-    if (!excludeSubject) {
-      diffValues(diffs, desc, aSubj, bSubj, subjectField);
-    }
-  }
-
-  private static String trimOrNull(String s) {
-    return s != null ? CharMatcher.whitespace().trimFrom(s) : null;
-  }
-
-  private static String cleanReviewDbSubject(String s) {
-    s = CharMatcher.is(' ').trimLeadingFrom(s);
-
-    // An old JGit bug failed to extract subjects from commits with "\r\n"
-    // terminators: https://bugs.eclipse.org/bugs/show_bug.cgi?id=400707
-    // Changes created with this bug may have "\r\n" converted to "\r " and the
-    // entire commit in the subject. The version of JGit used to read NoteDb
-    // changes parses these subjects correctly, so we need to clean up old
-    // ReviewDb subjects before comparing.
-    int rn = s.indexOf("\r \r ");
-    if (rn >= 0) {
-      s = s.substring(0, rn);
-    }
-    return NoteDbUtil.sanitizeFooter(s);
-  }
-
-  private static String cleanNoteDbSubject(String s) {
-    return NoteDbUtil.sanitizeFooter(s);
-  }
-
-  /**
-   * Set of fields that must always exactly match between ReviewDb and NoteDb.
-   *
-   * <p>Used to limit the worst-case quadratic search when pairing off matching messages below.
-   */
-  @AutoValue
-  abstract static class ChangeMessageCandidate {
-    static ChangeMessageCandidate create(ChangeMessage cm) {
-      return new AutoValue_ChangeBundle_ChangeMessageCandidate(
-          cm.getAuthor(), cm.getMessage(), cm.getTag());
-    }
-
-    @Nullable
-    abstract Account.Id author();
-
-    @Nullable
-    abstract String message();
-
-    @Nullable
-    abstract String tag();
-
-    // Exclude:
-    //  - patch set, which may be null on ReviewDb side but not NoteDb
-    //  - UUID, which is always different between ReviewDb and NoteDb
-    //  - writtenOn, which is fuzzy
-  }
-
-  private static void diffChangeMessages(
-      List<String> diffs, ChangeBundle bundleA, ChangeBundle bundleB) {
-    if (bundleA.source == REVIEW_DB && bundleB.source == REVIEW_DB) {
-      // Both came from ReviewDb: check all fields exactly.
-      Map<ChangeMessage.Key, ChangeMessage> as = changeMessageMap(bundleA.filterChangeMessages());
-      Map<ChangeMessage.Key, ChangeMessage> bs = changeMessageMap(bundleB.filterChangeMessages());
-
-      for (ChangeMessage.Key k : diffKeySets(diffs, as, bs)) {
-        ChangeMessage a = as.get(k);
-        ChangeMessage b = bs.get(k);
-        String desc = describe(k);
-        diffColumns(diffs, ChangeMessage.class, desc, bundleA, a, bundleB, b);
-      }
-      return;
-    }
-    Change.Id id = bundleA.getChange().getId();
-    checkArgument(id.equals(bundleB.getChange().getId()));
-
-    // Try to pair up matching ChangeMessages from each side, and succeed only
-    // if both collections are empty at the end. Quadratic in the worst case,
-    // but easy to reason about.
-    List<ChangeMessage> as = new LinkedList<>(bundleA.filterChangeMessages());
-
-    ListMultimap<ChangeMessageCandidate, ChangeMessage> bs = LinkedListMultimap.create();
-    for (ChangeMessage b : bundleB.filterChangeMessages()) {
-      bs.put(ChangeMessageCandidate.create(b), b);
-    }
-
-    Iterator<ChangeMessage> ait = as.iterator();
-    A:
-    while (ait.hasNext()) {
-      ChangeMessage a = ait.next();
-      Iterator<ChangeMessage> bit = bs.get(ChangeMessageCandidate.create(a)).iterator();
-      while (bit.hasNext()) {
-        ChangeMessage b = bit.next();
-        if (changeMessagesMatch(bundleA, a, bundleB, b)) {
-          ait.remove();
-          bit.remove();
-          continue A;
-        }
-      }
-    }
-
-    if (as.isEmpty() && bs.isEmpty()) {
-      return;
-    }
-    StringBuilder sb =
-        new StringBuilder("ChangeMessages differ for Change.Id ").append(id).append('\n');
-    if (!as.isEmpty()) {
-      sb.append("Only in A:");
-      for (ChangeMessage cm : as) {
-        sb.append("\n  ").append(cm);
-      }
-      if (!bs.isEmpty()) {
-        sb.append('\n');
-      }
-    }
-    if (!bs.isEmpty()) {
-      sb.append("Only in B:");
-      bs.values().stream()
-          .sorted(CHANGE_MESSAGE_COMPARATOR)
-          .forEach(cm -> sb.append("\n  ").append(cm));
-    }
-    diffs.add(sb.toString());
-  }
-
-  private static boolean changeMessagesMatch(
-      ChangeBundle bundleA, ChangeMessage a, ChangeBundle bundleB, ChangeMessage b) {
-    List<String> tempDiffs = new ArrayList<>();
-    String temp = "temp";
-
-    // ReviewDb allows timestamps before patch set was created, but NoteDb
-    // truncates this to the patch set creation timestamp.
-    Timestamp ta = a.getWrittenOn();
-    Timestamp tb = b.getWrittenOn();
-    PatchSet psa = bundleA.patchSets.get(a.getPatchSetId());
-    PatchSet psb = bundleB.patchSets.get(b.getPatchSetId());
-    boolean excludePatchSet = false;
-    boolean excludeWrittenOn = false;
-    if (bundleA.source == REVIEW_DB && bundleB.source == NOTE_DB) {
-      excludePatchSet = a.getPatchSetId() == null;
-      excludeWrittenOn =
-          psa != null
-              && psb != null
-              && ta.before(psa.getCreatedOn())
-              && tb.equals(psb.getCreatedOn());
-    } else if (bundleA.source == NOTE_DB && bundleB.source == REVIEW_DB) {
-      excludePatchSet = b.getPatchSetId() == null;
-      excludeWrittenOn =
-          psa != null
-              && psb != null
-              && tb.before(psb.getCreatedOn())
-              && ta.equals(psa.getCreatedOn());
-    }
-
-    List<String> exclude = Lists.newArrayList("key");
-    if (excludePatchSet) {
-      exclude.add("patchset");
-    }
-    if (excludeWrittenOn) {
-      exclude.add("writtenOn");
-    }
-
-    diffColumnsExcluding(tempDiffs, ChangeMessage.class, temp, bundleA, a, bundleB, b, exclude);
-    return tempDiffs.isEmpty();
-  }
-
-  private static void diffPatchSets(
-      List<String> diffs, ChangeBundle bundleA, ChangeBundle bundleB) {
-    Map<PatchSet.Id, PatchSet> as = bundleA.patchSets;
-    Map<PatchSet.Id, PatchSet> bs = bundleB.patchSets;
-    Optional<PatchSet.Id> minA = as.keySet().stream().min(intKeyOrdering());
-    Optional<PatchSet.Id> minB = bs.keySet().stream().min(intKeyOrdering());
-    Set<PatchSet.Id> ids = diffKeySets(diffs, as, bs);
-
-    // Old versions of Gerrit had a bug that created patch sets during
-    // rebase or submission with a createdOn timestamp earlier than the patch
-    // set it was replacing. (In the cases I examined, it was equal to createdOn
-    // for the change, but we're not counting on this exact behavior.)
-    //
-    // ChangeRebuilder ensures patch set events come out in order, but it's hard
-    // to predict what the resulting timestamps would look like. So, completely
-    // ignore the createdOn timestamps if both:
-    //   * ReviewDb timestamps are non-monotonic.
-    //   * NoteDb timestamps are monotonic.
-    //
-    // Allow the timestamp of the first patch set to match the creation time of
-    // the change.
-    boolean excludeAllCreatedOn = false;
-    if (bundleA.source == REVIEW_DB && bundleB.source == NOTE_DB) {
-      excludeAllCreatedOn = !createdOnIsMonotonic(as, ids) && createdOnIsMonotonic(bs, ids);
-    } else if (bundleA.source == NOTE_DB && bundleB.source == REVIEW_DB) {
-      excludeAllCreatedOn = createdOnIsMonotonic(as, ids) && !createdOnIsMonotonic(bs, ids);
-    }
-
-    for (PatchSet.Id id : ids) {
-      PatchSet a = as.get(id);
-      PatchSet b = bs.get(id);
-      String desc = describe(id);
-      String pushCertField = "pushCertificate";
-
-      boolean excludeCreatedOn = excludeAllCreatedOn;
-      boolean excludeDesc = false;
-      if (bundleA.source == REVIEW_DB && bundleB.source == NOTE_DB) {
-        excludeDesc = Objects.equals(trimOrNull(a.getDescription()), b.getDescription());
-        excludeCreatedOn |=
-            Optional.of(id).equals(minB) && b.getCreatedOn().equals(bundleB.change.getCreatedOn());
-      } else if (bundleA.source == NOTE_DB && bundleB.source == REVIEW_DB) {
-        excludeDesc = Objects.equals(a.getDescription(), trimOrNull(b.getDescription()));
-        excludeCreatedOn |=
-            Optional.of(id).equals(minA) && a.getCreatedOn().equals(bundleA.change.getCreatedOn());
-      }
-
-      List<String> exclude = Lists.newArrayList(pushCertField);
-      if (excludeCreatedOn) {
-        exclude.add("createdOn");
-      }
-      if (excludeDesc) {
-        exclude.add("description");
-      }
-
-      diffColumnsExcluding(diffs, PatchSet.class, desc, bundleA, a, bundleB, b, exclude);
-      diffValues(diffs, desc, trimPushCert(a), trimPushCert(b), pushCertField);
-    }
-  }
-
-  private static String trimPushCert(PatchSet ps) {
-    if (ps.getPushCertificate() == null) {
-      return null;
-    }
-    return CharMatcher.is('\n').trimTrailingFrom(ps.getPushCertificate());
-  }
-
-  private static boolean createdOnIsMonotonic(
-      Map<?, PatchSet> patchSets, Set<PatchSet.Id> limitToIds) {
-    List<PatchSet> orderedById =
-        patchSets.values().stream()
-            .filter(ps -> limitToIds.contains(ps.getId()))
-            .sorted(ChangeUtil.PS_ID_ORDER)
-            .collect(toList());
-    return Ordering.natural().onResultOf(PatchSet::getCreatedOn).isOrdered(orderedById);
-  }
-
-  private static void diffPatchSetApprovals(
-      List<String> diffs, ChangeBundle bundleA, ChangeBundle bundleB) {
-    Map<PatchSetApproval.Key, PatchSetApproval> as = bundleA.filterPatchSetApprovals();
-    Map<PatchSetApproval.Key, PatchSetApproval> bs = bundleB.filterPatchSetApprovals();
-    for (PatchSetApproval.Key k : diffKeySets(diffs, as, bs)) {
-      PatchSetApproval a = as.get(k);
-      PatchSetApproval b = bs.get(k);
-      String desc = describe(k);
-
-      // ReviewDb allows timestamps before patch set was created, but NoteDb
-      // truncates this to the patch set creation timestamp.
-      //
-      // ChangeRebuilder ensures all post-submit approvals happen after the
-      // actual submit, so the timestamps may not line up. This shouldn't really
-      // happen, because postSubmit shouldn't be set in ReviewDb until after the
-      // change is submitted in ReviewDb, but you never know.
-      //
-      // Due to a quirk of PostReview, post-submit 0 votes might not have the
-      // postSubmit bit set in ReviewDb. As these are only used for tombstone
-      // purposes, ignore the postSubmit bit in NoteDb in this case.
-      Timestamp ta = a.getGranted();
-      Timestamp tb = b.getGranted();
-      PatchSet psa = requireNonNull(bundleA.patchSets.get(a.getPatchSetId()));
-      PatchSet psb = requireNonNull(bundleB.patchSets.get(b.getPatchSetId()));
-      boolean excludeGranted = false;
-      boolean excludePostSubmit = false;
-      List<String> exclude = new ArrayList<>(1);
-      if (bundleA.source == REVIEW_DB && bundleB.source == NOTE_DB) {
-        excludeGranted =
-            (ta.before(psa.getCreatedOn()) && tb.equals(psb.getCreatedOn()))
-                || ta.compareTo(tb) < 0;
-        excludePostSubmit = a.getValue() == 0 && b.isPostSubmit();
-      } else if (bundleA.source == NOTE_DB && bundleB.source == REVIEW_DB) {
-        excludeGranted =
-            (tb.before(psb.getCreatedOn()) && ta.equals(psa.getCreatedOn()))
-                || (tb.compareTo(ta) < 0);
-        excludePostSubmit = b.getValue() == 0 && a.isPostSubmit();
-      }
-
-      // Legacy submit approvals may or may not have tags associated with them,
-      // depending on whether ChangeRebuilder happened to group them with the
-      // status change.
-      boolean excludeTag =
-          bundleA.source != bundleB.source && a.isLegacySubmit() && b.isLegacySubmit();
-
-      if (excludeGranted) {
-        exclude.add("granted");
-      }
-      if (excludePostSubmit) {
-        exclude.add("postSubmit");
-      }
-      if (excludeTag) {
-        exclude.add("tag");
-      }
-
-      diffColumnsExcluding(diffs, PatchSetApproval.class, desc, bundleA, a, bundleB, b, exclude);
-    }
-  }
-
-  private static void diffReviewers(
-      List<String> diffs, ChangeBundle bundleA, ChangeBundle bundleB) {
-    diffSets(diffs, bundleA.reviewers.all(), bundleB.reviewers.all(), "reviewer");
-  }
-
-  private static void diffPatchLineComments(
-      List<String> diffs, ChangeBundle bundleA, ChangeBundle bundleB) {
-    Map<PatchLineComment.Key, PatchLineComment> as = bundleA.filterPatchLineComments();
-    Map<PatchLineComment.Key, PatchLineComment> bs = bundleB.filterPatchLineComments();
-    for (PatchLineComment.Key k : diffKeySets(diffs, as, bs)) {
-      PatchLineComment a = as.get(k);
-      PatchLineComment b = bs.get(k);
-      String desc = describe(k);
-      diffColumns(diffs, PatchLineComment.class, desc, bundleA, a, bundleB, b);
-    }
-  }
-
-  private static <T> Set<T> diffKeySets(List<String> diffs, Map<T, ?> a, Map<T, ?> b) {
-    if (a.isEmpty() && b.isEmpty()) {
-      return a.keySet();
-    }
-    String clazz = keyClass((!a.isEmpty() ? a.keySet() : b.keySet()).iterator().next());
-    return diffSets(diffs, a.keySet(), b.keySet(), clazz);
-  }
-
-  private static <T> Set<T> diffSets(List<String> diffs, Set<T> as, Set<T> bs, String desc) {
-    if (as.isEmpty() && bs.isEmpty()) {
-      return as;
-    }
-
-    Set<T> aNotB = Sets.difference(as, bs);
-    Set<T> bNotA = Sets.difference(bs, as);
-    if (aNotB.isEmpty() && bNotA.isEmpty()) {
-      return as;
-    }
-    diffs.add(desc + " sets differ: " + aNotB + " only in A; " + bNotA + " only in B");
-    return Sets.intersection(as, bs);
-  }
-
-  private static <T> void diffColumns(
-      List<String> diffs,
-      Class<T> clazz,
-      String desc,
-      ChangeBundle bundleA,
-      T a,
-      ChangeBundle bundleB,
-      T b) {
-    diffColumnsExcluding(diffs, clazz, desc, bundleA, a, bundleB, b);
-  }
-
-  private static <T> void diffColumnsExcluding(
-      List<String> diffs,
-      Class<T> clazz,
-      String desc,
-      ChangeBundle bundleA,
-      T a,
-      ChangeBundle bundleB,
-      T b,
-      String... exclude) {
-    diffColumnsExcluding(diffs, clazz, desc, bundleA, a, bundleB, b, Arrays.asList(exclude));
-  }
-
-  private static <T> void diffColumnsExcluding(
-      List<String> diffs,
-      Class<T> clazz,
-      String desc,
-      ChangeBundle bundleA,
-      T a,
-      ChangeBundle bundleB,
-      T b,
-      Iterable<String> exclude) {
-    Set<String> toExclude = Sets.newLinkedHashSet(exclude);
-    for (Field f : clazz.getDeclaredFields()) {
-      Column col = f.getAnnotation(Column.class);
-      if (col == null) {
-        continue;
-      } else if (toExclude.remove(f.getName())) {
-        continue;
-      }
-      f.setAccessible(true);
-      try {
-        if (Timestamp.class.isAssignableFrom(f.getType())) {
-          diffTimestamps(diffs, desc, bundleA, a, bundleB, b, f.getName());
-        } else {
-          diffValues(diffs, desc, f.get(a), f.get(b), f.getName());
-        }
-      } catch (IllegalAccessException e) {
-        throw new IllegalArgumentException(e);
-      }
-    }
-    checkArgument(
-        toExclude.isEmpty(),
-        "requested columns to exclude not present in %s: %s",
-        clazz.getSimpleName(),
-        toExclude);
-  }
-
-  private static void diffTimestamps(
-      List<String> diffs,
-      String desc,
-      ChangeBundle bundleA,
-      Object a,
-      ChangeBundle bundleB,
-      Object b,
-      String field) {
-    checkArgument(a.getClass() == b.getClass());
-    Class<?> clazz = a.getClass();
-
-    Timestamp ta;
-    Timestamp tb;
-    try {
-      Field f = clazz.getDeclaredField(field);
-      checkArgument(f.getAnnotation(Column.class) != null);
-      f.setAccessible(true);
-      ta = (Timestamp) f.get(a);
-      tb = (Timestamp) f.get(b);
-    } catch (IllegalAccessException | NoSuchFieldException | SecurityException e) {
-      throw new IllegalArgumentException(e);
-    }
-    diffTimestamps(diffs, desc, bundleA, ta, bundleB, tb, field);
-  }
-
-  private static void diffTimestamps(
-      List<String> diffs,
-      String desc,
-      ChangeBundle bundleA,
-      Timestamp ta,
-      ChangeBundle bundleB,
-      Timestamp tb,
-      String fieldDesc) {
-    if (bundleA.source == bundleB.source || ta == null || tb == null) {
-      diffValues(diffs, desc, ta, tb, fieldDesc);
-    } else if (bundleA.source == NOTE_DB) {
-      diffTimestamps(diffs, desc, bundleA.getChange(), ta, bundleB.getChange(), tb, fieldDesc);
-    } else {
-      diffTimestamps(diffs, desc, bundleB.getChange(), tb, bundleA.getChange(), ta, fieldDesc);
-    }
-  }
-
-  private static boolean timestampsDiffer(
-      ChangeBundle bundleA, Timestamp ta, ChangeBundle bundleB, Timestamp tb) {
-    List<String> tempDiffs = new ArrayList<>(1);
-    diffTimestamps(tempDiffs, "temp", bundleA, ta, bundleB, tb, "temp");
-    return !tempDiffs.isEmpty();
-  }
-
-  private static void diffTimestamps(
-      List<String> diffs,
-      String desc,
-      Change changeFromNoteDb,
-      Timestamp tsFromNoteDb,
-      Change changeFromReviewDb,
-      Timestamp tsFromReviewDb,
-      String field) {
-    // Because ChangeRebuilder may batch events together that are several
-    // seconds apart, the timestamp in NoteDb may actually be several seconds
-    // *earlier* than the timestamp in ReviewDb that it was converted from.
-    checkArgument(
-        tsFromNoteDb.equals(truncateToSecond(tsFromNoteDb)),
-        "%s from NoteDb has non-rounded %s timestamp: %s",
-        desc,
-        field,
-        tsFromNoteDb);
-
-    if (tsFromReviewDb.before(changeFromReviewDb.getCreatedOn())
-        && tsFromNoteDb.equals(changeFromNoteDb.getCreatedOn())) {
-      // Timestamp predates change creation. These are truncated to change
-      // creation time during NoteDb conversion, so allow this if the timestamp
-      // in NoteDb matches the createdOn time in NoteDb.
-      return;
-    }
-
-    long delta = tsFromReviewDb.getTime() - tsFromNoteDb.getTime();
-    long max = ChangeRebuilderImpl.MAX_WINDOW_MS;
-    if (delta < 0 || delta > max) {
-      diffs.add(
-          field
-              + " differs for "
-              + desc
-              + " in NoteDb vs. ReviewDb:"
-              + " {"
-              + tsFromNoteDb
-              + "} != {"
-              + tsFromReviewDb
-              + "}");
-    }
-  }
-
-  private static void diffValues(
-      List<String> diffs, String desc, Object va, Object vb, String name) {
-    if (!Objects.equals(va, vb)) {
-      diffs.add(name + " differs for " + desc + ": {" + va + "} != {" + vb + "}");
-    }
-  }
-
-  private static String describe(Object key) {
-    return keyClass(key) + " " + key;
-  }
-
-  private static String keyClass(Object obj) {
-    Class<?> clazz = obj.getClass();
-    String name = clazz.getSimpleName();
-    checkArgument(name.endsWith("Key") || name.endsWith("Id"), "not an Id/Key class: %s", name);
-    if (name.equals("Key") || name.equals("Id")) {
-      return clazz.getEnclosingClass().getSimpleName() + "." + name;
-    } else if (name.startsWith("AutoValue_")) {
-      return name.substring(name.lastIndexOf('_') + 1);
-    }
-    return name;
-  }
-
-  @Override
-  public String toString() {
-    return getClass().getSimpleName()
-        + "{id="
-        + change.getId()
-        + ", ChangeMessage["
-        + changeMessages.size()
-        + "]"
-        + ", PatchSet["
-        + patchSets.size()
-        + "]"
-        + ", PatchSetApproval["
-        + patchSetApprovals.size()
-        + "]"
-        + ", PatchLineComment["
-        + patchLineComments.size()
-        + "]"
-        + "}";
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/ChangeBundleReader.java b/java/com/google/gerrit/server/notedb/ChangeBundleReader.java
deleted file mode 100644
index 3207c3b..0000000
--- a/java/com/google/gerrit/server/notedb/ChangeBundleReader.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb;
-
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.OrmException;
-
-public interface ChangeBundleReader {
-  @Nullable
-  ChangeBundle fromReviewDb(ReviewDb db, Change.Id id) throws OrmException;
-}
diff --git a/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java b/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
index d8390b7..2eb6a3c 100644
--- a/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
@@ -19,6 +19,7 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.Sets;
+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;
@@ -28,8 +29,6 @@
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 import java.io.IOException;
@@ -42,7 +41,6 @@
 import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 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;
@@ -92,9 +90,7 @@
 
   @AssistedInject
   private ChangeDraftUpdate(
-      @GerritServerConfig Config cfg,
       @GerritPersonIdent PersonIdent serverIdent,
-      NotesMigration migration,
       AllUsersName allUsers,
       ChangeNoteUtil noteUtil,
       @Assisted ChangeNotes notes,
@@ -102,25 +98,13 @@
       @Assisted("real") Account.Id realAccountId,
       @Assisted PersonIdent authorIdent,
       @Assisted Date when) {
-    super(
-        cfg,
-        migration,
-        noteUtil,
-        serverIdent,
-        notes,
-        null,
-        accountId,
-        realAccountId,
-        authorIdent,
-        when);
+    super(noteUtil, serverIdent, notes, null, accountId, realAccountId, authorIdent, when);
     this.draftsProject = allUsers;
   }
 
   @AssistedInject
   private ChangeDraftUpdate(
-      @GerritServerConfig Config cfg,
       @GerritPersonIdent PersonIdent serverIdent,
-      NotesMigration migration,
       AllUsersName allUsers,
       ChangeNoteUtil noteUtil,
       @Assisted Change change,
@@ -128,17 +112,7 @@
       @Assisted("real") Account.Id realAccountId,
       @Assisted PersonIdent authorIdent,
       @Assisted Date when) {
-    super(
-        cfg,
-        migration,
-        noteUtil,
-        serverIdent,
-        null,
-        change,
-        accountId,
-        realAccountId,
-        authorIdent,
-        when);
+    super(noteUtil, serverIdent, null, change, accountId, realAccountId, authorIdent, when);
     this.draftsProject = allUsers;
   }
 
@@ -158,7 +132,7 @@
 
   private CommitBuilder storeCommentsInNotes(
       RevWalk rw, ObjectInserter ins, ObjectId curr, CommitBuilder cb)
-      throws ConfigInvalidException, OrmException, IOException {
+      throws ConfigInvalidException, IOException {
     RevisionNoteMap<ChangeRevisionNote> rnm = getRevisionNoteMap(rw, curr);
     Set<RevId> updatedRevs = Sets.newHashSetWithExpectedSize(rnm.revisionNotes.size());
     RevisionNoteBuilder.Cache cache = new RevisionNoteBuilder.Cache(rnm);
@@ -207,20 +181,17 @@
   }
 
   private RevisionNoteMap<ChangeRevisionNote> getRevisionNoteMap(RevWalk rw, ObjectId curr)
-      throws ConfigInvalidException, OrmException, IOException {
-    if (migration.readChanges()) {
-      // If reading from changes is enabled, then the old DraftCommentNotes
-      // already parsed the revision notes. We can reuse them as long as the ref
-      // hasn't advanced.
-      ChangeNotes changeNotes = getNotes();
-      if (changeNotes != null) {
-        DraftCommentNotes draftNotes = changeNotes.load().getDraftCommentNotes();
-        if (draftNotes != null) {
-          ObjectId idFromNotes = firstNonNull(draftNotes.getRevision(), ObjectId.zeroId());
-          RevisionNoteMap<ChangeRevisionNote> rnm = draftNotes.getRevisionNoteMap();
-          if (idFromNotes.equals(curr) && rnm != null) {
-            return rnm;
-          }
+      throws ConfigInvalidException, IOException {
+    // The old DraftCommentNotes already parsed the revision notes. We can reuse them as long as
+    // the ref hasn't advanced.
+    ChangeNotes changeNotes = getNotes();
+    if (changeNotes != null) {
+      DraftCommentNotes draftNotes = changeNotes.load().getDraftCommentNotes();
+      if (draftNotes != null) {
+        ObjectId idFromNotes = firstNonNull(draftNotes.getRevision(), ObjectId.zeroId());
+        RevisionNoteMap<ChangeRevisionNote> rnm = draftNotes.getRevisionNoteMap();
+        if (idFromNotes.equals(curr) && rnm != null) {
+          return rnm;
         }
       }
     }
@@ -243,13 +214,13 @@
 
   @Override
   protected CommitBuilder applyImpl(RevWalk rw, ObjectInserter ins, ObjectId curr)
-      throws OrmException, IOException {
+      throws IOException {
     CommitBuilder cb = new CommitBuilder();
     cb.setMessage("Update draft comments");
     try {
       return storeCommentsInNotes(rw, ins, curr, cb);
     } catch (ConfigInvalidException e) {
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
   }
 
diff --git a/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java b/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
index 8a0cabe..628dfd2 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
@@ -39,7 +39,6 @@
   public static final FooterKey FOOTER_PATCH_SET_DESCRIPTION =
       new FooterKey("Patch-set-description");
   public static final FooterKey FOOTER_PRIVATE = new FooterKey("Private");
-  public static final FooterKey FOOTER_READ_ONLY_UNTIL = new FooterKey("Read-only-until");
   public static final FooterKey FOOTER_REAL_USER = new FooterKey("Real-user");
   public static final FooterKey FOOTER_STATUS = new FooterKey("Status");
   public static final FooterKey FOOTER_SUBJECT = new FooterKey("Subject");
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotes.java b/java/com/google/gerrit/server/notedb/ChangeNotes.java
index 086b2e2..764d41d 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -17,7 +17,6 @@
 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.server.notedb.NoteDbTable.CHANGES;
 import static java.util.Comparator.comparing;
 import static java.util.Objects.requireNonNull;
 
@@ -28,18 +27,16 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSortedMap;
 import com.google.common.collect.ImmutableSortedSet;
-import com.google.common.collect.Iterators;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.Multimaps;
 import com.google.common.collect.Ordering;
 import com.google.common.collect.Sets;
 import com.google.common.collect.Sets.SetView;
-import com.google.common.collect.Streams;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.SubmitRecord;
-import com.google.gerrit.metrics.Timer1;
+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;
@@ -51,33 +48,24 @@
 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.reviewdb.server.ReviewDb;
-import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.server.ReviewerByEmailSet;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.ReviewerStatusUpdate;
 import com.google.gerrit.server.git.RefCache;
-import com.google.gerrit.server.git.RepoRefCache;
-import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
-import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder;
 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;
-import com.google.gwtorm.server.OrmException;
 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.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.Set;
-import java.util.concurrent.TimeUnit;
 import java.util.function.Predicate;
 import java.util.stream.Stream;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -100,11 +88,6 @@
     return new ConfigInvalidException("Change " + changeId + ": " + String.format(fmt, args));
   }
 
-  @Nullable
-  public static Change readOneReviewDbChange(ReviewDb db, Change.Id id) throws OrmException {
-    return ReviewDbUtil.unwrapDb(db).changes().get(id);
-  }
-
   @Singleton
   public static class Factory {
     private final Args args;
@@ -120,27 +103,16 @@
       this.projectCache = projectCache;
     }
 
-    public ChangeNotes createChecked(ReviewDb db, Change c) throws OrmException {
-      return createChecked(db, c.getProject(), c.getId());
+    public ChangeNotes createChecked(Change c) {
+      return createChecked(c.getProject(), c.getId());
     }
 
-    public ChangeNotes createChecked(ReviewDb db, Project.NameKey project, Change.Id changeId)
-        throws OrmException {
-      Change change = readOneReviewDbChange(db, changeId);
-      if (change == null) {
-        if (!args.migration.readChanges()) {
-          throw new NoSuchChangeException(changeId);
-        }
-        // Change isn't in ReviewDb, but its primary storage might be in NoteDb.
-        // Prepopulate the change exists with proper noteDbState field.
-        change = newNoteDbOnlyChange(project, changeId);
-      } else if (!change.getProject().equals(project)) {
-        throw new NoSuchChangeException(changeId);
-      }
-      return new ChangeNotes(args, change).load();
+    public ChangeNotes createChecked(Project.NameKey project, Change.Id changeId) {
+      Change change = newChange(project, changeId);
+      return new ChangeNotes(args, change, true, null).load();
     }
 
-    public ChangeNotes createChecked(Change.Id changeId) throws OrmException {
+    public ChangeNotes createChecked(Change.Id changeId) {
       InternalChangeQuery query = queryProvider.get().noFields();
       List<ChangeData> changes = query.byLegacyChangeId(changeId);
       if (changes.isEmpty()) {
@@ -153,43 +125,14 @@
       return changes.get(0).notes();
     }
 
-    public static Change newNoteDbOnlyChange(Project.NameKey project, Change.Id changeId) {
-      Change change =
-          new Change(
-              null, changeId, null, new Branch.NameKey(project, "INVALID_NOTE_DB_ONLY"), null);
-      change.setNoteDbState(NoteDbChangeState.NOTE_DB_PRIMARY_STATE);
-      return change;
+    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);
     }
 
-    private Change loadChangeFromDb(ReviewDb db, Project.NameKey project, Change.Id changeId)
-        throws OrmException {
+    public ChangeNotes create(Project.NameKey project, Change.Id changeId) {
       checkArgument(project != null, "project is required");
-      Change change = readOneReviewDbChange(db, changeId);
-
-      if (change == null) {
-        if (args.migration.readChanges()) {
-          return newNoteDbOnlyChange(project, changeId);
-        }
-        throw new NoSuchChangeException(changeId);
-      }
-      checkArgument(
-          change.getProject().equals(project),
-          "passed project %s when creating ChangeNotes for %s, but actual project is %s",
-          project,
-          changeId,
-          change.getProject());
-      return change;
-    }
-
-    public ChangeNotes create(ReviewDb db, Project.NameKey project, Change.Id changeId)
-        throws OrmException {
-      return new ChangeNotes(args, loadChangeFromDb(db, project, changeId)).load();
-    }
-
-    public ChangeNotes createWithAutoRebuildingDisabled(
-        ReviewDb db, Project.NameKey project, Change.Id changeId) throws OrmException {
-      return new ChangeNotes(args, loadChangeFromDb(db, project, changeId), true, false, null)
-          .load();
+      return new ChangeNotes(args, newChange(project, changeId), true, null).load();
     }
 
     /**
@@ -200,200 +143,101 @@
      * @return change notes
      */
     public ChangeNotes createFromIndexedChange(Change change) {
-      return new ChangeNotes(args, change);
+      return new ChangeNotes(args, change, true, null);
     }
 
-    public ChangeNotes createForBatchUpdate(Change change, boolean shouldExist)
-        throws OrmException {
-      return new ChangeNotes(args, change, shouldExist, false, null).load();
+    public ChangeNotes createForBatchUpdate(Change change, boolean shouldExist) {
+      return new ChangeNotes(args, change, shouldExist, null).load();
     }
 
-    public ChangeNotes createWithAutoRebuildingDisabled(Change change, RefCache refs)
-        throws OrmException {
-      return new ChangeNotes(args, change, true, false, refs).load();
+    public ChangeNotes create(Change change, RefCache refs) {
+      return new ChangeNotes(args, change, true, refs).load();
     }
 
-    // TODO(ekempin): Remove when database backend is deleted
-    /**
-     * Instantiate ChangeNotes for a change that has been loaded by a batch read from the database.
-     */
-    private ChangeNotes createFromChangeOnlyWhenNoteDbDisabled(Change change) throws OrmException {
-      checkState(
-          !args.migration.readChanges(),
-          "do not call createFromChangeWhenNoteDbDisabled when NoteDb is enabled");
-      return new ChangeNotes(args, change).load();
-    }
-
-    public List<ChangeNotes> create(ReviewDb db, Collection<Change.Id> changeIds)
-        throws OrmException {
+    public List<ChangeNotes> create(Collection<Change.Id> changeIds) {
       List<ChangeNotes> notes = new ArrayList<>();
-      if (args.migration.readChanges()) {
-        for (Change.Id changeId : changeIds) {
-          try {
-            notes.add(createChecked(changeId));
-          } catch (NoSuchChangeException e) {
-            // Ignore missing changes to match Access#get(Iterable) behavior.
-          }
+      for (Change.Id changeId : changeIds) {
+        try {
+          notes.add(createChecked(changeId));
+        } catch (NoSuchChangeException e) {
+          // Ignore missing changes to match Access#get(Iterable) behavior.
         }
-        return notes;
-      }
-
-      for (Change c : ReviewDbUtil.unwrapDb(db).changes().get(changeIds)) {
-        notes.add(createFromChangeOnlyWhenNoteDbDisabled(c));
       }
       return notes;
     }
 
     public List<ChangeNotes> create(
-        ReviewDb db,
         Project.NameKey project,
         Collection<Change.Id> changeIds,
-        Predicate<ChangeNotes> predicate)
-        throws OrmException {
+        Predicate<ChangeNotes> predicate) {
       List<ChangeNotes> notes = new ArrayList<>();
-      if (args.migration.readChanges()) {
-        for (Change.Id cid : changeIds) {
-          try {
-            ChangeNotes cn = create(db, project, cid);
-            if (cn.getChange() != null && predicate.test(cn)) {
-              notes.add(cn);
-            }
-          } catch (NoSuchChangeException e) {
-            // Match ReviewDb behavior, returning not found; maybe the caller learned about it from
-            // a dangling patch set ref or something.
-            continue;
-          }
-        }
-        return notes;
-      }
-
-      for (Change c : ReviewDbUtil.unwrapDb(db).changes().get(changeIds)) {
-        if (c != null && project.equals(c.getDest().getParentKey())) {
-          ChangeNotes cn = createFromChangeOnlyWhenNoteDbDisabled(c);
-          if (predicate.test(cn)) {
+      for (Change.Id cid : changeIds) {
+        try {
+          ChangeNotes cn = create(project, cid);
+          if (cn.getChange() != null && predicate.test(cn)) {
             notes.add(cn);
           }
+        } catch (NoSuchChangeException e) {
+          // Match ReviewDb behavior, returning not found; maybe the caller learned about it from
+          // a dangling patch set ref or something.
+          continue;
         }
       }
       return notes;
     }
 
-    public ListMultimap<Project.NameKey, ChangeNotes> create(
-        ReviewDb db, Predicate<ChangeNotes> predicate) throws IOException, OrmException {
+    public ListMultimap<Project.NameKey, ChangeNotes> create(Predicate<ChangeNotes> predicate)
+        throws IOException {
       ListMultimap<Project.NameKey, ChangeNotes> m =
           MultimapBuilder.hashKeys().arrayListValues().build();
-      if (args.migration.readChanges()) {
-        for (Project.NameKey project : projectCache.all()) {
-          try (Repository repo = args.repoManager.openRepository(project)) {
-            scanNoteDb(repo, db, project)
-                .filter(r -> !r.error().isPresent())
-                .map(ChangeNotesResult::notes)
-                .filter(predicate)
-                .forEach(n -> m.put(n.getProjectName(), n));
-          }
-        }
-      } else {
-        for (Change change : ReviewDbUtil.unwrapDb(db).changes().all()) {
-          ChangeNotes notes = createFromChangeOnlyWhenNoteDbDisabled(change);
-          if (predicate.test(notes)) {
-            m.put(change.getProject(), notes);
-          }
+      for (Project.NameKey project : projectCache.all()) {
+        try (Repository repo = args.repoManager.openRepository(project)) {
+          scan(repo, project)
+              .filter(r -> !r.error().isPresent())
+              .map(ChangeNotesResult::notes)
+              .filter(predicate)
+              .forEach(n -> m.put(n.getProjectName(), n));
         }
       }
       return ImmutableListMultimap.copyOf(m);
     }
 
-    public Stream<ChangeNotesResult> scan(Repository repo, ReviewDb db, Project.NameKey project)
+    public Stream<ChangeNotesResult> scan(Repository repo, Project.NameKey project)
         throws IOException {
-      return args.migration.readChanges() ? scanNoteDb(repo, db, project) : scanReviewDb(repo, db);
-    }
-
-    private Stream<ChangeNotesResult> scanReviewDb(Repository repo, ReviewDb db)
-        throws IOException {
-      // Scan IDs that might exist in ReviewDb, assuming that each change has at least one patch set
-      // ref. Not all changes might exist: some patch set refs might have been written where the
-      // corresponding ReviewDb write failed. These will be silently filtered out by the batch get
-      // call below, which is intended.
-      Set<Change.Id> ids = scanChangeIds(repo).fromPatchSetRefs();
-
-      // A batch size of N may overload get(Iterable), so use something smaller, but still >1.
-      return Streams.stream(Iterators.partition(ids.iterator(), 30))
-          .flatMap(
-              batch -> {
-                try {
-                  return Streams.stream(ReviewDbUtil.unwrapDb(db).changes().get(batch))
-                      .map(this::toResult)
-                      .filter(Objects::nonNull);
-                } catch (OrmException e) {
-                  // Return this error for each Id in the input batch.
-                  return batch.stream().map(id -> ChangeNotesResult.error(id, e));
-                }
-              });
-    }
-
-    private Stream<ChangeNotesResult> scanNoteDb(
-        Repository repo, ReviewDb db, Project.NameKey project) throws IOException {
       ScanResult sr = scanChangeIds(repo);
-      PrimaryStorage defaultStorage = args.migration.changePrimaryStorage();
 
-      return sr.all().stream()
-          .map(id -> scanOneNoteDbChange(db, project, sr, defaultStorage, id))
-          .filter(Objects::nonNull);
+      return sr.all().stream().map(id -> scanOneChange(project, sr, id)).filter(Objects::nonNull);
     }
 
-    private ChangeNotesResult scanOneNoteDbChange(
-        ReviewDb db,
-        Project.NameKey project,
-        ScanResult sr,
-        PrimaryStorage defaultStorage,
-        Change.Id id) {
-      Change change;
-      try {
-        change = readOneReviewDbChange(db, id);
-      } catch (OrmException e) {
-        return ChangeNotesResult.error(id, e);
-      }
-
-      if (change == null) {
-        if (!sr.fromMetaRefs().contains(id)) {
-          // Stray patch set refs can happen due to normal error conditions, e.g. failed
-          // push processing, so aren't worth even a warning.
-          return null;
-        }
-        if (defaultStorage == PrimaryStorage.REVIEW_DB) {
-          // If changes should exist in ReviewDb, it's worth warning about a meta ref with
-          // no corresponding ReviewDb data.
-          logger.atWarning().log(
-              "skipping change %s found in project %s but not in ReviewDb", id, project);
-          return null;
-        }
-        // TODO(dborowitz): See discussion in NoteDbBatchUpdate#newChangeContext.
-        change = ChangeNotes.Factory.newNoteDbOnlyChange(project, id);
-      } else if (!change.getProject().equals(project)) {
-        logger.atSevere().log(
-            "skipping change %s found in project %s because ReviewDb change has project %s",
-            id, project, change.getProject());
+    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
+        // push processing, so aren't worth even a warning.
         return null;
       }
+
+      // 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);
     }
 
     @Nullable
-    private ChangeNotesResult toResult(Change rawChangeFromReviewDbOrNoteDb) {
-      ChangeNotes n = new ChangeNotes(args, rawChangeFromReviewDbOrNoteDb);
+    private ChangeNotesResult toResult(Change rawChangeFromNoteDb) {
+      ChangeNotes n = new ChangeNotes(args, rawChangeFromNoteDb, true, null);
       try {
         n.load();
-      } catch (OrmException e) {
+      } catch (StorageException e) {
         return ChangeNotesResult.error(n.getChangeId(), e);
       }
       return ChangeNotesResult.notes(n);
     }
 
-    /** Result of {@link #scan(Repository, ReviewDb, Project.NameKey)}. */
+    /** Result of {@link #scan(Repository,Project.NameKey)}. */
     @AutoValue
     public abstract static class ChangeNotesResult {
-      static ChangeNotesResult error(Change.Id id, OrmException e) {
+      static ChangeNotesResult error(Change.Id id, StorageException e) {
         return new AutoValue_ChangeNotes_Factory_ChangeNotesResult(id, Optional.of(e), null);
       }
 
@@ -406,7 +250,7 @@
       public abstract Change.Id id();
 
       /** Error encountered while loading this change, if any. */
-      public abstract Optional<OrmException> error();
+      public abstract Optional<StorageException> error();
 
       /**
        * Notes loaded for this change.
@@ -458,7 +302,6 @@
   // notes easier.
   RevisionNoteMap<ChangeRevisionNote> revisionNoteMap;
 
-  private NoteDbUpdateManager.Result rebuildResult;
   private DraftCommentNotes draftCommentNotes;
   private RobotCommentNotes robotCommentNotes;
 
@@ -469,13 +312,8 @@
   private ImmutableSet<Comment.Key> commentKeys;
 
   @VisibleForTesting
-  public ChangeNotes(Args args, Change change) {
-    this(args, change, true, true, null);
-  }
-
-  private ChangeNotes(
-      Args args, Change change, boolean shouldExist, boolean autoRebuild, @Nullable RefCache refs) {
-    super(args, change.getId(), PrimaryStorage.of(change), autoRebuild);
+  public ChangeNotes(Args args, Change change, boolean shouldExist, @Nullable RefCache refs) {
+    super(args, change.getId());
     this.change = new Change(change);
     this.shouldExist = shouldExist;
     this.refs = refs;
@@ -580,13 +418,12 @@
     return commentKeys;
   }
 
-  public ImmutableListMultimap<RevId, Comment> getDraftComments(Account.Id author)
-      throws OrmException {
+  public ImmutableListMultimap<RevId, Comment> getDraftComments(Account.Id author) {
     return getDraftComments(author, null);
   }
 
   public ImmutableListMultimap<RevId, Comment> getDraftComments(
-      Account.Id author, @Nullable Ref ref) throws OrmException {
+      Account.Id author, @Nullable Ref ref) {
     loadDraftComments(author, ref);
     // Filter out any zombie draft comments. These are drafts that are also in
     // the published map, and arise when the update to All-Users to delete them
@@ -596,7 +433,7 @@
             draftCommentNotes.getComments(), e -> !getCommentKeys().contains(e.getValue().key)));
   }
 
-  public ImmutableListMultimap<RevId, RobotComment> getRobotComments() throws OrmException {
+  public ImmutableListMultimap<RevId, RobotComment> getRobotComments() {
     loadRobotComments();
     return robotCommentNotes.getComments();
   }
@@ -606,15 +443,14 @@
    * However, this method will load the comments if no draft comments have been loaded or if the
    * caller would like the drafts for another author.
    */
-  private void loadDraftComments(Account.Id author, @Nullable Ref ref) throws OrmException {
+  private void loadDraftComments(Account.Id author, @Nullable Ref ref) {
     if (draftCommentNotes == null || !author.equals(draftCommentNotes.getAuthor()) || ref != null) {
-      draftCommentNotes =
-          new DraftCommentNotes(args, change, author, autoRebuild, rebuildResult, ref);
+      draftCommentNotes = new DraftCommentNotes(args, getChangeId(), author, ref);
       draftCommentNotes.load();
     }
   }
 
-  private void loadRobotComments() throws OrmException {
+  private void loadRobotComments() {
     if (robotCommentNotes == null) {
       robotCommentNotes = new RobotCommentNotes(args, change);
       robotCommentNotes.load();
@@ -630,7 +466,7 @@
     return robotCommentNotes;
   }
 
-  public boolean containsComment(Comment c) throws OrmException {
+  public boolean containsComment(Comment c) {
     if (containsCommentPublished(c)) {
       return true;
     }
@@ -658,19 +494,11 @@
         getPatchSets().get(psId), () -> String.format("missing current patch set %s", psId.get()));
   }
 
-  @VisibleForTesting
-  public Timestamp getReadOnlyUntil() {
-    return state.readOnlyUntil();
-  }
-
   @Override
-  protected void onLoad(LoadHandle handle)
-      throws NoSuchChangeException, IOException, ConfigInvalidException {
+  protected void onLoad(LoadHandle handle) throws NoSuchChangeException, IOException {
     ObjectId rev = handle.id();
     if (rev == null) {
-      if (args.migration.readChanges()
-          && PrimaryStorage.of(change) == PrimaryStorage.NOTE_DB
-          && shouldExist) {
+      if (shouldExist) {
         throw new NoSuchChangeException(getChangeId());
       }
       loadDefaults();
@@ -678,7 +506,7 @@
     }
 
     ChangeNotesCache.Value v =
-        args.cache.get().get(getProjectName(), getChangeId(), rev, handle.walk());
+        args.cache.get().get(getProjectName(), getChangeId(), rev, handle::walk);
     state = v.state();
     state.copyColumnsTo(change);
     revisionNoteMap = v.revisionNoteMap();
@@ -698,89 +526,4 @@
   protected ObjectId readRef(Repository repo) throws IOException {
     return refs != null ? refs.get(getRefName()).orElse(null) : super.readRef(repo);
   }
-
-  @Override
-  protected LoadHandle openHandle(Repository repo) throws NoSuchChangeException, IOException {
-    if (autoRebuild) {
-      NoteDbChangeState state = NoteDbChangeState.parse(change);
-      if (args.migration.disableChangeReviewDb()) {
-        checkState(
-            state != null,
-            "shouldn't have null NoteDbChangeState when ReviewDb disabled: %s",
-            change);
-      }
-      ObjectId id = readRef(repo);
-      if (id == null) {
-        // Meta ref doesn't exist in NoteDb.
-
-        if (state == null) {
-          // Either ReviewDb change is being newly created, or it exists in ReviewDb but has not yet
-          // been rebuilt for the first time, e.g. because we just turned on write-only mode. In
-          // both cases, we don't want to auto-rebuild, just proceed with an empty ChangeNotes.
-          return super.openHandle(repo, id);
-        } else if (shouldExist && state.getPrimaryStorage() == PrimaryStorage.NOTE_DB) {
-          throw new NoSuchChangeException(getChangeId());
-        }
-
-        // ReviewDb claims NoteDb state exists, but meta ref isn't present: fall through and
-        // auto-rebuild if necessary.
-      }
-      RefCache refs = this.refs != null ? this.refs : new RepoRefCache(repo);
-      if (!NoteDbChangeState.isChangeUpToDate(state, refs, getChangeId())) {
-        return rebuildAndOpen(repo, id);
-      }
-    }
-    return super.openHandle(repo);
-  }
-
-  private LoadHandle rebuildAndOpen(Repository repo, ObjectId oldId) throws IOException {
-    Timer1.Context timer = args.metrics.autoRebuildLatency.start(CHANGES);
-    try {
-      Change.Id cid = getChangeId();
-      ReviewDb db = args.db.get();
-      ChangeRebuilder rebuilder = args.rebuilder.get();
-      NoteDbUpdateManager.Result r;
-      try (NoteDbUpdateManager manager = rebuilder.stage(db, cid)) {
-        if (manager == null) {
-          return super.openHandle(repo, oldId); // May be null in tests.
-        }
-        manager.setRefLogMessage("Auto-rebuilding change");
-        r = manager.stageAndApplyDelta(change);
-        try {
-          rebuilder.execute(db, cid, manager);
-          repo.scanForRepoChanges();
-        } catch (OrmException | IOException e) {
-          // Rebuilding failed. Most likely cause is contention on one or more
-          // change refs; there are other types of errors that can happen during
-          // rebuilding, but generally speaking they should happen during stage(),
-          // not execute(). Assume that some other worker is going to successfully
-          // store the rebuilt state, which is deterministic given an input
-          // ChangeBundle.
-          //
-          // Parse notes from the staged result so we can return something useful
-          // to the caller instead of throwing.
-          logger.atFine().log("Rebuilding change %s failed: %s", getChangeId(), e.getMessage());
-          args.metrics.autoRebuildFailureCount.increment(CHANGES);
-          rebuildResult = requireNonNull(r);
-          requireNonNull(r.newState());
-          requireNonNull(r.staged());
-          requireNonNull(r.staged().changeObjects());
-          return LoadHandle.create(
-              ChangeNotesCommit.newStagedRevWalk(repo, r.staged().changeObjects()),
-              r.newState().getChangeMetaId());
-        }
-      }
-      return LoadHandle.create(ChangeNotesCommit.newRevWalk(repo), r.newState().getChangeMetaId());
-    } catch (NoSuchChangeException e) {
-      return super.openHandle(repo, oldId);
-    } catch (OrmException e) {
-      throw new IOException(e);
-    } finally {
-      logger.atFine().log(
-          "Rebuilt change %s in project %s in %s ms",
-          getChangeId(),
-          getProjectName(),
-          TimeUnit.MILLISECONDS.convert(timer.stop(), TimeUnit.NANOSECONDS));
-    }
-  }
 }
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesCache.java b/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
index cc316e5..e2af855 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.Table;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+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;
@@ -28,8 +29,7 @@
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesKeyProto;
 import com.google.gerrit.server.cache.serialize.CacheSerializer;
-import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers;
-import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers.ObjectIdConverter;
+import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
 import com.google.gerrit.server.notedb.AbstractChangeNotes.Args;
 import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
 import com.google.inject.Inject;
@@ -42,6 +42,7 @@
 import java.util.Set;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
+import java.util.function.Supplier;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.ObjectId;
 
@@ -85,7 +86,7 @@
 
       @Override
       public byte[] serialize(Key object) {
-        return ProtoCacheSerializers.toByteArray(
+        return Protos.toByteArray(
             ChangeNotesKeyProto.newBuilder()
                 .setProject(object.project().get())
                 .setChangeId(object.changeId().get())
@@ -95,8 +96,7 @@
 
       @Override
       public Key deserialize(byte[] in) {
-        ChangeNotesKeyProto proto =
-            ProtoCacheSerializers.parseUnchecked(ChangeNotesKeyProto.parser(), in);
+        ChangeNotesKeyProto proto = Protos.parseUnchecked(ChangeNotesKeyProto.parser(), in);
         return Key.create(
             new Project.NameKey(proto.getProject()),
             new Change.Id(proto.getChangeId()),
@@ -171,7 +171,6 @@
           + list(state.changeMessages(), changeMessage())
           + P
           + map(state.publishedComments().asMap(), comment())
-          + T // readOnlyUntil
           + 1 // isPrivate
           + 1 // workInProgress
           + 1; // reviewStarted
@@ -337,13 +336,13 @@
 
   private class Loader implements Callable<ChangeNotesState> {
     private final Key key;
-    private final ChangeNotesRevWalk rw;
+    private final Supplier<ChangeNotesRevWalk> walkSupplier;
 
     private RevisionNoteMap<ChangeRevisionNote> revisionNoteMap;
 
-    private Loader(Key key, ChangeNotesRevWalk rw) {
+    private Loader(Key key, Supplier<ChangeNotesRevWalk> walkSupplier) {
       this.key = key;
-      this.rw = rw;
+      this.walkSupplier = walkSupplier;
     }
 
     @Override
@@ -354,7 +353,7 @@
           new ChangeNotesParser(
               key.changeId(),
               key.id(),
-              rw,
+              walkSupplier.get(),
               args.changeNoteJson,
               args.legacyChangeNoteRead,
               args.metrics);
@@ -375,11 +374,15 @@
     this.args = args;
   }
 
-  Value get(Project.NameKey project, Change.Id changeId, ObjectId metaId, ChangeNotesRevWalk rw)
+  Value get(
+      Project.NameKey project,
+      Change.Id changeId,
+      ObjectId metaId,
+      Supplier<ChangeNotesRevWalk> walkSupplier)
       throws IOException {
     try {
       Key key = Key.create(project, changeId, metaId);
-      Loader loader = new Loader(key, rw);
+      Loader loader = new Loader(key, walkSupplier);
       ChangeNotesState s = cache.get(key, loader);
       return new AutoValue_ChangeNotesCache_Value(s, loader.revisionNoteMap);
     } catch (ExecutionException e) {
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index 98cbd02..d3b34b9 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -26,7 +26,6 @@
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET_DESCRIPTION;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PRIVATE;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_READ_ONLY_UNTIL;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_REAL_USER;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_REVERT_OF;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_STATUS;
@@ -38,6 +37,7 @@
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_WORK_IN_PROGRESS;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.parseCommitMessageRange;
 import static com.google.gerrit.server.notedb.NoteDbTable.CHANGES;
+import static java.util.Comparator.comparing;
 import static java.util.stream.Collectors.joining;
 
 import com.google.auto.value.AutoValue;
@@ -68,7 +68,6 @@
 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.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.server.ReviewerByEmailSet;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.ReviewerStatusUpdate;
@@ -77,7 +76,6 @@
 import java.io.IOException;
 import java.nio.charset.Charset;
 import java.sql.Timestamp;
-import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -85,7 +83,6 @@
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
@@ -99,7 +96,6 @@
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.notes.NoteMap;
 import org.eclipse.jgit.revwalk.FooterKey;
-import org.eclipse.jgit.util.GitDateParser;
 import org.eclipse.jgit.util.RawParseUtils;
 
 class ChangeNotesParser {
@@ -163,7 +159,6 @@
   private String submissionId;
   private String tag;
   private RevisionNoteMap<ChangeRevisionNote> revisionNoteMap;
-  private Timestamp readOnlyUntil;
   private Boolean isPrivate;
   private Boolean workInProgress;
   private Boolean previousWorkInProgressFooter;
@@ -266,7 +261,6 @@
         submitRecords,
         buildAllMessages(),
         comments,
-        readOnlyUntil,
         firstNonNull(isPrivate, false),
         firstNonNull(workInProgress, false),
         firstNonNull(hasReviewStarted, true),
@@ -401,10 +395,6 @@
       // behavior.
     }
 
-    if (readOnlyUntil == null) {
-      parseReadOnlyUntil(commit);
-    }
-
     if (isPrivate == null) {
       parseIsPrivate(commit);
     }
@@ -469,7 +459,7 @@
       throws ConfigInvalidException {
     String line = parseOneFooter(commit, footerKey);
     if (line == null) {
-      throw expectedOneFooter(footerKey, Collections.<String>emptyList());
+      throw expectedOneFooter(footerKey, Collections.emptyList());
     }
     return line;
   }
@@ -732,7 +722,7 @@
     Map<RevId, ChangeRevisionNote> rns = revisionNoteMap.revisionNotes;
 
     for (Map.Entry<RevId, ChangeRevisionNote> e : rns.entrySet()) {
-      for (Comment c : e.getValue().getComments()) {
+      for (Comment c : e.getValue().getEntities()) {
         comments.put(e.getKey(), c);
       }
     }
@@ -838,12 +828,8 @@
       throw pe;
     }
 
-    // Store an actual 0-vote approval in the map for a removed approval, for
-    // several reasons:
-    //  - This is closer to the ReviewDb representation, which leads to less
-    //    confusion and special-casing of NoteDb.
-    //  - More importantly, ApprovalCopier needs an actual approval in order to
-    //    block copying an earlier approval over a later delete.
+    // 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);
@@ -935,20 +921,6 @@
     }
   }
 
-  private void parseReadOnlyUntil(ChangeNotesCommit commit) throws ConfigInvalidException {
-    String raw = parseOneFooter(commit, FOOTER_READ_ONLY_UNTIL);
-    if (raw == null) {
-      return;
-    }
-    try {
-      readOnlyUntil = new Timestamp(GitDateParser.parse(raw, null, Locale.US).getTime());
-    } catch (ParseException e) {
-      ConfigInvalidException cie = invalidFooter(FOOTER_READ_ONLY_UNTIL, raw);
-      cie.initCause(e);
-      throw cie;
-    }
-  }
-
   private void parseIsPrivate(ChangeNotesCommit commit) throws ConfigInvalidException {
     String raw = parseOneFooter(commit, FOOTER_PRIVATE);
     if (raw == null) {
@@ -1030,7 +1002,7 @@
   }
 
   private void updatePatchSetStates() {
-    Set<PatchSet.Id> missing = new TreeSet<>(ReviewDbUtil.intKeyOrdering());
+    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)) {
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesState.java b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
index 02fe416..fd260e7 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesState.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
@@ -19,10 +19,6 @@
 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 com.google.gerrit.reviewdb.server.ReviewDbCodecs.APPROVAL_CODEC;
-import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.MESSAGE_CODEC;
-import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.PATCH_SET_CODEC;
-import static com.google.gerrit.server.cache.serialize.ProtoCacheSerializers.toByteString;
 import static java.util.Objects.requireNonNull;
 
 import com.google.auto.value.AutoValue;
@@ -39,7 +35,9 @@
 import com.google.common.collect.Table;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.SubmitRecord;
+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;
@@ -49,7 +47,10 @@
 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.OutputFormat;
+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.ReviewerByEmailSet;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.ReviewerStatusUpdate;
@@ -59,12 +60,11 @@
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerSetEntryProto;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerStatusUpdateProto;
 import com.google.gerrit.server.cache.serialize.CacheSerializer;
-import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers;
-import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers.ObjectIdConverter;
+import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
 import com.google.gerrit.server.index.change.ChangeField.StoredSubmitRecord;
-import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
 import com.google.gson.Gson;
-import java.io.IOException;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.MessageLite;
 import java.sql.Timestamp;
 import java.util.List;
 import java.util.Map;
@@ -118,7 +118,6 @@
       List<SubmitRecord> submitRecords,
       List<ChangeMessage> changeMessages,
       ListMultimap<RevId, Comment> publishedComments,
-      @Nullable Timestamp readOnlyUntil,
       boolean isPrivate,
       boolean workInProgress,
       boolean reviewStarted,
@@ -166,16 +165,12 @@
         .submitRecords(submitRecords)
         .changeMessages(changeMessages)
         .publishedComments(publishedComments)
-        .readOnlyUntil(readOnlyUntil)
         .build();
   }
 
   /**
    * Subset of Change columns that can be represented in NoteDb.
    *
-   * <p>Notable exceptions include rowVersion and noteDbState, which are only make sense when read
-   * from NoteDb, so they cannot be cached.
-   *
    * <p>Fields should match the column names in {@link Change}, and are in listed column order.
    */
   @AutoValue
@@ -302,9 +297,6 @@
 
   abstract ImmutableListMultimap<RevId, Comment> publishedComments();
 
-  @Nullable
-  abstract Timestamp readOnlyUntil();
-
   Change newChange(Project.NameKey project) {
     ChangeColumns c = requireNonNull(columns(), "columns are required");
     Change change =
@@ -315,17 +307,15 @@
             new Branch.NameKey(project, c.branch()),
             c.createdOn());
     copyNonConstructorColumnsTo(change);
-    change.setNoteDbState(NoteDbChangeState.NOTE_DB_PRIMARY_STATE);
     return change;
   }
 
-  void copyColumnsTo(Change change) throws IOException {
+  void copyColumnsTo(Change change) {
     ChangeColumns c = columns();
     checkState(
         c != null && metaId() != null,
         "missing columns or metaId in ChangeNotesState; is NoteDb enabled? %s",
         this);
-    checkMetaId(change);
     change.setKey(c.changeKey());
     change.setOwner(c.owner());
     change.setDest(new Branch.NameKey(change.getProject(), c.branch()));
@@ -333,26 +323,6 @@
     copyNonConstructorColumnsTo(change);
   }
 
-  private void checkMetaId(Change change) throws IOException {
-    NoteDbChangeState state = NoteDbChangeState.parse(change);
-    if (state == null) {
-      return; // Can happen during small NoteDb tests.
-    } else if (state.getPrimaryStorage() == PrimaryStorage.NOTE_DB) {
-      return;
-    }
-    checkState(state.getRefState().isPresent(), "expected RefState: %s", state);
-    ObjectId idFromState = state.getRefState().get().changeMetaId();
-    if (!idFromState.equals(metaId())) {
-      throw new IOException(
-          "cannot copy ChangeNotesState into Change "
-              + changeId()
-              + "; this ChangeNotesState was created from "
-              + metaId()
-              + ", but change requires state "
-              + idFromState);
-    }
-  }
-
   private void copyNonConstructorColumnsTo(Change change) {
     ChangeColumns c = requireNonNull(columns(), "columns are required");
     if (c.status() != null) {
@@ -428,8 +398,6 @@
 
     abstract Builder publishedComments(ListMultimap<RevId, Comment> publishedComments);
 
-    abstract Builder readOnlyUntil(@Nullable Timestamp readOnlyUntil);
-
     abstract ChangeNotesState build();
   }
 
@@ -455,8 +423,15 @@
 
       object.pastAssignees().forEach(a -> b.addPastAssignee(a.get()));
       object.hashtags().forEach(b::addHashtag);
-      object.patchSets().forEach(e -> b.addPatchSet(toByteString(e.getValue(), PATCH_SET_CODEC)));
-      object.approvals().forEach(e -> b.addApproval(toByteString(e.getValue(), APPROVAL_CODEC)));
+      object
+          .patchSets()
+          .forEach(e -> b.addPatchSet(toByteString(e.getValue(), PatchSetProtoConverter.INSTANCE)));
+      object
+          .approvals()
+          .forEach(
+              e ->
+                  b.addApproval(
+                      toByteString(e.getValue(), PatchSetApprovalProtoConverter.INSTANCE)));
 
       object.reviewers().asTable().cellSet().forEach(c -> b.addReviewer(toReviewerSetEntry(c)));
       object
@@ -480,14 +455,18 @@
       object
           .submitRecords()
           .forEach(r -> b.addSubmitRecord(GSON.toJson(new StoredSubmitRecord(r))));
-      object.changeMessages().forEach(m -> b.addChangeMessage(toByteString(m, MESSAGE_CODEC)));
+      object
+          .changeMessages()
+          .forEach(m -> b.addChangeMessage(toByteString(m, ChangeMessageProtoConverter.INSTANCE)));
       object.publishedComments().values().forEach(c -> b.addPublishedComment(GSON.toJson(c)));
 
-      if (object.readOnlyUntil() != null) {
-        b.setReadOnlyUntil(object.readOnlyUntil().getTime()).setHasReadOnlyUntil(true);
-      }
+      return Protos.toByteArray(b.build());
+    }
 
-      return ProtoCacheSerializers.toByteArray(b.build());
+    @VisibleForTesting
+    static <T> ByteString toByteString(T object, ProtoConverter<?, T> converter) {
+      MessageLite message = converter.toProto(object);
+      return Protos.toByteString(message);
     }
 
     private static ChangeColumnsProto toChangeColumnsProto(ChangeColumns cols) {
@@ -555,8 +534,7 @@
 
     @Override
     public ChangeNotesState deserialize(byte[] in) {
-      ChangeNotesStateProto proto =
-          ProtoCacheSerializers.parseUnchecked(ChangeNotesStateProto.parser(), in);
+      ChangeNotesStateProto proto = Protos.parseUnchecked(ChangeNotesStateProto.parser(), in);
       Change.Id changeId = new Change.Id(proto.getChangeId());
 
       ChangeNotesState.Builder b =
@@ -571,12 +549,12 @@
               .hashtags(proto.getHashtagList())
               .patchSets(
                   proto.getPatchSetList().stream()
-                      .map(PATCH_SET_CODEC::decode)
+                      .map(bytes -> parseProtoFrom(PatchSetProtoConverter.INSTANCE, bytes))
                       .map(ps -> Maps.immutableEntry(ps.getId(), ps))
                       .collect(toImmutableList()))
               .approvals(
                   proto.getApprovalList().stream()
-                      .map(APPROVAL_CODEC::decode)
+                      .map(bytes -> parseProtoFrom(PatchSetApprovalProtoConverter.INSTANCE, bytes))
                       .map(a -> Maps.immutableEntry(a.getPatchSetId(), a))
                       .collect(toImmutableList()))
               .reviewers(toReviewerSet(proto.getReviewerList()))
@@ -594,18 +572,21 @@
                       .collect(toImmutableList()))
               .changeMessages(
                   proto.getChangeMessageList().stream()
-                      .map(MESSAGE_CODEC::decode)
+                      .map(bytes -> parseProtoFrom(ChangeMessageProtoConverter.INSTANCE, bytes))
                       .collect(toImmutableList()))
               .publishedComments(
                   proto.getPublishedCommentList().stream()
                       .map(r -> GSON.fromJson(r, Comment.class))
                       .collect(toImmutableListMultimap(c -> new RevId(c.revId), c -> c)));
-      if (proto.getHasReadOnlyUntil()) {
-        b.readOnlyUntil(new Timestamp(proto.getReadOnlyUntil()));
-      }
       return b.build();
     }
 
+    private static <P extends MessageLite, T> T parseProtoFrom(
+        ProtoConverter<P, T> converter, ByteString byteString) {
+      P message = Protos.parseUnchecked(converter.getParser(), byteString);
+      return converter.fromProto(message);
+    }
+
     private static ChangeColumns toChangeColumns(Change.Id changeId, ChangeColumnsProto proto) {
       ChangeColumns.Builder b =
           ChangeColumns.builder()
diff --git a/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index bfb12ca..0cde363 100644
--- a/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -29,7 +29,6 @@
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET_DESCRIPTION;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PRIVATE;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_READ_ONLY_UNTIL;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_REAL_USER;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_REVERT_OF;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_STATUS;
@@ -50,8 +49,8 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Table;
 import com.google.common.collect.TreeBasedTable;
-import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.SubmitRecord;
+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;
@@ -62,16 +61,13 @@
 import com.google.gerrit.reviewdb.client.RobotComment;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.config.GerritServerConfig;
 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.gwtorm.server.OrmException;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 import java.io.IOException;
-import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.Date;
@@ -84,7 +80,6 @@
 import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 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;
@@ -106,18 +101,8 @@
  */
 public class ChangeUpdate extends AbstractChangeUpdate {
   public interface Factory {
-    ChangeUpdate create(ChangeNotes notes, CurrentUser user);
-
     ChangeUpdate create(ChangeNotes notes, CurrentUser user, Date when);
 
-    ChangeUpdate create(
-        Change change,
-        @Assisted("effective") @Nullable Account.Id accountId,
-        @Assisted("real") @Nullable Account.Id realAccountId,
-        PersonIdent authorIdent,
-        Date when,
-        Comparator<String> labelNameComparator);
-
     @VisibleForTesting
     ChangeUpdate create(
         ChangeNotes notes, CurrentUser user, Date when, Comparator<String> labelNameComparator);
@@ -152,7 +137,6 @@
   private boolean isAllowWriteToNewtRef;
   private String psDescription;
   private boolean currentPatchSet;
-  private Timestamp readOnlyUntil;
   private Boolean isPrivate;
   private Boolean workInProgress;
   private Integer revertOf;
@@ -164,37 +148,7 @@
 
   @AssistedInject
   private ChangeUpdate(
-      @GerritServerConfig Config cfg,
       @GerritPersonIdent PersonIdent serverIdent,
-      NotesMigration migration,
-      NoteDbUpdateManager.Factory updateManagerFactory,
-      ChangeDraftUpdate.Factory draftUpdateFactory,
-      RobotCommentUpdate.Factory robotCommentUpdateFactory,
-      DeleteCommentRewriter.Factory deleteCommentRewriterFactory,
-      ProjectCache projectCache,
-      @Assisted ChangeNotes notes,
-      @Assisted CurrentUser user,
-      ChangeNoteUtil noteUtil) {
-    this(
-        cfg,
-        serverIdent,
-        migration,
-        updateManagerFactory,
-        draftUpdateFactory,
-        robotCommentUpdateFactory,
-        deleteCommentRewriterFactory,
-        projectCache,
-        notes,
-        user,
-        serverIdent.getWhen(),
-        noteUtil);
-  }
-
-  @AssistedInject
-  private ChangeUpdate(
-      @GerritServerConfig Config cfg,
-      @GerritPersonIdent PersonIdent serverIdent,
-      NotesMigration migration,
       NoteDbUpdateManager.Factory updateManagerFactory,
       ChangeDraftUpdate.Factory draftUpdateFactory,
       RobotCommentUpdate.Factory robotCommentUpdateFactory,
@@ -205,9 +159,7 @@
       @Assisted Date when,
       ChangeNoteUtil noteUtil) {
     this(
-        cfg,
         serverIdent,
-        migration,
         updateManagerFactory,
         draftUpdateFactory,
         robotCommentUpdateFactory,
@@ -226,9 +178,7 @@
 
   @AssistedInject
   private ChangeUpdate(
-      @GerritServerConfig Config cfg,
       @GerritPersonIdent PersonIdent serverIdent,
-      NotesMigration migration,
       NoteDbUpdateManager.Factory updateManagerFactory,
       ChangeDraftUpdate.Factory draftUpdateFactory,
       RobotCommentUpdate.Factory robotCommentUpdateFactory,
@@ -238,7 +188,7 @@
       @Assisted Date when,
       @Assisted Comparator<String> labelNameComparator,
       ChangeNoteUtil noteUtil) {
-    super(cfg, migration, notes, user, serverIdent, noteUtil, when);
+    super(notes, user, serverIdent, noteUtil, when);
     this.updateManagerFactory = updateManagerFactory;
     this.draftUpdateFactory = draftUpdateFactory;
     this.robotCommentUpdateFactory = robotCommentUpdateFactory;
@@ -246,44 +196,9 @@
     this.approvals = approvals(labelNameComparator);
   }
 
-  @AssistedInject
-  private ChangeUpdate(
-      @GerritServerConfig Config cfg,
-      @GerritPersonIdent PersonIdent serverIdent,
-      NotesMigration migration,
-      NoteDbUpdateManager.Factory updateManagerFactory,
-      ChangeDraftUpdate.Factory draftUpdateFactory,
-      RobotCommentUpdate.Factory robotCommentUpdateFactory,
-      DeleteCommentRewriter.Factory deleteCommentRewriterFactory,
-      ChangeNoteUtil noteUtil,
-      @Assisted Change change,
-      @Assisted("effective") @Nullable Account.Id accountId,
-      @Assisted("real") @Nullable Account.Id realAccountId,
-      @Assisted PersonIdent authorIdent,
-      @Assisted Date when,
-      @Assisted Comparator<String> labelNameComparator) {
-    super(
-        cfg,
-        migration,
-        noteUtil,
-        serverIdent,
-        null,
-        change,
-        accountId,
-        realAccountId,
-        authorIdent,
-        when);
-    this.draftUpdateFactory = draftUpdateFactory;
-    this.robotCommentUpdateFactory = robotCommentUpdateFactory;
-    this.updateManagerFactory = updateManagerFactory;
-    this.deleteCommentRewriterFactory = deleteCommentRewriterFactory;
-    this.approvals = approvals(labelNameComparator);
-  }
-
-  public ObjectId commit() throws IOException, OrmException {
+  public ObjectId commit() throws IOException {
     try (NoteDbUpdateManager updateManager = updateManagerFactory.create(getProjectName())) {
       updateManager.add(this);
-      updateManager.stageAndApplyDelta(getChange());
       updateManager.execute();
     }
     return getResult();
@@ -396,9 +311,9 @@
         deleteCommentRewriterFactory.create(getChange().getId(), uuid, newMessage);
   }
 
-  public void deleteChangeMessageByRewritingHistory(int targetMessageIdx, String newMessage) {
+  public void deleteChangeMessageByRewritingHistory(String targetMessageId, String newMessage) {
     deleteChangeMessageRewriter =
-        new DeleteChangeMessageRewriter(getChange().getId(), targetMessageIdx, newMessage);
+        new DeleteChangeMessageRewriter(getChange().getId(), targetMessageId, newMessage);
   }
 
   @VisibleForTesting
@@ -514,7 +429,7 @@
 
   /** @return the tree id for the updated tree */
   private ObjectId storeRevisionNotes(RevWalk rw, ObjectInserter inserter, ObjectId curr)
-      throws ConfigInvalidException, OrmException, IOException {
+      throws ConfigInvalidException, IOException {
     if (comments.isEmpty() && pushCert == null) {
       return null;
     }
@@ -541,20 +456,17 @@
   }
 
   private RevisionNoteMap<ChangeRevisionNote> getRevisionNoteMap(RevWalk rw, ObjectId curr)
-      throws ConfigInvalidException, OrmException, IOException {
+      throws ConfigInvalidException, IOException {
     if (curr.equals(ObjectId.zeroId())) {
       return RevisionNoteMap.emptyMap();
     }
-    if (migration.readChanges()) {
-      // If reading from changes is enabled, then the old ChangeNotes may have
-      // already parsed the revision notes. We can reuse them as long as the ref
-      // hasn't advanced.
-      ChangeNotes notes = getNotes();
-      if (notes != null && notes.revisionNoteMap != null) {
-        ObjectId idFromNotes = firstNonNull(notes.load().getRevision(), ObjectId.zeroId());
-        if (idFromNotes.equals(curr)) {
-          return notes.revisionNoteMap;
-        }
+    // The old ChangeNotes may have already parsed the revision notes. We can reuse them as long as
+    // the ref hasn't advanced.
+    ChangeNotes notes = getNotes();
+    if (notes != null && notes.revisionNoteMap != null) {
+      ObjectId idFromNotes = firstNonNull(notes.load().getRevision(), ObjectId.zeroId());
+      if (idFromNotes.equals(curr)) {
+        return notes.revisionNoteMap;
       }
     }
     NoteMap noteMap = NoteMap.read(rw.getObjectReader(), rw.parseCommit(curr));
@@ -570,12 +482,11 @@
   }
 
   private void checkComments(
-      Map<RevId, ChangeRevisionNote> existingNotes, Map<RevId, RevisionNoteBuilder> toUpdate)
-      throws OrmException {
+      Map<RevId, ChangeRevisionNote> existingNotes, Map<RevId, RevisionNoteBuilder> toUpdate) {
     // Prohibit various kinds of illegal operations on comments.
     Set<Comment.Key> existing = new HashSet<>();
     for (ChangeRevisionNote rn : existingNotes.values()) {
-      for (Comment c : rn.getComments()) {
+      for (Comment c : rn.getEntities()) {
         existing.add(c.key);
         if (draftUpdate != null) {
           // Take advantage of an existing update on All-Users to prune any
@@ -601,7 +512,7 @@
     for (RevisionNoteBuilder b : toUpdate.values()) {
       for (Comment c : b.put.values()) {
         if (existing.contains(c.key)) {
-          throw new OrmException("Cannot update existing published comment: " + c);
+          throw new StorageException("Cannot update existing published comment: " + c);
         }
       }
     }
@@ -614,7 +525,7 @@
 
   @Override
   protected CommitBuilder applyImpl(RevWalk rw, ObjectInserter ins, ObjectId curr)
-      throws OrmException, IOException {
+      throws IOException {
     checkState(
         deleteCommentRewriter == null && deleteChangeMessageRewriter == null,
         "cannot update and rewrite ref in one BatchUpdate");
@@ -750,10 +661,6 @@
       addIdent(msg, realAccountId).append('\n');
     }
 
-    if (readOnlyUntil != null) {
-      addFooter(msg, FOOTER_READ_ONLY_UNTIL, NoteDbUtil.formatTime(serverIdent, readOnlyUntil));
-    }
-
     if (isPrivate != null) {
       addFooter(msg, FOOTER_PRIVATE, isPrivate);
     }
@@ -773,7 +680,7 @@
         cb.setTreeId(treeId);
       }
     } catch (ConfigInvalidException e) {
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
     return cb;
   }
@@ -813,7 +720,6 @@
         && tag == null
         && psDescription == null
         && !currentPatchSet
-        && readOnlyUntil == null
         && isPrivate == null
         && workInProgress == null
         && revertOf == null;
@@ -852,10 +758,6 @@
     this.workInProgress = workInProgress;
   }
 
-  void setReadOnlyUntil(Timestamp readOnlyUntil) {
-    this.readOnlyUntil = readOnlyUntil;
-  }
-
   private static StringBuilder addFooter(StringBuilder sb, FooterKey footer) {
     return sb.append(footer.getName()).append(": ");
   }
@@ -877,13 +779,4 @@
     sb.append('>');
     return sb;
   }
-
-  @Override
-  protected void checkNotReadOnly() throws OrmException {
-    // Allow setting Read-only-until to 0 to release an existing lease.
-    if (readOnlyUntil != null && readOnlyUntil.getTime() == 0) {
-      return;
-    }
-    super.checkNotReadOnly();
-  }
 }
diff --git a/java/com/google/gerrit/server/notedb/CommentJsonMigrator.java b/java/com/google/gerrit/server/notedb/CommentJsonMigrator.java
deleted file mode 100644
index 361c435..0000000
--- a/java/com/google/gerrit/server/notedb/CommentJsonMigrator.java
+++ /dev/null
@@ -1,253 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb;
-
-import static com.google.common.collect.ImmutableList.toImmutableList;
-import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
-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.config.AllUsersName;
-import com.google.gerrit.server.config.GerritServerIdProvider;
-import com.google.gerrit.server.update.RefUpdateUtil;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import java.io.IOException;
-import java.util.List;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.internal.storage.file.FileRepository;
-import org.eclipse.jgit.internal.storage.file.PackInserter;
-import org.eclipse.jgit.lib.BatchRefUpdate;
-import org.eclipse.jgit.lib.CommitBuilder;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.ObjectLoader;
-import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.notes.Note;
-import org.eclipse.jgit.notes.NoteMap;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevSort;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.ReceiveCommand;
-import org.eclipse.jgit.util.MutableInteger;
-
-@Singleton
-public class CommentJsonMigrator {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
-  public static class ProjectMigrationResult {
-    public int skipped;
-    public boolean ok;
-    public List<String> refsUpdated;
-  }
-
-  private final LegacyChangeNoteRead legacyChangeNoteRead;
-  private final ChangeNoteJson changeNoteJson;
-  private final AllUsersName allUsers;
-
-  @Inject
-  CommentJsonMigrator(
-      ChangeNoteJson changeNoteJson,
-      GerritServerIdProvider gerritServerIdProvider,
-      AllUsersName allUsers) {
-    this.changeNoteJson = changeNoteJson;
-    this.allUsers = allUsers;
-    this.legacyChangeNoteRead = new LegacyChangeNoteRead(gerritServerIdProvider.get());
-  }
-
-  CommentJsonMigrator(ChangeNoteJson changeNoteJson, String serverId, AllUsersName allUsers) {
-    this.changeNoteJson = changeNoteJson;
-    this.legacyChangeNoteRead = new LegacyChangeNoteRead(serverId);
-    this.allUsers = allUsers;
-  }
-
-  public ProjectMigrationResult migrateProject(
-      Project.NameKey project, Repository repo, boolean dryRun) {
-    ProjectMigrationResult progress = new ProjectMigrationResult();
-    progress.ok = true;
-    progress.skipped = 0;
-    progress.refsUpdated = ImmutableList.of();
-    try (RevWalk rw = new RevWalk(repo);
-        ObjectInserter ins = newPackInserter(repo)) {
-      BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
-      bru.setAllowNonFastForwards(true);
-      progress.ok &= migrateChanges(project, repo, rw, ins, bru);
-      if (project.equals(allUsers)) {
-        progress.ok &= migrateDrafts(allUsers, repo, rw, ins, bru);
-      }
-
-      progress.refsUpdated =
-          bru.getCommands().stream().map(c -> c.getRefName()).collect(toImmutableList());
-      if (!bru.getCommands().isEmpty()) {
-        if (!dryRun) {
-          ins.flush();
-          RefUpdateUtil.executeChecked(bru, rw);
-        }
-      } else {
-        progress.skipped++;
-      }
-    } catch (IOException e) {
-      progress.ok = false;
-    }
-
-    return progress;
-  }
-
-  private boolean migrateChanges(
-      Project.NameKey project, Repository repo, RevWalk rw, ObjectInserter ins, BatchRefUpdate bru)
-      throws IOException {
-    boolean ok = true;
-    for (Ref ref : repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_CHANGES)) {
-      Change.Id changeId = Change.Id.fromRef(ref.getName());
-      if (changeId == null || !ref.getName().equals(RefNames.changeMetaRef(changeId))) {
-        continue;
-      }
-      ok &= migrateOne(project, rw, ins, bru, Status.PUBLISHED, changeId, ref);
-    }
-    return ok;
-  }
-
-  private boolean migrateDrafts(
-      Project.NameKey allUsers,
-      Repository allUsersRepo,
-      RevWalk rw,
-      ObjectInserter ins,
-      BatchRefUpdate bru)
-      throws IOException {
-    boolean ok = true;
-    for (Ref ref : allUsersRepo.getRefDatabase().getRefsByPrefix(RefNames.REFS_DRAFT_COMMENTS)) {
-      Change.Id changeId = Change.Id.fromAllUsersRef(ref.getName());
-      if (changeId == null) {
-        continue;
-      }
-      ok &= migrateOne(allUsers, rw, ins, bru, Status.DRAFT, changeId, ref);
-    }
-    return ok;
-  }
-
-  private boolean migrateOne(
-      Project.NameKey project,
-      RevWalk rw,
-      ObjectInserter ins,
-      BatchRefUpdate bru,
-      Status status,
-      Change.Id changeId,
-      Ref ref) {
-    ObjectId oldId = ref.getObjectId();
-    try {
-      if (!hasAnyLegacyComments(rw, oldId)) {
-        return true;
-      }
-    } catch (IOException e) {
-      logger.atInfo().log(
-          String.format(
-              "Error reading change %s in %s; attempting migration anyway", changeId, project),
-          e);
-    }
-
-    try {
-      reset(rw, oldId);
-
-      ObjectReader reader = rw.getObjectReader();
-      ObjectId newId = null;
-      RevCommit c;
-      while ((c = rw.next()) != null) {
-        CommitBuilder cb = new CommitBuilder();
-        cb.setAuthor(c.getAuthorIdent());
-        cb.setCommitter(c.getCommitterIdent());
-        cb.setMessage(c.getFullMessage());
-        cb.setEncoding(c.getEncoding());
-        if (newId != null) {
-          cb.setParentId(newId);
-        }
-
-        // Read/write using the low-level RevisionNote API, which works regardless of NotesMigration
-        // state.
-        NoteMap noteMap = NoteMap.read(reader, c);
-        RevisionNoteMap<ChangeRevisionNote> revNoteMap =
-            RevisionNoteMap.parse(
-                changeNoteJson, legacyChangeNoteRead, changeId, reader, noteMap, status);
-        RevisionNoteBuilder.Cache cache = new RevisionNoteBuilder.Cache(revNoteMap);
-
-        for (RevId revId : revNoteMap.revisionNotes.keySet()) {
-          // Call cache.get on each known RevId to read the old note in whichever format, then write
-          // the note in JSON format.
-          byte[] data = cache.get(revId).build(changeNoteJson);
-          noteMap.set(ObjectId.fromString(revId.get()), ins.insert(OBJ_BLOB, data));
-        }
-        cb.setTreeId(noteMap.writeTree(ins));
-        newId = ins.insert(cb);
-      }
-
-      bru.addCommand(new ReceiveCommand(oldId, newId, ref.getName()));
-      return true;
-    } catch (ConfigInvalidException | IOException e) {
-      logger.atInfo().log(String.format("Error migrating change %s in %s", changeId, project), e);
-      return false;
-    }
-  }
-
-  private static boolean hasAnyLegacyComments(RevWalk rw, ObjectId id) throws IOException {
-    ObjectReader reader = rw.getObjectReader();
-    reset(rw, id);
-
-    // Check the note map at each commit, not just the tip. It's possible that the server switched
-    // from legacy to JSON partway through its history, which would have mixed legacy/JSON comments
-    // in its history. Although the tip commit would continue to parse once we remove the legacy
-    // parser, our goal is really to expunge all vestiges of the old format, which implies rewriting
-    // history (and thus returning true) in this case.
-    RevCommit c;
-    while ((c = rw.next()) != null) {
-      NoteMap noteMap = NoteMap.read(reader, c);
-      for (Note note : noteMap) {
-        // Match pre-parsing logic in RevisionNote#parse().
-        ObjectLoader objectLoader = reader.open(note.getData(), OBJ_BLOB);
-        if (objectLoader.isLarge()) {
-          throw new IOException(String.format("Comment note %s is too large", note.name()));
-        }
-        byte[] raw = objectLoader.getCachedBytes();
-        MutableInteger p = new MutableInteger();
-        RevisionNote.trimLeadingEmptyLines(raw, p);
-        if (!ChangeRevisionNote.isJson(raw, p.value)) {
-          return true;
-        }
-      }
-    }
-    return false;
-  }
-
-  private static void reset(RevWalk rw, ObjectId id) throws IOException {
-    rw.reset();
-    rw.sort(RevSort.TOPO);
-    rw.sort(RevSort.REVERSE);
-    rw.markStart(rw.parseCommit(id));
-  }
-
-  private static ObjectInserter newPackInserter(Repository repo) {
-    if (!(repo instanceof FileRepository)) {
-      return repo.newObjectInserter();
-    }
-    PackInserter ins = ((FileRepository) repo).getObjectDatabase().newPackInserter();
-    ins.checkExisting(false);
-    return ins;
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/DeleteChangeMessageRewriter.java b/java/com/google/gerrit/server/notedb/DeleteChangeMessageRewriter.java
index 212aa37..8fb28e1 100644
--- a/java/com/google/gerrit/server/notedb/DeleteChangeMessageRewriter.java
+++ b/java/com/google/gerrit/server/notedb/DeleteChangeMessageRewriter.java
@@ -15,6 +15,7 @@
 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 org.eclipse.jgit.util.RawParseUtils.decode;
@@ -40,12 +41,12 @@
 public class DeleteChangeMessageRewriter implements NoteDbRewriter {
 
   private final Change.Id changeId;
-  private final int targetMessageIdx;
+  private final String targetMessageId;
   private final String newChangeMessage;
 
-  DeleteChangeMessageRewriter(Change.Id changeId, int targetMessageIdx, String newChangeMessage) {
+  DeleteChangeMessageRewriter(Change.Id changeId, String targetMessageId, String newChangeMessage) {
     this.changeId = changeId;
-    this.targetMessageIdx = targetMessageIdx;
+    this.targetMessageId = checkNotNull(targetMessageId);
     this.newChangeMessage = newChangeMessage;
   }
 
@@ -67,21 +68,18 @@
 
     ObjectId newTipId = null;
     RevCommit originalCommit;
-    int idx = 0;
+    boolean startRewrite = false;
     while ((originalCommit = revWalk.next()) != null) {
-      if (idx < targetMessageIdx) {
+      boolean isTargetCommit = originalCommit.getId().getName().equals(targetMessageId);
+      if (!startRewrite && !isTargetCommit) {
         newTipId = originalCommit;
-        idx++;
         continue;
       }
 
+      startRewrite = true;
       String newCommitMessage =
-          (idx == targetMessageIdx)
-              ? createNewCommitMessage(originalCommit)
-              : originalCommit.getFullMessage();
+          isTargetCommit ? createNewCommitMessage(originalCommit) : originalCommit.getFullMessage();
       newTipId = rewriteOneCommit(originalCommit, newTipId, newCommitMessage, inserter);
-
-      idx++;
     }
     return newTipId;
   }
diff --git a/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java b/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java
index 656e9ec..c100550 100644
--- a/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java
+++ b/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java
@@ -25,7 +25,6 @@
 import com.google.gerrit.reviewdb.client.Comment;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
@@ -86,7 +85,7 @@
 
   @Override
   public ObjectId rewriteCommitHistory(RevWalk revWalk, ObjectInserter inserter, ObjectId currTip)
-      throws IOException, ConfigInvalidException, OrmException {
+      throws IOException, ConfigInvalidException {
     checkArgument(!currTip.equals(ObjectId.zeroId()));
 
     // Walk from the first commit of the branch.
@@ -142,7 +141,7 @@
     return RevisionNoteMap.parse(
             changeNoteJson, legacyChangeNoteRead, changeId, reader, noteMap, PUBLISHED)
         .revisionNotes.values().stream()
-        .flatMap(n -> n.getComments().stream())
+        .flatMap(n -> n.getEntities().stream())
         .collect(toMap(c -> c.key.uuid, Function.identity()));
   }
 
diff --git a/java/com/google/gerrit/server/notedb/DraftCommentNotes.java b/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
index 8acc8d4..213613e 100644
--- a/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
+++ b/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.gerrit.reviewdb.client.RefNames.refsDraftComments;
-import static com.google.gerrit.server.notedb.NoteDbTable.CHANGES;
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.annotations.VisibleForTesting;
@@ -25,24 +24,15 @@
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
-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.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.server.ReviewDb;
-import com.google.gerrit.server.git.RepoRefCache;
-import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
-import com.google.gerrit.server.notedb.NoteDbUpdateManager.StagedResult;
-import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder;
-import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 import java.io.IOException;
-import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectReader;
@@ -50,53 +40,29 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.notes.NoteMap;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.transport.ReceiveCommand;
 
 /** View of the draft comments for a single {@link Change} based on the log of its drafts branch. */
 public class DraftCommentNotes extends AbstractChangeNotes<DraftCommentNotes> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public interface Factory {
-    DraftCommentNotes create(Change change, Account.Id accountId);
-
-    DraftCommentNotes createWithAutoRebuildingDisabled(Change.Id changeId, Account.Id accountId);
+    DraftCommentNotes create(Change.Id changeId, Account.Id accountId);
   }
 
-  private final Change change;
   private final Account.Id author;
-  private final NoteDbUpdateManager.Result rebuildResult;
   private final Ref ref;
 
   private ImmutableListMultimap<RevId, Comment> comments;
   private RevisionNoteMap<ChangeRevisionNote> revisionNoteMap;
 
   @AssistedInject
-  DraftCommentNotes(Args args, @Assisted Change change, @Assisted Account.Id author) {
-    this(args, change, author, true, null, null);
-  }
-
-  @AssistedInject
   DraftCommentNotes(Args args, @Assisted Change.Id changeId, @Assisted Account.Id author) {
-    // PrimaryStorage is unknown; this should only called by
-    // PatchLineCommentsUtil#draftByAuthor, which can live with this.
-    super(args, changeId, null, false);
-    this.change = null;
-    this.author = author;
-    this.rebuildResult = null;
-    this.ref = null;
+    this(args, changeId, author, null);
   }
 
-  DraftCommentNotes(
-      Args args,
-      Change change,
-      Account.Id author,
-      boolean autoRebuild,
-      @Nullable NoteDbUpdateManager.Result rebuildResult,
-      @Nullable Ref ref) {
-    super(args, change.getId(), PrimaryStorage.of(change), autoRebuild);
-    this.change = change;
-    this.author = author;
-    this.rebuildResult = rebuildResult;
+  DraftCommentNotes(Args args, Change.Id changeId, Account.Id author, @Nullable Ref ref) {
+    super(args, changeId);
+    this.author = requireNonNull(author);
     this.ref = ref;
     if (ref != null) {
       checkArgument(
@@ -164,7 +130,7 @@
             PatchLineComment.Status.DRAFT);
     ListMultimap<RevId, Comment> cs = MultimapBuilder.hashKeys().arrayListValues().build();
     for (ChangeRevisionNote rn : revisionNoteMap.revisionNotes.values()) {
-      for (Comment c : rn.getComments()) {
+      for (Comment c : rn.getEntities()) {
         cs.put(new RevId(c.revId), c);
       }
     }
@@ -181,79 +147,6 @@
     return args.allUsers;
   }
 
-  @Override
-  protected LoadHandle openHandle(Repository repo) throws NoSuchChangeException, IOException {
-    if (rebuildResult != null) {
-      StagedResult sr = requireNonNull(rebuildResult.staged());
-      return LoadHandle.create(
-          ChangeNotesCommit.newStagedRevWalk(repo, sr.allUsersObjects()),
-          findNewId(sr.allUsersCommands(), getRefName()));
-    } else if (change != null && autoRebuild) {
-      NoteDbChangeState state = NoteDbChangeState.parse(change);
-      // Only check if this particular user's drafts are up to date, to avoid
-      // reading unnecessary refs.
-      if (!NoteDbChangeState.areDraftsUpToDate(
-          state, new RepoRefCache(repo), getChangeId(), author)) {
-        return rebuildAndOpen(repo);
-      }
-    }
-    return super.openHandle(repo);
-  }
-
-  private static ObjectId findNewId(Iterable<ReceiveCommand> cmds, String refName) {
-    for (ReceiveCommand cmd : cmds) {
-      if (cmd.getRefName().equals(refName)) {
-        return cmd.getNewId();
-      }
-    }
-    return null;
-  }
-
-  private LoadHandle rebuildAndOpen(Repository repo) throws NoSuchChangeException, IOException {
-    Timer1.Context timer = args.metrics.autoRebuildLatency.start(CHANGES);
-    try {
-      Change.Id cid = getChangeId();
-      ReviewDb db = args.db.get();
-      ChangeRebuilder rebuilder = args.rebuilder.get();
-      NoteDbUpdateManager.Result r;
-      try (NoteDbUpdateManager manager = rebuilder.stage(db, cid)) {
-        if (manager == null) {
-          return super.openHandle(repo); // May be null in tests.
-        }
-        r = manager.stageAndApplyDelta(change);
-        try {
-          rebuilder.execute(db, cid, manager);
-          repo.scanForRepoChanges();
-        } catch (OrmException | IOException e) {
-          // See ChangeNotes#rebuildAndOpen.
-          logger.atFine().log(
-              "Rebuilding change %s via drafts failed: %s", getChangeId(), e.getMessage());
-          args.metrics.autoRebuildFailureCount.increment(CHANGES);
-          requireNonNull(r.staged());
-          return LoadHandle.create(
-              ChangeNotesCommit.newStagedRevWalk(repo, r.staged().allUsersObjects()), draftsId(r));
-        }
-      }
-      return LoadHandle.create(ChangeNotesCommit.newRevWalk(repo), draftsId(r));
-    } catch (NoSuchChangeException e) {
-      return super.openHandle(repo);
-    } catch (OrmException e) {
-      throw new IOException(e);
-    } finally {
-      logger.atFine().log(
-          "Rebuilt change %s in %s in %s ms via drafts",
-          getChangeId(),
-          change != null ? "project " + change.getProject() : "unknown project",
-          TimeUnit.MILLISECONDS.convert(timer.stop(), TimeUnit.NANOSECONDS));
-    }
-  }
-
-  private ObjectId draftsId(NoteDbUpdateManager.Result r) {
-    requireNonNull(r);
-    requireNonNull(r.newState());
-    return r.newState().getDraftIds().get(author);
-  }
-
   @VisibleForTesting
   NoteMap getNoteMap() {
     return revisionNoteMap != null ? revisionNoteMap.noteMap : null;
diff --git a/java/com/google/gerrit/server/notedb/GwtormChangeBundleReader.java b/java/com/google/gerrit/server/notedb/GwtormChangeBundleReader.java
deleted file mode 100644
index 347ba48..0000000
--- a/java/com/google/gerrit/server/notedb/GwtormChangeBundleReader.java
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb;
-
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ReviewerSet;
-import com.google.gerrit.server.notedb.ChangeBundle.Source;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import java.util.List;
-
-@Singleton
-public class GwtormChangeBundleReader implements ChangeBundleReader {
-  @Inject
-  GwtormChangeBundleReader() {}
-
-  @Override
-  @Nullable
-  public ChangeBundle fromReviewDb(ReviewDb db, Change.Id id) throws OrmException {
-    Change reviewDbChange = db.changes().get(id);
-    if (reviewDbChange == null) {
-      return null;
-    }
-
-    // TODO(dborowitz): Figure out how to do this more consistently, e.g. hand-written inner joins.
-    List<PatchSetApproval> approvals = db.patchSetApprovals().byChange(id).toList();
-    return new ChangeBundle(
-        reviewDbChange,
-        db.changeMessages().byChange(id),
-        db.patchSets().byChange(id),
-        approvals,
-        db.patchComments().byChange(id),
-        ReviewerSet.fromApprovals(approvals),
-        Source.REVIEW_DB);
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/IntBlob.java b/java/com/google/gerrit/server/notedb/IntBlob.java
new file mode 100644
index 0000000..6305a54
--- /dev/null
+++ b/java/com/google/gerrit/server/notedb/IntBlob.java
@@ -0,0 +1,128 @@
+// 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 java.nio.charset.StandardCharsets.UTF_8;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.CharMatcher;
+import com.google.common.primitives.Ints;
+import com.google.gerrit.common.Nullable;
+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;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+@AutoValue
+public abstract class IntBlob {
+  public static Optional<IntBlob> parse(Repository repo, String refName) throws IOException {
+    try (ObjectReader or = repo.newObjectReader()) {
+      return parse(repo, refName, or);
+    }
+  }
+
+  public static Optional<IntBlob> parse(Repository repo, String refName, RevWalk rw)
+      throws IOException {
+    return parse(repo, refName, rw.getObjectReader());
+  }
+
+  private static Optional<IntBlob> parse(Repository repo, String refName, ObjectReader or)
+      throws IOException {
+    Ref ref = repo.exactRef(refName);
+    if (ref == null) {
+      return Optional.empty();
+    }
+    ObjectId id = ref.getObjectId();
+    ObjectLoader ol = or.open(id, OBJ_BLOB);
+    if (ol.getType() != OBJ_BLOB) {
+      // In theory this should be thrown by open but not all implementations may do it properly
+      // (certainly InMemoryRepository doesn't).
+      throw new IncorrectObjectTypeException(id, OBJ_BLOB);
+    }
+    String str = CharMatcher.whitespace().trimFrom(new String(ol.getCachedBytes(), UTF_8));
+    Integer value = Ints.tryParse(str);
+    if (value == null) {
+      throw new StorageException("invalid value in " + refName + " blob at " + id.name());
+    }
+    return Optional.of(IntBlob.create(id, value));
+  }
+
+  public static RefUpdate tryStore(
+      Repository repo,
+      RevWalk rw,
+      Project.NameKey projectName,
+      String refName,
+      @Nullable ObjectId oldId,
+      int val,
+      GitReferenceUpdated gitRefUpdated)
+      throws IOException {
+    ObjectId newId;
+    try (ObjectInserter ins = repo.newObjectInserter()) {
+      newId = ins.insert(OBJ_BLOB, Integer.toString(val).getBytes(UTF_8));
+      ins.flush();
+    }
+    RefUpdate ru = repo.updateRef(refName);
+    if (oldId != null) {
+      ru.setExpectedOldObjectId(oldId);
+    }
+    ru.disableRefLog();
+    ru.setNewObjectId(newId);
+    ru.setForceUpdate(true); // Required for non-commitish updates.
+    RefUpdate.Result result = ru.update(rw);
+    if (refUpdated(result)) {
+      gitRefUpdated.fire(projectName, ru, null);
+    }
+    return ru;
+  }
+
+  public static void store(
+      Repository repo,
+      RevWalk rw,
+      Project.NameKey projectName,
+      String refName,
+      @Nullable ObjectId oldId,
+      int val,
+      GitReferenceUpdated gitRefUpdated)
+      throws IOException {
+    RefUpdateUtil.checkResult(tryStore(repo, rw, projectName, refName, oldId, val, gitRefUpdated));
+  }
+
+  private static boolean refUpdated(RefUpdate.Result result) {
+    return result == RefUpdate.Result.NEW || result == RefUpdate.Result.FORCED;
+  }
+
+  @VisibleForTesting
+  static IntBlob create(AnyObjectId id, int value) {
+    return new AutoValue_IntBlob(id.copy(), value);
+  }
+
+  public abstract ObjectId id();
+
+  public abstract int value();
+}
diff --git a/java/com/google/gerrit/server/notedb/LegacyChangeNoteRead.java b/java/com/google/gerrit/server/notedb/LegacyChangeNoteRead.java
index 819c8ac..916cc16 100644
--- a/java/com/google/gerrit/server/notedb/LegacyChangeNoteRead.java
+++ b/java/com/google/gerrit/server/notedb/LegacyChangeNoteRead.java
@@ -21,7 +21,6 @@
 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.Comment.Key;
 import com.google.gerrit.reviewdb.client.CommentRange;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.RevId;
@@ -71,7 +70,7 @@
     if (p.value >= note.length) {
       return ImmutableList.of();
     }
-    Set<Key> seen = new HashSet<>();
+    Set<Comment.Key> seen = new HashSet<>();
     List<Comment> result = new ArrayList<>();
     int sizeOfNote = note.length;
     byte[] psb = ChangeNoteUtil.PATCH_SET.getBytes(UTF_8);
diff --git a/java/com/google/gerrit/server/notedb/LegacyChangeNoteWrite.java b/java/com/google/gerrit/server/notedb/LegacyChangeNoteWrite.java
index c9711b5..b51a59c 100644
--- a/java/com/google/gerrit/server/notedb/LegacyChangeNoteWrite.java
+++ b/java/com/google/gerrit/server/notedb/LegacyChangeNoteWrite.java
@@ -22,10 +22,10 @@
 import com.google.common.annotations.VisibleForTesting;
 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.UsedAt;
 import com.google.gerrit.server.config.GerritServerId;
 import com.google.inject.Inject;
 import java.io.OutputStream;
diff --git a/java/com/google/gerrit/server/notedb/MutableNotesMigration.java b/java/com/google/gerrit/server/notedb/MutableNotesMigration.java
deleted file mode 100644
index 7f4912b..0000000
--- a/java/com/google/gerrit/server/notedb/MutableNotesMigration.java
+++ /dev/null
@@ -1,104 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb;
-
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import java.util.function.Function;
-import org.eclipse.jgit.lib.Config;
-
-/**
- * {@link NotesMigration} with additional methods for altering the migration state at runtime.
- *
- * <p>Almost all callers care only about inspecting the migration state, and for safety should not
- * have access to mutation methods, which must be used with extreme care. Those callers should
- * inject {@link NotesMigration}.
- *
- * <p>Some callers, namely the NoteDb migration pipeline and tests, do need to alter the migration
- * state at runtime, and those callers are expected to take the necessary precautions such as
- * keeping the in-memory and on-disk config state in sync. Those callers use this class.
- *
- * <p>Mutations to the {@link MutableNotesMigration} are guaranteed to be instantly visible to all
- * callers that use the non-mutable {@link NotesMigration}. The current implementation accomplishes
- * this by always binding {@link NotesMigration} to {@link MutableNotesMigration} in Guice, so there
- * is just one {@link NotesMigration} instance process-wide.
- */
-@Singleton
-public class MutableNotesMigration extends NotesMigration {
-  public static MutableNotesMigration newDisabled() {
-    return new MutableNotesMigration(new Config());
-  }
-
-  public static MutableNotesMigration fromConfig(Config cfg) {
-    return new MutableNotesMigration(cfg);
-  }
-
-  @Inject
-  MutableNotesMigration(@GerritServerConfig Config cfg) {
-    super(Snapshot.create(cfg));
-  }
-
-  public MutableNotesMigration setReadChanges(boolean readChanges) {
-    return set(b -> b.setReadChanges(readChanges));
-  }
-
-  public MutableNotesMigration setWriteChanges(boolean writeChanges) {
-    return set(b -> b.setWriteChanges(writeChanges));
-  }
-
-  public MutableNotesMigration setReadChangeSequence(boolean readChangeSequence) {
-    return set(b -> b.setReadChangeSequence(readChangeSequence));
-  }
-
-  public MutableNotesMigration setChangePrimaryStorage(PrimaryStorage changePrimaryStorage) {
-    return set(b -> b.setChangePrimaryStorage(changePrimaryStorage));
-  }
-
-  public MutableNotesMigration setDisableChangeReviewDb(boolean disableChangeReviewDb) {
-    return set(b -> b.setDisableChangeReviewDb(disableChangeReviewDb));
-  }
-
-  public MutableNotesMigration setFailOnLoadForTest(boolean failOnLoadForTest) {
-    return set(b -> b.setFailOnLoadForTest(failOnLoadForTest));
-  }
-
-  /**
-   * Set the in-memory values returned by this instance to match the given state.
-   *
-   * <p>This method is only intended for use by {@link
-   * com.google.gerrit.server.notedb.rebuild.NoteDbMigrator}.
-   *
-   * <p>This <em>only</em> modifies the in-memory state; if this instance was initialized from a
-   * file-based config, the underlying storage is not updated. Callers are responsible for managing
-   * the underlying storage on their own.
-   */
-  public MutableNotesMigration setFrom(NotesMigrationState state) {
-    snapshot.set(state.snapshot());
-    return this;
-  }
-
-  /** @see #setFrom(NotesMigrationState) */
-  public MutableNotesMigration setFrom(NotesMigration other) {
-    snapshot.set(other.snapshot.get());
-    return this;
-  }
-
-  private MutableNotesMigration set(Function<Snapshot.Builder, Snapshot.Builder> f) {
-    snapshot.updateAndGet(s -> f.apply(s.toBuilder()).build());
-    return this;
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/NoteDbChangeState.java b/java/com/google/gerrit/server/notedb/NoteDbChangeState.java
deleted file mode 100644
index e6b82e6..0000000
--- a/java/com/google/gerrit/server/notedb/NoteDbChangeState.java
+++ /dev/null
@@ -1,477 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb;
-
-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.reviewdb.client.RefNames.refsDraftComments;
-import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.NOTE_DB;
-import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.REVIEW_DB;
-import static java.util.Objects.requireNonNull;
-
-import com.google.auto.value.AutoValue;
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Splitter;
-import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Maps;
-import com.google.common.primitives.Longs;
-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.server.ReviewDbUtil;
-import com.google.gerrit.server.git.RefCache;
-import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmRuntimeException;
-import java.io.IOException;
-import java.sql.Timestamp;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.concurrent.TimeUnit;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.ObjectId;
-
-/**
- * The state of all relevant NoteDb refs across all repos corresponding to a given Change entity.
- *
- * <p>Stored serialized in the {@code Change#noteDbState} field, and used to determine whether the
- * state in NoteDb is out of date.
- *
- * <p>Serialized in one of the forms:
- *
- * <ul>
- *   <li>[meta-sha],[account1]=[drafts-sha],[account2]=[drafts-sha]...
- *   <li>R,[meta-sha],[account1]=[drafts-sha],[account2]=[drafts-sha]...
- *   <li>R=[read-only-until],[meta-sha],[account1]=[drafts-sha],[account2]=[drafts-sha]...
- *   <li>N
- *   <li>N=[read-only-until]
- * </ul>
- *
- * in numeric account ID order, with hex SHA-1s for human readability.
- */
-public class NoteDbChangeState {
-  public static final String NOTE_DB_PRIMARY_STATE = "N";
-
-  public enum PrimaryStorage {
-    REVIEW_DB('R'),
-    NOTE_DB('N');
-
-    private final char code;
-
-    PrimaryStorage(char code) {
-      this.code = code;
-    }
-
-    public static PrimaryStorage of(@Nullable Change c) {
-      return of(NoteDbChangeState.parse(c));
-    }
-
-    public static PrimaryStorage of(@Nullable NoteDbChangeState s) {
-      return s != null ? s.getPrimaryStorage() : REVIEW_DB;
-    }
-  }
-
-  @AutoValue
-  public abstract static class Delta {
-    @VisibleForTesting
-    public static Delta create(
-        Change.Id changeId,
-        Optional<ObjectId> newChangeMetaId,
-        Map<Account.Id, ObjectId> newDraftIds) {
-      if (newDraftIds == null) {
-        newDraftIds = ImmutableMap.of();
-      }
-      return new AutoValue_NoteDbChangeState_Delta(
-          changeId, newChangeMetaId, ImmutableMap.copyOf(newDraftIds));
-    }
-
-    abstract Change.Id changeId();
-
-    abstract Optional<ObjectId> newChangeMetaId();
-
-    abstract ImmutableMap<Account.Id, ObjectId> newDraftIds();
-  }
-
-  @AutoValue
-  public abstract static class RefState {
-    @VisibleForTesting
-    public static RefState create(ObjectId changeMetaId, Map<Account.Id, ObjectId> draftIds) {
-      return new AutoValue_NoteDbChangeState_RefState(
-          changeMetaId.copy(),
-          ImmutableMap.copyOf(Maps.filterValues(draftIds, id -> !ObjectId.zeroId().equals(id))));
-    }
-
-    private static Optional<RefState> parse(Change.Id changeId, List<String> parts) {
-      checkArgument(!parts.isEmpty(), "missing state string for change %s", changeId);
-      ObjectId changeMetaId = ObjectId.fromString(parts.get(0));
-      Map<Account.Id, ObjectId> draftIds = Maps.newHashMapWithExpectedSize(parts.size() - 1);
-      Splitter s = Splitter.on('=');
-      for (int i = 1; i < parts.size(); i++) {
-        String p = parts.get(i);
-        List<String> draftParts = s.splitToList(p);
-        checkArgument(
-            draftParts.size() == 2, "invalid draft state part for change %s: %s", changeId, p);
-        Optional<Account.Id> accountId = Account.Id.tryParse(draftParts.get(0));
-        checkArgument(
-            accountId.isPresent(),
-            "invalid account ID in draft state part for change %s: %s",
-            changeId,
-            p);
-        draftIds.put(accountId.get(), ObjectId.fromString(draftParts.get(1)));
-      }
-      return Optional.of(create(changeMetaId, draftIds));
-    }
-
-    abstract ObjectId changeMetaId();
-
-    abstract ImmutableMap<Account.Id, ObjectId> draftIds();
-
-    @Override
-    public String toString() {
-      return appendTo(new StringBuilder()).toString();
-    }
-
-    StringBuilder appendTo(StringBuilder sb) {
-      sb.append(changeMetaId().name());
-      for (Account.Id id : ReviewDbUtil.intKeyOrdering().sortedCopy(draftIds().keySet())) {
-        sb.append(',').append(id.get()).append('=').append(draftIds().get(id).name());
-      }
-      return sb;
-    }
-  }
-
-  public static NoteDbChangeState parse(@Nullable Change c) {
-    return c != null ? parse(c.getId(), c.getNoteDbState()) : null;
-  }
-
-  @VisibleForTesting
-  public static NoteDbChangeState parse(Change.Id id, @Nullable String str) {
-    if (Strings.isNullOrEmpty(str)) {
-      // Return null rather than Optional as this is what goes in the field in
-      // ReviewDb.
-      return null;
-    }
-    List<String> parts = Splitter.on(',').splitToList(str);
-    String first = parts.get(0);
-    Optional<Timestamp> readOnlyUntil = parseReadOnlyUntil(id, str, first);
-
-    // Only valid NOTE_DB state is "N".
-    if (parts.size() == 1 && first.charAt(0) == NOTE_DB.code) {
-      return new NoteDbChangeState(id, NOTE_DB, Optional.empty(), readOnlyUntil);
-    }
-
-    // Otherwise it must be REVIEW_DB, either "R,<RefState>" or just
-    // "<RefState>". Allow length > 0 for forward compatibility.
-    if (first.length() > 0) {
-      Optional<RefState> refState;
-      if (first.charAt(0) == REVIEW_DB.code) {
-        refState = RefState.parse(id, parts.subList(1, parts.size()));
-      } else {
-        refState = RefState.parse(id, parts);
-      }
-      return new NoteDbChangeState(id, REVIEW_DB, refState, readOnlyUntil);
-    }
-    throw invalidState(id, str);
-  }
-
-  private static Optional<Timestamp> parseReadOnlyUntil(
-      Change.Id id, String fullStr, String first) {
-    if (first.length() > 2 && first.charAt(1) == '=') {
-      Long ts = Longs.tryParse(first.substring(2));
-      if (ts == null) {
-        throw invalidState(id, fullStr);
-      }
-      return Optional.of(new Timestamp(ts));
-    }
-    return Optional.empty();
-  }
-
-  private static IllegalArgumentException invalidState(Change.Id id, String str) {
-    return new IllegalArgumentException("invalid state string for change " + id + ": " + str);
-  }
-
-  /**
-   * Apply a delta to the state stored in a change entity.
-   *
-   * <p>This method does not check whether the old state was read-only; it is up to the caller to
-   * not violate read-only semantics when storing the change back in ReviewDb.
-   *
-   * @param change change entity. The delta is applied against this entity's {@code noteDbState} and
-   *     the new state is stored back in the entity as a side effect.
-   * @param delta delta to apply.
-   * @return new state, equivalent to what is stored in {@code change} as a side effect.
-   */
-  public static NoteDbChangeState applyDelta(Change change, Delta delta) {
-    if (delta == null) {
-      return null;
-    }
-    String oldStr = change.getNoteDbState();
-    if (oldStr == null && !delta.newChangeMetaId().isPresent()) {
-      // Neither an old nor a new meta ID was present, most likely because we
-      // aren't writing a NoteDb graph at all for this change at this point. No
-      // point in proceeding.
-      return null;
-    }
-    NoteDbChangeState oldState = parse(change.getId(), oldStr);
-    if (oldState != null && oldState.getPrimaryStorage() == NOTE_DB) {
-      // NOTE_DB state doesn't include RefState, so applying a delta is a no-op.
-      return oldState;
-    }
-
-    ObjectId changeMetaId;
-    if (delta.newChangeMetaId().isPresent()) {
-      changeMetaId = delta.newChangeMetaId().get();
-      if (changeMetaId.equals(ObjectId.zeroId())) {
-        change.setNoteDbState(null);
-        return null;
-      }
-    } else {
-      changeMetaId = oldState.getChangeMetaId();
-    }
-
-    Map<Account.Id, ObjectId> draftIds = new HashMap<>();
-    if (oldState != null) {
-      draftIds.putAll(oldState.getDraftIds());
-    }
-    for (Map.Entry<Account.Id, ObjectId> e : delta.newDraftIds().entrySet()) {
-      if (e.getValue().equals(ObjectId.zeroId())) {
-        draftIds.remove(e.getKey());
-      } else {
-        draftIds.put(e.getKey(), e.getValue());
-      }
-    }
-
-    NoteDbChangeState state =
-        new NoteDbChangeState(
-            change.getId(),
-            oldState != null ? oldState.getPrimaryStorage() : REVIEW_DB,
-            Optional.of(RefState.create(changeMetaId, draftIds)),
-            // Copy old read-only deadline rather than advancing it; the caller is
-            // still responsible for finishing the rest of its work before the lease
-            // runs out.
-            oldState != null ? oldState.getReadOnlyUntil() : Optional.empty());
-    change.setNoteDbState(state.toString());
-    return state;
-  }
-
-  // TODO(dborowitz): Ugly. Refactor these static methods into a Checker class
-  // or something. They do not belong in NoteDbChangeState itself because:
-  //  - need to inject Config but don't want a whole Factory
-  //  - can't be methods on NoteDbChangeState because state is nullable (though
-  //    we could also solve this by inventing an empty-but-non-null state)
-  // Also we should clean up duplicated code between static/non-static methods.
-  public static boolean isChangeUpToDate(
-      @Nullable NoteDbChangeState state, RefCache changeRepoRefs, Change.Id changeId)
-      throws IOException {
-    if (PrimaryStorage.of(state) == NOTE_DB) {
-      return true; // Primary storage is NoteDb, up to date by definition.
-    }
-    if (state == null) {
-      return !changeRepoRefs.get(changeMetaRef(changeId)).isPresent();
-    }
-    return state.isChangeUpToDate(changeRepoRefs);
-  }
-
-  public static boolean areDraftsUpToDate(
-      @Nullable NoteDbChangeState state,
-      RefCache draftsRepoRefs,
-      Change.Id changeId,
-      Account.Id accountId)
-      throws IOException {
-    if (PrimaryStorage.of(state) == NOTE_DB) {
-      return true; // Primary storage is NoteDb, up to date by definition.
-    }
-    if (state == null) {
-      return !draftsRepoRefs.get(refsDraftComments(changeId, accountId)).isPresent();
-    }
-    return state.areDraftsUpToDate(draftsRepoRefs, accountId);
-  }
-
-  public static long getReadOnlySkew(Config cfg) {
-    return cfg.getTimeUnit("notedb", null, "maxTimestampSkew", 1000, TimeUnit.MILLISECONDS);
-  }
-
-  static Timestamp timeForReadOnlyCheck(long skewMs) {
-    // Subtract some slop in case the machine that set the change's read-only
-    // lease has a clock behind ours.
-    return new Timestamp(TimeUtil.nowMs() - skewMs);
-  }
-
-  public static void checkNotReadOnly(@Nullable Change change, long skewMs) {
-    checkNotReadOnly(parse(change), skewMs);
-  }
-
-  public static void checkNotReadOnly(@Nullable NoteDbChangeState state, long skewMs) {
-    if (state == null) {
-      return; // No state means ReviewDb primary non-read-only.
-    } else if (state.isReadOnly(timeForReadOnlyCheck(skewMs))) {
-      throw new OrmRuntimeException(
-          "change "
-              + state.getChangeId()
-              + " is read-only until "
-              + state.getReadOnlyUntil().get());
-    }
-  }
-
-  private final Change.Id changeId;
-  private final PrimaryStorage primaryStorage;
-  private final Optional<RefState> refState;
-  private final Optional<Timestamp> readOnlyUntil;
-
-  public NoteDbChangeState(
-      Change.Id changeId,
-      PrimaryStorage primaryStorage,
-      Optional<RefState> refState,
-      Optional<Timestamp> readOnlyUntil) {
-    this.changeId = requireNonNull(changeId);
-    this.primaryStorage = requireNonNull(primaryStorage);
-    this.refState = requireNonNull(refState);
-    this.readOnlyUntil = requireNonNull(readOnlyUntil);
-
-    switch (primaryStorage) {
-      case REVIEW_DB:
-        checkArgument(
-            refState.isPresent(),
-            "expected RefState for change %s with primary storage %s",
-            changeId,
-            primaryStorage);
-        break;
-      case NOTE_DB:
-        checkArgument(
-            !refState.isPresent(),
-            "expected no RefState for change %s with primary storage %s",
-            changeId,
-            primaryStorage);
-        break;
-      default:
-        throw new IllegalStateException("invalid PrimaryStorage: " + primaryStorage);
-    }
-  }
-
-  public PrimaryStorage getPrimaryStorage() {
-    return primaryStorage;
-  }
-
-  public boolean isChangeUpToDate(RefCache changeRepoRefs) throws IOException {
-    if (primaryStorage == NOTE_DB) {
-      return true; // Primary storage is NoteDb, up to date by definition.
-    }
-    Optional<ObjectId> id = changeRepoRefs.get(changeMetaRef(changeId));
-    if (!id.isPresent()) {
-      return getChangeMetaId().equals(ObjectId.zeroId());
-    }
-    return id.get().equals(getChangeMetaId());
-  }
-
-  public boolean areDraftsUpToDate(RefCache draftsRepoRefs, Account.Id accountId)
-      throws IOException {
-    if (primaryStorage == NOTE_DB) {
-      return true; // Primary storage is NoteDb, up to date by definition.
-    }
-    Optional<ObjectId> id = draftsRepoRefs.get(refsDraftComments(changeId, accountId));
-    if (!id.isPresent()) {
-      return !getDraftIds().containsKey(accountId);
-    }
-    return id.get().equals(getDraftIds().get(accountId));
-  }
-
-  public boolean isUpToDate(RefCache changeRepoRefs, RefCache draftsRepoRefs) throws IOException {
-    if (primaryStorage == NOTE_DB) {
-      return true; // Primary storage is NoteDb, up to date by definition.
-    }
-    if (!isChangeUpToDate(changeRepoRefs)) {
-      return false;
-    }
-    for (Account.Id accountId : getDraftIds().keySet()) {
-      if (!areDraftsUpToDate(draftsRepoRefs, accountId)) {
-        return false;
-      }
-    }
-    return true;
-  }
-
-  public boolean isReadOnly(Timestamp now) {
-    return readOnlyUntil.isPresent() && now.before(readOnlyUntil.get());
-  }
-
-  public Optional<Timestamp> getReadOnlyUntil() {
-    return readOnlyUntil;
-  }
-
-  public NoteDbChangeState withReadOnlyUntil(Timestamp ts) {
-    return new NoteDbChangeState(changeId, primaryStorage, refState, Optional.of(ts));
-  }
-
-  public Change.Id getChangeId() {
-    return changeId;
-  }
-
-  public ObjectId getChangeMetaId() {
-    return refState().changeMetaId();
-  }
-
-  public ImmutableMap<Account.Id, ObjectId> getDraftIds() {
-    return refState().draftIds();
-  }
-
-  public Optional<RefState> getRefState() {
-    return refState;
-  }
-
-  private RefState refState() {
-    checkState(refState.isPresent(), "state for %s has no RefState: %s", changeId, this);
-    return refState.get();
-  }
-
-  @Override
-  public String toString() {
-    switch (primaryStorage) {
-      case REVIEW_DB:
-        if (!readOnlyUntil.isPresent()) {
-          // Don't include enum field, just IDs (though parse would accept it).
-          return refState().toString();
-        }
-        return primaryStorage.code + "=" + readOnlyUntil.get().getTime() + "," + refState.get();
-      case NOTE_DB:
-        if (!readOnlyUntil.isPresent()) {
-          return NOTE_DB_PRIMARY_STATE;
-        }
-        return primaryStorage.code + "=" + readOnlyUntil.get().getTime();
-      default:
-        throw new IllegalArgumentException("Unsupported PrimaryStorage: " + primaryStorage);
-    }
-  }
-
-  @Override
-  public int hashCode() {
-    return Objects.hash(changeId, primaryStorage, refState, readOnlyUntil);
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    if (!(o instanceof NoteDbChangeState)) {
-      return false;
-    }
-    NoteDbChangeState s = (NoteDbChangeState) o;
-    return changeId.equals(s.changeId)
-        && primaryStorage.equals(s.primaryStorage)
-        && refState.equals(s.refState)
-        && readOnlyUntil.equals(s.readOnlyUntil);
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/NoteDbMetrics.java b/java/com/google/gerrit/server/notedb/NoteDbMetrics.java
index be06d11..61f475f 100644
--- a/java/com/google/gerrit/server/notedb/NoteDbMetrics.java
+++ b/java/com/google/gerrit/server/notedb/NoteDbMetrics.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.notedb;
 
-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;
@@ -44,16 +43,6 @@
    */
   final Timer1<NoteDbTable> parseLatency;
 
-  /**
-   * Latency due to auto-rebuilding entities when out of date.
-   *
-   * <p>Excludes latency from reading ref to check whether the entity is up to date.
-   */
-  final Timer1<NoteDbTable> autoRebuildLatency;
-
-  /** Count of auto-rebuild attempts that failed. */
-  final Counter1<NoteDbTable> autoRebuildFailureCount;
-
   @Inject
   NoteDbMetrics(MetricMaker metrics) {
     Field<NoteDbTable> view = Field.ofEnum(NoteDbTable.class, "table");
@@ -89,19 +78,5 @@
                 .setCumulative()
                 .setUnit(Units.MICROSECONDS),
             view);
-
-    autoRebuildLatency =
-        metrics.newTimer(
-            "notedb/auto_rebuild_latency",
-            new Description("NoteDb auto-rebuilding latency by table")
-                .setCumulative()
-                .setUnit(Units.MILLISECONDS),
-            view);
-
-    autoRebuildFailureCount =
-        metrics.newCounter(
-            "notedb/auto_rebuild_failure_count",
-            new Description("NoteDb auto-rebuilding attempts that failed by table").setCumulative(),
-            view);
   }
 }
diff --git a/java/com/google/gerrit/server/notedb/NoteDbModule.java b/java/com/google/gerrit/server/notedb/NoteDbModule.java
index c76c39b..d8a5fd5 100644
--- a/java/com/google/gerrit/server/notedb/NoteDbModule.java
+++ b/java/com/google/gerrit/server/notedb/NoteDbModule.java
@@ -17,33 +17,21 @@
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheBuilder;
 import com.google.gerrit.extensions.config.FactoryModule;
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Change.Id;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.notedb.NoteDbUpdateManager.Result;
-import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder;
-import com.google.gerrit.server.notedb.rebuild.ChangeRebuilderImpl;
-import com.google.gerrit.server.notedb.rebuild.NotesMigrationStateListener;
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Names;
-import org.eclipse.jgit.lib.Config;
 
 public class NoteDbModule extends FactoryModule {
-  private final Config cfg;
   private final boolean useTestBindings;
 
-  static NoteDbModule forTest(Config cfg) {
-    return new NoteDbModule(cfg, true);
+  static NoteDbModule forTest() {
+    return new NoteDbModule(true);
   }
 
-  public NoteDbModule(Config cfg) {
-    this(cfg, false);
+  public NoteDbModule() {
+    this(false);
   }
 
-  private NoteDbModule(Config cfg, boolean useTestBindings) {
-    this.cfg = cfg;
+  private NoteDbModule(boolean useTestBindings) {
     this.useTestBindings = useTestBindings;
   }
 
@@ -56,60 +44,13 @@
     factory(NoteDbUpdateManager.Factory.class);
     factory(RobotCommentNotes.Factory.class);
     factory(RobotCommentUpdate.Factory.class);
-    DynamicSet.setOf(binder(), NotesMigrationStateListener.class);
 
     if (!useTestBindings) {
       install(ChangeNotesCache.module());
-      if (cfg.getBoolean("noteDb", null, "testRebuilderWrapper", false)) {
-        // Yes, another variety of test bindings with a different way of
-        // configuring it.
-        bind(ChangeRebuilder.class).to(TestChangeRebuilderWrapper.class);
-      } else {
-        bind(ChangeRebuilder.class).to(ChangeRebuilderImpl.class);
-      }
     } else {
-      bind(ChangeRebuilder.class)
-          .toInstance(
-              new ChangeRebuilder(null) {
-                @Override
-                public Result rebuild(ReviewDb db, Change.Id changeId) {
-                  return null;
-                }
-
-                @Override
-                public Result rebuildEvenIfReadOnly(ReviewDb db, Id changeId) {
-                  return null;
-                }
-
-                @Override
-                public Result rebuild(NoteDbUpdateManager manager, ChangeBundle bundle) {
-                  return null;
-                }
-
-                @Override
-                public NoteDbUpdateManager stage(ReviewDb db, Change.Id changeId) {
-                  return null;
-                }
-
-                @Override
-                public Result execute(
-                    ReviewDb db, Change.Id changeId, NoteDbUpdateManager manager) {
-                  return null;
-                }
-
-                @Override
-                public void buildUpdates(NoteDbUpdateManager manager, ChangeBundle bundle) {
-                  // Do nothing.
-                }
-
-                @Override
-                public void rebuildReviewDb(ReviewDb db, Project.NameKey project, Id changeId) {
-                  // Do nothing.
-                }
-              });
       bind(new TypeLiteral<Cache<ChangeNotesCache.Key, ChangeNotesState>>() {})
           .annotatedWith(Names.named(ChangeNotesCache.CACHE_NAME))
-          .toInstance(CacheBuilder.newBuilder().<ChangeNotesCache.Key, ChangeNotesState>build());
+          .toInstance(CacheBuilder.newBuilder().build());
     }
   }
 }
diff --git a/java/com/google/gerrit/server/notedb/NoteDbRewriter.java b/java/com/google/gerrit/server/notedb/NoteDbRewriter.java
index 3c7b0a3..19754d1 100644
--- a/java/com/google/gerrit/server/notedb/NoteDbRewriter.java
+++ b/java/com/google/gerrit/server/notedb/NoteDbRewriter.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.notedb;
 
-import com.google.gwtorm.server.OrmException;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.ObjectId;
@@ -35,5 +34,5 @@
    * @return the {@code ObjectId} of the ref's new tip commit.
    */
   ObjectId rewriteCommitHistory(RevWalk revWalk, ObjectInserter inserter, ObjectId currTip)
-      throws IOException, ConfigInvalidException, OrmException;
+      throws IOException, ConfigInvalidException;
 }
diff --git a/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java b/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
index 12448fb..ea42a9d 100644
--- a/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
+++ b/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
@@ -17,21 +17,18 @@
 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.REFS_DRAFT_COMMENTS;
 import static com.google.gerrit.server.notedb.NoteDbTable.CHANGES;
 import static java.util.Objects.requireNonNull;
 
-import com.google.auto.value.AutoValue;
-import com.google.common.collect.HashBasedTable;
 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.common.collect.Table;
 import com.google.gerrit.common.Nullable;
+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.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -40,18 +37,13 @@
 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.notedb.NoteDbChangeState.PrimaryStorage;
 import com.google.gerrit.server.update.ChainedReceiveCommands;
-import com.google.gerrit.server.update.RefUpdateUtil;
 import com.google.gerrit.server.update.RetryingRestModifyView;
-import com.google.gwtorm.server.OrmConcurrencyException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Optional;
@@ -78,8 +70,6 @@
  * {@link #stage()}.
  */
 public class NoteDbUpdateManager implements AutoCloseable {
-  public static final String CHANGES_READ_ONLY = "NoteDb changes are read-only";
-
   private static final ImmutableList<String> PACKAGE_PREFIXES =
       ImmutableList.of("com.google.gerrit.server.", "com.google.gerrit.");
   private static final ImmutableSet<String> SERVLET_NAMES =
@@ -90,77 +80,6 @@
     NoteDbUpdateManager create(Project.NameKey projectName);
   }
 
-  @AutoValue
-  public abstract static class StagedResult {
-    private static StagedResult create(
-        Change.Id id, NoteDbChangeState.Delta delta, OpenRepo changeRepo, OpenRepo allUsersRepo) {
-      ImmutableList<ReceiveCommand> changeCommands = ImmutableList.of();
-      ImmutableList<InsertedObject> changeObjects = ImmutableList.of();
-      if (changeRepo != null) {
-        changeCommands = changeRepo.getCommandsSnapshot();
-        changeObjects = changeRepo.getInsertedObjects();
-      }
-      ImmutableList<ReceiveCommand> allUsersCommands = ImmutableList.of();
-      ImmutableList<InsertedObject> allUsersObjects = ImmutableList.of();
-      if (allUsersRepo != null) {
-        allUsersCommands = allUsersRepo.getCommandsSnapshot();
-        allUsersObjects = allUsersRepo.getInsertedObjects();
-      }
-      return new AutoValue_NoteDbUpdateManager_StagedResult(
-          id, delta,
-          changeCommands, changeObjects,
-          allUsersCommands, allUsersObjects);
-    }
-
-    public abstract Change.Id id();
-
-    @Nullable
-    public abstract NoteDbChangeState.Delta delta();
-
-    public abstract ImmutableList<ReceiveCommand> changeCommands();
-
-    /**
-     * Objects inserted into the change repo for this change.
-     *
-     * <p>Includes all objects inserted for any change in this repo that may have been processed by
-     * the corresponding {@link NoteDbUpdateManager} instance, not just those objects that were
-     * inserted to handle this specific change's updates.
-     *
-     * @return inserted objects, or null if the corresponding {@link NoteDbUpdateManager} was
-     *     configured not to {@link NoteDbUpdateManager#setSaveObjects(boolean) save objects}.
-     */
-    @Nullable
-    public abstract ImmutableList<InsertedObject> changeObjects();
-
-    public abstract ImmutableList<ReceiveCommand> allUsersCommands();
-
-    /**
-     * Objects inserted into the All-Users repo for this change.
-     *
-     * <p>Includes all objects inserted into All-Users for any change that may have been processed
-     * by the corresponding {@link NoteDbUpdateManager} instance, not just those objects that were
-     * inserted to handle this specific change's updates.
-     *
-     * @return inserted objects, or null if the corresponding {@link NoteDbUpdateManager} was
-     *     configured not to {@link NoteDbUpdateManager#setSaveObjects(boolean) save objects}.
-     */
-    @Nullable
-    public abstract ImmutableList<InsertedObject> allUsersObjects();
-  }
-
-  @AutoValue
-  public abstract static class Result {
-    static Result create(NoteDbUpdateManager.StagedResult staged, NoteDbChangeState newState) {
-      return new AutoValue_NoteDbUpdateManager_Result(newState, staged);
-    }
-
-    @Nullable
-    public abstract NoteDbChangeState newState();
-
-    @Nullable
-    abstract NoteDbUpdateManager.StagedResult staged();
-  }
-
   public static class OpenRepo implements AutoCloseable {
     public final Repository repo;
     public final RevWalk rw;
@@ -171,15 +90,13 @@
     @Nullable private final ObjectInserter finalIns;
 
     private final boolean close;
-    private final boolean saveObjects;
 
     private OpenRepo(
         Repository repo,
         RevWalk rw,
         @Nullable ObjectInserter ins,
         ChainedReceiveCommands cmds,
-        boolean close,
-        boolean saveObjects) {
+        boolean close) {
       ObjectReader reader = rw.getObjectReader();
       checkArgument(
           ins == null || reader.getCreatedFromInserter() == ins,
@@ -188,44 +105,25 @@
           reader.getCreatedFromInserter());
       this.repo = requireNonNull(repo);
 
-      if (saveObjects) {
-        this.inMemIns = new InMemoryInserter(rw.getObjectReader());
-        this.tempIns = inMemIns;
-      } else {
-        checkArgument(ins != null);
-        this.inMemIns = null;
-        this.tempIns = ins;
-      }
+      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;
-      this.saveObjects = saveObjects;
     }
 
     public Optional<ObjectId> getObjectId(String refName) throws IOException {
       return cmds.get(refName);
     }
 
-    ImmutableList<ReceiveCommand> getCommandsSnapshot() {
-      return ImmutableList.copyOf(cmds.getCommands().values());
-    }
-
-    @Nullable
-    ImmutableList<InsertedObject> getInsertedObjects() {
-      return saveObjects ? inMemIns.getInsertedObjects() : null;
-    }
-
     void flush() throws IOException {
       flushToFinalInserter();
       finalIns.flush();
     }
 
     void flushToFinalInserter() throws IOException {
-      if (!saveObjects) {
-        return;
-      }
       checkState(finalIns != null);
       for (InsertedObject obj : inMemIns.getInsertedObjects()) {
         finalIns.insert(obj.type(), obj.data().toByteArray());
@@ -248,7 +146,6 @@
 
   private final Provider<PersonIdent> serverIdent;
   private final GitRepositoryManager repoManager;
-  private final NotesMigration migration;
   private final AllUsersName allUsersName;
   private final NoteDbMetrics metrics;
   private final Project.NameKey projectName;
@@ -260,10 +157,7 @@
 
   private OpenRepo changeRepo;
   private OpenRepo allUsersRepo;
-  private Map<Change.Id, StagedResult> staged;
-  private boolean checkExpectedState = true;
-  private boolean saveObjects = true;
-  private boolean atomicRefUpdates = true;
+  private boolean executed;
   private String refLogMessage;
   private PersonIdent refLogIdent;
   private PushCertificate pushCert;
@@ -272,13 +166,11 @@
   NoteDbUpdateManager(
       @GerritPersonIdent Provider<PersonIdent> serverIdent,
       GitRepositoryManager repoManager,
-      NotesMigration migration,
       AllUsersName allUsersName,
       NoteDbMetrics metrics,
       @Assisted Project.NameKey projectName) {
     this.serverIdent = serverIdent;
     this.repoManager = repoManager;
-    this.migration = migration;
     this.allUsersName = allUsersName;
     this.metrics = metrics;
     this.projectName = projectName;
@@ -309,50 +201,7 @@
   public NoteDbUpdateManager setChangeRepo(
       Repository repo, RevWalk rw, @Nullable ObjectInserter ins, ChainedReceiveCommands cmds) {
     checkState(changeRepo == null, "change repo already initialized");
-    changeRepo = new OpenRepo(repo, rw, ins, cmds, false, saveObjects);
-    return this;
-  }
-
-  public NoteDbUpdateManager setAllUsersRepo(
-      Repository repo, RevWalk rw, @Nullable ObjectInserter ins, ChainedReceiveCommands cmds) {
-    checkState(allUsersRepo == null, "All-Users repo already initialized");
-    allUsersRepo = new OpenRepo(repo, rw, ins, cmds, false, saveObjects);
-    return this;
-  }
-
-  public NoteDbUpdateManager setCheckExpectedState(boolean checkExpectedState) {
-    this.checkExpectedState = checkExpectedState;
-    return this;
-  }
-
-  /**
-   * Set whether to save objects and make them available in {@link StagedResult}s.
-   *
-   * <p>If set, all objects inserted into all repos managed by this instance will be buffered in
-   * memory, and the {@link StagedResult}s will return non-null lists from {@link
-   * StagedResult#changeObjects()} and {@link StagedResult#allUsersObjects()}.
-   *
-   * <p>Not recommended if modifying a large number of changes with a single manager.
-   *
-   * @param saveObjects whether to save objects; defaults to true.
-   * @return this
-   */
-  public NoteDbUpdateManager setSaveObjects(boolean saveObjects) {
-    this.saveObjects = saveObjects;
-    return this;
-  }
-
-  /**
-   * Set whether to use atomic ref updates.
-   *
-   * <p>Can be set to false when the change updates represented by this manager aren't logically
-   * related, e.g. when the updater is only used to group objects together with a single inserter.
-   *
-   * @param atomicRefUpdates whether to use atomic ref updates; defaults to true.
-   * @return this
-   */
-  public NoteDbUpdateManager setAtomicRefUpdates(boolean atomicRefUpdates) {
-    this.atomicRefUpdates = atomicRefUpdates;
+    changeRepo = new OpenRepo(repo, rw, ins, cmds, false);
     return this;
   }
 
@@ -385,16 +234,6 @@
     return this;
   }
 
-  public OpenRepo getChangeRepo() throws IOException {
-    initChangeRepo();
-    return changeRepo;
-  }
-
-  public OpenRepo getAllUsersRepo() throws IOException {
-    initAllUsersRepo();
-    return allUsersRepo;
-  }
-
   private void initChangeRepo() throws IOException {
     if (changeRepo == null) {
       changeRepo = openRepo(projectName);
@@ -412,7 +251,7 @@
     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, saveObjects) {
+      return new OpenRepo(repo, rw, ins, new ChainedReceiveCommands(repo), true) {
         @Override
         public void close() {
           reader.close();
@@ -423,9 +262,6 @@
   }
 
   private boolean isEmpty() {
-    if (!migration.commitChangeWrites()) {
-      return true;
-    }
     return changeUpdates.isEmpty()
         && draftUpdates.isEmpty()
         && robotCommentUpdates.isEmpty()
@@ -448,12 +284,12 @@
    * @param update the update to add.
    */
   public void add(ChangeUpdate update) {
+    checkNotExecuted();
     checkArgument(
         update.getProjectName().equals(projectName),
         "update for project %s cannot be added to manager for project %s",
         update.getProjectName(),
         projectName);
-    checkState(staged == null, "cannot add new update after staging");
     checkArgument(
         !rewriters.containsKey(update.getRefName()),
         "cannot update & rewrite ref %s in one BatchUpdate",
@@ -500,105 +336,36 @@
   }
 
   public void add(ChangeDraftUpdate draftUpdate) {
-    checkState(staged == null, "cannot add new update after staging");
+    checkNotExecuted();
     draftUpdates.put(draftUpdate.getRefName(), draftUpdate);
   }
 
   public void deleteChange(Change.Id id) {
-    checkState(staged == null, "cannot add new change to delete after staging");
+    checkNotExecuted();
     toDelete.add(id);
   }
 
   /**
    * Stage updates in the manager's internal list of commands.
    *
-   * @return map of the state that would get written to the applicable repo(s) for each affected
-   *     change.
-   * @throws OrmException if a database layer error occurs.
    * @throws IOException if a storage layer error occurs.
    */
-  public Map<Change.Id, StagedResult> stage() throws OrmException, IOException {
-    if (staged != null) {
-      return staged;
-    }
+  private void stage() throws IOException {
     try (Timer1.Context timer = metrics.stageUpdateLatency.start(CHANGES)) {
-      staged = new HashMap<>();
       if (isEmpty()) {
-        return staged;
+        return;
       }
 
       initChangeRepo();
       if (!draftUpdates.isEmpty() || !toDelete.isEmpty()) {
         initAllUsersRepo();
       }
-      checkExpectedState();
       addCommands();
-
-      Table<Change.Id, Account.Id, ObjectId> allDraftIds = getDraftIds();
-      Set<Change.Id> changeIds = new HashSet<>();
-      for (ReceiveCommand cmd : changeRepo.getCommandsSnapshot()) {
-        Change.Id changeId = Change.Id.fromRef(cmd.getRefName());
-        if (changeId == null || !cmd.getRefName().equals(RefNames.changeMetaRef(changeId))) {
-          // Not a meta ref update, likely due to a repo update along with the change meta update.
-          continue;
-        }
-        changeIds.add(changeId);
-        Optional<ObjectId> metaId = Optional.of(cmd.getNewId());
-        staged.put(
-            changeId,
-            StagedResult.create(
-                changeId,
-                NoteDbChangeState.Delta.create(
-                    changeId, metaId, allDraftIds.rowMap().remove(changeId)),
-                changeRepo,
-                allUsersRepo));
-      }
-
-      for (Map.Entry<Change.Id, Map<Account.Id, ObjectId>> e : allDraftIds.rowMap().entrySet()) {
-        // If a change remains in the table at this point, it means we are
-        // updating its drafts but not the change itself.
-        StagedResult r =
-            StagedResult.create(
-                e.getKey(),
-                NoteDbChangeState.Delta.create(e.getKey(), Optional.empty(), e.getValue()),
-                changeRepo,
-                allUsersRepo);
-        checkState(
-            r.changeCommands().isEmpty(),
-            "should not have change commands when updating only drafts: %s",
-            r);
-        staged.put(r.id(), r);
-      }
-
-      return staged;
     }
   }
 
-  public Result stageAndApplyDelta(Change change) throws OrmException, IOException {
-    StagedResult sr = stage().get(change.getId());
-    NoteDbChangeState newState =
-        NoteDbChangeState.applyDelta(change, sr != null ? sr.delta() : null);
-    return Result.create(sr, newState);
-  }
-
-  private Table<Change.Id, Account.Id, ObjectId> getDraftIds() {
-    Table<Change.Id, Account.Id, ObjectId> draftIds = HashBasedTable.create();
-    if (allUsersRepo == null) {
-      return draftIds;
-    }
-    for (ReceiveCommand cmd : allUsersRepo.getCommandsSnapshot()) {
-      String r = cmd.getRefName();
-      if (r.startsWith(REFS_DRAFT_COMMENTS)) {
-        Change.Id changeId = Change.Id.fromRefPart(r.substring(REFS_DRAFT_COMMENTS.length()));
-        Account.Id accountId = Account.Id.fromRefSuffix(r);
-        checkDraftRef(accountId != null && changeId != null, r);
-        draftIds.put(changeId, accountId, cmd.getNewId());
-      }
-    }
-    return draftIds;
-  }
-
   public void flush() throws IOException {
+    checkNotExecuted();
     if (changeRepo != null) {
       changeRepo.flush();
     }
@@ -608,17 +375,15 @@
   }
 
   @Nullable
-  public BatchRefUpdate execute() throws OrmException, IOException {
+  public BatchRefUpdate execute() throws IOException {
     return execute(false);
   }
 
   @Nullable
-  public BatchRefUpdate execute(boolean dryrun) throws OrmException, IOException {
-    // Check before even inspecting the list, as this is a programmer error.
-    if (migration.failChangeWrites()) {
-      throw new OrmException(CHANGES_READ_ONLY);
-    }
+  public BatchRefUpdate execute(boolean dryrun) throws IOException {
+    checkNotExecuted();
     if (isEmpty()) {
+      executed = true;
       return null;
     }
     try (Timer1.Context timer = metrics.updateLatency.start(CHANGES)) {
@@ -633,6 +398,7 @@
       // comments can only go from DRAFT to PUBLISHED, not vice versa.
       BatchRefUpdate result = execute(changeRepo, dryrun, pushCert);
       execute(allUsersRepo, dryrun, null);
+      executed = true;
       return result;
     } finally {
       close();
@@ -660,7 +426,7 @@
       bru.setRefLogMessage(firstNonNull(guessRestApiHandler(), "Update NoteDb refs"), false);
     }
     bru.setRefLogIdent(refLogIdent != null ? refLogIdent : serverIdent.get());
-    bru.setAtomic(atomicRefUpdates);
+    bru.setAtomic(true);
     or.cmds.addTo(bru);
     bru.setAllowNonFastForwards(true);
 
@@ -709,7 +475,7 @@
     return -1;
   }
 
-  private void addCommands() throws OrmException, IOException {
+  private void addCommands() throws IOException {
     if (isEmpty()) {
       return;
     }
@@ -731,7 +497,6 @@
     for (Change.Id id : toDelete) {
       doDelete(id);
     }
-    checkExpectedState();
   }
 
   private void doDelete(Change.Id id) throws IOException {
@@ -751,78 +516,12 @@
     }
   }
 
-  public static class MismatchedStateException extends OrmException {
-    private static final long serialVersionUID = 1L;
-
-    private MismatchedStateException(Change.Id id, NoteDbChangeState expectedState) {
-      super(
-          String.format(
-              "cannot apply NoteDb updates for change %s; change meta ref does not match %s",
-              id, expectedState.getChangeMetaId().name()));
-    }
-  }
-
-  private void checkExpectedState() throws OrmException, IOException {
-    if (!checkExpectedState) {
-      return;
-    }
-
-    // Refuse to apply an update unless the state in NoteDb matches the state
-    // claimed in the ref. This means we may have failed a NoteDb ref update,
-    // and it would be incorrect to claim that the ref is up to date after this
-    // pipeline.
-    //
-    // Generally speaking, this case should be rare; in most cases, we should
-    // have detected and auto-fixed the stale state when creating ChangeNotes
-    // that got passed into the ChangeUpdate.
-    for (Collection<ChangeUpdate> us : changeUpdates.asMap().values()) {
-      ChangeUpdate u = us.iterator().next();
-      NoteDbChangeState expectedState = NoteDbChangeState.parse(u.getChange());
-
-      if (expectedState == null) {
-        // No previous state means we haven't previously written NoteDb graphs
-        // for this change yet. This means either:
-        //  - The change is new, and we'll be creating its ref.
-        //  - We short-circuited before adding any commands that update this
-        //    ref, and we won't stage a delta for this change either.
-        // Either way, it is safe to proceed here rather than throwing
-        // MismatchedStateException.
-        continue;
-      }
-
-      if (expectedState.getPrimaryStorage() == PrimaryStorage.NOTE_DB) {
-        // NoteDb is primary, no need to compare state to ReviewDb.
-        continue;
-      }
-
-      if (!expectedState.isChangeUpToDate(changeRepo.cmds.getRepoRefCache())) {
-        throw new MismatchedStateException(u.getId(), expectedState);
-      }
-    }
-
-    for (Collection<ChangeDraftUpdate> us : draftUpdates.asMap().values()) {
-      ChangeDraftUpdate u = us.iterator().next();
-      NoteDbChangeState expectedState = NoteDbChangeState.parse(u.getChange());
-
-      if (expectedState == null || expectedState.getPrimaryStorage() == PrimaryStorage.NOTE_DB) {
-        continue; // See above.
-      }
-
-      Account.Id accountId = u.getAccountId();
-      if (!expectedState.areDraftsUpToDate(allUsersRepo.cmds.getRepoRefCache(), accountId)) {
-        ObjectId expectedDraftId =
-            firstNonNull(expectedState.getDraftIds().get(accountId), ObjectId.zeroId());
-        throw new OrmConcurrencyException(
-            String.format(
-                "cannot apply NoteDb updates for change %s;"
-                    + " draft ref for account %s does not match %s",
-                u.getId(), accountId, expectedDraftId.name()));
-      }
-    }
+  private void checkNotExecuted() {
+    checkState(!executed, "update has already been executed");
   }
 
   private static <U extends AbstractChangeUpdate> void addUpdates(
-      ListMultimap<String, U> all, OpenRepo or) throws OrmException, IOException {
+      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();
@@ -837,7 +536,7 @@
       ObjectId curr = old;
       for (U u : updates) {
         if (u.isRootOnly() && !old.equals(ObjectId.zeroId())) {
-          throw new OrmException("Given ChangeUpdate is only allowed on initial commit");
+          throw new StorageException("Given ChangeUpdate is only allowed on initial commit");
         }
         ObjectId next = u.apply(or.rw, or.tempIns, curr);
         if (next == null) {
@@ -852,13 +551,13 @@
   }
 
   private static void addRewrites(ListMultimap<String, NoteDbRewriter> rewriters, OpenRepo openRepo)
-      throws OrmException, IOException {
+      throws IOException {
     for (Map.Entry<String, Collection<NoteDbRewriter>> entry : rewriters.asMap().entrySet()) {
       String refName = entry.getKey();
       ObjectId oldTip = openRepo.cmds.get(refName).orElse(ObjectId.zeroId());
 
       if (oldTip.equals(ObjectId.zeroId())) {
-        throw new OrmException(String.format("Ref %s is empty", refName));
+        throw new StorageException(String.format("Ref %s is empty", refName));
       }
 
       ObjectId currTip = oldTip;
@@ -871,7 +570,7 @@
           }
         }
       } catch (ConfigInvalidException e) {
-        throw new OrmException("Cannot rewrite commit history", e);
+        throw new StorageException("Cannot rewrite commit history", e);
       }
 
       if (!oldTip.equals(currTip)) {
@@ -887,8 +586,4 @@
     }
     return updates.iterator().next().allowWriteToNewRef();
   }
-
-  private static void checkDraftRef(boolean condition, String refName) {
-    checkState(condition, "invalid draft ref: %s", refName);
-  }
 }
diff --git a/java/com/google/gerrit/server/notedb/NoteDbUtil.java b/java/com/google/gerrit/server/notedb/NoteDbUtil.java
index 21fada8..667ceab 100644
--- a/java/com/google/gerrit/server/notedb/NoteDbUtil.java
+++ b/java/com/google/gerrit/server/notedb/NoteDbUtil.java
@@ -33,7 +33,7 @@
     String email = ident.getEmailAddress();
     int at = email.indexOf('@');
     if (at >= 0) {
-      String host = email.substring(at + 1, email.length());
+      String host = email.substring(at + 1);
       if (host.equals(serverId)) {
         Integer id = Ints.tryParse(email.substring(0, at));
         if (id != null) {
diff --git a/java/com/google/gerrit/server/notedb/NotesMigration.java b/java/com/google/gerrit/server/notedb/NotesMigration.java
deleted file mode 100644
index 9cee2cd..0000000
--- a/java/com/google/gerrit/server/notedb/NotesMigration.java
+++ /dev/null
@@ -1,250 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.gerrit.server.notedb.NoteDbTable.CHANGES;
-
-import com.google.auto.value.AutoValue;
-import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
-import com.google.inject.AbstractModule;
-import java.util.concurrent.atomic.AtomicReference;
-import org.eclipse.jgit.lib.Config;
-
-/**
- * Current low-level settings of the NoteDb migration for changes.
- *
- * <p>This class only describes the migration state of the {@link
- * com.google.gerrit.reviewdb.client.Change Change} entity group, since it is possible for a given
- * site to be in different states of the Change NoteDb migration process while staying at the same
- * ReviewDb schema version. It does <em>not</em> describe the migration state of non-Change tables;
- * those are automatically migrated using the ReviewDb schema migration process, so the NoteDb
- * migration state at a given ReviewDb schema cannot vary.
- *
- * <p>In many places, core Gerrit code should not directly care about the NoteDb migration state,
- * and should prefer high-level APIs like {@link com.google.gerrit.server.ApprovalsUtil
- * ApprovalsUtil} that don't require callers to inspect the migration state. The
- * <em>implementation</em> of those utilities does care about the state, and should query the {@code
- * NotesMigration} for the properties of the migration, for example, {@link #changePrimaryStorage()
- * where new changes should be stored}.
- *
- * <p>Core Gerrit code is mostly interested in one facet of the migration at a time (reading or
- * writing, say), but not all combinations of return values are supported or even make sense.
- *
- * <p>This class controls the state of the migration according to options in {@code gerrit.config}.
- * In general, any changes to these options should only be made by adventurous administrators, who
- * know what they're doing, on non-production data, for the purposes of testing the NoteDb
- * implementation. Changing options quite likely requires re-running {@code MigrateToNoteDb}. For
- * these reasons, the options remain undocumented.
- *
- * <p><strong>Note:</strong> Callers should not assume the values returned by {@code
- * NotesMigration}'s methods will not change in a running server.
- */
-public abstract class NotesMigration {
-  public static final String SECTION_NOTE_DB = "noteDb";
-  public static final String READ = "read";
-  public static final String WRITE = "write";
-  public static final String DISABLE_REVIEW_DB = "disableReviewDb";
-
-  private static final String PRIMARY_STORAGE = "primaryStorage";
-  private static final String SEQUENCE = "sequence";
-
-  public static class Module extends AbstractModule {
-    @Override
-    public void configure() {
-      bind(MutableNotesMigration.class);
-      bind(NotesMigration.class).to(MutableNotesMigration.class);
-    }
-  }
-
-  @AutoValue
-  abstract static class Snapshot {
-    static Builder builder() {
-      // Default values are defined as what we would read from an empty config.
-      return create(new Config()).toBuilder();
-    }
-
-    static Snapshot create(Config cfg) {
-      return new AutoValue_NotesMigration_Snapshot.Builder()
-          .setWriteChanges(cfg.getBoolean(SECTION_NOTE_DB, CHANGES.key(), WRITE, false))
-          .setReadChanges(cfg.getBoolean(SECTION_NOTE_DB, CHANGES.key(), READ, false))
-          .setReadChangeSequence(cfg.getBoolean(SECTION_NOTE_DB, CHANGES.key(), SEQUENCE, false))
-          .setChangePrimaryStorage(
-              cfg.getEnum(
-                  SECTION_NOTE_DB, CHANGES.key(), PRIMARY_STORAGE, PrimaryStorage.REVIEW_DB))
-          .setDisableChangeReviewDb(
-              cfg.getBoolean(SECTION_NOTE_DB, CHANGES.key(), DISABLE_REVIEW_DB, false))
-          .setFailOnLoadForTest(false) // Only set in tests, can't be set via config.
-          .build();
-    }
-
-    abstract boolean writeChanges();
-
-    abstract boolean readChanges();
-
-    abstract boolean readChangeSequence();
-
-    abstract PrimaryStorage changePrimaryStorage();
-
-    abstract boolean disableChangeReviewDb();
-
-    abstract boolean failOnLoadForTest();
-
-    abstract Builder toBuilder();
-
-    void setConfigValues(Config cfg) {
-      cfg.setBoolean(SECTION_NOTE_DB, CHANGES.key(), WRITE, writeChanges());
-      cfg.setBoolean(SECTION_NOTE_DB, CHANGES.key(), READ, readChanges());
-      cfg.setBoolean(SECTION_NOTE_DB, CHANGES.key(), SEQUENCE, readChangeSequence());
-      cfg.setEnum(SECTION_NOTE_DB, CHANGES.key(), PRIMARY_STORAGE, changePrimaryStorage());
-      cfg.setBoolean(SECTION_NOTE_DB, CHANGES.key(), DISABLE_REVIEW_DB, disableChangeReviewDb());
-    }
-
-    @AutoValue.Builder
-    abstract static class Builder {
-      abstract Builder setWriteChanges(boolean writeChanges);
-
-      abstract Builder setReadChanges(boolean readChanges);
-
-      abstract Builder setReadChangeSequence(boolean readChangeSequence);
-
-      abstract Builder setChangePrimaryStorage(PrimaryStorage changePrimaryStorage);
-
-      abstract Builder setDisableChangeReviewDb(boolean disableChangeReviewDb);
-
-      abstract Builder setFailOnLoadForTest(boolean failOnLoadForTest);
-
-      abstract Snapshot autoBuild();
-
-      Snapshot build() {
-        Snapshot s = autoBuild();
-        checkArgument(
-            !(s.disableChangeReviewDb() && s.changePrimaryStorage() != PrimaryStorage.NOTE_DB),
-            "cannot disable ReviewDb for changes if default change primary storage is ReviewDb");
-        return s;
-      }
-    }
-  }
-
-  protected final AtomicReference<Snapshot> snapshot;
-
-  /**
-   * Read changes from NoteDb.
-   *
-   * <p>Change data is read from NoteDb refs, but ReviewDb is still the source of truth. If the
-   * loader determines NoteDb is out of date, the change data in NoteDb will be transparently
-   * rebuilt. This means that some code paths that look read-only may in fact attempt to write.
-   *
-   * <p>If true and {@code writeChanges() = false}, changes can still be read from NoteDb, but any
-   * attempts to write will generate an error.
-   */
-  public final boolean readChanges() {
-    return snapshot.get().readChanges();
-  }
-
-  /**
-   * Write changes to NoteDb.
-   *
-   * <p>This method is awkwardly named because you should be using either {@link
-   * #commitChangeWrites()} or {@link #failChangeWrites()} instead.
-   *
-   * <p>Updates to change data are written to NoteDb refs, but ReviewDb is still the source of
-   * truth. Change data will not be written unless the NoteDb refs are already up to date, and the
-   * write path will attempt to rebuild the change if not.
-   *
-   * <p>If false, the behavior when attempting to write depends on {@code readChanges()}. If {@code
-   * readChanges() = false}, writes to NoteDb are simply ignored; if {@code true}, any attempts to
-   * write will generate an error.
-   */
-  public final boolean rawWriteChangesSetting() {
-    return snapshot.get().writeChanges();
-  }
-
-  /**
-   * Read sequential change ID numbers from NoteDb.
-   *
-   * <p>If true, change IDs are read from {@code refs/sequences/changes} in All-Projects. If false,
-   * change IDs are read from ReviewDb's native sequences.
-   */
-  public final boolean readChangeSequence() {
-    return snapshot.get().readChangeSequence();
-  }
-
-  /** @return default primary storage for new changes. */
-  public final PrimaryStorage changePrimaryStorage() {
-    return snapshot.get().changePrimaryStorage();
-  }
-
-  /**
-   * Disable ReviewDb access for changes.
-   *
-   * <p>When set, ReviewDb operations involving the Changes table become no-ops. Lookups return no
-   * results; updates do nothing, as does opening, committing, or rolling back a transaction on the
-   * Changes table.
-   */
-  public final boolean disableChangeReviewDb() {
-    return snapshot.get().disableChangeReviewDb();
-  }
-
-  /**
-   * Whether to fail when reading any data from NoteDb.
-   *
-   * <p>Used in conjunction with {@link #readChanges()} for tests.
-   */
-  public boolean failOnLoadForTest() {
-    return snapshot.get().failOnLoadForTest();
-  }
-
-  public final boolean commitChangeWrites() {
-    // It may seem odd that readChanges() without writeChanges() means we should
-    // attempt to commit writes. However, this method is used by callers to know
-    // whether or not they should short-circuit and skip attempting to read or
-    // write NoteDb refs.
-    //
-    // It is possible for commitChangeWrites() to return true and
-    // failChangeWrites() to also return true, causing an error later in the
-    // same codepath. This specific condition is used by the auto-rebuilding
-    // path to rebuild a change and stage the results, but not commit them due
-    // to failChangeWrites().
-    return rawWriteChangesSetting() || readChanges();
-  }
-
-  public final boolean failChangeWrites() {
-    return !rawWriteChangesSetting() && readChanges();
-  }
-
-  public final void setConfigValues(Config cfg) {
-    snapshot.get().setConfigValues(cfg);
-  }
-
-  @Override
-  public final boolean equals(Object o) {
-    return o instanceof NotesMigration
-        && snapshot.get().equals(((NotesMigration) o).snapshot.get());
-  }
-
-  @Override
-  public final int hashCode() {
-    return snapshot.get().hashCode();
-  }
-
-  protected NotesMigration(Snapshot snapshot) {
-    this.snapshot = new AtomicReference<>(snapshot);
-  }
-
-  final Snapshot snapshot() {
-    return snapshot.get();
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/NotesMigrationState.java b/java/com/google/gerrit/server/notedb/NotesMigrationState.java
deleted file mode 100644
index c682aed..0000000
--- a/java/com/google/gerrit/server/notedb/NotesMigrationState.java
+++ /dev/null
@@ -1,92 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb;
-
-import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
-import com.google.gerrit.server.notedb.NotesMigration.Snapshot;
-import java.util.Optional;
-import java.util.stream.Stream;
-import org.eclipse.jgit.lib.Config;
-
-/**
- * Possible high-level states of the NoteDb migration for changes.
- *
- * <p>This class describes the series of states required to migrate a site from ReviewDb-only to
- * NoteDb-only. This process has several steps, and covers only a small subset of the theoretically
- * possible combinations of {@link NotesMigration} return values.
- *
- * <p>These states are ordered: a one-way migration from ReviewDb to NoteDb will pass through states
- * in the order in which they are defined.
- */
-public enum NotesMigrationState {
-  REVIEW_DB(false, false, false, PrimaryStorage.REVIEW_DB, false),
-
-  WRITE(false, true, false, PrimaryStorage.REVIEW_DB, false),
-
-  READ_WRITE_NO_SEQUENCE(true, true, false, PrimaryStorage.REVIEW_DB, false),
-
-  READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY(true, true, true, PrimaryStorage.REVIEW_DB, false),
-
-  READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY(true, true, true, PrimaryStorage.NOTE_DB, false),
-
-  NOTE_DB(true, true, true, PrimaryStorage.NOTE_DB, true);
-
-  public static final NotesMigrationState FINAL = NOTE_DB;
-
-  public static Optional<NotesMigrationState> forConfig(Config cfg) {
-    return forSnapshot(Snapshot.create(cfg));
-  }
-
-  public static Optional<NotesMigrationState> forNotesMigration(NotesMigration migration) {
-    return forSnapshot(migration.snapshot());
-  }
-
-  private static Optional<NotesMigrationState> forSnapshot(Snapshot s) {
-    return Stream.of(values()).filter(v -> v.snapshot.equals(s)).findFirst();
-  }
-
-  private final Snapshot snapshot;
-
-  NotesMigrationState(
-      // Arguments match abstract methods in NotesMigration.
-      boolean readChanges,
-      boolean rawWriteChangesSetting,
-      boolean readChangeSequence,
-      PrimaryStorage changePrimaryStorage,
-      boolean disableChangeReviewDb) {
-    this.snapshot =
-        Snapshot.builder()
-            .setReadChanges(readChanges)
-            .setWriteChanges(rawWriteChangesSetting)
-            .setReadChangeSequence(readChangeSequence)
-            .setChangePrimaryStorage(changePrimaryStorage)
-            .setDisableChangeReviewDb(disableChangeReviewDb)
-            .build();
-  }
-
-  public void setConfigValues(Config cfg) {
-    snapshot.setConfigValues(cfg);
-  }
-
-  public String toText() {
-    Config cfg = new Config();
-    setConfigValues(cfg);
-    return cfg.toText();
-  }
-
-  Snapshot snapshot() {
-    return snapshot;
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/PrimaryStorageMigrator.java b/java/com/google/gerrit/server/notedb/PrimaryStorageMigrator.java
deleted file mode 100644
index 7b427b4..0000000
--- a/java/com/google/gerrit/server/notedb/PrimaryStorageMigrator.java
+++ /dev/null
@@ -1,510 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb;
-
-import static com.google.common.base.Preconditions.checkState;
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
-import static java.util.concurrent.TimeUnit.NANOSECONDS;
-import static java.util.concurrent.TimeUnit.SECONDS;
-
-import com.github.rholder.retry.RetryException;
-import com.github.rholder.retry.Retryer;
-import com.github.rholder.retry.RetryerBuilder;
-import com.github.rholder.retry.StopStrategies;
-import com.github.rholder.retry.WaitStrategies;
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Stopwatch;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.Nullable;
-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.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.reviewdb.server.ReviewDbUtil;
-import com.google.gerrit.server.InternalUser;
-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.RepoRefCache;
-import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
-import com.google.gerrit.server.notedb.NoteDbChangeState.RefState;
-import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder;
-import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.InternalChangeQuery;
-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.RetryHelper;
-import com.google.gerrit.server.update.UpdateException;
-import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.AtomicUpdate;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.OrmRuntimeException;
-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.util.List;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.TreeSet;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.atomic.AtomicBoolean;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-
-/** Helper to migrate the {@link PrimaryStorage} of individual changes. */
-@Singleton
-public class PrimaryStorageMigrator {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
-  /**
-   * Exception thrown during migration if the change has no {@code noteDbState} field at the
-   * beginning of the migration.
-   */
-  public static class NoNoteDbStateException extends RuntimeException {
-    private static final long serialVersionUID = 1L;
-
-    private NoNoteDbStateException(Change.Id id) {
-      super("change " + id + " has no note_db_state; rebuild it first");
-    }
-  }
-
-  private final AllUsersName allUsers;
-  private final ChangeNotes.Factory changeNotesFactory;
-  private final ChangeRebuilder rebuilder;
-  private final ChangeUpdate.Factory updateFactory;
-  private final GitRepositoryManager repoManager;
-  private final InternalUser.Factory internalUserFactory;
-  private final Provider<InternalChangeQuery> queryProvider;
-  private final Provider<ReviewDb> db;
-  private final RetryHelper retryHelper;
-
-  private final long skewMs;
-  private final long timeoutMs;
-  private final Retryer<NoteDbChangeState> testEnsureRebuiltRetryer;
-
-  @Inject
-  PrimaryStorageMigrator(
-      @GerritServerConfig Config cfg,
-      Provider<ReviewDb> db,
-      GitRepositoryManager repoManager,
-      AllUsersName allUsers,
-      ChangeRebuilder rebuilder,
-      ChangeNotes.Factory changeNotesFactory,
-      Provider<InternalChangeQuery> queryProvider,
-      ChangeUpdate.Factory updateFactory,
-      InternalUser.Factory internalUserFactory,
-      RetryHelper retryHelper) {
-    this(
-        cfg,
-        db,
-        repoManager,
-        allUsers,
-        rebuilder,
-        null,
-        changeNotesFactory,
-        queryProvider,
-        updateFactory,
-        internalUserFactory,
-        retryHelper);
-  }
-
-  @VisibleForTesting
-  public PrimaryStorageMigrator(
-      Config cfg,
-      Provider<ReviewDb> db,
-      GitRepositoryManager repoManager,
-      AllUsersName allUsers,
-      ChangeRebuilder rebuilder,
-      @Nullable Retryer<NoteDbChangeState> testEnsureRebuiltRetryer,
-      ChangeNotes.Factory changeNotesFactory,
-      Provider<InternalChangeQuery> queryProvider,
-      ChangeUpdate.Factory updateFactory,
-      InternalUser.Factory internalUserFactory,
-      RetryHelper retryHelper) {
-    this.db = db;
-    this.repoManager = repoManager;
-    this.allUsers = allUsers;
-    this.rebuilder = rebuilder;
-    this.testEnsureRebuiltRetryer = testEnsureRebuiltRetryer;
-    this.changeNotesFactory = changeNotesFactory;
-    this.queryProvider = queryProvider;
-    this.updateFactory = updateFactory;
-    this.internalUserFactory = internalUserFactory;
-    this.retryHelper = retryHelper;
-    skewMs = NoteDbChangeState.getReadOnlySkew(cfg);
-
-    String s = "notedb";
-    timeoutMs =
-        cfg.getTimeUnit(
-            s,
-            null,
-            "primaryStorageMigrationTimeout",
-            MILLISECONDS.convert(60, SECONDS),
-            MILLISECONDS);
-  }
-
-  /**
-   * Migrate a change's primary storage from ReviewDb to NoteDb.
-   *
-   * <p>This method will return only if the primary storage of the change is NoteDb afterwards. (It
-   * may return early if the primary storage was already NoteDb.)
-   *
-   * <p>If this method throws an exception, then the primary storage of the change is probably not
-   * NoteDb. (It is possible that the primary storage of the change is NoteDb in this case, but
-   * there was an error reading the state.) Moreover, after an exception, the change may be
-   * read-only until a lease expires. If the caller chooses to retry, they should wait until the
-   * read-only lease expires; this method will fail relatively quickly if called on a read-only
-   * change.
-   *
-   * <p>Note that if the change is read-only after this method throws an exception, that does not
-   * necessarily guarantee that the read-only lease was acquired during that particular method
-   * invocation; this call may have in fact failed because another thread acquired the lease first.
-   *
-   * @param id change ID.
-   * @throws OrmException if a ReviewDb-level error occurs.
-   * @throws IOException if a repo-level error occurs.
-   */
-  public void migrateToNoteDbPrimary(Change.Id id) throws OrmException, IOException {
-    // Since there are multiple non-atomic steps in this method, we need to
-    // consider what happens when there is another writer concurrent with the
-    // thread executing this method.
-    //
-    // Let:
-    // * OR = other writer writes noteDbState & new data to ReviewDb (in one
-    //        transaction)
-    // * ON = other writer writes to NoteDb
-    // * MRO = migrator sets state to read-only
-    // * MR = ensureRebuilt writes rebuilt noteDbState to ReviewDb (but does not
-    //        otherwise update ReviewDb in this transaction)
-    // * MN = ensureRebuilt writes rebuilt state to NoteDb
-    //
-    // Consider all the interleavings of these operations.
-    //
-    // * OR,ON,MRO,...
-    //   Other writer completes before migrator begins; this is not a concurrent
-    //   write.
-    // * MRO,...,OR,...
-    //   OR will fail, since it atomically checks that the noteDbState is not
-    //   read-only before proceeding. This results in an exception, but not a
-    //   concurrent write.
-    //
-    // Thus all the "interesting" interleavings start with OR,MRO, and differ on
-    // where ON falls relative to MR/MN.
-    //
-    // * OR,MRO,ON,MR,MN
-    //   The other NoteDb write succeeds despite the noteDbState being
-    //   read-only. Because the read-only state from MRO includes the update
-    //   from OR, the change is up-to-date at this point. Thus MR,MN is a no-op.
-    //   The end result is an up-to-date, read-only change.
-    //
-    // * OR,MRO,MR,ON,MN
-    //   The change is out-of-date when ensureRebuilt begins, because OR
-    //   succeeded but the corresponding ON has not happened yet. ON will
-    //   succeed, because there have been no intervening NoteDb writes. MN will
-    //   fail, because ON updated the state in NoteDb to something other than
-    //   what MR claimed. This leaves the change in an out-of-date, read-only
-    //   state.
-    //
-    //   If this method threw an exception in this case, the change would
-    //   eventually switch back to read-write when the read-only lease expires,
-    //   so this situation is recoverable. However, it would be inconvenient for
-    //   a change to be read-only for so long.
-    //
-    //   Thus, as an optimization, we have a retry loop that attempts
-    //   ensureRebuilt while still holding the same read-only lease. This
-    //   effectively results in the interleaving OR,MR,ON,MR,MN; in contrast
-    //   with the previous case, here, MR/MN actually rebuilds the change. In
-    //   the case of a write failure, MR/MN might fail and get retried again. If
-    //   it exceeds the maximum number of retries, an exception is thrown.
-    //
-    // * OR,MRO,MR,MN,ON
-    //   The change is out-of-date when ensureRebuilt begins. The change is
-    //   rebuilt, leaving a new state in NoteDb. ON will fail, because the old
-    //   NoteDb state has changed since the ref state was read when the update
-    //   began (prior to OR). This results in an exception from ON, but the end
-    //   result is still an up-to-date, read-only change. The end user that
-    //   initiated the other write observes an error, but this is no different
-    //   from other errors that need retrying, e.g. due to a backend write
-    //   failure.
-
-    Stopwatch sw = Stopwatch.createStarted();
-    Change readOnlyChange = setReadOnlyInReviewDb(id); // MRO
-    if (readOnlyChange == null) {
-      return; // Already migrated.
-    }
-
-    NoteDbChangeState rebuiltState;
-    try {
-      // MR,MN
-      rebuiltState =
-          ensureRebuiltRetryer(sw)
-              .call(
-                  () ->
-                      ensureRebuilt(
-                          readOnlyChange.getProject(),
-                          id,
-                          NoteDbChangeState.parse(readOnlyChange)));
-    } catch (RetryException | ExecutionException e) {
-      throw new OrmException(e);
-    }
-
-    // At this point, the noteDbState in ReviewDb is read-only, and it is
-    // guaranteed to match the state actually in NoteDb. Now it is safe to set
-    // the primary storage to NoteDb.
-
-    setPrimaryStorageNoteDb(id, rebuiltState);
-    logger.atFine().log(
-        "Migrated change %s to NoteDb primary in %sms", id, sw.elapsed(MILLISECONDS));
-  }
-
-  private Change setReadOnlyInReviewDb(Change.Id id) throws OrmException {
-    AtomicBoolean alreadyMigrated = new AtomicBoolean(false);
-    Change result =
-        db().changes()
-            .atomicUpdate(
-                id,
-                new AtomicUpdate<Change>() {
-                  @Override
-                  public Change update(Change change) {
-                    NoteDbChangeState state = NoteDbChangeState.parse(change);
-                    if (state == null) {
-                      // Could rebuild the change here, but that's more complexity, and this
-                      // normally shouldn't happen.
-                      //
-                      // Known cases where this happens are described in and handled by
-                      // NoteDbMigrator#canSkipPrimaryStorageMigration.
-                      throw new NoNoteDbStateException(id);
-                    }
-                    // If the change is already read-only, then the lease is held by another
-                    // (likely failed) migrator thread. Fail early, as we can't take over
-                    // the lease.
-                    NoteDbChangeState.checkNotReadOnly(change, skewMs);
-                    if (state.getPrimaryStorage() != PrimaryStorage.NOTE_DB) {
-                      Timestamp now = TimeUtil.nowTs();
-                      Timestamp until = new Timestamp(now.getTime() + timeoutMs);
-                      change.setNoteDbState(state.withReadOnlyUntil(until).toString());
-                    } else {
-                      alreadyMigrated.set(true);
-                    }
-                    return change;
-                  }
-                });
-    return alreadyMigrated.get() ? null : result;
-  }
-
-  private Retryer<NoteDbChangeState> ensureRebuiltRetryer(Stopwatch sw) {
-    if (testEnsureRebuiltRetryer != null) {
-      return testEnsureRebuiltRetryer;
-    }
-    // Retry the ensureRebuilt step with backoff until half the timeout has
-    // expired, leaving the remaining half for the rest of the steps.
-    long remainingNanos = (MILLISECONDS.toNanos(timeoutMs) / 2) - sw.elapsed(NANOSECONDS);
-    remainingNanos = Math.max(remainingNanos, 0);
-    return RetryerBuilder.<NoteDbChangeState>newBuilder()
-        .retryIfException(e -> (e instanceof IOException) || (e instanceof OrmException))
-        .withWaitStrategy(
-            WaitStrategies.join(
-                WaitStrategies.exponentialWait(250, MILLISECONDS),
-                WaitStrategies.randomWait(50, MILLISECONDS)))
-        .withStopStrategy(StopStrategies.stopAfterDelay(remainingNanos, NANOSECONDS))
-        .build();
-  }
-
-  private NoteDbChangeState ensureRebuilt(
-      Project.NameKey project, Change.Id id, NoteDbChangeState readOnlyState)
-      throws IOException, OrmException, RepositoryNotFoundException {
-    try (Repository changeRepo = repoManager.openRepository(project);
-        Repository allUsersRepo = repoManager.openRepository(allUsers)) {
-      if (!readOnlyState.isUpToDate(new RepoRefCache(changeRepo), new RepoRefCache(allUsersRepo))) {
-        NoteDbUpdateManager.Result r = rebuilder.rebuildEvenIfReadOnly(db(), id);
-        checkState(
-            r.newState().getReadOnlyUntil().equals(readOnlyState.getReadOnlyUntil()),
-            "state after rebuilding has different read-only lease: %s != %s",
-            r.newState(),
-            readOnlyState);
-        readOnlyState = r.newState();
-      }
-    }
-    return readOnlyState;
-  }
-
-  private void setPrimaryStorageNoteDb(Change.Id id, NoteDbChangeState expectedState)
-      throws OrmException {
-    db().changes()
-        .atomicUpdate(
-            id,
-            new AtomicUpdate<Change>() {
-              @Override
-              public Change update(Change change) {
-                NoteDbChangeState state = NoteDbChangeState.parse(change);
-                if (!Objects.equals(state, expectedState)) {
-                  throw new OrmRuntimeException(badState(state, expectedState));
-                }
-                Timestamp until = state.getReadOnlyUntil().get();
-                if (TimeUtil.nowTs().after(until)) {
-                  throw new OrmRuntimeException(
-                      "read-only lease on change " + id + " expired at " + until);
-                }
-                change.setNoteDbState(NoteDbChangeState.NOTE_DB_PRIMARY_STATE);
-                return change;
-              }
-            });
-  }
-
-  private ReviewDb db() {
-    return ReviewDbUtil.unwrapDb(db.get());
-  }
-
-  private String badState(NoteDbChangeState actual, NoteDbChangeState expected) {
-    return "state changed unexpectedly: " + actual + " != " + expected;
-  }
-
-  public void migrateToReviewDbPrimary(Change.Id id, @Nullable Project.NameKey project)
-      throws OrmException, IOException {
-    // Migrating back to ReviewDb primary is much simpler than the original migration to NoteDb
-    // primary, because when NoteDb is primary, each write only goes to one storage location rather
-    // than both. We only need to consider whether a concurrent writer (OR) conflicts with the first
-    // setReadOnlyInNoteDb step (MR) in this method.
-    //
-    // If OR wins, then either:
-    // * MR will set read-only after OR is completed, which is not a concurrent write.
-    // * MR will fail to set read-only with a lock failure. The caller will have to retry, but the
-    //   change is not in a read-only state, so behavior is not degraded in the meantime.
-    //
-    // If MR wins, then either:
-    // * OR will fail with a read-only exception (via AbstractChangeNotes#apply).
-    // * OR will fail with a lock failure.
-    //
-    // In all of these scenarios, the change is read-only if and only if MR succeeds.
-    //
-    // There will be no concurrent writes to ReviewDb for this change until
-    // setPrimaryStorageReviewDb completes, because ReviewDb writes are not attempted when primary
-    // storage is NoteDb. After the primary storage changes back, it is possible for subsequent
-    // NoteDb writes to conflict with the releaseReadOnlyLeaseInNoteDb step, but at this point,
-    // since ReviewDb is primary, we are back to ignoring them.
-    Stopwatch sw = Stopwatch.createStarted();
-    if (project == null) {
-      project = getProject(id);
-    }
-    ObjectId newMetaId = setReadOnlyInNoteDb(project, id);
-    rebuilder.rebuildReviewDb(db(), project, id);
-    setPrimaryStorageReviewDb(id, newMetaId);
-    releaseReadOnlyLeaseInNoteDb(project, id);
-    logger.atFine().log(
-        "Migrated change %s to ReviewDb primary in %sms", id, sw.elapsed(MILLISECONDS));
-  }
-
-  private ObjectId setReadOnlyInNoteDb(Project.NameKey project, Change.Id id)
-      throws OrmException, IOException {
-    Timestamp now = TimeUtil.nowTs();
-    Timestamp until = new Timestamp(now.getTime() + timeoutMs);
-    ChangeUpdate update =
-        updateFactory.create(
-            changeNotesFactory.createChecked(db.get(), project, id), internalUserFactory.create());
-    update.setReadOnlyUntil(until);
-    return update.commit();
-  }
-
-  private void setPrimaryStorageReviewDb(Change.Id id, ObjectId newMetaId)
-      throws OrmException, IOException {
-    ImmutableMap.Builder<Account.Id, ObjectId> draftIds = ImmutableMap.builder();
-    try (Repository repo = repoManager.openRepository(allUsers)) {
-      for (Ref draftRef :
-          repo.getRefDatabase().getRefsByPrefix(RefNames.refsDraftCommentsPrefix(id))) {
-        Account.Id accountId = Account.Id.fromRef(draftRef.getName());
-        if (accountId != null) {
-          draftIds.put(accountId, draftRef.getObjectId().copy());
-        }
-      }
-    }
-    NoteDbChangeState newState =
-        new NoteDbChangeState(
-            id,
-            PrimaryStorage.REVIEW_DB,
-            Optional.of(RefState.create(newMetaId, draftIds.build())),
-            Optional.empty());
-    db().changes()
-        .atomicUpdate(
-            id,
-            new AtomicUpdate<Change>() {
-              @Override
-              public Change update(Change change) {
-                if (PrimaryStorage.of(change) != PrimaryStorage.NOTE_DB) {
-                  throw new OrmRuntimeException(
-                      "change " + id + " is not NoteDb primary: " + change.getNoteDbState());
-                }
-                change.setNoteDbState(newState.toString());
-                return change;
-              }
-            });
-  }
-
-  private void releaseReadOnlyLeaseInNoteDb(Project.NameKey project, Change.Id id)
-      throws OrmException {
-    // Use a BatchUpdate since ReviewDb is primary at this point, so it needs to reflect the update.
-    // (In practice retrying won't happen, since we aren't using fused updates at this point.)
-    try {
-      retryHelper.execute(
-          updateFactory -> {
-            try (BatchUpdate bu =
-                updateFactory.create(
-                    db.get(), project, internalUserFactory.create(), TimeUtil.nowTs())) {
-              bu.addOp(
-                  id,
-                  new BatchUpdateOp() {
-                    @Override
-                    public boolean updateChange(ChangeContext ctx) {
-                      ctx.getUpdate(ctx.getChange().currentPatchSetId())
-                          .setReadOnlyUntil(new Timestamp(0));
-                      return true;
-                    }
-                  });
-              bu.execute();
-              return null;
-            }
-          });
-    } catch (RestApiException | UpdateException e) {
-      throw new OrmException(e);
-    }
-  }
-
-  private Project.NameKey getProject(Change.Id id) throws OrmException {
-    List<ChangeData> cds =
-        queryProvider.get().setRequestedFields(ChangeField.PROJECT).byLegacyChangeId(id);
-    Set<Project.NameKey> projects = new TreeSet<>();
-    for (ChangeData cd : cds) {
-      projects.add(cd.project());
-    }
-    if (projects.size() != 1) {
-      throw new OrmException(
-          "zero or multiple projects found for change "
-              + id
-              + ", must specify project explicitly: "
-              + projects);
-    }
-    return projects.iterator().next();
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/RepoSequence.java b/java/com/google/gerrit/server/notedb/RepoSequence.java
index 4c497ac..1c33a68 100644
--- a/java/com/google/gerrit/server/notedb/RepoSequence.java
+++ b/java/com/google/gerrit/server/notedb/RepoSequence.java
@@ -27,33 +27,27 @@
 import com.github.rholder.retry.StopStrategies;
 import com.github.rholder.retry.WaitStrategies;
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.CharMatcher;
-import com.google.common.base.Predicates;
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
-import com.google.common.primitives.Ints;
 import com.google.common.util.concurrent.Runnables;
-import com.google.gerrit.common.Nullable;
+import com.google.gerrit.exceptions.StorageException;
+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 com.google.gwtorm.server.OrmException;
 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;
 import java.util.concurrent.locks.ReentrantLock;
-import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.ObjectLoader;
-import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.RefUpdate.Result;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.ReceiveCommand;
@@ -71,13 +65,13 @@
 public class RepoSequence {
   @FunctionalInterface
   public interface Seed {
-    int get() throws OrmException;
+    int get();
   }
 
   @VisibleForTesting
-  static RetryerBuilder<RefUpdate.Result> retryerBuilder() {
-    return RetryerBuilder.<RefUpdate.Result>newBuilder()
-        .retryIfResult(Predicates.equalTo(RefUpdate.Result.LOCK_FAILURE))
+  static RetryerBuilder<RefUpdate> retryerBuilder() {
+    return RetryerBuilder.<RefUpdate>newBuilder()
+        .retryIfResult(ru -> ru != null && RefUpdate.Result.LOCK_FAILURE.equals(ru.getResult()))
         .withWaitStrategy(
             WaitStrategies.join(
                 WaitStrategies.exponentialWait(5, TimeUnit.SECONDS),
@@ -85,7 +79,7 @@
         .withStopStrategy(StopStrategies.stopAfterDelay(30, TimeUnit.SECONDS));
   }
 
-  private static final Retryer<RefUpdate.Result> RETRYER = retryerBuilder().build();
+  private static final Retryer<RefUpdate> RETRYER = retryerBuilder().build();
 
   private final GitRepositoryManager repoManager;
   private final GitReferenceUpdated gitRefUpdated;
@@ -95,7 +89,7 @@
   private final int floor;
   private final int batchSize;
   private final Runnable afterReadRef;
-  private final Retryer<RefUpdate.Result> retryer;
+  private final Retryer<RefUpdate> retryer;
 
   // Protects all non-final fields.
   private final Lock counterLock;
@@ -153,7 +147,7 @@
       Seed seed,
       int batchSize,
       Runnable afterReadRef,
-      Retryer<RefUpdate.Result> retryer) {
+      Retryer<RefUpdate> retryer) {
     this(repoManager, gitRefUpdated, projectName, name, seed, batchSize, afterReadRef, retryer, 0);
   }
 
@@ -165,7 +159,7 @@
       Seed seed,
       int batchSize,
       Runnable afterReadRef,
-      Retryer<RefUpdate.Result> retryer,
+      Retryer<RefUpdate> retryer,
       int floor) {
     this.repoManager = requireNonNull(repoManager, "repoManager");
     this.gitRefUpdated = requireNonNull(gitRefUpdated, "gitRefUpdated");
@@ -190,7 +184,7 @@
     counterLock = new ReentrantLock(true);
   }
 
-  public int next() throws OrmException {
+  public int next() {
     counterLock.lock();
     try {
       if (counter >= limit) {
@@ -202,7 +196,7 @@
     }
   }
 
-  public ImmutableList<Integer> next(int count) throws OrmException {
+  public ImmutableList<Integer> next(int count) {
     if (count == 0) {
       return ImmutableList.of();
     }
@@ -226,74 +220,25 @@
     }
   }
 
-  @VisibleForTesting
-  public void set(int val) throws OrmException {
-    // Don't bother spinning. This is only for tests, and a test that calls set
-    // concurrently with other writes is doing it wrong.
-    counterLock.lock();
-    try {
-      try (Repository repo = repoManager.openRepository(projectName);
-          RevWalk rw = new RevWalk(repo)) {
-        checkResult(store(repo, rw, null, val));
-        counter = limit;
-      } catch (IOException e) {
-        throw new OrmException(e);
-      }
-    } finally {
-      counterLock.unlock();
-    }
-  }
-
-  public void increaseTo(int val) throws OrmException {
-    counterLock.lock();
-    try {
-      try (Repository repo = repoManager.openRepository(projectName);
-          RevWalk rw = new RevWalk(repo)) {
-        TryIncreaseTo attempt = new TryIncreaseTo(repo, rw, val);
-        checkResult(retryer.call(attempt));
-        counter = limit;
-      } catch (ExecutionException | RetryException e) {
-        if (e.getCause() != null) {
-          Throwables.throwIfInstanceOf(e.getCause(), OrmException.class);
-        }
-        throw new OrmException(e);
-      } catch (IOException e) {
-        throw new OrmException(e);
-      }
-    } finally {
-      counterLock.unlock();
-    }
-  }
-
-  private void acquire(int count) throws OrmException {
+  private void acquire(int count) {
     try (Repository repo = repoManager.openRepository(projectName);
         RevWalk rw = new RevWalk(repo)) {
       TryAcquire attempt = new TryAcquire(repo, rw, count);
-      checkResult(retryer.call(attempt));
+      RefUpdateUtil.checkResult(retryer.call(attempt));
       counter = attempt.next;
       limit = counter + count;
       acquireCount++;
     } catch (ExecutionException | RetryException e) {
       if (e.getCause() != null) {
-        Throwables.throwIfInstanceOf(e.getCause(), OrmException.class);
+        Throwables.throwIfInstanceOf(e.getCause(), StorageException.class);
       }
-      throw new OrmException(e);
+      throw new StorageException(e);
     } catch (IOException e) {
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
   }
 
-  private void checkResult(RefUpdate.Result result) throws OrmException {
-    if (!refUpdated(result) && result != Result.NO_CHANGE) {
-      throw new OrmException("failed to update " + refName + ": " + result);
-    }
-  }
-
-  private boolean refUpdated(RefUpdate.Result result) {
-    return result == RefUpdate.Result.NEW || result == RefUpdate.Result.FORCED;
-  }
-
-  private class TryAcquire implements Callable<RefUpdate.Result> {
+  private class TryAcquire implements Callable<RefUpdate> {
     private final Repository repo;
     private final RevWalk rw;
     private final int count;
@@ -307,88 +252,22 @@
     }
 
     @Override
-    public RefUpdate.Result call() throws Exception {
-      Ref ref = repo.exactRef(refName);
+    public RefUpdate call() throws Exception {
+      Optional<IntBlob> blob = IntBlob.parse(repo, refName, rw);
       afterReadRef.run();
       ObjectId oldId;
-      if (ref == null) {
+      if (!blob.isPresent()) {
         oldId = ObjectId.zeroId();
         next = seed.get();
       } else {
-        oldId = ref.getObjectId();
-        next = parse(rw, oldId);
+        oldId = blob.get().id();
+        next = blob.get().value();
       }
       next = Math.max(floor, next);
-      return store(repo, rw, oldId, next + count);
+      return IntBlob.tryStore(repo, rw, projectName, refName, oldId, next + count, gitRefUpdated);
     }
   }
 
-  private class TryIncreaseTo implements Callable<RefUpdate.Result> {
-    private final Repository repo;
-    private final RevWalk rw;
-    private final int value;
-
-    private TryIncreaseTo(Repository repo, RevWalk rw, int value) {
-      this.repo = repo;
-      this.rw = rw;
-      this.value = value;
-    }
-
-    @Override
-    public RefUpdate.Result call() throws Exception {
-      Ref ref = repo.exactRef(refName);
-      afterReadRef.run();
-      ObjectId oldId;
-      if (ref == null) {
-        oldId = ObjectId.zeroId();
-      } else {
-        oldId = ref.getObjectId();
-        int next = parse(rw, oldId);
-        if (next >= value) {
-          // a concurrent write updated the ref already to this or a higher value
-          return RefUpdate.Result.NO_CHANGE;
-        }
-      }
-      return store(repo, rw, oldId, value);
-    }
-  }
-
-  private int parse(RevWalk rw, ObjectId id) throws IOException, OrmException {
-    ObjectLoader ol = rw.getObjectReader().open(id, OBJ_BLOB);
-    if (ol.getType() != OBJ_BLOB) {
-      // In theory this should be thrown by open but not all implementations
-      // may do it properly (certainly InMemoryRepository doesn't).
-      throw new IncorrectObjectTypeException(id, OBJ_BLOB);
-    }
-    String str = CharMatcher.whitespace().trimFrom(new String(ol.getCachedBytes(), UTF_8));
-    Integer val = Ints.tryParse(str);
-    if (val == null) {
-      throw new OrmException("invalid value in " + refName + " blob at " + id.name());
-    }
-    return val;
-  }
-
-  private RefUpdate.Result store(Repository repo, RevWalk rw, @Nullable ObjectId oldId, int val)
-      throws IOException {
-    ObjectId newId;
-    try (ObjectInserter ins = repo.newObjectInserter()) {
-      newId = ins.insert(OBJ_BLOB, Integer.toString(val).getBytes(UTF_8));
-      ins.flush();
-    }
-    RefUpdate ru = repo.updateRef(refName);
-    if (oldId != null) {
-      ru.setExpectedOldObjectId(oldId);
-    }
-    ru.disableRefLog();
-    ru.setNewObjectId(newId);
-    ru.setForceUpdate(true); // Required for non-commitish updates.
-    RefUpdate.Result result = ru.update(rw);
-    if (refUpdated(result)) {
-      gitRefUpdated.fire(projectName, ru, null);
-    }
-    return result;
-  }
-
   public static ReceiveCommand storeNew(ObjectInserter ins, String name, int val)
       throws IOException {
     ObjectId newId = ins.insert(OBJ_BLOB, Integer.toString(val).getBytes(UTF_8));
diff --git a/java/com/google/gerrit/server/notedb/RevisionNote.java b/java/com/google/gerrit/server/notedb/RevisionNote.java
index deec7e9..ff649a9 100644
--- a/java/com/google/gerrit/server/notedb/RevisionNote.java
+++ b/java/com/google/gerrit/server/notedb/RevisionNote.java
@@ -18,7 +18,7 @@
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 
 import com.google.common.collect.ImmutableList;
-import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.common.UsedAt;
 import java.io.IOException;
 import java.util.List;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -26,7 +26,8 @@
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.util.MutableInteger;
 
-abstract class RevisionNote<T extends Comment> {
+@UsedAt(UsedAt.Project.PLUGIN_CHECKS)
+public abstract class RevisionNote<T> {
   static final int MAX_NOTE_SZ = 25 << 20;
 
   protected static void trimLeadingEmptyLines(byte[] bytes, MutableInteger p) {
@@ -39,9 +40,9 @@
   private final ObjectId noteId;
 
   private byte[] raw;
-  private ImmutableList<T> comments;
+  private ImmutableList<T> entities;
 
-  RevisionNote(ObjectReader reader, ObjectId noteId) {
+  public RevisionNote(ObjectReader reader, ObjectId noteId) {
     this.reader = reader;
     this.noteId = noteId;
   }
@@ -51,9 +52,16 @@
     return raw;
   }
 
-  public ImmutableList<T> getComments() {
+  @UsedAt(UsedAt.Project.PLUGIN_CHECKS)
+  public T getOnlyEntity() {
     checkParsed();
-    return comments;
+    checkState(entities.size() == 1, "expected exactly one entity");
+    return entities.get(0);
+  }
+
+  public ImmutableList<T> getEntities() {
+    checkParsed();
+    return entities;
   }
 
   public void parse() throws IOException, ConfigInvalidException {
@@ -61,11 +69,11 @@
     MutableInteger p = new MutableInteger();
     trimLeadingEmptyLines(raw, p);
     if (p.value >= raw.length) {
-      comments = ImmutableList.of();
+      entities = ImmutableList.of();
       return;
     }
 
-    comments = ImmutableList.copyOf(parse(raw, p.value));
+    entities = ImmutableList.copyOf(parse(raw, p.value));
   }
 
   protected abstract List<T> parse(byte[] raw, int offset)
diff --git a/java/com/google/gerrit/server/notedb/RevisionNoteBuilder.java b/java/com/google/gerrit/server/notedb/RevisionNoteBuilder.java
index b8c7d7d..ac7a89d 100644
--- a/java/com/google/gerrit/server/notedb/RevisionNoteBuilder.java
+++ b/java/com/google/gerrit/server/notedb/RevisionNoteBuilder.java
@@ -68,7 +68,7 @@
   RevisionNoteBuilder(RevisionNote<? extends Comment> base) {
     if (base != null) {
       baseRaw = base.getRaw();
-      baseComments = base.getComments();
+      baseComments = base.getEntities();
       put = Maps.newHashMapWithExpectedSize(baseComments.size());
       if (base instanceof ChangeRevisionNote) {
         pushCert = ((ChangeRevisionNote) base).getPushCert();
diff --git a/java/com/google/gerrit/server/notedb/RevisionNoteMap.java b/java/com/google/gerrit/server/notedb/RevisionNoteMap.java
index 17a061a..da790e2 100644
--- a/java/com/google/gerrit/server/notedb/RevisionNoteMap.java
+++ b/java/com/google/gerrit/server/notedb/RevisionNoteMap.java
@@ -64,7 +64,7 @@
   }
 
   static <T extends RevisionNote<? extends Comment>> RevisionNoteMap<T> emptyMap() {
-    return new RevisionNoteMap<>(NoteMap.newEmptyMap(), ImmutableMap.<RevId, T>of());
+    return new RevisionNoteMap<>(NoteMap.newEmptyMap(), ImmutableMap.of());
   }
 
   private RevisionNoteMap(NoteMap noteMap, ImmutableMap<RevId, T> revisionNotes) {
diff --git a/java/com/google/gerrit/server/notedb/RobotCommentNotes.java b/java/com/google/gerrit/server/notedb/RobotCommentNotes.java
index a05e6a1..92dd7d8 100644
--- a/java/com/google/gerrit/server/notedb/RobotCommentNotes.java
+++ b/java/com/google/gerrit/server/notedb/RobotCommentNotes.java
@@ -24,7 +24,6 @@
 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.notedb.NoteDbChangeState.PrimaryStorage;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
@@ -49,7 +48,7 @@
 
   @Inject
   RobotCommentNotes(Args args, @Assisted Change change) {
-    super(args, change.getId(), PrimaryStorage.of(change), false);
+    super(args, change.getId());
     this.change = change;
   }
 
@@ -98,7 +97,7 @@
             args.changeNoteJson, reader, NoteMap.read(reader, tipCommit));
     ListMultimap<RevId, RobotComment> cs = MultimapBuilder.hashKeys().arrayListValues().build();
     for (RobotCommentsRevisionNote rn : revisionNoteMap.revisionNotes.values()) {
-      for (RobotComment c : rn.getComments()) {
+      for (RobotComment c : rn.getEntities()) {
         cs.put(new RevId(c.revId), c);
       }
     }
diff --git a/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java b/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java
index e7ac5b7..0304ab8 100644
--- a/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java
+++ b/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java
@@ -19,14 +19,13 @@
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 
 import com.google.common.collect.Sets;
+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.gerrit.server.config.GerritServerConfig;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 import java.io.IOException;
@@ -38,7 +37,6 @@
 import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 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;
@@ -74,50 +72,26 @@
 
   @AssistedInject
   private RobotCommentUpdate(
-      @GerritServerConfig Config cfg,
       @GerritPersonIdent PersonIdent serverIdent,
-      NotesMigration migration,
       ChangeNoteUtil noteUtil,
       @Assisted ChangeNotes notes,
       @Assisted("effective") Account.Id accountId,
       @Assisted("real") Account.Id realAccountId,
       @Assisted PersonIdent authorIdent,
       @Assisted Date when) {
-    super(
-        cfg,
-        migration,
-        noteUtil,
-        serverIdent,
-        notes,
-        null,
-        accountId,
-        realAccountId,
-        authorIdent,
-        when);
+    super(noteUtil, serverIdent, notes, null, accountId, realAccountId, authorIdent, when);
   }
 
   @AssistedInject
   private RobotCommentUpdate(
-      @GerritServerConfig Config cfg,
       @GerritPersonIdent PersonIdent serverIdent,
-      NotesMigration migration,
       ChangeNoteUtil noteUtil,
       @Assisted Change change,
       @Assisted("effective") Account.Id accountId,
       @Assisted("real") Account.Id realAccountId,
       @Assisted PersonIdent authorIdent,
       @Assisted Date when) {
-    super(
-        cfg,
-        migration,
-        noteUtil,
-        serverIdent,
-        null,
-        change,
-        accountId,
-        realAccountId,
-        authorIdent,
-        when);
+    super(noteUtil, serverIdent, null, change, accountId, realAccountId, authorIdent, when);
   }
 
   public void putComment(RobotComment c) {
@@ -127,7 +101,7 @@
 
   private CommitBuilder storeCommentsInNotes(
       RevWalk rw, ObjectInserter ins, ObjectId curr, CommitBuilder cb)
-      throws ConfigInvalidException, OrmException, IOException {
+      throws ConfigInvalidException, IOException {
     RevisionNoteMap<RobotCommentsRevisionNote> rnm = getRevisionNoteMap(rw, curr);
     Set<RevId> updatedRevs = Sets.newHashSetWithExpectedSize(rnm.revisionNotes.size());
     RevisionNoteBuilder.Cache cache = new RevisionNoteBuilder.Cache(rnm);
@@ -174,23 +148,20 @@
   }
 
   private RevisionNoteMap<RobotCommentsRevisionNote> getRevisionNoteMap(RevWalk rw, ObjectId curr)
-      throws ConfigInvalidException, OrmException, IOException {
+      throws ConfigInvalidException, IOException {
     if (curr.equals(ObjectId.zeroId())) {
       return RevisionNoteMap.emptyMap();
     }
-    if (migration.readChanges()) {
-      // If reading from changes is enabled, then the old RobotCommentNotes
-      // already parsed the revision notes. We can reuse them as long as the ref
-      // hasn't advanced.
-      ChangeNotes changeNotes = getNotes();
-      if (changeNotes != null) {
-        RobotCommentNotes robotCommentNotes = changeNotes.load().getRobotCommentNotes();
-        if (robotCommentNotes != null) {
-          ObjectId idFromNotes = firstNonNull(robotCommentNotes.getRevision(), ObjectId.zeroId());
-          RevisionNoteMap<RobotCommentsRevisionNote> rnm = robotCommentNotes.getRevisionNoteMap();
-          if (idFromNotes.equals(curr) && rnm != null) {
-            return rnm;
-          }
+    // The old RobotCommentNotes already parsed the revision notes. We can reuse them as long as
+    // the ref hasn't advanced.
+    ChangeNotes changeNotes = getNotes();
+    if (changeNotes != null) {
+      RobotCommentNotes robotCommentNotes = changeNotes.load().getRobotCommentNotes();
+      if (robotCommentNotes != null) {
+        ObjectId idFromNotes = firstNonNull(robotCommentNotes.getRevision(), ObjectId.zeroId());
+        RevisionNoteMap<RobotCommentsRevisionNote> rnm = robotCommentNotes.getRevisionNoteMap();
+        if (idFromNotes.equals(curr) && rnm != null) {
+          return rnm;
         }
       }
     }
@@ -208,13 +179,13 @@
 
   @Override
   protected CommitBuilder applyImpl(RevWalk rw, ObjectInserter ins, ObjectId curr)
-      throws OrmException, IOException {
+      throws IOException {
     CommitBuilder cb = new CommitBuilder();
     cb.setMessage("Update robot comments");
     try {
       return storeCommentsInNotes(rw, ins, curr, cb);
     } catch (ConfigInvalidException e) {
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
   }
 
diff --git a/java/com/google/gerrit/server/notedb/Sequences.java b/java/com/google/gerrit/server/notedb/Sequences.java
new file mode 100644
index 0000000..9e8c541
--- /dev/null
+++ b/java/com/google/gerrit/server/notedb/Sequences.java
@@ -0,0 +1,125 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb;
+
+import com.google.common.collect.ImmutableList;
+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.Timer2;
+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.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import org.eclipse.jgit.lib.Config;
+
+@Singleton
+public class Sequences {
+  public static final String NAME_ACCOUNTS = "accounts";
+  public static final String NAME_GROUPS = "groups";
+  public static final String NAME_CHANGES = "changes";
+
+  public static final int FIRST_ACCOUNT_ID = 1000000;
+  public static final int FIRST_GROUP_ID = 1;
+  public static final int FIRST_CHANGE_ID = 1;
+
+  private enum SequenceType {
+    ACCOUNTS,
+    CHANGES,
+    GROUPS;
+  }
+
+  private final RepoSequence accountSeq;
+  private final RepoSequence changeSeq;
+  private final RepoSequence groupSeq;
+  private final Timer2<SequenceType, Boolean> nextIdLatency;
+
+  @Inject
+  public Sequences(
+      @GerritServerConfig Config cfg,
+      GitRepositoryManager repoManager,
+      GitReferenceUpdated gitRefUpdated,
+      AllProjectsName allProjects,
+      AllUsersName allUsers,
+      MetricMaker metrics) {
+
+    int accountBatchSize = cfg.getInt("noteDb", "accounts", "sequenceBatchSize", 1);
+    accountSeq =
+        new RepoSequence(
+            repoManager,
+            gitRefUpdated,
+            allUsers,
+            NAME_ACCOUNTS,
+            () -> FIRST_ACCOUNT_ID,
+            accountBatchSize);
+
+    int changeBatchSize = cfg.getInt("noteDb", "changes", "sequenceBatchSize", 20);
+    changeSeq =
+        new RepoSequence(
+            repoManager,
+            gitRefUpdated,
+            allProjects,
+            NAME_CHANGES,
+            () -> FIRST_CHANGE_ID,
+            changeBatchSize);
+
+    int groupBatchSize = 1;
+    groupSeq =
+        new RepoSequence(
+            repoManager,
+            gitRefUpdated,
+            allUsers,
+            NAME_GROUPS,
+            () -> FIRST_GROUP_ID,
+            groupBatchSize);
+
+    nextIdLatency =
+        metrics.newTimer(
+            "sequence/next_id_latency",
+            new Description("Latency of requesting IDs from repo sequences")
+                .setCumulative()
+                .setUnit(Units.MILLISECONDS),
+            Field.ofEnum(SequenceType.class, "sequence"),
+            Field.ofBoolean("multiple"));
+  }
+
+  public int nextAccountId() {
+    try (Timer2.Context timer = nextIdLatency.start(SequenceType.ACCOUNTS, false)) {
+      return accountSeq.next();
+    }
+  }
+
+  public int nextChangeId() {
+    try (Timer2.Context 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)) {
+      return changeSeq.next(count);
+    }
+  }
+
+  public int nextGroupId() {
+    try (Timer2.Context timer = nextIdLatency.start(SequenceType.GROUPS, false)) {
+      return groupSeq.next();
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/notedb/TestChangeRebuilderWrapper.java b/java/com/google/gerrit/server/notedb/TestChangeRebuilderWrapper.java
deleted file mode 100644
index 11fef24..0000000
--- a/java/com/google/gerrit/server/notedb/TestChangeRebuilderWrapper.java
+++ /dev/null
@@ -1,125 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Change.Id;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.notedb.NoteDbUpdateManager.Result;
-import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder;
-import com.google.gerrit.server.notedb.rebuild.ChangeRebuilderImpl;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import java.io.IOException;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-@VisibleForTesting
-@Singleton
-public class TestChangeRebuilderWrapper extends ChangeRebuilder {
-  private final ChangeRebuilderImpl delegate;
-  private final AtomicBoolean failNextUpdate;
-  private final AtomicBoolean stealNextUpdate;
-
-  @Inject
-  TestChangeRebuilderWrapper(SchemaFactory<ReviewDb> schemaFactory, ChangeRebuilderImpl rebuilder) {
-    super(schemaFactory);
-    this.delegate = rebuilder;
-    this.failNextUpdate = new AtomicBoolean();
-    this.stealNextUpdate = new AtomicBoolean();
-  }
-
-  public void failNextUpdate() {
-    failNextUpdate.set(true);
-  }
-
-  public void stealNextUpdate() {
-    stealNextUpdate.set(true);
-  }
-
-  @Override
-  public Result rebuild(ReviewDb db, Change.Id changeId) throws IOException, OrmException {
-    return rebuild(db, changeId, true);
-  }
-
-  @Override
-  public Result rebuildEvenIfReadOnly(ReviewDb db, Change.Id changeId)
-      throws IOException, OrmException {
-    return rebuild(db, changeId, false);
-  }
-
-  private Result rebuild(ReviewDb db, Change.Id changeId, boolean checkReadOnly)
-      throws IOException, OrmException {
-    if (failNextUpdate.getAndSet(false)) {
-      throw new IOException("Update failed");
-    }
-    Result result =
-        checkReadOnly
-            ? delegate.rebuild(db, changeId)
-            : delegate.rebuildEvenIfReadOnly(db, changeId);
-    if (stealNextUpdate.getAndSet(false)) {
-      throw new IOException("Update stolen");
-    }
-    return result;
-  }
-
-  @Override
-  public Result rebuild(NoteDbUpdateManager manager, ChangeBundle bundle)
-      throws IOException, OrmException {
-    // stealNextUpdate doesn't really apply in this case because the IOException
-    // would normally come from the manager.execute() method, which isn't called
-    // here.
-    return delegate.rebuild(manager, bundle);
-  }
-
-  @Override
-  public NoteDbUpdateManager stage(ReviewDb db, Change.Id changeId)
-      throws IOException, OrmException {
-    // Don't inspect stealNextUpdate; that happens in execute() below.
-    return delegate.stage(db, changeId);
-  }
-
-  @Override
-  public Result execute(ReviewDb db, Change.Id changeId, NoteDbUpdateManager manager)
-      throws OrmException, IOException {
-    if (failNextUpdate.getAndSet(false)) {
-      throw new IOException("Update failed");
-    }
-    Result result = delegate.execute(db, changeId, manager);
-    if (stealNextUpdate.getAndSet(false)) {
-      throw new IOException("Update stolen");
-    }
-    return result;
-  }
-
-  @Override
-  public void buildUpdates(NoteDbUpdateManager manager, ChangeBundle bundle)
-      throws IOException, OrmException {
-    // Don't check for manual failure; that happens in execute().
-    delegate.buildUpdates(manager, bundle);
-  }
-
-  @Override
-  public void rebuildReviewDb(ReviewDb db, Project.NameKey project, Id changeId)
-      throws OrmException {
-    if (failNextUpdate.getAndSet(false)) {
-      throw new OrmException("Update failed");
-    }
-    delegate.rebuildReviewDb(db, project, changeId);
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/rebuild/AbortUpdateException.java b/java/com/google/gerrit/server/notedb/rebuild/AbortUpdateException.java
deleted file mode 100644
index 0e6d3e9..0000000
--- a/java/com/google/gerrit/server/notedb/rebuild/AbortUpdateException.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb.rebuild;
-
-import com.google.gwtorm.server.OrmRuntimeException;
-
-class AbortUpdateException extends OrmRuntimeException {
-  private static final long serialVersionUID = 1L;
-
-  AbortUpdateException() {
-    super("aborted");
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/rebuild/ApprovalEvent.java b/java/com/google/gerrit/server/notedb/rebuild/ApprovalEvent.java
deleted file mode 100644
index 9ecf476..0000000
--- a/java/com/google/gerrit/server/notedb/rebuild/ApprovalEvent.java
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb.rebuild;
-
-import com.google.common.base.MoreObjects.ToStringHelper;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.server.notedb.ChangeUpdate;
-import java.sql.Timestamp;
-
-class ApprovalEvent extends Event {
-  private PatchSetApproval psa;
-
-  ApprovalEvent(PatchSetApproval psa, Timestamp changeCreatedOn) {
-    super(
-        psa.getPatchSetId(),
-        psa.getAccountId(),
-        psa.getRealAccountId(),
-        psa.getGranted(),
-        changeCreatedOn,
-        psa.getTag());
-    this.psa = psa;
-  }
-
-  @Override
-  boolean uniquePerUpdate() {
-    return false;
-  }
-
-  @Override
-  protected boolean canHaveTag() {
-    // Legacy SUBM approvals don't have a tag field set, but the corresponding
-    // ChangeMessage for merging the change does. We need to let these be in the
-    // same meta commit so the SUBM approval isn't counted as post-submit.
-    return !psa.isLegacySubmit();
-  }
-
-  @Override
-  void apply(ChangeUpdate update) {
-    checkUpdate(update);
-    update.putApproval(psa.getLabel(), psa.getValue());
-  }
-
-  @Override
-  protected boolean isPostSubmitApproval() {
-    return psa.isPostSubmit();
-  }
-
-  @Override
-  protected void addToString(ToStringHelper helper) {
-    helper.add("approval", psa);
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/rebuild/ChangeMessageEvent.java b/java/com/google/gerrit/server/notedb/rebuild/ChangeMessageEvent.java
deleted file mode 100644
index 53c9dc4..0000000
--- a/java/com/google/gerrit/server/notedb/rebuild/ChangeMessageEvent.java
+++ /dev/null
@@ -1,185 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb.rebuild;
-
-import com.google.common.base.MoreObjects.ToStringHelper;
-import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableMap;
-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.notedb.ChangeUpdate;
-import com.google.gwtorm.server.OrmException;
-import java.sql.Timestamp;
-import java.util.Map;
-import java.util.Optional;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-class ChangeMessageEvent extends Event {
-  private static final ImmutableMap<Change.Status, Pattern> STATUS_PATTERNS =
-      ImmutableMap.of(
-          Change.Status.ABANDONED, Pattern.compile("^Abandoned(\n.*)*$"),
-          Change.Status.MERGED,
-              Pattern.compile(
-                  "^Change has been successfully (merged|cherry-picked|rebased|pushed).*$"),
-          Change.Status.NEW, Pattern.compile("^Restored(\n.*)*$"));
-
-  private static final Pattern PRIVATE_SET_REGEXP = Pattern.compile("^Set private$");
-  private static final Pattern PRIVATE_UNSET_REGEXP = Pattern.compile("^Unset private$");
-
-  private static final Pattern TOPIC_SET_REGEXP = Pattern.compile("^Topic set to (.+)$");
-  private static final Pattern TOPIC_CHANGED_REGEXP =
-      Pattern.compile("^Topic changed from (.+) to (.+)$");
-  private static final Pattern TOPIC_REMOVED_REGEXP = Pattern.compile("^Topic (.+) removed$");
-
-  private static final Pattern WIP_SET_REGEXP = Pattern.compile("^Set Work In Progress$");
-  private static final Pattern WIP_UNSET_REGEXP = Pattern.compile("^Set Ready For Review$");
-
-  private final Change change;
-  private final Change noteDbChange;
-  private final Optional<Change.Status> status;
-  private final ChangeMessage message;
-
-  ChangeMessageEvent(
-      Change change, Change noteDbChange, ChangeMessage message, Timestamp changeCreatedOn) {
-    super(
-        message.getPatchSetId(),
-        message.getAuthor(),
-        message.getRealAuthor(),
-        message.getWrittenOn(),
-        changeCreatedOn,
-        message.getTag());
-    this.change = change;
-    this.noteDbChange = noteDbChange;
-    this.message = message;
-    this.status = parseStatus(message);
-  }
-
-  @Override
-  boolean uniquePerUpdate() {
-    return true;
-  }
-
-  @Override
-  protected boolean isSubmit() {
-    return status.isPresent() && status.get() == Change.Status.MERGED;
-  }
-
-  @Override
-  protected boolean canHaveTag() {
-    return true;
-  }
-
-  @SuppressWarnings("deprecation")
-  @Override
-  void apply(ChangeUpdate update) throws OrmException {
-    checkUpdate(update);
-    update.setChangeMessage(message.getMessage());
-    setPrivate(update);
-    setTopic(update);
-    setWorkInProgress(update);
-
-    if (status.isPresent()) {
-      Change.Status s = status.get();
-      update.fixStatus(s);
-      noteDbChange.setStatus(s);
-      if (s == Change.Status.MERGED) {
-        update.setSubmissionId(change.getSubmissionId());
-        noteDbChange.setSubmissionId(change.getSubmissionId());
-      }
-    }
-  }
-
-  private static Optional<Change.Status> parseStatus(ChangeMessage message) {
-    String msg = message.getMessage();
-    if (msg == null) {
-      return Optional.empty();
-    }
-    for (Map.Entry<Change.Status, Pattern> e : STATUS_PATTERNS.entrySet()) {
-      if (e.getValue().matcher(msg).matches()) {
-        return Optional.of(e.getKey());
-      }
-    }
-    return Optional.empty();
-  }
-
-  private void setPrivate(ChangeUpdate update) {
-    String msg = message.getMessage();
-    if (msg == null) {
-      return;
-    }
-    Matcher m = PRIVATE_SET_REGEXP.matcher(msg);
-    if (m.matches()) {
-      update.setPrivate(true);
-      noteDbChange.setPrivate(true);
-      return;
-    }
-
-    m = PRIVATE_UNSET_REGEXP.matcher(msg);
-    if (m.matches()) {
-      update.setPrivate(false);
-      noteDbChange.setPrivate(false);
-    }
-  }
-
-  private void setTopic(ChangeUpdate update) {
-    String msg = message.getMessage();
-    if (msg == null) {
-      return;
-    }
-    Matcher m = TOPIC_SET_REGEXP.matcher(msg);
-    if (m.matches()) {
-      String topic = m.group(1);
-      update.setTopic(topic);
-      noteDbChange.setTopic(topic);
-      return;
-    }
-
-    m = TOPIC_CHANGED_REGEXP.matcher(msg);
-    if (m.matches()) {
-      String topic = m.group(2);
-      update.setTopic(topic);
-      noteDbChange.setTopic(topic);
-      return;
-    }
-
-    if (TOPIC_REMOVED_REGEXP.matcher(msg).matches()) {
-      update.setTopic(null);
-      noteDbChange.setTopic(null);
-    }
-  }
-
-  private void setWorkInProgress(ChangeUpdate update) {
-    String msg = Strings.nullToEmpty(message.getMessage());
-    String tag = message.getTag();
-    if (ChangeMessagesUtil.TAG_SET_WIP.equals(tag)
-        || ChangeMessagesUtil.TAG_UPLOADED_WIP_PATCH_SET.equals(tag)
-        || WIP_SET_REGEXP.matcher(msg).matches()) {
-      update.setWorkInProgress(true);
-      noteDbChange.setWorkInProgress(true);
-    } else if (ChangeMessagesUtil.TAG_SET_READY.equals(tag)
-        || ChangeMessagesUtil.TAG_UPLOADED_PATCH_SET.equals(tag)
-        || WIP_UNSET_REGEXP.matcher(msg).matches()) {
-      update.setWorkInProgress(false);
-      noteDbChange.setWorkInProgress(false);
-    }
-  }
-
-  @Override
-  protected void addToString(ToStringHelper helper) {
-    helper.add("message", message);
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilder.java b/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilder.java
deleted file mode 100644
index 8ce9987..0000000
--- a/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilder.java
+++ /dev/null
@@ -1,81 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb.rebuild;
-
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.ListeningExecutorService;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.notedb.ChangeBundle;
-import com.google.gerrit.server.notedb.NoteDbUpdateManager;
-import com.google.gerrit.server.notedb.NoteDbUpdateManager.Result;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import java.io.IOException;
-
-public abstract class ChangeRebuilder {
-  public static class NoPatchSetsException extends OrmException {
-    private static final long serialVersionUID = 1L;
-
-    NoPatchSetsException(Change.Id changeId) {
-      super("Change " + changeId + " cannot be rebuilt because it has no patch sets");
-    }
-  }
-
-  private final SchemaFactory<ReviewDb> schemaFactory;
-
-  protected ChangeRebuilder(SchemaFactory<ReviewDb> schemaFactory) {
-    this.schemaFactory = schemaFactory;
-  }
-
-  public final ListenableFuture<Result> rebuildAsync(
-      Change.Id id, ListeningExecutorService executor) {
-    return executor.submit(
-        () -> {
-          try (ReviewDb db = schemaFactory.open()) {
-            return rebuild(db, id);
-          }
-        });
-  }
-
-  /**
-   * Rebuild ReviewDb contents by copying from NoteDb.
-   *
-   * <p>Requires NoteDb to be the primary storage for the change.
-   */
-  public abstract void rebuildReviewDb(ReviewDb db, Project.NameKey project, Change.Id changeId)
-      throws OrmException;
-
-  // In the following methods "rebuilding" always refers to copying the state
-  // from ReviewDb to NoteDb, i.e. assuming ReviewDb is the primary storage.
-
-  public abstract Result rebuild(ReviewDb db, Change.Id changeId) throws IOException, OrmException;
-
-  public abstract Result rebuildEvenIfReadOnly(ReviewDb db, Change.Id changeId)
-      throws IOException, OrmException;
-
-  public abstract Result rebuild(NoteDbUpdateManager manager, ChangeBundle bundle)
-      throws IOException, OrmException;
-
-  public abstract void buildUpdates(NoteDbUpdateManager manager, ChangeBundle bundle)
-      throws IOException, OrmException;
-
-  public abstract NoteDbUpdateManager stage(ReviewDb db, Change.Id changeId)
-      throws IOException, OrmException;
-
-  public abstract Result execute(ReviewDb db, Change.Id changeId, NoteDbUpdateManager manager)
-      throws OrmException, IOException;
-}
diff --git a/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java b/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
deleted file mode 100644
index 0d516e5..0000000
--- a/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
+++ /dev/null
@@ -1,685 +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.notedb.rebuild;
-
-import static com.google.common.base.MoreObjects.firstNonNull;
-import static com.google.common.base.Preconditions.checkState;
-import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_HASHTAGS;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET;
-import static java.util.Objects.requireNonNull;
-import static java.util.concurrent.TimeUnit.SECONDS;
-import static java.util.stream.Collectors.toList;
-
-import com.google.common.base.Splitter;
-import com.google.common.collect.FluentIterable;
-import com.google.common.collect.ImmutableCollection;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.ListMultimap;
-import com.google.common.collect.Lists;
-import com.google.common.collect.MultimapBuilder;
-import com.google.common.collect.Ordering;
-import com.google.common.collect.Sets;
-import com.google.common.collect.Table;
-import com.google.common.primitives.Ints;
-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.ChangeMessage;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
-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.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.reviewdb.server.ReviewDbUtil;
-import com.google.gerrit.server.CommentsUtil;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.config.GerritServerId;
-import com.google.gerrit.server.notedb.ChangeBundle;
-import com.google.gerrit.server.notedb.ChangeBundleReader;
-import com.google.gerrit.server.notedb.ChangeDraftUpdate;
-import com.google.gerrit.server.notedb.ChangeNoteUtil;
-import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.notedb.NoteDbChangeState;
-import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
-import com.google.gerrit.server.notedb.NoteDbUpdateManager;
-import com.google.gerrit.server.notedb.NoteDbUpdateManager.OpenRepo;
-import com.google.gerrit.server.notedb.NoteDbUpdateManager.Result;
-import com.google.gerrit.server.notedb.NotesMigration;
-import com.google.gerrit.server.notedb.ReviewerStateInternal;
-import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.update.ChainedReceiveCommands;
-import com.google.gwtorm.client.Key;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.AtomicUpdate;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-import java.io.IOException;
-import java.sql.Timestamp;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.TreeMap;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.ReceiveCommand;
-
-public class ChangeRebuilderImpl extends ChangeRebuilder {
-  /**
-   * The maximum amount of time between the ReviewDb timestamp of the first and last events batched
-   * together into a single NoteDb update.
-   *
-   * <p>Used to account for the fact that different records with their own timestamps (e.g. {@link
-   * PatchSetApproval} and {@link ChangeMessage}) historically didn't necessarily use the same
-   * timestamp, and tended to call {@code System.currentTimeMillis()} independently.
-   */
-  public static final long MAX_WINDOW_MS = SECONDS.toMillis(3);
-
-  /**
-   * The maximum amount of time between two consecutive events to consider them to be in the same
-   * batch.
-   */
-  static final long MAX_DELTA_MS = SECONDS.toMillis(1);
-
-  private final ChangeBundleReader bundleReader;
-  private final ChangeDraftUpdate.Factory draftUpdateFactory;
-  private final ChangeNoteUtil changeNoteUtil;
-  private final ChangeNotes.Factory notesFactory;
-  private final ChangeUpdate.Factory updateFactory;
-  private final CommentsUtil commentsUtil;
-  private final NoteDbUpdateManager.Factory updateManagerFactory;
-  private final NotesMigration migration;
-  private final PatchListCache patchListCache;
-  private final PersonIdent serverIdent;
-  private final ProjectCache projectCache;
-  private final String serverId;
-  private final long skewMs;
-
-  @Inject
-  ChangeRebuilderImpl(
-      @GerritServerConfig Config cfg,
-      SchemaFactory<ReviewDb> schemaFactory,
-      ChangeBundleReader bundleReader,
-      ChangeDraftUpdate.Factory draftUpdateFactory,
-      ChangeNoteUtil changeNoteUtil,
-      ChangeNotes.Factory notesFactory,
-      ChangeUpdate.Factory updateFactory,
-      CommentsUtil commentsUtil,
-      NoteDbUpdateManager.Factory updateManagerFactory,
-      NotesMigration migration,
-      PatchListCache patchListCache,
-      @GerritPersonIdent PersonIdent serverIdent,
-      @Nullable ProjectCache projectCache,
-      @GerritServerId String serverId) {
-    super(schemaFactory);
-    this.bundleReader = bundleReader;
-    this.draftUpdateFactory = draftUpdateFactory;
-    this.changeNoteUtil = changeNoteUtil;
-    this.notesFactory = notesFactory;
-    this.updateFactory = updateFactory;
-    this.commentsUtil = commentsUtil;
-    this.updateManagerFactory = updateManagerFactory;
-    this.migration = migration;
-    this.patchListCache = patchListCache;
-    this.serverIdent = serverIdent;
-    this.projectCache = projectCache;
-    this.serverId = serverId;
-    this.skewMs = NoteDbChangeState.getReadOnlySkew(cfg);
-  }
-
-  @Override
-  public Result rebuild(ReviewDb db, Change.Id changeId) throws IOException, OrmException {
-    return rebuild(db, changeId, true);
-  }
-
-  @Override
-  public Result rebuildEvenIfReadOnly(ReviewDb db, Change.Id changeId)
-      throws IOException, OrmException {
-    return rebuild(db, changeId, false);
-  }
-
-  private Result rebuild(ReviewDb db, Change.Id changeId, boolean checkReadOnly)
-      throws IOException, OrmException {
-    db = ReviewDbUtil.unwrapDb(db);
-    // Read change just to get project; this instance is then discarded so we can read a consistent
-    // ChangeBundle inside a transaction.
-    Change change = db.changes().get(changeId);
-    if (change == null) {
-      throw new NoSuchChangeException(changeId);
-    }
-    try (NoteDbUpdateManager manager = updateManagerFactory.create(change.getProject())) {
-      buildUpdates(manager, bundleReader.fromReviewDb(db, changeId));
-      return execute(db, changeId, manager, checkReadOnly, true);
-    }
-  }
-
-  @Override
-  public Result rebuild(NoteDbUpdateManager manager, ChangeBundle bundle)
-      throws NoSuchChangeException, IOException, OrmException {
-    Change change = new Change(bundle.getChange());
-    buildUpdates(manager, bundle);
-    return manager.stageAndApplyDelta(change);
-  }
-
-  @Override
-  public NoteDbUpdateManager stage(ReviewDb db, Change.Id changeId)
-      throws IOException, OrmException {
-    db = ReviewDbUtil.unwrapDb(db);
-    Change change = checkNoteDbState(ChangeNotes.readOneReviewDbChange(db, changeId));
-    if (change == null) {
-      throw new NoSuchChangeException(changeId);
-    }
-    NoteDbUpdateManager manager = updateManagerFactory.create(change.getProject());
-    buildUpdates(manager, bundleReader.fromReviewDb(db, changeId));
-    manager.stage();
-    return manager;
-  }
-
-  @Override
-  public Result execute(ReviewDb db, Change.Id changeId, NoteDbUpdateManager manager)
-      throws OrmException, IOException {
-    return execute(db, changeId, manager, true, true);
-  }
-
-  public Result execute(
-      ReviewDb db,
-      Change.Id changeId,
-      NoteDbUpdateManager manager,
-      boolean checkReadOnly,
-      boolean executeManager)
-      throws OrmException, IOException {
-    db = ReviewDbUtil.unwrapDb(db);
-    Change change = checkNoteDbState(ChangeNotes.readOneReviewDbChange(db, changeId));
-    if (change == null) {
-      throw new NoSuchChangeException(changeId);
-    }
-
-    String oldNoteDbStateStr = change.getNoteDbState();
-    Result r = manager.stageAndApplyDelta(change);
-    String newNoteDbStateStr = change.getNoteDbState();
-    if (newNoteDbStateStr == null) {
-      throw new OrmException(
-          String.format(
-              "Rebuilding change %s produced no writes to NoteDb: %s",
-              changeId, bundleReader.fromReviewDb(db, changeId)));
-    }
-    NoteDbChangeState newNoteDbState =
-        requireNonNull(NoteDbChangeState.parse(changeId, newNoteDbStateStr));
-    try {
-      db.changes()
-          .atomicUpdate(
-              changeId,
-              new AtomicUpdate<Change>() {
-                @Override
-                public Change update(Change change) {
-                  if (checkReadOnly) {
-                    NoteDbChangeState.checkNotReadOnly(change, skewMs);
-                  }
-                  String currNoteDbStateStr = change.getNoteDbState();
-                  if (Objects.equals(currNoteDbStateStr, newNoteDbStateStr)) {
-                    // Another thread completed the same rebuild we were about to.
-                    throw new AbortUpdateException();
-                  } else if (!Objects.equals(oldNoteDbStateStr, currNoteDbStateStr)) {
-                    // Another thread updated the state to something else.
-                    throw new ConflictingUpdateRuntimeException(change, oldNoteDbStateStr);
-                  }
-                  change.setNoteDbState(newNoteDbStateStr);
-                  return change;
-                }
-              });
-    } catch (ConflictingUpdateRuntimeException e) {
-      // Rethrow as an OrmException so the caller knows to use staged results. Strictly speaking
-      // they are not completely up to date, but result we send to the caller is the same as if this
-      // rebuild had executed before the other thread.
-      throw new ConflictingUpdateException(e);
-    } catch (AbortUpdateException e) {
-      if (newNoteDbState.isUpToDate(
-          manager.getChangeRepo().cmds.getRepoRefCache(),
-          manager.getAllUsersRepo().cmds.getRepoRefCache())) {
-        // If the state in ReviewDb matches NoteDb at this point, it means another thread
-        // successfully completed this rebuild. It's ok to not execute the update in this case,
-        // since the object referenced in the Result was flushed to the repo by whatever thread won
-        // the race.
-        return r;
-      }
-      // If the state doesn't match, that means another thread attempted this rebuild, but
-      // failed. Fall through and try to update the ref again.
-    }
-    if (migration.failChangeWrites()) {
-      // Don't even attempt to execute if read-only, it would fail anyway. But do throw an exception
-      // to the caller so they know to use the staged results instead of reading from the repo.
-      throw new OrmException(NoteDbUpdateManager.CHANGES_READ_ONLY);
-    }
-    if (executeManager) {
-      manager.execute();
-    }
-    return r;
-  }
-
-  static Change checkNoteDbState(Change c) throws OrmException {
-    // Can only rebuild a change if its primary storage is ReviewDb.
-    NoteDbChangeState s = NoteDbChangeState.parse(c);
-    if (s != null && s.getPrimaryStorage() != PrimaryStorage.REVIEW_DB) {
-      throw new OrmException(String.format("cannot rebuild change %s with state %s", c.getId(), s));
-    }
-    return c;
-  }
-
-  @Override
-  public void buildUpdates(NoteDbUpdateManager manager, ChangeBundle bundle)
-      throws IOException, OrmException {
-    manager.setCheckExpectedState(false).setRefLogMessage("Rebuilding change");
-    Change change = new Change(bundle.getChange());
-    if (bundle.getPatchSets().isEmpty()) {
-      throw new NoPatchSetsException(change.getId());
-    }
-    if (change.getLastUpdatedOn().compareTo(change.getCreatedOn()) < 0) {
-      // A bug in data migration might set created_on to the time of the migration. The
-      // correct timestamps were lost, but we can at least set it so created_on is not after
-      // last_updated_on.
-      // See https://bugs.chromium.org/p/gerrit/issues/detail?id=7397
-      change.setCreatedOn(change.getLastUpdatedOn());
-    }
-
-    // We will rebuild all events, except for draft comments, in buckets based on author and
-    // timestamp.
-    List<Event> events = new ArrayList<>();
-    ListMultimap<Account.Id, DraftCommentEvent> draftCommentEvents =
-        MultimapBuilder.hashKeys().arrayListValues().build();
-
-    events.addAll(getHashtagsEvents(change, manager));
-
-    // Delete ref only after hashtags have been read.
-    deleteChangeMetaRef(change, manager.getChangeRepo().cmds);
-    deleteDraftRefs(change, manager.getAllUsersRepo());
-
-    Integer minPsNum = getMinPatchSetNum(bundle);
-    TreeMap<PatchSet.Id, PatchSetEvent> patchSetEvents =
-        new TreeMap<>(ReviewDbUtil.intKeyOrdering());
-
-    for (PatchSet ps : bundle.getPatchSets()) {
-      PatchSetEvent pse = new PatchSetEvent(change, ps, manager.getChangeRepo().rw);
-      patchSetEvents.put(ps.getId(), pse);
-      events.add(pse);
-      for (Comment c : getComments(bundle, serverId, Status.PUBLISHED, ps)) {
-        CommentEvent e = new CommentEvent(c, change, ps, patchListCache);
-        events.add(e.addDep(pse));
-      }
-      for (Comment c : getComments(bundle, serverId, Status.DRAFT, ps)) {
-        DraftCommentEvent e = new DraftCommentEvent(c, change, ps, patchListCache);
-        draftCommentEvents.put(c.author.getId(), e);
-      }
-    }
-    ensurePatchSetOrder(patchSetEvents);
-
-    for (PatchSetApproval psa : bundle.getPatchSetApprovals()) {
-      PatchSetEvent pse = patchSetEvents.get(psa.getPatchSetId());
-      if (pse != null) {
-        events.add(new ApprovalEvent(psa, change.getCreatedOn()).addDep(pse));
-      }
-    }
-
-    for (Table.Cell<ReviewerStateInternal, Account.Id, Timestamp> r :
-        bundle.getReviewers().asTable().cellSet()) {
-      events.add(new ReviewerEvent(r, change.getCreatedOn()));
-    }
-
-    Change noteDbChange = new Change(null, null, null, null, null);
-    for (ChangeMessage msg : bundle.getChangeMessages()) {
-      Event msgEvent = new ChangeMessageEvent(change, noteDbChange, msg, change.getCreatedOn());
-      if (msg.getPatchSetId() != null) {
-        PatchSetEvent pse = patchSetEvents.get(msg.getPatchSetId());
-        if (pse == null) {
-          continue; // Ignore events for missing patch sets.
-        }
-        msgEvent.addDep(pse);
-      }
-      events.add(msgEvent);
-    }
-
-    sortAndFillEvents(change, noteDbChange, bundle.getPatchSets(), events, minPsNum);
-
-    EventList<Event> el = new EventList<>();
-    for (Event e : events) {
-      if (!el.canAdd(e)) {
-        flushEventsToUpdate(manager, el, change);
-        checkState(el.canAdd(e));
-      }
-      el.add(e);
-    }
-    flushEventsToUpdate(manager, el, change);
-
-    EventList<DraftCommentEvent> plcel = new EventList<>();
-    for (Account.Id author : draftCommentEvents.keys()) {
-      for (DraftCommentEvent e : Ordering.natural().sortedCopy(draftCommentEvents.get(author))) {
-        if (!plcel.canAdd(e)) {
-          flushEventsToDraftUpdate(manager, plcel, change);
-          checkState(plcel.canAdd(e));
-        }
-        plcel.add(e);
-      }
-      flushEventsToDraftUpdate(manager, plcel, change);
-    }
-  }
-
-  private static Integer getMinPatchSetNum(ChangeBundle bundle) {
-    Integer minPsNum = null;
-    for (PatchSet ps : bundle.getPatchSets()) {
-      int n = ps.getId().get();
-      if (minPsNum == null || n < minPsNum) {
-        minPsNum = n;
-      }
-    }
-    return minPsNum;
-  }
-
-  private static void ensurePatchSetOrder(TreeMap<PatchSet.Id, PatchSetEvent> events) {
-    if (events.isEmpty()) {
-      return;
-    }
-    Iterator<PatchSetEvent> it = events.values().iterator();
-    PatchSetEvent curr = it.next();
-    while (it.hasNext()) {
-      PatchSetEvent next = it.next();
-      next.addDep(curr);
-      curr = next;
-    }
-  }
-
-  private static List<Comment> getComments(
-      ChangeBundle bundle, String serverId, PatchLineComment.Status status, PatchSet ps) {
-    return bundle.getPatchLineComments().stream()
-        .filter(c -> c.getPatchSetId().equals(ps.getId()) && c.getStatus() == status)
-        .map(plc -> plc.asComment(serverId))
-        .sorted(CommentsUtil.COMMENT_ORDER)
-        .collect(toList());
-  }
-
-  private void sortAndFillEvents(
-      Change change,
-      Change noteDbChange,
-      ImmutableCollection<PatchSet> patchSets,
-      List<Event> events,
-      Integer minPsNum) {
-    Event finalUpdates = new FinalUpdatesEvent(change, noteDbChange, patchSets);
-    events.add(finalUpdates);
-    setPostSubmitDeps(events);
-    new EventSorter(events).sort();
-
-    // Ensure the first event in the list creates the change, setting the author and any required
-    // footers. Also force the creation time of the first patch set to match the creation time of
-    // the change.
-    Event first = events.get(0);
-    if (first instanceof PatchSetEvent && change.getOwner().equals(first.user)) {
-      first.when = change.getCreatedOn();
-      ((PatchSetEvent) first).createChange = true;
-    } else {
-      events.add(0, new CreateChangeEvent(change, minPsNum));
-    }
-
-    // Final pass to correct some inconsistencies.
-    //
-    // First, fill in any missing patch set IDs using the latest patch set of the change at the time
-    // of the event, because NoteDb can't represent actions with no associated patch set ID. This
-    // workaround is as if a user added a ChangeMessage on the change by replying from the latest
-    // patch set.
-    //
-    // Start with the first patch set that actually exists. If there are no patch sets at all,
-    // minPsNum will be null, so just bail and use 1 as the patch set ID.
-    //
-    // Second, ensure timestamps are nondecreasing, by copying the previous timestamp if this
-    // happens. This assumes that the only way this can happen is due to dependency constraints, and
-    // it is ok to give an event the same timestamp as one of its dependencies.
-    int ps = firstNonNull(minPsNum, 1);
-    for (int i = 0; i < events.size(); i++) {
-      Event e = events.get(i);
-      if (e.psId == null) {
-        e.psId = new PatchSet.Id(change.getId(), ps);
-      } else {
-        ps = Math.max(ps, e.psId.get());
-      }
-
-      if (i > 0) {
-        Event p = events.get(i - 1);
-        if (e.when.before(p.when)) {
-          e.when = p.when;
-        }
-      }
-    }
-  }
-
-  private void setPostSubmitDeps(List<Event> events) {
-    Optional<Event> submitEvent =
-        Lists.reverse(events).stream().filter(Event::isSubmit).findFirst();
-    if (submitEvent.isPresent()) {
-      events.stream().filter(Event::isPostSubmitApproval).forEach(e -> e.addDep(submitEvent.get()));
-    }
-  }
-
-  private void flushEventsToUpdate(
-      NoteDbUpdateManager manager, EventList<Event> events, Change change)
-      throws OrmException, IOException {
-    if (events.isEmpty()) {
-      return;
-    }
-    Comparator<String> labelNameComparator;
-    if (projectCache != null) {
-      labelNameComparator = projectCache.get(change.getProject()).getLabelTypes().nameComparator();
-    } else {
-      // No project cache available, bail and use natural ordering; there's no semantic difference
-      // anyway difference.
-      labelNameComparator = Ordering.natural();
-    }
-    ChangeUpdate update =
-        updateFactory.create(
-            change,
-            events.getAccountId(),
-            events.getRealAccountId(),
-            newAuthorIdent(events),
-            events.getWhen(),
-            labelNameComparator);
-    update.setAllowWriteToNewRef(true);
-    update.setPatchSetId(events.getPatchSetId());
-    update.setTag(events.getTag());
-    for (Event e : events) {
-      e.apply(update);
-    }
-    manager.add(update);
-    events.clear();
-  }
-
-  private void flushEventsToDraftUpdate(
-      NoteDbUpdateManager manager, EventList<DraftCommentEvent> events, Change change) {
-    if (events.isEmpty()) {
-      return;
-    }
-    ChangeDraftUpdate update =
-        draftUpdateFactory.create(
-            change,
-            events.getAccountId(),
-            events.getRealAccountId(),
-            newAuthorIdent(events),
-            events.getWhen());
-    update.setPatchSetId(events.getPatchSetId());
-    for (DraftCommentEvent e : events) {
-      e.applyDraft(update);
-    }
-    manager.add(update);
-    events.clear();
-  }
-
-  private PersonIdent newAuthorIdent(EventList<?> events) {
-    Account.Id id = events.getAccountId();
-    if (id == null) {
-      return new PersonIdent(serverIdent, events.getWhen());
-    }
-    return changeNoteUtil.newIdent(id, events.getWhen(), serverIdent);
-  }
-
-  private List<HashtagsEvent> getHashtagsEvents(Change change, NoteDbUpdateManager manager)
-      throws IOException {
-    String refName = changeMetaRef(change.getId());
-    Optional<ObjectId> old = manager.getChangeRepo().getObjectId(refName);
-    if (!old.isPresent()) {
-      return Collections.emptyList();
-    }
-
-    RevWalk rw = manager.getChangeRepo().rw;
-    List<HashtagsEvent> events = new ArrayList<>();
-    rw.reset();
-    rw.markStart(rw.parseCommit(old.get()));
-    for (RevCommit commit : rw) {
-      Account.Id authorId;
-      try {
-        authorId =
-            changeNoteUtil
-                .getLegacyChangeNoteRead()
-                .parseIdent(commit.getAuthorIdent(), change.getId());
-      } catch (ConfigInvalidException e) {
-        continue; // Corrupt data, no valid hashtags in this commit.
-      }
-      PatchSet.Id psId = parsePatchSetId(change, commit);
-      Set<String> hashtags = parseHashtags(commit);
-      if (authorId == null || psId == null || hashtags == null) {
-        continue;
-      }
-
-      Timestamp commitTime = new Timestamp(commit.getCommitterIdent().getWhen().getTime());
-      events.add(new HashtagsEvent(psId, authorId, commitTime, hashtags, change.getCreatedOn()));
-    }
-    return events;
-  }
-
-  private Set<String> parseHashtags(RevCommit commit) {
-    List<String> hashtagsLines = commit.getFooterLines(FOOTER_HASHTAGS);
-    if (hashtagsLines.isEmpty() || hashtagsLines.size() > 1) {
-      return null;
-    }
-
-    if (hashtagsLines.get(0).isEmpty()) {
-      return ImmutableSet.of();
-    }
-    return Sets.newHashSet(Splitter.on(',').split(hashtagsLines.get(0)));
-  }
-
-  private PatchSet.Id parsePatchSetId(Change change, RevCommit commit) {
-    List<String> psIdLines = commit.getFooterLines(FOOTER_PATCH_SET);
-    if (psIdLines.size() != 1) {
-      return null;
-    }
-    Integer psId = Ints.tryParse(psIdLines.get(0));
-    if (psId == null) {
-      return null;
-    }
-    return new PatchSet.Id(change.getId(), psId);
-  }
-
-  private void deleteChangeMetaRef(Change change, ChainedReceiveCommands cmds) throws IOException {
-    String refName = changeMetaRef(change.getId());
-    Optional<ObjectId> old = cmds.get(refName);
-    if (old.isPresent()) {
-      cmds.add(new ReceiveCommand(old.get(), ObjectId.zeroId(), refName));
-    }
-  }
-
-  private void deleteDraftRefs(Change change, OpenRepo allUsersRepo) throws IOException {
-    for (Ref r :
-        allUsersRepo
-            .repo
-            .getRefDatabase()
-            .getRefsByPrefix(RefNames.refsDraftCommentsPrefix(change.getId()))) {
-      allUsersRepo.cmds.add(new ReceiveCommand(r.getObjectId(), ObjectId.zeroId(), r.getName()));
-    }
-  }
-
-  static void createChange(ChangeUpdate update, Change change) {
-    update.setSubjectForCommit("Create change");
-    update.setChangeId(change.getKey().get());
-    update.setBranch(change.getDest().get());
-    update.setSubject(change.getOriginalSubject());
-    if (change.getRevertOf() != null) {
-      update.setRevertOf(change.getRevertOf().get());
-    }
-  }
-
-  @Override
-  public void rebuildReviewDb(ReviewDb db, Project.NameKey project, Change.Id changeId)
-      throws OrmException {
-    // TODO(dborowitz): Fail fast if changes tables are disabled in ReviewDb.
-    ChangeNotes notes = notesFactory.create(db, project, changeId);
-    ChangeBundle bundle = ChangeBundle.fromNotes(commentsUtil, notes);
-
-    db = ReviewDbUtil.unwrapDb(db);
-    db.changes().beginTransaction(changeId);
-    try {
-      Change c = db.changes().get(changeId);
-      if (c != null) {
-        PrimaryStorage ps = PrimaryStorage.of(c);
-        switch (ps) {
-          case REVIEW_DB:
-            return; // Nothing to do.
-          case NOTE_DB:
-            break; // Continue and rebuild.
-          default:
-            throw new OrmException("primary storage of " + changeId + " is " + ps);
-        }
-      } else {
-        c = notes.getChange();
-      }
-      db.changes().upsert(Collections.singleton(c));
-      putExactlyEntities(
-          db.changeMessages(), db.changeMessages().byChange(c.getId()), bundle.getChangeMessages());
-      putExactlyEntities(db.patchSets(), db.patchSets().byChange(c.getId()), bundle.getPatchSets());
-      putExactlyEntities(
-          db.patchSetApprovals(),
-          db.patchSetApprovals().byChange(c.getId()),
-          bundle.getPatchSetApprovals());
-      putExactlyEntities(
-          db.patchComments(),
-          db.patchComments().byChange(c.getId()),
-          bundle.getPatchLineComments());
-      db.commit();
-    } finally {
-      db.rollback();
-    }
-  }
-
-  private static <T, K extends Key<?>> void putExactlyEntities(
-      Access<T, K> access, Iterable<T> existing, Collection<T> ents) throws OrmException {
-    Set<K> toKeep = access.toMap(ents).keySet();
-    access.delete(
-        FluentIterable.from(existing).filter(e -> !toKeep.contains(access.primaryKey(e))));
-    access.upsert(ents);
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/rebuild/CommentEvent.java b/java/com/google/gerrit/server/notedb/rebuild/CommentEvent.java
deleted file mode 100644
index 8f7b387..0000000
--- a/java/com/google/gerrit/server/notedb/rebuild/CommentEvent.java
+++ /dev/null
@@ -1,82 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb.rebuild;
-
-import static com.google.gerrit.server.CommentsUtil.setCommentRevId;
-
-import com.google.common.base.MoreObjects.ToStringHelper;
-import com.google.common.flogger.FluentLogger;
-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.PatchSet;
-import com.google.gerrit.server.CommentsUtil;
-import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gerrit.server.patch.PatchListNotAvailableException;
-
-class CommentEvent extends Event {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
-  public final Comment c;
-  private final Change change;
-  private final PatchSet ps;
-  private final PatchListCache cache;
-
-  CommentEvent(Comment c, Change change, PatchSet ps, PatchListCache cache) {
-    super(
-        CommentsUtil.getCommentPsId(change.getId(), c),
-        c.author.getId(),
-        c.getRealAuthor().getId(),
-        c.writtenOn,
-        change.getCreatedOn(),
-        c.tag);
-    this.c = c;
-    this.change = change;
-    this.ps = ps;
-    this.cache = cache;
-  }
-
-  @Override
-  boolean uniquePerUpdate() {
-    return false;
-  }
-
-  @Override
-  protected boolean canHaveTag() {
-    return true;
-  }
-
-  @Override
-  void apply(ChangeUpdate update) {
-    checkUpdate(update);
-    if (c.revId == null) {
-      try {
-        setCommentRevId(c, cache, change, ps);
-      } catch (PatchListNotAvailableException e) {
-        logger.atWarning().log(
-            "Unable to determine parent commit of patch set %s (%s); omitting inline comment %s",
-            ps.getId(), ps.getRevision(), c);
-        return;
-      }
-    }
-    update.putComment(PatchLineComment.Status.PUBLISHED, c);
-  }
-
-  @Override
-  protected void addToString(ToStringHelper helper) {
-    helper.add("message", c.message);
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/rebuild/ConflictingUpdateException.java b/java/com/google/gerrit/server/notedb/rebuild/ConflictingUpdateException.java
deleted file mode 100644
index d8e7480..0000000
--- a/java/com/google/gerrit/server/notedb/rebuild/ConflictingUpdateException.java
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb.rebuild;
-
-import com.google.gwtorm.server.OrmException;
-
-/**
- * {@link com.google.gwtorm.server.OrmException} thrown by {@link ChangeRebuilder} when rebuilding a
- * change failed because another operation modified its {@link
- * com.google.gerrit.server.notedb.NoteDbChangeState}.
- */
-public class ConflictingUpdateException extends OrmException {
-  private static final long serialVersionUID = 1L;
-
-  // Always created from a ConflictingUpdateRuntimeException because it originates from an
-  // AtomicUpdate, which cannot throw checked exceptions.
-  ConflictingUpdateException(ConflictingUpdateRuntimeException cause) {
-    super(cause.getMessage(), cause);
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/rebuild/ConflictingUpdateRuntimeException.java b/java/com/google/gerrit/server/notedb/rebuild/ConflictingUpdateRuntimeException.java
deleted file mode 100644
index abfafa2..0000000
--- a/java/com/google/gerrit/server/notedb/rebuild/ConflictingUpdateRuntimeException.java
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb.rebuild;
-
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gwtorm.server.OrmRuntimeException;
-
-class ConflictingUpdateRuntimeException extends OrmRuntimeException {
-  private static final long serialVersionUID = 1L;
-
-  ConflictingUpdateRuntimeException(Change change, String expectedNoteDbState) {
-    super(
-        String.format(
-            "Expected change %s to have noteDbState %s but was %s",
-            change.getId(), expectedNoteDbState, change.getNoteDbState()));
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/rebuild/CreateChangeEvent.java b/java/com/google/gerrit/server/notedb/rebuild/CreateChangeEvent.java
deleted file mode 100644
index d01071b..0000000
--- a/java/com/google/gerrit/server/notedb/rebuild/CreateChangeEvent.java
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb.rebuild;
-
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gwtorm.server.OrmException;
-import java.io.IOException;
-
-class CreateChangeEvent extends Event {
-  private final Change change;
-
-  private static PatchSet.Id psId(Change change, Integer minPsNum) {
-    int n;
-    if (minPsNum == null) {
-      // There were no patch sets for the change at all, so something is very
-      // wrong. Bail and use 1 as the patch set.
-      n = 1;
-    } else {
-      n = minPsNum;
-    }
-    return new PatchSet.Id(change.getId(), n);
-  }
-
-  CreateChangeEvent(Change change, Integer minPsNum) {
-    super(
-        psId(change, minPsNum),
-        change.getOwner(),
-        change.getOwner(),
-        change.getCreatedOn(),
-        change.getCreatedOn(),
-        null);
-    this.change = change;
-  }
-
-  @Override
-  boolean uniquePerUpdate() {
-    return true;
-  }
-
-  @Override
-  void apply(ChangeUpdate update) throws IOException, OrmException {
-    checkUpdate(update);
-    ChangeRebuilderImpl.createChange(update, change);
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/rebuild/DraftCommentEvent.java b/java/com/google/gerrit/server/notedb/rebuild/DraftCommentEvent.java
deleted file mode 100644
index 2a2795d..0000000
--- a/java/com/google/gerrit/server/notedb/rebuild/DraftCommentEvent.java
+++ /dev/null
@@ -1,81 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb.rebuild;
-
-import static com.google.gerrit.server.CommentsUtil.setCommentRevId;
-
-import com.google.common.base.MoreObjects.ToStringHelper;
-import com.google.common.flogger.FluentLogger;
-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.CommentsUtil;
-import com.google.gerrit.server.notedb.ChangeDraftUpdate;
-import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gerrit.server.patch.PatchListNotAvailableException;
-
-class DraftCommentEvent extends Event {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
-  public final Comment c;
-  private final Change change;
-  private final PatchSet ps;
-  private final PatchListCache cache;
-
-  DraftCommentEvent(Comment c, Change change, PatchSet ps, PatchListCache cache) {
-    super(
-        CommentsUtil.getCommentPsId(change.getId(), c),
-        c.author.getId(),
-        c.getRealAuthor().getId(),
-        c.writtenOn,
-        change.getCreatedOn(),
-        c.tag);
-    this.c = c;
-    this.change = change;
-    this.ps = ps;
-    this.cache = cache;
-  }
-
-  @Override
-  boolean uniquePerUpdate() {
-    return false;
-  }
-
-  @Override
-  void apply(ChangeUpdate update) {
-    throw new UnsupportedOperationException();
-  }
-
-  void applyDraft(ChangeDraftUpdate draftUpdate) {
-    if (c.revId == null) {
-      try {
-        setCommentRevId(c, cache, change, ps);
-      } catch (PatchListNotAvailableException e) {
-        logger.atWarning().log(
-            "Unable to determine parent commit of patch set %s (%s);"
-                + " omitting draft inline comment %s",
-            ps.getId(), ps.getRevision(), c);
-        return;
-      }
-    }
-    draftUpdate.putComment(c);
-  }
-
-  @Override
-  protected void addToString(ToStringHelper helper) {
-    helper.add("message", c.message);
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/rebuild/Event.java b/java/com/google/gerrit/server/notedb/rebuild/Event.java
deleted file mode 100644
index 3957c5c..0000000
--- a/java/com/google/gerrit/server/notedb/rebuild/Event.java
+++ /dev/null
@@ -1,146 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb.rebuild;
-
-import static com.google.common.base.Preconditions.checkState;
-import static com.google.gerrit.server.notedb.rebuild.ChangeRebuilderImpl.MAX_WINDOW_MS;
-
-import com.google.common.base.MoreObjects;
-import com.google.common.base.MoreObjects.ToStringHelper;
-import com.google.common.collect.ComparisonChain;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.server.ReviewDbUtil;
-import com.google.gerrit.server.notedb.AbstractChangeUpdate;
-import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gwtorm.server.OrmException;
-import java.io.IOException;
-import java.sql.Timestamp;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-
-abstract class Event implements Comparable<Event> {
-  // NOTE: EventList only supports direct subclasses, not an arbitrary
-  // hierarchy.
-
-  final Account.Id user;
-  final Account.Id realUser;
-  final String tag;
-  final boolean predatesChange;
-
-  /** Dependencies of this event; other events that must happen before this one. */
-  final List<Event> deps;
-
-  Timestamp when;
-  PatchSet.Id psId;
-
-  protected Event(
-      PatchSet.Id psId,
-      Account.Id effectiveUser,
-      Account.Id realUser,
-      Timestamp when,
-      Timestamp changeCreatedOn,
-      String tag) {
-    this.psId = psId;
-    this.user = effectiveUser;
-    this.realUser = realUser != null ? realUser : effectiveUser;
-    this.tag = tag;
-    // Truncate timestamps at the change's createdOn timestamp.
-    predatesChange = when.before(changeCreatedOn);
-    this.when = predatesChange ? changeCreatedOn : when;
-    deps = new ArrayList<>();
-  }
-
-  protected void checkUpdate(AbstractChangeUpdate update) {
-    checkState(
-        Objects.equals(update.getPatchSetId(), psId),
-        "cannot apply event for %s to update for %s",
-        update.getPatchSetId(),
-        psId);
-    checkState(
-        when.getTime() - update.getWhen().getTime() <= MAX_WINDOW_MS,
-        "event at %s outside update window starting at %s",
-        when,
-        update.getWhen());
-    checkState(
-        Objects.equals(update.getNullableAccountId(), user),
-        "cannot apply event by %s to update by %s",
-        user,
-        update.getNullableAccountId());
-  }
-
-  Event addDep(Event e) {
-    deps.add(e);
-    return this;
-  }
-
-  /**
-   * @return whether this event type must be unique per {@link ChangeUpdate}, i.e. there may be at
-   *     most one of this type.
-   */
-  abstract boolean uniquePerUpdate();
-
-  abstract void apply(ChangeUpdate update) throws OrmException, IOException;
-
-  protected boolean isPostSubmitApproval() {
-    return false;
-  }
-
-  protected boolean isSubmit() {
-    return false;
-  }
-
-  protected boolean canHaveTag() {
-    return false;
-  }
-
-  @Override
-  public String toString() {
-    ToStringHelper helper =
-        MoreObjects.toStringHelper(this)
-            .add("psId", psId)
-            .add("effectiveUser", user)
-            .add("realUser", realUser)
-            .add("when", when)
-            .add("tag", tag);
-    addToString(helper);
-    return helper.toString();
-  }
-
-  /** @param helper toString helper to add fields to */
-  protected void addToString(ToStringHelper helper) {}
-
-  @Override
-  public int compareTo(Event other) {
-    return ComparisonChain.start()
-        .compareFalseFirst(this.isFinalUpdates(), other.isFinalUpdates())
-        .compare(this.when, other.when)
-        .compareTrueFirst(isPatchSet(), isPatchSet())
-        .compareTrueFirst(this.predatesChange, other.predatesChange)
-        .compare(this.user, other.user, ReviewDbUtil.intKeyOrdering())
-        .compare(this.realUser, other.realUser, ReviewDbUtil.intKeyOrdering())
-        .compare(this.psId, other.psId, ReviewDbUtil.intKeyOrdering().nullsLast())
-        .result();
-  }
-
-  private boolean isPatchSet() {
-    return this instanceof PatchSetEvent;
-  }
-
-  private boolean isFinalUpdates() {
-    return this instanceof FinalUpdatesEvent;
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/rebuild/EventList.java b/java/com/google/gerrit/server/notedb/rebuild/EventList.java
deleted file mode 100644
index e83814d..0000000
--- a/java/com/google/gerrit/server/notedb/rebuild/EventList.java
+++ /dev/null
@@ -1,170 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb.rebuild;
-
-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.Lists;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import java.sql.Timestamp;
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.Objects;
-
-class EventList<E extends Event> implements Iterable<E> {
-  private final ArrayList<E> list = new ArrayList<>();
-  private boolean isSubmit;
-
-  @Override
-  public Iterator<E> iterator() {
-    return list.iterator();
-  }
-
-  void add(E e) {
-    list.add(e);
-    if (e.isSubmit()) {
-      isSubmit = true;
-    }
-  }
-
-  void clear() {
-    list.clear();
-    isSubmit = false;
-  }
-
-  boolean isEmpty() {
-    return list.isEmpty();
-  }
-
-  boolean canAdd(E e) {
-    if (isEmpty()) {
-      return true;
-    }
-    if (e instanceof FinalUpdatesEvent) {
-      return false; // FinalUpdatesEvent always gets its own update.
-    }
-
-    Event last = getLast();
-    if (!Objects.equals(e.user, last.user)
-        || !Objects.equals(e.realUser, last.realUser)
-        || !e.psId.equals(last.psId)) {
-      return false; // Different patch set or author.
-    }
-    if (e.canHaveTag() && canHaveTag() && !Objects.equals(e.tag, getTag())) {
-      // We should trust the tag field, and it doesn't match.
-      return false;
-    }
-    if (e.isPostSubmitApproval() && isSubmit) {
-      // Post-submit approvals must come after the update that submits.
-      return false;
-    }
-
-    long t = e.when.getTime();
-    long tFirst = getFirstTime();
-    long tLast = getLastTime();
-    checkArgument(t >= tLast, "event %s is before previous event in list %s", e, last);
-    if (t - tLast > ChangeRebuilderImpl.MAX_DELTA_MS
-        || t - tFirst > ChangeRebuilderImpl.MAX_WINDOW_MS) {
-      return false; // Too much time elapsed.
-    }
-
-    if (!e.uniquePerUpdate()) {
-      return true;
-    }
-    for (Event o : this) {
-      if (e.getClass() == o.getClass()) {
-        return false; // Only one event of this type allowed per update.
-      }
-    }
-
-    // TODO(dborowitz): Additional heuristics, like keeping events separate if
-    // they affect overlapping fields within a single entity.
-
-    return true;
-  }
-
-  Timestamp getWhen() {
-    return get(0).when;
-  }
-
-  PatchSet.Id getPatchSetId() {
-    PatchSet.Id id = requireNonNull(get(0).psId);
-    for (int i = 1; i < size(); i++) {
-      checkState(
-          get(i).psId.equals(id), "mismatched patch sets in EventList: %s != %s", id, get(i).psId);
-    }
-    return id;
-  }
-
-  Account.Id getAccountId() {
-    Account.Id id = get(0).user;
-    for (int i = 1; i < size(); i++) {
-      checkState(
-          Objects.equals(id, get(i).user),
-          "mismatched users in EventList: %s != %s",
-          id,
-          get(i).user);
-    }
-    return id;
-  }
-
-  Account.Id getRealAccountId() {
-    Account.Id id = get(0).realUser;
-    for (int i = 1; i < size(); i++) {
-      checkState(
-          Objects.equals(id, get(i).realUser),
-          "mismatched real users in EventList: %s != %s",
-          id,
-          get(i).realUser);
-    }
-    return id;
-  }
-
-  String getTag() {
-    for (E e : Lists.reverse(list)) {
-      if (e.tag != null) {
-        return e.tag;
-      }
-    }
-    return null;
-  }
-
-  private boolean canHaveTag() {
-    return list.stream().anyMatch(Event::canHaveTag);
-  }
-
-  private E get(int i) {
-    return list.get(i);
-  }
-
-  private int size() {
-    return list.size();
-  }
-
-  private E getLast() {
-    return list.get(list.size() - 1);
-  }
-
-  private long getLastTime() {
-    return getLast().when.getTime();
-  }
-
-  private long getFirstTime() {
-    return list.get(0).when.getTime();
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/rebuild/EventSorter.java b/java/com/google/gerrit/server/notedb/rebuild/EventSorter.java
deleted file mode 100644
index 077a027..0000000
--- a/java/com/google/gerrit/server/notedb/rebuild/EventSorter.java
+++ /dev/null
@@ -1,112 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb.rebuild;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkState;
-
-import com.google.common.collect.ListMultimap;
-import com.google.common.collect.MultimapBuilder;
-import com.google.common.collect.SetMultimap;
-import java.util.Collections;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.PriorityQueue;
-
-/**
- * Helper to sort a list of events.
- *
- * <p>Events are sorted in two passes:
- *
- * <ol>
- *   <li>Sort by natural order (timestamp, patch set, author, etc.)
- *   <li>Postpone any events with dependencies to occur only after all of their dependencies, where
- *       this violates natural order.
- * </ol>
- *
- * {@link #sort()} modifies the event list in place (similar to {@link Collections#sort(List)}), but
- * does not modify any event. In particular, events might end up out of order with respect to
- * timestamp; callers are responsible for adjusting timestamps later if they prefer monotonicity.
- */
-class EventSorter {
-  private final List<Event> out;
-  private final LinkedHashSet<Event> sorted;
-  private ListMultimap<Event, Event> waiting;
-  private SetMultimap<Event, Event> deps;
-
-  EventSorter(List<Event> events) {
-    LinkedHashSet<Event> all = new LinkedHashSet<>(events);
-    out = events;
-
-    for (Event e : events) {
-      for (Event d : e.deps) {
-        checkArgument(all.contains(d), "dep %s of %s not in input list", d, e);
-      }
-    }
-
-    all.clear();
-    sorted = all; // Presized.
-  }
-
-  void sort() {
-    // First pass: sort by natural order.
-    PriorityQueue<Event> todo = new PriorityQueue<>(out);
-
-    // Populate waiting map after initial sort to preserve natural order.
-    waiting = MultimapBuilder.hashKeys().arrayListValues().build();
-    deps = MultimapBuilder.hashKeys().hashSetValues().build();
-    for (Event e : todo) {
-      for (Event d : e.deps) {
-        deps.put(e, d);
-        waiting.put(d, e);
-      }
-    }
-
-    // Second pass: enforce dependencies.
-    int size = out.size();
-    while (!todo.isEmpty()) {
-      process(todo.remove(), todo);
-    }
-    checkState(
-        sorted.size() == size, "event sort expected %s elements, got %s", size, sorted.size());
-
-    // Modify out in-place a la Collections#sort.
-    out.clear();
-    out.addAll(sorted);
-  }
-
-  void process(Event e, PriorityQueue<Event> todo) {
-    if (sorted.contains(e)) {
-      return; // Already emitted.
-    }
-    if (!deps.get(e).isEmpty()) {
-      // Not all events that e depends on have been emitted yet. Ignore e for
-      // now; it will get added back to the queue in the block below once its
-      // last dependency is processed.
-      return;
-    }
-
-    // All events that e depends on have been emitted, so e can be emitted.
-    sorted.add(e);
-
-    // Remove e from the dependency set of all events waiting on e, and add
-    // those events back to the queue in the original priority order for
-    // reconsideration.
-    for (Event w : waiting.get(e)) {
-      deps.get(w).remove(e);
-      todo.add(w);
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/rebuild/FinalUpdatesEvent.java b/java/com/google/gerrit/server/notedb/rebuild/FinalUpdatesEvent.java
deleted file mode 100644
index 55d5a31..0000000
--- a/java/com/google/gerrit/server/notedb/rebuild/FinalUpdatesEvent.java
+++ /dev/null
@@ -1,101 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb.rebuild;
-
-import static com.google.gerrit.reviewdb.server.ReviewDbUtil.intKeyOrdering;
-
-import com.google.common.base.MoreObjects.ToStringHelper;
-import com.google.common.collect.ImmutableCollection;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gwtorm.server.OrmException;
-import java.util.Objects;
-
-class FinalUpdatesEvent extends Event {
-  private final Change change;
-  private final Change noteDbChange;
-  private final ImmutableCollection<PatchSet> patchSets;
-
-  FinalUpdatesEvent(Change change, Change noteDbChange, ImmutableCollection<PatchSet> patchSets) {
-    super(
-        change.currentPatchSetId(),
-        change.getOwner(),
-        change.getOwner(),
-        change.getLastUpdatedOn(),
-        change.getCreatedOn(),
-        null);
-    this.change = change;
-    this.noteDbChange = noteDbChange;
-    this.patchSets = patchSets;
-  }
-
-  @Override
-  boolean uniquePerUpdate() {
-    return true;
-  }
-
-  @SuppressWarnings("deprecation")
-  @Override
-  void apply(ChangeUpdate update) throws OrmException {
-    if (!Objects.equals(change.getTopic(), noteDbChange.getTopic())) {
-      update.setTopic(change.getTopic());
-    }
-    if (!statusMatches()) {
-      // TODO(dborowitz): Stamp approximate approvals at this time.
-      update.fixStatus(change.getStatus());
-    }
-    if (change.isPrivate() != noteDbChange.isPrivate()) {
-      update.setPrivate(change.isPrivate());
-    }
-    if (change.isWorkInProgress() != noteDbChange.isWorkInProgress()) {
-      update.setWorkInProgress(change.isWorkInProgress());
-    }
-    if (change.getSubmissionId() != null && noteDbChange.getSubmissionId() == null) {
-      update.setSubmissionId(change.getSubmissionId());
-    }
-    if (!Objects.equals(change.getAssignee(), noteDbChange.getAssignee())) {
-      // TODO(dborowitz): Parse intermediate values out from messages.
-      update.setAssignee(change.getAssignee());
-    }
-    if (!patchSets.isEmpty() && !highestNumberedPatchSetIsCurrent()) {
-      update.setCurrentPatchSet();
-    }
-    if (!update.isEmpty()) {
-      update.setSubjectForCommit("Final NoteDb migration updates");
-    }
-  }
-
-  private boolean statusMatches() {
-    return Objects.equals(change.getStatus(), noteDbChange.getStatus());
-  }
-
-  private boolean highestNumberedPatchSetIsCurrent() {
-    PatchSet.Id max = patchSets.stream().map(PatchSet::getId).max(intKeyOrdering()).get();
-    return max.equals(change.currentPatchSetId());
-  }
-
-  @Override
-  protected boolean isSubmit() {
-    return change.getStatus() == Change.Status.MERGED;
-  }
-
-  @Override
-  protected void addToString(ToStringHelper helper) {
-    if (!statusMatches()) {
-      helper.add("status", change.getStatus());
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/rebuild/GcAllUsers.java b/java/com/google/gerrit/server/notedb/rebuild/GcAllUsers.java
deleted file mode 100644
index f2a5cc6..0000000
--- a/java/com/google/gerrit/server/notedb/rebuild/GcAllUsers.java
+++ /dev/null
@@ -1,121 +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.rebuild;
-
-import static java.util.Objects.requireNonNull;
-import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_GC_SECTION;
-import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_AUTO;
-
-import com.google.common.base.Throwables;
-import com.google.common.collect.ImmutableList;
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.GarbageCollectionResult;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.git.GarbageCollection;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.LocalDiskRepositoryManager;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.util.function.Consumer;
-import org.eclipse.jgit.lib.Repository;
-
-@Singleton
-public class GcAllUsers {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
-  private final AllUsersName allUsers;
-  private final GarbageCollection.Factory gcFactory;
-  private final GitRepositoryManager repoManager;
-
-  @Inject
-  GcAllUsers(
-      AllUsersName allUsers,
-      GarbageCollection.Factory gcFactory,
-      GitRepositoryManager repoManager) {
-    this.allUsers = allUsers;
-    this.gcFactory = gcFactory;
-    this.repoManager = repoManager;
-  }
-
-  public void runWithLogger() {
-    // Print log messages using logger, and skip progress.
-    run(s -> logger.atInfo().log(s), null);
-  }
-
-  public void run(PrintWriter writer) {
-    // Print both log messages and progress to given writer.
-    run(requireNonNull(writer)::println, writer);
-  }
-
-  private void run(Consumer<String> logOneLine, @Nullable PrintWriter progressWriter) {
-    if (!(repoManager instanceof LocalDiskRepositoryManager)) {
-      logOneLine.accept("Skipping GC of " + allUsers + "; not a local disk repo");
-      return;
-    }
-    if (!enableAutoGc(logOneLine)) {
-      logOneLine.accept(
-          "Skipping GC of "
-              + allUsers
-              + " due to disabling "
-              + CONFIG_GC_SECTION
-              + "."
-              + CONFIG_KEY_AUTO);
-      logOneLine.accept(
-          "If loading accounts is slow after the NoteDb migration, run `git gc` on "
-              + allUsers
-              + " manually");
-      return;
-    }
-
-    if (progressWriter == null) {
-      // Mimic log line from GarbageCollection.
-      logOneLine.accept("collecting garbage for \"" + allUsers + "\":\n");
-    }
-    GarbageCollectionResult result =
-        gcFactory.create().run(ImmutableList.of(allUsers), progressWriter);
-    if (!result.hasErrors()) {
-      return;
-    }
-    for (GarbageCollectionResult.Error e : result.getErrors()) {
-      switch (e.getType()) {
-        case GC_ALREADY_SCHEDULED:
-          logOneLine.accept("GC already scheduled for " + e.getProjectName());
-          break;
-        case GC_FAILED:
-          logOneLine.accept("GC failed for " + e.getProjectName());
-          break;
-        case REPOSITORY_NOT_FOUND:
-          logOneLine.accept(e.getProjectName() + " repo not found");
-          break;
-        default:
-          logOneLine.accept("GC failed for " + e.getProjectName() + ": " + e.getType());
-          break;
-      }
-    }
-  }
-
-  private boolean enableAutoGc(Consumer<String> logOneLine) {
-    try (Repository repo = repoManager.openRepository(allUsers)) {
-      return repo.getConfig().getInt(CONFIG_GC_SECTION, CONFIG_KEY_AUTO, -1) != 0;
-    } catch (IOException e) {
-      logOneLine.accept(
-          "Error reading config for " + allUsers + ":\n" + Throwables.getStackTraceAsString(e));
-      return false;
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/rebuild/HashtagsEvent.java b/java/com/google/gerrit/server/notedb/rebuild/HashtagsEvent.java
deleted file mode 100644
index 4f6f6ad..0000000
--- a/java/com/google/gerrit/server/notedb/rebuild/HashtagsEvent.java
+++ /dev/null
@@ -1,62 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb.rebuild;
-
-import com.google.common.base.MoreObjects.ToStringHelper;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gwtorm.server.OrmException;
-import java.sql.Timestamp;
-import java.util.Set;
-
-class HashtagsEvent extends Event {
-  private final Set<String> hashtags;
-
-  HashtagsEvent(
-      PatchSet.Id psId,
-      Account.Id who,
-      Timestamp when,
-      Set<String> hashtags,
-      Timestamp changeCreatdOn) {
-    super(
-        psId,
-        who,
-        who,
-        when,
-        changeCreatdOn,
-        // Somewhat confusingly, hashtags do not use the setTag method on
-        // AbstractChangeUpdate, so pass null as the tag.
-        null);
-    this.hashtags = hashtags;
-  }
-
-  @Override
-  boolean uniquePerUpdate() {
-    // Since these are produced from existing commits in the old NoteDb graph,
-    // we know that there must be one per commit in the rebuilt graph.
-    return true;
-  }
-
-  @Override
-  void apply(ChangeUpdate update) throws OrmException {
-    update.setHashtags(hashtags);
-  }
-
-  @Override
-  protected void addToString(ToStringHelper helper) {
-    helper.add("hashtags", hashtags);
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/rebuild/MigrationException.java b/java/com/google/gerrit/server/notedb/rebuild/MigrationException.java
deleted file mode 100644
index 0cf78be..0000000
--- a/java/com/google/gerrit/server/notedb/rebuild/MigrationException.java
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb.rebuild;
-
-import java.io.IOException;
-
-/** Exception thrown by {@link NoteDbMigrator} when migration fails. */
-public class MigrationException extends IOException {
-  private static final long serialVersionUID = 1L;
-
-  MigrationException(String message) {
-    super(message);
-  }
-
-  MigrationException(String message, Throwable why) {
-    super(message, why);
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java b/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java
deleted file mode 100644
index ebec9c5..0000000
--- a/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java
+++ /dev/null
@@ -1,1066 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb.rebuild;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkState;
-import static com.google.gerrit.reviewdb.server.ReviewDbUtil.unwrapDb;
-import static com.google.gerrit.server.notedb.NotesMigration.SECTION_NOTE_DB;
-import static com.google.gerrit.server.notedb.NotesMigrationState.NOTE_DB;
-import static com.google.gerrit.server.notedb.NotesMigrationState.READ_WRITE_NO_SEQUENCE;
-import static com.google.gerrit.server.notedb.NotesMigrationState.READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY;
-import static com.google.gerrit.server.notedb.NotesMigrationState.READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY;
-import static com.google.gerrit.server.notedb.NotesMigrationState.WRITE;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.Comparator.comparing;
-import static java.util.Objects.requireNonNull;
-import static java.util.stream.Collectors.joining;
-import static java.util.stream.Collectors.toList;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Stopwatch;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.MultimapBuilder;
-import com.google.common.collect.Ordering;
-import com.google.common.collect.SetMultimap;
-import com.google.common.collect.Streams;
-import com.google.common.flogger.FluentLogger;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.ListeningExecutorService;
-import com.google.common.util.concurrent.MoreExecutors;
-import com.google.gerrit.common.FormatUtil;
-import com.google.gerrit.common.Nullable;
-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.server.ReviewDb;
-import com.google.gerrit.reviewdb.server.ReviewDbWrapper;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.InternalUser;
-import com.google.gerrit.server.Sequences;
-import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.config.GerritServerConfigProvider;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.LockFailureException;
-import com.google.gerrit.server.git.WorkQueue;
-import com.google.gerrit.server.notedb.ChangeBundleReader;
-import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.notedb.MutableNotesMigration;
-import com.google.gerrit.server.notedb.NoteDbTable;
-import com.google.gerrit.server.notedb.NoteDbUpdateManager;
-import com.google.gerrit.server.notedb.NotesMigrationState;
-import com.google.gerrit.server.notedb.PrimaryStorageMigrator;
-import com.google.gerrit.server.notedb.PrimaryStorageMigrator.NoNoteDbStateException;
-import com.google.gerrit.server.notedb.RepoSequence;
-import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder.NoPatchSetsException;
-import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.update.ChainedReceiveCommands;
-import com.google.gerrit.server.update.RefUpdateUtil;
-import com.google.gerrit.server.util.ManualRequestContext;
-import com.google.gerrit.server.util.ThreadLocalRequestContext;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.io.BufferedWriter;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.Optional;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.function.Consumer;
-import java.util.function.Predicate;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.internal.storage.file.FileRepository;
-import org.eclipse.jgit.internal.storage.file.PackInserter;
-import org.eclipse.jgit.lib.BatchRefUpdate;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.ProgressMonitor;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.lib.TextProgressMonitor;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.transport.ReceiveCommand;
-import org.eclipse.jgit.util.FS;
-
-/** One stop shop for migrating a site's change storage from ReviewDb to NoteDb. */
-public class NoteDbMigrator implements AutoCloseable {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
-  private static final String AUTO_MIGRATE = "autoMigrate";
-  private static final String TRIAL = "trial";
-
-  public static boolean getAutoMigrate(Config cfg) {
-    return cfg.getBoolean(SECTION_NOTE_DB, NoteDbTable.CHANGES.key(), AUTO_MIGRATE, false);
-  }
-
-  private static void setAutoMigrate(Config cfg, boolean autoMigrate) {
-    cfg.setBoolean(SECTION_NOTE_DB, NoteDbTable.CHANGES.key(), AUTO_MIGRATE, autoMigrate);
-  }
-
-  public static boolean getTrialMode(Config cfg) {
-    return cfg.getBoolean(SECTION_NOTE_DB, NoteDbTable.CHANGES.key(), TRIAL, false);
-  }
-
-  public static void setTrialMode(Config cfg, boolean trial) {
-    cfg.setBoolean(SECTION_NOTE_DB, NoteDbTable.CHANGES.key(), TRIAL, trial);
-  }
-
-  private static class NoteDbMigrationLoggerOut extends OutputStream {
-    private StringBuilder outputBuffer = new StringBuilder();
-
-    @Override
-    public synchronized void write(int b) throws IOException {
-      if (b == '\r' || b == '\n') {
-        logger.atInfo().log(outputBuffer.toString());
-        outputBuffer = new StringBuilder();
-      } else {
-        outputBuffer.append(Character.toChars(b));
-      }
-    }
-  }
-
-  public static class Builder {
-    private final Config cfg;
-    private final SitePaths sitePaths;
-    private final Provider<PersonIdent> serverIdent;
-    private final AllUsersName allUsers;
-    private final SchemaFactory<ReviewDb> schemaFactory;
-    private final GitRepositoryManager repoManager;
-    private final NoteDbUpdateManager.Factory updateManagerFactory;
-    private final ChangeBundleReader bundleReader;
-    private final AllProjectsName allProjects;
-    private final InternalUser.Factory userFactory;
-    private final ThreadLocalRequestContext requestContext;
-    private final ChangeRebuilderImpl rebuilder;
-    private final WorkQueue workQueue;
-    private final MutableNotesMigration globalNotesMigration;
-    private final PrimaryStorageMigrator primaryStorageMigrator;
-    private final DynamicSet<NotesMigrationStateListener> listeners;
-
-    private int threads;
-    private ImmutableList<Project.NameKey> projects = ImmutableList.of();
-    private ImmutableList<Project.NameKey> skipProjects = ImmutableList.of();
-    private ImmutableList<Change.Id> changes = ImmutableList.of();
-    private OutputStream progressOut = new NoteDbMigrationLoggerOut();
-    private NotesMigrationState stopAtState;
-    private boolean trial;
-    private boolean forceRebuild;
-    private int sequenceGap = -1;
-    private boolean autoMigrate;
-
-    @Inject
-    Builder(
-        GerritServerConfigProvider configProvider,
-        SitePaths sitePaths,
-        @GerritPersonIdent Provider<PersonIdent> serverIdent,
-        AllUsersName allUsers,
-        SchemaFactory<ReviewDb> schemaFactory,
-        GitRepositoryManager repoManager,
-        NoteDbUpdateManager.Factory updateManagerFactory,
-        ChangeBundleReader bundleReader,
-        AllProjectsName allProjects,
-        ThreadLocalRequestContext requestContext,
-        InternalUser.Factory userFactory,
-        ChangeRebuilderImpl rebuilder,
-        WorkQueue workQueue,
-        MutableNotesMigration globalNotesMigration,
-        PrimaryStorageMigrator primaryStorageMigrator,
-        DynamicSet<NotesMigrationStateListener> listeners) {
-      // Reload gerrit.config/notedb.config on each migrator invocation, in case a previous
-      // migration in the same process modified the on-disk contents. This ensures the defaults for
-      // trial/autoMigrate get set correctly below.
-      this.cfg = configProvider.loadConfig();
-      this.sitePaths = sitePaths;
-      this.serverIdent = serverIdent;
-      this.allUsers = allUsers;
-      this.schemaFactory = schemaFactory;
-      this.repoManager = repoManager;
-      this.updateManagerFactory = updateManagerFactory;
-      this.bundleReader = bundleReader;
-      this.allProjects = allProjects;
-      this.requestContext = requestContext;
-      this.userFactory = userFactory;
-      this.rebuilder = rebuilder;
-      this.workQueue = workQueue;
-      this.globalNotesMigration = globalNotesMigration;
-      this.primaryStorageMigrator = primaryStorageMigrator;
-      this.listeners = listeners;
-      this.trial = getTrialMode(cfg);
-      this.autoMigrate = getAutoMigrate(cfg);
-    }
-
-    /**
-     * Set the number of threads used by parallelizable phases of the migration, such as rebuilding
-     * all changes.
-     *
-     * <p>Not all phases are parallelizable, and calling {@link #rebuild()} directly will do
-     * substantial work in the calling thread regardless of the number of threads configured.
-     *
-     * <p>By default, all work is done in the calling thread.
-     *
-     * @param threads thread count; if less than 2, all work happens in the calling thread.
-     * @return this.
-     */
-    public Builder setThreads(int threads) {
-      this.threads = threads;
-      return this;
-    }
-
-    /**
-     * Limit the set of projects that are processed.
-     *
-     * <p>Incompatible with {@link #setChanges(Collection)}.
-     *
-     * <p>By default, all projects will be processed.
-     *
-     * @param projects set of projects; if null or empty, all projects will be processed.
-     * @return this.
-     */
-    public Builder setProjects(@Nullable Collection<Project.NameKey> projects) {
-      this.projects = projects != null ? ImmutableList.copyOf(projects) : ImmutableList.of();
-      return this;
-    }
-
-    /**
-     * Process all projects except these
-     *
-     * <p>Incompatible with {@link #setProjects(Collection)} and {@link #setChanges(Collection)}
-     *
-     * <p>By default, all projects will be processed.
-     *
-     * @param skipProjects set of projects; if null or empty all project will be processed
-     * @return this.
-     */
-    public Builder setSkipProjects(@Nullable Collection<Project.NameKey> skipProjects) {
-      this.skipProjects =
-          skipProjects != null ? ImmutableList.copyOf(skipProjects) : ImmutableList.of();
-      return this;
-    }
-
-    /**
-     * Limit the set of changes that are processed.
-     *
-     * <p>Incompatible with {@link #setProjects(Collection)}.
-     *
-     * <p>By default, all changes will be processed.
-     *
-     * @param changes set of changes; if null or empty, all changes will be processed.
-     * @return this.
-     */
-    public Builder setChanges(@Nullable Collection<Change.Id> changes) {
-      this.changes = changes != null ? ImmutableList.copyOf(changes) : ImmutableList.of();
-      return this;
-    }
-
-    /**
-     * Set output stream for progress monitors.
-     *
-     * <p>By default, there is no progress monitor output (although there may be other logs).
-     *
-     * @param progressOut output stream.
-     * @return this.
-     */
-    public Builder setProgressOut(OutputStream progressOut) {
-      this.progressOut = requireNonNull(progressOut);
-      return this;
-    }
-
-    /**
-     * Stop at a specific migration state, for testing only.
-     *
-     * @param stopAtState state to stop at.
-     * @return this.
-     */
-    @VisibleForTesting
-    public Builder setStopAtStateForTesting(NotesMigrationState stopAtState) {
-      this.stopAtState = stopAtState;
-      return this;
-    }
-
-    /**
-     * Rebuild in "trial mode": configure Gerrit to write to and read from NoteDb, but leave
-     * ReviewDb as the source of truth for all changes.
-     *
-     * <p>By default, trial mode is off, and NoteDb is the source of truth for all changes following
-     * the migration.
-     *
-     * @param trial whether to rebuild in trial mode.
-     * @return this.
-     */
-    public Builder setTrialMode(boolean trial) {
-      this.trial = trial;
-      return this;
-    }
-
-    /**
-     * Rebuild all changes in NoteDb from ReviewDb, even if Gerrit is currently configured to read
-     * from NoteDb.
-     *
-     * <p>Only supported if ReviewDb is still the source of truth for all changes.
-     *
-     * <p>By default, force rebuilding is off.
-     *
-     * @param forceRebuild whether to force rebuilding.
-     * @return this.
-     */
-    public Builder setForceRebuild(boolean forceRebuild) {
-      this.forceRebuild = forceRebuild;
-      return this;
-    }
-
-    /**
-     * Gap between ReviewDb change sequence numbers and NoteDb.
-     *
-     * <p>If NoteDb sequences are enabled in a running server, there is a race between the migration
-     * step that calls {@code nextChangeId()} to seed the ref, and other threads that call {@code
-     * nextChangeId()} to create new changes. In order to prevent these operations stepping on one
-     * another, we use this value to skip some predefined sequence numbers. This is strongly
-     * recommended in a running server.
-     *
-     * <p>If the migration takes place offline, there is no race with other threads, and this option
-     * may be set to 0. However, admins may still choose to use a gap, for example to make it easier
-     * to distinguish changes that were created before and after the NoteDb migration.
-     *
-     * <p>By default, uses the value from {@code noteDb.changes.initialSequenceGap} in {@code
-     * gerrit.config}, which defaults to 1000.
-     *
-     * @param sequenceGap sequence gap size; if negative, use the default.
-     * @return this.
-     */
-    public Builder setSequenceGap(int sequenceGap) {
-      this.sequenceGap = sequenceGap;
-      return this;
-    }
-
-    /**
-     * Enable auto-migration on subsequent daemon launches.
-     *
-     * <p>If true, prior to running any migration steps, sets the necessary configuration in {@code
-     * gerrit.config} to make {@code gerrit.war daemon} retry the migration on next startup, if it
-     * fails.
-     *
-     * @param autoMigrate whether to set auto-migration config.
-     * @return this.
-     */
-    public Builder setAutoMigrate(boolean autoMigrate) {
-      this.autoMigrate = autoMigrate;
-      return this;
-    }
-
-    public NoteDbMigrator build() throws MigrationException {
-      return new NoteDbMigrator(
-          sitePaths,
-          schemaFactory,
-          serverIdent,
-          allUsers,
-          repoManager,
-          updateManagerFactory,
-          bundleReader,
-          allProjects,
-          requestContext,
-          userFactory,
-          rebuilder,
-          globalNotesMigration,
-          primaryStorageMigrator,
-          listeners,
-          threads > 1
-              ? MoreExecutors.listeningDecorator(
-                  workQueue.createQueue(threads, "RebuildChange", true))
-              : MoreExecutors.newDirectExecutorService(),
-          projects,
-          skipProjects,
-          changes,
-          progressOut,
-          stopAtState,
-          trial,
-          forceRebuild,
-          sequenceGap >= 0 ? sequenceGap : Sequences.getChangeSequenceGap(cfg),
-          autoMigrate);
-    }
-  }
-
-  private final FileBasedConfig gerritConfig;
-  private final FileBasedConfig noteDbConfig;
-  private final SchemaFactory<ReviewDb> schemaFactory;
-  private final Provider<PersonIdent> serverIdent;
-  private final AllUsersName allUsers;
-  private final GitRepositoryManager repoManager;
-  private final NoteDbUpdateManager.Factory updateManagerFactory;
-  private final ChangeBundleReader bundleReader;
-  private final AllProjectsName allProjects;
-  private final ThreadLocalRequestContext requestContext;
-  private final InternalUser.Factory userFactory;
-  private final ChangeRebuilderImpl rebuilder;
-  private final MutableNotesMigration globalNotesMigration;
-  private final PrimaryStorageMigrator primaryStorageMigrator;
-  private final DynamicSet<NotesMigrationStateListener> listeners;
-
-  private final ListeningExecutorService executor;
-  private final ImmutableList<Project.NameKey> projects;
-  private final ImmutableList<Project.NameKey> skipProjects;
-  private final ImmutableList<Change.Id> changes;
-  private final OutputStream progressOut;
-  private final NotesMigrationState stopAtState;
-  private final boolean trial;
-  private final boolean forceRebuild;
-  private final int sequenceGap;
-  private final boolean autoMigrate;
-
-  private NoteDbMigrator(
-      SitePaths sitePaths,
-      SchemaFactory<ReviewDb> schemaFactory,
-      Provider<PersonIdent> serverIdent,
-      AllUsersName allUsers,
-      GitRepositoryManager repoManager,
-      NoteDbUpdateManager.Factory updateManagerFactory,
-      ChangeBundleReader bundleReader,
-      AllProjectsName allProjects,
-      ThreadLocalRequestContext requestContext,
-      InternalUser.Factory userFactory,
-      ChangeRebuilderImpl rebuilder,
-      MutableNotesMigration globalNotesMigration,
-      PrimaryStorageMigrator primaryStorageMigrator,
-      DynamicSet<NotesMigrationStateListener> listeners,
-      ListeningExecutorService executor,
-      ImmutableList<Project.NameKey> projects,
-      ImmutableList<Project.NameKey> skipProjects,
-      ImmutableList<Change.Id> changes,
-      OutputStream progressOut,
-      NotesMigrationState stopAtState,
-      boolean trial,
-      boolean forceRebuild,
-      int sequenceGap,
-      boolean autoMigrate)
-      throws MigrationException {
-    if (ImmutableList.of(!changes.isEmpty(), !projects.isEmpty(), !skipProjects.isEmpty()).stream()
-            .filter(e -> e)
-            .count()
-        > 1) {
-      throw new MigrationException("Cannot combine changes, projects and skipProjects");
-    }
-    if (sequenceGap < 0) {
-      throw new MigrationException("Sequence gap must be non-negative: " + sequenceGap);
-    }
-
-    this.schemaFactory = schemaFactory;
-    this.serverIdent = serverIdent;
-    this.allUsers = allUsers;
-    this.rebuilder = rebuilder;
-    this.repoManager = repoManager;
-    this.updateManagerFactory = updateManagerFactory;
-    this.bundleReader = bundleReader;
-    this.allProjects = allProjects;
-    this.requestContext = requestContext;
-    this.userFactory = userFactory;
-    this.globalNotesMigration = globalNotesMigration;
-    this.primaryStorageMigrator = primaryStorageMigrator;
-    this.listeners = listeners;
-    this.executor = executor;
-    this.projects = projects;
-    this.skipProjects = skipProjects;
-    this.changes = changes;
-    this.progressOut = progressOut;
-    this.stopAtState = stopAtState;
-    this.trial = trial;
-    this.forceRebuild = forceRebuild;
-    this.sequenceGap = sequenceGap;
-    this.autoMigrate = autoMigrate;
-
-    // Stack notedb.config over gerrit.config, in the same way as GerritServerConfigProvider.
-    this.gerritConfig = new FileBasedConfig(sitePaths.gerrit_config.toFile(), FS.detect());
-    this.noteDbConfig =
-        new FileBasedConfig(gerritConfig, sitePaths.notedb_config.toFile(), FS.detect());
-  }
-
-  @Override
-  public void close() {
-    executor.shutdownNow();
-  }
-
-  public void migrate() throws OrmException, IOException {
-    if (!changes.isEmpty() || !projects.isEmpty() || !skipProjects.isEmpty()) {
-      throw new MigrationException(
-          "Cannot set changes or projects or skipProjects during full migration; call rebuild() instead");
-    }
-    Optional<NotesMigrationState> maybeState = loadState();
-    if (!maybeState.isPresent()) {
-      throw new MigrationException("Could not determine initial migration state");
-    }
-
-    NotesMigrationState state = maybeState.get();
-    if (trial && state.compareTo(READ_WRITE_NO_SEQUENCE) > 0) {
-      throw new MigrationException(
-          "Migration has already progressed past the endpoint of the \"trial mode\" state;"
-              + " NoteDb is already the primary storage for some changes");
-    }
-    if (forceRebuild && state.compareTo(READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY) > 0) {
-      throw new MigrationException(
-          "Cannot force rebuild changes; NoteDb is already the primary storage for some changes");
-    }
-    setControlFlags();
-
-    boolean rebuilt = false;
-    while (state.compareTo(NOTE_DB) < 0) {
-      if (state.equals(stopAtState)) {
-        return;
-      }
-      boolean stillNeedsRebuild = forceRebuild && !rebuilt;
-      if (trial && state.compareTo(READ_WRITE_NO_SEQUENCE) >= 0) {
-        if (stillNeedsRebuild && state == READ_WRITE_NO_SEQUENCE) {
-          // We're at the end state of trial mode, but still need a rebuild due to forceRebuild. Let
-          // the loop go one more time.
-        } else {
-          return;
-        }
-      }
-      switch (state) {
-        case REVIEW_DB:
-          state = turnOnWrites(state);
-          break;
-        case WRITE:
-          state = rebuildAndEnableReads(state);
-          rebuilt = true;
-          break;
-        case READ_WRITE_NO_SEQUENCE:
-          if (stillNeedsRebuild) {
-            state = rebuildAndEnableReads(state);
-            rebuilt = true;
-          } else {
-            state = enableSequences(state);
-          }
-          break;
-        case READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY:
-          if (stillNeedsRebuild) {
-            state = rebuildAndEnableReads(state);
-            rebuilt = true;
-          } else {
-            state = setNoteDbPrimary(state);
-          }
-          break;
-        case READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY:
-          // The only way we can get here is if there was a failure on a previous run of
-          // setNoteDbPrimary, since that method moves to NOTE_DB if it completes
-          // successfully. Assume that not all changes were converted and re-run the step.
-          // migrateToNoteDbPrimary is a relatively fast no-op for already-migrated changes, so this
-          // isn't actually repeating work.
-          state = setNoteDbPrimary(state);
-          break;
-        case NOTE_DB:
-          // Done!
-          break;
-        default:
-          throw new MigrationException(
-              "Migration out of the following state is not supported:\n" + state.toText());
-      }
-    }
-  }
-
-  private NotesMigrationState turnOnWrites(NotesMigrationState prev) throws IOException {
-    return saveState(prev, WRITE);
-  }
-
-  private NotesMigrationState rebuildAndEnableReads(NotesMigrationState prev)
-      throws OrmException, IOException {
-    rebuild();
-    return saveState(prev, READ_WRITE_NO_SEQUENCE);
-  }
-
-  private NotesMigrationState enableSequences(NotesMigrationState prev)
-      throws OrmException, IOException {
-    try (ReviewDb db = schemaFactory.open()) {
-      @SuppressWarnings("deprecation")
-      final int nextChangeId = db.nextChangeId();
-
-      RepoSequence seq =
-          new RepoSequence(
-              repoManager,
-              GitReferenceUpdated.DISABLED,
-              allProjects,
-              Sequences.NAME_CHANGES,
-              // If sequenceGap is 0, this writes into the sequence ref the same ID that is returned
-              // by the call to seq.next() below. If we actually used this as a change ID, that
-              // would be a problem, but we just discard it, so this is safe.
-              () -> nextChangeId + sequenceGap - 1,
-              1,
-              nextChangeId);
-      seq.next();
-    }
-    return saveState(prev, READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY);
-  }
-
-  private NotesMigrationState setNoteDbPrimary(NotesMigrationState prev)
-      throws MigrationException, OrmException, IOException {
-    checkState(
-        projects.isEmpty() && changes.isEmpty() && skipProjects.isEmpty(),
-        "Should not have attempted setNoteDbPrimary with a subset of changes");
-    checkState(
-        prev == READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY
-            || prev == READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY,
-        "Unexpected start state for setNoteDbPrimary: %s",
-        prev);
-
-    // Before changing the primary storage of old changes, ensure new changes are created with
-    // NoteDb primary.
-    prev = saveState(prev, READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY);
-
-    Stopwatch sw = Stopwatch.createStarted();
-    logger.atInfo().log("Setting primary storage to NoteDb");
-    List<Change.Id> allChanges;
-    try (ReviewDb db = unwrapDb(schemaFactory.open())) {
-      allChanges = Streams.stream(db.changes().all()).map(Change::getId).collect(toList());
-    }
-
-    try (ContextHelper contextHelper = new ContextHelper()) {
-      List<ListenableFuture<Boolean>> futures =
-          allChanges.stream()
-              .map(
-                  id ->
-                      executor.submit(
-                          () -> {
-                            try (ManualRequestContext ctx = contextHelper.open()) {
-                              try {
-                                primaryStorageMigrator.migrateToNoteDbPrimary(id);
-                              } catch (NoNoteDbStateException e) {
-                                if (canSkipPrimaryStorageMigration(
-                                    ctx.getReviewDbProvider().get(), id)) {
-                                  logger.atWarning().withCause(e).log(
-                                      "Change %s previously failed to rebuild;"
-                                          + " skipping primary storage migration",
-                                      id);
-                                } else {
-                                  throw e;
-                                }
-                              }
-                              return true;
-                            } catch (Exception e) {
-                              logger.atSevere().withCause(e).log(
-                                  "Error migrating primary storage for %s", id);
-                              return false;
-                            }
-                          }))
-              .collect(toList());
-
-      boolean ok = futuresToBoolean(futures, "Error migrating primary storage");
-      double t = sw.elapsed(TimeUnit.MILLISECONDS) / 1000d;
-      logger.atInfo().log(
-          "Migrated primary storage of %d changes in %.01fs (%.01f/s)\n",
-          allChanges.size(), t, allChanges.size() / t);
-      if (!ok) {
-        throw new MigrationException("Migrating primary storage for some changes failed, see log");
-      }
-    }
-
-    return disableReviewDb(prev);
-  }
-
-  /**
-   * Checks whether a change is so corrupt that it can be completely skipped by the primary storage
-   * migration step.
-   *
-   * <p>To get to the point where this method is called from {@link #setNoteDbPrimary}, it means we
-   * attempted to rebuild it, and encountered an error that was then caught in {@link
-   * #rebuildProject} and skipped. As a result, there is no {@code noteDbState} field in the change
-   * by the time we get to {@link #setNoteDbPrimary}, so {@code migrateToNoteDbPrimary} throws an
-   * exception.
-   *
-   * <p>We have to do this hacky double-checking because we don't have a way for the rebuilding
-   * phase to communicate to the primary storage migration phase that the change is skippable. It
-   * would be possible to store this info in some field in this class, but there is no guarantee
-   * that the rebuild and primary storage migration phases are run in the same JVM invocation.
-   *
-   * <p>In an ideal world, we could do this through the {@link
-   * com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage} enum, having a separate value
-   * for errors. However, that would be an invasive change touching many non-migration-related parts
-   * of the NoteDb migration code, which is too risky to attempt in the stable branch where this bug
-   * had to be fixed.
-   *
-   * <p>As of this writing, the only case where this happens is when a change has no patch sets.
-   */
-  private static boolean canSkipPrimaryStorageMigration(ReviewDb db, Change.Id id) {
-    try {
-      return Iterables.isEmpty(unwrapDb(db).patchSets().byChange(id));
-    } catch (Exception e) {
-      logger.atSevere().withCause(e).log(
-          "Error checking if change %s can be skipped, assuming no", id);
-      return false;
-    }
-  }
-
-  private NotesMigrationState disableReviewDb(NotesMigrationState prev) throws IOException {
-    return saveState(prev, NOTE_DB, c -> setAutoMigrate(c, false));
-  }
-
-  private Optional<NotesMigrationState> loadState() throws IOException {
-    try {
-      gerritConfig.load();
-      noteDbConfig.load();
-      return NotesMigrationState.forConfig(noteDbConfig);
-    } catch (ConfigInvalidException | IllegalArgumentException e) {
-      logger.atWarning().withCause(e).log(
-          "error reading NoteDb migration options from %s", noteDbConfig.getFile());
-      return Optional.empty();
-    }
-  }
-
-  private NotesMigrationState saveState(
-      NotesMigrationState expectedOldState, NotesMigrationState newState) throws IOException {
-    return saveState(expectedOldState, newState, c -> {});
-  }
-
-  private NotesMigrationState saveState(
-      NotesMigrationState expectedOldState,
-      NotesMigrationState newState,
-      Consumer<Config> additionalUpdates)
-      throws IOException {
-    synchronized (globalNotesMigration) {
-      // This read-modify-write is racy. We're counting on the fact that no other Gerrit operation
-      // modifies gerrit.config, and hoping that admins don't either.
-      Optional<NotesMigrationState> actualOldState = loadState();
-      if (!actualOldState.equals(Optional.of(expectedOldState))) {
-        throw new MigrationException(
-            "Cannot move to new state:\n"
-                + newState.toText()
-                + "\n\n"
-                + "Expected this state in gerrit.config:\n"
-                + expectedOldState.toText()
-                + "\n\n"
-                + (actualOldState.isPresent()
-                    ? "But found this state:\n" + actualOldState.get().toText()
-                    : "But could not parse the current state"));
-      }
-
-      preStateChange(expectedOldState, newState);
-
-      newState.setConfigValues(noteDbConfig);
-      additionalUpdates.accept(noteDbConfig);
-      noteDbConfig.save();
-
-      // Only set in-memory state once it's been persisted to storage.
-      globalNotesMigration.setFrom(newState);
-      logger.atInfo().log("Migration state: %s => %s", expectedOldState, newState);
-
-      return newState;
-    }
-  }
-
-  private void preStateChange(NotesMigrationState oldState, NotesMigrationState newState)
-      throws IOException {
-    for (NotesMigrationStateListener listener : listeners) {
-      listener.preStateChange(oldState, newState);
-    }
-  }
-
-  private void setControlFlags() throws MigrationException {
-    synchronized (globalNotesMigration) {
-      try {
-        noteDbConfig.load();
-        setAutoMigrate(noteDbConfig, autoMigrate);
-        setTrialMode(noteDbConfig, trial);
-        noteDbConfig.save();
-      } catch (ConfigInvalidException | IOException e) {
-        throw new MigrationException("Error saving auto-migration config", e);
-      }
-    }
-  }
-
-  public void rebuild() throws MigrationException, OrmException {
-    if (!globalNotesMigration.commitChangeWrites()) {
-      throw new MigrationException("Cannot rebuild without noteDb.changes.write=true");
-    }
-    Stopwatch sw = Stopwatch.createStarted();
-    logger.atInfo().log("Rebuilding changes in NoteDb");
-
-    ImmutableListMultimap<Project.NameKey, Change.Id> changesByProject = getChangesByProject();
-    List<ListenableFuture<Boolean>> futures = new ArrayList<>();
-    try (ContextHelper contextHelper = new ContextHelper()) {
-      List<Project.NameKey> projectNames =
-          Ordering.usingToString().sortedCopy(changesByProject.keySet());
-      for (Project.NameKey project : projectNames) {
-        ListenableFuture<Boolean> future =
-            executor.submit(
-                () -> {
-                  try {
-                    return rebuildProject(contextHelper.getReviewDb(), changesByProject, project);
-                  } catch (Exception e) {
-                    logger.atSevere().withCause(e).log("Error rebuilding project %s", project);
-                    return false;
-                  }
-                });
-        futures.add(future);
-      }
-
-      boolean ok = futuresToBoolean(futures, "Error rebuilding projects");
-      double t = sw.elapsed(TimeUnit.MILLISECONDS) / 1000d;
-      logger.atInfo().log(
-          "Rebuilt %d changes in %.01fs (%.01f/s)\n",
-          changesByProject.size(), t, changesByProject.size() / t);
-      if (!ok) {
-        throw new MigrationException("Rebuilding some changes failed, see log");
-      }
-    }
-  }
-
-  private ImmutableListMultimap<Project.NameKey, Change.Id> getChangesByProject()
-      throws OrmException {
-    // Memoize all changes so we can close the db connection and allow other threads to use the full
-    // connection pool.
-    SetMultimap<Project.NameKey, Change.Id> out =
-        MultimapBuilder.treeKeys(comparing(Project.NameKey::get))
-            .treeSetValues(comparing(Change.Id::get))
-            .build();
-    try (ReviewDb db = unwrapDb(schemaFactory.open())) {
-      if (!projects.isEmpty()) {
-        return byProject(db.changes().all(), c -> projects.contains(c.getProject()), out);
-      }
-      if (!skipProjects.isEmpty()) {
-        return byProject(db.changes().all(), c -> !skipProjects.contains(c.getProject()), out);
-      }
-      if (!changes.isEmpty()) {
-        return byProject(db.changes().get(changes), c -> true, out);
-      }
-      return byProject(db.changes().all(), c -> true, out);
-    }
-  }
-
-  private static ImmutableListMultimap<Project.NameKey, Change.Id> byProject(
-      Iterable<Change> changes,
-      Predicate<Change> pred,
-      SetMultimap<Project.NameKey, Change.Id> out) {
-    Streams.stream(changes).filter(pred).forEach(c -> out.put(c.getProject(), c.getId()));
-    return ImmutableListMultimap.copyOf(out);
-  }
-
-  private static ObjectInserter newPackInserter(Repository repo) {
-    if (!(repo instanceof FileRepository)) {
-      return repo.newObjectInserter();
-    }
-    PackInserter ins = ((FileRepository) repo).getObjectDatabase().newPackInserter();
-    ins.checkExisting(false);
-    return ins;
-  }
-
-  private boolean rebuildProject(
-      ReviewDb db,
-      ImmutableListMultimap<Project.NameKey, Change.Id> allChanges,
-      Project.NameKey project) {
-    checkArgument(allChanges.containsKey(project));
-    boolean ok = true;
-    ProgressMonitor pm =
-        new TextProgressMonitor(
-            new PrintWriter(new BufferedWriter(new OutputStreamWriter(progressOut, UTF_8))));
-    try (Repository changeRepo = repoManager.openRepository(project);
-        // Only use a PackInserter for the change repo, not All-Users.
-        //
-        // It's not possible to share a single inserter for All-Users across all project tasks, and
-        // we don't want to add one pack per project to All-Users. Adding many loose objects is
-        // preferable to many packs.
-        //
-        // Anyway, the number of objects inserted into All-Users is proportional to the number
-        // of pending draft comments, which should not be high (relative to the total number of
-        // changes), so the number of loose objects shouldn't be too unreasonable.
-        ObjectInserter changeIns = newPackInserter(changeRepo);
-        ObjectReader changeReader = changeIns.newReader();
-        RevWalk changeRw = new RevWalk(changeReader);
-        Repository allUsersRepo = repoManager.openRepository(allUsers);
-        ObjectInserter allUsersIns = allUsersRepo.newObjectInserter();
-        ObjectReader allUsersReader = allUsersIns.newReader();
-        RevWalk allUsersRw = new RevWalk(allUsersReader)) {
-      ChainedReceiveCommands changeCmds = new ChainedReceiveCommands(changeRepo);
-      ChainedReceiveCommands allUsersCmds = new ChainedReceiveCommands(allUsersRepo);
-
-      Collection<Change.Id> changes = allChanges.get(project);
-      pm.beginTask(FormatUtil.elide("Rebuilding " + project.get(), 50), changes.size());
-      int toSave = 0;
-      try {
-        for (Change.Id changeId : changes) {
-          // NoteDbUpdateManager assumes that all commands in its OpenRepo were added by itself, so
-          // we can't share the top-level ChainedReceiveCommands. Use a new set of commands sharing
-          // the same underlying repo, and copy commands back to the top-level
-          // ChainedReceiveCommands later. This also assumes that each ref in the final list of
-          // commands was only modified by a single NoteDbUpdateManager; since we use one manager
-          // per change, and each ref corresponds to exactly one change, this assumption should be
-          // safe.
-          ChainedReceiveCommands tmpChangeCmds =
-              new ChainedReceiveCommands(changeCmds.getRepoRefCache());
-          ChainedReceiveCommands tmpAllUsersCmds =
-              new ChainedReceiveCommands(allUsersCmds.getRepoRefCache());
-
-          try (NoteDbUpdateManager manager =
-              updateManagerFactory
-                  .create(project)
-                  .setAtomicRefUpdates(false)
-                  .setSaveObjects(false)
-                  .setChangeRepo(changeRepo, changeRw, changeIns, tmpChangeCmds)
-                  .setAllUsersRepo(allUsersRepo, allUsersRw, allUsersIns, tmpAllUsersCmds)) {
-            rebuild(db, changeId, manager);
-
-            // Executing with dryRun=true writes all objects to the underlying inserters and adds
-            // commands to the ChainedReceiveCommands. Afterwards, we can discard the manager, so we
-            // don't keep using any memory beyond what may be buffered in the PackInserter.
-            manager.execute(true);
-
-            tmpChangeCmds.getCommands().values().forEach(c -> addCommand(changeCmds, c));
-            tmpAllUsersCmds.getCommands().values().forEach(c -> addCommand(allUsersCmds, c));
-
-            toSave++;
-          } catch (NoPatchSetsException e) {
-            logger.atWarning().log(e.getMessage());
-          } catch (ConflictingUpdateException ex) {
-            logger.atWarning().log(
-                "Rebuilding detected a conflicting ReviewDb update for change %s;"
-                    + " will be auto-rebuilt at runtime",
-                changeId);
-          } catch (Throwable t) {
-            logger.atSevere().withCause(t).log("Failed to rebuild change %s", changeId);
-            ok = false;
-          }
-          pm.update(1);
-        }
-      } finally {
-        pm.endTask();
-      }
-
-      pm.beginTask(FormatUtil.elide("Saving " + project.get(), 50), ProgressMonitor.UNKNOWN);
-      try {
-        save(changeRepo, changeRw, changeIns, changeCmds);
-        save(allUsersRepo, allUsersRw, allUsersIns, allUsersCmds);
-        // This isn't really useful progress. If we passed a real ProgressMonitor to
-        // BatchRefUpdate#execute we might get something more incremental, but that doesn't allow us
-        // to specify the repo name in the task text.
-        pm.update(toSave);
-      } catch (LockFailureException e) {
-        logger.atWarning().log(
-            "Rebuilding detected a conflicting NoteDb update for the following refs, which will"
-                + " be auto-rebuilt at runtime: %s",
-            e.getFailedRefs().stream().distinct().sorted().collect(joining(", ")));
-      } catch (IOException e) {
-        logger.atSevere().withCause(e).log("Failed to save NoteDb state for %s", project);
-      } finally {
-        pm.endTask();
-      }
-    } catch (RepositoryNotFoundException e) {
-      logger.atWarning().log("Repository %s not found", project);
-    } catch (IOException e) {
-      logger.atSevere().withCause(e).log("Failed to rebuild project %s", project);
-    }
-    return ok;
-  }
-
-  private void rebuild(ReviewDb db, Change.Id changeId, NoteDbUpdateManager manager)
-      throws OrmException, IOException {
-    // Match ChangeRebuilderImpl#stage, but without calling manager.stage(), since that can only be
-    // called after building updates for all changes.
-    Change change =
-        ChangeRebuilderImpl.checkNoteDbState(ChangeNotes.readOneReviewDbChange(db, changeId));
-    if (change == null) {
-      // Could log here instead, but this matches the behavior of ChangeRebuilderImpl#stage.
-      throw new NoSuchChangeException(changeId);
-    }
-    rebuilder.buildUpdates(manager, bundleReader.fromReviewDb(db, changeId));
-
-    rebuilder.execute(db, changeId, manager, true, false);
-  }
-
-  private static void addCommand(ChainedReceiveCommands cmds, ReceiveCommand cmd) {
-    // ChainedReceiveCommands doesn't allow no-ops, but these occur when rebuilding a
-    // previously-rebuilt change.
-    if (!cmd.getOldId().equals(cmd.getNewId())) {
-      cmds.add(cmd);
-    }
-  }
-
-  private void save(Repository repo, RevWalk rw, ObjectInserter ins, ChainedReceiveCommands cmds)
-      throws IOException {
-    if (cmds.isEmpty()) {
-      return;
-    }
-    ins.flush();
-    BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
-    bru.setRefLogMessage("Migrate changes to NoteDb", false);
-    bru.setRefLogIdent(serverIdent.get());
-    bru.setAtomic(false);
-    bru.setAllowNonFastForwards(true);
-    cmds.addTo(bru);
-    RefUpdateUtil.executeChecked(bru, rw);
-  }
-
-  private static boolean futuresToBoolean(List<ListenableFuture<Boolean>> futures, String errMsg) {
-    try {
-      return Futures.allAsList(futures).get().stream().allMatch(b -> b);
-    } catch (InterruptedException | ExecutionException e) {
-      logger.atSevere().withCause(e).log(errMsg);
-      return false;
-    }
-  }
-
-  private class ContextHelper implements AutoCloseable {
-    private final Thread callingThread;
-    private ReviewDb db;
-    private Runnable closeDb;
-
-    ContextHelper() {
-      callingThread = Thread.currentThread();
-    }
-
-    ManualRequestContext open() throws OrmException {
-      return new ManualRequestContext(
-          userFactory.create(),
-          // Reuse the same lazily-opened ReviewDb on the original calling thread, otherwise open
-          // SchemaFactory in the normal way.
-          Thread.currentThread().equals(callingThread) ? this::getReviewDb : schemaFactory,
-          requestContext);
-    }
-
-    synchronized ReviewDb getReviewDb() throws OrmException {
-      if (db == null) {
-        ReviewDb actual = schemaFactory.open();
-        closeDb = actual::close;
-        db =
-            new ReviewDbWrapper(unwrapDb(actual)) {
-              @Override
-              public void close() {
-                // Closed by ContextHelper#close.
-              }
-            };
-      }
-      return db;
-    }
-
-    @Override
-    public synchronized void close() {
-      if (db != null) {
-        closeDb.run();
-        db = null;
-        closeDb = null;
-      }
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/rebuild/NotesMigrationStateListener.java b/java/com/google/gerrit/server/notedb/rebuild/NotesMigrationStateListener.java
deleted file mode 100644
index aef30a2..0000000
--- a/java/com/google/gerrit/server/notedb/rebuild/NotesMigrationStateListener.java
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb.rebuild;
-
-import com.google.gerrit.extensions.annotations.ExtensionPoint;
-import com.google.gerrit.server.notedb.NotesMigrationState;
-import java.io.IOException;
-
-/** Listener for state changes performed by {@link OnlineNoteDbMigrator}. */
-@ExtensionPoint
-public interface NotesMigrationStateListener {
-  /**
-   * Invoked just before saving the new migration state.
-   *
-   * @param oldState state prior to this state change.
-   * @param newState state after this state change.
-   * @throws IOException if an error occurred, which will cause the migration to abort. Exceptions
-   *     that should be considered non-fatal must be caught (and ideally logged) by the
-   *     implementation rather than thrown.
-   */
-  void preStateChange(NotesMigrationState oldState, NotesMigrationState newState)
-      throws IOException;
-}
diff --git a/java/com/google/gerrit/server/notedb/rebuild/OnlineNoteDbMigrator.java b/java/com/google/gerrit/server/notedb/rebuild/OnlineNoteDbMigrator.java
deleted file mode 100644
index b5a8236..0000000
--- a/java/com/google/gerrit/server/notedb/rebuild/OnlineNoteDbMigrator.java
+++ /dev/null
@@ -1,108 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb.rebuild;
-
-import com.google.common.base.Stopwatch;
-import com.google.common.flogger.FluentLogger;
-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.index.OnlineUpgrader;
-import com.google.gerrit.server.index.VersionManager;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
-import com.google.inject.name.Named;
-import com.google.inject.name.Names;
-import java.util.concurrent.TimeUnit;
-import org.eclipse.jgit.lib.Config;
-
-@Singleton
-public class OnlineNoteDbMigrator implements LifecycleListener {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
-  private static final String TRIAL = "OnlineNoteDbMigrator/trial";
-
-  public static class Module extends LifecycleModule {
-    private final boolean trial;
-
-    public Module(boolean trial) {
-      this.trial = trial;
-    }
-
-    @Override
-    public void configure() {
-      listener().to(OnlineNoteDbMigrator.class);
-      bindConstant().annotatedWith(Names.named(TRIAL)).to(trial);
-    }
-  }
-
-  private final GcAllUsers gcAllUsers;
-  private final OnlineUpgrader indexUpgrader;
-  private final Provider<NoteDbMigrator.Builder> migratorBuilderProvider;
-  private final boolean upgradeIndex;
-  private final boolean trial;
-
-  @Inject
-  OnlineNoteDbMigrator(
-      @GerritServerConfig Config cfg,
-      GcAllUsers gcAllUsers,
-      OnlineUpgrader indexUpgrader,
-      Provider<NoteDbMigrator.Builder> migratorBuilderProvider,
-      @Named(TRIAL) boolean trial) {
-    this.gcAllUsers = gcAllUsers;
-    this.indexUpgrader = indexUpgrader;
-    this.migratorBuilderProvider = migratorBuilderProvider;
-    this.upgradeIndex = VersionManager.getOnlineUpgrade(cfg);
-    this.trial = trial || NoteDbMigrator.getTrialMode(cfg);
-  }
-
-  @Override
-  public void start() {
-    Thread t = new Thread(this::migrate);
-    t.setDaemon(true);
-    t.setName(getClass().getSimpleName());
-    t.start();
-  }
-
-  private void migrate() {
-    logger.atInfo().log("Starting online NoteDb migration");
-    if (upgradeIndex) {
-      logger.atInfo().log(
-          "Online index schema upgrades will be deferred until NoteDb migration is complete");
-    }
-    Stopwatch sw = Stopwatch.createStarted();
-    // TODO(dborowitz): Tune threads, maybe expose a progress monitor somewhere.
-    try (NoteDbMigrator migrator =
-        migratorBuilderProvider.get().setAutoMigrate(true).setTrialMode(trial).build()) {
-      migrator.migrate();
-    } catch (Exception e) {
-      logger.atSevere().withCause(e).log("Error in online NoteDb migration");
-    }
-    gcAllUsers.runWithLogger();
-    logger.atInfo().log("Online NoteDb migration completed in %ss", sw.elapsed(TimeUnit.SECONDS));
-
-    if (upgradeIndex) {
-      logger.atInfo().log("Starting deferred index schema upgrades");
-      indexUpgrader.start();
-    }
-  }
-
-  @Override
-  public void stop() {
-    // Do nothing; upgrade process uses daemon threads and knows how to recover from failures on
-    // next attempt.
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/rebuild/PatchSetEvent.java b/java/com/google/gerrit/server/notedb/rebuild/PatchSetEvent.java
deleted file mode 100644
index acb80c0..0000000
--- a/java/com/google/gerrit/server/notedb/rebuild/PatchSetEvent.java
+++ /dev/null
@@ -1,86 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb.rebuild;
-
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gwtorm.server.OrmException;
-import java.io.IOException;
-import java.util.List;
-import org.eclipse.jgit.errors.InvalidObjectIdException;
-import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.revwalk.RevWalk;
-
-class PatchSetEvent extends Event {
-  private final Change change;
-  private final PatchSet ps;
-  private final RevWalk rw;
-  boolean createChange;
-
-  PatchSetEvent(Change change, PatchSet ps, RevWalk rw) {
-    super(
-        ps.getId(),
-        ps.getUploader(),
-        ps.getUploader(),
-        ps.getCreatedOn(),
-        change.getCreatedOn(),
-        null);
-    this.change = change;
-    this.ps = ps;
-    this.rw = rw;
-  }
-
-  @Override
-  boolean uniquePerUpdate() {
-    return true;
-  }
-
-  @Override
-  void apply(ChangeUpdate update) throws IOException, OrmException {
-    checkUpdate(update);
-    if (createChange) {
-      ChangeRebuilderImpl.createChange(update, change);
-    } else {
-      update.setSubject(change.getSubject());
-      update.setSubjectForCommit("Create patch set " + ps.getPatchSetId());
-    }
-    setRevision(update, ps);
-    update.setPsDescription(ps.getDescription());
-    List<String> groups = ps.getGroups();
-    if (!groups.isEmpty()) {
-      update.setGroups(ps.getGroups());
-    }
-  }
-
-  private void setRevision(ChangeUpdate update, PatchSet ps) throws IOException {
-    String rev = ps.getRevision().get();
-    String cert = ps.getPushCertificate();
-    ObjectId id;
-    try {
-      id = ObjectId.fromString(rev);
-    } catch (InvalidObjectIdException e) {
-      update.setRevisionForMissingCommit(rev, cert);
-      return;
-    }
-    try {
-      update.setCommit(rw, id, cert);
-    } catch (MissingObjectException e) {
-      update.setRevisionForMissingCommit(rev, cert);
-      return;
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/rebuild/ReviewerEvent.java b/java/com/google/gerrit/server/notedb/rebuild/ReviewerEvent.java
deleted file mode 100644
index 2ecf969..0000000
--- a/java/com/google/gerrit/server/notedb/rebuild/ReviewerEvent.java
+++ /dev/null
@@ -1,63 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb.rebuild;
-
-import com.google.common.base.MoreObjects.ToStringHelper;
-import com.google.common.collect.Table;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.notedb.ReviewerStateInternal;
-import com.google.gwtorm.server.OrmException;
-import java.io.IOException;
-import java.sql.Timestamp;
-
-class ReviewerEvent extends Event {
-  private Table.Cell<ReviewerStateInternal, Account.Id, Timestamp> reviewer;
-
-  ReviewerEvent(
-      Table.Cell<ReviewerStateInternal, Account.Id, Timestamp> reviewer,
-      Timestamp changeCreatedOn) {
-    super(
-        // Reviewers aren't generally associated with a particular patch set
-        // (although as an implementation detail they were in ReviewDb). Just
-        // use the latest patch set at the time of the event.
-        null,
-        reviewer.getColumnKey(),
-        // TODO(dborowitz): Real account ID shouldn't really matter for
-        // reviewers, but we might have to deal with this to avoid ChangeBundle
-        // diffs when run against real data.
-        reviewer.getColumnKey(),
-        reviewer.getValue(),
-        changeCreatedOn,
-        null);
-    this.reviewer = reviewer;
-  }
-
-  @Override
-  boolean uniquePerUpdate() {
-    return false;
-  }
-
-  @Override
-  void apply(ChangeUpdate update) throws IOException, OrmException {
-    checkUpdate(update);
-    update.putReviewer(reviewer.getColumnKey(), reviewer.getRowKey());
-  }
-
-  @Override
-  protected void addToString(ToStringHelper helper) {
-    helper.add("account", reviewer.getColumnKey()).add("state", reviewer.getRowKey());
-  }
-}
diff --git a/java/com/google/gerrit/server/patch/AutoMerger.java b/java/com/google/gerrit/server/patch/AutoMerger.java
index 285c37d..18aa8b9 100644
--- a/java/com/google/gerrit/server/patch/AutoMerger.java
+++ b/java/com/google/gerrit/server/patch/AutoMerger.java
@@ -18,9 +18,9 @@
 
 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.server.GerritPersonIdent;
-import com.google.gerrit.server.UsedAt;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.InMemoryInserter;
 import com.google.gerrit.server.git.MergeUtil;
diff --git a/java/com/google/gerrit/server/patch/IntraLineDiff.java b/java/com/google/gerrit/server/patch/IntraLineDiff.java
index a182335..1c3d78a 100644
--- a/java/com/google/gerrit/server/patch/IntraLineDiff.java
+++ b/java/com/google/gerrit/server/patch/IntraLineDiff.java
@@ -21,6 +21,7 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.jgit.diff.ReplaceEdit;
 import com.google.gerrit.reviewdb.client.CodedEnum;
 import java.io.IOException;
 import java.io.InputStream;
@@ -32,7 +33,6 @@
 import java.util.Collections;
 import java.util.List;
 import org.eclipse.jgit.diff.Edit;
-import org.eclipse.jgit.diff.ReplaceEdit;
 
 public class IntraLineDiff implements Serializable {
   static final long serialVersionUID = IntraLineDiffKey.serialVersionUID;
diff --git a/java/com/google/gerrit/server/patch/IntraLineLoader.java b/java/com/google/gerrit/server/patch/IntraLineLoader.java
index 022fd9e..34ac3d8 100644
--- a/java/com/google/gerrit/server/patch/IntraLineLoader.java
+++ b/java/com/google/gerrit/server/patch/IntraLineLoader.java
@@ -19,6 +19,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.jgit.diff.ReplaceEdit;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
@@ -34,7 +35,6 @@
 import java.util.regex.Pattern;
 import org.eclipse.jgit.diff.Edit;
 import org.eclipse.jgit.diff.MyersDiff;
-import org.eclipse.jgit.diff.ReplaceEdit;
 import org.eclipse.jgit.lib.Config;
 
 class IntraLineLoader implements Callable<IntraLineDiff> {
diff --git a/java/com/google/gerrit/server/patch/PatchFile.java b/java/com/google/gerrit/server/patch/PatchFile.java
index e950c1b..0e5bcc1 100644
--- a/java/com/google/gerrit/server/patch/PatchFile.java
+++ b/java/com/google/gerrit/server/patch/PatchFile.java
@@ -16,7 +16,7 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import com.google.gerrit.common.errors.NoSuchEntityException;
+import com.google.gerrit.exceptions.NoSuchEntityException;
 import com.google.gerrit.reviewdb.client.Patch;
 import java.io.IOException;
 import org.eclipse.jgit.errors.CorruptObjectException;
diff --git a/java/com/google/gerrit/server/patch/PatchScriptBuilder.java b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
index 61f0180..c2caa01 100644
--- a/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
+++ b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
@@ -133,9 +133,7 @@
     edits = new ArrayList<>(content.getEdits());
     ImmutableSet<Edit> editsDueToRebase = content.getEditsDueToRebase();
 
-    if (!isModify(content)) {
-      intralineDifferenceIsPossible = false;
-    } else if (diffPrefs.intralineDifference) {
+    if (isModify(content) && diffPrefs.intralineDifference && isIntralineModeAllowed(b)) {
       IntraLineDiff d =
           patchListCache.getIntraLineDiff(
               IntraLineDiffKey.create(a.id, b.id, diffPrefs.ignoreWhitespace),
@@ -274,6 +272,16 @@
     }
   }
 
+  private static boolean isIntralineModeAllowed(Side side) {
+    // The intraline diff cache keys are the same for these cases. It's better to not show
+    // intraline results than showing completely wrong diffs or to run into a server error.
+    return !Patch.isMagic(side.path) && !isSubmoduleCommit(side.mode);
+  }
+
+  private static boolean isSubmoduleCommit(FileMode mode) {
+    return mode.getObjectType() == Constants.OBJ_COMMIT;
+  }
+
   private void correctForDifferencesInNewlineAtEnd() {
     // a.src.size() is the size ignoring a newline at the end whereas a.size() considers it.
     int aSize = a.src.size();
@@ -286,6 +294,12 @@
       return;
     }
 
+    if (edits.isEmpty() && (aSize != bSize)) {
+      // Only edits due to rebase were present. If we now added the edits for the newlines, the
+      // code which later assembles the file contents would fail.
+      return;
+    }
+
     Optional<Edit> lastEdit = getLast(edits);
     if (isNewlineAtEndDeleted()) {
       Optional<Edit> lastLineEdit = lastEdit.filter(edit -> edit.getEndA() == aSize);
diff --git a/java/com/google/gerrit/server/patch/PatchScriptFactory.java b/java/com/google/gerrit/server/patch/PatchScriptFactory.java
index bfcb325..a7e7738 100644
--- a/java/com/google/gerrit/server/patch/PatchScriptFactory.java
+++ b/java/com/google/gerrit/server/patch/PatchScriptFactory.java
@@ -29,7 +29,6 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.PatchSetUtil;
@@ -44,7 +43,6 @@
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
@@ -82,7 +80,6 @@
   private final PatchSetUtil psUtil;
   private final Provider<PatchScriptBuilder> builderFactory;
   private final PatchListCache patchListCache;
-  private final ReviewDb db;
   private final CommentsUtil commentsUtil;
 
   private final String fileName;
@@ -112,7 +109,6 @@
       PatchSetUtil psUtil,
       Provider<PatchScriptBuilder> builderFactory,
       PatchListCache patchListCache,
-      ReviewDb db,
       CommentsUtil commentsUtil,
       ChangeEditUtil editReader,
       Provider<CurrentUser> userProvider,
@@ -127,7 +123,6 @@
     this.psUtil = psUtil;
     this.builderFactory = builderFactory;
     this.patchListCache = patchListCache;
-    this.db = db;
     this.notes = notes;
     this.commentsUtil = commentsUtil;
     this.editReader = editReader;
@@ -150,7 +145,6 @@
       PatchSetUtil psUtil,
       Provider<PatchScriptBuilder> builderFactory,
       PatchListCache patchListCache,
-      ReviewDb db,
       CommentsUtil commentsUtil,
       ChangeEditUtil editReader,
       Provider<CurrentUser> userProvider,
@@ -165,7 +159,6 @@
     this.psUtil = psUtil;
     this.builderFactory = builderFactory;
     this.patchListCache = patchListCache;
-    this.db = db;
     this.notes = notes;
     this.commentsUtil = commentsUtil;
     this.editReader = editReader;
@@ -193,18 +186,18 @@
 
   @Override
   public PatchScript call()
-      throws OrmException, LargeObjectException, AuthException, InvalidChangeOperationException,
-          IOException, PermissionBackendException {
+      throws LargeObjectException, AuthException, InvalidChangeOperationException, IOException,
+          PermissionBackendException {
     if (parentNum < 0) {
       validatePatchSetId(psa);
     }
     validatePatchSetId(psb);
 
-    PatchSet psEntityA = psa != null ? psUtil.get(db, notes, psa) : null;
-    PatchSet psEntityB = psb.get() == 0 ? new PatchSet(psb) : psUtil.get(db, notes, 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).database(db).check(ChangePermission.READ);
+        permissionBackend.currentUser().change(notes).check(ChangePermission.READ);
       } catch (AuthException e) {
         throw new NoSuchChangeException(changeId, e);
       }
@@ -265,7 +258,7 @@
     return b;
   }
 
-  private ObjectId toObjectId(PatchSet ps) throws AuthException, IOException, OrmException {
+  private ObjectId toObjectId(PatchSet ps) throws AuthException, IOException {
     if (ps.getId().get() == 0) {
       return getEditRev();
     }
@@ -281,7 +274,7 @@
     }
   }
 
-  private ObjectId getEditRev() throws AuthException, IOException, OrmException {
+  private ObjectId getEditRev() throws AuthException, IOException {
     edit = editReader.byChange(notes);
     if (edit.isPresent()) {
       return edit.get().getEditCommit();
@@ -297,8 +290,7 @@
     }
   }
 
-  private void loadCommentsAndHistory(ChangeType changeType, String oldName, String newName)
-      throws OrmException {
+  private void loadCommentsAndHistory(ChangeType changeType, String oldName, String newName) {
     Map<Patch.Key, Patch> byKey = new HashMap<>();
 
     if (loadHistory) {
@@ -308,7 +300,7 @@
       // proper rename detection between the patch sets.
       //
       history = new ArrayList<>();
-      for (PatchSet ps : psUtil.byChange(db, notes)) {
+      for (PatchSet ps : psUtil.byChange(notes)) {
         String name = fileName;
         if (psa != null) {
           switch (changeType) {
@@ -390,8 +382,8 @@
     }
   }
 
-  private void loadPublished(Map<Patch.Key, Patch> byKey, String file) throws OrmException {
-    for (Comment c : commentsUtil.publishedByChangeFile(db, notes, changeId, file)) {
+  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);
@@ -402,9 +394,8 @@
     }
   }
 
-  private void loadDrafts(Map<Patch.Key, Patch> byKey, Account.Id me, String file)
-      throws OrmException {
-    for (Comment c : commentsUtil.draftByChangeFileAuthor(db, notes, file, me)) {
+  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);
diff --git a/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java b/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
index 41bade6..0eb5588 100644
--- a/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
+++ b/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
@@ -14,18 +14,17 @@
 
 package com.google.gerrit.server.patch;
 
+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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.account.Emails;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -54,8 +53,7 @@
     this.emails = emails;
   }
 
-  public PatchSetInfo get(RevWalk rw, RevCommit src, PatchSet.Id psi)
-      throws IOException, OrmException {
+  public PatchSetInfo get(RevWalk rw, RevCommit src, PatchSet.Id psi) throws IOException {
     rw.parseBody(src);
     PatchSetInfo info = new PatchSetInfo(psi);
     info.setSubject(src.getShortMessage());
@@ -66,12 +64,12 @@
     return info;
   }
 
-  public PatchSetInfo get(ReviewDb db, ChangeNotes notes, PatchSet.Id psId)
+  public PatchSetInfo get(ChangeNotes notes, PatchSet.Id psId)
       throws PatchSetInfoNotAvailableException {
     try {
-      PatchSet patchSet = psUtil.get(db, notes, psId);
+      PatchSet patchSet = psUtil.get(notes, psId);
       return get(notes.getProjectName(), patchSet);
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       throw new PatchSetInfoNotAvailableException(e);
     }
   }
@@ -84,13 +82,13 @@
       PatchSetInfo info = get(rw, src, patchSet.getId());
       info.setParents(toParentInfos(src.getParents(), rw));
       return info;
-    } catch (IOException | OrmException e) {
+    } catch (IOException | StorageException e) {
       throw new PatchSetInfoNotAvailableException(e);
     }
   }
 
   // TODO: The same method exists in EventFactory, find a common place for it
-  private UserIdentity toUserIdentity(PersonIdent who) throws IOException, OrmException {
+  private UserIdentity toUserIdentity(PersonIdent who) throws IOException {
     final UserIdentity u = new UserIdentity();
     u.setName(who.getName());
     u.setEmail(who.getEmailAddress());
diff --git a/java/com/google/gerrit/server/permissions/ChangeControl.java b/java/com/google/gerrit/server/permissions/ChangeControl.java
index f4e659e..ee362002 100644
--- a/java/com/google/gerrit/server/permissions/ChangeControl.java
+++ b/java/com/google/gerrit/server/permissions/ChangeControl.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.permissions;
 
-import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.server.permissions.DefaultPermissionMappings.labelPermissionName;
 import static com.google.gerrit.server.permissions.LabelPermission.ForUser.ON_BEHALF_OF;
 
@@ -23,19 +22,17 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRange;
+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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.PermissionBackend.ForChange;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.util.Collection;
 import java.util.EnumSet;
@@ -55,10 +52,8 @@
       this.notesFactory = notesFactory;
     }
 
-    ChangeControl create(
-        RefControl refControl, ReviewDb db, Project.NameKey project, Change.Id changeId)
-        throws OrmException {
-      return create(refControl, notesFactory.create(db, project, changeId));
+    ChangeControl create(RefControl refControl, Project.NameKey project, Change.Id changeId) {
+      return create(refControl, notesFactory.create(project, changeId));
     }
 
     ChangeControl create(RefControl refControl, ChangeNotes notes) {
@@ -77,8 +72,8 @@
     this.notes = notes;
   }
 
-  ForChange asForChange(@Nullable ChangeData cd, @Nullable Provider<ReviewDb> db) {
-    return new ForChangeImpl(cd, db);
+  ForChange asForChange(@Nullable ChangeData cd) {
+    return new ForChangeImpl(cd);
   }
 
   private CurrentUser getUser() {
@@ -94,8 +89,8 @@
   }
 
   /** Can this user see this change? */
-  private boolean isVisible(ReviewDb db, @Nullable ChangeData cd) throws OrmException {
-    if (getChange().isPrivate() && !isPrivateVisible(db, cd)) {
+  private boolean isVisible(@Nullable ChangeData cd) {
+    if (getChange().isPrivate() && !isPrivateVisible(cd)) {
       return false;
     }
     return refControl.isVisible();
@@ -158,9 +153,9 @@
   }
 
   /** Is this user a reviewer for the change? */
-  private boolean isReviewer(ReviewDb db, @Nullable ChangeData cd) throws OrmException {
+  private boolean isReviewer(@Nullable ChangeData cd) {
     if (getUser().isIdentifiedUser()) {
-      cd = cd != null ? cd : changeDataFactory.create(db, notes);
+      cd = cd != null ? cd : changeDataFactory.create(notes);
       Collection<Account.Id> results = cd.reviewers().all();
       return results.contains(getUser().getAccountId());
     }
@@ -169,7 +164,7 @@
 
   /** Can this user edit the topic name? */
   private boolean canEditTopicName() {
-    if (getChange().getStatus().isOpen()) {
+    if (getChange().isNew()) {
       return isOwner() // owner (aka creator) of the change can edit topic
           || refControl.isOwner() // branch owner can edit topic
           || getProjectControl().isOwner() // project owner can edit topic
@@ -180,9 +175,17 @@
     return refControl.canForceEditTopicName();
   }
 
+  /** Can this user toggle WorkInProgress state? */
+  private boolean canToggleWorkInProgressState() {
+    return isOwner()
+        || getProjectControl().isOwner()
+        || refControl.canPerform(Permission.TOGGLE_WORK_IN_PROGRESS_STATE)
+        || getProjectControl().isAdmin();
+  }
+
   /** Can this user edit the description? */
   private boolean canEditDescription() {
-    if (getChange().getStatus().isOpen()) {
+    if (getChange().isNew()) {
       return isOwner() // owner (aka creator) of the change can edit desc
           || refControl.isOwner() // branch owner can edit desc
           || getProjectControl().isOwner() // project owner can edit desc
@@ -208,9 +211,9 @@
         || getProjectControl().isAdmin();
   }
 
-  private boolean isPrivateVisible(ReviewDb db, ChangeData cd) throws OrmException {
+  private boolean isPrivateVisible(ChangeData cd) {
     return isOwner()
-        || isReviewer(db, cd)
+        || isReviewer(cd)
         || refControl.canPerform(Permission.VIEW_PRIVATE_CHANGES)
         || getUser().isInternalUser();
   }
@@ -220,26 +223,13 @@
     private Map<String, PermissionRange> labels;
     private String resourcePath;
 
-    ForChangeImpl(@Nullable ChangeData cd, @Nullable Provider<ReviewDb> db) {
+    ForChangeImpl(@Nullable ChangeData cd) {
       this.cd = cd;
-      this.db = db;
-    }
-
-    private ReviewDb db() {
-      if (db != null) {
-        return db.get();
-      } else if (cd != null) {
-        return cd.db();
-      } else {
-        return null;
-      }
     }
 
     private ChangeData changeData() {
       if (cd == null) {
-        ReviewDb reviewDb = db();
-        checkState(reviewDb != null, "need ReviewDb");
-        cd = changeDataFactory.create(reviewDb, notes);
+        cd = changeDataFactory.create(notes);
       }
       return cd;
     }
@@ -295,7 +285,7 @@
       try {
         switch (perm) {
           case READ:
-            return isVisible(db(), changeData());
+            return isVisible(changeData());
           case ABANDON:
             return canAbandon();
           case DELETE:
@@ -316,12 +306,14 @@
             return canRestore();
           case SUBMIT:
             return refControl.canSubmit(isOwner());
+          case TOGGLE_WORK_IN_PROGRESS_STATE:
+            return canToggleWorkInProgressState();
 
           case REMOVE_REVIEWER:
           case SUBMIT_AS:
             return refControl.canPerform(changePermissionName(perm));
         }
-      } catch (OrmException e) {
+      } catch (StorageException e) {
         throw new PermissionBackendException("unavailable", e);
       }
       throw new PermissionBackendException(perm + " unsupported");
diff --git a/java/com/google/gerrit/server/permissions/ChangePermission.java b/java/com/google/gerrit/server/permissions/ChangePermission.java
index ca1c460..2fba4ef 100644
--- a/java/com/google/gerrit/server/permissions/ChangePermission.java
+++ b/java/com/google/gerrit/server/permissions/ChangePermission.java
@@ -55,7 +55,8 @@
    */
   REBASE,
   SUBMIT,
-  SUBMIT_AS("submit on behalf of other users");
+  SUBMIT_AS("submit on behalf of other users"),
+  TOGGLE_WORK_IN_PROGRESS_STATE;
 
   private final String description;
 
diff --git a/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java b/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
index 1782ac9..b23c85f 100644
--- a/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
@@ -40,7 +40,6 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.util.Collection;
-import java.util.EnumSet;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
@@ -109,7 +108,7 @@
             PerThreadCache.getOrCompute(
                 PerThreadCache.Key.create(ProjectControl.class, project, user.getCacheKey()),
                 () -> projectControlFactory.create(user, state));
-        return control.asForProject().database(db);
+        return control.asForProject();
       } catch (Exception e) {
         Throwable cause = e.getCause() != null ? e.getCause() : e;
         return FailedPermissionBackend.project(
@@ -128,7 +127,7 @@
     @Override
     public <T extends GlobalOrPluginPermission> Set<T> test(Collection<T> permSet)
         throws PermissionBackendException {
-      Set<T> ok = newSet(permSet);
+      Set<T> ok = Sets.newHashSetWithExpectedSize(permSet.size());
       for (T perm : permSet) {
         if (can(perm)) {
           ok.add(perm);
@@ -147,7 +146,7 @@
         return can((GlobalPermission) perm);
       } else if (perm instanceof PluginPermission) {
         PluginPermission pluginPermission = (PluginPermission) perm;
-        return has(DefaultPermissionMappings.pluginPermissionName(pluginPermission))
+        return has(DefaultPermissionMappings.pluginCapabilityName(pluginPermission))
             || (pluginPermission.fallBackToAdmin() && isAdmin());
       }
       throw new PermissionBackendException(perm + " unsupported");
@@ -267,14 +266,4 @@
       return denied.isEmpty() || !user.getEffectiveGroups().containsAnyOf(denied);
     }
   }
-
-  private static <T extends GlobalOrPluginPermission> Set<T> newSet(Collection<T> permSet) {
-    if (permSet instanceof EnumSet) {
-      @SuppressWarnings({"unchecked", "rawtypes"})
-      Set<T> s = ((EnumSet) permSet).clone();
-      s.clear();
-      return s;
-    }
-    return Sets.newHashSetWithExpectedSize(permSet.size());
-  }
 }
diff --git a/java/com/google/gerrit/server/permissions/DefaultPermissionMappings.java b/java/com/google/gerrit/server/permissions/DefaultPermissionMappings.java
index ece29df..8215083 100644
--- a/java/com/google/gerrit/server/permissions/DefaultPermissionMappings.java
+++ b/java/com/google/gerrit/server/permissions/DefaultPermissionMappings.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission;
 import com.google.gerrit.extensions.api.access.PluginPermission;
+import com.google.gerrit.extensions.api.access.PluginProjectPermission;
 import com.google.gerrit.server.permissions.LabelPermission.ForUser;
 import java.util.EnumSet;
 import java.util.Optional;
@@ -97,6 +98,9 @@
           .put(ChangePermission.REBASE, Permission.REBASE)
           .put(ChangePermission.SUBMIT, Permission.SUBMIT)
           .put(ChangePermission.SUBMIT_AS, Permission.SUBMIT_AS)
+          .put(
+              ChangePermission.TOGGLE_WORK_IN_PROGRESS_STATE,
+              Permission.TOGGLE_WORK_IN_PROGRESS_STATE)
           .build();
 
   private static <T extends Enum<T>> void checkMapContainsAllEnumValues(
@@ -117,14 +121,18 @@
     return Optional.ofNullable(CAPABILITIES.inverse().get(capabilityName));
   }
 
-  public static String pluginPermissionName(PluginPermission pluginPermission) {
+  public static String pluginCapabilityName(PluginPermission pluginPermission) {
     return pluginPermission.pluginName() + '-' + pluginPermission.capability();
   }
 
+  public static String pluginProjectPermissionName(PluginProjectPermission pluginPermission) {
+    return "plugin-" + pluginPermission.pluginName() + '-' + pluginPermission.permission();
+  }
+
   public static String globalOrPluginPermissionName(GlobalOrPluginPermission permission) {
     return permission instanceof GlobalPermission
         ? globalPermissionName((GlobalPermission) permission)
-        : pluginPermissionName((PluginPermission) permission);
+        : pluginCapabilityName((PluginPermission) permission);
   }
 
   public static Optional<String> projectPermissionName(ProjectPermission projectPermission) {
diff --git a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
index 55d7c6c..a5174e1 100644
--- a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
+++ b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
@@ -14,18 +14,24 @@
 
 package com.google.gerrit.server.permissions;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+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_CACHE_AUTOMERGE;
-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.reviewdb.client.RefNames.REFS_USERS_SELF;
 import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.toMap;
 
+import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Maps;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+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;
@@ -36,7 +42,6 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupCache;
@@ -50,9 +55,7 @@
 import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -76,7 +79,6 @@
   private final TagCache tagCache;
   private final ChangeNotes.Factory changeNotesFactory;
   @Nullable private final SearchingChangeCacheImpl changeCache;
-  private final Provider<ReviewDb> db;
   private final GroupCache groupCache;
   private final PermissionBackend permissionBackend;
   private final ProjectControl projectControl;
@@ -94,7 +96,6 @@
       TagCache tagCache,
       ChangeNotes.Factory changeNotesFactory,
       @Nullable SearchingChangeCacheImpl changeCache,
-      Provider<ReviewDb> db,
       GroupCache groupCache,
       PermissionBackend permissionBackend,
       @GerritServerConfig Config config,
@@ -103,7 +104,6 @@
     this.tagCache = tagCache;
     this.changeNotesFactory = changeNotesFactory;
     this.changeCache = changeCache;
-    this.db = db;
     this.groupCache = groupCache;
     this.permissionBackend = permissionBackend;
     this.skipFullRefEvaluationIfAllRefsAreVisible =
@@ -113,7 +113,7 @@
     this.user = projectControl.getUser();
     this.projectState = projectControl.getProjectState();
     this.permissionBackendForProject =
-        permissionBackend.user(user).database(db).project(projectState.getNameKey());
+        permissionBackend.user(user).project(projectState.getNameKey());
     this.fullFilterCount =
         metricMaker.newCounter(
             "permissions/ref_filter/full_filter_count",
@@ -127,20 +127,70 @@
                 .setRate());
   }
 
+  /** Filters given refs and tags by visibility. */
   Map<String, Ref> filter(Map<String, Ref> refs, Repository repo, RefFilterOptions opts)
       throws PermissionBackendException {
+    // 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)) {
+        return ImmutableMap.of();
+      }
+      if (RefNames.isRefsChanges(refName)) {
+        return canSeeSingleChangeRef(refName) ? refs : 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
+    // 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());
+
+      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);
+          }
+        } catch (IOException e) {
+          throw new PermissionBackendException(e);
+        }
+      }
+    }
+    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
+   * 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);
     }
 
+    // TODO(hiesel): Remove when optimization is done.
+    boolean hasReadOnRefsStar =
+        checkProjectPermission(permissionBackendForProject, ProjectPermission.READ);
     if (skipFullRefEvaluationIfAllRefsAreVisible && !projectState.isAllUsers()) {
-      if (projectState.statePermitsRead()
-          && checkProjectPermission(permissionBackendForProject, ProjectPermission.READ)) {
+      if (projectState.statePermitsRead() && hasReadOnRefsStar) {
         skipFilterCount.increment();
-        return refs;
+        return new AutoValue_DefaultRefFilter_Result(refs, ImmutableList.of());
       } else if (projectControl.allRefsAreVisible(ImmutableSet.of(RefNames.REFS_CONFIG))) {
         skipFilterCount.increment();
-        return fastHideRefsMetaConfig(refs);
+        return new AutoValue_DefaultRefFilter_Result(
+            fastHideRefsMetaConfig(refs), ImmutableList.of());
       }
     }
     fullFilterCount.increment();
@@ -164,7 +214,6 @@
 
     Map<String, Ref> result = new HashMap<>();
     List<Ref> deferredTags = new ArrayList<>();
-
     for (Ref ref : refs.values()) {
       String name = ref.getName();
       Change.Id changeId;
@@ -197,9 +246,22 @@
           result.put(name, ref);
         }
       } else if (isTag(ref)) {
-        // If its a tag, consider it later.
-        if (ref.getObjectId() != null) {
-          deferredTags.add(ref);
+        if (hasReadOnRefsStar) {
+          // The user has READ on refs/*. This is the broadest permission one can assign. There is
+          // no way to grant access to (specific) tags in Gerrit, so we have to assume that these
+          // users can see all tags because there could be tags that aren't reachable by any visible
+          // ref while the user can see all non-Gerrit refs. This matches Gerrit's historic
+          // behavior.
+          // This makes it so that these users could see commits that they can't see otherwise
+          // (e.g. a private change ref) if a tag was attached to it. Tags are meant to be used on
+          // the regular Git tree that users interact with, not on any of the Gerrit trees, so this
+          // is a negligible risk.
+          result.put(name, ref);
+        } else {
+          // If its a tag, consider it later.
+          if (ref.getObjectId() != null) {
+            deferredTags.add(ref);
+          }
         }
       } else if (name.startsWith(RefNames.REFS_SEQUENCES)) {
         // Sequences are internal database implementation details.
@@ -226,32 +288,29 @@
         }
       }
     }
+    return new AutoValue_DefaultRefFilter_Result(result, deferredTags);
+  }
 
-    // If we have tags that were deferred, we need to do a revision walk
-    // to identify what tags we can actually reach, and what we cannot.
-    //
-    if (!deferredTags.isEmpty() && (!result.isEmpty() || opts.filterTagsSeparately())) {
-      TagMatcher tags =
-          tagCache
-              .get(projectState.getNameKey())
-              .matcher(
-                  tagCache,
-                  repo,
-                  opts.filterTagsSeparately()
-                      ? filter(
-                              repo.getAllRefs(),
-                              repo,
-                              opts.toBuilder().setFilterTagsSeparately(false).build())
-                          .values()
-                      : result.values());
-      for (Ref tag : deferredTags) {
-        if (tags.isReachable(tag)) {
-          result.put(tag.getName(), tag);
-        }
-      }
+  /**
+   * Returns all refs tag we regard as starting points for reachability computation for tags. In
+   * general, these are all refs not managed by Gerrit excluding symbolic refs and tags.
+   *
+   * <p>We exclude symbolic refs because their target will be included and this will suffice for
+   * computing reachability.
+   */
+  private static Map<String, Ref> getTaggableRefsMap(Repository repo)
+      throws PermissionBackendException {
+    try {
+      return repo.getRefDatabase().getRefs().stream()
+          .filter(
+              r ->
+                  !RefNames.isGerritRef(r.getName())
+                      && !r.getName().startsWith(RefNames.REFS_TAGS)
+                      && !r.isSymbolic())
+          .collect(toMap(Ref::getName, r -> r));
+    } catch (IOException e) {
+      throw new PermissionBackendException(e);
     }
-
-    return result;
   }
 
   private Map<String, Ref> fastHideRefsMetaConfig(Map<String, Ref> refs)
@@ -296,6 +355,7 @@
     if (id == null) {
       return false;
     }
+
     if (user.isIdentifiedUser()
         && name.startsWith(RefNames.refsEditPrefix(user.asIdentifiedUser().getAccountId()))
         && visible(repo, id)) {
@@ -320,7 +380,7 @@
     Project.NameKey project = projectState.getNameKey();
     try {
       Map<Change.Id, Branch.NameKey> visibleChanges = new HashMap<>();
-      for (ChangeData cd : changeCache.getChangeData(db.get(), project)) {
+      for (ChangeData cd : changeCache.getChangeData(project)) {
         ChangeNotes notes = changeNotesFactory.createFromIndexedChange(cd.change());
         if (!projectState.statePermitsRead()) {
           continue;
@@ -333,7 +393,7 @@
         }
       }
       return visibleChanges;
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log(
           "Cannot load changes for project %s, assuming no changes are visible", project);
       return Collections.emptyMap();
@@ -345,7 +405,7 @@
     Project.NameKey p = projectState.getNameKey();
     ImmutableList<ChangeNotesResult> changes;
     try {
-      changes = changeNotesFactory.scan(repo, db.get(), p).collect(toImmutableList());
+      changes = changeNotesFactory.scan(repo, p).collect(toImmutableList());
     } catch (IOException e) {
       logger.atSevere().withCause(e).log(
           "Cannot load changes for project %s, assuming no changes are visible", p);
@@ -384,7 +444,7 @@
   }
 
   private boolean isMetadata(String name) {
-    return name.startsWith(REFS_CHANGES) || RefNames.isRefsEdit(name);
+    return RefNames.isRefsChanges(name) || RefNames.isRefsEdit(name);
   }
 
   private static boolean isTag(Ref ref) {
@@ -423,4 +483,46 @@
     return isAdmin
         || (user != null && user.getEffectiveGroups().contains(group.getOwnerGroupUUID()));
   }
+
+  /**
+   * Returns true if the user can see the provided change ref. Uses NoteDb for evaluation, hence
+   * does not suffer from the limitations documented in {@link SearchingChangeCacheImpl}.
+   *
+   * <p>This code lets users fetch changes that are not among the fraction of most recently modified
+   * changes that {@link SearchingChangeCacheImpl} returns. This works only when Git Protocol v2
+   * with refs-in-wants is used as that enables Gerrit to skip traditional advertisement of all
+   * visible refs.
+   */
+  private boolean canSeeSingleChangeRef(String refName) throws PermissionBackendException {
+    // We are treating just a single change ref. We are therefore not going through regular ref
+    // filtering, but use NoteDb directly. This makes it so that we can always serve this ref
+    // even if the change is not part of the set of most recent changes that
+    // SearchingChangeCacheImpl returns.
+    Change.Id cId = Change.Id.fromRef(refName);
+    checkNotNull(cId, "invalid change id for ref %s", refName);
+    ChangeNotes notes;
+    try {
+      notes = changeNotesFactory.create(projectState.getNameKey(), cId);
+    } catch (StorageException e) {
+      throw new PermissionBackendException("can't construct change notes", e);
+    }
+    try {
+      permissionBackendForProject.change(notes).check(ChangePermission.READ);
+      return true;
+    } catch (AuthException e) {
+      return false;
+    }
+  }
+
+  @AutoValue
+  abstract static class Result {
+    /** Subset of the refs passed into the computation that is visible to the user. */
+    abstract Map<String, Ref> visibleRefs();
+
+    /**
+     * List of tags where we couldn't figure out visibility in the first pass and need to do an
+     * expensive ref walk.
+     */
+    abstract List<Ref> deferredTags();
+  }
 }
diff --git a/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java b/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
index bd7c549..5c7ee0d 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.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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.PermissionBackend.ForChange;
 import com.google.gerrit.server.permissions.PermissionBackend.ForProject;
@@ -25,7 +25,6 @@
 import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
 import com.google.gerrit.server.permissions.PermissionBackend.WithUser;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.inject.Provider;
 import java.util.Collection;
 import java.util.Map;
 import java.util.Set;
@@ -115,11 +114,6 @@
     }
 
     @Override
-    public ForProject database(Provider<ReviewDb> db) {
-      return this;
-    }
-
-    @Override
     public String resourcePath() {
       throw new UnsupportedOperationException(
           "FailedPermissionBackend is not scoped to a resource");
@@ -131,18 +125,18 @@
     }
 
     @Override
-    public void check(ProjectPermission perm) throws PermissionBackendException {
+    public void check(CoreOrPluginProjectPermission perm) throws PermissionBackendException {
       throw new PermissionBackendException(message, cause);
     }
 
     @Override
-    public Set<ProjectPermission> test(Collection<ProjectPermission> permSet)
+    public <T extends CoreOrPluginProjectPermission> Set<T> test(Collection<T> permSet)
         throws PermissionBackendException {
       throw new PermissionBackendException(message, cause);
     }
 
     @Override
-    public BooleanCondition testCond(ProjectPermission perm) {
+    public BooleanCondition testCond(CoreOrPluginProjectPermission perm) {
       throw new UnsupportedOperationException(
           "FailedPermissionBackend does not support conditions");
     }
@@ -164,11 +158,6 @@
     }
 
     @Override
-    public ForRef database(Provider<ReviewDb> db) {
-      return this;
-    }
-
-    @Override
     public String resourcePath() {
       throw new UnsupportedOperationException(
           "FailedPermissionBackend is not scoped to a resource");
@@ -217,11 +206,6 @@
     }
 
     @Override
-    public ForChange database(Provider<ReviewDb> db) {
-      return this;
-    }
-
-    @Override
     public String resourcePath() {
       throw new UnsupportedOperationException(
           "FailedPermissionBackend is not scoped to a resource");
diff --git a/java/com/google/gerrit/server/permissions/PermissionBackend.java b/java/com/google/gerrit/server/permissions/PermissionBackend.java
index fea42b5..0537729 100644
--- a/java/com/google/gerrit/server/permissions/PermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/PermissionBackend.java
@@ -15,12 +15,16 @@
 package com.google.gerrit.server.permissions;
 
 import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.toMap;
 import static java.util.stream.Collectors.toSet;
 
 import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.LabelType;
+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;
@@ -28,18 +32,15 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.ImplementedBy;
-import com.google.inject.Provider;
-import com.google.inject.util.Providers;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -71,7 +72,7 @@
  *   private final PermissionBackend permissions;
  *   private final Provider<CurrentUser> user;
  *
- *   @Inject
+ *   {@literal @}Inject
  *   Foo(PermissionBackend permissions, Provider<CurrentUser> user) {
  *     this.permissions = permissions;
  *     this.user = user;
@@ -150,42 +151,21 @@
     // delegates to the appropriate testOrFalse method in PermissionBackend.
   }
 
-  /** PermissionBackend with an optional per-request ReviewDb handle. */
-  public abstract static class AcceptsReviewDb<T> {
-    protected Provider<ReviewDb> db;
-
-    public T database(Provider<ReviewDb> db) {
-      if (db != null) {
-        this.db = db;
-      }
-      return self();
-    }
-
-    public T database(ReviewDb db) {
-      return database(Providers.of(requireNonNull(db, "ReviewDb")));
-    }
-
-    @SuppressWarnings("unchecked")
-    private T self() {
-      return (T) this;
-    }
-  }
-
   /** PermissionBackend scoped to a specific user. */
-  public abstract static class WithUser extends AcceptsReviewDb<WithUser> {
+  public abstract static class WithUser {
     /** Returns an instance scoped for the specified project. */
     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()).database(db);
+      return project(ref.getParentKey()).ref(ref.get());
     }
 
     /** Returns an instance scoped for the change, and its destination ref and project. */
     public ForChange change(ChangeData cd) {
       try {
         return ref(cd.change().getDest()).change(cd);
-      } catch (OrmException e) {
+      } catch (StorageException e) {
         return FailedPermissionBackend.change("unavailable", e);
       }
     }
@@ -290,7 +270,7 @@
   }
 
   /** PermissionBackend scoped to a user and project. */
-  public abstract static class ForProject extends AcceptsReviewDb<ForProject> {
+  public abstract static class ForProject {
     /** Returns the fully qualified resource path that this instance is scoped to. */
     public abstract String resourcePath();
 
@@ -301,7 +281,7 @@
     public ForChange change(ChangeData cd) {
       try {
         return ref(cd.change().getDest().get()).change(cd);
-      } catch (OrmException e) {
+      } catch (StorageException e) {
         return FailedPermissionBackend.change("unavailable", e);
       }
     }
@@ -321,18 +301,23 @@
     }
 
     /** Verify scoped user can {@code perm}, throwing if denied. */
-    public abstract void check(ProjectPermission perm)
+    public abstract void check(CoreOrPluginProjectPermission perm)
         throws AuthException, PermissionBackendException;
 
     /** Filter {@code permSet} to permissions scoped user might be able to perform. */
-    public abstract Set<ProjectPermission> test(Collection<ProjectPermission> permSet)
+    public abstract <T extends CoreOrPluginProjectPermission> Set<T> test(Collection<T> permSet)
         throws PermissionBackendException;
 
-    public boolean test(ProjectPermission perm) throws PermissionBackendException {
-      return test(EnumSet.of(perm)).contains(perm);
+    public boolean test(CoreOrPluginProjectPermission perm) throws PermissionBackendException {
+      if (perm instanceof ProjectPermission) {
+        return test(EnumSet.of((ProjectPermission) perm)).contains(perm);
+      }
+
+      // TODO(xchangcheng): implement for plugin defined project permissions.
+      return false;
     }
 
-    public boolean testOrFalse(ProjectPermission perm) {
+    public boolean testOrFalse(CoreOrPluginProjectPermission perm) {
       try {
         return test(perm);
       } catch (PermissionBackendException e) {
@@ -341,7 +326,7 @@
       }
     }
 
-    public abstract BooleanCondition testCond(ProjectPermission perm);
+    public abstract BooleanCondition testCond(CoreOrPluginProjectPermission perm);
 
     /**
      * Filter a map of references by visibility.
@@ -356,6 +341,21 @@
     public abstract Map<String, Ref> filter(
         Map<String, Ref> refs, Repository repo, RefFilterOptions opts)
         throws PermissionBackendException;
+
+    /**
+     * Filter a list of references by visibility.
+     *
+     * @param refs a list of references to filter.
+     * @param repo an open {@link Repository} handle for this instance's project
+     * @param opts further options for filtering.
+     * @return a partition of the provided refs that are visible to the user that this instance is
+     *     scoped to.
+     * @throws PermissionBackendException if failure consulting backend configuration.
+     */
+    public Map<String, Ref> filter(List<Ref> refs, Repository repo, RefFilterOptions opts)
+        throws PermissionBackendException {
+      return filter(refs.stream().collect(toMap(Ref::getName, r -> r, (a, b) -> b)), repo, opts);
+    }
   }
 
   /** Options for filtering refs using {@link ForProject}. */
@@ -364,22 +364,25 @@
     /** Remove all NoteDb refs (refs/changes/*, refs/users/*, edit refs) from the result. */
     public abstract boolean filterMeta();
 
-    /** Separately add reachable tags. */
-    public abstract boolean filterTagsSeparately();
+    /**
+     * Select only refs with names matching prefixes per {@link
+     * org.eclipse.jgit.lib.RefDatabase#getRefsByPrefix}.
+     */
+    public abstract ImmutableList<String> prefixes();
 
     public abstract Builder toBuilder();
 
     public static Builder builder() {
       return new AutoValue_PermissionBackend_RefFilterOptions.Builder()
           .setFilterMeta(false)
-          .setFilterTagsSeparately(false);
+          .setPrefixes(Collections.singletonList(""));
     }
 
     @AutoValue.Builder
     public abstract static class Builder {
       public abstract Builder setFilterMeta(boolean val);
 
-      public abstract Builder setFilterTagsSeparately(boolean val);
+      public abstract Builder setPrefixes(List<String> prefixes);
 
       public abstract RefFilterOptions build();
     }
@@ -390,7 +393,7 @@
   }
 
   /** PermissionBackend scoped to a user, project and reference. */
-  public abstract static class ForRef extends AcceptsReviewDb<ForRef> {
+  public abstract static class ForRef {
     /** Returns a fully qualified resource path that this instance is scoped to. */
     public abstract String resourcePath();
 
@@ -440,7 +443,7 @@
   }
 
   /** PermissionBackend scoped to a user, project, reference and change. */
-  public abstract static class ForChange extends AcceptsReviewDb<ForChange> {
+  public abstract static class ForChange {
     /** Returns the fully qualified resource path that this instance is scoped to. */
     public abstract String resourcePath();
 
diff --git a/java/com/google/gerrit/server/permissions/PermissionBackendCondition.java b/java/com/google/gerrit/server/permissions/PermissionBackendCondition.java
index 1b6b087..a92e504 100644
--- a/java/com/google/gerrit/server/permissions/PermissionBackendCondition.java
+++ b/java/com/google/gerrit/server/permissions/PermissionBackendCondition.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.permissions;
 
+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.conditions.PrivateInternals_BooleanCondition;
@@ -100,10 +101,11 @@
 
   public static class ForProject extends PermissionBackendCondition {
     private final PermissionBackend.ForProject impl;
-    private final ProjectPermission perm;
+    private final CoreOrPluginProjectPermission perm;
     private final CurrentUser user;
 
-    public ForProject(PermissionBackend.ForProject impl, ProjectPermission perm, CurrentUser user) {
+    public ForProject(
+        PermissionBackend.ForProject impl, CoreOrPluginProjectPermission perm, CurrentUser user) {
       this.impl = impl;
       this.perm = perm;
       this.user = user;
@@ -113,7 +115,7 @@
       return impl;
     }
 
-    public ProjectPermission permission() {
+    public CoreOrPluginProjectPermission permission() {
       return perm;
     }
 
diff --git a/java/com/google/gerrit/server/permissions/PluginPermissionsUtil.java b/java/com/google/gerrit/server/permissions/PluginPermissionsUtil.java
new file mode 100644
index 0000000..b147926
--- /dev/null
+++ b/java/com/google/gerrit/server/permissions/PluginPermissionsUtil.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.permissions;
+
+import static com.google.gerrit.extensions.api.access.PluginProjectPermission.PLUGIN_PERMISSION_NAME_PATTERN_STRING;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.config.CapabilityDefinition;
+import com.google.gerrit.extensions.config.PluginPermissionDefinition;
+import com.google.gerrit.extensions.config.PluginProjectPermissionDefinition;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.Extension;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.regex.Pattern;
+
+/** Utilities for plugin permissions. */
+@Singleton
+public final class PluginPermissionsUtil {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private static final String PLUGIN_NAME_PATTERN_STRING = "[a-zA-Z0-9-]+";
+
+  /**
+   * Name pattern for a plugin non-capability permission stored in the config file.
+   *
+   * <p>This pattern requires a plugin declared permission to have a name in the access section of
+   * {@code ProjectConfig} with a format like "plugin-{pluginName}-{permissionName}", which makes it
+   * easier to tell if a config name represents a plugin permission or not. Note "-" isn't clear
+   * enough for this purpose since some core permissions, e.g. "label-", also contain "-".
+   */
+  private static final Pattern PLUGIN_PERMISSION_NAME_IN_CONFIG_PATTERN =
+      Pattern.compile(
+          "^plugin-"
+              + PLUGIN_NAME_PATTERN_STRING
+              + "-"
+              + PLUGIN_PERMISSION_NAME_PATTERN_STRING
+              + "$");
+
+  /** Name pattern for a Gerrit plugin. */
+  private static final Pattern PLUGIN_NAME_PATTERN =
+      Pattern.compile("^" + PLUGIN_NAME_PATTERN_STRING + "$");
+
+  private final DynamicMap<CapabilityDefinition> capabilityDefinitions;
+  private final DynamicMap<PluginProjectPermissionDefinition> pluginProjectPermissionDefinitions;
+
+  @Inject
+  PluginPermissionsUtil(
+      DynamicMap<CapabilityDefinition> capabilityDefinitions,
+      DynamicMap<PluginProjectPermissionDefinition> pluginProjectPermissionDefinitions) {
+    this.capabilityDefinitions = capabilityDefinitions;
+    this.pluginProjectPermissionDefinitions = pluginProjectPermissionDefinitions;
+  }
+
+  /**
+   * Collects all the plugin declared capabilities.
+   *
+   * @return a map of plugin declared capabilities with "pluginName" as its keys and
+   *     "pluginName-{permissionName}" as its values.
+   */
+  public ImmutableMap<String, String> collectPluginCapabilities() {
+    return collectPermissions(capabilityDefinitions, "");
+  }
+
+  /**
+   * Collects all the plugin declared project permissions.
+   *
+   * @return a map of plugin declared project permissions with "{pluginName}" as its keys and
+   *     "plugin-{pluginName}-{permissionName}" as its values.
+   */
+  public ImmutableMap<String, String> collectPluginProjectPermissions() {
+    return collectPermissions(pluginProjectPermissionDefinitions, "plugin-");
+  }
+
+  private static <T extends PluginPermissionDefinition>
+      ImmutableMap<String, String> collectPermissions(DynamicMap<T> definitions, String prefix) {
+    ImmutableMap.Builder<String, String> permissionIdNames = ImmutableMap.builder();
+
+    for (Extension<T> extension : definitions) {
+      String pluginName = extension.getPluginName();
+      if (!PLUGIN_NAME_PATTERN.matcher(pluginName).matches()) {
+        logger.atWarning().log(
+            "Plugin name '%s' must match '%s' to use permissions; rename the plugin",
+            pluginName, PLUGIN_NAME_PATTERN.pattern());
+        continue;
+      }
+
+      String id = prefix + pluginName + "-" + extension.getExportName();
+      permissionIdNames.put(id, extension.get().getDescription());
+    }
+
+    return permissionIdNames.build();
+  }
+
+  /**
+   * Checks if a given name matches the plugin declared permission name pattern for configs.
+   *
+   * @param name a config name which may stand for a plugin permission.
+   * @return whether the name matches the plugin permission name pattern for configs.
+   */
+  public static boolean isValidPluginPermission(String name) {
+    return PLUGIN_PERMISSION_NAME_IN_CONFIG_PATTERN.matcher(name).matches();
+  }
+}
diff --git a/java/com/google/gerrit/server/permissions/ProjectControl.java b/java/com/google/gerrit/server/permissions/ProjectControl.java
index 0094979..dfc3339 100644
--- a/java/com/google/gerrit/server/permissions/ProjectControl.java
+++ b/java/com/google/gerrit/server/permissions/ProjectControl.java
@@ -16,13 +16,17 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.gerrit.common.data.AccessSection.ALL;
-import static com.google.gerrit.common.data.RefConfigSection.REGEX_PREFIX;
+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.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.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;
@@ -30,7 +34,6 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.config.GitReceivePackGroups;
@@ -44,12 +47,10 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.SectionMatcher;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -101,9 +102,9 @@
     return new ForProjectImpl();
   }
 
-  ChangeControl controlFor(ReviewDb db, Change change) throws OrmException {
+  ChangeControl controlFor(Change change) {
     return changeControlFactory.create(
-        controlForRef(change.getDest()), db, change.getProject(), change.getId());
+        controlForRef(change.getDest()), change.getProject(), change.getId());
   }
 
   ChangeControl controlFor(ChangeNotes notes) {
@@ -349,7 +350,7 @@
 
     @Override
     public ForRef ref(String ref) {
-      return controlForRef(ref).asForRef().database(db);
+      return controlForRef(ref).asForRef();
     }
 
     @Override
@@ -357,7 +358,7 @@
       try {
         checkProject(cd.change());
         return super.change(cd);
-      } catch (OrmException e) {
+      } catch (StorageException e) {
         return FailedPermissionBackend.change("unavailable", e);
       }
     }
@@ -378,17 +379,18 @@
     }
 
     @Override
-    public void check(ProjectPermission perm) throws AuthException, PermissionBackendException {
+    public void check(CoreOrPluginProjectPermission perm)
+        throws AuthException, PermissionBackendException {
       if (!can(perm)) {
         throw new AuthException(perm.describeForException() + " not permitted");
       }
     }
 
     @Override
-    public Set<ProjectPermission> test(Collection<ProjectPermission> permSet)
+    public <T extends CoreOrPluginProjectPermission> Set<T> test(Collection<T> permSet)
         throws PermissionBackendException {
-      EnumSet<ProjectPermission> ok = EnumSet.noneOf(ProjectPermission.class);
-      for (ProjectPermission perm : permSet) {
+      Set<T> ok = Sets.newHashSetWithExpectedSize(permSet.size());
+      for (T perm : permSet) {
         if (can(perm)) {
           ok.add(perm);
         }
@@ -397,7 +399,7 @@
     }
 
     @Override
-    public BooleanCondition testCond(ProjectPermission perm) {
+    public BooleanCondition testCond(CoreOrPluginProjectPermission perm) {
       return new PermissionBackendCondition.ForProject(this, perm, getUser());
     }
 
@@ -410,6 +412,17 @@
       return refFilter.filter(refs, repo, opts);
     }
 
+    private boolean can(CoreOrPluginProjectPermission perm) throws PermissionBackendException {
+      if (perm instanceof ProjectPermission) {
+        return can((ProjectPermission) perm);
+      } else if (perm instanceof PluginProjectPermission) {
+        // TODO(xchangcheng): implement for plugin defined project permissions.
+        return false;
+      }
+
+      throw new PermissionBackendException(perm.describeForException() + " unsupported");
+    }
+
     private boolean can(ProjectPermission perm) throws PermissionBackendException {
       switch (perm) {
         case ACCESS:
diff --git a/java/com/google/gerrit/server/permissions/ProjectPermission.java b/java/com/google/gerrit/server/permissions/ProjectPermission.java
index ca04f3b..653303a 100644
--- a/java/com/google/gerrit/server/permissions/ProjectPermission.java
+++ b/java/com/google/gerrit/server/permissions/ProjectPermission.java
@@ -16,10 +16,11 @@
 
 import static java.util.Objects.requireNonNull;
 
+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 GerritPermission {
+public enum ProjectPermission implements CoreOrPluginProjectPermission {
   /**
    * Can access at least one reference or change within the repository.
    *
diff --git a/java/com/google/gerrit/server/permissions/RefControl.java b/java/com/google/gerrit/server/permissions/RefControl.java
index 74b04a3..9a2ecdd 100644
--- a/java/com/google/gerrit/server/permissions/RefControl.java
+++ b/java/com/google/gerrit/server/permissions/RefControl.java
@@ -21,6 +21,7 @@
 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.exceptions.StorageException;
 import com.google.gerrit.extensions.conditions.BooleanCondition;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Change;
@@ -33,8 +34,6 @@
 import com.google.gerrit.server.permissions.PermissionBackend.ForRef;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.util.MagicBranch;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.util.Providers;
 import java.util.Collection;
 import java.util.EnumSet;
 import java.util.List;
@@ -197,7 +196,6 @@
       case GIT:
         return false;
 
-      case JSON_RPC:
       case REST_API:
       case SSH_COMMAND:
       case UNKNOWN:
@@ -229,7 +227,6 @@
       case GIT:
         return canPushWithForce() || canPerform(Permission.DELETE);
 
-      case JSON_RPC:
       case REST_API:
       case SSH_COMMAND:
       case UNKNOWN:
@@ -444,10 +441,8 @@
     public ForChange change(ChangeData cd) {
       try {
         // TODO(hiesel) Force callers to call database() and use db instead of cd.db()
-        return getProjectControl()
-            .controlFor(cd.db(), cd.change())
-            .asForChange(cd, Providers.of(cd.db()));
-      } catch (OrmException e) {
+        return getProjectControl().controlFor(cd.change()).asForChange(cd);
+      } catch (StorageException e) {
         return FailedPermissionBackend.change("unavailable", e);
       }
     }
@@ -461,12 +456,12 @@
           "expected change in project %s, not %s",
           project,
           change.getProject());
-      return getProjectControl().controlFor(notes).asForChange(null, db);
+      return getProjectControl().controlFor(notes).asForChange(null);
     }
 
     @Override
     public ForChange indexedChange(ChangeData cd, ChangeNotes notes) {
-      return getProjectControl().controlFor(notes).asForChange(cd, db);
+      return getProjectControl().controlFor(notes).asForChange(cd);
     }
 
     @Override
diff --git a/java/com/google/gerrit/server/plugins/CopyConfigModule.java b/java/com/google/gerrit/server/plugins/CopyConfigModule.java
index 9f937e6..090d257 100644
--- a/java/com/google/gerrit/server/plugins/CopyConfigModule.java
+++ b/java/com/google/gerrit/server/plugins/CopyConfigModule.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.plugins;
 
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.GerritPersonIdentProvider;
 import com.google.gerrit.server.config.AnonymousCowardName;
@@ -24,7 +23,6 @@
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.securestore.SecureStore;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Provides;
@@ -73,13 +71,6 @@
     return gerritServerConfig;
   }
 
-  @Inject private SchemaFactory<ReviewDb> schemaFactory;
-
-  @Provides
-  SchemaFactory<ReviewDb> getSchemaFactory() {
-    return schemaFactory;
-  }
-
   @Inject private GitRepositoryManager gitRepositoryManager;
 
   @Provides
diff --git a/java/com/google/gerrit/server/plugins/JarScanner.java b/java/com/google/gerrit/server/plugins/JarScanner.java
index 1a9b859..0f87135 100644
--- a/java/com/google/gerrit/server/plugins/JarScanner.java
+++ b/java/com/google/gerrit/server/plugins/JarScanner.java
@@ -114,7 +114,7 @@
     for (Class<? extends Annotation> annotoation : annotations) {
       String descr = classObjToClassDescr.get(annotoation);
       Collection<ClassData> discoverdData = rawMap.get(descr);
-      Collection<ClassData> values = firstNonNull(discoverdData, Collections.<ClassData>emptySet());
+      Collection<ClassData> values = firstNonNull(discoverdData, Collections.emptySet());
 
       result.put(
           annotoation,
@@ -144,7 +144,7 @@
         continue;
       }
 
-      ClassData def = new ClassData(Collections.<String>emptySet());
+      ClassData def = new ClassData(Collections.emptySet());
       try {
         new ClassReader(read(jarFile, entry)).accept(def, SKIP_ALL);
       } catch (RuntimeException err) {
@@ -195,7 +195,7 @@
     Collection<String> exports;
 
     private ClassData(Collection<String> exports) {
-      super(Opcodes.ASM6);
+      super(Opcodes.ASM7);
       this.exports = exports;
     }
 
@@ -263,7 +263,7 @@
 
   private abstract static class AbstractAnnotationVisitor extends AnnotationVisitor {
     AbstractAnnotationVisitor() {
-      super(Opcodes.ASM6);
+      super(Opcodes.ASM7);
     }
 
     @Override
@@ -331,13 +331,6 @@
     if (attributes == null) {
       return Collections.emptyMap();
     }
-    return Maps.transformEntries(
-        attributes,
-        new Maps.EntryTransformer<Object, Object, String>() {
-          @Override
-          public String transformEntry(Object key, Object value) {
-            return (String) value;
-          }
-        });
+    return Maps.transformEntries(attributes, (key, value) -> (String) value);
   }
 }
diff --git a/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java b/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
index ed9d2c7..c032c46 100644
--- a/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
+++ b/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
@@ -23,6 +23,7 @@
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.extensions.annotations.RootRelative;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.registration.DynamicItem;
@@ -226,7 +227,8 @@
     return httpModule != null;
   }
 
-  Module getHttpModule() {
+  @UsedAt(UsedAt.Project.GOOGLE)
+  public Module getHttpModule() {
     return httpModule;
   }
 
diff --git a/java/com/google/gerrit/server/plugins/PluginLoader.java b/java/com/google/gerrit/server/plugins/PluginLoader.java
index 57e7e49..9279f0fe 100644
--- a/java/com/google/gerrit/server/plugins/PluginLoader.java
+++ b/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -55,7 +55,6 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
 import java.util.Queue;
 import java.util.Set;
 import java.util.TreeSet;
@@ -290,12 +289,7 @@
 
   private void removeStalePluginFiles() {
     DirectoryStream.Filter<Path> filter =
-        new DirectoryStream.Filter<Path>() {
-          @Override
-          public boolean accept(Path entry) throws IOException {
-            return entry.getFileName().toString().startsWith("plugin_");
-          }
-        };
+        entry -> entry.getFileName().toString().startsWith("plugin_");
     try (DirectoryStream<Path> files = Files.newDirectoryStream(tempDir, filter)) {
       for (Path file : files) {
         logger.atInfo().log("Removing stale plugin file: %s", file.toFile().getName());
@@ -436,20 +430,21 @@
     cleanInBackground();
   }
 
-  private void addAllEntries(Map<String, Path> from, TreeSet<Entry<String, Path>> to) {
-    Iterator<Entry<String, Path>> it = from.entrySet().iterator();
+  private void addAllEntries(Map<String, Path> from, TreeSet<Map.Entry<String, Path>> to) {
+    Iterator<Map.Entry<String, Path>> it = from.entrySet().iterator();
     while (it.hasNext()) {
-      Entry<String, Path> entry = it.next();
+      Map.Entry<String, Path> entry = it.next();
       to.add(new AbstractMap.SimpleImmutableEntry<>(entry.getKey(), entry.getValue()));
     }
   }
 
-  private TreeSet<Entry<String, Path>> jarsFirstSortedPluginsSet(Map<String, Path> activePlugins) {
-    TreeSet<Entry<String, Path>> sortedPlugins =
+  private TreeSet<Map.Entry<String, Path>> jarsFirstSortedPluginsSet(
+      Map<String, Path> activePlugins) {
+    TreeSet<Map.Entry<String, Path>> sortedPlugins =
         Sets.newTreeSet(
-            new Comparator<Entry<String, Path>>() {
+            new Comparator<Map.Entry<String, Path>>() {
               @Override
-              public int compare(Entry<String, Path> e1, Entry<String, Path> e2) {
+              public int compare(Map.Entry<String, Path> e1, Map.Entry<String, Path> e2) {
                 Path n1 = e1.getValue().getFileName();
                 Path n2 = e2.getValue().getFileName();
                 return ComparisonChain.start()
diff --git a/java/com/google/gerrit/server/plugins/PluginMetricMaker.java b/java/com/google/gerrit/server/plugins/PluginMetricMaker.java
index b1a01d3..b49a190 100644
--- a/java/com/google/gerrit/server/plugins/PluginMetricMaker.java
+++ b/java/com/google/gerrit/server/plugins/PluginMetricMaker.java
@@ -47,7 +47,7 @@
   public PluginMetricMaker(MetricMaker root, String prefix) {
     this.root = root;
     this.prefix = prefix.endsWith("/") ? prefix : prefix + "/";
-    cleanup = Collections.synchronizedSet(new HashSet<RegistrationHandle>());
+    cleanup = Collections.synchronizedSet(new HashSet<>());
   }
 
   @Override
@@ -160,12 +160,9 @@
   public RegistrationHandle newTrigger(Set<CallbackMetric<?>> metrics, Runnable trigger) {
     final RegistrationHandle handle = root.newTrigger(metrics, trigger);
     cleanup.add(handle);
-    return new RegistrationHandle() {
-      @Override
-      public void remove() {
-        handle.remove();
-        cleanup.remove(handle);
-      }
+    return () -> {
+      handle.remove();
+      cleanup.remove(handle);
     };
   }
 
diff --git a/java/com/google/gerrit/server/plugins/PluginUtil.java b/java/com/google/gerrit/server/plugins/PluginUtil.java
index 5e67e2c..d4110ca 100644
--- a/java/com/google/gerrit/server/plugins/PluginUtil.java
+++ b/java/com/google/gerrit/server/plugins/PluginUtil.java
@@ -34,17 +34,14 @@
       return ImmutableList.of();
     }
     DirectoryStream.Filter<Path> filter =
-        new DirectoryStream.Filter<Path>() {
-          @Override
-          public boolean accept(Path entry) throws IOException {
-            String n = entry.getFileName().toString();
-            boolean accept =
-                !n.startsWith(".last_") && !n.startsWith(".next_") && Files.isRegularFile(entry);
-            if (!Strings.isNullOrEmpty(suffix)) {
-              accept &= n.endsWith(suffix);
-            }
-            return accept;
+        entry -> {
+          String n = entry.getFileName().toString();
+          boolean accept =
+              !n.startsWith(".last_") && !n.startsWith(".next_") && Files.isRegularFile(entry);
+          if (!Strings.isNullOrEmpty(suffix)) {
+            accept &= n.endsWith(suffix);
           }
+          return accept;
         };
     try (DirectoryStream<Path> files = Files.newDirectoryStream(pluginsDir, filter)) {
       return Ordering.natural().sortedCopy(files);
diff --git a/java/com/google/gerrit/server/plugins/UniversalServerPluginProvider.java b/java/com/google/gerrit/server/plugins/UniversalServerPluginProvider.java
index 4d89482..22cd84c 100644
--- a/java/com/google/gerrit/server/plugins/UniversalServerPluginProvider.java
+++ b/java/com/google/gerrit/server/plugins/UniversalServerPluginProvider.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.plugins;
 
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.registration.PluginName;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.nio.file.Path;
@@ -28,10 +28,10 @@
 class UniversalServerPluginProvider implements ServerPluginProvider {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  private final DynamicSet<ServerPluginProvider> serverPluginProviders;
+  private final PluginSetContext<ServerPluginProvider> serverPluginProviders;
 
   @Inject
-  UniversalServerPluginProvider(DynamicSet<ServerPluginProvider> sf) {
+  UniversalServerPluginProvider(PluginSetContext<ServerPluginProvider> sf) {
     this.serverPluginProviders = sf;
   }
 
@@ -80,15 +80,16 @@
 
   private List<ServerPluginProvider> providersForHandlingPlugin(Path srcPath) {
     List<ServerPluginProvider> providers = new ArrayList<>();
-    for (ServerPluginProvider serverPluginProvider : serverPluginProviders) {
-      boolean handles = serverPluginProvider.handles(srcPath);
-      logger.atFine().log(
-          "File %s handled by %s ? => %s",
-          srcPath, serverPluginProvider.getProviderPluginName(), handles);
-      if (handles) {
-        providers.add(serverPluginProvider);
-      }
-    }
+    serverPluginProviders.runEach(
+        serverPluginProvider -> {
+          boolean handles = serverPluginProvider.handles(srcPath);
+          logger.atFine().log(
+              "File %s handled by %s ? => %s",
+              srcPath, serverPluginProvider.getProviderPluginName(), handles);
+          if (handles) {
+            providers.add(serverPluginProvider);
+          }
+        });
     return providers;
   }
 }
diff --git a/java/com/google/gerrit/server/project/ContributorAgreementsChecker.java b/java/com/google/gerrit/server/project/ContributorAgreementsChecker.java
index 84b8af3..b33fcb5 100644
--- a/java/com/google/gerrit/server/project/ContributorAgreementsChecker.java
+++ b/java/com/google/gerrit/server/project/ContributorAgreementsChecker.java
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.server.project;
 
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.Objects.requireNonNull;
+
 import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.data.PermissionRule.Action;
@@ -35,6 +38,8 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
 
 @Singleton
 public class ContributorAgreementsChecker {
@@ -94,6 +99,20 @@
       List<AccountGroup.UUID> groupIds;
       groupIds = okGroupIds;
 
+      // matchProjects defaults to match all projects when missing.
+      List<String> matchProjectsRegexes = ca.getMatchProjectsRegexes();
+      if (!matchProjectsRegexes.isEmpty()
+          && !projectMatchesAnyPattern(project.get(), matchProjectsRegexes)) {
+        // Doesn't match, isn't checked.
+        continue;
+      }
+      // excludeProjects defaults to exclude no projects when missing.
+      List<String> excludeProjectsRegexes = ca.getExcludeProjectsRegexes();
+      if (!excludeProjectsRegexes.isEmpty()
+          && projectMatchesAnyPattern(project.get(), excludeProjectsRegexes)) {
+        // Matches, isn't checked.
+        continue;
+      }
       for (PermissionRule rule : ca.getAccepted()) {
         if ((rule.getAction() == Action.ALLOW)
             && (rule.getGroup() != null)
@@ -103,7 +122,7 @@
       }
     }
 
-    if (!iUser.getEffectiveGroups().containsAnyOf(okGroupIds)) {
+    if (!okGroupIds.isEmpty() && !iUser.getEffectiveGroups().containsAnyOf(okGroupIds)) {
       final StringBuilder msg = new StringBuilder();
       msg.append("No Contributor Agreement on file for user ")
           .append(iUser.getNameEmail())
@@ -115,4 +134,23 @@
       throw new AuthException(msg.toString());
     }
   }
+
+  private boolean projectMatchesAnyPattern(String projectName, List<String> regexes) {
+    requireNonNull(regexes);
+    checkArgument(!regexes.isEmpty());
+    for (String patternString : regexes) {
+      Pattern pattern;
+      try {
+        pattern = Pattern.compile(patternString);
+      } catch (PatternSyntaxException e) {
+        // Should never happen: Regular expressions validated when reading project.config.
+        throw new IllegalStateException(
+            "Invalid matchProjects or excludeProjects clause in project.config", e);
+      }
+      if (pattern.matcher(projectName).find()) {
+        return true;
+      }
+    }
+    return false;
+  }
 }
diff --git a/java/com/google/gerrit/server/project/CreateProjectArgs.java b/java/com/google/gerrit/server/project/CreateProjectArgs.java
index a68bd84..df31c19 100644
--- a/java/com/google/gerrit/server/project/CreateProjectArgs.java
+++ b/java/com/google/gerrit/server/project/CreateProjectArgs.java
@@ -49,6 +49,7 @@
     enableSignedPush = InheritableBoolean.INHERIT;
     requireSignedPush = InheritableBoolean.INHERIT;
     submitType = SubmitType.MERGE_IF_NECESSARY;
+    rejectEmptyCommit = InheritableBoolean.INHERIT;
   }
 
   public Project.NameKey getProject() {
diff --git a/java/com/google/gerrit/server/project/CreateRefControl.java b/java/com/google/gerrit/server/project/CreateRefControl.java
index f89e298..34f3c33 100644
--- a/java/com/google/gerrit/server/project/CreateRefControl.java
+++ b/java/com/google/gerrit/server/project/CreateRefControl.java
@@ -27,6 +27,7 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
+import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -121,7 +122,7 @@
    */
   private void checkCreateCommit(
       Repository repo, RevCommit commit, Project.NameKey project, PermissionBackend.ForRef forRef)
-      throws AuthException, PermissionBackendException {
+      throws AuthException, PermissionBackendException, IOException {
     try {
       // If the user has update (push) permission, they can create the ref regardless
       // of whether they are pushing any new objects along with the create.
@@ -130,7 +131,11 @@
     } catch (AuthException denied) {
       // Fall through to check reachability.
     }
-    if (reachable.fromHeadsOrTags(project, repo, commit)) {
+    if (reachable.fromRefs(
+        project,
+        repo,
+        commit,
+        repo.getRefDatabase().getRefsByPrefix(Constants.R_HEADS, Constants.R_TAGS))) {
       // If the user has no push permissions, check whether the object is
       // merged into a branch or tag readable by this user. If so, they are
       // not effectively "pushing" more objects, so they can create the ref
@@ -138,9 +143,15 @@
       return;
     }
 
-    throw new AuthException(
+    AuthException e =
+        new AuthException(
+            String.format(
+                "%s for creating new commit object not permitted",
+                RefPermission.UPDATE.describeForException()));
+    e.setAdvice(
         String.format(
-            "%s for creating new commit object not permitted",
+            "use a SHA1 visible to you, or get %s permission on the ref",
             RefPermission.UPDATE.describeForException()));
+    throw e;
   }
 }
diff --git a/java/com/google/gerrit/server/project/NoSuchChangeException.java b/java/com/google/gerrit/server/project/NoSuchChangeException.java
index 7946a3a..6f65659 100644
--- a/java/com/google/gerrit/server/project/NoSuchChangeException.java
+++ b/java/com/google/gerrit/server/project/NoSuchChangeException.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gwtorm.server.OrmException;
 
 /** Indicates the change does not exist. */
-public class NoSuchChangeException extends OrmException {
+public class NoSuchChangeException extends StorageException {
   private static final long serialVersionUID = 1L;
 
   public NoSuchChangeException(Change.Id key) {
diff --git a/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index 8ddeea3..4a85554 100644
--- a/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -26,6 +26,10 @@
 import com.google.common.flogger.FluentLogger;
 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;
@@ -90,6 +94,7 @@
   private final Lock listLock;
   private final ProjectCacheClock clock;
   private final Provider<ProjectIndexer> indexer;
+  private final Timer0 guessRelevantGroupsLatency;
 
   @Inject
   ProjectCacheImpl(
@@ -98,7 +103,8 @@
       @Named(CACHE_NAME) LoadingCache<String, ProjectState> byName,
       @Named(CACHE_LIST) LoadingCache<ListKey, ImmutableSortedSet<Project.NameKey>> list,
       ProjectCacheClock clock,
-      Provider<ProjectIndexer> indexer) {
+      Provider<ProjectIndexer> indexer,
+      MetricMaker metricMaker) {
     this.allProjectsName = allProjectsName;
     this.allUsersName = allUsersName;
     this.byName = byName;
@@ -106,6 +112,13 @@
     this.listLock = new ReentrantLock(true /* fair */);
     this.clock = clock;
     this.indexer = indexer;
+
+    this.guessRelevantGroupsLatency =
+        metricMaker.newTimer(
+            "group/guess_relevant_groups_latency",
+            new Description("Latency for guessing relevant groups")
+                .setCumulative()
+                .setUnit(Units.NANOSECONDS));
   }
 
   @Override
@@ -235,14 +248,16 @@
 
   @Override
   public Set<AccountGroup.UUID> guessRelevantGroupUUIDs() {
-    return all().stream()
-        .map(n -> byName.getIfPresent(n.get()))
-        .filter(Objects::nonNull)
-        .flatMap(p -> p.getConfig().getAllGroupUUIDs().stream())
-        // getAllGroupUUIDs shouldn't really return null UUIDs, but harden
-        // against them just in case there is a bug or corner case.
-        .filter(id -> id != null && id.get() != null)
-        .collect(toSet());
+    try (Timer0.Context ignored = guessRelevantGroupsLatency.start()) {
+      return all().stream()
+          .map(n -> byName.getIfPresent(n.get()))
+          .filter(Objects::nonNull)
+          .flatMap(p -> p.getConfig().getAllGroupUUIDs().stream())
+          // getAllGroupUUIDs shouldn't really return null UUIDs, but harden
+          // against them just in case there is a bug or corner case.
+          .filter(id -> id != null && id.get() != null)
+          .collect(toSet());
+    }
   }
 
   @Override
@@ -262,12 +277,18 @@
     private final ProjectState.Factory projectStateFactory;
     private final GitRepositoryManager mgr;
     private final ProjectCacheClock clock;
+    private final ProjectConfig.Factory projectConfigFactory;
 
     @Inject
-    Loader(ProjectState.Factory psf, GitRepositoryManager g, ProjectCacheClock clock) {
+    Loader(
+        ProjectState.Factory psf,
+        GitRepositoryManager g,
+        ProjectCacheClock clock,
+        ProjectConfig.Factory projectConfigFactory) {
       projectStateFactory = psf;
       mgr = g;
       this.clock = clock;
+      this.projectConfigFactory = projectConfigFactory;
     }
 
     @Override
@@ -276,7 +297,7 @@
         long now = clock.read();
         Project.NameKey key = new Project.NameKey(projectName);
         try (Repository git = mgr.openRepository(key)) {
-          ProjectConfig cfg = new ProjectConfig(key);
+          ProjectConfig cfg = projectConfigFactory.create(key);
           cfg.load(key, git);
 
           ProjectState state = projectStateFactory.create(cfg);
diff --git a/java/com/google/gerrit/server/project/ProjectConfig.java b/java/com/google/gerrit/server/project/ProjectConfig.java
index 9ac1e54..a7133bb 100644
--- a/java/com/google/gerrit/server/project/ProjectConfig.java
+++ b/java/com/google/gerrit/server/project/ProjectConfig.java
@@ -18,6 +18,7 @@
 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.server.permissions.PluginPermissionsUtil.isValidPluginPermission;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.CharMatcher;
@@ -28,6 +29,7 @@
 import com.google.common.collect.Maps;
 import com.google.common.primitives.Shorts;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.data.GlobalCapability;
@@ -39,9 +41,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.common.data.RefConfigSection;
 import com.google.gerrit.common.data.SubscribeSection;
-import com.google.gerrit.common.errors.InvalidNameException;
+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;
@@ -50,16 +51,19 @@
 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.UsedAt;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
+import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.BranchOrderSection;
 import com.google.gerrit.server.git.NotifyConfig;
 import com.google.gerrit.server.git.ValidationError;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.git.meta.VersionedMetaData;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -72,7 +76,6 @@
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
-import java.util.Map.Entry;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
@@ -83,8 +86,11 @@
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.eclipse.jgit.transport.RefSpec;
+import org.eclipse.jgit.util.FS;
 
 public class ProjectConfig extends VersionedMetaData implements ValidationError.Sink {
   public static final String COMMENTLINK = "commentlink";
@@ -128,6 +134,8 @@
   private static final String KEY_ACCEPTED = "accepted";
   private static final String KEY_AUTO_VERIFY = "autoVerify";
   private static final String KEY_AGREEMENT_URL = "agreementUrl";
+  private static final String KEY_MATCH_PROJECTS = "matchProjects";
+  private static final String KEY_EXCLUDE_PROJECTS = "excludeProjects";
 
   private static final String NOTIFY = "notify";
   private static final String KEY_EMAIL = "email";
@@ -166,6 +174,59 @@
 
   private static final Pattern EXCLUSIVE_PERMISSIONS_SPLIT_PATTERN = Pattern.compile("[, \t]{1,}");
 
+  // Don't use an assisted factory, since instances created by an assisted factory retain references
+  // to their enclosing injector. Instances of ProjectConfig are cached for a long time in the
+  // ProjectCache, so this would retain lots more memory.
+  @Singleton
+  public static class Factory {
+    @Nullable
+    public static StoredConfig getBaseConfig(
+        SitePaths sitePaths, AllProjectsName allProjects, Project.NameKey projectName) {
+      return projectName.equals(allProjects)
+          // Delay loading till onLoad method.
+          ? new FileBasedConfig(
+              sitePaths.etc_dir.resolve(allProjects.get()).resolve(PROJECT_CONFIG).toFile(),
+              FS.DETECTED)
+          : null;
+    }
+
+    private final SitePaths sitePaths;
+    private final AllProjectsName allProjects;
+
+    @Inject
+    Factory(SitePaths sitePaths, AllProjectsName allProjects) {
+      this.sitePaths = sitePaths;
+      this.allProjects = allProjects;
+    }
+
+    public ProjectConfig create(Project.NameKey projectName) {
+      return new ProjectConfig(projectName, getBaseConfig(sitePaths, allProjects, projectName));
+    }
+
+    public ProjectConfig read(MetaDataUpdate update) throws IOException, ConfigInvalidException {
+      ProjectConfig r = create(update.getProjectName());
+      r.load(update);
+      return r;
+    }
+
+    public ProjectConfig read(MetaDataUpdate update, ObjectId id)
+        throws IOException, ConfigInvalidException {
+      ProjectConfig r = create(update.getProjectName());
+      r.load(update, id);
+      return r;
+    }
+
+    @UsedAt(UsedAt.Project.COLLABNET)
+    public ProjectConfig read(Repository repo, Project.NameKey name)
+        throws IOException, ConfigInvalidException {
+      ProjectConfig r = create(name);
+      r.load(repo);
+      return r;
+    }
+  }
+
+  private final StoredConfig baseConfig;
+
   private Project project;
   private AccountsSection accountsSection;
   private GroupList groupList;
@@ -176,7 +237,7 @@
   private Map<String, LabelType> labelSections;
   private ConfiguredMimeTypes mimeTypes;
   private Map<Project.NameKey, SubscribeSection> subscribeSections;
-  private List<CommentLinkInfoImpl> commentLinkSections;
+  private Map<String, CommentLinkInfoImpl> commentLinkSections;
   private List<ValidationError> validationErrors;
   private ObjectId rulesId;
   private long maxObjectSizeLimit;
@@ -187,28 +248,6 @@
   private Map<String, List<String>> extensionPanelSections;
   private Map<String, GroupReference> groupsByName;
 
-  public static ProjectConfig read(MetaDataUpdate update)
-      throws IOException, ConfigInvalidException {
-    ProjectConfig r = new ProjectConfig(update.getProjectName());
-    r.load(update);
-    return r;
-  }
-
-  public static ProjectConfig read(MetaDataUpdate update, ObjectId id)
-      throws IOException, ConfigInvalidException {
-    ProjectConfig r = new ProjectConfig(update.getProjectName());
-    r.load(update, id);
-    return r;
-  }
-
-  @UsedAt(UsedAt.Project.COLLABNET)
-  public static ProjectConfig read(Repository repo, Project.NameKey name)
-      throws IOException, ConfigInvalidException {
-    ProjectConfig r = new ProjectConfig(name);
-    r.load(repo);
-    return r;
-  }
-
   public static CommentLinkInfoImpl buildCommentLink(Config cfg, String name, boolean allowRaw)
       throws IllegalArgumentException {
     String match = cfg.getString(COMMENTLINK, name, KEY_MATCH);
@@ -247,11 +286,12 @@
   }
 
   public void addCommentLinkSection(CommentLinkInfoImpl commentLink) {
-    commentLinkSections.add(commentLink);
+    commentLinkSections.put(commentLink.name, commentLink);
   }
 
-  public ProjectConfig(Project.NameKey projectName) {
+  private ProjectConfig(Project.NameKey projectName, @Nullable StoredConfig baseConfig) {
     this.projectName = projectName;
+    this.baseConfig = baseConfig;
   }
 
   public void load(Repository repo) throws IOException, ConfigInvalidException {
@@ -328,7 +368,7 @@
       String name = section.getName();
       if (sectionsWithUnknownPermissions.contains(name)) {
         AccessSection a = accessSections.get(name);
-        a.setPermissions(new ArrayList<Permission>());
+        a.setPermissions(new ArrayList<>());
       } else {
         accessSections.remove(name);
       }
@@ -424,7 +464,7 @@
   }
 
   public Collection<CommentLinkInfoImpl> getCommentLinkSections() {
-    return commentLinkSections;
+    return commentLinkSections.values();
   }
 
   public ConfiguredMimeTypes getMimeTypes() {
@@ -513,11 +553,14 @@
 
   @Override
   protected void onLoad() throws IOException, ConfigInvalidException {
+    if (baseConfig != null) {
+      baseConfig.load();
+    }
     readGroupList();
     groupsByName = mapGroupReferences();
 
     rulesId = getObjectId("rules.pl");
-    Config rc = readConfig(PROJECT_CONFIG);
+    Config rc = readConfig(PROJECT_CONFIG, baseConfig);
     project = new Project(projectName);
 
     Project p = project;
@@ -602,6 +645,9 @@
       ca.setAgreementUrl(rc.getString(CONTRIBUTOR_AGREEMENT, name, KEY_AGREEMENT_URL));
       ca.setAccepted(
           loadPermissionRules(rc, CONTRIBUTOR_AGREEMENT, name, KEY_ACCEPTED, groupsByName, false));
+      ca.setExcludeProjectsRegexes(
+          loadPatterns(rc, CONTRIBUTOR_AGREEMENT, name, KEY_EXCLUDE_PROJECTS));
+      ca.setMatchProjectsRegexes(loadPatterns(rc, CONTRIBUTOR_AGREEMENT, name, KEY_MATCH_PROJECTS));
 
       List<PermissionRule> rules =
           loadPermissionRules(
@@ -703,13 +749,13 @@
     accessSections = new HashMap<>();
     sectionsWithUnknownPermissions = new HashSet<>();
     for (String refName : rc.getSubsections(ACCESS)) {
-      if (RefConfigSection.isValid(refName) && isValidRegex(refName)) {
+      if (AccessSection.isValidRefSectionName(refName) && isValidRegex(refName)) {
         AccessSection as = getAccessSection(refName, true);
 
         for (String varName : rc.getStringList(ACCESS, refName, KEY_GROUP_PERMISSIONS)) {
           for (String n : Splitter.on(EXCLUSIVE_PERMISSIONS_SPLIT_PATTERN).split(varName)) {
             n = convertLegacyPermission(n);
-            if (isPermission(n)) {
+            if (isCoreOrPluginPermission(n)) {
               as.getPermission(n, true).setExclusiveGroup(true);
             }
           }
@@ -717,7 +763,7 @@
 
         for (String varName : rc.getNames(ACCESS, refName)) {
           String convertedName = convertLegacyPermission(varName);
-          if (isPermission(convertedName)) {
+          if (isCoreOrPluginPermission(convertedName)) {
             Permission perm = as.getPermission(convertedName, true);
             loadPermissionRules(
                 rc,
@@ -746,6 +792,12 @@
     }
   }
 
+  private boolean isCoreOrPluginPermission(String permission) {
+    // Since plugins are loaded dynamically, here we can't load all plugin permissions and verify
+    // their existence.
+    return isPermission(permission) || isValidPluginPermission(permission);
+  }
+
   private boolean isValidRegex(String refPattern) {
     try {
       RefPattern.validateRegExp(refPattern);
@@ -762,6 +814,22 @@
     }
   }
 
+  private ImmutableList<String> loadPatterns(
+      Config rc, String section, String subsection, String varName) {
+    ImmutableList.Builder<String> patterns = ImmutableList.builder();
+    for (String patternString : rc.getStringList(section, subsection, varName)) {
+      try {
+        // While one could just use getStringList directly, compiling first will cause the server
+        // to fail fast if any of the patterns are invalid.
+        patterns.add(Pattern.compile(patternString).pattern());
+      } catch (PatternSyntaxException e) {
+        error(new ValidationError(PROJECT_CONFIG, "Invalid regular expression: " + e.getMessage()));
+        continue;
+      }
+    }
+    return patterns.build();
+  }
+
   private ImmutableList<PermissionRule> loadPermissionRules(
       Config rc,
       String section,
@@ -947,10 +1015,10 @@
 
   private void loadCommentLinkSections(Config rc) {
     Set<String> subsections = rc.getSubsections(COMMENTLINK);
-    commentLinkSections = new ArrayList<>(subsections.size());
+    commentLinkSections = new LinkedHashMap<>(subsections.size());
     for (String name : subsections) {
       try {
-        commentLinkSections.add(buildCommentLink(rc, name, false));
+        commentLinkSections.put(name, buildCommentLink(rc, name, false));
       } catch (PatternSyntaxException e) {
         error(
             new ValidationError(
@@ -1137,7 +1205,7 @@
 
   private void saveCommentLinkSections(Config rc) {
     if (commentLinkSections != null) {
-      for (CommentLinkInfoImpl cm : commentLinkSections) {
+      for (CommentLinkInfoImpl cm : commentLinkSections.values()) {
         rc.setString(COMMENTLINK, cm.name, KEY_MATCH, cm.match);
         if (!Strings.isNullOrEmpty(cm.html)) {
           rc.setString(COMMENTLINK, cm.name, KEY_HTML, cm.html);
@@ -1172,13 +1240,23 @@
           ca.getName(),
           KEY_ACCEPTED,
           ruleToStringList(ca.getAccepted(), keepGroups));
+      rc.setStringList(
+          CONTRIBUTOR_AGREEMENT,
+          ca.getName(),
+          KEY_EXCLUDE_PROJECTS,
+          patternToStringList(ca.getExcludeProjectsRegexes()));
+      rc.setStringList(
+          CONTRIBUTOR_AGREEMENT,
+          ca.getName(),
+          KEY_MATCH_PROJECTS,
+          patternToStringList(ca.getMatchProjectsRegexes()));
     }
   }
 
   private void saveNotifySections(Config rc, Set<AccountGroup.UUID> keepGroups) {
     for (NotifyConfig nc : sort(notifySections.values())) {
       nc.getGroups().stream()
-          .map(gr -> gr.getUUID())
+          .map(GroupReference::getUUID)
           .filter(Objects::nonNull)
           .forEach(keepGroups::add);
       List<String> email =
@@ -1213,6 +1291,10 @@
     }
   }
 
+  private List<String> patternToStringList(List<String> list) {
+    return list;
+  }
+
   private List<String> ruleToStringList(
       List<PermissionRule> list, Set<AccountGroup.UUID> keepGroups) {
     List<String> rules = new ArrayList<>();
@@ -1290,7 +1372,7 @@
       }
 
       for (String varName : rc.getNames(ACCESS, refName)) {
-        if (isPermission(convertLegacyPermission(varName))
+        if (isCoreOrPluginPermission(convertLegacyPermission(varName))
             && !have.contains(varName.toLowerCase())) {
           rc.unset(ACCESS, refName, varName);
         }
@@ -1298,7 +1380,7 @@
     }
 
     for (String name : rc.getSubsections(ACCESS)) {
-      if (RefConfigSection.isValid(name) && !accessSections.containsKey(name)) {
+      if (AccessSection.isValidRefSectionName(name) && !accessSections.containsKey(name)) {
         rc.unsetSection(ACCESS, name);
       }
     }
@@ -1411,7 +1493,7 @@
       rc.unsetSection(PLUGIN, name);
     }
 
-    for (Entry<String, Config> e : pluginConfigs.entrySet()) {
+    for (Map.Entry<String, Config> e : pluginConfigs.entrySet()) {
       String plugin = e.getKey();
       Config pluginConfig = e.getValue();
       for (String name : pluginConfig.getNames(PLUGIN, plugin)) {
@@ -1473,6 +1555,7 @@
     return m.stream().sorted().collect(toImmutableList());
   }
 
+  @UsedAt(UsedAt.Project.GOOGLE)
   public boolean hasLegacyPermissions() {
     return hasLegacyPermissions;
   }
diff --git a/java/com/google/gerrit/server/project/ProjectCreator.java b/java/com/google/gerrit/server/project/ProjectCreator.java
new file mode 100644
index 0000000..35ecd7c
--- /dev/null
+++ b/java/com/google/gerrit/server/project/ProjectCreator.java
@@ -0,0 +1,256 @@
+// 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.project;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.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;
+import com.google.gerrit.server.config.RepositoryConfig;
+import com.google.gerrit.server.extensions.events.AbstractNoNotifyEvent;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.RepositoryCaseMismatchException;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.io.IOException;
+import java.util.List;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+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.PersonIdent;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.RefUpdate.Result;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+public class ProjectCreator {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final GitRepositoryManager repoManager;
+  private final PluginSetContext<NewProjectCreatedListener> createdListeners;
+  private final ProjectCache projectCache;
+  private final GroupBackend groupBackend;
+  private final MetaDataUpdate.User metaDataUpdateFactory;
+  private final GitReferenceUpdated referenceUpdated;
+  private final RepositoryConfig repositoryCfg;
+  private final Provider<PersonIdent> serverIdent;
+  private final Provider<IdentifiedUser> identifiedUser;
+  private final ProjectConfig.Factory projectConfigFactory;
+
+  @Inject
+  ProjectCreator(
+      GitRepositoryManager repoManager,
+      PluginSetContext<NewProjectCreatedListener> createdListeners,
+      ProjectCache projectCache,
+      GroupBackend groupBackend,
+      MetaDataUpdate.User metaDataUpdateFactory,
+      GitReferenceUpdated referenceUpdated,
+      RepositoryConfig repositoryCfg,
+      @GerritPersonIdent Provider<PersonIdent> serverIdent,
+      Provider<IdentifiedUser> identifiedUser,
+      ProjectConfig.Factory projectConfigFactory) {
+    this.repoManager = repoManager;
+    this.createdListeners = createdListeners;
+    this.projectCache = projectCache;
+    this.groupBackend = groupBackend;
+    this.metaDataUpdateFactory = metaDataUpdateFactory;
+    this.referenceUpdated = referenceUpdated;
+    this.repositoryCfg = repositoryCfg;
+    this.serverIdent = serverIdent;
+    this.identifiedUser = identifiedUser;
+    this.projectConfigFactory = projectConfigFactory;
+  }
+
+  public ProjectState createProject(CreateProjectArgs args)
+      throws BadRequestException, ResourceConflictException, IOException, ConfigInvalidException {
+    final Project.NameKey nameKey = args.getProject();
+    try {
+      final String head = args.permissionsOnly ? RefNames.REFS_CONFIG : args.branch.get(0);
+      try (Repository repo = repoManager.openRepository(nameKey)) {
+        if (repo.getObjectDatabase().exists()) {
+          throw new ResourceConflictException("project \"" + nameKey + "\" exists");
+        }
+      } catch (RepositoryNotFoundException e) {
+        // It does not exist, safe to ignore.
+      }
+      try (Repository repo = repoManager.createRepository(nameKey)) {
+        RefUpdate u = repo.updateRef(Constants.HEAD);
+        u.disableRefLog();
+        u.link(head);
+
+        createProjectConfig(args);
+
+        if (!args.permissionsOnly && args.createEmptyCommit) {
+          createEmptyCommits(repo, nameKey, args.branch);
+        }
+
+        fire(nameKey, head);
+
+        return projectCache.get(nameKey);
+      }
+    } catch (RepositoryCaseMismatchException e) {
+      throw new ResourceConflictException(
+          "Cannot create "
+              + nameKey.get()
+              + " because the name is already occupied by another project."
+              + " The other project has the same name, only spelled in a"
+              + " different case.",
+          e);
+    } catch (RepositoryNotFoundException badName) {
+      throw new BadRequestException("invalid project name: " + nameKey, badName);
+    } catch (ConfigInvalidException e) {
+      String msg = "Cannot create " + nameKey;
+      logger.atSevere().withCause(e).log(msg);
+      throw e;
+    }
+  }
+
+  private void createProjectConfig(CreateProjectArgs args)
+      throws IOException, ConfigInvalidException {
+    try (MetaDataUpdate md = metaDataUpdateFactory.create(args.getProject())) {
+      ProjectConfig config = projectConfigFactory.read(md);
+
+      Project newProject = config.getProject();
+      newProject.setDescription(args.projectDescription);
+      newProject.setSubmitType(
+          MoreObjects.firstNonNull(
+              args.submitType, repositoryCfg.getDefaultSubmitType(args.getProject())));
+      newProject.setBooleanConfig(
+          BooleanProjectConfig.USE_CONTRIBUTOR_AGREEMENTS, args.contributorAgreements);
+      newProject.setBooleanConfig(BooleanProjectConfig.USE_SIGNED_OFF_BY, args.signedOffBy);
+      newProject.setBooleanConfig(BooleanProjectConfig.USE_CONTENT_MERGE, args.contentMerge);
+      newProject.setBooleanConfig(
+          BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET,
+          args.newChangeForAllNotInTarget);
+      newProject.setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, args.changeIdRequired);
+      newProject.setBooleanConfig(BooleanProjectConfig.REJECT_EMPTY_COMMIT, args.rejectEmptyCommit);
+      newProject.setMaxObjectSizeLimit(args.maxObjectSizeLimit);
+      newProject.setBooleanConfig(BooleanProjectConfig.ENABLE_SIGNED_PUSH, args.enableSignedPush);
+      newProject.setBooleanConfig(BooleanProjectConfig.REQUIRE_SIGNED_PUSH, args.requireSignedPush);
+      if (args.newParent != null) {
+        newProject.setParentName(args.newParent);
+      }
+
+      if (!args.ownerIds.isEmpty()) {
+        AccessSection all = config.getAccessSection(AccessSection.ALL, true);
+        for (AccountGroup.UUID ownerId : args.ownerIds) {
+          GroupDescription.Basic g = groupBackend.get(ownerId);
+          if (g != null) {
+            GroupReference group = config.resolve(GroupReference.forGroup(g));
+            all.getPermission(Permission.OWNER, true).add(new PermissionRule(group));
+          }
+        }
+      }
+
+      md.setMessage("Created project\n");
+      config.commit(md);
+      md.getRepository().setGitwebDescription(args.projectDescription);
+    }
+    projectCache.onCreateProject(args.getProject());
+  }
+
+  private void createEmptyCommits(Repository repo, Project.NameKey project, List<String> refs)
+      throws IOException {
+    try (ObjectInserter oi = repo.newObjectInserter()) {
+      CommitBuilder cb = new CommitBuilder();
+      cb.setTreeId(oi.insert(Constants.OBJ_TREE, new byte[] {}));
+      cb.setAuthor(metaDataUpdateFactory.getUserPersonIdent());
+      cb.setCommitter(serverIdent.get());
+      cb.setMessage("Initial empty repository\n");
+
+      ObjectId id = oi.insert(cb);
+      oi.flush();
+
+      for (String ref : refs) {
+        RefUpdate ru = repo.updateRef(ref);
+        ru.setNewObjectId(id);
+        Result result = ru.update();
+        switch (result) {
+          case NEW:
+            referenceUpdated.fire(
+                project, ru, ReceiveCommand.Type.CREATE, identifiedUser.get().state());
+            break;
+          case FAST_FORWARD:
+          case FORCED:
+          case IO_FAILURE:
+          case LOCK_FAILURE:
+          case NOT_ATTEMPTED:
+          case NO_CHANGE:
+          case REJECTED:
+          case REJECTED_CURRENT_BRANCH:
+          case RENAMED:
+          case REJECTED_MISSING_OBJECT:
+          case REJECTED_OTHER_REASON:
+          default:
+            {
+              throw new IOException(
+                  String.format("Failed to create ref \"%s\": %s", ref, result.name()));
+            }
+        }
+      }
+    } catch (IOException e) {
+      logger.atSevere().withCause(e).log("Cannot create empty commit for %s", project.get());
+      throw e;
+    }
+  }
+
+  private void fire(Project.NameKey name, String head) {
+    if (createdListeners.isEmpty()) {
+      return;
+    }
+
+    ProjectCreator.Event event = new ProjectCreator.Event(name, head);
+    createdListeners.runEach(l -> l.onNewProjectCreated(event));
+  }
+
+  static class Event extends AbstractNoNotifyEvent implements NewProjectCreatedListener.Event {
+    private final Project.NameKey name;
+    private final String head;
+
+    Event(Project.NameKey name, String head) {
+      this.name = name;
+      this.head = head;
+    }
+
+    @Override
+    public String getProjectName() {
+      return name.get();
+    }
+
+    @Override
+    public String getHeadName() {
+      return head;
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/project/ProjectState.java b/java/com/google/gerrit/server/project/ProjectState.java
index 0589ac1..0e3a940 100644
--- a/java/com/google/gerrit/server/project/ProjectState.java
+++ b/java/com/google/gerrit/server/project/ProjectState.java
@@ -15,12 +15,10 @@
 package com.google.gerrit.server.project;
 
 import static com.google.gerrit.common.data.PermissionRule.Action.ALLOW;
-import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.AccessSection;
@@ -29,10 +27,8 @@
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.common.data.RefConfigSection;
 import com.google.gerrit.common.data.SubscribeSection;
 import com.google.gerrit.extensions.api.projects.CommentLinkInfo;
-import com.google.gerrit.extensions.api.projects.ThemeInfo;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.index.project.ProjectData;
@@ -49,7 +45,6 @@
 import com.google.gerrit.server.account.CapabilityCollection;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.BranchOrderSection;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.TransferConfig;
@@ -57,14 +52,11 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -88,7 +80,6 @@
 
   private final boolean isAllProjects;
   private final boolean isAllUsers;
-  private final SitePaths sitePaths;
   private final AllProjectsName allProjectsName;
   private final ProjectCache projectCache;
   private final GitRepositoryManager gitMgr;
@@ -109,17 +100,11 @@
   /** Local access sections, wrapped in SectionMatchers for faster evaluation. */
   private volatile List<SectionMatcher> localAccessSections;
 
-  // TODO(dborowitz): Delete when the GWT UI gets deleted; in the meantime, don't bother with any
-  // refactoring.
-  /** Theme information loaded from site_path/themes. */
-  private volatile ThemeInfo theme;
-
   /** If this is all projects, the capabilities used by the server. */
   private final CapabilityCollection capabilities;
 
   @Inject
   public ProjectState(
-      SitePaths sitePaths,
       ProjectCache projectCache,
       AllProjectsName allProjectsName,
       AllUsersName allUsersName,
@@ -129,7 +114,6 @@
       TransferConfig transferConfig,
       MetricMaker metricMaker,
       @Assisted ProjectConfig config) {
-    this.sitePaths = sitePaths;
     this.projectCache = projectCache;
     this.isAllProjects = config.getProject().getNameKey().equals(allProjectsName);
     this.isAllUsers = config.getProject().getNameKey().equals(allUsersName);
@@ -421,12 +405,7 @@
    *     Starts from this project and progresses up the hierarchy to All-Projects.
    */
   public Iterable<ProjectState> tree() {
-    return new Iterable<ProjectState>() {
-      @Override
-      public Iterator<ProjectState> iterator() {
-        return new ProjectHierarchyIterator(projectCache, allProjectsName, ProjectState.this);
-      }
-    };
+    return () -> new ProjectHierarchyIterator(projectCache, allProjectsName, ProjectState.this);
   }
 
   /**
@@ -515,7 +494,7 @@
             continue;
           }
 
-          if (RefConfigSection.isValid(refPattern) && match(destination, refPattern)) {
+          if (AccessSection.isValidRefSectionName(refPattern) && match(destination, refPattern)) {
             r.add(l);
             break;
           }
@@ -566,24 +545,6 @@
     return ret;
   }
 
-  public ThemeInfo getTheme() {
-    ThemeInfo theme = this.theme;
-    if (theme == null) {
-      synchronized (this) {
-        theme = this.theme;
-        if (theme == null) {
-          theme = loadTheme();
-          this.theme = theme;
-        }
-      }
-    }
-    if (theme == ThemeInfo.INHERIT) {
-      ProjectState parent = Iterables.getFirst(parents(), null);
-      return parent != null ? parent.getTheme() : null;
-    }
-    return theme;
-  }
-
   public Set<GroupReference> getAllGroups() {
     return getGroups(getAllSections());
   }
@@ -615,26 +576,6 @@
     return all;
   }
 
-  private ThemeInfo loadTheme() {
-    String name = getConfig().getProject().getName();
-    Path dir = sitePaths.themes_dir.resolve(name);
-    if (!Files.exists(dir)) {
-      return ThemeInfo.INHERIT;
-    } else if (!Files.isDirectory(dir)) {
-      logger.atWarning().log("Bad theme for %s: not a directory", name);
-      return ThemeInfo.INHERIT;
-    }
-    try {
-      return new ThemeInfo(
-          readFile(dir.resolve(SitePaths.CSS_FILENAME)),
-          readFile(dir.resolve(SitePaths.HEADER_FILENAME)),
-          readFile(dir.resolve(SitePaths.FOOTER_FILENAME)));
-    } catch (IOException e) {
-      logger.atSevere().withCause(e).log("Error reading theme for %s", name);
-      return ThemeInfo.INHERIT;
-    }
-  }
-
   public ProjectData toProjectData() {
     ProjectData project = null;
     for (ProjectState state : treeInOrder()) {
@@ -643,10 +584,6 @@
     return project;
   }
 
-  private String readFile(Path p) throws IOException {
-    return Files.exists(p) ? new String(Files.readAllBytes(p), UTF_8) : null;
-  }
-
   private boolean match(Branch.NameKey destination, String refPattern) {
     return RefPatternMatcher.getMatcher(refPattern).match(destination.get(), null);
   }
diff --git a/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java b/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java
index 894037d..32dbe1c 100644
--- a/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java
+++ b/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java
@@ -24,6 +24,7 @@
 import com.google.common.base.Strings;
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.FixInput;
 import com.google.gerrit.extensions.api.projects.CheckProjectInput;
 import com.google.gerrit.extensions.api.projects.CheckProjectInput.AutoCloseableChangesCheckInput;
@@ -51,7 +52,6 @@
 import com.google.gerrit.server.query.change.RefPredicate;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryHelper.ActionType;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -94,7 +94,7 @@
   }
 
   public CheckProjectResultInfo check(Project.NameKey projectName, CheckProjectInput input)
-      throws IOException, OrmException, RestApiException {
+      throws IOException, RestApiException {
     CheckProjectResultInfo r = new CheckProjectResultInfo();
     if (input.autoCloseableChangesCheck != null) {
       r.autoCloseableChangesCheckResult =
@@ -105,7 +105,7 @@
 
   private AutoCloseableChangesCheckResult checkForAutoCloseableChanges(
       Project.NameKey projectName, AutoCloseableChangesCheckInput input)
-      throws IOException, OrmException, RestApiException {
+      throws IOException, RestApiException {
     AutoCloseableChangesCheckResult r = new AutoCloseableChangesCheckResult();
     if (Strings.isNullOrEmpty(input.branch)) {
       throw new BadRequestException("branch is required");
@@ -256,8 +256,7 @@
       List<Predicate<ChangeData>> predicates,
       boolean fix,
       Map<Change.Key, ObjectId> changeIdToMergedSha1,
-      List<ObjectId> mergedSha1s)
-      throws OrmException {
+      List<ObjectId> mergedSha1s) {
     if (predicates.isEmpty()) {
       return ImmutableList.of();
     }
@@ -273,7 +272,7 @@
                     .setRequestedFields(ChangeField.CHANGE, ChangeField.PATCH_SET)
                     .query(and(basePredicate, or(predicates)));
               },
-              OrmException.class::isInstance);
+              StorageException.class::isInstance);
 
       // Result for this query that we want to return to the client.
       List<ChangeInfo> autoCloseableChangesByBranch = new ArrayList<>();
@@ -307,15 +306,14 @@
                 }
                 return null;
               },
-              OrmException.class::isInstance);
+              StorageException.class::isInstance);
         }
       }
 
       return autoCloseableChangesByBranch;
     } catch (Exception e) {
       Throwables.throwIfUnchecked(e);
-      Throwables.throwIfInstanceOf(e, OrmException.class);
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
   }
 
diff --git a/java/com/google/gerrit/server/project/Reachable.java b/java/com/google/gerrit/server/project/Reachable.java
index 63fda19..8119ef5 100644
--- a/java/com/google/gerrit/server/project/Reachable.java
+++ b/java/com/google/gerrit/server/project/Reachable.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.server.project;
 
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Maps;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.change.IncludedInResolver;
@@ -25,11 +23,9 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.util.Collection;
+import java.util.List;
 import java.util.Map;
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -49,15 +45,19 @@
     this.permissionBackend = permissionBackend;
   }
 
-  /** @return true if a commit is reachable from a given set of refs. */
+  /**
+   * @return true if a commit is reachable from a given set of refs. This method enforces
+   *     permissions on the given set of refs and performs a reachability check. Tags are not
+   *     filtered separately and will only be returned if reachable by a provided ref.
+   */
   public boolean fromRefs(
-      Project.NameKey project, Repository repo, RevCommit commit, Map<String, Ref> refs) {
+      Project.NameKey project, Repository repo, RevCommit commit, List<Ref> refs) {
     try (RevWalk rw = new RevWalk(repo)) {
       Map<String, Ref> filtered =
           permissionBackend
               .currentUser()
               .project(project)
-              .filter(refs, repo, RefFilterOptions.builder().setFilterTagsSeparately(true).build());
+              .filter(refs, repo, RefFilterOptions.defaults());
       return IncludedInResolver.includedInAny(repo, rw, commit, filtered.values());
     } catch (IOException | PermissionBackendException e) {
       logger.atSevere().withCause(e).log(
@@ -65,22 +65,4 @@
       return false;
     }
   }
-
-  /** @return true if a commit is reachable from a repo's branches and tags. */
-  boolean fromHeadsOrTags(Project.NameKey project, Repository repo, RevCommit commit) {
-    try {
-      RefDatabase refdb = repo.getRefDatabase();
-      Collection<Ref> heads = refdb.getRefsByPrefix(Constants.R_HEADS);
-      Collection<Ref> tags = refdb.getRefsByPrefix(Constants.R_TAGS);
-      Map<String, Ref> refs = Maps.newHashMapWithExpectedSize(heads.size() + tags.size());
-      for (Ref r : Iterables.concat(heads, tags)) {
-        refs.put(r.getName(), r);
-      }
-      return fromRefs(project, repo, commit, refs);
-    } catch (IOException e) {
-      logger.atSevere().withCause(e).log(
-          "Cannot verify permissions to commit object %s in repository %s", commit.name(), project);
-      return false;
-    }
-  }
 }
diff --git a/java/com/google/gerrit/server/project/RefPattern.java b/java/com/google/gerrit/server/project/RefPattern.java
index 72face2..0e916fb 100644
--- a/java/com/google/gerrit/server/project/RefPattern.java
+++ b/java/com/google/gerrit/server/project/RefPattern.java
@@ -19,8 +19,7 @@
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
 import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.RefConfigSection;
-import com.google.gerrit.common.errors.InvalidNameException;
+import com.google.gerrit.exceptions.InvalidNameException;
 import dk.brics.automaton.RegExp;
 import java.util.concurrent.ExecutionException;
 import java.util.regex.Pattern;
@@ -78,11 +77,11 @@
   }
 
   public static void validate(String refPattern) throws InvalidNameException {
-    if (refPattern.startsWith(RefConfigSection.REGEX_PREFIX)) {
+    if (refPattern.startsWith(AccessSection.REGEX_PREFIX)) {
       if (!Repository.isValidRefName(shortestExample(refPattern))) {
         throw new InvalidNameException(refPattern);
       }
-    } else if (refPattern.equals(RefConfigSection.ALL)) {
+    } else if (refPattern.equals(AccessSection.ALL)) {
       // This is a special case we have to allow, it fails below.
     } else if (refPattern.endsWith("/*")) {
       String prefix = refPattern.substring(0, refPattern.length() - 2);
diff --git a/java/com/google/gerrit/server/project/RemoveReviewerControl.java b/java/com/google/gerrit/server/project/RemoveReviewerControl.java
index df98f5e..eeb2a65 100644
--- a/java/com/google/gerrit/server/project/RemoveReviewerControl.java
+++ b/java/com/google/gerrit/server/project/RemoveReviewerControl.java
@@ -18,7 +18,6 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.ChangePermission;
@@ -27,20 +26,16 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 @Singleton
 public class RemoveReviewerControl {
   private final PermissionBackend permissionBackend;
-  private final Provider<ReviewDb> dbProvider;
 
   @Inject
-  RemoveReviewerControl(PermissionBackend permissionBackend, Provider<ReviewDb> dbProvider) {
+  RemoveReviewerControl(PermissionBackend permissionBackend) {
     this.permissionBackend = permissionBackend;
-    this.dbProvider = dbProvider;
   }
 
   /**
@@ -70,16 +65,12 @@
   /** @return true if the user is allowed to remove this reviewer. */
   public boolean testRemoveReviewer(
       ChangeData cd, CurrentUser currentUser, Account.Id reviewer, int value)
-      throws PermissionBackendException, OrmException {
+      throws PermissionBackendException {
     if (canRemoveReviewerWithoutPermissionCheck(
         permissionBackend, cd.change(), currentUser, reviewer, value)) {
       return true;
     }
-    return permissionBackend
-        .user(currentUser)
-        .change(cd)
-        .database(dbProvider)
-        .test(ChangePermission.REMOVE_REVIEWER);
+    return permissionBackend.user(currentUser).change(cd).test(ChangePermission.REMOVE_REVIEWER);
   }
 
   private void checkRemoveReviewer(
@@ -90,11 +81,7 @@
       return;
     }
 
-    permissionBackend
-        .user(currentUser)
-        .change(notes)
-        .database(dbProvider)
-        .check(ChangePermission.REMOVE_REVIEWER);
+    permissionBackend.user(currentUser).change(notes).check(ChangePermission.REMOVE_REVIEWER);
   }
 
   private static boolean canRemoveReviewerWithoutPermissionCheck(
@@ -104,7 +91,7 @@
       Account.Id reviewer,
       int value)
       throws PermissionBackendException {
-    if (change.getStatus().equals(Change.Status.MERGED)) {
+    if (change.isMerged()) {
       return false;
     }
 
diff --git a/java/com/google/gerrit/server/project/SectionMatcher.java b/java/com/google/gerrit/server/project/SectionMatcher.java
index 11b1f37..a8ebd98 100644
--- a/java/com/google/gerrit/server/project/SectionMatcher.java
+++ b/java/com/google/gerrit/server/project/SectionMatcher.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.NameKey;
 import com.google.gerrit.server.CurrentUser;
 
 /**
@@ -28,7 +27,7 @@
 public class SectionMatcher extends RefPatternMatcher {
   static SectionMatcher wrap(Project.NameKey project, AccessSection section) {
     String ref = section.getName();
-    if (AccessSection.isValid(ref)) {
+    if (AccessSection.isValidRefSectionName(ref)) {
       return new SectionMatcher(project, section, getMatcher(ref));
     }
     return null;
@@ -57,7 +56,7 @@
     return matcher;
   }
 
-  public NameKey getProject() {
+  public Project.NameKey getProject() {
     return project;
   }
 }
diff --git a/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java b/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
index 3b70eee..1b1869c 100644
--- a/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
+++ b/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
@@ -18,13 +18,13 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitTypeRecord;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.index.OnlineReindexMode;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.rules.PrologRule;
 import com.google.gerrit.server.rules.SubmitRule;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.util.Collection;
@@ -92,18 +92,18 @@
     try {
       change = cd.change();
       if (change == null) {
-        throw new OrmException("Change not found");
+        throw new StorageException("Change not found");
       }
 
       projectState = projectCache.get(cd.project());
       if (projectState == null) {
         throw new NoSuchProjectException(cd.project());
       }
-    } catch (OrmException | NoSuchProjectException e) {
+    } catch (StorageException | NoSuchProjectException e) {
       return ruleError("Error looking up change " + cd.getId(), e);
     }
 
-    if ((!opts.allowClosed() || OnlineReindexMode.isActive()) && change.getStatus().isClosed()) {
+    if ((!opts.allowClosed() || OnlineReindexMode.isActive()) && change.isClosed()) {
       SubmitRecord rec = new SubmitRecord();
       rec.status = SubmitRecord.Status.CLOSED;
       return Collections.singletonList(rec);
diff --git a/java/com/google/gerrit/server/project/testing/Util.java b/java/com/google/gerrit/server/project/testing/Util.java
index abfd2bd..204fa7b 100644
--- a/java/com/google/gerrit/server/project/testing/Util.java
+++ b/java/com/google/gerrit/server/project/testing/Util.java
@@ -124,16 +124,28 @@
 
   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)) {
-      PermissionRange.WithDefaults range = GlobalCapability.getRange(capabilityName);
-      if (range != null) {
-        rule.setRange(range.getDefaultMin(), range.getDefaultMax());
+      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;
   }
diff --git a/java/com/google/gerrit/server/query/account/AccountIsVisibleToPredicate.java b/java/com/google/gerrit/server/query/account/AccountIsVisibleToPredicate.java
index bd7b7fe..f4ff441 100644
--- a/java/com/google/gerrit/server/query/account/AccountIsVisibleToPredicate.java
+++ b/java/com/google/gerrit/server/query/account/AccountIsVisibleToPredicate.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.server.account.AccountControl;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.index.IndexUtils;
-import com.google.gwtorm.server.OrmException;
 
 public class AccountIsVisibleToPredicate extends IsVisibleToPredicate<AccountState> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -32,7 +31,7 @@
   }
 
   @Override
-  public boolean match(AccountState accountState) throws OrmException {
+  public boolean match(AccountState accountState) {
     boolean canSee = accountControl.canSee(accountState);
     if (!canSee) {
       logger.atFine().log("Filter out non-visisble account: %s", accountState);
diff --git a/java/com/google/gerrit/server/query/account/AccountPredicates.java b/java/com/google/gerrit/server/query/account/AccountPredicates.java
index 57a0dcc..cb96bc5 100644
--- a/java/com/google/gerrit/server/query/account/AccountPredicates.java
+++ b/java/com/google/gerrit/server/query/account/AccountPredicates.java
@@ -27,7 +27,6 @@
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.index.account.AccountField;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gwtorm.server.OrmException;
 import java.util.List;
 
 public class AccountPredicates {
@@ -126,7 +125,7 @@
 
   public static Predicate<AccountState> cansee(
       AccountQueryBuilder.Arguments args, ChangeNotes changeNotes) {
-    return new CanSeeChangePredicate(args.db, args.permissionBackend, changeNotes);
+    return new CanSeeChangePredicate(args.permissionBackend, changeNotes);
   }
 
   static class AccountPredicate extends IndexPredicate<AccountState>
@@ -140,7 +139,7 @@
     }
 
     @Override
-    public boolean match(AccountState object) throws OrmException {
+    public boolean match(AccountState object) {
       return true;
     }
 
diff --git a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
index feb9ccb..38336c1 100644
--- a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
@@ -1,5 +1,4 @@
 // 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
@@ -18,7 +17,8 @@
 import com.google.common.collect.Lists;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
-import com.google.gerrit.common.errors.NotSignedInException;
+import com.google.gerrit.exceptions.NotSignedInException;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.index.Index;
 import com.google.gerrit.index.Schema;
@@ -27,7 +27,6 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountState;
@@ -39,13 +38,12 @@
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
 
 /** Parses a query string meant to be applied to account objects. */
-public class AccountQueryBuilder extends QueryBuilder<AccountState> {
+public class AccountQueryBuilder extends QueryBuilder<AccountState, AccountQueryBuilder> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static final String FIELD_ACCOUNT = "account";
@@ -62,7 +60,6 @@
       new QueryBuilder.Definition<>(AccountQueryBuilder.class);
 
   public static class Arguments {
-    final Provider<ReviewDb> db;
     final ChangeFinder changeFinder;
     final PermissionBackend permissionBackend;
 
@@ -73,12 +70,10 @@
     public Arguments(
         Provider<CurrentUser> self,
         AccountIndexCollection indexes,
-        Provider<ReviewDb> db,
         ChangeFinder changeFinder,
         PermissionBackend permissionBackend) {
       this.self = self;
       this.indexes = indexes;
-      this.db = db;
       this.changeFinder = changeFinder;
       this.permissionBackend = permissionBackend;
     }
@@ -113,24 +108,20 @@
 
   @Inject
   AccountQueryBuilder(Arguments args) {
-    super(mydef);
+    super(mydef, null);
     this.args = args;
   }
 
   @Operator
   public Predicate<AccountState> cansee(String change)
-      throws QueryParseException, OrmException, PermissionBackendException {
+      throws QueryParseException, PermissionBackendException {
     ChangeNotes changeNotes = args.changeFinder.findOne(change);
     if (changeNotes == null) {
       throw error(String.format("change %s not found", change));
     }
 
     try {
-      args.permissionBackend
-          .user(args.getUser())
-          .database(args.db)
-          .change(changeNotes)
-          .check(ChangePermission.READ);
+      args.permissionBackend.user(args.getUser()).change(changeNotes).check(ChangePermission.READ);
     } catch (AuthException e) {
       String msg = String.format("change %s not found", change);
       logger.atSevere().withCause(e).log(msg);
@@ -206,7 +197,7 @@
     if (query.startsWith("cansee:")) {
       try {
         return cansee(query.substring(7));
-      } catch (OrmException | QueryParseException | PermissionBackendException e) {
+      } catch (StorageException | QueryParseException | PermissionBackendException e) {
         // Ignore, fall back to default query
       }
     }
diff --git a/java/com/google/gerrit/server/query/account/CanSeeChangePredicate.java b/java/com/google/gerrit/server/query/account/CanSeeChangePredicate.java
index fb3549c..8bbeb24 100644
--- a/java/com/google/gerrit/server/query/account/CanSeeChangePredicate.java
+++ b/java/com/google/gerrit/server/query/account/CanSeeChangePredicate.java
@@ -14,41 +14,35 @@
 
 package com.google.gerrit.server.query.account;
 
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.index.query.PostFilterPredicate;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.AccountState;
 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.gwtorm.server.OrmException;
-import com.google.inject.Provider;
 
 public class CanSeeChangePredicate extends PostFilterPredicate<AccountState> {
-  private final Provider<ReviewDb> db;
   private final PermissionBackend permissionBackend;
   private final ChangeNotes changeNotes;
 
-  CanSeeChangePredicate(
-      Provider<ReviewDb> db, PermissionBackend permissionBackend, ChangeNotes changeNotes) {
+  CanSeeChangePredicate(PermissionBackend permissionBackend, ChangeNotes changeNotes) {
     super(AccountQueryBuilder.FIELD_CAN_SEE, changeNotes.getChangeId().toString());
-    this.db = db;
     this.permissionBackend = permissionBackend;
     this.changeNotes = changeNotes;
   }
 
   @Override
-  public boolean match(AccountState accountState) throws OrmException {
+  public boolean match(AccountState accountState) {
     try {
       permissionBackend
           .absentUser(accountState.getAccount().getId())
-          .database(db)
           .change(changeNotes)
           .check(ChangePermission.READ);
       return true;
     } catch (PermissionBackendException e) {
-      throw new OrmException("Failed to check if account can see change", e);
+      throw new StorageException("Failed to check if account can see change", e);
     } catch (AuthException e) {
       return false;
     }
diff --git a/java/com/google/gerrit/server/query/account/InternalAccountQuery.java b/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
index 3a7d3e5..48b0dc2 100644
--- a/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
+++ b/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
@@ -23,17 +23,16 @@
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.Multimap;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.UsedAt;
 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.UsedAt;
 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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.util.Arrays;
 import java.util.List;
@@ -45,7 +44,7 @@
  * <p>Instances are one-time-use. Other singleton classes should inject a Provider rather than
  * holding on to a single instance.
  */
-public class InternalAccountQuery extends InternalQuery<AccountState> {
+public class InternalAccountQuery extends InternalQuery<AccountState, InternalAccountQuery> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   @Inject
@@ -56,45 +55,20 @@
     super(queryProcessor, indexes, indexConfig);
   }
 
-  @Override
-  public InternalAccountQuery setLimit(int n) {
-    super.setLimit(n);
-    return this;
-  }
-
-  @Override
-  public InternalAccountQuery enforceVisibility(boolean enforce) {
-    super.enforceVisibility(enforce);
-    return this;
-  }
-
-  @SafeVarargs
-  @Override
-  public final InternalAccountQuery setRequestedFields(FieldDef<AccountState, ?>... fields) {
-    super.setRequestedFields(fields);
-    return this;
-  }
-
-  @Override
-  public InternalAccountQuery noFields() {
-    super.noFields();
-    return this;
-  }
-
-  public List<AccountState> byDefault(String query) throws OrmException {
+  public List<AccountState> byDefault(String query) {
     return query(AccountPredicates.defaultPredicate(schema(), true, query));
   }
 
-  public List<AccountState> byExternalId(String scheme, String id) throws OrmException {
+  public List<AccountState> byExternalId(String scheme, String id) {
     return byExternalId(ExternalId.Key.create(scheme, id));
   }
 
-  public List<AccountState> byExternalId(ExternalId.Key externalId) throws OrmException {
+  public List<AccountState> byExternalId(ExternalId.Key externalId) {
     return query(AccountPredicates.externalIdIncludingSecondaryEmails(externalId.toString()));
   }
 
   @UsedAt(UsedAt.Project.COLLABNET)
-  public AccountState oneByExternalId(ExternalId.Key externalId) throws OrmException {
+  public AccountState oneByExternalId(ExternalId.Key externalId) {
     List<AccountState> accountStates = byExternalId(externalId);
     if (accountStates.size() == 1) {
       return accountStates.get(0);
@@ -109,7 +83,7 @@
     return null;
   }
 
-  public List<AccountState> byFullName(String fullName) throws OrmException {
+  public List<AccountState> byFullName(String fullName) {
     return query(AccountPredicates.fullName(fullName));
   }
 
@@ -118,9 +92,8 @@
    *
    * @param email preferred email by which accounts should be found
    * @return list of accounts that have a preferred email that exactly matches the given email
-   * @throws OrmException if query cannot be parsed
    */
-  public List<AccountState> byPreferredEmail(String email) throws OrmException {
+  public List<AccountState> byPreferredEmail(String email) {
     if (hasPreferredEmailExact()) {
       return query(AccountPredicates.preferredEmailExact(email));
     }
@@ -140,9 +113,8 @@
    * @param emails preferred emails by which accounts should be found
    * @return multimap of the given emails to accounts that have a preferred email that exactly
    *     matches this email
-   * @throws OrmException if query cannot be parsed
    */
-  public Multimap<String, AccountState> byPreferredEmail(String... emails) throws OrmException {
+  public Multimap<String, AccountState> byPreferredEmail(String... emails) {
     List<String> emailList = Arrays.asList(emails);
 
     if (hasPreferredEmailExact()) {
@@ -173,7 +145,7 @@
     return accountsByEmail;
   }
 
-  public List<AccountState> byWatchedProject(Project.NameKey project) throws OrmException {
+  public List<AccountState> byWatchedProject(Project.NameKey project) {
     return query(AccountPredicates.watchedProject(project));
   }
 
diff --git a/java/com/google/gerrit/server/query/change/AddedPredicate.java b/java/com/google/gerrit/server/query/change/AddedPredicate.java
index 099e841..1f526c5 100644
--- a/java/com/google/gerrit/server/query/change/AddedPredicate.java
+++ b/java/com/google/gerrit/server/query/change/AddedPredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 public class AddedPredicate extends IntegerRangeChangePredicate {
   public AddedPredicate(String value) throws QueryParseException {
@@ -24,7 +23,7 @@
   }
 
   @Override
-  protected Integer getValueInt(ChangeData changeData) throws OrmException {
+  protected Integer getValueInt(ChangeData changeData) {
     return ChangeField.ADDED.get(changeData);
   }
 }
diff --git a/java/com/google/gerrit/server/query/change/AfterPredicate.java b/java/com/google/gerrit/server/query/change/AfterPredicate.java
index de57b3b..df5a71d 100644
--- a/java/com/google/gerrit/server/query/change/AfterPredicate.java
+++ b/java/com/google/gerrit/server/query/change/AfterPredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 import java.util.Date;
 
 public class AfterPredicate extends TimestampRangeChangePredicate {
@@ -38,7 +37,7 @@
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
+  public boolean match(ChangeData cd) {
     return cd.change().getLastUpdatedOn().getTime() >= cut.getTime();
   }
 }
diff --git a/java/com/google/gerrit/server/query/change/AgePredicate.java b/java/com/google/gerrit/server/query/change/AgePredicate.java
index 29f1b8a..1cf2c2f 100644
--- a/java/com/google/gerrit/server/query/change/AgePredicate.java
+++ b/java/com/google/gerrit/server/query/change/AgePredicate.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import java.sql.Timestamp;
 
 public class AgePredicate extends TimestampRangeChangePredicate {
@@ -46,7 +45,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     Change change = object.change();
     return change != null && change.getLastUpdatedOn().getTime() <= cut;
   }
diff --git a/java/com/google/gerrit/server/query/change/AndChangeSource.java b/java/com/google/gerrit/server/query/change/AndChangeSource.java
index ff1ab23..4a3b936 100644
--- a/java/com/google/gerrit/server/query/change/AndChangeSource.java
+++ b/java/com/google/gerrit/server/query/change/AndChangeSource.java
@@ -17,8 +17,6 @@
 import com.google.gerrit.index.query.AndSource;
 import com.google.gerrit.index.query.IsVisibleToPredicate;
 import com.google.gerrit.index.query.Predicate;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.OrmRuntimeException;
 import java.util.Collection;
 import java.util.List;
 
@@ -43,13 +41,9 @@
   }
 
   @Override
-  protected List<ChangeData> transformBuffer(List<ChangeData> buffer) throws OrmRuntimeException {
+  protected List<ChangeData> transformBuffer(List<ChangeData> buffer) {
     if (!hasChange()) {
-      try {
-        ChangeData.ensureChangeLoaded(buffer);
-      } catch (OrmException e) {
-        throw new OrmRuntimeException(e);
-      }
+      ChangeData.ensureChangeLoaded(buffer);
     }
     return super.transformBuffer(buffer);
   }
diff --git a/java/com/google/gerrit/server/query/change/AssigneePredicate.java b/java/com/google/gerrit/server/query/change/AssigneePredicate.java
index 63f7467..fb19e85 100644
--- a/java/com/google/gerrit/server/query/change/AssigneePredicate.java
+++ b/java/com/google/gerrit/server/query/change/AssigneePredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 public class AssigneePredicate extends ChangeIndexPredicate {
   protected final Account.Id id;
@@ -27,7 +26,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     if (id.get() == ChangeField.NO_ASSIGNEE) {
       Account.Id assignee = object.change().getAssignee();
       return assignee == null;
diff --git a/java/com/google/gerrit/server/query/change/AuthorPredicate.java b/java/com/google/gerrit/server/query/change/AuthorPredicate.java
index 3ee3352..79914a3 100644
--- a/java/com/google/gerrit/server/query/change/AuthorPredicate.java
+++ b/java/com/google/gerrit/server/query/change/AuthorPredicate.java
@@ -18,8 +18,6 @@
 import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_AUTHOR;
 
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
-import java.io.IOException;
 
 public class AuthorPredicate extends ChangeIndexPredicate {
   public AuthorPredicate(String value) {
@@ -27,12 +25,8 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
-    try {
-      return ChangeField.getAuthorParts(object).contains(getValue().toLowerCase());
-    } catch (IOException e) {
-      throw new OrmException(e);
-    }
+  public boolean match(ChangeData object) {
+    return ChangeField.getAuthorParts(object).contains(getValue().toLowerCase());
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/query/change/BeforePredicate.java b/java/com/google/gerrit/server/query/change/BeforePredicate.java
index 4d6ed69..dacabc0 100644
--- a/java/com/google/gerrit/server/query/change/BeforePredicate.java
+++ b/java/com/google/gerrit/server/query/change/BeforePredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 import java.util.Date;
 
 public class BeforePredicate extends TimestampRangeChangePredicate {
@@ -38,7 +37,7 @@
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
+  public boolean match(ChangeData cd) {
     return cd.change().getLastUpdatedOn().getTime() <= cut.getTime();
   }
 }
diff --git a/java/com/google/gerrit/server/query/change/BooleanPredicate.java b/java/com/google/gerrit/server/query/change/BooleanPredicate.java
index 5930b74..68f83e8 100644
--- a/java/com/google/gerrit/server/query/change/BooleanPredicate.java
+++ b/java/com/google/gerrit/server/query/change/BooleanPredicate.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.gerrit.index.FieldDef;
-import com.google.gwtorm.server.OrmException;
 
 public class BooleanPredicate extends ChangeIndexPredicate {
   public BooleanPredicate(FieldDef<ChangeData, String> field) {
@@ -23,7 +22,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     return getValue().equals(getField().get(object));
   }
 
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index 8e9a08f..3cd64b3 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.query.change;
 
-import static com.google.gerrit.server.ApprovalsUtil.sortApprovals;
 import static java.util.Objects.requireNonNull;
 import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toMap;
@@ -30,10 +29,12 @@
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.common.primitives.Ints;
 import com.google.gerrit.common.Nullable;
 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.exceptions.StorageException;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -46,7 +47,6 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.CommentsUtil;
@@ -64,7 +64,6 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.patch.DiffSummary;
 import com.google.gerrit.server.patch.DiffSummaryKey;
 import com.google.gerrit.server.patch.PatchListCache;
@@ -75,8 +74,6 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.project.SubmitRuleOptions;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
@@ -92,9 +89,6 @@
 import java.util.Set;
 import java.util.function.Function;
 import java.util.stream.Stream;
-import org.eclipse.jgit.errors.IncorrectObjectTypeException;
-import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
@@ -104,9 +98,7 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 
 public class ChangeData {
-  private static final int BATCH_SIZE = 50;
-
-  public static List<Change> asChanges(List<ChangeData> changeDatas) throws OrmException {
+  public static List<Change> asChanges(List<ChangeData> changeDatas) {
     List<Change> result = new ArrayList<>(changeDatas.size());
     for (ChangeData cd : changeDatas) {
       result.add(cd.change());
@@ -118,154 +110,65 @@
     return changes.stream().collect(toMap(ChangeData::getId, Function.identity()));
   }
 
-  public static void ensureChangeLoaded(Iterable<ChangeData> changes) throws OrmException {
+  public static void ensureChangeLoaded(Iterable<ChangeData> changes) {
     ChangeData first = Iterables.getFirst(changes, null);
     if (first == null) {
       return;
-    } else if (first.notesMigration.readChanges()) {
-      for (ChangeData cd : changes) {
-        cd.change();
-      }
-      return;
     }
 
-    Map<Change.Id, ChangeData> missing = new HashMap<>();
     for (ChangeData cd : changes) {
-      if (cd.change == null) {
-        missing.put(cd.getId(), cd);
-      }
-    }
-    if (missing.isEmpty()) {
-      return;
-    }
-    for (ChangeNotes notes : first.notesFactory.create(first.db, missing.keySet())) {
-      missing.get(notes.getChangeId()).change = notes.getChange();
+      cd.change();
     }
   }
 
-  public static void ensureAllPatchSetsLoaded(Iterable<ChangeData> changes) throws OrmException {
+  public static void ensureAllPatchSetsLoaded(Iterable<ChangeData> changes) {
     ChangeData first = Iterables.getFirst(changes, null);
     if (first == null) {
       return;
-    } else if (first.notesMigration.readChanges()) {
-      for (ChangeData cd : changes) {
-        cd.patchSets();
-      }
-      return;
     }
 
-    List<ResultSet<PatchSet>> results = new ArrayList<>(BATCH_SIZE);
-    for (List<ChangeData> batch : Iterables.partition(changes, BATCH_SIZE)) {
-      results.clear();
-      for (ChangeData cd : batch) {
-        if (cd.patchSets == null) {
-          results.add(cd.db.patchSets().byChange(cd.getId()));
-        } else {
-          results.add(null);
-        }
-      }
-      for (int i = 0; i < batch.size(); i++) {
-        ResultSet<PatchSet> result = results.get(i);
-        if (result != null) {
-          batch.get(i).patchSets = result.toList();
-        }
-      }
-    }
-  }
-
-  public static void ensureCurrentPatchSetLoaded(Iterable<ChangeData> changes) throws OrmException {
-    ChangeData first = Iterables.getFirst(changes, null);
-    if (first == null) {
-      return;
-    } else if (first.notesMigration.readChanges()) {
-      for (ChangeData cd : changes) {
-        cd.currentPatchSet();
-      }
-      return;
-    }
-
-    Map<PatchSet.Id, ChangeData> missing = new HashMap<>();
     for (ChangeData cd : changes) {
-      if (cd.currentPatchSet == null && cd.patchSets == null) {
-        missing.put(cd.change().currentPatchSetId(), cd);
-      }
-    }
-    if (missing.isEmpty()) {
-      return;
-    }
-    for (PatchSet ps : first.db.patchSets().get(missing.keySet())) {
-      missing.get(ps.getId()).currentPatchSet = ps;
+      cd.patchSets();
     }
   }
 
-  public static void ensureCurrentApprovalsLoaded(Iterable<ChangeData> changes)
-      throws OrmException {
+  public static void ensureCurrentPatchSetLoaded(Iterable<ChangeData> changes) {
     ChangeData first = Iterables.getFirst(changes, null);
     if (first == null) {
       return;
-    } else if (first.notesMigration.readChanges()) {
-      for (ChangeData cd : changes) {
-        cd.currentApprovals();
-      }
-      return;
     }
 
-    List<ResultSet<PatchSetApproval>> results = new ArrayList<>(BATCH_SIZE);
-    for (List<ChangeData> batch : Iterables.partition(changes, BATCH_SIZE)) {
-      results.clear();
-      for (ChangeData cd : batch) {
-        if (cd.currentApprovals == null) {
-          PatchSet.Id psId = cd.change().currentPatchSetId();
-          results.add(cd.db.patchSetApprovals().byPatchSet(psId));
-        } else {
-          results.add(null);
-        }
-      }
-      for (int i = 0; i < batch.size(); i++) {
-        ResultSet<PatchSetApproval> result = results.get(i);
-        if (result != null) {
-          batch.get(i).currentApprovals = sortApprovals(result);
-        }
-      }
+    for (ChangeData cd : changes) {
+      cd.currentPatchSet();
     }
   }
 
-  public static void ensureMessagesLoaded(Iterable<ChangeData> changes) throws OrmException {
+  public static void ensureCurrentApprovalsLoaded(Iterable<ChangeData> changes) {
     ChangeData first = Iterables.getFirst(changes, null);
     if (first == null) {
       return;
-    } else if (first.notesMigration.readChanges()) {
-      for (ChangeData cd : changes) {
-        cd.messages();
-      }
-      return;
     }
 
-    List<ResultSet<ChangeMessage>> results = new ArrayList<>(BATCH_SIZE);
-    for (List<ChangeData> batch : Iterables.partition(changes, BATCH_SIZE)) {
-      results.clear();
-      for (ChangeData cd : batch) {
-        if (cd.messages == null) {
-          PatchSet.Id psId = cd.change().currentPatchSetId();
-          results.add(cd.db.changeMessages().byPatchSet(psId));
-        } else {
-          results.add(null);
-        }
-      }
-      for (int i = 0; i < batch.size(); i++) {
-        ResultSet<ChangeMessage> result = results.get(i);
-        if (result != null) {
-          batch.get(i).messages = result.toList();
-        }
-      }
+    for (ChangeData cd : changes) {
+      cd.currentApprovals();
     }
   }
 
-  public static void ensureReviewedByLoadedForOpenChanges(Iterable<ChangeData> changes)
-      throws OrmException {
+  public static void ensureMessagesLoaded(Iterable<ChangeData> changes) {
+    ChangeData first = Iterables.getFirst(changes, null);
+    if (first == null) {
+      return;
+    }
+
+    for (ChangeData cd : changes) {
+      cd.messages();
+    }
+  }
+
+  public static void ensureReviewedByLoadedForOpenChanges(Iterable<ChangeData> changes) {
     List<ChangeData> pending = new ArrayList<>();
     for (ChangeData cd : changes) {
-      if (cd.reviewedBy == null && cd.change().getStatus().isOpen()) {
+      if (cd.reviewedBy == null && cd.change().isNew()) {
         pending.add(cd);
       }
     }
@@ -287,23 +190,22 @@
       this.assistedFactory = assistedFactory;
     }
 
-    public ChangeData create(ReviewDb db, Project.NameKey project, Change.Id id) {
-      return assistedFactory.create(db, project, id, null, null);
+    public ChangeData create(Project.NameKey project, Change.Id id) {
+      return assistedFactory.create(project, id, null, null);
     }
 
-    public ChangeData create(ReviewDb db, Change change) {
-      return assistedFactory.create(db, change.getProject(), change.getId(), change, null);
+    public ChangeData create(Change change) {
+      return assistedFactory.create(change.getProject(), change.getId(), change, null);
     }
 
-    public ChangeData create(ReviewDb db, ChangeNotes notes) {
+    public ChangeData create(ChangeNotes notes) {
       return assistedFactory.create(
-          db, notes.getChange().getProject(), notes.getChangeId(), notes.getChange(), notes);
+          notes.getChange().getProject(), notes.getChangeId(), notes.getChange(), notes);
     }
   }
 
   public interface AssistedFactory {
     ChangeData create(
-        ReviewDb db,
         Project.NameKey project,
         Change.Id id,
         @Nullable Change change,
@@ -324,7 +226,7 @@
     ChangeData cd =
         new ChangeData(
             null, null, null, null, null, null, null, null, null, null, null, null, null, null,
-            null, null, null, project, id, null, null);
+            null, project, id, null, null);
     cd.currentPatchSet = new PatchSet(new PatchSet.Id(id, currentPatchSetId));
     return cd;
   }
@@ -339,7 +241,6 @@
   private final GitRepositoryManager repoManager;
   private final MergeUtil.Factory mergeUtilFactory;
   private final MergeabilityCache mergeabilityCache;
-  private final NotesMigration notesMigration;
   private final PatchListCache patchListCache;
   private final PatchSetUtil psUtil;
   private final ProjectCache projectCache;
@@ -348,7 +249,6 @@
   private final SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory;
 
   // Required assisted injected fields.
-  private final ReviewDb db;
   private final Project.NameKey project;
   private final Change.Id legacyId;
 
@@ -391,6 +291,7 @@
   private PersonIdent committer;
   private int parentCount;
   private Integer unresolvedCommentCount;
+  private Integer totalCommentCount;
   private LabelTypes labelTypes;
 
   private ImmutableList<byte[]> refStates;
@@ -407,14 +308,12 @@
       GitRepositoryManager repoManager,
       MergeUtil.Factory mergeUtilFactory,
       MergeabilityCache mergeabilityCache,
-      NotesMigration notesMigration,
       PatchListCache patchListCache,
       PatchSetUtil psUtil,
       ProjectCache projectCache,
       TrackingFooters trackingFooters,
       PureRevert pureRevert,
       SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory,
-      @Assisted ReviewDb db,
       @Assisted Project.NameKey project,
       @Assisted Change.Id id,
       @Assisted @Nullable Change change,
@@ -427,7 +326,6 @@
     this.repoManager = repoManager;
     this.mergeUtilFactory = mergeUtilFactory;
     this.mergeabilityCache = mergeabilityCache;
-    this.notesMigration = notesMigration;
     this.patchListCache = patchListCache;
     this.psUtil = psUtil;
     this.projectCache = projectCache;
@@ -436,11 +334,6 @@
     this.pureRevert = pureRevert;
     this.submitRuleEvaluatorFactory = submitRuleEvaluatorFactory;
 
-    // May be null in tests when created via createForTest above, in which case lazy-loading will
-    // intentionally fail with NPE. Still not marked @Nullable in the constructor, to force callers
-    // using Guice to pass a non-null value.
-    this.db = db;
-
     this.project = project;
     this.legacyId = id;
 
@@ -460,22 +353,18 @@
     return this;
   }
 
-  public ReviewDb db() {
-    return db;
-  }
-
   public AllUsersName getAllUsersNameForIndexing() {
     return allUsersName;
   }
 
-  public void setCurrentFilePaths(List<String> filePaths) throws OrmException {
+  public void setCurrentFilePaths(List<String> filePaths) {
     PatchSet ps = currentPatchSet();
     if (ps != null) {
       currentFiles = ImmutableList.copyOf(filePaths);
     }
   }
 
-  public List<String> currentFilePaths() throws IOException, OrmException {
+  public List<String> currentFilePaths() {
     if (currentFiles == null) {
       if (!lazyLoad) {
         return Collections.emptyList();
@@ -486,7 +375,7 @@
     return currentFiles;
   }
 
-  private Optional<DiffSummary> getDiffSummary() throws OrmException, IOException {
+  private Optional<DiffSummary> getDiffSummary() {
     if (diffSummary == null) {
       if (!lazyLoad) {
         return Optional.empty();
@@ -514,7 +403,7 @@
     return diffSummary;
   }
 
-  private Optional<ChangedLines> computeChangedLines() throws OrmException, IOException {
+  private Optional<ChangedLines> computeChangedLines() {
     Optional<DiffSummary> ds = getDiffSummary();
     if (ds.isPresent()) {
       return Optional.of(ds.get().getChangedLines());
@@ -522,7 +411,7 @@
     return Optional.empty();
   }
 
-  public Optional<ChangedLines> changedLines() throws OrmException, IOException {
+  public Optional<ChangedLines> changedLines() {
     if (changedLines == null) {
       if (!lazyLoad) {
         return Optional.empty();
@@ -556,7 +445,7 @@
     visibleTo = user;
   }
 
-  public Change change() throws OrmException {
+  public Change change() {
     if (change == null && lazyLoad) {
       reloadChange();
     }
@@ -567,41 +456,41 @@
     change = c;
   }
 
-  public Change reloadChange() throws OrmException {
+  public Change reloadChange() {
     try {
-      notes = notesFactory.createChecked(db, project, legacyId);
+      notes = notesFactory.createChecked(project, legacyId);
     } catch (NoSuchChangeException e) {
-      throw new OrmException("Unable to load change " + legacyId, e);
+      throw new StorageException("Unable to load change " + legacyId, e);
     }
     change = notes.getChange();
     setPatchSets(null);
     return change;
   }
 
-  public LabelTypes getLabelTypes() throws OrmException {
+  public LabelTypes getLabelTypes() {
     if (labelTypes == null) {
       ProjectState state;
       try {
         state = projectCache.checkedGet(project());
       } catch (IOException e) {
-        throw new OrmException("project state not available", e);
+        throw new StorageException("project state not available", e);
       }
       labelTypes = state.getLabelTypes(change().getDest());
     }
     return labelTypes;
   }
 
-  public ChangeNotes notes() throws OrmException {
+  public ChangeNotes notes() {
     if (notes == null) {
       if (!lazyLoad) {
-        throw new OrmException("ChangeNotes not available, lazyLoad = false");
+        throw new StorageException("ChangeNotes not available, lazyLoad = false");
       }
-      notes = notesFactory.create(db, project(), legacyId);
+      notes = notesFactory.create(project(), legacyId);
     }
     return notes;
   }
 
-  public PatchSet currentPatchSet() throws OrmException {
+  public PatchSet currentPatchSet() {
     if (currentPatchSet == null) {
       Change c = change();
       if (c == null) {
@@ -617,7 +506,7 @@
     return currentPatchSet;
   }
 
-  public List<PatchSetApproval> currentApprovals() throws OrmException {
+  public List<PatchSetApproval> currentApprovals() {
     if (currentApprovals == null) {
       if (!lazyLoad) {
         return Collections.emptyList();
@@ -629,8 +518,8 @@
         try {
           currentApprovals =
               ImmutableList.copyOf(
-                  approvalsUtil.byPatchSet(db, notes(), c.currentPatchSetId(), null, null));
-        } catch (OrmException e) {
+                  approvalsUtil.byPatchSet(notes(), c.currentPatchSetId(), null, null));
+        } catch (StorageException e) {
           if (e.getCause() instanceof NoSuchChangeException) {
             currentApprovals = Collections.emptyList();
           } else {
@@ -646,7 +535,7 @@
     currentApprovals = approvals;
   }
 
-  public String commitMessage() throws IOException, OrmException {
+  public String commitMessage() {
     if (commitMessage == null) {
       if (!loadCommitData()) {
         return null;
@@ -655,7 +544,7 @@
     return commitMessage;
   }
 
-  public List<FooterLine> commitFooters() throws IOException, OrmException {
+  public List<FooterLine> commitFooters() {
     if (commitFooters == null) {
       if (!loadCommitData()) {
         return null;
@@ -664,11 +553,11 @@
     return commitFooters;
   }
 
-  public ListMultimap<String, String> trackingFooters() throws IOException, OrmException {
+  public ListMultimap<String, String> trackingFooters() {
     return trackingFooters.extract(commitFooters());
   }
 
-  public PersonIdent getAuthor() throws IOException, OrmException {
+  public PersonIdent getAuthor() {
     if (author == null) {
       if (!loadCommitData()) {
         return null;
@@ -677,7 +566,7 @@
     return author;
   }
 
-  public PersonIdent getCommitter() throws IOException, OrmException {
+  public PersonIdent getCommitter() {
     if (committer == null) {
       if (!loadCommitData()) {
         return null;
@@ -686,9 +575,7 @@
     return committer;
   }
 
-  private boolean loadCommitData()
-      throws OrmException, RepositoryNotFoundException, IOException, MissingObjectException,
-          IncorrectObjectTypeException {
+  private boolean loadCommitData() {
     PatchSet ps = currentPatchSet();
     if (ps == null) {
       return false;
@@ -702,17 +589,16 @@
       author = c.getAuthorIdent();
       committer = c.getCommitterIdent();
       parentCount = c.getParentCount();
+    } catch (IOException e) {
+      throw new StorageException(e);
     }
     return true;
   }
 
-  /**
-   * @return patches for the change, in patch set ID order.
-   * @throws OrmException an error occurred reading the database.
-   */
-  public Collection<PatchSet> patchSets() throws OrmException {
+  /** @return patches for the change, in patch set ID order. */
+  public Collection<PatchSet> patchSets() {
     if (patchSets == null) {
-      patchSets = psUtil.byChange(db, notes());
+      patchSets = psUtil.byChange(notes());
     }
     return patchSets;
   }
@@ -722,11 +608,8 @@
     this.patchSets = patchSets;
   }
 
-  /**
-   * @return patch with the given ID, or null if it does not exist.
-   * @throws OrmException an error occurred reading the database.
-   */
-  public PatchSet patchSet(PatchSet.Id psId) throws OrmException {
+  /** @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)) {
       return currentPatchSet;
     }
@@ -741,27 +624,23 @@
   /**
    * @return all patch set approvals for the change, keyed by ID, ordered by timestamp within each
    *     patch set.
-   * @throws OrmException an error occurred reading the database.
    */
-  public ListMultimap<PatchSet.Id, PatchSetApproval> approvals() throws OrmException {
+  public ListMultimap<PatchSet.Id, PatchSetApproval> approvals() {
     if (allApprovals == null) {
       if (!lazyLoad) {
         return ImmutableListMultimap.of();
       }
-      allApprovals = approvalsUtil.byChange(db, notes());
+      allApprovals = approvalsUtil.byChange(notes());
     }
     return allApprovals;
   }
 
-  /**
-   * @return The submit ('SUBM') approval label
-   * @throws OrmException an error occurred reading the database.
-   */
-  public Optional<PatchSetApproval> getSubmitApproval() throws OrmException {
+  /** @return The submit ('SUBM') approval label */
+  public Optional<PatchSetApproval> getSubmitApproval() {
     return currentApprovals().stream().filter(PatchSetApproval::isLegacySubmit).findFirst();
   }
 
-  public ReviewerSet reviewers() throws OrmException {
+  public ReviewerSet reviewers() {
     if (reviewers == null) {
       if (!lazyLoad) {
         return ReviewerSet.empty();
@@ -779,7 +658,7 @@
     return reviewers;
   }
 
-  public ReviewerByEmailSet reviewersByEmail() throws OrmException {
+  public ReviewerByEmailSet reviewersByEmail() {
     if (reviewersByEmail == null) {
       if (!lazyLoad) {
         return ReviewerByEmailSet.empty();
@@ -805,7 +684,7 @@
     return this.pendingReviewers;
   }
 
-  public ReviewerSet pendingReviewers() throws OrmException {
+  public ReviewerSet pendingReviewers() {
     if (pendingReviewers == null) {
       if (!lazyLoad) {
         return ReviewerSet.empty();
@@ -823,7 +702,7 @@
     return pendingReviewersByEmail;
   }
 
-  public ReviewerByEmailSet pendingReviewersByEmail() throws OrmException {
+  public ReviewerByEmailSet pendingReviewersByEmail() {
     if (pendingReviewersByEmail == null) {
       if (!lazyLoad) {
         return ReviewerByEmailSet.empty();
@@ -833,7 +712,7 @@
     return pendingReviewersByEmail;
   }
 
-  public List<ReviewerStatusUpdate> reviewerUpdates() throws OrmException {
+  public List<ReviewerStatusUpdate> reviewerUpdates() {
     if (reviewerUpdates == null) {
       if (!lazyLoad) {
         return Collections.emptyList();
@@ -851,17 +730,17 @@
     return reviewerUpdates;
   }
 
-  public Collection<Comment> publishedComments() throws OrmException {
+  public Collection<Comment> publishedComments() {
     if (publishedComments == null) {
       if (!lazyLoad) {
         return Collections.emptyList();
       }
-      publishedComments = commentsUtil.publishedByChange(db, notes());
+      publishedComments = commentsUtil.publishedByChange(notes());
     }
     return publishedComments;
   }
 
-  public Collection<RobotComment> robotComments() throws OrmException {
+  public Collection<RobotComment> robotComments() {
     if (robotComments == null) {
       if (!lazyLoad) {
         return Collections.emptyList();
@@ -871,7 +750,7 @@
     return robotComments;
   }
 
-  public Integer unresolvedCommentCount() throws OrmException {
+  public Integer unresolvedCommentCount() {
     if (unresolvedCommentCount == null) {
       if (!lazyLoad) {
         return null;
@@ -925,12 +804,29 @@
     this.unresolvedCommentCount = count;
   }
 
-  public List<ChangeMessage> messages() throws OrmException {
+  public Integer totalCommentCount() {
+    if (totalCommentCount == null) {
+      if (!lazyLoad) {
+        return null;
+      }
+
+      // Fail on overflow.
+      totalCommentCount =
+          Ints.checkedCast((long) publishedComments().size() + robotComments().size());
+    }
+    return totalCommentCount;
+  }
+
+  public void setTotalCommentCount(Integer count) {
+    this.totalCommentCount = count;
+  }
+
+  public List<ChangeMessage> messages() {
     if (messages == null) {
       if (!lazyLoad) {
         return Collections.emptyList();
       }
-      messages = cmUtil.byChange(db, notes());
+      messages = cmUtil.byChange(notes());
     }
     return messages;
   }
@@ -983,15 +879,15 @@
   }
 
   @Nullable
-  public Boolean isMergeable() throws OrmException {
+  public Boolean isMergeable() {
     if (mergeable == null) {
       Change c = change();
       if (c == null) {
         return null;
       }
-      if (c.getStatus() == Change.Status.MERGED) {
+      if (c.isMerged()) {
         mergeable = true;
-      } else if (c.getStatus() == Change.Status.ABANDONED) {
+      } else if (c.isAbandoned()) {
         return null;
       } else if (c.isWorkInProgress()) {
         return null;
@@ -1023,18 +919,18 @@
                   c.getDest(),
                   repo);
         } catch (IOException e) {
-          throw new OrmException(e);
+          throw new StorageException(e);
         }
       }
     }
     return mergeable;
   }
 
-  public Set<Account.Id> editsByUser() throws OrmException {
+  public Set<Account.Id> editsByUser() {
     return editRefs().keySet();
   }
 
-  public Map<Account.Id, Ref> editRefs() throws OrmException {
+  public Map<Account.Id, Ref> editRefs() {
     if (editsByUser == null) {
       if (!lazyLoad) {
         return Collections.emptyMap();
@@ -1056,17 +952,17 @@
           }
         }
       } catch (IOException e) {
-        throw new OrmException(e);
+        throw new StorageException(e);
       }
     }
     return editsByUser;
   }
 
-  public Set<Account.Id> draftsByUser() throws OrmException {
+  public Set<Account.Id> draftsByUser() {
     return draftRefs().keySet();
   }
 
-  public Map<Account.Id, Ref> draftRefs() throws OrmException {
+  public Map<Account.Id, Ref> draftRefs() {
     if (draftsByUser == null) {
       if (!lazyLoad) {
         return Collections.emptyMap();
@@ -1077,30 +973,24 @@
       }
 
       draftsByUser = new HashMap<>();
-      if (notesMigration.readChanges()) {
-        for (Ref ref : commentsUtil.getDraftRefs(notes.getChangeId())) {
-          Account.Id account = Account.Id.fromRefSuffix(ref.getName());
-          if (account != null
-              // Double-check that any drafts exist for this user after
-              // filtering out zombies. If some but not all drafts in the ref
-              // were zombies, the returned Ref still includes those zombies;
-              // this is suboptimal, but is ok for the purposes of
-              // draftsByUser(), and easier than trying to rebuild the change at
-              // this point.
-              && !notes().getDraftComments(account, ref).isEmpty()) {
-            draftsByUser.put(account, ref);
-          }
-        }
-      } else {
-        for (Comment sc : commentsUtil.draftByChange(db, notes())) {
-          draftsByUser.put(sc.author.getId(), null);
+      for (Ref ref : commentsUtil.getDraftRefs(notes.getChangeId())) {
+        Account.Id account = Account.Id.fromRefSuffix(ref.getName());
+        if (account != null
+            // Double-check that any drafts exist for this user after
+            // filtering out zombies. If some but not all drafts in the ref
+            // were zombies, the returned Ref still includes those zombies;
+            // this is suboptimal, but is ok for the purposes of
+            // draftsByUser(), and easier than trying to rebuild the change at
+            // this point.
+            && !notes().getDraftComments(account, ref).isEmpty()) {
+          draftsByUser.put(account, ref);
         }
       }
     }
     return draftsByUser;
   }
 
-  public boolean isReviewedBy(Account.Id accountId) throws OrmException {
+  public boolean isReviewedBy(Account.Id accountId) {
     Collection<String> stars = stars(accountId);
 
     PatchSet ps = currentPatchSet();
@@ -1117,7 +1007,7 @@
     return reviewedBy().contains(accountId);
   }
 
-  public Set<Account.Id> reviewedBy() throws OrmException {
+  public Set<Account.Id> reviewedBy() {
     if (reviewedBy == null) {
       if (!lazyLoad) {
         return Collections.emptySet();
@@ -1149,7 +1039,7 @@
     this.reviewedBy = reviewedBy;
   }
 
-  public Set<String> hashtags() throws OrmException {
+  public Set<String> hashtags() {
     if (hashtags == null) {
       if (!lazyLoad) {
         return Collections.emptySet();
@@ -1163,7 +1053,7 @@
     this.hashtags = hashtags;
   }
 
-  public ImmutableListMultimap<Account.Id, String> stars() throws OrmException {
+  public ImmutableListMultimap<Account.Id, String> stars() {
     if (stars == null) {
       if (!lazyLoad) {
         return ImmutableListMultimap.of();
@@ -1181,7 +1071,7 @@
     this.stars = ImmutableListMultimap.copyOf(stars);
   }
 
-  public ImmutableMap<Account.Id, StarRef> starRefs() throws OrmException {
+  public ImmutableMap<Account.Id, StarRef> starRefs() {
     if (starRefs == null) {
       if (!lazyLoad) {
         return ImmutableMap.of();
@@ -1191,7 +1081,7 @@
     return starRefs;
   }
 
-  public Set<String> stars(Account.Id accountId) throws OrmException {
+  public Set<String> stars(Account.Id accountId) {
     if (starsOf != null) {
       if (!starsOf.accountId().equals(accountId)) {
         starsOf = null;
@@ -1215,14 +1105,14 @@
    *     false otherwise.
    */
   @Nullable
-  public Boolean isPureRevert() throws OrmException {
+  public Boolean isPureRevert() {
     if (change().getRevertOf() == null) {
       return null;
     }
     try {
-      return pureRevert.get(notes(), null).isPureRevert;
+      return pureRevert.get(notes(), Optional.empty());
     } catch (IOException | BadRequestException | ResourceConflictException e) {
-      throw new OrmException("could not compute pure revert", e);
+      throw new StorageException("could not compute pure revert", e);
     }
   }
 
diff --git a/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java b/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java
index d541d18..74ad0ef 100644
--- a/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 /** Predicate over Change-Id strings (aka Change.Key). */
 public class ChangeIdPredicate extends ChangeIndexPredicate {
@@ -25,7 +24,7 @@
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
+  public boolean match(ChangeData cd) {
     Change change = cd.change();
     if (change == null) {
       return false;
diff --git a/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java b/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java
index 1eb2770..7428e3a 100644
--- a/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java
@@ -17,9 +17,22 @@
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.query.IndexPredicate;
 import com.google.gerrit.index.query.Matchable;
+import com.google.gerrit.index.query.Predicate;
 
 public abstract class ChangeIndexPredicate extends IndexPredicate<ChangeData>
     implements Matchable<ChangeData> {
+  /**
+   * Returns an index predicate that matches no changes in the index.
+   *
+   * <p>This predicate should be used in preference to a non-index predicate (such as {@code
+   * Predicate.not(Predicate.any())}), since it can be matched efficiently against the index.
+   *
+   * @return an index predicate matching no changes.
+   */
+  public static Predicate<ChangeData> none() {
+    return ChangeStatusPredicate.NONE;
+  }
+
   protected ChangeIndexPredicate(FieldDef<ChangeData, ?> def, String value) {
     super(def, value);
   }
diff --git a/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java b/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
index f81ea15..60b4d38 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.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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.index.IndexUtils;
@@ -28,7 +28,7 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -36,22 +36,20 @@
 public class ChangeIsVisibleToPredicate extends IsVisibleToPredicate<ChangeData> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  protected final Provider<ReviewDb> db;
   protected final ChangeNotes.Factory notesFactory;
   protected final CurrentUser user;
   protected final PermissionBackend permissionBackend;
   protected final ProjectCache projectCache;
   private final Provider<AnonymousUser> anonymousUserProvider;
 
+  @Inject
   public ChangeIsVisibleToPredicate(
-      Provider<ReviewDb> db,
       ChangeNotes.Factory notesFactory,
       CurrentUser user,
       PermissionBackend permissionBackend,
       ProjectCache projectCache,
       Provider<AnonymousUser> anonymousUserProvider) {
     super(ChangeQueryBuilder.FIELD_VISIBLETO, IndexUtils.describe(user));
-    this.db = db;
     this.notesFactory = notesFactory;
     this.user = user;
     this.permissionBackend = permissionBackend;
@@ -60,7 +58,7 @@
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
+  public boolean match(ChangeData cd) {
     if (cd.fastIsVisibleTo(user)) {
       return true;
     }
@@ -82,7 +80,7 @@
         return false;
       }
     } catch (IOException e) {
-      throw new OrmException("unable to read project state", e);
+      throw new StorageException("unable to read project state", e);
     }
 
     PermissionBackend.WithUser withUser =
@@ -90,7 +88,7 @@
             ? permissionBackend.absentUser(user.getAccountId())
             : permissionBackend.user(anonymousUserProvider.get());
     try {
-      withUser.indexedChange(cd, notes).database(db).check(ChangePermission.READ);
+      withUser.indexedChange(cd, notes).check(ChangePermission.READ);
     } catch (PermissionBackendException e) {
       Throwable cause = e.getCause();
       if (cause instanceof RepositoryNotFoundException) {
@@ -99,7 +97,7 @@
             cd, cd.project());
         return false;
       }
-      throw new OrmException("unable to check permissions on change " + cd.getId(), e);
+      throw new StorageException("unable to check permissions on change " + cd.getId(), e);
     } catch (AuthException e) {
       logger.atFine().log("Filter out non-visisble change: %s", cd);
       return false;
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 4cc119f..d306e54 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -14,7 +14,9 @@
 
 package com.google.gerrit.server.query.change;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.gerrit.reviewdb.client.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;
 import static java.util.stream.Collectors.toSet;
@@ -27,9 +29,9 @@
 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.common.errors.NotSignedInException;
+import com.google.gerrit.exceptions.NotSignedInException;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.registration.DynamicMap;
-import com.google.gerrit.extensions.registration.Extension;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.SchemaUtil;
@@ -44,7 +46,6 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.CurrentUser;
@@ -52,6 +53,8 @@
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException;
+import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupBackends;
 import com.google.gerrit.server.account.GroupMembers;
@@ -60,20 +63,21 @@
 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;
 import com.google.gerrit.server.index.change.ChangeIndexRewriter;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.ChildProjects;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.submit.SubmitDryRun;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
@@ -92,10 +96,11 @@
 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. */
-public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
+public class ChangeQueryBuilder extends QueryBuilder<ChangeData, ChangeQueryBuilder> {
   public interface ChangeOperatorFactory extends OperatorFactory<ChangeData, ChangeQueryBuilder> {}
 
   /**
@@ -121,8 +126,8 @@
 
   static final int MAX_ACCOUNTS_PER_DEFAULT_FIELD = 10;
 
-  // NOTE: As new search operations are added, please keep the
-  // SearchSuggestOracle up to date.
+  // NOTE: As new search operations are added, please keep the suggestions in
+  // gr-search-bar.js up to date.
 
   public static final String FIELD_ADDED = "added";
   public static final String FIELD_AGE = "age";
@@ -136,7 +141,11 @@
   public static final String FIELD_COMMENTBY = "commentby";
   public static final String FIELD_COMMIT = "commit";
   public static final String FIELD_COMMITTER = "committer";
+  public static final String FIELD_DIRECTORY = "directory";
   public static final String FIELD_EXACTCOMMITTER = "exactcommitter";
+  public static final String FIELD_EXTENSION = "extension";
+  public static final String FIELD_ONLY_EXTENSIONS = "onlyextensions";
+  public static final String FIELD_FOOTER = "footer";
   public static final String FIELD_CONFLICTS = "conflicts";
   public static final String FIELD_DELETED = "deleted";
   public static final String FIELD_DELTA = "delta";
@@ -206,12 +215,10 @@
     final GroupBackend groupBackend;
     final IdentifiedUser.GenericFactory userFactory;
     final IndexConfig indexConfig;
-    final NotesMigration notesMigration;
     final PatchListCache patchListCache;
     final ProjectCache projectCache;
     final Provider<InternalChangeQuery> queryProvider;
     final ChildProjects childProjects;
-    final Provider<ReviewDb> db;
     final StarredChangesUtil starredChangesUtil;
     final SubmitDryRun submitDryRun;
     final GroupMembers groupMembers;
@@ -222,7 +229,6 @@
     @Inject
     @VisibleForTesting
     public Arguments(
-        Provider<ReviewDb> db,
         Provider<InternalChangeQuery> queryProvider,
         ChangeIndexRewriter rewriter,
         DynamicMap<ChangeOperatorFactory> opFactories,
@@ -247,11 +253,9 @@
         IndexConfig indexConfig,
         StarredChangesUtil starredChangesUtil,
         AccountCache accountCache,
-        NotesMigration notesMigration,
         GroupMembers groupMembers,
         Provider<AnonymousUser> anonymousUserProvider) {
       this(
-          db,
           queryProvider,
           rewriter,
           opFactories,
@@ -276,13 +280,11 @@
           indexConfig,
           starredChangesUtil,
           accountCache,
-          notesMigration,
           groupMembers,
           anonymousUserProvider);
     }
 
     private Arguments(
-        Provider<ReviewDb> db,
         Provider<InternalChangeQuery> queryProvider,
         ChangeIndexRewriter rewriter,
         DynamicMap<ChangeOperatorFactory> opFactories,
@@ -307,10 +309,8 @@
         IndexConfig indexConfig,
         StarredChangesUtil starredChangesUtil,
         AccountCache accountCache,
-        NotesMigration notesMigration,
         GroupMembers groupMembers,
         Provider<AnonymousUser> anonymousUserProvider) {
-      this.db = db;
       this.queryProvider = queryProvider;
       this.rewriter = rewriter;
       this.opFactories = opFactories;
@@ -335,14 +335,12 @@
       this.starredChangesUtil = starredChangesUtil;
       this.accountCache = accountCache;
       this.hasOperands = hasOperands;
-      this.notesMigration = notesMigration;
       this.groupMembers = groupMembers;
       this.anonymousUserProvider = anonymousUserProvider;
     }
 
     Arguments asUser(CurrentUser otherUser) {
       return new Arguments(
-          db,
           queryProvider,
           rewriter,
           opFactories,
@@ -367,7 +365,6 @@
           indexConfig,
           starredChangesUtil,
           accountCache,
-          notesMigration,
           groupMembers,
           anonymousUserProvider);
     }
@@ -411,27 +408,19 @@
 
   private final Arguments args;
 
+  private @Inject @GerritServerConfig Config cfg;
+
   @Inject
   ChangeQueryBuilder(Arguments args) {
-    super(mydef);
-    this.args = args;
-    setupDynamicOperators();
+    this(mydef, args);
   }
 
   @VisibleForTesting
-  protected ChangeQueryBuilder(
-      Definition<ChangeData, ? extends QueryBuilder<ChangeData>> def, Arguments args) {
-    super(def);
+  protected ChangeQueryBuilder(Definition<ChangeData, ChangeQueryBuilder> def, Arguments args) {
+    super(def, args.opFactories);
     this.args = args;
   }
 
-  private void setupDynamicOperators() {
-    for (Extension<ChangeOperatorFactory> e : args.opFactories) {
-      String name = e.getExportName() + "_" + e.getPluginName();
-      opFactories.put(name, e.getProvider().get());
-    }
-  }
-
   public Arguments getArgs() {
     return args;
   }
@@ -563,9 +552,9 @@
       if (args.getSchema().hasField(ChangeField.WIP)) {
         return Predicate.and(
             Predicate.not(new BooleanPredicate(ChangeField.WIP)),
-            ReviewerPredicate.reviewer(args, self()));
+            ReviewerPredicate.reviewer(self()));
       }
-      return ReviewerPredicate.reviewer(args, self());
+      return ReviewerPredicate.reviewer(self());
     }
 
     if ("cc".equalsIgnoreCase(value)) {
@@ -624,7 +613,7 @@
   }
 
   @Operator
-  public Predicate<ChangeData> conflicts(String value) throws OrmException, QueryParseException {
+  public Predicate<ChangeData> conflicts(String value) throws QueryParseException {
     List<Change> changes = parseChange(value);
     List<Predicate<ChangeData>> or = new ArrayList<>(changes.size());
     for (Change c : changes) {
@@ -745,16 +734,76 @@
   }
 
   @Operator
+  public Predicate<ChangeData> ext(String ext) throws QueryParseException {
+    return extension(ext);
+  }
+
+  @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");
+  }
+
+  @Operator
+  public Predicate<ChangeData> onlyexts(String extList) throws QueryParseException {
+    return onlyextensions(extList);
+  }
+
+  @Operator
+  public Predicate<ChangeData> onlyextensions(String extList) throws QueryParseException {
+    if (args.getSchema().hasField(ChangeField.ONLY_EXTENSIONS)) {
+      return new FileExtensionListPredicate(extList);
+    }
+    throw new QueryParseException(
+        "'onlyextensions' operator is not supported by change index version");
+  }
+
+  @Operator
+  public Predicate<ChangeData> footer(String footer) throws QueryParseException {
+    if (args.getSchema().hasField(ChangeField.FOOTER)) {
+      return new FooterPredicate(footer);
+    }
+    throw new QueryParseException("'footer' operator is not supported by change index version");
+  }
+
+  @Operator
+  public Predicate<ChangeData> dir(String directory) throws QueryParseException {
+    return directory(directory);
+  }
+
+  @Operator
+  public Predicate<ChangeData> directory(String directory) throws QueryParseException {
+    if (args.getSchema().hasField(ChangeField.DIRECTORY)) {
+      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");
+  }
+
+  @Operator
   public Predicate<ChangeData> label(String name)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
+      throws QueryParseException, IOException, ConfigInvalidException {
     Set<Account.Id> accounts = null;
     AccountGroup.UUID group = null;
 
     // Parse for:
-    // label:CodeReview=1,user=jsmith or
-    // label:CodeReview=1,jsmith or
-    // label:CodeReview=1,group=android_approvers or
-    // label:CodeReview=1,android_approvers
+    // label:Code-Review=1,user=jsmith or
+    // label:Code-Review=1,jsmith or
+    // label:Code-Review=1,group=android_approvers or
+    // label:Code-Review=1,android_approvers
     // user/groups without a label will first attempt to match user
     // Special case: votes by owners can be tracked with ",owner":
     // label:Code-Review+2,owner
@@ -846,7 +895,7 @@
 
   @Operator
   public Predicate<ChangeData> starredby(String who)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
+      throws QueryParseException, IOException, ConfigInvalidException {
     return starredby(parseAccount(who));
   }
 
@@ -864,7 +913,7 @@
 
   @Operator
   public Predicate<ChangeData> watchedby(String who)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
+      throws QueryParseException, IOException, ConfigInvalidException {
     Set<Account.Id> m = parseAccount(who);
     List<IsWatchedByPredicate> p = Lists.newArrayListWithCapacity(m.size());
 
@@ -889,7 +938,7 @@
 
   @Operator
   public Predicate<ChangeData> draftby(String who)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
+      throws QueryParseException, IOException, ConfigInvalidException {
     Set<Account.Id> m = parseAccount(who);
     List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(m.size());
     for (Account.Id id : m) {
@@ -902,26 +951,24 @@
     return new HasDraftByPredicate(who);
   }
 
-  private boolean isSelf(String who) {
-    return "self".equals(who) || "me".equals(who);
-  }
-
   @Operator
   public Predicate<ChangeData> visibleto(String who)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
+      throws QueryParseException, IOException, ConfigInvalidException {
     if (isSelf(who)) {
       return is_visible();
     }
-    Set<Account.Id> m = args.accountResolver.findAll(who);
-    if (!m.isEmpty()) {
-      List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(m.size());
-      for (Account.Id id : m) {
-        return visibleto(args.userFactory.create(id));
+    try {
+      return Predicate.or(
+          parseAccount(who).stream()
+              .map(a -> visibleto(args.userFactory.create(a)))
+              .collect(toImmutableList()));
+    } catch (QueryParseException e) {
+      if (e instanceof QueryRequiresAuthException) {
+        throw e;
       }
-      return Predicate.or(p);
+      // Otherwise continue: if it's not an account, maybe it's a group?
     }
 
-    // If its not an account, maybe its a group?
     Collection<GroupReference> suggestions = args.groupBackend.suggest(who, null);
     if (!suggestions.isEmpty()) {
       HashSet<AccountGroup.UUID> ids = new HashSet<>();
@@ -936,7 +983,6 @@
 
   public Predicate<ChangeData> visibleto(CurrentUser user) {
     return new ChangeIsVisibleToPredicate(
-        args.db,
         args.notesFactory,
         user,
         args.permissionBackend,
@@ -950,14 +996,14 @@
 
   @Operator
   public Predicate<ChangeData> o(String who)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
+      throws QueryParseException, IOException, ConfigInvalidException {
     return owner(who);
   }
 
   @Operator
   public Predicate<ChangeData> owner(String who)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
-    return owner(parseAccount(who));
+      throws QueryParseException, IOException, ConfigInvalidException {
+    return owner(parseAccount(who, (AccountState s) -> true));
   }
 
   private Predicate<ChangeData> owner(Set<Account.Id> who) {
@@ -969,7 +1015,7 @@
   }
 
   private Predicate<ChangeData> ownerDefaultField(String who)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
+      throws QueryParseException, IOException, ConfigInvalidException {
     Set<Account.Id> accounts = parseAccount(who);
     if (accounts.size() > MAX_ACCOUNTS_PER_DEFAULT_FIELD) {
       return Predicate.any();
@@ -979,8 +1025,8 @@
 
   @Operator
   public Predicate<ChangeData> assignee(String who)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
-    return assignee(parseAccount(who));
+      throws QueryParseException, IOException, ConfigInvalidException {
+    return assignee(parseAccount(who, (AccountState s) -> true));
   }
 
   private Predicate<ChangeData> assignee(Set<Account.Id> who) {
@@ -1014,23 +1060,23 @@
 
   @Operator
   public Predicate<ChangeData> r(String who)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
+      throws QueryParseException, IOException, ConfigInvalidException {
     return reviewer(who);
   }
 
   @Operator
   public Predicate<ChangeData> reviewer(String who)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
+      throws QueryParseException, IOException, ConfigInvalidException {
     return reviewer(who, false);
   }
 
   private Predicate<ChangeData> reviewerDefaultField(String who)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
+      throws QueryParseException, IOException, ConfigInvalidException {
     return reviewer(who, true);
   }
 
   private Predicate<ChangeData> reviewer(String who, boolean forDefaultField)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
+      throws QueryParseException, IOException, ConfigInvalidException {
     Predicate<ChangeData> byState =
         reviewerByState(who, ReviewerStateInternal.REVIEWER, forDefaultField);
     if (Objects.equals(byState, Predicate.<ChangeData>any())) {
@@ -1044,7 +1090,7 @@
 
   @Operator
   public Predicate<ChangeData> cc(String who)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
+      throws QueryParseException, IOException, ConfigInvalidException {
     return reviewerByState(who, ReviewerStateInternal.CC, false);
   }
 
@@ -1098,7 +1144,7 @@
 
   @Operator
   public Predicate<ChangeData> commentby(String who)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
+      throws QueryParseException, IOException, ConfigInvalidException {
     return commentby(parseAccount(who));
   }
 
@@ -1112,7 +1158,7 @@
 
   @Operator
   public Predicate<ChangeData> from(String who)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
+      throws QueryParseException, IOException, ConfigInvalidException {
     Set<Account.Id> ownerIds = parseAccount(who);
     return Predicate.or(owner(ownerIds), commentby(ownerIds));
   }
@@ -1137,7 +1183,7 @@
 
   @Operator
   public Predicate<ChangeData> reviewedby(String who)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
+      throws QueryParseException, IOException, ConfigInvalidException {
     return IsReviewedPredicate.create(parseAccount(who));
   }
 
@@ -1227,7 +1273,7 @@
       if (!Objects.equals(p, Predicate.<ChangeData>any())) {
         predicates.add(p);
       }
-    } catch (OrmException | IOException | ConfigInvalidException | QueryParseException e) {
+    } catch (StorageException | IOException | ConfigInvalidException | QueryParseException e) {
       // Skip.
     }
     try {
@@ -1235,13 +1281,13 @@
       if (!Objects.equals(p, Predicate.<ChangeData>any())) {
         predicates.add(p);
       }
-    } catch (OrmException | IOException | ConfigInvalidException | QueryParseException e) {
+    } catch (StorageException | IOException | ConfigInvalidException | QueryParseException e) {
       // Skip.
     }
     predicates.add(file(query));
     try {
       predicates.add(label(query));
-    } catch (OrmException | IOException | ConfigInvalidException | QueryParseException e) {
+    } catch (StorageException | IOException | ConfigInvalidException | QueryParseException e) {
       // Skip.
     }
     predicates.add(commit(query));
@@ -1295,15 +1341,28 @@
   }
 
   private Set<Account.Id> parseAccount(String who)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
-    if (isSelf(who)) {
-      return Collections.singleton(self());
+      throws QueryParseException, IOException, ConfigInvalidException {
+    try {
+      return args.accountResolver.resolve(who).asNonEmptyIdSet();
+    } catch (UnresolvableAccountException e) {
+      if (e.isSelf()) {
+        throw new QueryRequiresAuthException(e.getMessage(), e);
+      }
+      throw new QueryParseException(e.getMessage(), e);
     }
-    Set<Account.Id> matches = args.accountResolver.findAll(who);
-    if (matches.isEmpty()) {
-      throw error("User " + who + " not found");
+  }
+
+  private Set<Account.Id> parseAccount(
+      String who, java.util.function.Predicate<AccountState> activityFilter)
+      throws QueryParseException, IOException, ConfigInvalidException {
+    try {
+      return args.accountResolver.resolve(who, activityFilter).asNonEmptyIdSet();
+    } catch (UnresolvableAccountException e) {
+      if (e.isSelf()) {
+        throw new QueryRequiresAuthException(e.getMessage(), e);
+      }
+      throw new QueryParseException(e.getMessage(), e);
     }
-    return matches;
   }
 
   private GroupReference parseGroup(String group) throws QueryParseException {
@@ -1314,7 +1373,7 @@
     return g;
   }
 
-  private List<Change> parseChange(String value) throws OrmException, QueryParseException {
+  private List<Change> parseChange(String value) throws QueryParseException {
     if (PAT_LEGACY_ID.matcher(value).matches()) {
       return asChanges(args.queryProvider.get().byLegacyChangeId(Change.Id.parse(value)));
     } else if (PAT_CHANGE_ID.matcher(value).matches()) {
@@ -1341,7 +1400,7 @@
 
   public Predicate<ChangeData> reviewerByState(
       String who, ReviewerStateInternal state, boolean forDefaultField)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
+      throws QueryParseException, IOException, ConfigInvalidException {
     Predicate<ChangeData> reviewerByEmailPredicate = null;
     if (args.index.getSchema().hasField(ChangeField.REVIEWER_BY_EMAIL)) {
       Address address = Address.tryParse(who);
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java b/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
index 9a49ffe..f9263a9 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
@@ -17,20 +17,25 @@
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_LIMIT;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
 import com.google.gerrit.extensions.common.PluginDefinedInfo;
-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.index.IndexConfig;
 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.QueryProcessor;
 import com.google.gerrit.metrics.MetricMaker;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.DynamicOptions;
 import com.google.gerrit.server.DynamicOptions.DynamicBean;
 import com.google.gerrit.server.account.AccountLimits;
+import com.google.gerrit.server.change.ChangeAttributeFactory;
+import com.google.gerrit.server.change.PluginDefinedAttributesFactories;
+import com.google.gerrit.server.change.PluginDefinedAttributesFactory;
 import com.google.gerrit.server.index.change.ChangeIndexCollection;
 import com.google.gerrit.server.index.change.ChangeIndexRewriter;
 import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
@@ -40,9 +45,7 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -53,21 +56,10 @@
  * holding on to a single instance.
  */
 public class ChangeQueryProcessor extends QueryProcessor<ChangeData>
-    implements DynamicOptions.BeanReceiver, PluginDefinedAttributesFactory {
-  /**
-   * Register a ChangeAttributeFactory in a config Module like this:
-   *
-   * <p>bind(ChangeAttributeFactory.class) .annotatedWith(Exports.named("export-name"))
-   * .to(YourClass.class);
-   */
-  public interface ChangeAttributeFactory {
-    PluginDefinedInfo create(ChangeData a, ChangeQueryProcessor qp, String plugin);
-  }
-
-  private final Provider<ReviewDb> db;
+    implements DynamicOptions.BeanReceiver, DynamicOptions.BeanProvider {
   private final Provider<CurrentUser> userProvider;
   private final ChangeNotes.Factory notesFactory;
-  private final DynamicMap<ChangeAttributeFactory> attributeFactories;
+  private final ImmutableListMultimap<String, ChangeAttributeFactory> attributeFactoriesByPlugin;
   private final PermissionBackend permissionBackend;
   private final ProjectCache projectCache;
   private final Provider<AnonymousUser> anonymousUserProvider;
@@ -88,9 +80,8 @@
       IndexConfig indexConfig,
       ChangeIndexCollection indexes,
       ChangeIndexRewriter rewriter,
-      Provider<ReviewDb> db,
       ChangeNotes.Factory notesFactory,
-      DynamicMap<ChangeAttributeFactory> attributeFactories,
+      DynamicSet<ChangeAttributeFactory> attributeFactories,
       PermissionBackend permissionBackend,
       ProjectCache projectCache,
       Provider<AnonymousUser> anonymousUserProvider) {
@@ -102,13 +93,18 @@
         rewriter,
         FIELD_LIMIT,
         () -> limitsFactory.create(userProvider.get()).getQueryLimit());
-    this.db = db;
     this.userProvider = userProvider;
     this.notesFactory = notesFactory;
-    this.attributeFactories = attributeFactories;
     this.permissionBackend = permissionBackend;
     this.projectCache = projectCache;
     this.anonymousUserProvider = anonymousUserProvider;
+
+    ImmutableListMultimap.Builder<String, ChangeAttributeFactory> factoriesBuilder =
+        ImmutableListMultimap.builder();
+    // Eagerly call Extension#get() rather than storing Extensions, since that method invokes the
+    // Provider on every call, which could be expensive if we invoke it once for every change.
+    attributeFactories.entries().forEach(e -> factoriesBuilder.put(e.getPluginName(), e.get()));
+    attributeFactoriesByPlugin = factoriesBuilder.build();
   }
 
   @Override
@@ -128,32 +124,21 @@
     dynamicBeans.put(plugin, dynamicBean);
   }
 
+  @Override
   public DynamicBean getDynamicBean(String plugin) {
     return dynamicBeans.get(plugin);
   }
 
-  @Override
-  public List<PluginDefinedInfo> create(ChangeData cd) {
-    List<PluginDefinedInfo> plugins = new ArrayList<>(attributeFactories.plugins().size());
-    for (String plugin : attributeFactories.plugins()) {
-      for (Provider<ChangeAttributeFactory> provider :
-          attributeFactories.byPlugin(plugin).values()) {
-        PluginDefinedInfo pda = null;
-        try {
-          pda = provider.get().create(cd, this, plugin);
-        } catch (RuntimeException e) {
-          /* Eat runtime exceptions so that queries don't fail. */
-        }
-        if (pda != null) {
-          pda.name = plugin;
-          plugins.add(pda);
-        }
-      }
-    }
-    if (plugins.isEmpty()) {
-      plugins = null;
-    }
-    return plugins;
+  public PluginDefinedAttributesFactory getAttributesFactory() {
+    return this::buildPluginInfo;
+  }
+
+  private ImmutableList<PluginDefinedInfo> buildPluginInfo(ChangeData cd) {
+    return PluginDefinedAttributesFactories.createAll(
+        cd,
+        this,
+        attributeFactoriesByPlugin.entries().stream()
+            .map(e -> new Extension<>(e.getKey(), e::getValue)));
   }
 
   @Override
@@ -161,7 +146,6 @@
     return new AndChangeSource(
         pred,
         new ChangeIsVisibleToPredicate(
-            db,
             notesFactory,
             userProvider.get(),
             permissionBackend,
diff --git a/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java b/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
index 8dc17d3..66790e7 100644
--- a/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -41,7 +40,7 @@
  */
 public final class ChangeStatusPredicate extends ChangeIndexPredicate {
   private static final String INVALID_STATUS = "__invalid__";
-  private static final Predicate<ChangeData> NONE = new ChangeStatusPredicate(null);
+  static final Predicate<ChangeData> NONE = new ChangeStatusPredicate(null);
 
   private static final TreeMap<String, Predicate<ChangeData>> PREDICATES;
   private static final Predicate<ChangeData> CLOSED;
@@ -119,7 +118,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     Change change = object.change();
     return change != null && Objects.equals(status, change.getStatus());
   }
diff --git a/java/com/google/gerrit/server/query/change/CommentByPredicate.java b/java/com/google/gerrit/server/query/change/CommentByPredicate.java
index 7ad7afe..0747bb2 100644
--- a/java/com/google/gerrit/server/query/change/CommentByPredicate.java
+++ b/java/com/google/gerrit/server/query/change/CommentByPredicate.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.Comment;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 import java.util.Objects;
 
 public class CommentByPredicate extends ChangeIndexPredicate {
@@ -34,7 +33,7 @@
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
+  public boolean match(ChangeData cd) {
     for (ChangeMessage m : cd.messages()) {
       if (Objects.equals(m.getAuthor(), id)) {
         return true;
diff --git a/java/com/google/gerrit/server/query/change/CommentPredicate.java b/java/com/google/gerrit/server/query/change/CommentPredicate.java
index 5a6d186..d193bb6 100644
--- a/java/com/google/gerrit/server/query/change/CommentPredicate.java
+++ b/java/com/google/gerrit/server/query/change/CommentPredicate.java
@@ -14,12 +14,12 @@
 
 package com.google.gerrit.server.query.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.server.index.change.ChangeField;
 import com.google.gerrit.server.index.change.ChangeIndex;
 import com.google.gerrit.server.index.change.IndexedChangeQuery;
-import com.google.gwtorm.server.OrmException;
 
 public class CommentPredicate extends ChangeIndexPredicate {
   protected final ChangeIndex index;
@@ -30,7 +30,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     try {
       Predicate<ChangeData> p = Predicate.and(new LegacyChangeIdPredicate(object.getId()), this);
       for (ChangeData cData : index.getSource(p, IndexedChangeQuery.oneResult()).read()) {
@@ -39,7 +39,7 @@
         }
       }
     } catch (QueryParseException e) {
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
 
     return false;
diff --git a/java/com/google/gerrit/server/query/change/CommitPredicate.java b/java/com/google/gerrit/server/query/change/CommitPredicate.java
index d1ae529..567f58d 100644
--- a/java/com/google/gerrit/server/query/change/CommitPredicate.java
+++ b/java/com/google/gerrit/server/query/change/CommitPredicate.java
@@ -20,7 +20,6 @@
 
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwtorm.server.OrmException;
 
 public class CommitPredicate extends ChangeIndexPredicate {
   static FieldDef<ChangeData, ?> commitField(String id) {
@@ -35,7 +34,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     String id = getValue().toLowerCase();
     for (PatchSet p : object.patchSets()) {
       if (equals(p, id)) {
diff --git a/java/com/google/gerrit/server/query/change/CommitterPredicate.java b/java/com/google/gerrit/server/query/change/CommitterPredicate.java
index 797cb9d..1dcf97f 100644
--- a/java/com/google/gerrit/server/query/change/CommitterPredicate.java
+++ b/java/com/google/gerrit/server/query/change/CommitterPredicate.java
@@ -18,8 +18,6 @@
 import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_COMMITTER;
 
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
-import java.io.IOException;
 
 public class CommitterPredicate extends ChangeIndexPredicate {
   public CommitterPredicate(String value) {
@@ -27,12 +25,8 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
-    try {
-      return ChangeField.getCommitterParts(object).contains(getValue().toLowerCase());
-    } catch (IOException e) {
-      throw new OrmException(e);
-    }
+  public boolean match(ChangeData object) {
+    return ChangeField.getCommitterParts(object).contains(getValue().toLowerCase());
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/query/change/ConflictKey.java b/java/com/google/gerrit/server/query/change/ConflictKey.java
index 42f5b13..01fdbfa 100644
--- a/java/com/google/gerrit/server/query/change/ConflictKey.java
+++ b/java/com/google/gerrit/server/query/change/ConflictKey.java
@@ -20,10 +20,10 @@
 import com.google.common.base.Enums;
 import com.google.common.collect.Ordering;
 import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.proto.Protos;
 import com.google.gerrit.server.cache.proto.Cache.ConflictKeyProto;
 import com.google.gerrit.server.cache.serialize.CacheSerializer;
-import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers;
-import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers.ObjectIdConverter;
+import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.ObjectId;
 
@@ -70,7 +70,7 @@
     @Override
     public byte[] serialize(ConflictKey object) {
       ObjectIdConverter idConverter = ObjectIdConverter.create();
-      return ProtoCacheSerializers.toByteArray(
+      return Protos.toByteArray(
           ConflictKeyProto.newBuilder()
               .setCommit(idConverter.toByteString(object.commit()))
               .setOtherCommit(idConverter.toByteString(object.otherCommit()))
@@ -81,7 +81,7 @@
 
     @Override
     public ConflictKey deserialize(byte[] in) {
-      ConflictKeyProto proto = ProtoCacheSerializers.parseUnchecked(ConflictKeyProto.parser(), in);
+      ConflictKeyProto proto = Protos.parseUnchecked(ConflictKeyProto.parser(), in);
       ObjectIdConverter idConverter = ObjectIdConverter.create();
       return create(
           idConverter.fromByteString(proto.getCommit()),
diff --git a/java/com/google/gerrit/server/query/change/ConflictsPredicate.java b/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
index 7dc7a0b..d415f71 100644
--- a/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
@@ -14,13 +14,20 @@
 
 package com.google.gerrit.server.query.change;
 
+import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.common.flogger.LazyArgs.lazy;
+import static java.util.concurrent.TimeUnit.MINUTES;
+
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.SubmitTypeRecord;
+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;
@@ -29,7 +36,6 @@
 import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments;
 import com.google.gerrit.server.submit.IntegrationException;
 import com.google.gerrit.server.submit.SubmitDryRun;
-import com.google.gwtorm.server.OrmException;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -41,20 +47,27 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 
 public class ConflictsPredicate {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   // UI code may depend on this string, so use caution when changing.
   protected static final String TOO_MANY_FILES = "too many files to find conflicts";
 
   private ConflictsPredicate() {}
 
   public static Predicate<ChangeData> create(Arguments args, String value, Change c)
-      throws QueryParseException, OrmException {
+      throws QueryParseException {
     ChangeData cd;
     List<String> files;
     try {
-      cd = args.changeDataFactory.create(args.db.get(), c);
+      cd = args.changeDataFactory.create(c);
       files = cd.currentFilePaths();
-    } catch (IOException e) {
-      throw new OrmException(e);
+    } catch (StorageException e) {
+      warnWithOccasionalStackTrace(
+          e,
+          "Error constructing conflicts predicates for change %s in %s",
+          c.getId(),
+          c.getProject());
+      return ChangeIndexPredicate.none();
     }
 
     if (3 + files.size() > args.indexConfig.maxTerms()) {
@@ -95,51 +108,66 @@
     }
 
     @Override
-    public boolean match(ChangeData object) throws OrmException {
-      Change otherChange = object.change();
-      if (otherChange == null || !otherChange.getDest().equals(dest)) {
-        return false;
-      }
-
-      SubmitTypeRecord str = object.submitTypeRecord();
-      if (!str.isOk()) {
-        return false;
-      }
-
-      ProjectState projectState;
+    public boolean match(ChangeData object) {
+      Change.Id id = object.getId();
+      Project.NameKey otherProject = null;
+      ObjectId other = null;
       try {
-        projectState = changeDataCache.getProjectState();
-      } catch (NoSuchProjectException e) {
-        return false;
-      }
+        Change otherChange = object.change();
+        if (otherChange == null || !otherChange.getDest().equals(dest)) {
+          return false;
+        }
+        otherProject = otherChange.getProject();
 
-      ObjectId other = ObjectId.fromString(object.currentPatchSet().getRevision().get());
-      ConflictKey conflictsKey =
-          ConflictKey.create(
-              changeDataCache.getTestAgainst(),
-              other,
-              str.type,
-              projectState.is(BooleanProjectConfig.USE_CONTENT_MERGE));
-      Boolean maybeConflicts = args.conflictsCache.getIfPresent(conflictsKey);
-      if (maybeConflicts != null) {
-        return maybeConflicts;
-      }
+        SubmitTypeRecord str = object.submitTypeRecord();
+        if (!str.isOk()) {
+          return false;
+        }
 
-      try (Repository repo = args.repoManager.openRepository(otherChange.getProject());
-          CodeReviewRevWalk rw = CodeReviewCommit.newRevWalk(repo)) {
-        boolean conflicts =
-            !args.submitDryRun.run(
-                str.type,
-                repo,
-                rw,
-                otherChange.getDest(),
+        ProjectState projectState;
+        try {
+          projectState = changeDataCache.getProjectState();
+        } catch (NoSuchProjectException e) {
+          return false;
+        }
+
+        other = ObjectId.fromString(object.currentPatchSet().getRevision().get());
+        ConflictKey conflictsKey =
+            ConflictKey.create(
                 changeDataCache.getTestAgainst(),
                 other,
-                getAlreadyAccepted(repo, rw));
-        args.conflictsCache.put(conflictsKey, conflicts);
-        return conflicts;
-      } catch (IntegrationException | NoSuchProjectException | IOException e) {
-        throw new OrmException(e);
+                str.type,
+                projectState.is(BooleanProjectConfig.USE_CONTENT_MERGE));
+        Boolean maybeConflicts = args.conflictsCache.getIfPresent(conflictsKey);
+        if (maybeConflicts != null) {
+          return maybeConflicts;
+        }
+
+        try (Repository repo = args.repoManager.openRepository(otherChange.getProject());
+            CodeReviewRevWalk rw = CodeReviewCommit.newRevWalk(repo)) {
+          boolean conflicts =
+              !args.submitDryRun.run(
+                  null,
+                  str.type,
+                  repo,
+                  rw,
+                  otherChange.getDest(),
+                  changeDataCache.getTestAgainst(),
+                  other,
+                  getAlreadyAccepted(repo, rw));
+          args.conflictsCache.put(conflictsKey, conflicts);
+          return conflicts;
+        }
+      } catch (IntegrationException | NoSuchProjectException | StorageException | IOException e) {
+        ObjectId finalOther = other;
+        warnWithOccasionalStackTrace(
+            e,
+            "Merge failure checking conflicts of change %s in %s (%s): %s",
+            id,
+            firstNonNull(otherProject, "unknown project"),
+            lazy(() -> finalOther != null ? finalOther.name() : "unknown commit"),
+            e.getMessage());
+        return false;
       }
     }
 
@@ -158,7 +186,7 @@
           accepted.add(rw.parseCommit(tip));
         }
         return accepted;
-      } catch (OrmException | IOException e) {
+      } catch (StorageException | IOException e) {
         throw new IntegrationException("Failed to determine already accepted commits.", e);
       }
     }
@@ -177,7 +205,7 @@
       this.projectCache = projectCache;
     }
 
-    ObjectId getTestAgainst() throws OrmException {
+    ObjectId getTestAgainst() {
       if (testAgainst == null) {
         testAgainst = ObjectId.fromString(cd.currentPatchSet().getRevision().get());
       }
@@ -201,4 +229,13 @@
       return alreadyAccepted;
     }
   }
+
+  private static void warnWithOccasionalStackTrace(Throwable cause, String format, Object... args) {
+    logger.atWarning().logVarargs(format, args);
+    logger
+        .atWarning()
+        .withCause(cause)
+        .atMostEvery(1, MINUTES)
+        .logVarargs("(Re-logging with stack trace) " + format, args);
+  }
 }
diff --git a/java/com/google/gerrit/server/query/change/DeletedPredicate.java b/java/com/google/gerrit/server/query/change/DeletedPredicate.java
index 6232fc5..d4bdc67 100644
--- a/java/com/google/gerrit/server/query/change/DeletedPredicate.java
+++ b/java/com/google/gerrit/server/query/change/DeletedPredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 public class DeletedPredicate extends IntegerRangeChangePredicate {
   public DeletedPredicate(String value) throws QueryParseException {
@@ -24,7 +23,7 @@
   }
 
   @Override
-  protected Integer getValueInt(ChangeData changeData) throws OrmException {
+  protected Integer getValueInt(ChangeData changeData) {
     return ChangeField.DELETED.get(changeData);
   }
 }
diff --git a/java/com/google/gerrit/server/query/change/DeltaPredicate.java b/java/com/google/gerrit/server/query/change/DeltaPredicate.java
index aae0a20..821ec94 100644
--- a/java/com/google/gerrit/server/query/change/DeltaPredicate.java
+++ b/java/com/google/gerrit/server/query/change/DeltaPredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 public class DeltaPredicate extends IntegerRangeChangePredicate {
   public DeltaPredicate(String value) throws QueryParseException {
@@ -24,7 +23,7 @@
   }
 
   @Override
-  protected Integer getValueInt(ChangeData changeData) throws OrmException {
+  protected Integer getValueInt(ChangeData changeData) {
     return ChangeField.DELTA.get(changeData);
   }
 }
diff --git a/java/com/google/gerrit/server/query/change/DestinationPredicate.java b/java/com/google/gerrit/server/query/change/DestinationPredicate.java
index a824a87..702745e 100644
--- a/java/com/google/gerrit/server/query/change/DestinationPredicate.java
+++ b/java/com/google/gerrit/server/query/change/DestinationPredicate.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.index.query.PostFilterPredicate;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gwtorm.server.OrmException;
 import java.util.Set;
 
 public class DestinationPredicate extends PostFilterPredicate<ChangeData> {
@@ -29,7 +28,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     Change change = object.change();
     if (change == null) {
       return false;
diff --git a/java/com/google/gerrit/server/query/change/DirectoryPredicate.java b/java/com/google/gerrit/server/query/change/DirectoryPredicate.java
new file mode 100644
index 0000000..3ab3e26
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/DirectoryPredicate.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 com.google.common.base.CharMatcher;
+import com.google.gerrit.server.index.change.ChangeField;
+import java.util.Locale;
+
+public class DirectoryPredicate extends ChangeIndexPredicate {
+  private static String clean(String directory) {
+    return CharMatcher.is('/').trimFrom(directory).toLowerCase(Locale.US);
+  }
+
+  DirectoryPredicate(String value) {
+    super(ChangeField.DIRECTORY, clean(value));
+  }
+
+  @Override
+  public boolean match(ChangeData cd) {
+    return ChangeField.getDirectories(cd).contains(value);
+  }
+
+  @Override
+  public int getCost() {
+    return 0;
+  }
+}
diff --git a/java/com/google/gerrit/server/query/change/EditByPredicate.java b/java/com/google/gerrit/server/query/change/EditByPredicate.java
index 3238dc9..dfe7310 100644
--- a/java/com/google/gerrit/server/query/change/EditByPredicate.java
+++ b/java/com/google/gerrit/server/query/change/EditByPredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 public class EditByPredicate extends ChangeIndexPredicate {
   protected final Account.Id id;
@@ -27,7 +26,7 @@
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
+  public boolean match(ChangeData cd) {
     return cd.editsByUser().contains(id);
   }
 
diff --git a/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java b/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java
index b5a2d05..9c033b6 100644
--- a/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java
+++ b/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments;
-import com.google.gwtorm.server.OrmException;
 
 public class EqualsFilePredicate extends ChangeIndexPredicate {
   public static Predicate<ChangeData> create(Arguments args, String value) {
@@ -33,7 +32,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     return ChangeField.getFileParts(object).contains(value);
   }
 
diff --git a/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java b/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
index 54e22f3..acf2e25 100644
--- a/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
@@ -21,7 +21,6 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.permissions.ChangePermission;
@@ -29,15 +28,12 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Provider;
 import java.io.IOException;
 
 public class EqualsLabelPredicate extends ChangeIndexPredicate {
   protected final ProjectCache projectCache;
   protected final PermissionBackend permissionBackend;
   protected final IdentifiedUser.GenericFactory userFactory;
-  protected final Provider<ReviewDb> dbProvider;
   protected final String label;
   protected final int expVal;
   protected final Account.Id account;
@@ -49,7 +45,6 @@
     this.permissionBackend = args.permissionBackend;
     this.projectCache = args.projectCache;
     this.userFactory = args.userFactory;
-    this.dbProvider = args.dbProvider;
     this.group = args.group;
     this.label = label;
     this.expVal = expVal;
@@ -57,7 +52,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     Change c = object.change();
     if (c == null) {
       // The change has disappeared.
@@ -123,8 +118,7 @@
 
     // Check the user has 'READ' permission.
     try {
-      PermissionBackend.ForChange perm =
-          permissionBackend.absentUser(approver).database(dbProvider).change(cd);
+      PermissionBackend.ForChange perm = permissionBackend.absentUser(approver).change(cd);
       ProjectState projectState = projectCache.checkedGet(cd.project());
       if (projectState == null || !projectState.statePermitsRead()) {
         return false;
diff --git a/java/com/google/gerrit/server/query/change/EqualsPathPredicate.java b/java/com/google/gerrit/server/query/change/EqualsPathPredicate.java
index fc00283..76936fa 100644
--- a/java/com/google/gerrit/server/query/change/EqualsPathPredicate.java
+++ b/java/com/google/gerrit/server/query/change/EqualsPathPredicate.java
@@ -15,10 +15,7 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
-import java.io.IOException;
 import java.util.Collections;
-import java.util.List;
 
 public class EqualsPathPredicate extends ChangeIndexPredicate {
   public EqualsPathPredicate(String fieldName, String value) {
@@ -26,14 +23,8 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
-    List<String> files;
-    try {
-      files = object.currentFilePaths();
-    } catch (IOException e) {
-      throw new OrmException(e);
-    }
-    return Collections.binarySearch(files, value) >= 0;
+  public boolean match(ChangeData object) {
+    return Collections.binarySearch(object.currentFilePaths(), value) >= 0;
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/query/change/ExactAuthorPredicate.java b/java/com/google/gerrit/server/query/change/ExactAuthorPredicate.java
index bca5d3b..c1b6928 100644
--- a/java/com/google/gerrit/server/query/change/ExactAuthorPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ExactAuthorPredicate.java
@@ -18,8 +18,6 @@
 import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_EXACTAUTHOR;
 
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
-import java.io.IOException;
 import java.util.Locale;
 
 public class ExactAuthorPredicate extends ChangeIndexPredicate {
@@ -28,12 +26,8 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
-    try {
-      return ChangeField.getAuthorNameAndEmail(object).contains(getValue());
-    } catch (IOException e) {
-      throw new OrmException(e);
-    }
+  public boolean match(ChangeData object) {
+    return ChangeField.getAuthorNameAndEmail(object).contains(getValue());
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/query/change/ExactCommitterPredicate.java b/java/com/google/gerrit/server/query/change/ExactCommitterPredicate.java
index 3fae5e5..dac63af 100644
--- a/java/com/google/gerrit/server/query/change/ExactCommitterPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ExactCommitterPredicate.java
@@ -18,8 +18,6 @@
 import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_EXACTCOMMITTER;
 
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
-import java.io.IOException;
 import java.util.Locale;
 
 public class ExactCommitterPredicate extends ChangeIndexPredicate {
@@ -28,12 +26,8 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
-    try {
-      return ChangeField.getCommitterNameAndEmail(object).contains(getValue());
-    } catch (IOException e) {
-      throw new OrmException(e);
-    }
+  public boolean match(ChangeData object) {
+    return ChangeField.getCommitterNameAndEmail(object).contains(getValue());
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/query/change/ExactTopicPredicate.java b/java/com/google/gerrit/server/query/change/ExactTopicPredicate.java
index 138cce5..c6ade75e 100644
--- a/java/com/google/gerrit/server/query/change/ExactTopicPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ExactTopicPredicate.java
@@ -17,7 +17,6 @@
 import static com.google.gerrit.server.index.change.ChangeField.EXACT_TOPIC;
 
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gwtorm.server.OrmException;
 
 public class ExactTopicPredicate extends ChangeIndexPredicate {
   public ExactTopicPredicate(String topic) {
@@ -25,7 +24,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     Change change = object.change();
     if (change == null) {
       return false;
diff --git a/java/com/google/gerrit/server/query/change/FileExtensionListPredicate.java b/java/com/google/gerrit/server/query/change/FileExtensionListPredicate.java
new file mode 100644
index 0000000..bddd2ec
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/FileExtensionListPredicate.java
@@ -0,0 +1,44 @@
+// 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 java.util.stream.Collectors.joining;
+
+import com.google.common.base.Splitter;
+import com.google.gerrit.server.index.change.ChangeField;
+
+public class FileExtensionListPredicate extends ChangeIndexPredicate {
+  private static String clean(String extList) {
+    return Splitter.on(',').splitToList(extList).stream()
+        .map(FileExtensionPredicate::clean)
+        .distinct()
+        .sorted()
+        .collect(joining(","));
+  }
+
+  FileExtensionListPredicate(String value) {
+    super(ChangeField.ONLY_EXTENSIONS, clean(value));
+  }
+
+  @Override
+  public boolean match(ChangeData cd) {
+    return ChangeField.getAllExtensionsAsList(cd).equals(value);
+  }
+
+  @Override
+  public int getCost() {
+    return 0;
+  }
+}
diff --git a/java/com/google/gerrit/server/query/change/FileExtensionPredicate.java b/java/com/google/gerrit/server/query/change/FileExtensionPredicate.java
new file mode 100644
index 0000000..ee573a7
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/FileExtensionPredicate.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.query.change;
+
+import com.google.gerrit.server.index.change.ChangeField;
+import java.util.Locale;
+
+public class FileExtensionPredicate extends ChangeIndexPredicate {
+  static String clean(String ext) {
+    if (ext.startsWith(".")) {
+      ext = ext.substring(1);
+    }
+    return ext.toLowerCase(Locale.US);
+  }
+
+  FileExtensionPredicate(String value) {
+    super(ChangeField.EXTENSION, clean(value));
+  }
+
+  @Override
+  public boolean match(ChangeData object) {
+    return ChangeField.getExtensions(object).contains(value);
+  }
+
+  @Override
+  public int getCost() {
+    return 0;
+  }
+}
diff --git a/java/com/google/gerrit/server/query/change/FileWithNoExtensionInElasticPredicate.java b/java/com/google/gerrit/server/query/change/FileWithNoExtensionInElasticPredicate.java
new file mode 100644
index 0000000..d886baf
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/FileWithNoExtensionInElasticPredicate.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.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/FooterPredicate.java b/java/com/google/gerrit/server/query/change/FooterPredicate.java
new file mode 100644
index 0000000..4d7588c
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/FooterPredicate.java
@@ -0,0 +1,45 @@
+// 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.server.index.change.ChangeField;
+import java.util.Locale;
+
+public class FooterPredicate extends ChangeIndexPredicate {
+  private static String clean(String value) {
+    int indexEquals = value.indexOf('=');
+    int indexColon = value.indexOf(':');
+
+    // footer key cannot contain '='
+    if (indexEquals > 0 && (indexEquals < indexColon || indexColon < 0)) {
+      value = value.substring(0, indexEquals) + ": " + value.substring(indexEquals + 1);
+    }
+    return value.toLowerCase(Locale.US);
+  }
+
+  FooterPredicate(String value) {
+    super(ChangeField.FOOTER, clean(value));
+  }
+
+  @Override
+  public boolean match(ChangeData cd) {
+    return ChangeField.getFooters(cd).contains(value);
+  }
+
+  @Override
+  public int getCost() {
+    return 0;
+  }
+}
diff --git a/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java b/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java
index 545b668..140f26b 100644
--- a/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java
+++ b/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java
@@ -17,12 +17,12 @@
 import static com.google.gerrit.server.index.change.ChangeField.FUZZY_TOPIC;
 
 import com.google.common.collect.Iterables;
+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;
-import com.google.gwtorm.server.OrmException;
 
 public class FuzzyTopicPredicate extends ChangeIndexPredicate {
   protected final ChangeIndex index;
@@ -33,7 +33,7 @@
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
+  public boolean match(ChangeData cd) {
     Change change = cd.change();
     if (change == null) {
       return false;
@@ -48,7 +48,7 @@
           index.getSource(and(thisId, this), IndexedChangeQuery.oneResult()).read();
       return !Iterables.isEmpty(results);
     } catch (QueryParseException e) {
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
   }
 
diff --git a/java/com/google/gerrit/server/query/change/GroupPredicate.java b/java/com/google/gerrit/server/query/change/GroupPredicate.java
index d2645dc..7f7bcff 100644
--- a/java/com/google/gerrit/server/query/change/GroupPredicate.java
+++ b/java/com/google/gerrit/server/query/change/GroupPredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 import java.util.List;
 
 public class GroupPredicate extends ChangeIndexPredicate {
@@ -25,7 +24,7 @@
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
+  public boolean match(ChangeData cd) {
     for (PatchSet ps : cd.patchSets()) {
       List<String> groups = ps.getGroups();
       if (groups != null && groups.contains(getValue())) {
diff --git a/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java b/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
index e422b74..e57a8b3 100644
--- a/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
+++ b/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 public class HasDraftByPredicate extends ChangeIndexPredicate {
   protected final Account.Id accountId;
@@ -27,7 +26,7 @@
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
+  public boolean match(ChangeData cd) {
     return cd.draftsByUser().contains(accountId);
   }
 
diff --git a/java/com/google/gerrit/server/query/change/HasStarsPredicate.java b/java/com/google/gerrit/server/query/change/HasStarsPredicate.java
index b17fffd..0c99cdf 100644
--- a/java/com/google/gerrit/server/query/change/HasStarsPredicate.java
+++ b/java/com/google/gerrit/server/query/change/HasStarsPredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 public class HasStarsPredicate extends ChangeIndexPredicate {
   protected final Account.Id accountId;
@@ -27,7 +26,7 @@
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
+  public boolean match(ChangeData cd) {
     return cd.stars().containsKey(accountId);
   }
 
diff --git a/java/com/google/gerrit/server/query/change/HashtagPredicate.java b/java/com/google/gerrit/server/query/change/HashtagPredicate.java
index 95ecf89..1fe4af4 100644
--- a/java/com/google/gerrit/server/query/change/HashtagPredicate.java
+++ b/java/com/google/gerrit/server/query/change/HashtagPredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.server.change.HashtagsUtil;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 public class HashtagPredicate extends ChangeIndexPredicate {
   public HashtagPredicate(String hashtag) {
@@ -26,7 +25,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     for (String hashtag : object.notes().load().getHashtags()) {
       if (hashtag.equalsIgnoreCase(getValue())) {
         return true;
diff --git a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
index 495d27c..74a0f71 100644
--- a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
+++ b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
@@ -25,7 +25,6 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
-import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.query.InternalQuery;
 import com.google.gerrit.index.query.Predicate;
@@ -33,10 +32,8 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.index.change.ChangeIndexCollection;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -46,6 +43,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.function.Supplier;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
@@ -56,7 +54,7 @@
  * <p>Instances are one-time-use. Other singleton classes should inject a Provider rather than
  * holding on to a single instance.
  */
-public class InternalChangeQuery extends InternalQuery<ChangeData> {
+public class InternalChangeQuery extends InternalQuery<ChangeData, InternalChangeQuery> {
   private static Predicate<ChangeData> ref(Branch.NameKey branch) {
     return new RefPredicate(branch.get());
   }
@@ -92,44 +90,19 @@
     this.notesFactory = notesFactory;
   }
 
-  @Override
-  public InternalChangeQuery setLimit(int n) {
-    super.setLimit(n);
-    return this;
-  }
-
-  @Override
-  public InternalChangeQuery enforceVisibility(boolean enforce) {
-    super.enforceVisibility(enforce);
-    return this;
-  }
-
-  @SafeVarargs
-  @Override
-  public final InternalChangeQuery setRequestedFields(FieldDef<ChangeData, ?>... fields) {
-    super.setRequestedFields(fields);
-    return this;
-  }
-
-  @Override
-  public InternalChangeQuery noFields() {
-    super.noFields();
-    return this;
-  }
-
-  public List<ChangeData> byKey(Change.Key key) throws OrmException {
+  public List<ChangeData> byKey(Change.Key key) {
     return byKeyPrefix(key.get());
   }
 
-  public List<ChangeData> byKeyPrefix(String prefix) throws OrmException {
+  public List<ChangeData> byKeyPrefix(String prefix) {
     return query(new ChangeIdPredicate(prefix));
   }
 
-  public List<ChangeData> byLegacyChangeId(Change.Id id) throws OrmException {
+  public List<ChangeData> byLegacyChangeId(Change.Id id) {
     return query(new LegacyChangeIdPredicate(id));
   }
 
-  public List<ChangeData> byLegacyChangeIds(Collection<Change.Id> ids) throws OrmException {
+  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));
@@ -137,12 +110,11 @@
     return query(or(preds));
   }
 
-  public List<ChangeData> byBranchKey(Branch.NameKey branch, Change.Key key) throws OrmException {
+  public List<ChangeData> byBranchKey(Branch.NameKey branch, Change.Key key) {
     return query(byBranchKeyPred(branch, key));
   }
 
-  public List<ChangeData> byBranchKeyOpen(Project.NameKey project, String branch, Change.Key key)
-      throws OrmException {
+  public List<ChangeData> byBranchKeyOpen(Project.NameKey project, String branch, Change.Key key) {
     return query(and(byBranchKeyPred(new Branch.NameKey(project, branch), key), open()));
   }
 
@@ -155,24 +127,22 @@
     return and(ref(branch), project(branch.getParentKey()), change(key));
   }
 
-  public List<ChangeData> byProject(Project.NameKey project) throws OrmException {
+  public List<ChangeData> byProject(Project.NameKey project) {
     return query(project(project));
   }
 
-  public List<ChangeData> byBranchOpen(Branch.NameKey branch) throws OrmException {
+  public List<ChangeData> byBranchOpen(Branch.NameKey branch) {
     return query(and(ref(branch), project(branch.getParentKey()), open()));
   }
 
-  public List<ChangeData> byBranchNew(Branch.NameKey branch) throws OrmException {
+  public List<ChangeData> byBranchNew(Branch.NameKey branch) {
     return query(and(ref(branch), project(branch.getParentKey()), status(Change.Status.NEW)));
   }
 
   public Iterable<ChangeData> byCommitsOnBranchNotMerged(
-      Repository repo, ReviewDb db, Branch.NameKey branch, Collection<String> hashes)
-      throws OrmException, IOException {
+      Repository repo, Branch.NameKey branch, Collection<String> hashes) throws IOException {
     return byCommitsOnBranchNotMerged(
         repo,
-        db,
         branch,
         hashes,
         // Account for all commit predicates plus ref, project, status.
@@ -181,21 +151,16 @@
 
   @VisibleForTesting
   Iterable<ChangeData> byCommitsOnBranchNotMerged(
-      Repository repo,
-      ReviewDb db,
-      Branch.NameKey branch,
-      Collection<String> hashes,
-      int indexLimit)
-      throws OrmException, IOException {
+      Repository repo, Branch.NameKey branch, Collection<String> hashes, int indexLimit)
+      throws IOException {
     if (hashes.size() > indexLimit) {
-      return byCommitsOnBranchNotMergedFromDatabase(repo, db, branch, hashes);
+      return byCommitsOnBranchNotMergedFromDatabase(repo, branch, hashes);
     }
     return byCommitsOnBranchNotMergedFromIndex(branch, hashes);
   }
 
   private Iterable<ChangeData> byCommitsOnBranchNotMergedFromDatabase(
-      Repository repo, ReviewDb db, Branch.NameKey branch, Collection<String> hashes)
-      throws OrmException, IOException {
+      Repository repo, Branch.NameKey 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)) {
@@ -215,18 +180,17 @@
 
     List<ChangeNotes> notes =
         notesFactory.create(
-            db,
             branch.getParentKey(),
             changeIds,
             cn -> {
               Change c = cn.getChange();
-              return c.getDest().equals(branch) && c.getStatus() != Change.Status.MERGED;
+              return c.getDest().equals(branch) && !c.isMerged();
             });
-    return Lists.transform(notes, n -> changeDataFactory.create(db, n));
+    return Lists.transform(notes, n -> changeDataFactory.create(n));
   }
 
   private Iterable<ChangeData> byCommitsOnBranchNotMergedFromIndex(
-      Branch.NameKey branch, Collection<String> hashes) throws OrmException {
+      Branch.NameKey branch, Collection<String> hashes) {
     return query(
         and(
             ref(branch),
@@ -243,50 +207,45 @@
     return commits;
   }
 
-  public List<ChangeData> byProjectOpen(Project.NameKey project) throws OrmException {
+  public List<ChangeData> byProjectOpen(Project.NameKey project) {
     return query(and(project(project), open()));
   }
 
-  public List<ChangeData> byTopicOpen(String topic) throws OrmException {
+  public List<ChangeData> byTopicOpen(String topic) {
     return query(and(new ExactTopicPredicate(topic), open()));
   }
 
-  public List<ChangeData> byCommit(ObjectId id) throws OrmException {
+  public List<ChangeData> byCommit(ObjectId id) {
     return byCommit(id.name());
   }
 
-  public List<ChangeData> byCommit(String hash) throws OrmException {
+  public List<ChangeData> byCommit(String hash) {
     return query(commit(hash));
   }
 
-  public List<ChangeData> byProjectCommit(Project.NameKey project, ObjectId id)
-      throws OrmException {
+  public List<ChangeData> byProjectCommit(Project.NameKey project, ObjectId id) {
     return byProjectCommit(project, id.name());
   }
 
-  public List<ChangeData> byProjectCommit(Project.NameKey project, String hash)
-      throws OrmException {
+  public List<ChangeData> byProjectCommit(Project.NameKey project, String hash) {
     return query(and(project(project), commit(hash)));
   }
 
-  public List<ChangeData> byProjectCommits(Project.NameKey project, List<String> hashes)
-      throws OrmException {
+  public List<ChangeData> byProjectCommits(Project.NameKey project, List<String> hashes) {
     int n = indexConfig.maxTerms() - 1;
     checkArgument(hashes.size() <= n, "cannot exceed %s commits", n);
     return query(and(project(project), or(commits(hashes))));
   }
 
-  public List<ChangeData> byBranchCommit(String project, String branch, String hash)
-      throws OrmException {
+  public List<ChangeData> byBranchCommit(String project, String branch, String hash) {
     return query(byBranchCommitPred(project, branch, hash));
   }
 
-  public List<ChangeData> byBranchCommit(Branch.NameKey branch, String hash) throws OrmException {
+  public List<ChangeData> byBranchCommit(Branch.NameKey branch, String hash) {
     return byBranchCommit(branch.getParentKey().get(), branch.get(), hash);
   }
 
-  public List<ChangeData> byBranchCommitOpen(String project, String branch, String hash)
-      throws OrmException {
+  public List<ChangeData> byBranchCommitOpen(String project, String branch, String hash) {
     return query(and(byBranchCommitPred(project, branch, hash), open()));
   }
 
@@ -300,41 +259,48 @@
     return and(new ProjectPredicate(project), new RefPredicate(branch), commit(hash));
   }
 
-  public List<ChangeData> bySubmissionId(String cs) throws OrmException {
+  public List<ChangeData> bySubmissionId(String cs) {
     if (Strings.isNullOrEmpty(cs)) {
       return Collections.emptyList();
     }
     return query(new SubmissionIdPredicate(cs));
   }
 
-  private List<ChangeData> byProjectGroups(Project.NameKey project, Collection<String> groups)
-      throws OrmException {
+  private static Predicate<ChangeData> byProjectGroupsPredicate(
+      IndexConfig indexConfig, Project.NameKey project, Collection<String> groups) {
     int n = indexConfig.maxTerms() - 1;
     checkArgument(groups.size() <= n, "cannot exceed %s groups", n);
     List<GroupPredicate> groupPredicates = new ArrayList<>(groups.size());
     for (String g : groups) {
       groupPredicates.add(new GroupPredicate(g));
     }
-    return query(and(project(project), or(groupPredicates)));
+    return and(project(project), or(groupPredicates));
   }
 
-  // Batching via multiple queries requires passing in a Provider since the underlying
-  // QueryProcessor instance is not reusable.
   public static List<ChangeData> byProjectGroups(
       Provider<InternalChangeQuery> queryProvider,
       IndexConfig indexConfig,
       Project.NameKey project,
-      Collection<String> groups)
-      throws OrmException {
+      Collection<String> groups) {
+    // These queries may be complex along multiple dimensions:
+    //  * Many groups per change, if there are very many patch sets. This requires partitioning the
+    //    list of predicates and combining results.
+    //  * Many changes with the same set of groups, if the relation chain is very long. This
+    //    requires querying exhaustively with pagination.
+    // For both cases, we need to invoke the queryProvider multiple times, since each
+    // InternalChangeQuery is single-use.
+
+    Supplier<InternalChangeQuery> querySupplier = () -> queryProvider.get().enforceVisibility(true);
     int batchSize = indexConfig.maxTerms() - 1;
     if (groups.size() <= batchSize) {
-      return queryProvider.get().enforceVisibility(true).byProjectGroups(project, groups);
+      return queryExhaustively(
+          querySupplier, byProjectGroupsPredicate(indexConfig, project, groups));
     }
     Set<Change.Id> seen = new HashSet<>();
     List<ChangeData> result = new ArrayList<>();
     for (List<String> part : Iterables.partition(groups, batchSize)) {
       for (ChangeData cd :
-          queryProvider.get().enforceVisibility(true).byProjectGroups(project, part)) {
+          queryExhaustively(querySupplier, byProjectGroupsPredicate(indexConfig, project, part))) {
         if (!seen.add(cd.getId())) {
           result.add(cd);
         }
diff --git a/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java b/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
index 7ff5a28..ddb6f32 100644
--- a/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
+++ b/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
@@ -19,7 +19,6 @@
 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.gwtorm.server.OrmException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
@@ -48,7 +47,7 @@
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
+  public boolean match(ChangeData cd) {
     Set<Account.Id> reviewedBy = cd.reviewedBy();
     return !reviewedBy.isEmpty() ? reviewedBy.contains(id) : id.equals(NOT_REVIEWED);
   }
diff --git a/java/com/google/gerrit/server/query/change/IsUnresolvedPredicate.java b/java/com/google/gerrit/server/query/change/IsUnresolvedPredicate.java
index 225dc454..27309af 100644
--- a/java/com/google/gerrit/server/query/change/IsUnresolvedPredicate.java
+++ b/java/com/google/gerrit/server/query/change/IsUnresolvedPredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 public class IsUnresolvedPredicate extends IntegerRangeChangePredicate {
   public IsUnresolvedPredicate() throws QueryParseException {
@@ -28,7 +27,7 @@
   }
 
   @Override
-  protected Integer getValueInt(ChangeData changeData) throws OrmException {
+  protected Integer getValueInt(ChangeData changeData) {
     return ChangeField.UNRESOLVED_COMMENT_COUNT.get(changeData);
   }
 }
diff --git a/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java b/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
index 87845d4..6028f2d 100644
--- a/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
+++ b/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
@@ -81,7 +81,7 @@
       }
     }
     if (r.isEmpty()) {
-      return none();
+      return ImmutableList.of(ChangeIndexPredicate.none());
     } else if (checkIsVisible) {
       return ImmutableList.of(or(r), builder.is_visible());
     } else {
@@ -95,12 +95,7 @@
     if (user.isIdentifiedUser()) {
       return user.asIdentifiedUser().state().getProjectWatches().keySet();
     }
-    return Collections.<ProjectWatchKey>emptySet();
-  }
-
-  protected static List<Predicate<ChangeData>> none() {
-    Predicate<ChangeData> any = any();
-    return ImmutableList.of(not(any));
+    return Collections.emptySet();
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/query/change/LabelPredicate.java b/java/com/google/gerrit/server/query/change/LabelPredicate.java
index f8bd2e3..b5d375c 100644
--- a/java/com/google/gerrit/server/query/change/LabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/LabelPredicate.java
@@ -21,12 +21,10 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.util.LabelVote;
-import com.google.inject.Provider;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
@@ -38,7 +36,6 @@
     protected final ProjectCache projectCache;
     protected final PermissionBackend permissionBackend;
     protected final IdentifiedUser.GenericFactory userFactory;
-    protected final Provider<ReviewDb> dbProvider;
     protected final String value;
     protected final Set<Account.Id> accounts;
     protected final AccountGroup.UUID group;
@@ -47,14 +44,12 @@
         ProjectCache projectCache,
         PermissionBackend permissionBackend,
         IdentifiedUser.GenericFactory userFactory,
-        Provider<ReviewDb> dbProvider,
         String value,
         Set<Account.Id> accounts,
         AccountGroup.UUID group) {
       this.projectCache = projectCache;
       this.permissionBackend = permissionBackend;
       this.userFactory = userFactory;
-      this.dbProvider = dbProvider;
       this.value = value;
       this.accounts = accounts;
       this.group = group;
@@ -82,8 +77,7 @@
       AccountGroup.UUID group) {
     super(
         predicates(
-            new Args(
-                a.projectCache, a.permissionBackend, a.userFactory, a.db, value, accounts, group)));
+            new Args(a.projectCache, a.permissionBackend, a.userFactory, value, accounts, group)));
     this.value = value;
   }
 
diff --git a/java/com/google/gerrit/server/query/change/MessagePredicate.java b/java/com/google/gerrit/server/query/change/MessagePredicate.java
index 0cfcedb..0bd8c88 100644
--- a/java/com/google/gerrit/server/query/change/MessagePredicate.java
+++ b/java/com/google/gerrit/server/query/change/MessagePredicate.java
@@ -14,12 +14,12 @@
 
 package com.google.gerrit.server.query.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.server.index.change.ChangeField;
 import com.google.gerrit.server.index.change.ChangeIndex;
 import com.google.gerrit.server.index.change.IndexedChangeQuery;
-import com.google.gwtorm.server.OrmException;
 
 /** Predicate to match changes that contains specified text in commit messages body. */
 public class MessagePredicate extends ChangeIndexPredicate {
@@ -31,7 +31,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     try {
       Predicate<ChangeData> p = Predicate.and(new LegacyChangeIdPredicate(object.getId()), this);
       for (ChangeData cData : index.getSource(p, IndexedChangeQuery.oneResult()).read()) {
@@ -40,7 +40,7 @@
         }
       }
     } catch (QueryParseException e) {
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
 
     return false;
diff --git a/java/com/google/gerrit/server/query/change/OrSource.java b/java/com/google/gerrit/server/query/change/OrSource.java
index b3e1c27..ba06b89 100644
--- a/java/com/google/gerrit/server/query/change/OrSource.java
+++ b/java/com/google/gerrit/server/query/change/OrSource.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.server.query.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.OrPredicate;
 import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.index.query.ResultSet;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gwtorm.server.ListResultSet;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
@@ -35,7 +35,7 @@
   }
 
   @Override
-  public ResultSet<ChangeData> read() throws OrmException {
+  public ResultSet<ChangeData> read() {
     // TODO(spearce) This probably should be more lazy.
     //
     List<ChangeData> r = new ArrayList<>();
@@ -48,14 +48,14 @@
           }
         }
       } else {
-        throw new OrmException("No ChangeDataSource: " + p);
+        throw new StorageException("No ChangeDataSource: " + p);
       }
     }
     return new ListResultSet<>(r);
   }
 
   @Override
-  public ResultSet<FieldBundle> readRaw() throws OrmException {
+  public ResultSet<FieldBundle> readRaw() {
     throw new UnsupportedOperationException("not implemented");
   }
 
diff --git a/java/com/google/gerrit/server/query/change/OutputStreamQuery.java b/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
index 17c23b6..08e6f33 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.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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.DynamicOptions;
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gerrit.server.data.ChangeAttribute;
@@ -35,7 +35,6 @@
 import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gson.Gson;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.BufferedWriter;
 import java.io.IOException;
@@ -76,7 +75,8 @@
     JSON
   }
 
-  private final ReviewDb db;
+  public static final Gson GSON = new Gson();
+
   private final GitRepositoryManager repoManager;
   private final ChangeQueryBuilder queryBuilder;
   private final ChangeQueryProcessor queryProcessor;
@@ -100,14 +100,12 @@
 
   @Inject
   OutputStreamQuery(
-      ReviewDb db,
       GitRepositoryManager repoManager,
       ChangeQueryBuilder queryBuilder,
       ChangeQueryProcessor queryProcessor,
       EventFactory eventFactory,
       TrackingFooters trackingFooters,
       SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory) {
-    this.db = db;
     this.repoManager = repoManager;
     this.queryBuilder = queryBuilder;
     this.queryProcessor = queryProcessor;
@@ -120,6 +118,10 @@
     queryProcessor.setUserProvidedLimit(n);
   }
 
+  public void setNoLimit(boolean on) {
+    queryProcessor.setNoLimit(on);
+  }
+
   public void setStart(int n) {
     queryProcessor.setStart(n);
   }
@@ -217,7 +219,7 @@
         stats.moreChanges = results.more();
         stats.runTimeMilliseconds = TimeUtil.nowMs() - stats.runTimeMilliseconds;
         show(stats);
-      } catch (OrmException err) {
+      } catch (StorageException err) {
         logger.atSevere().withCause(err).log("Cannot execute query: %s", queryString);
 
         ErrorMessage m = new ErrorMessage();
@@ -240,9 +242,9 @@
 
   private ChangeAttribute buildChangeAttribute(
       ChangeData d, Map<Project.NameKey, Repository> repos, Map<Project.NameKey, RevWalk> revWalks)
-      throws OrmException, IOException {
+      throws IOException {
     LabelTypes labelTypes = d.getLabelTypes();
-    ChangeAttribute c = eventFactory.asChangeAttribute(db, d.change(), d.notes());
+    ChangeAttribute c = eventFactory.asChangeAttribute(d.change(), d.notes());
     eventFactory.extend(c, d.change());
 
     if (!trackingFooters.isEmpty()) {
@@ -250,7 +252,7 @@
     }
 
     if (includeAllReviewers) {
-      eventFactory.addAllReviewers(db, c, d.notes());
+      eventFactory.addAllReviewers(c, d.notes());
     }
 
     if (includeSubmitRecords) {
@@ -277,7 +279,6 @@
 
     if (includePatchSets) {
       eventFactory.addPatchSets(
-          db,
           rw,
           c,
           d.patchSets(),
@@ -290,7 +291,7 @@
     if (includeCurrentPatchSet) {
       PatchSet current = d.currentPatchSet();
       if (current != null) {
-        c.currentPatchSet = eventFactory.asPatchSetAttribute(db, rw, d.change(), current);
+        c.currentPatchSet = eventFactory.asPatchSetAttribute(rw, d.change(), current);
         eventFactory.addApprovals(c.currentPatchSet, d.currentApprovals(), labelTypes);
 
         if (includeFiles) {
@@ -306,7 +307,6 @@
       eventFactory.addComments(c, d.messages());
       if (includePatchSets) {
         eventFactory.addPatchSets(
-            db,
             rw,
             c,
             d.patchSets(),
@@ -324,7 +324,7 @@
       eventFactory.addDependencies(rw, c, d.change(), d.currentPatchSet());
     }
 
-    c.plugins = queryProcessor.create(d);
+    c.plugins = queryProcessor.getAttributesFactory().create(d);
     return c;
   }
 
@@ -357,7 +357,7 @@
         break;
 
       case JSON:
-        out.print(new Gson().toJson(data));
+        out.print(GSON.toJson(data));
         out.print('\n');
         break;
     }
@@ -399,7 +399,7 @@
       // Idention for multi-line text is
       // current depth indetion + length of field + length of ": "
       indent = indent(indent.length() + field.length() + spacesDepthRatio);
-      out.print(((String) value).replaceAll("\n", "\n" + indent).trim());
+      out.print(((String) value).replace("\n", "\n" + indent).trim());
       out.print('\n');
     } else if (value instanceof Long && isDateField(field)) {
       out.print(' ');
diff --git a/java/com/google/gerrit/server/query/change/OwnerPredicate.java b/java/com/google/gerrit/server/query/change/OwnerPredicate.java
index ff494fc..100a66c 100644
--- a/java/com/google/gerrit/server/query/change/OwnerPredicate.java
+++ b/java/com/google/gerrit/server/query/change/OwnerPredicate.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 public class OwnerPredicate extends ChangeIndexPredicate {
   protected final Account.Id id;
@@ -32,7 +31,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     Change change = object.change();
     return change != null && id.equals(change.getOwner());
   }
diff --git a/java/com/google/gerrit/server/query/change/OwnerinPredicate.java b/java/com/google/gerrit/server/query/change/OwnerinPredicate.java
index c48bdd5..41b3204 100644
--- a/java/com/google/gerrit/server/query/change/OwnerinPredicate.java
+++ b/java/com/google/gerrit/server/query/change/OwnerinPredicate.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gwtorm.server.OrmException;
 
 public class OwnerinPredicate extends PostFilterPredicate<ChangeData> {
   protected final IdentifiedUser.GenericFactory userFactory;
@@ -31,7 +30,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     final Change change = object.change();
     if (change == null) {
       return false;
diff --git a/java/com/google/gerrit/server/query/change/PluginDefinedAttributesFactory.java b/java/com/google/gerrit/server/query/change/PluginDefinedAttributesFactory.java
deleted file mode 100644
index a795025..0000000
--- a/java/com/google/gerrit/server/query/change/PluginDefinedAttributesFactory.java
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query.change;
-
-import com.google.gerrit.extensions.common.PluginDefinedInfo;
-import java.util.List;
-
-public interface PluginDefinedAttributesFactory {
-  List<PluginDefinedInfo> create(ChangeData cd);
-}
diff --git a/java/com/google/gerrit/server/query/change/ProjectPredicate.java b/java/com/google/gerrit/server/query/change/ProjectPredicate.java
index 09a46a4..febd6c6 100644
--- a/java/com/google/gerrit/server/query/change/ProjectPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ProjectPredicate.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 public class ProjectPredicate extends ChangeIndexPredicate {
   public ProjectPredicate(String id) {
@@ -29,7 +28,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     Change change = object.change();
     if (change == null) {
       return false;
diff --git a/java/com/google/gerrit/server/query/change/ProjectPrefixPredicate.java b/java/com/google/gerrit/server/query/change/ProjectPrefixPredicate.java
index 28b1302..744f4d2 100644
--- a/java/com/google/gerrit/server/query/change/ProjectPrefixPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ProjectPrefixPredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 public class ProjectPrefixPredicate extends ChangeIndexPredicate {
   public ProjectPrefixPredicate(String prefix) {
@@ -24,7 +23,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     Change c = object.change();
     return c != null && c.getDest().getParentKey().get().startsWith(getValue());
   }
diff --git a/java/com/google/gerrit/server/query/change/RefPredicate.java b/java/com/google/gerrit/server/query/change/RefPredicate.java
index c9314e4..2ed4c99 100644
--- a/java/com/google/gerrit/server/query/change/RefPredicate.java
+++ b/java/com/google/gerrit/server/query/change/RefPredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 public class RefPredicate extends ChangeIndexPredicate {
   public RefPredicate(String ref) {
@@ -24,7 +23,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     Change change = object.change();
     if (change == null) {
       return false;
diff --git a/java/com/google/gerrit/server/query/change/RegexDirectoryPredicate.java b/java/com/google/gerrit/server/query/change/RegexDirectoryPredicate.java
new file mode 100644
index 0000000..1787c76
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/RegexDirectoryPredicate.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.query.change;
+
+import com.google.gerrit.server.index.change.ChangeField;
+import dk.brics.automaton.RegExp;
+import dk.brics.automaton.RunAutomaton;
+
+public class RegexDirectoryPredicate extends ChangeRegexPredicate {
+  protected final RunAutomaton pattern;
+
+  public RegexDirectoryPredicate(String re) {
+    super(ChangeField.DIRECTORY, re);
+
+    if (re.startsWith("^")) {
+      re = re.substring(1);
+    }
+
+    if (re.endsWith("$") && !re.endsWith("\\$")) {
+      re = re.substring(0, re.length() - 1);
+    }
+
+    this.pattern = new RunAutomaton(new RegExp(re).toAutomaton());
+  }
+
+  @Override
+  public boolean match(ChangeData cd) {
+    return ChangeField.getDirectories(cd).stream().anyMatch(pattern::run);
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/java/com/google/gerrit/server/query/change/RegexPathPredicate.java b/java/com/google/gerrit/server/query/change/RegexPathPredicate.java
index f694904..4c3c04c 100644
--- a/java/com/google/gerrit/server/query/change/RegexPathPredicate.java
+++ b/java/com/google/gerrit/server/query/change/RegexPathPredicate.java
@@ -16,9 +16,6 @@
 
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.ioutil.RegexListSearcher;
-import com.google.gwtorm.server.OrmException;
-import java.io.IOException;
-import java.util.List;
 
 public class RegexPathPredicate extends ChangeRegexPredicate {
   public RegexPathPredicate(String re) {
@@ -26,14 +23,11 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
-    List<String> files;
-    try {
-      files = object.currentFilePaths();
-    } catch (IOException e) {
-      throw new OrmException(e);
-    }
-    return RegexListSearcher.ofStrings(getValue()).search(files).findAny().isPresent();
+  public boolean match(ChangeData object) {
+    return RegexListSearcher.ofStrings(getValue())
+        .search(object.currentFilePaths())
+        .findAny()
+        .isPresent();
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java b/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java
index 1efc77d..46a17f6 100644
--- a/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java
+++ b/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 import dk.brics.automaton.RegExp;
 import dk.brics.automaton.RunAutomaton;
 
@@ -39,7 +38,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     Change change = object.change();
     if (change == null) {
       return false;
diff --git a/java/com/google/gerrit/server/query/change/RegexRefPredicate.java b/java/com/google/gerrit/server/query/change/RegexRefPredicate.java
index 92abafb..af211e6 100644
--- a/java/com/google/gerrit/server/query/change/RegexRefPredicate.java
+++ b/java/com/google/gerrit/server/query/change/RegexRefPredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 import dk.brics.automaton.RegExp;
 import dk.brics.automaton.RunAutomaton;
 
@@ -38,7 +37,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     Change change = object.change();
     if (change == null) {
       return false;
diff --git a/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java b/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
index 2b58c88..0441afa 100644
--- a/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
+++ b/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
@@ -17,7 +17,6 @@
 import static com.google.gerrit.server.index.change.ChangeField.EXACT_TOPIC;
 
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gwtorm.server.OrmException;
 import dk.brics.automaton.RegExp;
 import dk.brics.automaton.RunAutomaton;
 
@@ -39,7 +38,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     Change change = object.change();
     if (change == null || change.getTopic() == null) {
       return false;
diff --git a/java/com/google/gerrit/server/query/change/RevertOfPredicate.java b/java/com/google/gerrit/server/query/change/RevertOfPredicate.java
index 7f4ade0..eea1b1e 100644
--- a/java/com/google/gerrit/server/query/change/RevertOfPredicate.java
+++ b/java/com/google/gerrit/server/query/change/RevertOfPredicate.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 public class RevertOfPredicate extends ChangeIndexPredicate {
   public RevertOfPredicate(String revertOf) {
@@ -23,7 +22,7 @@
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
+  public boolean match(ChangeData cd) {
     if (cd.change().getRevertOf() == null) {
       return false;
     }
diff --git a/java/com/google/gerrit/server/query/change/ReviewerByEmailPredicate.java b/java/com/google/gerrit/server/query/change/ReviewerByEmailPredicate.java
index 667c630..070f800 100644
--- a/java/com/google/gerrit/server/query/change/ReviewerByEmailPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ReviewerByEmailPredicate.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
-import com.google.gwtorm.server.OrmException;
 
 class ReviewerByEmailPredicate extends ChangeIndexPredicate {
 
@@ -43,7 +42,7 @@
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
+  public boolean match(ChangeData cd) {
     return cd.reviewersByEmail().asTable().get(state, adr) != null;
   }
 
diff --git a/java/com/google/gerrit/server/query/change/ReviewerPredicate.java b/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
index 5364a66..19104d3 100644
--- a/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
@@ -15,15 +15,11 @@
 package com.google.gerrit.server.query.change;
 
 import static com.google.common.base.Preconditions.checkArgument;
-import static java.util.stream.Collectors.toList;
 
 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;
-import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments;
-import com.google.gwtorm.server.OrmException;
-import java.util.stream.Stream;
 
 public class ReviewerPredicate extends ChangeIndexPredicate {
   protected static Predicate<ChangeData> forState(Account.Id id, ReviewerStateInternal state) {
@@ -31,31 +27,14 @@
     return new ReviewerPredicate(state, id);
   }
 
-  protected static Predicate<ChangeData> reviewer(Arguments args, Account.Id id) {
-    if (args.notesMigration.readChanges()) {
-      // With NoteDb, Reviewer/CC are clearly distinct states, so only choose reviewer.
-      return new ReviewerPredicate(ReviewerStateInternal.REVIEWER, id);
-    }
-    // Without NoteDb, Reviewer/CC are a bit unpredictable; maintain the old behavior of matching
-    // any reviewer state.
-    return anyReviewerState(id);
+  protected static Predicate<ChangeData> reviewer(Account.Id id) {
+    return new ReviewerPredicate(ReviewerStateInternal.REVIEWER, id);
   }
 
   protected static Predicate<ChangeData> cc(Account.Id id) {
-    // As noted above, CC is nebulous without NoteDb, but it certainly doesn't make sense to return
-    // Reviewers for cc:foo. Most likely this will just not match anything, but let the index sort
-    // it out.
     return new ReviewerPredicate(ReviewerStateInternal.CC, id);
   }
 
-  protected static Predicate<ChangeData> anyReviewerState(Account.Id id) {
-    return Predicate.or(
-        Stream.of(ReviewerStateInternal.values())
-            .filter(s -> s != ReviewerStateInternal.REMOVED)
-            .map(s -> new ReviewerPredicate(s, id))
-            .collect(toList()));
-  }
-
   protected final ReviewerStateInternal state;
   protected final Account.Id id;
 
@@ -70,7 +49,7 @@
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
+  public boolean match(ChangeData cd) {
     return cd.reviewers().asTable().get(state, id) != null;
   }
 
diff --git a/java/com/google/gerrit/server/query/change/ReviewerinPredicate.java b/java/com/google/gerrit/server/query/change/ReviewerinPredicate.java
index a0aa8b5..542a357 100644
--- a/java/com/google/gerrit/server/query/change/ReviewerinPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ReviewerinPredicate.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
-import com.google.gwtorm.server.OrmException;
 
 public class ReviewerinPredicate extends PostFilterPredicate<ChangeData> {
   protected final IdentifiedUser.GenericFactory userFactory;
@@ -36,7 +35,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     for (Account.Id accountId : object.reviewers().byState(ReviewerStateInternal.REVIEWER)) {
       IdentifiedUser reviewer = userFactory.create(accountId);
       if (reviewer.getEffectiveGroups().contains(uuid)) {
diff --git a/java/com/google/gerrit/server/query/change/StarPredicate.java b/java/com/google/gerrit/server/query/change/StarPredicate.java
index 12d4753..6c5fd78 100644
--- a/java/com/google/gerrit/server/query/change/StarPredicate.java
+++ b/java/com/google/gerrit/server/query/change/StarPredicate.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 public class StarPredicate extends ChangeIndexPredicate {
   protected final Account.Id accountId;
@@ -30,7 +29,7 @@
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
+  public boolean match(ChangeData cd) {
     return cd.stars().get(accountId).contains(label);
   }
 
diff --git a/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.java b/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.java
index 5fdeb68..0995a59 100644
--- a/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.java
+++ b/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 public class SubmissionIdPredicate extends ChangeIndexPredicate {
   public SubmissionIdPredicate(String changeSet) {
@@ -24,7 +23,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     Change change = object.change();
     if (change == null) {
       return false;
diff --git a/java/com/google/gerrit/server/query/change/SubmitRecordPredicate.java b/java/com/google/gerrit/server/query/change/SubmitRecordPredicate.java
index 6a81ff6..e59ae43 100644
--- a/java/com/google/gerrit/server/query/change/SubmitRecordPredicate.java
+++ b/java/com/google/gerrit/server/query/change/SubmitRecordPredicate.java
@@ -20,7 +20,6 @@
 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.gwtorm.server.OrmException;
 import java.util.Set;
 
 public class SubmitRecordPredicate extends ChangeIndexPredicate {
@@ -41,7 +40,7 @@
   }
 
   @Override
-  public boolean match(ChangeData in) throws OrmException {
+  public boolean match(ChangeData in) {
     return ChangeField.formatSubmitRecordValues(in).contains(getValue());
   }
 
diff --git a/java/com/google/gerrit/server/query/change/SubmittablePredicate.java b/java/com/google/gerrit/server/query/change/SubmittablePredicate.java
index 8a2c889..c507f1c 100644
--- a/java/com/google/gerrit/server/query/change/SubmittablePredicate.java
+++ b/java/com/google/gerrit/server/query/change/SubmittablePredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 public class SubmittablePredicate extends ChangeIndexPredicate {
   protected final SubmitRecord.Status status;
@@ -27,7 +26,7 @@
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
+  public boolean match(ChangeData cd) {
     return cd.submitRecords(ChangeField.SUBMIT_RULE_OPTIONS_STRICT).stream()
         .anyMatch(r -> r.status == status);
   }
diff --git a/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java b/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java
index 4f751c5..622fa2c 100644
--- a/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java
+++ b/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java
@@ -14,26 +14,16 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
-import java.io.IOException;
 
 public class TrackingIdPredicate extends ChangeIndexPredicate {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
   public TrackingIdPredicate(String trackingId) {
     super(ChangeField.TR, trackingId);
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
-    try {
-      return cd.trackingFooters().containsValue(getValue());
-    } catch (IOException e) {
-      logger.atWarning().withCause(e).log("Cannot extract footers from %s", cd.getId());
-    }
-    return false;
+  public boolean match(ChangeData cd) {
+    return cd.trackingFooters().containsValue(getValue());
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/query/group/GroupIsVisibleToPredicate.java b/java/com/google/gerrit/server/query/group/GroupIsVisibleToPredicate.java
index 144a81c..8248bf5 100644
--- a/java/com/google/gerrit/server/query/group/GroupIsVisibleToPredicate.java
+++ b/java/com/google/gerrit/server/query/group/GroupIsVisibleToPredicate.java
@@ -15,14 +15,13 @@
 package com.google.gerrit.server.query.group;
 
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.index.query.IsVisibleToPredicate;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.index.IndexUtils;
 import com.google.gerrit.server.query.account.AccountQueryBuilder;
-import com.google.gwtorm.server.OrmException;
 
 public class GroupIsVisibleToPredicate extends IsVisibleToPredicate<InternalGroup> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -38,7 +37,7 @@
   }
 
   @Override
-  public boolean match(InternalGroup group) throws OrmException {
+  public boolean match(InternalGroup group) {
     try {
       boolean canSee = groupControlFactory.controlFor(user, group.getGroupUUID()).isVisible();
       if (!canSee) {
diff --git a/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java b/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java
index 296dc17..215e36f 100644
--- a/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java
@@ -24,14 +24,15 @@
 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;
 import com.google.gerrit.server.account.GroupBackends;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.group.InternalGroup;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.List;
@@ -40,7 +41,7 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
 /** Parses a query string meant to be applied to group objects. */
-public class GroupQueryBuilder extends QueryBuilder<InternalGroup> {
+public class GroupQueryBuilder extends QueryBuilder<InternalGroup, GroupQueryBuilder> {
   public static final String FIELD_UUID = "uuid";
   public static final String FIELD_DESCRIPTION = "description";
   public static final String FIELD_INNAME = "inname";
@@ -68,7 +69,7 @@
 
   @Inject
   GroupQueryBuilder(Arguments args) {
-    super(mydef);
+    super(mydef, null);
     this.args = args;
   }
 
@@ -133,7 +134,7 @@
 
   @Operator
   public Predicate<InternalGroup> member(String query)
-      throws QueryParseException, OrmException, ConfigInvalidException, IOException {
+      throws QueryParseException, ConfigInvalidException, IOException {
     Set<Account.Id> accounts = parseAccount(query);
     List<Predicate<InternalGroup>> predicates =
         accounts.stream().map(GroupPredicates::member).collect(toImmutableList());
@@ -156,12 +157,15 @@
   }
 
   private Set<Account.Id> parseAccount(String nameOrEmail)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
-    Set<Account.Id> foundAccounts = args.accountResolver.findAll(nameOrEmail);
-    if (foundAccounts.isEmpty()) {
-      throw error("User " + nameOrEmail + " not found");
+      throws QueryParseException, IOException, ConfigInvalidException {
+    try {
+      return args.accountResolver.resolve(nameOrEmail).asNonEmptyIdSet();
+    } catch (UnresolvableAccountException e) {
+      if (e.isSelf()) {
+        throw new QueryRequiresAuthException(e.getMessage(), e);
+      }
+      throw new QueryParseException(e.getMessage(), e);
     }
-    return foundAccounts;
   }
 
   private AccountGroup.UUID parseGroup(String groupNameOrUuid) throws QueryParseException {
diff --git a/java/com/google/gerrit/server/query/group/InternalGroupQuery.java b/java/com/google/gerrit/server/query/group/InternalGroupQuery.java
index d9808f2..5749809 100644
--- a/java/com/google/gerrit/server/query/group/InternalGroupQuery.java
+++ b/java/com/google/gerrit/server/query/group/InternalGroupQuery.java
@@ -26,7 +26,6 @@
 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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.util.List;
 import java.util.Optional;
@@ -37,7 +36,7 @@
  * <p>Instances are one-time-use. Other singleton classes should inject a Provider rather than
  * holding on to a single instance.
  */
-public class InternalGroupQuery extends InternalQuery<InternalGroup> {
+public class InternalGroupQuery extends InternalQuery<InternalGroup, InternalGroupQuery> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   @Inject
@@ -46,24 +45,24 @@
     super(queryProcessor, indexes, indexConfig);
   }
 
-  public Optional<InternalGroup> byName(AccountGroup.NameKey groupName) throws OrmException {
+  public Optional<InternalGroup> byName(AccountGroup.NameKey groupName) {
     return getOnlyGroup(GroupPredicates.name(groupName.get()), "group name '" + groupName + "'");
   }
 
-  public Optional<InternalGroup> byId(AccountGroup.Id groupId) throws OrmException {
+  public Optional<InternalGroup> byId(AccountGroup.Id groupId) {
     return getOnlyGroup(GroupPredicates.id(groupId), "group id '" + groupId + "'");
   }
 
-  public List<InternalGroup> byMember(Account.Id memberId) throws OrmException {
+  public List<InternalGroup> byMember(Account.Id memberId) {
     return query(GroupPredicates.member(memberId));
   }
 
-  public List<InternalGroup> bySubgroup(AccountGroup.UUID subgroupId) throws OrmException {
+  public List<InternalGroup> bySubgroup(AccountGroup.UUID subgroupId) {
     return query(GroupPredicates.subgroup(subgroupId));
   }
 
   private Optional<InternalGroup> getOnlyGroup(
-      Predicate<InternalGroup> predicate, String groupDescription) throws OrmException {
+      Predicate<InternalGroup> predicate, String groupDescription) {
     List<InternalGroup> groups = query(predicate);
     if (groups.isEmpty()) {
       return Optional.empty();
diff --git a/java/com/google/gerrit/server/query/project/ProjectIsVisibleToPredicate.java b/java/com/google/gerrit/server/query/project/ProjectIsVisibleToPredicate.java
index b1c5af0..2c84b9a 100644
--- a/java/com/google/gerrit/server/query/project/ProjectIsVisibleToPredicate.java
+++ b/java/com/google/gerrit/server/query/project/ProjectIsVisibleToPredicate.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.query.account.AccountQueryBuilder;
-import com.google.gwtorm.server.OrmException;
 
 public class ProjectIsVisibleToPredicate extends IsVisibleToPredicate<ProjectData> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -37,7 +36,7 @@
   }
 
   @Override
-  public boolean match(ProjectData pd) throws OrmException {
+  public boolean match(ProjectData pd) {
     if (!pd.getProject().getState().permitsRead()) {
       logger.atFine().log("Filter out non-readable project: %s", pd);
       return false;
diff --git a/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java b/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java
index 57db5e7..8b4cb53 100644
--- a/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java
@@ -28,7 +28,7 @@
 import java.util.List;
 
 /** Parses a query string meant to be applied to project objects. */
-public class ProjectQueryBuilder extends QueryBuilder<ProjectData> {
+public class ProjectQueryBuilder extends QueryBuilder<ProjectData, ProjectQueryBuilder> {
   public static final String FIELD_LIMIT = "limit";
 
   private static final QueryBuilder.Definition<ProjectData, ProjectQueryBuilder> mydef =
@@ -36,7 +36,7 @@
 
   @Inject
   ProjectQueryBuilder() {
-    super(mydef);
+    super(mydef, null);
   }
 
   @Operator
diff --git a/java/com/google/gerrit/server/quota/DefaultQuotaBackend.java b/java/com/google/gerrit/server/quota/DefaultQuotaBackend.java
new file mode 100644
index 0000000..d39e55c
--- /dev/null
+++ b/java/com/google/gerrit/server/quota/DefaultQuotaBackend.java
@@ -0,0 +1,182 @@
+// 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.quota;
+
+import static com.google.common.base.Preconditions.checkState;
+
+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.server.CurrentUser;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
+import com.google.gerrit.server.plugincontext.PluginSetEntryContext;
+import com.google.gerrit.server.quota.QuotaResponse.Aggregated;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.util.ArrayList;
+import java.util.List;
+
+@Singleton
+public class DefaultQuotaBackend implements QuotaBackend {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final Provider<CurrentUser> userProvider;
+  private final PluginSetContext<QuotaEnforcer> quotaEnforcers;
+
+  @Inject
+  DefaultQuotaBackend(
+      Provider<CurrentUser> userProvider, PluginSetContext<QuotaEnforcer> quotaEnforcers) {
+    this.userProvider = userProvider;
+    this.quotaEnforcers = quotaEnforcers;
+  }
+
+  @Override
+  public WithUser currentUser() {
+    return new WithUser(quotaEnforcers, userProvider.get());
+  }
+
+  @Override
+  public WithUser user(CurrentUser user) {
+    return new WithUser(quotaEnforcers, user);
+  }
+
+  private static QuotaResponse.Aggregated request(
+      PluginSetContext<QuotaEnforcer> quotaEnforcers,
+      String quotaGroup,
+      QuotaRequestContext requestContext,
+      long numTokens,
+      boolean deduct) {
+    checkState(numTokens > 0, "numTokens must be a positive, non-zero long");
+
+    // PluginSets can change their content when plugins (de-)register. Copy the currently registered
+    // plugins so that we can iterate twice on a stable list.
+    List<PluginSetEntryContext<QuotaEnforcer>> enforcers = ImmutableList.copyOf(quotaEnforcers);
+    List<QuotaResponse> responses = new ArrayList<>(enforcers.size());
+    for (PluginSetEntryContext<QuotaEnforcer> enforcer : enforcers) {
+      try {
+        if (deduct) {
+          responses.add(enforcer.call(p -> p.requestTokens(quotaGroup, requestContext, numTokens)));
+        } else {
+          responses.add(enforcer.call(p -> p.dryRun(quotaGroup, requestContext, numTokens)));
+        }
+      } catch (RuntimeException e) {
+        // Roll back the quota request for all enforcers that deducted the quota. Rethrow the
+        // exception to adhere to the API contract.
+        if (deduct) {
+          refillAfterErrorOrException(enforcers, responses, quotaGroup, requestContext, numTokens);
+        }
+        throw e;
+      }
+    }
+
+    if (deduct && responses.stream().anyMatch(r -> r.status().isError())) {
+      // Roll back the quota request for all enforcers that deducted the quota (= the request
+      // succeeded). Don't touch failed enforcers as the interface contract said that failed
+      // requests should not be deducted.
+      refillAfterErrorOrException(enforcers, responses, quotaGroup, requestContext, numTokens);
+    }
+
+    logger.atFine().log(
+        "Quota request for %s with %s (deduction=%s) for %s token returned %s",
+        quotaGroup,
+        requestContext,
+        deduct ? "(deduction=yes)" : "(deduction=no)",
+        numTokens,
+        responses);
+    return QuotaResponse.Aggregated.create(ImmutableList.copyOf(responses));
+  }
+
+  private static QuotaResponse.Aggregated availableTokens(
+      PluginSetContext<QuotaEnforcer> quotaEnforcers,
+      String quotaGroup,
+      QuotaRequestContext requestContext) {
+    // PluginSets can change their content when plugins (de-)register. Copy the currently registered
+    // plugins so that we can iterate twice on a stable list.
+    List<PluginSetEntryContext<QuotaEnforcer>> enforcers = ImmutableList.copyOf(quotaEnforcers);
+    List<QuotaResponse> responses = new ArrayList<>(enforcers.size());
+    for (PluginSetEntryContext<QuotaEnforcer> enforcer : enforcers) {
+      responses.add(enforcer.call(p -> p.availableTokens(quotaGroup, requestContext)));
+    }
+    return QuotaResponse.Aggregated.create(responses);
+  }
+
+  private static void refillAfterErrorOrException(
+      List<PluginSetEntryContext<QuotaEnforcer>> enforcers,
+      List<QuotaResponse> collectedResponses,
+      String quotaGroup,
+      QuotaRequestContext requestContext,
+      long numTokens) {
+    for (int i = 0; i < collectedResponses.size(); i++) {
+      if (collectedResponses.get(i).status().isOk()) {
+        enforcers.get(i).run(p -> p.refill(quotaGroup, requestContext, numTokens));
+      }
+    }
+  }
+
+  static class WithUser extends WithResource implements QuotaBackend.WithUser {
+    WithUser(PluginSetContext<QuotaEnforcer> quotaEnforcers, CurrentUser user) {
+      super(quotaEnforcers, QuotaRequestContext.builder().user(user).build());
+    }
+
+    @Override
+    public QuotaBackend.WithResource account(Account.Id account) {
+      QuotaRequestContext ctx = requestContext.toBuilder().account(account).build();
+      return new WithResource(quotaEnforcers, ctx);
+    }
+
+    @Override
+    public QuotaBackend.WithResource project(Project.NameKey project) {
+      QuotaRequestContext ctx = requestContext.toBuilder().project(project).build();
+      return new WithResource(quotaEnforcers, ctx);
+    }
+
+    @Override
+    public QuotaBackend.WithResource change(Change.Id change, Project.NameKey project) {
+      QuotaRequestContext ctx = requestContext.toBuilder().change(change).project(project).build();
+      return new WithResource(quotaEnforcers, ctx);
+    }
+  }
+
+  static class WithResource implements QuotaBackend.WithResource {
+    protected final QuotaRequestContext requestContext;
+    protected final PluginSetContext<QuotaEnforcer> quotaEnforcers;
+
+    private WithResource(
+        PluginSetContext<QuotaEnforcer> quotaEnforcers, QuotaRequestContext quotaRequestContext) {
+      this.quotaEnforcers = quotaEnforcers;
+      this.requestContext = quotaRequestContext;
+    }
+
+    @Override
+    public QuotaResponse.Aggregated requestTokens(String quotaGroup, long numTokens) {
+      return DefaultQuotaBackend.request(
+          quotaEnforcers, quotaGroup, requestContext, numTokens, true);
+    }
+
+    @Override
+    public QuotaResponse.Aggregated dryRun(String quotaGroup, long numTokens) {
+      return DefaultQuotaBackend.request(
+          quotaEnforcers, quotaGroup, requestContext, numTokens, false);
+    }
+
+    @Override
+    public Aggregated availableTokens(String quotaGroup) {
+      return DefaultQuotaBackend.availableTokens(quotaEnforcers, quotaGroup, requestContext);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/quota/QuotaBackend.java b/java/com/google/gerrit/server/quota/QuotaBackend.java
new file mode 100644
index 0000000..11ce61f
--- /dev/null
+++ b/java/com/google/gerrit/server/quota/QuotaBackend.java
@@ -0,0 +1,103 @@
+// 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.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.server.CurrentUser;
+import com.google.inject.ImplementedBy;
+
+/**
+ * Backend interface to perform quota requests on. By default, this interface is backed by {@link
+ * DefaultQuotaBackend} which calls all plugins that implement {@link QuotaEnforcer}. A different
+ * implementation might be bound in tests. Plugins are not supposed to implement this interface, but
+ * bind a {@link QuotaEnforcer} implementation instead.
+ *
+ * <p>All quota requests require a quota group and a user. Enriching them with a top-level entity
+ * {@code Change, Project, Account} is optional but should be done if the request is targeted.
+ *
+ * <p>Example usage:
+ *
+ * <pre>
+ *   quotaBackend.currentUser().project(projectName).requestToken("/projects/create").throwOnError();
+ *   quotaBackend.user(user).requestToken("/restapi/config/put").throwOnError();
+ *   QuotaResponse.Aggregated result = quotaBackend.currentUser().account(accountId).requestToken("/restapi/accounts/emails/validate");
+ *   QuotaResponse.Aggregated result = quotaBackend.currentUser().project(projectName).requestTokens("/projects/git/upload", numBytesInPush);
+ * </pre>
+ *
+ * <p>All quota groups must be documented in {@code quota.txt} and detail the metadata that is
+ * provided (i.e. the parameters used to scope down the quota request).
+ */
+@ImplementedBy(DefaultQuotaBackend.class)
+public interface QuotaBackend {
+  /** Constructs a request for the current user. */
+  WithUser currentUser();
+
+  /**
+   * See {@link #currentUser()}. Use this method only if you can't guarantee that the request is for
+   * the current user (e.g. impersonation).
+   */
+  WithUser user(CurrentUser user);
+
+  /**
+   * An interface capable of issuing quota requests. Scope can be futher reduced by providing a
+   * top-level entity.
+   */
+  interface WithUser extends WithResource {
+    /** Scope the request down to an account. */
+    WithResource account(Account.Id account);
+
+    /** Scope the request down to a project. */
+    WithResource project(Project.NameKey project);
+
+    /** Scope the request down to a change. */
+    WithResource change(Change.Id change, Project.NameKey project);
+  }
+
+  /** An interface capable of issuing quota requests. */
+  interface WithResource {
+    /** Issues a single quota request for {@code 1} token. */
+    default QuotaResponse.Aggregated requestToken(String quotaGroup) {
+      return requestTokens(quotaGroup, 1);
+    }
+
+    /** Issues a single quota request for {@code numTokens} tokens. */
+    QuotaResponse.Aggregated requestTokens(String quotaGroup, long numTokens);
+
+    /**
+     * Issues a single quota request for {@code numTokens} tokens but signals the implementations
+     * not to deduct any quota yet. Can be used to do pre-flight requests where necessary
+     */
+    QuotaResponse.Aggregated dryRun(String quotaGroup, long tokens);
+
+    /**
+     * Requests a minimum number of tokens available in implementations. This is a pre-flight check
+     * for the exceptional case when the requested number of tokens is not known in advance but
+     * boundary can be specified. For instance, when the commit is received its size is not known
+     * until the transfer happens however one can specify how many bytes can be accepted to meet the
+     * repository size quota.
+     *
+     * <p>By definition, this is not an allocating request, therefore, it should be followed by the
+     * call to {@link #requestTokens(String, long)} when the size gets determined so that quota
+     * could be properly adjusted. It is in developer discretion to ensure that it gets called.
+     * There might be a case when particular quota gets temporarily overbooked when multiple
+     * requests are performed but the following calls to {@link #requestTokens(String, long)} will
+     * fail at the moment when a quota is exhausted. It is not a subject of quota backend to reclaim
+     * tokens that were used due to overbooking.
+     */
+    QuotaResponse.Aggregated availableTokens(String quotaGroup);
+  }
+}
diff --git a/java/com/google/gerrit/server/quota/QuotaEnforcer.java b/java/com/google/gerrit/server/quota/QuotaEnforcer.java
new file mode 100644
index 0000000..4c1de14
--- /dev/null
+++ b/java/com/google/gerrit/server/quota/QuotaEnforcer.java
@@ -0,0 +1,85 @@
+// 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.quota;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+/**
+ * Allows plugins to enforce different types of quota.
+ *
+ * <p>Enforcing quotas can be helpful in many scenarios. For example:
+ *
+ * <ul>
+ *   <li>Reducing the number of QPS a user can send to Gerrit on the REST API
+ *   <li>Limiting the size of a repository (project)
+ *   <li>Limiting the number of changes in a repository
+ *   <li>Limiting the number of actions that have the potential for spam, abuse or flooding if not
+ *       limited
+ * </ul>
+ *
+ * This endpoint gives plugins the capability to enforce any of these limits. The server will ask
+ * all plugins that registered this endpoint and collect all results. In case {@link
+ * #requestTokens(String, QuotaRequestContext, long)} was called and one or more plugins returned an
+ * erroneous result, the server will call {@link #refill(String, QuotaRequestContext, long)} on all
+ * plugins with the same parameters. Plugins that deducted tokens in the {@link
+ * #requestTokens(String, QuotaRequestContext, long)} call can refill them so that users don't get
+ * charged any quota for failed requests.
+ *
+ * <p>Not all implementations will need to deduct quota on {@link #requestTokens(String,
+ * QuotaRequestContext, long)}}. Implementations that work on top of instance-attributes, such as
+ * the number of projects per instance can choose not to keep any state and always check how many
+ * existing projects there are and if adding the inquired number would exceed the limit. In this
+ * case, {@link #requestTokens(String, QuotaRequestContext, long)} and {@link #dryRun(String,
+ * QuotaRequestContext, long)} share the same implementation and {@link #refill(String,
+ * QuotaRequestContext, long)} is a no-op.
+ */
+@ExtensionPoint
+public interface QuotaEnforcer {
+  /**
+   * Checks if there is at least {@code numTokens} quota to fulfil the request. Bucket-based
+   * implementations can deduct the inquired number of tokens from the bucket.
+   */
+  QuotaResponse requestTokens(String quotaGroup, QuotaRequestContext ctx, long numTokens);
+
+  /**
+   * Checks if there is at least {@code numTokens} quota to fulfil the request. This is a pre-flight
+   * request, implementations should not deduct tokens from a bucket, yet.
+   */
+  QuotaResponse dryRun(String quotaGroup, QuotaRequestContext ctx, long numTokens);
+
+  /**
+   * Returns available tokens that can be later requested.
+   *
+   * <p>This is used as a pre-flight check for the exceptional case when the requested number of
+   * tokens is not known in advance. Implementation should not deduct tokens from a bucket. It
+   * should be followed by a call to {@link #requestTokens(String, QuotaRequestContext, long)} with
+   * the number of tokens that were eventually used. It is in {@link QuotaBackend} callers
+   * discretion to ensure that {@link
+   * com.google.gerrit.server.quota.QuotaBackend.WithResource#requestTokens(String, long)} is
+   * called.
+   */
+  QuotaResponse availableTokens(String quotaGroup, QuotaRequestContext ctx);
+
+  /**
+   * A previously requested and deducted quota has to be refilled (if possible) because the request
+   * failed other quota checks. Implementations can choose to leave this a no-op in case they are
+   * the first line of defence (e.g. always deduct HTTP quota even if the request failed for other
+   * quota issues so that the user gets throttled).
+   *
+   * <p>Will not be called if the {@link #requestTokens(String, QuotaRequestContext, long)} call
+   * returned {@link QuotaResponse.Status#NO_OP}.
+   */
+  void refill(String quotaGroup, QuotaRequestContext ctx, long numTokens);
+}
diff --git a/java/com/google/gerrit/server/quota/QuotaException.java b/java/com/google/gerrit/server/quota/QuotaException.java
new file mode 100644
index 0000000..be13c0ec0
--- /dev/null
+++ b/java/com/google/gerrit/server/quota/QuotaException.java
@@ -0,0 +1,29 @@
+// 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.quota;
+
+import com.google.gerrit.extensions.restapi.RestApiException;
+
+/**
+ * Exception that was encountered while checking if there is sufficient quota to fulfil the request.
+ * Can be propagated directly to the REST API.
+ */
+public class QuotaException extends RestApiException {
+  private static final long serialVersionUID = 1L;
+
+  public QuotaException(String reason) {
+    super(reason);
+  }
+}
diff --git a/java/com/google/gerrit/server/quota/QuotaGroupDefinitions.java b/java/com/google/gerrit/server/quota/QuotaGroupDefinitions.java
new file mode 100644
index 0000000..5110538
--- /dev/null
+++ b/java/com/google/gerrit/server/quota/QuotaGroupDefinitions.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.quota;
+
+public class QuotaGroupDefinitions {
+  /**
+   * Definition of repository size quota group. {@link QuotaEnforcer} implementations for repository
+   * size quota have to act on requests with this group name.
+   */
+  public static final String REPOSITORY_SIZE_GROUP = "/repository:size";
+
+  private QuotaGroupDefinitions() {}
+}
diff --git a/java/com/google/gerrit/server/quota/QuotaRequestContext.java b/java/com/google/gerrit/server/quota/QuotaRequestContext.java
new file mode 100644
index 0000000..90b501c
--- /dev/null
+++ b/java/com/google/gerrit/server/quota/QuotaRequestContext.java
@@ -0,0 +1,54 @@
+// 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.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.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
+import java.util.Optional;
+
+@AutoValue
+public abstract class QuotaRequestContext {
+
+  public static Builder builder() {
+    return new AutoValue_QuotaRequestContext.Builder().user(new AnonymousUser());
+  }
+
+  public abstract CurrentUser user();
+
+  public abstract Optional<Project.NameKey> project();
+
+  public abstract Optional<Change.Id> change();
+
+  public abstract Optional<Account.Id> account();
+
+  public abstract Builder toBuilder();
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+    public abstract QuotaRequestContext.Builder user(CurrentUser user);
+
+    public abstract QuotaRequestContext.Builder account(Account.Id account);
+
+    public abstract QuotaRequestContext.Builder project(Project.NameKey project);
+
+    public abstract QuotaRequestContext.Builder change(Change.Id change);
+
+    public abstract QuotaRequestContext build();
+  }
+}
diff --git a/java/com/google/gerrit/server/quota/QuotaResponse.java b/java/com/google/gerrit/server/quota/QuotaResponse.java
new file mode 100644
index 0000000..940f731
--- /dev/null
+++ b/java/com/google/gerrit/server/quota/QuotaResponse.java
@@ -0,0 +1,133 @@
+// 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.quota;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Streams;
+import java.util.Collection;
+import java.util.Optional;
+import java.util.OptionalLong;
+import java.util.stream.Collectors;
+
+@AutoValue
+public abstract class QuotaResponse {
+  public enum Status {
+    /** The quota requests succeeded. */
+    OK,
+
+    /**
+     * The quota succeeded, but was a no-op because the plugin does not enforce this quota group
+     * (equivalent to OK, but relevant for debugging).
+     */
+    NO_OP,
+
+    /**
+     * The requested quota could not be allocated. This status code is not used to indicate
+     * processing failures as these are propagated as {@code RuntimeException}s.
+     */
+    ERROR;
+
+    public boolean isOk() {
+      return this == OK;
+    }
+
+    public boolean isError() {
+      return this == ERROR;
+    }
+  }
+
+  public static QuotaResponse ok() {
+    return new AutoValue_QuotaResponse.Builder().status(Status.OK).build();
+  }
+
+  public static QuotaResponse ok(long tokens) {
+    return new AutoValue_QuotaResponse.Builder().status(Status.OK).availableTokens(tokens).build();
+  }
+
+  public static QuotaResponse noOp() {
+    return new AutoValue_QuotaResponse.Builder().status(Status.NO_OP).build();
+  }
+
+  public static QuotaResponse error(String message) {
+    return new AutoValue_QuotaResponse.Builder().status(Status.ERROR).message(message).build();
+  }
+
+  public abstract Status status();
+
+  public abstract Optional<Long> availableTokens();
+
+  public abstract Optional<String> message();
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+    public abstract QuotaResponse.Builder status(Status status);
+
+    public abstract QuotaResponse.Builder availableTokens(Long tokens);
+
+    public abstract QuotaResponse.Builder message(String message);
+
+    public abstract QuotaResponse build();
+  }
+
+  @AutoValue
+  public abstract static class Aggregated {
+    public static Aggregated create(Collection<QuotaResponse> responses) {
+      return new AutoValue_QuotaResponse_Aggregated(ImmutableList.copyOf(responses));
+    }
+
+    protected abstract ImmutableList<QuotaResponse> responses();
+
+    public boolean hasError() {
+      return responses().stream().anyMatch(r -> r.status().isError());
+    }
+
+    public ImmutableList<QuotaResponse> all() {
+      return responses();
+    }
+
+    public ImmutableList<QuotaResponse> ok() {
+      return responses().stream().filter(r -> r.status().isOk()).collect(toImmutableList());
+    }
+
+    public OptionalLong availableTokens() {
+      return responses().stream()
+          .filter(r -> r.status().isOk() && r.availableTokens().isPresent())
+          .mapToLong(r -> r.availableTokens().get())
+          .min();
+    }
+
+    public ImmutableList<QuotaResponse> error() {
+      return responses().stream().filter(r -> r.status().isError()).collect(toImmutableList());
+    }
+
+    public String errorMessage() {
+      return error().stream()
+          .map(QuotaResponse::message)
+          .flatMap(Streams::stream)
+          .collect(Collectors.joining(", "));
+    }
+
+    public void throwOnError() throws QuotaException {
+      String errorMessage = errorMessage();
+      if (!Strings.isNullOrEmpty(errorMessage)) {
+        throw new QuotaException(errorMessage);
+      }
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/BUILD b/java/com/google/gerrit/server/restapi/BUILD
index 6661fd7..9e733b2 100644
--- a/java/com/google/gerrit/server/restapi/BUILD
+++ b/java/com/google/gerrit/server/restapi/BUILD
@@ -10,10 +10,13 @@
     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/index",
         "//java/com/google/gerrit/index:query_exception",
         "//java/com/google/gerrit/index/project",
+        "//java/com/google/gerrit/jgit",
+        "//java/com/google/gerrit/json",
         "//java/com/google/gerrit/mail",
         "//java/com/google/gerrit/metrics",
         "//java/com/google/gerrit/prettify:server",
@@ -23,12 +26,10 @@
         "//java/com/google/gerrit/server/logging",
         "//java/com/google/gerrit/server/util/time",
         "//java/com/google/gerrit/util/cli",
-        "//java/org/eclipse/jgit:server",
         "//lib:args4j",
         "//lib:blame-cache",
         "//lib:gson",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib:servlet-api-3_1",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
diff --git a/java/com/google/gerrit/server/restapi/access/ListAccess.java b/java/com/google/gerrit/server/restapi/access/ListAccess.java
index 3f01c6c..a3e9530 100644
--- a/java/com/google/gerrit/server/restapi/access/ListAccess.java
+++ b/java/com/google/gerrit/server/restapi/access/ListAccess.java
@@ -22,7 +22,6 @@
 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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -50,7 +49,7 @@
   @Override
   public Map<String, ProjectAccessInfo> apply(TopLevelResource resource)
       throws ResourceNotFoundException, ResourceConflictException, IOException,
-          PermissionBackendException, OrmException {
+          PermissionBackendException {
     Map<String, ProjectAccessInfo> access = new TreeMap<>();
     for (String p : projects) {
       access.put(p, getAccess.apply(new Project.NameKey(p)));
diff --git a/java/com/google/gerrit/server/restapi/account/AccountsCollection.java b/java/com/google/gerrit/server/restapi/account/AccountsCollection.java
index c301ab2..119e2e4 100644
--- a/java/com/google/gerrit/server/restapi/account/AccountsCollection.java
+++ b/java/com/google/gerrit/server/restapi/account/AccountsCollection.java
@@ -21,11 +21,9 @@
 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.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountControl;
 import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException;
 import com.google.gerrit.server.account.AccountResource;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -35,32 +33,30 @@
 @Singleton
 public class AccountsCollection implements RestCollection<TopLevelResource, AccountResource> {
   private final AccountResolver accountResolver;
-  private final AccountControl.Factory accountControlFactory;
   private final Provider<QueryAccounts> list;
   private final DynamicMap<RestView<AccountResource>> views;
 
   @Inject
   public AccountsCollection(
       AccountResolver accountResolver,
-      AccountControl.Factory accountControlFactory,
       Provider<QueryAccounts> list,
       DynamicMap<RestView<AccountResource>> views) {
     this.accountResolver = accountResolver;
-    this.accountControlFactory = accountControlFactory;
     this.list = list;
     this.views = views;
   }
 
   @Override
   public AccountResource parse(TopLevelResource root, IdString id)
-      throws ResourceNotFoundException, AuthException, OrmException, IOException,
-          ConfigInvalidException {
-    IdentifiedUser user = accountResolver.parseId(id.get());
-    if (user == null || !accountControlFactory.get().canSee(user.getAccount())) {
-      throw new ResourceNotFoundException(
-          String.format("Account '%s' is not found or ambiguous", id));
+      throws ResourceNotFoundException, AuthException, IOException, ConfigInvalidException {
+    try {
+      return new AccountResource(accountResolver.resolve(id.get()).asUniqueUser());
+    } catch (UnresolvableAccountException e) {
+      if (e.isSelf()) {
+        throw new AuthException(e.getMessage(), e);
+      }
+      throw new ResourceNotFoundException(e.getMessage(), e);
     }
-    return new AccountResource(user);
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/restapi/account/AddSshKey.java b/java/com/google/gerrit/server/restapi/account/AddSshKey.java
index 4bdf52c..7c05f4e 100644
--- a/java/com/google/gerrit/server/restapi/account/AddSshKey.java
+++ b/java/com/google/gerrit/server/restapi/account/AddSshKey.java
@@ -18,8 +18,8 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.common.io.ByteSource;
-import com.google.gerrit.common.errors.EmailException;
-import com.google.gerrit.common.errors.InvalidSshKeyException;
+import com.google.gerrit.exceptions.EmailException;
+import com.google.gerrit.exceptions.InvalidSshKeyException;
 import com.google.gerrit.extensions.api.accounts.SshKeyInput;
 import com.google.gerrit.extensions.common.SshKeyInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -37,7 +37,6 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.ssh.SshKeyCache;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -72,7 +71,7 @@
 
   @Override
   public Response<SshKeyInfo> apply(AccountResource rsrc, SshKeyInput input)
-      throws AuthException, BadRequestException, OrmException, IOException, ConfigInvalidException,
+      throws AuthException, BadRequestException, IOException, ConfigInvalidException,
           PermissionBackendException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
@@ -109,7 +108,7 @@
       }
 
       user.getUserName().ifPresent(sshKeyCache::evict);
-      return Response.<SshKeyInfo>created(GetSshKeys.newSshKeyInfo(sshKey));
+      return Response.created(GetSshKeys.newSshKeyInfo(sshKey));
     } catch (InvalidSshKeyException e) {
       throw new BadRequestException(e.getMessage());
     }
diff --git a/java/com/google/gerrit/server/restapi/account/CreateAccount.java b/java/com/google/gerrit/server/restapi/account/CreateAccount.java
index 42b3daf..bf50e10 100644
--- a/java/com/google/gerrit/server/restapi/account/CreateAccount.java
+++ b/java/com/google/gerrit/server/restapi/account/CreateAccount.java
@@ -22,12 +22,11 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.errors.InvalidSshKeyException;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.InvalidSshKeyException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.api.accounts.AccountInput;
 import com.google.gerrit.extensions.common.AccountInfo;
-import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -37,7 +36,6 @@
 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.Sequences;
 import com.google.gerrit.server.UserInitiated;
 import com.google.gerrit.server.account.AccountExternalIdCreator;
 import com.google.gerrit.server.account.AccountLoader;
@@ -50,9 +48,10 @@
 import com.google.gerrit.server.group.db.GroupsUpdate;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
 import com.google.gerrit.server.mail.send.OutgoingEmailValidator;
+import com.google.gerrit.server.notedb.Sequences;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.gerrit.server.ssh.SshKeyCache;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -73,7 +72,7 @@
   private final SshKeyCache sshKeyCache;
   private final Provider<AccountsUpdate> accountsUpdateProvider;
   private final AccountLoader.Factory infoLoader;
-  private final DynamicSet<AccountExternalIdCreator> externalIdCreators;
+  private final PluginSetContext<AccountExternalIdCreator> externalIdCreators;
   private final Provider<GroupsUpdate> groupsUpdate;
   private final OutgoingEmailValidator validator;
 
@@ -85,7 +84,7 @@
       SshKeyCache sshKeyCache,
       @UserInitiated Provider<AccountsUpdate> accountsUpdateProvider,
       AccountLoader.Factory infoLoader,
-      DynamicSet<AccountExternalIdCreator> externalIdCreators,
+      PluginSetContext<AccountExternalIdCreator> externalIdCreators,
       @UserInitiated Provider<GroupsUpdate> groupsUpdate,
       OutgoingEmailValidator validator) {
     this.seq = seq;
@@ -103,13 +102,13 @@
   public Response<AccountInfo> apply(
       TopLevelResource rsrc, IdString id, @Nullable AccountInput input)
       throws BadRequestException, ResourceConflictException, UnprocessableEntityException,
-          OrmException, IOException, ConfigInvalidException, PermissionBackendException {
+          IOException, ConfigInvalidException, PermissionBackendException {
     return apply(id, input != null ? input : new AccountInput());
   }
 
   public Response<AccountInfo> apply(IdString id, AccountInput input)
       throws BadRequestException, ResourceConflictException, UnprocessableEntityException,
-          OrmException, IOException, ConfigInvalidException, PermissionBackendException {
+          IOException, ConfigInvalidException, PermissionBackendException {
     String username = id.get();
     if (input.username != null && !username.equals(input.username)) {
       throw new BadRequestException("username must match URL");
@@ -131,9 +130,7 @@
     }
 
     extIds.add(ExternalId.createUsername(username, accountId, input.httpPassword));
-    for (AccountExternalIdCreator c : externalIdCreators) {
-      extIds.addAll(c.create(accountId, username, input.email));
-    }
+    externalIdCreators.runEach(c -> extIds.addAll(c.create(accountId, username, input.email)));
 
     try {
       accountsUpdateProvider
@@ -191,7 +188,7 @@
   }
 
   private void addGroupMember(AccountGroup.UUID groupUuid, Account.Id accountId)
-      throws OrmException, IOException, NoSuchGroupException, ConfigInvalidException {
+      throws IOException, NoSuchGroupException, ConfigInvalidException {
     InternalGroupUpdate groupUpdate =
         InternalGroupUpdate.builder()
             .setMemberModification(memberIds -> Sets.union(memberIds, ImmutableSet.of(accountId)))
diff --git a/java/com/google/gerrit/server/restapi/account/CreateEmail.java b/java/com/google/gerrit/server/restapi/account/CreateEmail.java
index e4e8525..cc4cf21 100644
--- a/java/com/google/gerrit/server/restapi/account/CreateEmail.java
+++ b/java/com/google/gerrit/server/restapi/account/CreateEmail.java
@@ -17,7 +17,7 @@
 import static com.google.gerrit.extensions.client.AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT;
 
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.accounts.EmailInput;
 import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.extensions.common.EmailInfo;
@@ -41,7 +41,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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -84,7 +83,7 @@
 
   @Override
   public Response<EmailInfo> apply(AccountResource rsrc, IdString id, EmailInput input)
-      throws RestApiException, OrmException, EmailException, MethodNotAllowedException, IOException,
+      throws RestApiException, EmailException, MethodNotAllowedException, IOException,
           ConfigInvalidException, PermissionBackendException {
     if (input == null) {
       input = new EmailInput();
@@ -103,7 +102,7 @@
 
   /** To be used from plugins that want to create emails without permission checks. */
   public Response<EmailInfo> apply(IdentifiedUser user, IdString id, EmailInput input)
-      throws RestApiException, OrmException, EmailException, MethodNotAllowedException, IOException,
+      throws RestApiException, EmailException, MethodNotAllowedException, IOException,
           ConfigInvalidException, PermissionBackendException {
     String email = id.get().trim();
 
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteActive.java b/java/com/google/gerrit/server/restapi/account/DeleteActive.java
index 4302513..ffd7893 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteActive.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteActive.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.SetInactiveFlag;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -46,7 +45,7 @@
 
   @Override
   public Response<?> apply(AccountResource rsrc, Input input)
-      throws RestApiException, OrmException, IOException, ConfigInvalidException {
+      throws RestApiException, IOException, ConfigInvalidException {
     if (self.get().hasSameAccountId(rsrc.getUser())) {
       throw new ResourceConflictException("cannot deactivate own account");
     }
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java b/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java
index 2a7fe9a..4788301 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java
@@ -32,11 +32,9 @@
 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.Account.Id;
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.PatchSetUtil;
@@ -58,7 +56,6 @@
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -74,7 +71,6 @@
 public class DeleteDraftComments
     implements RestModifyView<AccountResource, DeleteDraftCommentsInput> {
 
-  private final Provider<ReviewDb> db;
   private final Provider<CurrentUser> userProvider;
   private final BatchUpdate.Factory batchUpdateFactory;
   private final Provider<ChangeQueryBuilder> queryBuilderProvider;
@@ -88,7 +84,6 @@
 
   @Inject
   DeleteDraftComments(
-      Provider<ReviewDb> db,
       Provider<CurrentUser> userProvider,
       Factory batchUpdateFactory,
       Provider<ChangeQueryBuilder> queryBuilderProvider,
@@ -99,7 +94,6 @@
       CommentsUtil commentsUtil,
       PatchSetUtil psUtil,
       PatchListCache patchListCache) {
-    this.db = db;
     this.userProvider = userProvider;
     this.batchUpdateFactory = batchUpdateFactory;
     this.queryBuilderProvider = queryBuilderProvider;
@@ -115,7 +109,7 @@
   @Override
   public ImmutableList<DeletedDraftCommentInfo> apply(
       AccountResource rsrc, DeleteDraftCommentsInput input)
-      throws RestApiException, OrmException, UpdateException {
+      throws RestApiException, UpdateException {
     CurrentUser user = userProvider.get();
     if (!user.isIdentifiedUser()) {
       throw new AuthException("Authentication required");
@@ -142,7 +136,7 @@
             .query(predicate(accountId, input))) {
       BatchUpdate update =
           updates.computeIfAbsent(
-              cd.project(), p -> batchUpdateFactory.create(db.get(), p, rsrc.getUser(), now));
+              cd.project(), p -> batchUpdateFactory.create(p, rsrc.getUser(), now));
       Op op = new Op(commentFormatter, accountId);
       update.addOp(cd.getId(), op);
       ops.add(op);
@@ -151,7 +145,7 @@
     // Currently there's no way to let some updates succeed even if others fail. Even if there were,
     // all updates from this operation only happen in All-Users and thus are fully atomic, so
     // allowing partial failure would have little value.
-    batchUpdateFactory.execute(updates.values(), BatchUpdateListener.NONE, false);
+    BatchUpdate.execute(updates.values(), BatchUpdateListener.NONE, false);
 
     return ops.stream().map(Op::getResult).filter(Objects::nonNull).collect(toImmutableList());
   }
@@ -174,22 +168,21 @@
     private final Account.Id accountId;
     private DeletedDraftCommentInfo result;
 
-    Op(CommentFormatter commentFormatter, Id accountId) {
+    Op(CommentFormatter commentFormatter, Account.Id accountId) {
       this.commentFormatter = commentFormatter;
       this.accountId = accountId;
     }
 
     @Override
     public boolean updateChange(ChangeContext ctx)
-        throws OrmException, PatchListNotAvailableException, PermissionBackendException {
+        throws PatchListNotAvailableException, PermissionBackendException {
       ImmutableList.Builder<CommentInfo> comments = ImmutableList.builder();
       boolean dirty = false;
-      for (Comment c : commentsUtil.draftByChangeAuthor(ctx.getDb(), ctx.getNotes(), accountId)) {
+      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.getDb(), ctx.getNotes(), psId));
-        commentsUtil.deleteComments(ctx.getDb(), ctx.getUpdate(psId), Collections.singleton(c));
+        setCommentRevId(c, patchListCache, ctx.getChange(), psUtil.get(ctx.getNotes(), psId));
+        commentsUtil.deleteComments(ctx.getUpdate(psId), Collections.singleton(c));
         comments.add(commentFormatter.format(c));
       }
       if (dirty) {
@@ -197,10 +190,9 @@
         result.change =
             changeJsonFactory
                 .create(ListChangesOption.SKIP_MERGEABLE)
-                .format(changeDataFactory.create(ctx.getDb(), ctx.getNotes()));
+                .format(changeDataFactory.create(ctx.getNotes()));
         result.deleted = comments.build();
       }
-      ctx.dontBumpLastUpdatedOn();
       return dirty;
     }
 
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteEmail.java b/java/com/google/gerrit/server/restapi/account/DeleteEmail.java
index 6bacde2..7a03005 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteEmail.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteEmail.java
@@ -35,7 +35,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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -69,7 +68,7 @@
   @Override
   public Response<?> apply(AccountResource.Email rsrc, Input input)
       throws AuthException, ResourceNotFoundException, ResourceConflictException,
-          MethodNotAllowedException, OrmException, IOException, ConfigInvalidException,
+          MethodNotAllowedException, IOException, ConfigInvalidException,
           PermissionBackendException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
@@ -79,7 +78,7 @@
 
   public Response<?> apply(IdentifiedUser user, String email)
       throws ResourceNotFoundException, ResourceConflictException, MethodNotAllowedException,
-          OrmException, IOException, ConfigInvalidException {
+          IOException, ConfigInvalidException {
     if (!realm.allowsEdit(AccountFieldName.REGISTER_NEW_EMAIL)) {
       throw new MethodNotAllowedException("realm does not allow deleting emails");
     }
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteExternalIds.java b/java/com/google/gerrit/server/restapi/account/DeleteExternalIds.java
index 9fb535c..82b445f 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteExternalIds.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteExternalIds.java
@@ -33,7 +33,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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -66,8 +65,7 @@
 
   @Override
   public Response<?> apply(AccountResource resource, List<String> extIds)
-      throws RestApiException, IOException, OrmException, ConfigInvalidException,
-          PermissionBackendException {
+      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
     if (!self.get().hasSameAccountId(resource.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.ACCESS_DATABASE);
     }
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteSshKey.java b/java/com/google/gerrit/server/restapi/account/DeleteSshKey.java
index a60fd8e..054f4bc 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteSshKey.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteSshKey.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.restapi.account;
 
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.Response;
@@ -29,7 +29,6 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.ssh.SshKeyCache;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -63,8 +62,8 @@
 
   @Override
   public Response<?> apply(AccountResource.SshKey rsrc, Input input)
-      throws AuthException, OrmException, RepositoryNotFoundException, IOException,
-          ConfigInvalidException, PermissionBackendException {
+      throws AuthException, RepositoryNotFoundException, 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/DeleteWatchedProjects.java b/java/com/google/gerrit/server/restapi/account/DeleteWatchedProjects.java
index deb70fe..798aad1 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteWatchedProjects.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteWatchedProjects.java
@@ -31,7 +31,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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -59,8 +58,8 @@
 
   @Override
   public Response<?> apply(AccountResource rsrc, List<ProjectWatchInfo> input)
-      throws AuthException, UnprocessableEntityException, OrmException, IOException,
-          ConfigInvalidException, PermissionBackendException {
+      throws AuthException, UnprocessableEntityException, 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/GetAccount.java b/java/com/google/gerrit/server/restapi/account/GetAccount.java
index 6b73ae3b..6544f8d 100644
--- a/java/com/google/gerrit/server/restapi/account/GetAccount.java
+++ b/java/com/google/gerrit/server/restapi/account/GetAccount.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -33,7 +32,7 @@
   }
 
   @Override
-  public AccountInfo apply(AccountResource rsrc) throws OrmException, PermissionBackendException {
+  public AccountInfo apply(AccountResource rsrc) throws PermissionBackendException {
     AccountLoader loader = infoFactory.create(true);
     AccountInfo info = loader.get(rsrc.getUser().getAccountId());
     loader.fill();
diff --git a/java/com/google/gerrit/server/restapi/account/GetCapabilities.java b/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
index 7889f6e..77f1668 100644
--- a/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
+++ b/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
@@ -17,7 +17,7 @@
 import static com.google.gerrit.common.data.GlobalCapability.PRIORITY;
 import static com.google.gerrit.server.permissions.DefaultPermissionMappings.globalOrPluginPermissionName;
 import static com.google.gerrit.server.permissions.DefaultPermissionMappings.globalPermissionName;
-import static com.google.gerrit.server.permissions.DefaultPermissionMappings.pluginPermissionName;
+import static com.google.gerrit.server.permissions.DefaultPermissionMappings.pluginCapabilityName;
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.common.data.GlobalCapability;
@@ -30,9 +30,9 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 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.OutputFormat;
 import com.google.gerrit.server.account.AccountLimits;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.AccountResource.Capability;
@@ -113,7 +113,7 @@
     for (String pluginName : pluginCapabilities.plugins()) {
       for (String capability : pluginCapabilities.byPlugin(pluginName).keySet()) {
         PluginPermission p = new PluginPermission(pluginName, capability);
-        if (want(pluginPermissionName(p))) {
+        if (want(pluginCapabilityName(p))) {
           toTest.add(p);
         }
       }
diff --git a/java/com/google/gerrit/server/restapi/account/GetDetail.java b/java/com/google/gerrit/server/restapi/account/GetDetail.java
index 97d0c60..72044c4 100644
--- a/java/com/google/gerrit/server/restapi/account/GetDetail.java
+++ b/java/com/google/gerrit/server/restapi/account/GetDetail.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.InternalAccountDirectory;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.Collections;
@@ -37,8 +36,7 @@
   }
 
   @Override
-  public AccountDetailInfo apply(AccountResource rsrc)
-      throws OrmException, PermissionBackendException {
+  public AccountDetailInfo apply(AccountResource rsrc) throws PermissionBackendException {
     Account a = rsrc.getUser().getAccount();
     AccountDetailInfo info = new AccountDetailInfo(a.getId().get());
     info.registeredOn = a.getRegisteredOn();
diff --git a/java/com/google/gerrit/server/restapi/account/GetExternalIds.java b/java/com/google/gerrit/server/restapi/account/GetExternalIds.java
index 7a420ab..ef448dc 100644
--- a/java/com/google/gerrit/server/restapi/account/GetExternalIds.java
+++ b/java/com/google/gerrit/server/restapi/account/GetExternalIds.java
@@ -29,7 +29,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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -60,7 +59,7 @@
 
   @Override
   public List<AccountExternalIdInfo> apply(AccountResource resource)
-      throws RestApiException, IOException, OrmException, PermissionBackendException {
+      throws RestApiException, IOException, PermissionBackendException {
     if (!self.get().hasSameAccountId(resource.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.ACCESS_DATABASE);
     }
diff --git a/java/com/google/gerrit/server/restapi/account/GetGroups.java b/java/com/google/gerrit/server/restapi/account/GetGroups.java
index ad9746e..569ff76 100644
--- a/java/com/google/gerrit/server/restapi/account/GetGroups.java
+++ b/java/com/google/gerrit/server/restapi/account/GetGroups.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.restapi.account;
 
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Account;
@@ -24,7 +24,6 @@
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.group.GroupJson;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.ArrayList;
@@ -43,8 +42,7 @@
   }
 
   @Override
-  public List<GroupInfo> apply(AccountResource resource)
-      throws OrmException, PermissionBackendException {
+  public List<GroupInfo> apply(AccountResource resource) throws PermissionBackendException {
     IdentifiedUser user = resource.getUser();
     Account.Id userId = user.getAccountId();
     Set<AccountGroup.UUID> knownGroups = user.getEffectiveGroups().getKnownGroups();
diff --git a/java/com/google/gerrit/server/restapi/account/GetSshKeys.java b/java/com/google/gerrit/server/restapi/account/GetSshKeys.java
index a49f9df..408aa5f 100644
--- a/java/com/google/gerrit/server/restapi/account/GetSshKeys.java
+++ b/java/com/google/gerrit/server/restapi/account/GetSshKeys.java
@@ -27,7 +27,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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -55,8 +54,8 @@
 
   @Override
   public List<SshKeyInfo> apply(AccountResource rsrc)
-      throws AuthException, OrmException, RepositoryNotFoundException, IOException,
-          ConfigInvalidException, PermissionBackendException {
+      throws AuthException, RepositoryNotFoundException, IOException, ConfigInvalidException,
+          PermissionBackendException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
diff --git a/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java b/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
index ba8c72a..fce324e 100644
--- a/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
+++ b/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
@@ -33,7 +33,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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -57,8 +56,8 @@
 
   @Override
   public List<ProjectWatchInfo> apply(AccountResource rsrc)
-      throws OrmException, AuthException, IOException, ConfigInvalidException,
-          PermissionBackendException, ResourceNotFoundException {
+      throws AuthException, IOException, ConfigInvalidException, PermissionBackendException,
+          ResourceNotFoundException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     }
diff --git a/java/com/google/gerrit/server/restapi/account/Module.java b/java/com/google/gerrit/server/restapi/account/Module.java
index 9b012f7..f41764d 100644
--- a/java/com/google/gerrit/server/restapi/account/Module.java
+++ b/java/com/google/gerrit/server/restapi/account/Module.java
@@ -118,7 +118,7 @@
   @ServerInitiated
   AccountsUpdate provideServerInitiatedAccountsUpdate(
       AccountsUpdate.Factory accountsUpdateFactory, ExternalIdNotes.Factory extIdNotesFactory) {
-    return accountsUpdateFactory.create(null, extIdNotesFactory);
+    return accountsUpdateFactory.createWithServerIdent(extIdNotesFactory);
   }
 
   @Provides
diff --git a/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java b/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java
index f29a0eb..14bd492 100644
--- a/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java
+++ b/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java
@@ -29,7 +29,6 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.project.ProjectsCollection;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -66,8 +65,7 @@
 
   @Override
   public List<ProjectWatchInfo> apply(AccountResource rsrc, List<ProjectWatchInfo> input)
-      throws OrmException, RestApiException, IOException, ConfigInvalidException,
-          PermissionBackendException {
+      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/PutActive.java b/java/com/google/gerrit/server/restapi/account/PutActive.java
index 8255781..a6ffaa6 100644
--- a/java/com/google/gerrit/server/restapi/account/PutActive.java
+++ b/java/com/google/gerrit/server/restapi/account/PutActive.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.SetInactiveFlag;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -41,7 +40,7 @@
 
   @Override
   public Response<String> apply(AccountResource rsrc, Input input)
-      throws RestApiException, OrmException, IOException, ConfigInvalidException {
+      throws RestApiException, IOException, ConfigInvalidException {
     return setInactiveFlag.activate(rsrc.getUser().getAccountId());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/PutAgreement.java b/java/com/google/gerrit/server/restapi/account/PutAgreement.java
index ee244d0..147eec8 100644
--- a/java/com/google/gerrit/server/restapi/account/PutAgreement.java
+++ b/java/com/google/gerrit/server/restapi/account/PutAgreement.java
@@ -17,7 +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.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.api.accounts.AgreementInput;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -35,7 +35,6 @@
 import com.google.gerrit.server.extensions.events.AgreementSignup;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.restapi.group.AddMembers;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -67,7 +66,7 @@
 
   @Override
   public Response<String> apply(AccountResource resource, AgreementInput input)
-      throws IOException, OrmException, RestApiException, ConfigInvalidException {
+      throws IOException, RestApiException, ConfigInvalidException {
     if (!agreementsEnabled) {
       throw new MethodNotAllowedException("contributor agreements disabled");
     }
diff --git a/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java b/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java
index d671ebf..f029ef9 100644
--- a/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java
+++ b/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java
@@ -18,7 +18,8 @@
 
 import com.google.common.base.Strings;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.common.UsedAt;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.common.HttpPasswordInput;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -27,7 +28,6 @@
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.UsedAt;
 import com.google.gerrit.server.UserInitiated;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.AccountsUpdate;
@@ -37,7 +37,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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -85,8 +84,8 @@
 
   @Override
   public Response<String> apply(AccountResource rsrc, HttpPasswordInput input)
-      throws AuthException, ResourceNotFoundException, ResourceConflictException, OrmException,
-          IOException, ConfigInvalidException, PermissionBackendException {
+      throws AuthException, ResourceNotFoundException, ResourceConflictException, IOException,
+          ConfigInvalidException, PermissionBackendException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     }
@@ -111,7 +110,7 @@
 
   @UsedAt(UsedAt.Project.PLUGIN_SERVICEUSER)
   public Response<String> apply(IdentifiedUser user, String newPassword)
-      throws ResourceNotFoundException, ResourceConflictException, OrmException, IOException,
+      throws ResourceNotFoundException, ResourceConflictException, IOException,
           ConfigInvalidException {
     String userName =
         user.getUserName().orElseThrow(() -> new ResourceConflictException("username must be set"));
@@ -137,7 +136,7 @@
           "Cannot send HttpPassword update message to %s", user.getAccount().getPreferredEmail());
     }
 
-    return Strings.isNullOrEmpty(newPassword) ? Response.<String>none() : Response.ok(newPassword);
+    return Strings.isNullOrEmpty(newPassword) ? Response.none() : Response.ok(newPassword);
   }
 
   @UsedAt(UsedAt.Project.PLUGIN_SERVICEUSER)
diff --git a/java/com/google/gerrit/server/restapi/account/PutName.java b/java/com/google/gerrit/server/restapi/account/PutName.java
index 1e00aac..30dfc66 100644
--- a/java/com/google/gerrit/server/restapi/account/PutName.java
+++ b/java/com/google/gerrit/server/restapi/account/PutName.java
@@ -32,7 +32,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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -60,8 +59,8 @@
 
   @Override
   public Response<String> apply(AccountResource rsrc, NameInput input)
-      throws AuthException, MethodNotAllowedException, ResourceNotFoundException, OrmException,
-          IOException, PermissionBackendException, ConfigInvalidException {
+      throws AuthException, MethodNotAllowedException, ResourceNotFoundException, IOException,
+          PermissionBackendException, ConfigInvalidException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
@@ -70,7 +69,7 @@
 
   public Response<String> apply(IdentifiedUser user, NameInput input)
       throws MethodNotAllowedException, ResourceNotFoundException, IOException,
-          ConfigInvalidException, OrmException {
+          ConfigInvalidException {
     if (input == null) {
       input = new NameInput();
     }
diff --git a/java/com/google/gerrit/server/restapi/account/PutPreferred.java b/java/com/google/gerrit/server/restapi/account/PutPreferred.java
index 265bf53..b8edec3 100644
--- a/java/com/google/gerrit/server/restapi/account/PutPreferred.java
+++ b/java/com/google/gerrit/server/restapi/account/PutPreferred.java
@@ -34,7 +34,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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -69,8 +68,7 @@
 
   @Override
   public Response<String> apply(AccountResource.Email rsrc, Input input)
-      throws RestApiException, OrmException, IOException, PermissionBackendException,
-          ConfigInvalidException {
+      throws RestApiException, IOException, PermissionBackendException, ConfigInvalidException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
@@ -78,7 +76,7 @@
   }
 
   public Response<String> apply(IdentifiedUser user, String preferredEmail)
-      throws RestApiException, IOException, ConfigInvalidException, OrmException {
+      throws RestApiException, IOException, ConfigInvalidException {
     AtomicReference<Optional<RestApiException>> exception = new AtomicReference<>(Optional.empty());
     AtomicBoolean alreadyPreferred = new AtomicBoolean(false);
     accountsUpdateProvider
diff --git a/java/com/google/gerrit/server/restapi/account/PutStatus.java b/java/com/google/gerrit/server/restapi/account/PutStatus.java
index 9aee0a3..4f1128d 100644
--- a/java/com/google/gerrit/server/restapi/account/PutStatus.java
+++ b/java/com/google/gerrit/server/restapi/account/PutStatus.java
@@ -29,7 +29,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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -54,8 +53,8 @@
 
   @Override
   public Response<String> apply(AccountResource rsrc, StatusInput input)
-      throws AuthException, ResourceNotFoundException, OrmException, IOException,
-          PermissionBackendException, ConfigInvalidException {
+      throws AuthException, ResourceNotFoundException, IOException, PermissionBackendException,
+          ConfigInvalidException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
@@ -63,7 +62,7 @@
   }
 
   public Response<String> apply(IdentifiedUser user, StatusInput input)
-      throws ResourceNotFoundException, IOException, ConfigInvalidException, OrmException {
+      throws ResourceNotFoundException, IOException, ConfigInvalidException {
     if (input == null) {
       input = new StatusInput();
     }
diff --git a/java/com/google/gerrit/server/restapi/account/PutUsername.java b/java/com/google/gerrit/server/restapi/account/PutUsername.java
index 7fff626..76aece7 100644
--- a/java/com/google/gerrit/server/restapi/account/PutUsername.java
+++ b/java/com/google/gerrit/server/restapi/account/PutUsername.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
 
 import com.google.common.base.Strings;
+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;
@@ -36,8 +37,6 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.ssh.SshKeyCache;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -73,7 +72,7 @@
   @Override
   public String apply(AccountResource rsrc, UsernameInput input)
       throws AuthException, MethodNotAllowedException, UnprocessableEntityException,
-          ResourceConflictException, OrmException, IOException, ConfigInvalidException,
+          ResourceConflictException, IOException, ConfigInvalidException,
           PermissionBackendException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
@@ -108,7 +107,7 @@
               "Set Username via API",
               accountId,
               u -> u.addExternalId(ExternalId.create(key, accountId, null, null)));
-    } catch (OrmDuplicateKeyException dupeErr) {
+    } catch (DuplicateKeyException dupeErr) {
       // 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)) {
diff --git a/java/com/google/gerrit/server/restapi/account/QueryAccounts.java b/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
index 7f27edd..522301d 100644
--- a/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
+++ b/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.extensions.client.ListAccountsOption;
+import com.google.gerrit.extensions.client.ListOption;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.AccountVisibility;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -40,7 +41,6 @@
 import com.google.gerrit.server.query.account.AccountPredicates;
 import com.google.gerrit.server.query.account.AccountQueryBuilder;
 import com.google.gerrit.server.query.account.AccountQueryProcessor;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.util.Collections;
@@ -99,7 +99,7 @@
 
   @Option(name = "-O", usage = "Output option flags, in hex")
   void setOptionFlagsHex(String hex) {
-    options.addAll(ListAccountsOption.fromBits(Integer.parseInt(hex, 16)));
+    options.addAll(ListOption.fromBits(ListAccountsOption.class, Integer.parseInt(hex, 16)));
   }
 
   @Option(
@@ -150,7 +150,7 @@
 
   @Override
   public List<AccountInfo> apply(TopLevelResource rsrc)
-      throws OrmException, RestApiException, PermissionBackendException {
+      throws RestApiException, PermissionBackendException {
     if (Strings.isNullOrEmpty(query)) {
       throw new BadRequestException("missing query field");
     }
diff --git a/java/com/google/gerrit/server/restapi/account/SetDiffPreferences.java b/java/com/google/gerrit/server/restapi/account/SetDiffPreferences.java
index f4fa354..ee72ab7 100644
--- a/java/com/google/gerrit/server/restapi/account/SetDiffPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/SetDiffPreferences.java
@@ -29,7 +29,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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -56,7 +55,7 @@
   @Override
   public DiffPreferencesInfo apply(AccountResource rsrc, DiffPreferencesInfo input)
       throws RestApiException, ConfigInvalidException, RepositoryNotFoundException, IOException,
-          PermissionBackendException, OrmException {
+          PermissionBackendException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
diff --git a/java/com/google/gerrit/server/restapi/account/SetEditPreferences.java b/java/com/google/gerrit/server/restapi/account/SetEditPreferences.java
index 4e3f1d5..27d32f2 100644
--- a/java/com/google/gerrit/server/restapi/account/SetEditPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/SetEditPreferences.java
@@ -29,7 +29,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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -57,7 +56,7 @@
   @Override
   public EditPreferencesInfo apply(AccountResource rsrc, EditPreferencesInfo input)
       throws RestApiException, RepositoryNotFoundException, IOException, ConfigInvalidException,
-          PermissionBackendException, OrmException {
+          PermissionBackendException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
diff --git a/java/com/google/gerrit/server/restapi/account/SetPreferences.java b/java/com/google/gerrit/server/restapi/account/SetPreferences.java
index 2471689..c6623db 100644
--- a/java/com/google/gerrit/server/restapi/account/SetPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/SetPreferences.java
@@ -34,7 +34,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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -62,8 +61,7 @@
 
   @Override
   public GeneralPreferencesInfo apply(AccountResource rsrc, GeneralPreferencesInfo input)
-      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException,
-          OrmException {
+      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
diff --git a/java/com/google/gerrit/server/restapi/account/SshKeys.java b/java/com/google/gerrit/server/restapi/account/SshKeys.java
index 5802131..a6c4d80 100644
--- a/java/com/google/gerrit/server/restapi/account/SshKeys.java
+++ b/java/com/google/gerrit/server/restapi/account/SshKeys.java
@@ -28,7 +28,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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -64,7 +63,7 @@
 
   @Override
   public AccountResource.SshKey parse(AccountResource rsrc, IdString id)
-      throws ResourceNotFoundException, OrmException, IOException, ConfigInvalidException,
+      throws ResourceNotFoundException, IOException, ConfigInvalidException,
           PermissionBackendException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       try {
diff --git a/java/com/google/gerrit/server/restapi/account/StarredChanges.java b/java/com/google/gerrit/server/restapi/account/StarredChanges.java
index 88ffab8..c108dcb 100644
--- a/java/com/google/gerrit/server/restapi/account/StarredChanges.java
+++ b/java/com/google/gerrit/server/restapi/account/StarredChanges.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.server.restapi.account;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.DuplicateKeyException;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -40,8 +42,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.change.ChangesCollection;
 import com.google.gerrit.server.restapi.change.QueryChanges;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -68,7 +68,7 @@
 
   @Override
   public AccountResource.StarredChange parse(AccountResource parent, IdString id)
-      throws RestApiException, OrmException, PermissionBackendException, IOException {
+      throws RestApiException, PermissionBackendException, IOException {
     IdentifiedUser user = parent.getUser();
     ChangeResource change = changes.parse(TopLevelResource.INSTANCE, id);
     if (starredChangesUtil
@@ -86,15 +86,12 @@
 
   @Override
   public RestView<AccountResource> list() throws ResourceNotFoundException {
-    return new RestReadView<AccountResource>() {
-      @Override
-      public Object apply(AccountResource self)
-          throws BadRequestException, AuthException, OrmException, PermissionBackendException {
-        QueryChanges query = changes.list();
-        query.addQuery("starredby:" + self.getUser().getAccountId().get());
-        return query.apply(TopLevelResource.INSTANCE);
-      }
-    };
+    return (RestReadView<AccountResource>)
+        self -> {
+          QueryChanges query = changes.list();
+          query.addQuery("starredby:" + self.getUser().getAccountId().get());
+          return query.apply(TopLevelResource.INSTANCE);
+        };
   }
 
   @Singleton
@@ -117,7 +114,7 @@
 
     @Override
     public Response<?> apply(AccountResource rsrc, IdString id, EmptyInput in)
-        throws RestApiException, OrmException, IOException {
+        throws RestApiException, IOException {
       if (!self.get().hasSameAccountId(rsrc.getUser())) {
         throw new AuthException("not allowed to add starred change");
       }
@@ -127,7 +124,7 @@
         change = changes.parse(TopLevelResource.INSTANCE, id);
       } catch (ResourceNotFoundException e) {
         throw new UnprocessableEntityException(String.format("change %s not found", id.get()), e);
-      } catch (OrmException | PermissionBackendException | IOException e) {
+      } catch (StorageException | PermissionBackendException | IOException e) {
         logger.atSevere().withCause(e).log("cannot resolve change");
         throw new UnprocessableEntityException("internal server error", e);
       }
@@ -143,7 +140,7 @@
         throw new ResourceConflictException(e.getMessage());
       } catch (IllegalLabelException e) {
         throw new BadRequestException(e.getMessage());
-      } catch (OrmDuplicateKeyException e) {
+      } catch (DuplicateKeyException e) {
         return Response.none();
       }
       return Response.none();
@@ -182,7 +179,7 @@
 
     @Override
     public Response<?> apply(AccountResource.StarredChange rsrc, EmptyInput in)
-        throws AuthException, OrmException, IOException, IllegalLabelException {
+        throws AuthException, IOException, IllegalLabelException {
       if (!self.get().hasSameAccountId(rsrc.getUser())) {
         throw new AuthException("not allowed remove starred change");
       }
diff --git a/java/com/google/gerrit/server/restapi/account/Stars.java b/java/com/google/gerrit/server/restapi/account/Stars.java
index 5c4c4d5..c610adf 100644
--- a/java/com/google/gerrit/server/restapi/account/Stars.java
+++ b/java/com/google/gerrit/server/restapi/account/Stars.java
@@ -36,7 +36,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.change.ChangesCollection;
 import com.google.gerrit.server.restapi.change.QueryChanges;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -68,7 +67,7 @@
 
   @Override
   public Star parse(AccountResource parent, IdString id)
-      throws RestApiException, OrmException, PermissionBackendException, IOException {
+      throws RestApiException, PermissionBackendException, IOException {
     IdentifiedUser user = parent.getUser();
     ChangeResource change = changes.parse(TopLevelResource.INSTANCE, id);
     Set<String> labels = starredChangesUtil.getLabels(user.getAccountId(), change.getId());
@@ -99,7 +98,7 @@
     @Override
     @SuppressWarnings("unchecked")
     public List<ChangeInfo> apply(AccountResource rsrc)
-        throws BadRequestException, AuthException, OrmException, PermissionBackendException {
+        throws BadRequestException, AuthException, PermissionBackendException {
       if (!self.get().hasSameAccountId(rsrc.getUser())) {
         throw new AuthException("not allowed to list stars of another account");
       }
@@ -121,7 +120,7 @@
     }
 
     @Override
-    public SortedSet<String> apply(AccountResource.Star rsrc) throws AuthException, OrmException {
+    public 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");
       }
@@ -142,7 +141,7 @@
 
     @Override
     public Collection<String> apply(AccountResource.Star rsrc, StarsInput in)
-        throws AuthException, BadRequestException, OrmException {
+        throws AuthException, BadRequestException {
       if (!self.get().hasSameAccountId(rsrc.getUser())) {
         throw new AuthException("not allowed to update stars of another account");
       }
diff --git a/java/com/google/gerrit/server/restapi/change/Abandon.java b/java/com/google/gerrit/server/restapi/change/Abandon.java
index ccce998..c3327e4 100644
--- a/java/com/google/gerrit/server/restapi/change/Abandon.java
+++ b/java/com/google/gerrit/server/restapi/change/Abandon.java
@@ -14,25 +14,21 @@
 
 package com.google.gerrit.server.restapi.change;
 
-import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.ListMultimap;
 import com.google.common.flogger.FluentLogger;
+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.api.changes.RecipientType;
 import com.google.gerrit.extensions.common.ChangeInfo;
 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.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.change.AbandonOp;
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.ChangeResource;
-import com.google.gerrit.server.change.NotifyUtil;
+import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -41,9 +37,7 @@
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -53,37 +47,34 @@
     implements UiAction<ChangeResource> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  private final Provider<ReviewDb> dbProvider;
   private final ChangeJson.Factory json;
   private final AbandonOp.Factory abandonOpFactory;
-  private final NotifyUtil notifyUtil;
+  private final NotifyResolver notifyResolver;
   private final PatchSetUtil patchSetUtil;
 
   @Inject
   Abandon(
-      Provider<ReviewDb> dbProvider,
       ChangeJson.Factory json,
       RetryHelper retryHelper,
       AbandonOp.Factory abandonOpFactory,
-      NotifyUtil notifyUtil,
+      NotifyResolver notifyResolver,
       PatchSetUtil patchSetUtil) {
     super(retryHelper);
-    this.dbProvider = dbProvider;
     this.json = json;
     this.abandonOpFactory = abandonOpFactory;
-    this.notifyUtil = notifyUtil;
+    this.notifyResolver = notifyResolver;
     this.patchSetUtil = patchSetUtil;
   }
 
   @Override
   protected ChangeInfo applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, AbandonInput input)
-      throws RestApiException, UpdateException, OrmException, PermissionBackendException,
-          IOException, ConfigInvalidException {
+      throws RestApiException, UpdateException, PermissionBackendException, IOException,
+          ConfigInvalidException {
     // Not allowed to abandon if the current patch set is locked.
     patchSetUtil.checkPatchSetNotLocked(rsrc.getNotes());
 
-    rsrc.permissions().database(dbProvider).check(ChangePermission.ABANDON);
+    rsrc.permissions().check(ChangePermission.ABANDON);
 
     NotifyHandling notify = input.notify == null ? defaultNotify(rsrc.getChange()) : input.notify;
     Change change =
@@ -92,8 +83,7 @@
             rsrc.getNotes(),
             rsrc.getUser(),
             input.message,
-            notify,
-            notifyUtil.resolveAccounts(input.notifyDetails));
+            notifyResolver.resolve(notify, input.notifyDetails));
     return json.noOptions().format(change);
   }
 
@@ -108,8 +98,7 @@
         notes,
         user,
         "",
-        defaultNotify(notes.getChange()),
-        ImmutableListMultimap.of());
+        NotifyResolver.Result.create(defaultNotify(notes.getChange())));
   }
 
   public Change abandon(
@@ -120,8 +109,7 @@
         notes,
         user,
         msgTxt,
-        defaultNotify(notes.getChange()),
-        ImmutableListMultimap.of());
+        NotifyResolver.Result.create(defaultNotify(notes.getChange())));
   }
 
   public Change abandon(
@@ -129,13 +117,12 @@
       ChangeNotes notes,
       CurrentUser user,
       String msgTxt,
-      NotifyHandling notifyHandling,
-      ListMultimap<RecipientType, Account.Id> accountsToNotify)
+      NotifyResolver.Result notify)
       throws RestApiException, UpdateException {
     AccountState accountState = user.isIdentifiedUser() ? user.asIdentifiedUser().state() : null;
-    AbandonOp op = abandonOpFactory.create(accountState, msgTxt, notifyHandling, accountsToNotify);
-    try (BatchUpdate u =
-        updateFactory.create(dbProvider.get(), notes.getProjectName(), user, TimeUtil.nowTs())) {
+    AbandonOp op = abandonOpFactory.create(accountState, msgTxt);
+    try (BatchUpdate u = updateFactory.create(notes.getProjectName(), user, TimeUtil.nowTs())) {
+      u.setNotify(notify);
       u.addOp(notes.getChangeId(), op).execute();
     }
     return op.getChange();
@@ -150,7 +137,7 @@
             .setVisible(false);
 
     Change change = rsrc.getChange();
-    if (!change.getStatus().isOpen()) {
+    if (!change.isNew()) {
       return description;
     }
 
@@ -158,7 +145,7 @@
       if (patchSetUtil.isPatchSetLocked(rsrc.getNotes())) {
         return description;
       }
-    } catch (OrmException | IOException e) {
+    } catch (StorageException | IOException e) {
       logger.atSevere().withCause(e).log(
           "Failed to check if the current patch set of change %s is locked", change.getId());
       return description;
diff --git a/java/com/google/gerrit/server/restapi/change/ApplyFix.java b/java/com/google/gerrit/server/restapi/change/ApplyFix.java
index e4940ec..085f9de 100644
--- a/java/com/google/gerrit/server/restapi/change/ApplyFix.java
+++ b/java/com/google/gerrit/server/restapi/change/ApplyFix.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.extensions.common.EditInfo;
 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;
@@ -34,7 +35,6 @@
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -67,7 +67,7 @@
 
   @Override
   public Response<EditInfo> apply(FixResource fixResource, Void nothing)
-      throws AuthException, OrmException, ResourceConflictException, IOException,
+      throws AuthException, BadRequestException, ResourceConflictException, IOException,
           ResourceNotFoundException, PermissionBackendException {
     RevisionResource revisionResource = fixResource.getRevisionResource();
     Project.NameKey project = revisionResource.getProject();
diff --git a/java/com/google/gerrit/server/restapi/change/ChangeEdits.java b/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
index cabb30d..d4cc034 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
@@ -54,7 +54,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -95,7 +94,7 @@
 
   @Override
   public ChangeEditResource parse(ChangeResource rsrc, IdString id)
-      throws ResourceNotFoundException, AuthException, IOException, OrmException {
+      throws ResourceNotFoundException, AuthException, IOException {
     Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getNotes(), rsrc.getUser());
     if (!edit.isPresent()) {
       throw new ResourceNotFoundException(id);
@@ -119,7 +118,7 @@
 
     @Override
     public Response<?> apply(ChangeResource resource, IdString id, Put.Input input)
-        throws AuthException, ResourceConflictException, IOException, OrmException,
+        throws AuthException, ResourceConflictException, BadRequestException, IOException,
             PermissionBackendException {
       putEdit.apply(resource, id.get(), input.content);
       return Response.none();
@@ -137,7 +136,7 @@
 
     @Override
     public Response<?> apply(ChangeResource rsrc, IdString id, Input in)
-        throws IOException, AuthException, ResourceConflictException, OrmException,
+        throws IOException, AuthException, BadRequestException, ResourceConflictException,
             PermissionBackendException {
       return deleteContent.apply(rsrc, id.get());
     }
@@ -184,8 +183,7 @@
 
     @Override
     public Response<EditInfo> apply(ChangeResource rsrc)
-        throws AuthException, IOException, ResourceNotFoundException, OrmException,
-            PermissionBackendException {
+        throws AuthException, IOException, ResourceNotFoundException, PermissionBackendException {
       Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getNotes(), rsrc.getUser());
       if (!edit.isPresent()) {
         return Response.none();
@@ -240,7 +238,7 @@
 
     @Override
     public Response<?> apply(ChangeResource resource, Post.Input input)
-        throws AuthException, IOException, ResourceConflictException, OrmException,
+        throws AuthException, BadRequestException, IOException, ResourceConflictException,
             PermissionBackendException {
       Project.NameKey project = resource.getProject();
       try (Repository repository = repositoryManager.openRepository(project)) {
@@ -286,13 +284,13 @@
 
     @Override
     public Response<?> apply(ChangeEditResource rsrc, Input input)
-        throws AuthException, ResourceConflictException, IOException, OrmException,
+        throws AuthException, ResourceConflictException, BadRequestException, IOException,
             PermissionBackendException {
       return apply(rsrc.getChangeResource(), rsrc.getPath(), input.content);
     }
 
     public Response<?> apply(ChangeResource rsrc, String path, RawInput newContent)
-        throws ResourceConflictException, AuthException, IOException, OrmException,
+        throws ResourceConflictException, AuthException, BadRequestException, IOException,
             PermissionBackendException {
       if (Strings.isNullOrEmpty(path) || path.charAt(0) == '/') {
         throw new ResourceConflictException("Invalid path: " + path);
@@ -327,13 +325,13 @@
 
     @Override
     public Response<?> apply(ChangeEditResource rsrc, Input input)
-        throws AuthException, ResourceConflictException, OrmException, IOException,
+        throws AuthException, BadRequestException, ResourceConflictException, IOException,
             PermissionBackendException {
       return apply(rsrc.getChangeResource(), rsrc.getPath());
     }
 
     public Response<?> apply(ChangeResource rsrc, String filePath)
-        throws AuthException, IOException, OrmException, ResourceConflictException,
+        throws AuthException, BadRequestException, IOException, ResourceConflictException,
             PermissionBackendException {
       try (Repository repository = repositoryManager.openRepository(rsrc.getProject())) {
         editModifier.deleteFile(repository, rsrc.getNotes(), filePath);
@@ -429,7 +427,7 @@
     @Override
     public Object apply(ChangeResource rsrc, Input input)
         throws AuthException, IOException, BadRequestException, ResourceConflictException,
-            OrmException, PermissionBackendException {
+            PermissionBackendException {
       if (input == null || Strings.isNullOrEmpty(input.message)) {
         throw new BadRequestException("commit message must be provided");
       }
@@ -463,7 +461,7 @@
 
     @Override
     public BinaryResult apply(ChangeResource rsrc)
-        throws AuthException, IOException, ResourceNotFoundException, OrmException {
+        throws AuthException, IOException, ResourceNotFoundException {
       Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getNotes(), rsrc.getUser());
       String msg;
       if (edit.isPresent()) {
diff --git a/java/com/google/gerrit/server/restapi/change/ChangeIncludedIn.java b/java/com/google/gerrit/server/restapi/change/ChangeIncludedIn.java
index 12b3797..6ec4fdb 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangeIncludedIn.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangeIncludedIn.java
@@ -18,33 +18,27 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.IncludedIn;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 
 @Singleton
 public class ChangeIncludedIn implements RestReadView<ChangeResource> {
-  private Provider<ReviewDb> db;
   private PatchSetUtil psUtil;
   private IncludedIn includedIn;
 
   @Inject
-  ChangeIncludedIn(Provider<ReviewDb> db, PatchSetUtil psUtil, IncludedIn includedIn) {
-    this.db = db;
+  ChangeIncludedIn(PatchSetUtil psUtil, IncludedIn includedIn) {
     this.psUtil = psUtil;
     this.includedIn = includedIn;
   }
 
   @Override
-  public IncludedInInfo apply(ChangeResource rsrc)
-      throws RestApiException, OrmException, IOException {
-    PatchSet ps = psUtil.current(db.get(), rsrc.getNotes());
+  public IncludedInInfo apply(ChangeResource rsrc) throws RestApiException, IOException {
+    PatchSet ps = psUtil.current(rsrc.getNotes());
     return includedIn.apply(rsrc.getProject(), ps.getRevision().get());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/ChangeMessages.java b/java/com/google/gerrit/server/restapi/change/ChangeMessages.java
index 25fc350..96c517f 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangeMessages.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangeMessages.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.server.change.ChangeMessageResource;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.List;
@@ -52,7 +51,7 @@
 
   @Override
   public ChangeMessageResource parse(ChangeResource parent, IdString id)
-      throws OrmException, ResourceNotFoundException, PermissionBackendException {
+      throws ResourceNotFoundException, PermissionBackendException {
     String uuid = id.get();
 
     List<ChangeMessageInfo> changeMessages = listChangeMessages.apply(parent);
diff --git a/java/com/google/gerrit/server/restapi/change/ChangesCollection.java b/java/com/google/gerrit/server/restapi/change/ChangesCollection.java
index 561c27c..9f2a52c 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangesCollection.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangesCollection.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AuthException;
 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.RestApiException;
 import com.google.gerrit.extensions.restapi.RestCollection;
@@ -24,7 +25,6 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.ChangeFinder;
 import com.google.gerrit.server.change.ChangeResource;
@@ -34,7 +34,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -43,7 +42,6 @@
 
 @Singleton
 public class ChangesCollection implements RestCollection<TopLevelResource, ChangeResource> {
-  private final Provider<ReviewDb> db;
   private final Provider<CurrentUser> user;
   private final Provider<QueryChanges> queryFactory;
   private final DynamicMap<RestView<ChangeResource>> views;
@@ -54,7 +52,6 @@
 
   @Inject
   public ChangesCollection(
-      Provider<ReviewDb> db,
       Provider<CurrentUser> user,
       Provider<QueryChanges> queryFactory,
       DynamicMap<RestView<ChangeResource>> views,
@@ -62,7 +59,6 @@
       ChangeResource.Factory changeResourceFactory,
       PermissionBackend permissionBackend,
       ProjectCache projectCache) {
-    this.db = db;
     this.user = user;
     this.queryFactory = queryFactory;
     this.views = views;
@@ -84,7 +80,7 @@
 
   @Override
   public ChangeResource parse(TopLevelResource root, IdString id)
-      throws RestApiException, OrmException, PermissionBackendException, IOException {
+      throws RestApiException, PermissionBackendException, IOException {
     List<ChangeNotes> notes = changeFinder.find(id.encoded(), true);
     if (notes.isEmpty()) {
       throw new ResourceNotFoundException(id);
@@ -101,7 +97,8 @@
   }
 
   public ChangeResource parse(Change.Id id)
-      throws RestApiException, OrmException, PermissionBackendException, IOException {
+      throws ResourceConflictException, ResourceNotFoundException, PermissionBackendException,
+          IOException {
     List<ChangeNotes> notes = changeFinder.find(id);
     if (notes.isEmpty()) {
       throw new ResourceNotFoundException(toIdString(id));
@@ -127,7 +124,7 @@
 
   private boolean canRead(ChangeNotes notes) throws PermissionBackendException, IOException {
     try {
-      permissionBackend.currentUser().change(notes).database(db).check(ChangePermission.READ);
+      permissionBackend.currentUser().change(notes).check(ChangePermission.READ);
     } catch (AuthException e) {
       return false;
     }
@@ -139,7 +136,7 @@
   }
 
   private void checkProjectStatePermitsRead(Project.NameKey project)
-      throws IOException, RestApiException {
+      throws IOException, ResourceNotFoundException, ResourceConflictException {
     ProjectState projectState = projectCache.checkedGet(project);
     if (projectState == null) {
       throw new ResourceNotFoundException("project not found: " + project.get());
diff --git a/java/com/google/gerrit/server/restapi/change/Check.java b/java/com/google/gerrit/server/restapi/change/Check.java
index dd0ce10..f62aa5a 100644
--- a/java/com/google/gerrit/server/restapi/change/Check.java
+++ b/java/com/google/gerrit/server/restapi/change/Check.java
@@ -29,7 +29,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -47,14 +46,13 @@
   }
 
   @Override
-  public Response<ChangeInfo> apply(ChangeResource rsrc) throws RestApiException, OrmException {
+  public Response<ChangeInfo> apply(ChangeResource rsrc) throws RestApiException {
     return Response.withMustRevalidate(newChangeJson().format(rsrc));
   }
 
   @Override
   public Response<ChangeInfo> apply(ChangeResource rsrc, FixInput input)
-      throws RestApiException, OrmException, PermissionBackendException, NoSuchProjectException,
-          IOException {
+      throws RestApiException, PermissionBackendException, NoSuchProjectException, IOException {
     PermissionBackend.WithUser perm = permissionBackend.currentUser();
     if (!rsrc.isUserOwner()) {
       try {
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPick.java b/java/com/google/gerrit/server/restapi/change/CherryPick.java
index b68122e..72781c3 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPick.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPick.java
@@ -41,7 +41,6 @@
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -78,8 +77,8 @@
   @Override
   public CherryPickChangeInfo applyImpl(
       BatchUpdate.Factory updateFactory, RevisionResource rsrc, CherryPickInput input)
-      throws OrmException, IOException, UpdateException, RestApiException,
-          PermissionBackendException, ConfigInvalidException, NoSuchProjectException {
+      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");
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
index 1104761..c1fd0ba 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
@@ -14,12 +14,15 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.common.base.MoreObjects.firstNonNull;
+
 import com.google.auto.value.AutoValue;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MergeConflictException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -28,18 +31,15 @@
 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.Change.Status;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.ReviewerSet;
-import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.change.ChangeInserter;
-import com.google.gerrit.server.change.NotifyUtil;
+import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.change.PatchSetInserter;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
@@ -47,6 +47,7 @@
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
+import com.google.gerrit.server.notedb.Sequences;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
@@ -58,7 +59,6 @@
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -70,6 +70,7 @@
 import java.util.TimeZone;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.InvalidObjectIdException;
+import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.ObjectReader;
@@ -93,7 +94,6 @@
     abstract ImmutableSet<String> filesWithGitConflicts();
   }
 
-  private final Provider<ReviewDb> dbProvider;
   private final Sequences seq;
   private final Provider<InternalChangeQuery> queryProvider;
   private final GitRepositoryManager gitManager;
@@ -105,11 +105,10 @@
   private final ChangeNotes.Factory changeNotesFactory;
   private final ProjectCache projectCache;
   private final ApprovalsUtil approvalsUtil;
-  private final NotifyUtil notifyUtil;
+  private final NotifyResolver notifyResolver;
 
   @Inject
   CherryPickChange(
-      Provider<ReviewDb> dbProvider,
       Sequences seq,
       Provider<InternalChangeQuery> queryProvider,
       @GerritPersonIdent PersonIdent myIdent,
@@ -121,8 +120,7 @@
       ChangeNotes.Factory changeNotesFactory,
       ProjectCache projectCache,
       ApprovalsUtil approvalsUtil,
-      NotifyUtil notifyUtil) {
-    this.dbProvider = dbProvider;
+      NotifyResolver notifyResolver) {
     this.seq = seq;
     this.queryProvider = queryProvider;
     this.gitManager = gitManager;
@@ -134,7 +132,7 @@
     this.changeNotesFactory = changeNotesFactory;
     this.projectCache = projectCache;
     this.approvalsUtil = approvalsUtil;
-    this.notifyUtil = notifyUtil;
+    this.notifyResolver = notifyResolver;
   }
 
   public Result cherryPick(
@@ -143,8 +141,8 @@
       PatchSet patch,
       CherryPickInput input,
       Branch.NameKey dest)
-      throws OrmException, IOException, InvalidChangeOperationException, IntegrationException,
-          UpdateException, RestApiException, ConfigInvalidException, NoSuchProjectException {
+      throws IOException, InvalidChangeOperationException, IntegrationException, UpdateException,
+          RestApiException, ConfigInvalidException, NoSuchProjectException {
     return cherryPick(
         batchUpdateFactory,
         change,
@@ -161,8 +159,8 @@
       ObjectId sourceCommit,
       CherryPickInput input,
       Branch.NameKey dest)
-      throws OrmException, IOException, InvalidChangeOperationException, IntegrationException,
-          UpdateException, RestApiException, ConfigInvalidException, NoSuchProjectException {
+      throws IOException, InvalidChangeOperationException, IntegrationException, UpdateException,
+          RestApiException, ConfigInvalidException, NoSuchProjectException {
 
     IdentifiedUser identifiedUser = user.get();
     try (Repository git = gitManager.openRepository(project);
@@ -248,14 +246,14 @@
                   + " reside on the same branch. "
                   + "Cannot create a new patch set.");
         }
-        try (BatchUpdate bu =
-            batchUpdateFactory.create(dbProvider.get(), project, identifiedUser, now)) {
+        try (BatchUpdate bu = batchUpdateFactory.create(project, identifiedUser, now)) {
           bu.setRepository(git, revWalk, oi);
+          bu.setNotify(resolveNotify(input));
           Change.Id changeId;
           if (destChanges.size() == 1) {
             // The change key exists on the destination branch. The cherry pick
             // will be added as a new patch set.
-            changeId = insertPatchSet(bu, git, destChanges.get(0).notes(), cherryPickCommit, input);
+            changeId = insertPatchSet(bu, git, destChanges.get(0).notes(), cherryPickCommit);
           } else {
             // Change key not found on destination branch. We can create a new
             // change.
@@ -277,7 +275,7 @@
   }
 
   private RevCommit getBaseCommit(Ref destRef, String project, RevWalk revWalk, String base)
-      throws RestApiException, IOException, OrmException {
+      throws RestApiException, IOException {
     RevCommit destRefTip = revWalk.parseCommit(destRef.getObjectId());
     // The tip commit of the destination ref is the default base for the newly created change.
     if (Strings.isNullOrEmpty(base)) {
@@ -292,7 +290,14 @@
           String.format("Base %s doesn't represent a valid SHA-1", base), e);
     }
 
-    RevCommit baseCommit = revWalk.parseCommit(baseObjectId);
+    RevCommit baseCommit;
+    try {
+      baseCommit = revWalk.parseCommit(baseObjectId);
+    } catch (MissingObjectException e) {
+      throw new UnprocessableEntityException(
+          String.format("Base %s doesn't exist", baseObjectId.name()), e);
+    }
+
     InternalChangeQuery changeQuery = queryProvider.get();
     changeQuery.enforceVisibility(true);
     List<ChangeData> changeDatas = changeQuery.byBranchCommit(project, destRef.getName(), base);
@@ -309,31 +314,24 @@
     }
 
     Change change = changeDatas.get(0).change();
-    Change.Status status = change.getStatus();
-    if (status == Status.NEW || status == Status.MERGED) {
+    if (!change.isAbandoned()) {
       // The base commit is a valid change revision.
       return baseCommit;
     }
 
     throw new ResourceConflictException(
         String.format(
-            "Change %s with commit %s is %s", change.getChangeId(), base, status.asChangeStatus()));
+            "Change %s with commit %s is %s",
+            change.getChangeId(), base, ChangeUtil.status(change)));
   }
 
   private Change.Id insertPatchSet(
-      BatchUpdate bu,
-      Repository git,
-      ChangeNotes destNotes,
-      CodeReviewCommit cherryPickCommit,
-      CherryPickInput input)
-      throws IOException, OrmException, BadRequestException, ConfigInvalidException {
+      BatchUpdate bu, Repository git, ChangeNotes destNotes, CodeReviewCommit cherryPickCommit)
+      throws IOException {
     Change destChange = destNotes.getChange();
     PatchSet.Id psId = ChangeUtil.nextPatchSetId(git, destChange.currentPatchSetId());
     PatchSetInserter inserter = patchSetInserterFactory.create(destNotes, psId, cherryPickCommit);
-    inserter
-        .setMessage("Uploaded patch set " + inserter.getPatchSetId().get() + ".")
-        .setNotify(input.notify)
-        .setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
+    inserter.setMessage("Uploaded patch set " + inserter.getPatchSetId().get() + ".");
     bu.addOp(destChange.getId(), inserter);
     return destChange.getId();
   }
@@ -346,7 +344,7 @@
       @Nullable Change sourceChange,
       ObjectId sourceCommit,
       CherryPickInput input)
-      throws OrmException, IOException, BadRequestException, ConfigInvalidException {
+      throws IOException {
     Change.Id changeId = new Change.Id(seq.nextChangeId());
     ChangeInserter ins = changeInserterFactory.create(changeId, cherryPickCommit, refName);
     Branch.NameKey sourceBranch = sourceChange == null ? null : sourceChange.getDest();
@@ -356,13 +354,10 @@
         .setTopic(topic)
         .setWorkInProgress(
             (sourceChange != null && sourceChange.isWorkInProgress())
-                || !cherryPickCommit.getFilesWithGitConflicts().isEmpty())
-        .setNotify(input.notify)
-        .setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
+                || !cherryPickCommit.getFilesWithGitConflicts().isEmpty());
     if (input.keepReviewers && sourceChange != null) {
       ReviewerSet reviewerSet =
-          approvalsUtil.getReviewers(
-              dbProvider.get(), changeNotesFactory.createChecked(dbProvider.get(), sourceChange));
+          approvalsUtil.getReviewers(changeNotesFactory.createChecked(sourceChange));
       Set<Account.Id> reviewers =
           new HashSet<>(reviewerSet.byState(ReviewerStateInternal.REVIEWER));
       reviewers.add(sourceChange.getOwner());
@@ -375,6 +370,12 @@
     return changeId;
   }
 
+  private NotifyResolver.Result resolveNotify(CherryPickInput input)
+      throws BadRequestException, ConfigInvalidException, IOException {
+    return notifyResolver.resolve(
+        firstNonNull(input.notify, NotifyHandling.ALL), input.notifyDetails);
+  }
+
   private String messageForDestinationChange(
       PatchSet.Id patchSetId,
       Branch.NameKey sourceBranch,
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java b/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java
index f76689c..f34f178 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java
@@ -37,7 +37,6 @@
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -73,8 +72,8 @@
   @Override
   public CherryPickChangeInfo applyImpl(
       BatchUpdate.Factory updateFactory, CommitResource rsrc, CherryPickInput input)
-      throws OrmException, IOException, UpdateException, RestApiException,
-          PermissionBackendException, ConfigInvalidException, NoSuchProjectException {
+      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;
diff --git a/java/com/google/gerrit/server/restapi/change/Comments.java b/java/com/google/gerrit/server/restapi/change/Comments.java
index f563cc6..d9a0d57 100644
--- a/java/com/google/gerrit/server/restapi/change/Comments.java
+++ b/java/com/google/gerrit/server/restapi/change/Comments.java
@@ -20,32 +20,26 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.change.CommentResource;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 @Singleton
 public class Comments implements ChildCollection<RevisionResource, CommentResource> {
   private final DynamicMap<RestView<CommentResource>> views;
   private final ListRevisionComments list;
-  private final Provider<ReviewDb> dbProvider;
   private final CommentsUtil commentsUtil;
 
   @Inject
   Comments(
       DynamicMap<RestView<CommentResource>> views,
       ListRevisionComments list,
-      Provider<ReviewDb> dbProvider,
       CommentsUtil commentsUtil) {
     this.views = views;
     this.list = list;
-    this.dbProvider = dbProvider;
     this.commentsUtil = commentsUtil;
   }
 
@@ -60,13 +54,11 @@
   }
 
   @Override
-  public CommentResource parse(RevisionResource rev, IdString id)
-      throws ResourceNotFoundException, OrmException {
+  public CommentResource parse(RevisionResource rev, IdString id) throws ResourceNotFoundException {
     String uuid = id.get();
     ChangeNotes notes = rev.getNotes();
 
-    for (Comment c :
-        commentsUtil.publishedByPatchSet(dbProvider.get(), notes, rev.getPatchSet().getId())) {
+    for (Comment c : commentsUtil.publishedByPatchSet(notes, rev.getPatchSet().getId())) {
       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 565777e..0d1a001 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateChange.java
@@ -14,14 +14,15 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.common.base.MoreObjects.firstNonNull;
 import static org.eclipse.jgit.lib.Constants.SIGNED_OFF_BY_TAG;
 
 import com.google.common.base.Joiner;
-import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.client.ChangeStatus;
-import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeInput;
@@ -30,6 +31,7 @@
 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.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
@@ -39,23 +41,21 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
-import com.google.gerrit.server.Sequences;
-import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.change.ChangeFinder;
 import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.ChangeResource;
-import com.google.gerrit.server.change.NotifyUtil;
+import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.Sequences;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -71,18 +71,19 @@
 import com.google.gerrit.server.update.RetryingRestCollectionModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.io.UnsupportedEncodingException;
 import java.sql.Timestamp;
 import java.util.Collections;
 import java.util.List;
 import java.util.TimeZone;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.InvalidObjectIdException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.errors.NoMergeBaseException;
+import org.eclipse.jgit.errors.NoMergeBaseException.MergeBaseFailureReason;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
@@ -101,7 +102,6 @@
     extends RetryingRestCollectionModifyView<
         TopLevelResource, ChangeResource, ChangeInput, Response<ChangeInfo>> {
   private final String anonymousCowardName;
-  private final Provider<ReviewDb> db;
   private final GitRepositoryManager gitManager;
   private final Sequences seq;
   private final TimeZone serverTimeZone;
@@ -115,14 +115,13 @@
   private final PatchSetUtil psUtil;
   private final MergeUtil.Factory mergeUtilFactory;
   private final SubmitType submitType;
-  private final NotifyUtil notifyUtil;
+  private final NotifyResolver notifyResolver;
   private final ContributorAgreementsChecker contributorAgreements;
   private final boolean disablePrivateChanges;
 
   @Inject
   CreateChange(
       @AnonymousCowardName String anonymousCowardName,
-      Provider<ReviewDb> db,
       GitRepositoryManager gitManager,
       Sequences seq,
       @GerritPersonIdent PersonIdent myIdent,
@@ -137,11 +136,10 @@
       PatchSetUtil psUtil,
       @GerritServerConfig Config config,
       MergeUtil.Factory mergeUtilFactory,
-      NotifyUtil notifyUtil,
+      NotifyResolver notifyResolver,
       ContributorAgreementsChecker contributorAgreements) {
     super(retryHelper);
     this.anonymousCowardName = anonymousCowardName;
-    this.db = db;
     this.gitManager = gitManager;
     this.seq = seq;
     this.serverTimeZone = myIdent.getTimeZone();
@@ -156,15 +154,44 @@
     this.submitType = config.getEnum("project", null, "submitType", SubmitType.MERGE_IF_NECESSARY);
     this.disablePrivateChanges = config.getBoolean("change", null, "disablePrivateChanges", false);
     this.mergeUtilFactory = mergeUtilFactory;
-    this.notifyUtil = notifyUtil;
+    this.notifyResolver = notifyResolver;
     this.contributorAgreements = contributorAgreements;
   }
 
   @Override
   protected Response<ChangeInfo> applyImpl(
       BatchUpdate.Factory updateFactory, TopLevelResource parent, ChangeInput input)
-      throws OrmException, IOException, InvalidChangeOperationException, RestApiException,
-          UpdateException, PermissionBackendException, ConfigInvalidException {
+      throws IOException, InvalidChangeOperationException, RestApiException, UpdateException,
+          PermissionBackendException, ConfigInvalidException {
+    IdentifiedUser me = user.get().asIdentifiedUser();
+    checkAndSanitizeChangeInput(input, me);
+
+    ProjectResource projectResource = projectsCollection.parse(input.project);
+    ProjectState projectState = projectResource.getProjectState();
+    projectState.checkStatePermitsWrite();
+
+    Project.NameKey project = projectResource.getNameKey();
+    contributorAgreements.check(project, user.get());
+
+    checkRequiredPermissions(project, input.branch);
+
+    Change newChange = createNewChange(input, me, projectState, updateFactory);
+    ChangeJson json = jsonFactory.noOptions();
+    return Response.created(json.format(newChange));
+  }
+
+  /**
+   * Checks and sanitizes the user input, e.g. check whether the input is legal; clean the input so
+   * that it meets the requirement for creating a change; set a field based on the global configs,
+   * etc.
+   *
+   * @param input the {@code ChangeInput} from the request. Note this method modify the {@code
+   *     ChangeInput} object so that it can be reused directly by follow-up code.
+   * @param me the user who sent the current request to create a change.
+   * @throws BadRequestException if the input is not legal.
+   */
+  private void checkAndSanitizeChangeInput(ChangeInput input, IdentifiedUser me)
+      throws RestApiException, PermissionBackendException, IOException {
     if (Strings.isNullOrEmpty(input.project)) {
       throw new BadRequestException("project must be non-empty");
     }
@@ -172,165 +199,238 @@
     if (Strings.isNullOrEmpty(input.branch)) {
       throw new BadRequestException("branch must be non-empty");
     }
+    input.branch = RefNames.fullName(input.branch);
 
-    String subject = clean(Strings.nullToEmpty(input.subject));
-    if (Strings.isNullOrEmpty(subject)) {
+    String subject = Strings.nullToEmpty(input.subject);
+    subject = subject.replaceAll("(?m)^#.*$\n?", "").trim();
+    if (subject.isEmpty()) {
       throw new BadRequestException("commit message must be non-empty");
     }
+    input.subject = subject;
 
-    if (input.status != null) {
-      if (input.status != ChangeStatus.NEW) {
-        throw new BadRequestException("unsupported change status");
-      }
+    if (input.topic != null) {
+      input.topic = Strings.emptyToNull(input.topic.trim());
+    }
+
+    if (input.status != null && input.status != ChangeStatus.NEW) {
+      throw new BadRequestException("unsupported change status");
     }
 
     if (input.baseChange != null && input.baseCommit != null) {
       throw new BadRequestException("only provide one of base_change or base_commit");
     }
 
-    ProjectResource rsrc = projectsCollection.parse(input.project);
-    boolean privateByDefault = rsrc.getProjectState().is(BooleanProjectConfig.PRIVATE_BY_DEFAULT);
+    ProjectResource projectResource = projectsCollection.parse(input.project);
+    // Checks whether the change to be created should be a private change.
+    boolean privateByDefault =
+        projectResource.getProjectState().is(BooleanProjectConfig.PRIVATE_BY_DEFAULT);
     boolean isPrivate = input.isPrivate == null ? privateByDefault : input.isPrivate;
-
     if (isPrivate && disablePrivateChanges) {
       throw new MethodNotAllowedException("private changes are disabled");
     }
+    input.isPrivate = isPrivate;
 
-    contributorAgreements.check(rsrc.getNameKey(), rsrc.getUser());
+    ProjectState projectState = projectResource.getProjectState();
 
-    Project.NameKey project = rsrc.getNameKey();
-    String refName = RefNames.fullName(input.branch);
+    if (input.workInProgress == null) {
+      if (projectState.is(BooleanProjectConfig.WORK_IN_PROGRESS_BY_DEFAULT)) {
+        input.workInProgress = true;
+      } else {
+        input.workInProgress =
+            firstNonNull(me.state().getGeneralPreferences().workInProgressByDefault, false);
+      }
+    }
+
+    if (input.merge != null) {
+      if (!(submitType.equals(SubmitType.MERGE_ALWAYS)
+          || submitType.equals(SubmitType.MERGE_IF_NECESSARY))) {
+        throw new BadRequestException("Submit type: " + submitType + " is not supported");
+      }
+    }
+  }
+
+  private void checkRequiredPermissions(Project.NameKey project, String refName)
+      throws ResourceNotFoundException, AuthException, PermissionBackendException {
+    try {
+      permissionBackend.currentUser().project(project).ref(refName).check(RefPermission.READ);
+    } catch (AuthException e) {
+      throw new ResourceNotFoundException(String.format("ref %s not found", refName), e);
+    }
+
     permissionBackend
         .currentUser()
         .project(project)
         .ref(refName)
         .check(RefPermission.CREATE_CHANGE);
-    rsrc.getProjectState().checkStatePermitsWrite();
+  }
 
-    try (Repository git = gitManager.openRepository(project);
+  private Change createNewChange(
+      ChangeInput input,
+      IdentifiedUser me,
+      ProjectState projectState,
+      BatchUpdate.Factory updateFactory)
+      throws RestApiException, PermissionBackendException, IOException, ConfigInvalidException,
+          UpdateException {
+    try (Repository git = gitManager.openRepository(projectState.getNameKey());
         ObjectInserter oi = git.newObjectInserter();
         ObjectReader reader = oi.newReader();
         RevWalk rw = new RevWalk(reader)) {
-      ObjectId parentCommit;
-      List<String> groups;
-      Ref destRef = git.getRefDatabase().exactRef(refName);
+      PatchSet basePatchSet = null;
+      List<String> groups = Collections.emptyList();
       if (input.baseChange != null) {
-        List<ChangeNotes> notes = changeFinder.find(input.baseChange);
-        if (notes.size() != 1) {
-          throw new UnprocessableEntityException("Base change not found: " + input.baseChange);
-        }
-        ChangeNotes change = Iterables.getOnlyElement(notes);
-        try {
-          permissionBackend.currentUser().change(change).database(db).check(ChangePermission.READ);
-        } catch (AuthException e) {
-          throw new UnprocessableEntityException("Read not permitted for " + input.baseChange, e);
-        }
-        PatchSet ps = psUtil.current(db.get(), change);
-        parentCommit = ObjectId.fromString(ps.getRevision().get());
-        groups = ps.getGroups();
-      } else if (input.baseCommit != null) {
-        try {
-          parentCommit = ObjectId.fromString(input.baseCommit);
-        } catch (InvalidObjectIdException e) {
-          throw new UnprocessableEntityException(
-              String.format("Base %s doesn't represent a valid SHA-1", input.baseCommit), e);
-        }
-        RevCommit parentRevCommit = rw.parseCommit(parentCommit);
-        RevCommit destRefRevCommit = rw.parseCommit(destRef.getObjectId());
-        if (!rw.isMergedInto(parentRevCommit, destRefRevCommit)) {
-          throw new BadRequestException(
-              String.format("Commit %s doesn't exist on ref %s", input.baseCommit, refName));
-        }
-        groups = Collections.emptyList();
-      } else {
-        if (destRef != null) {
-          if (Boolean.TRUE.equals(input.newBranch)) {
-            throw new ResourceConflictException(
-                String.format("Branch %s already exists.", refName));
-          }
-          parentCommit = destRef.getObjectId();
-        } else {
-          if (Boolean.TRUE.equals(input.newBranch)) {
-            parentCommit = null;
-          } else {
-            throw new BadRequestException("Must provide a destination branch");
-          }
-        }
-        groups = Collections.emptyList();
+        ChangeNotes baseChange = getBaseChange(input.baseChange);
+        basePatchSet = psUtil.current(baseChange);
+        groups = basePatchSet.getGroups();
       }
+      ObjectId parentCommit =
+          getParentCommit(
+              git, rw, input.branch, input.newBranch, basePatchSet, input.baseCommit, input.merge);
+
       RevCommit mergeTip = parentCommit == null ? null : rw.parseCommit(parentCommit);
 
       Timestamp now = TimeUtil.nowTs();
-      IdentifiedUser me = user.get().asIdentifiedUser();
       PersonIdent author = me.newCommitterIdent(now, serverTimeZone);
-      AccountState accountState = me.state();
-      GeneralPreferencesInfo info = accountState.getGeneralPreferences();
-
-      boolean isWorkInProgress =
-          input.workInProgress == null
-              ? rsrc.getProjectState().is(BooleanProjectConfig.WORK_IN_PROGRESS_BY_DEFAULT)
-                  || MoreObjects.firstNonNull(info.workInProgressByDefault, false)
-              : input.workInProgress;
-
-      // Add a Change-Id line if there isn't already one
-      String commitMessage = subject;
-      if (ChangeIdUtil.indexOfChangeId(commitMessage, "\n") == -1) {
-        ObjectId treeId = mergeTip == null ? emptyTreeId(oi) : mergeTip.getTree();
-        ObjectId id = ChangeIdUtil.computeChangeId(treeId, mergeTip, author, author, commitMessage);
-        commitMessage = ChangeIdUtil.insertId(commitMessage, id);
-      }
-
-      if (Boolean.TRUE.equals(info.signedOffBy)) {
-        commitMessage =
-            Joiner.on("\n")
-                .join(
-                    commitMessage.trim(),
-                    String.format(
-                        "%s%s",
-                        SIGNED_OFF_BY_TAG,
-                        accountState.getAccount().getNameEmail(anonymousCowardName)));
-      }
+      String commitMessage = getCommitMessage(input.subject, me, oi, mergeTip, author);
 
       RevCommit c;
       if (input.merge != null) {
         // create a merge commit
-        if (!(submitType.equals(SubmitType.MERGE_ALWAYS)
-            || submitType.equals(SubmitType.MERGE_IF_NECESSARY))) {
-          throw new BadRequestException("Submit type: " + submitType + " is not supported");
-        }
-        c =
-            newMergeCommit(
-                git, oi, rw, rsrc.getProjectState(), mergeTip, input.merge, author, commitMessage);
+        c = newMergeCommit(git, oi, rw, projectState, mergeTip, input.merge, author, commitMessage);
       } else {
         // create an empty commit
         c = newCommit(oi, rw, author, mergeTip, commitMessage);
       }
 
       Change.Id changeId = new Change.Id(seq.nextChangeId());
-      ChangeInserter ins = changeInserterFactory.create(changeId, c, refName);
+      ChangeInserter ins = changeInserterFactory.create(changeId, c, input.branch);
       ins.setMessage(String.format("Uploaded patch set %s.", ins.getPatchSetId().get()));
-      String topic = input.topic;
-      if (topic != null) {
-        topic = Strings.emptyToNull(topic.trim());
-      }
-      ins.setTopic(topic);
-      ins.setPrivate(isPrivate);
-      ins.setWorkInProgress(isWorkInProgress);
+      ins.setTopic(input.topic);
+      ins.setPrivate(input.isPrivate);
+      ins.setWorkInProgress(input.workInProgress);
       ins.setGroups(groups);
-      ins.setNotify(input.notify);
-      ins.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
-      try (BatchUpdate bu = updateFactory.create(db.get(), project, me, now)) {
+      try (BatchUpdate bu = updateFactory.create(projectState.getNameKey(), me, now)) {
         bu.setRepository(git, rw, oi);
+        bu.setNotify(
+            notifyResolver.resolve(
+                firstNonNull(input.notify, NotifyHandling.ALL), input.notifyDetails));
         bu.insertChange(ins);
         bu.execute();
       }
-      ChangeJson json = jsonFactory.noOptions();
-      return Response.created(json.format(ins.getChange()));
+      return ins.getChange();
     } catch (IllegalArgumentException e) {
       throw new BadRequestException(e.getMessage());
     }
   }
 
+  private ChangeNotes getBaseChange(String baseChange)
+      throws UnprocessableEntityException, PermissionBackendException {
+    List<ChangeNotes> notes = changeFinder.find(baseChange);
+    if (notes.size() != 1) {
+      throw new UnprocessableEntityException("Base change not found: " + baseChange);
+    }
+    ChangeNotes change = Iterables.getOnlyElement(notes);
+    try {
+      permissionBackend.currentUser().change(change).check(ChangePermission.READ);
+    } catch (AuthException e) {
+      throw new UnprocessableEntityException("Read not permitted for " + baseChange, e);
+    }
+
+    return change;
+  }
+
+  @Nullable
+  private ObjectId getParentCommit(
+      Repository repo,
+      RevWalk revWalk,
+      String inputBranch,
+      @Nullable Boolean newBranch,
+      @Nullable PatchSet basePatchSet,
+      @Nullable String baseCommit,
+      @Nullable MergeInput mergeInput)
+      throws BadRequestException, IOException, UnprocessableEntityException,
+          ResourceConflictException {
+    if (basePatchSet != null) {
+      return ObjectId.fromString(basePatchSet.getRevision().get());
+    }
+
+    Ref destRef = repo.getRefDatabase().exactRef(inputBranch);
+    ObjectId parentCommit;
+    if (baseCommit != null) {
+      try {
+        parentCommit = ObjectId.fromString(baseCommit);
+      } catch (InvalidObjectIdException e) {
+        throw new UnprocessableEntityException(
+            String.format("Base %s doesn't represent a valid SHA-1", baseCommit), e);
+      }
+
+      RevCommit parentRevCommit;
+      try {
+        parentRevCommit = revWalk.parseCommit(parentCommit);
+      } catch (MissingObjectException e) {
+        throw new UnprocessableEntityException(
+            String.format("Base %s doesn't exist", baseCommit), e);
+      }
+
+      if (destRef == null) {
+        throw new BadRequestException("Destination branch does not exist");
+      }
+      RevCommit destRefRevCommit = revWalk.parseCommit(destRef.getObjectId());
+
+      if (!revWalk.isMergedInto(parentRevCommit, destRefRevCommit)) {
+        throw new BadRequestException(
+            String.format("Commit %s doesn't exist on ref %s", baseCommit, inputBranch));
+      }
+    } else {
+      if (destRef != null) {
+        if (Boolean.TRUE.equals(newBranch)) {
+          throw new ResourceConflictException(
+              String.format("Branch %s already exists.", inputBranch));
+        }
+        parentCommit = destRef.getObjectId();
+      } else {
+        if (Boolean.TRUE.equals(newBranch)) {
+          if (mergeInput != null) {
+            throw new BadRequestException("Cannot create merge: destination branch does not exist");
+          }
+          parentCommit = null;
+        } else {
+          throw new BadRequestException("Destination branch does not exist");
+        }
+      }
+    }
+
+    return parentCommit;
+  }
+
+  private String getCommitMessage(
+      String subject,
+      IdentifiedUser me,
+      ObjectInserter objectInserter,
+      RevCommit mergeTip,
+      PersonIdent author)
+      throws IOException {
+    // Add a Change-Id line if there isn't already one
+    String commitMessage = subject;
+    if (ChangeIdUtil.indexOfChangeId(commitMessage, "\n") == -1) {
+      ObjectId treeId = mergeTip == null ? emptyTreeId(objectInserter) : mergeTip.getTree();
+      ObjectId id = ChangeIdUtil.computeChangeId(treeId, mergeTip, author, author, commitMessage);
+      commitMessage = ChangeIdUtil.insertId(commitMessage, id);
+    }
+
+    if (Boolean.TRUE.equals(me.state().getGeneralPreferences().signedOffBy)) {
+      commitMessage =
+          Joiner.on("\n")
+              .join(
+                  commitMessage.trim(),
+                  String.format(
+                      "%s%s",
+                      SIGNED_OFF_BY_TAG,
+                      me.state().getAccount().getNameEmail(anonymousCowardName)));
+    }
+
+    return commitMessage;
+  }
+
   private static RevCommit newCommit(
       ObjectInserter oi,
       RevWalk rw,
@@ -373,22 +473,29 @@
     MergeUtil mergeUtil = mergeUtilFactory.create(projectState);
     // default merge strategy from project settings
     String mergeStrategy =
-        MoreObjects.firstNonNull(
-            Strings.emptyToNull(merge.strategy), mergeUtil.mergeStrategyName());
+        firstNonNull(Strings.emptyToNull(merge.strategy), mergeUtil.mergeStrategyName());
 
-    return MergeUtil.createMergeCommit(
-        oi,
-        repo.getConfig(),
-        mergeTip,
-        sourceCommit,
-        mergeStrategy,
-        authorIdent,
-        commitMessage,
-        rw);
+    try {
+      return MergeUtil.createMergeCommit(
+          oi,
+          repo.getConfig(),
+          mergeTip,
+          sourceCommit,
+          mergeStrategy,
+          authorIdent,
+          commitMessage,
+          rw);
+    } catch (NoMergeBaseException e) {
+      if (MergeBaseFailureReason.TOO_MANY_MERGE_BASES == e.getReason()
+          || MergeBaseFailureReason.CONFLICTS_DURING_MERGE_BASE_CALCULATION == e.getReason()) {
+        throw new ResourceConflictException(
+            String.format("Cannot create merge commit: %s", e.getMessage()), e);
+      }
+      throw e;
+    }
   }
 
-  private static ObjectId insert(ObjectInserter inserter, CommitBuilder commit)
-      throws IOException, UnsupportedEncodingException {
+  private static ObjectId insert(ObjectInserter inserter, CommitBuilder commit) throws IOException {
     ObjectId id = inserter.insert(commit);
     inserter.flush();
     return id;
@@ -397,16 +504,4 @@
   private static ObjectId emptyTreeId(ObjectInserter inserter) throws IOException {
     return inserter.insert(new TreeFormatter());
   }
-
-  /**
-   * Remove comment lines from a commit message.
-   *
-   * <p>Based on {@link org.eclipse.jgit.util.ChangeIdUtil#clean}.
-   *
-   * @param msg
-   * @return message without comment lines, possibly empty.
-   */
-  private String clean(String msg) {
-    return msg.replaceAll("(?m)^#.*$\n?", "").trim();
-  }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java b/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
index 0e93c55..b6e7628 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
@@ -28,7 +28,6 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.RevisionResource;
@@ -42,7 +41,6 @@
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -51,7 +49,6 @@
 @Singleton
 public class CreateDraftComment
     extends RetryingRestModifyView<RevisionResource, DraftInput, Response<CommentInfo>> {
-  private final Provider<ReviewDb> db;
   private final Provider<CommentJson> commentJson;
   private final CommentsUtil commentsUtil;
   private final PatchSetUtil psUtil;
@@ -59,14 +56,12 @@
 
   @Inject
   CreateDraftComment(
-      Provider<ReviewDb> db,
       RetryHelper retryHelper,
       Provider<CommentJson> commentJson,
       CommentsUtil commentsUtil,
       PatchSetUtil psUtil,
       PatchListCache patchListCache) {
     super(retryHelper);
-    this.db = db;
     this.commentJson = commentJson;
     this.commentsUtil = commentsUtil;
     this.psUtil = psUtil;
@@ -76,7 +71,7 @@
   @Override
   protected Response<CommentInfo> applyImpl(
       BatchUpdate.Factory updateFactory, RevisionResource rsrc, DraftInput in)
-      throws RestApiException, UpdateException, OrmException, PermissionBackendException {
+      throws RestApiException, UpdateException, PermissionBackendException {
     if (Strings.isNullOrEmpty(in.path)) {
       throw new BadRequestException("path must be non-empty");
     } else if (in.message == null || in.message.trim().isEmpty()) {
@@ -88,7 +83,7 @@
     }
 
     try (BatchUpdate bu =
-        updateFactory.create(db.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+        updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
       Op op = new Op(rsrc.getPatchSet().getId(), in);
       bu.addOp(rsrc.getChange().getId(), op);
       bu.execute();
@@ -110,9 +105,9 @@
 
     @Override
     public boolean updateChange(ChangeContext ctx)
-        throws ResourceNotFoundException, OrmException, UnprocessableEntityException,
+        throws ResourceNotFoundException, UnprocessableEntityException,
             PatchListNotAvailableException {
-      PatchSet ps = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
+      PatchSet ps = psUtil.get(ctx.getNotes(), psId);
       if (ps == null) {
         throw new ResourceNotFoundException("patch set not found: " + psId);
       }
@@ -126,9 +121,7 @@
 
       setCommentRevId(comment, patchListCache, ctx.getChange(), ps);
 
-      commentsUtil.putComments(
-          ctx.getDb(), ctx.getUpdate(psId), Status.DRAFT, Collections.singleton(comment));
-      ctx.dontBumpLastUpdatedOn();
+      commentsUtil.putComments(ctx.getUpdate(psId), 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 296ca3f..b7bcaf9 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
@@ -17,7 +17,6 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.MergeInput;
@@ -33,7 +32,6 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
@@ -42,6 +40,7 @@
 import com.google.gerrit.server.change.ChangeFinder;
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.change.PatchSetInserter;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeUtil;
@@ -58,7 +57,6 @@
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -79,7 +77,6 @@
 @Singleton
 public class CreateMergePatchSet
     extends RetryingRestModifyView<ChangeResource, MergePatchSetInput, Response<ChangeInfo>> {
-  private final Provider<ReviewDb> db;
   private final GitRepositoryManager gitManager;
   private final CommitsCollection commits;
   private final TimeZone serverTimeZone;
@@ -94,7 +91,6 @@
 
   @Inject
   CreateMergePatchSet(
-      Provider<ReviewDb> db,
       GitRepositoryManager gitManager,
       CommitsCollection commits,
       @GerritPersonIdent PersonIdent myIdent,
@@ -108,7 +104,6 @@
       ChangeFinder changeFinder,
       PermissionBackend permissionBackend) {
     super(retryHelper);
-    this.db = db;
     this.gitManager = gitManager;
     this.commits = commits;
     this.serverTimeZone = myIdent.getTimeZone();
@@ -125,12 +120,11 @@
   @Override
   protected Response<ChangeInfo> applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, MergePatchSetInput in)
-      throws OrmException, IOException, RestApiException, UpdateException,
-          PermissionBackendException {
+      throws IOException, RestApiException, UpdateException, PermissionBackendException {
     // Not allowed to create a new patch set if the current patch set is locked.
     psUtil.checkPatchSetNotLocked(rsrc.getNotes());
 
-    rsrc.permissions().database(db).check(ChangePermission.ADD_PATCH_SET);
+    rsrc.permissions().check(ChangePermission.ADD_PATCH_SET);
 
     ProjectState projectState = projectCache.checkedGet(rsrc.getProject());
     projectState.checkStatePermitsWrite();
@@ -141,7 +135,7 @@
     }
     in.baseChange = Strings.nullToEmpty(in.baseChange).trim();
 
-    PatchSet ps = psUtil.current(db.get(), rsrc.getNotes());
+    PatchSet ps = psUtil.current(rsrc.getNotes());
     Change change = rsrc.getChange();
     Project.NameKey project = change.getProject();
     Branch.NameKey dest = change.getDest();
@@ -185,13 +179,12 @@
       PatchSet.Id nextPsId = ChangeUtil.nextPatchSetId(ps.getId());
       PatchSetInserter psInserter =
           patchSetInserterFactory.create(rsrc.getNotes(), nextPsId, newCommit);
-      try (BatchUpdate bu = updateFactory.create(db.get(), project, me, now)) {
+      try (BatchUpdate bu = updateFactory.create(project, me, now)) {
         bu.setRepository(git, rw, oi);
+        bu.setNotify(NotifyResolver.Result.none());
         psInserter
             .setMessage("Uploaded patch set " + nextPsId.get() + ".")
-            .setNotify(NotifyHandling.NONE)
-            .setCheckAddPatchSetPermission(false)
-            .setNotify(NotifyHandling.NONE);
+            .setCheckAddPatchSetPermission(false);
         if (groups != null) {
           psInserter.setGroups(groups);
         }
@@ -205,18 +198,18 @@
   }
 
   private PatchSet findBasePatchSet(String baseChange)
-      throws PermissionBackendException, OrmException, UnprocessableEntityException {
+      throws PermissionBackendException, UnprocessableEntityException {
     List<ChangeNotes> notes = changeFinder.find(baseChange);
     if (notes.size() != 1) {
       throw new UnprocessableEntityException("Base change not found: " + baseChange);
     }
     ChangeNotes change = Iterables.getOnlyElement(notes);
     try {
-      permissionBackend.currentUser().change(change).database(db).check(ChangePermission.READ);
+      permissionBackend.currentUser().change(change).check(ChangePermission.READ);
     } catch (AuthException e) {
       throw new UnprocessableEntityException("Read not permitted for " + baseChange, e);
     }
-    return psUtil.current(db.get(), change);
+    return psUtil.current(change);
   }
 
   private RevCommit createMergeCommit(
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java b/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java
index 7d68022..02387be 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java
@@ -21,7 +21,6 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountLoader;
@@ -39,9 +38,7 @@
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 @Singleton
@@ -49,7 +46,6 @@
     extends RetryingRestModifyView<ChangeResource, Input, Response<AccountInfo>> {
 
   private final ChangeMessagesUtil cmUtil;
-  private final Provider<ReviewDb> db;
   private final AssigneeChanged assigneeChanged;
   private final IdentifiedUser.GenericFactory userFactory;
   private final AccountLoader.Factory accountLoaderFactory;
@@ -58,13 +54,11 @@
   DeleteAssignee(
       RetryHelper retryHelper,
       ChangeMessagesUtil cmUtil,
-      Provider<ReviewDb> db,
       AssigneeChanged assigneeChanged,
       IdentifiedUser.GenericFactory userFactory,
       AccountLoader.Factory accountLoaderFactory) {
     super(retryHelper);
     this.cmUtil = cmUtil;
-    this.db = db;
     this.assigneeChanged = assigneeChanged;
     this.userFactory = userFactory;
     this.accountLoaderFactory = accountLoaderFactory;
@@ -73,11 +67,11 @@
   @Override
   protected Response<AccountInfo> applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input)
-      throws RestApiException, UpdateException, OrmException, PermissionBackendException {
+      throws RestApiException, UpdateException, PermissionBackendException {
     rsrc.permissions().check(ChangePermission.EDIT_ASSIGNEE);
 
     try (BatchUpdate bu =
-        updateFactory.create(db.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+        updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
       Op op = new Op();
       bu.addOp(rsrc.getChange().getId(), op);
       bu.execute();
@@ -93,7 +87,7 @@
     private AccountState deletedAssignee;
 
     @Override
-    public boolean updateChange(ChangeContext ctx) throws RestApiException, OrmException {
+    public boolean updateChange(ChangeContext ctx) throws RestApiException {
       change = ctx.getChange();
       ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId());
       Account.Id currentAssigneeId = change.getAssignee();
@@ -114,18 +108,18 @@
       return deletedAssignee != null ? deletedAssignee.getAccount().getId() : null;
     }
 
-    private void addMessage(ChangeContext ctx, ChangeUpdate update, IdentifiedUser deletedAssignee)
-        throws OrmException {
+    private void addMessage(
+        ChangeContext ctx, ChangeUpdate update, IdentifiedUser deletedAssignee) {
       ChangeMessage cmsg =
           ChangeMessagesUtil.newMessage(
               ctx,
               "Assignee deleted: " + deletedAssignee.getNameEmail(),
               ChangeMessagesUtil.TAG_DELETE_ASSIGNEE);
-      cmUtil.addChangeMessage(ctx.getDb(), update, cmsg);
+      cmUtil.addChangeMessage(update, cmsg);
     }
 
     @Override
-    public void postUpdate(Context ctx) throws OrmException {
+    public void postUpdate(Context ctx) {
       assigneeChanged.fire(change, ctx.getAccount(), deletedAssignee, ctx.getWhen());
     }
   }
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteChange.java b/java/com/google/gerrit/server/restapi/change/DeleteChange.java
index fe2c516..3021d81 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteChange.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteChange.java
@@ -22,51 +22,44 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.DeleteChangeOp;
 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.update.BatchUpdate;
-import com.google.gerrit.server.update.Order;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 @Singleton
 public class DeleteChange extends RetryingRestModifyView<ChangeResource, Input, Response<?>>
     implements UiAction<ChangeResource> {
 
-  private final Provider<ReviewDb> db;
-  private final Provider<DeleteChangeOp> opProvider;
+  private final DeleteChangeOp.Factory opFactory;
 
   @Inject
-  public DeleteChange(
-      Provider<ReviewDb> db, RetryHelper retryHelper, Provider<DeleteChangeOp> opProvider) {
+  public DeleteChange(RetryHelper retryHelper, DeleteChangeOp.Factory opFactory) {
     super(retryHelper);
-    this.db = db;
-    this.opProvider = opProvider;
+    this.opFactory = opFactory;
   }
 
   @Override
   protected Response<?> applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input)
       throws RestApiException, UpdateException, PermissionBackendException {
-    if (!isChangeDeletable(rsrc.getChange().getStatus())) {
+    if (!isChangeDeletable(rsrc)) {
       throw new MethodNotAllowedException("delete not permitted");
     }
-    rsrc.permissions().database(db).check(ChangePermission.DELETE);
+    rsrc.permissions().check(ChangePermission.DELETE);
 
     try (BatchUpdate bu =
-        updateFactory.create(db.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+        updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
       Change.Id id = rsrc.getChange().getId();
-      bu.setOrder(Order.DB_BEFORE_REPO);
-      bu.addOp(id, opProvider.get());
+      bu.addOp(id, opFactory.create(id));
       bu.execute();
     }
     return Response.none();
@@ -74,16 +67,16 @@
 
   @Override
   public UiAction.Description getDescription(ChangeResource rsrc) {
-    Change.Status status = rsrc.getChange().getStatus();
-    PermissionBackend.ForChange perm = rsrc.permissions().database(db);
+    PermissionBackend.ForChange perm = rsrc.permissions();
     return new UiAction.Description()
         .setLabel("Delete")
         .setTitle("Delete change " + rsrc.getId())
-        .setVisible(and(isChangeDeletable(status), perm.testCond(ChangePermission.DELETE)));
+        .setVisible(and(isChangeDeletable(rsrc), perm.testCond(ChangePermission.DELETE)));
   }
 
-  private static boolean isChangeDeletable(Change.Status status) {
-    if (status == Change.Status.MERGED) {
+  private static boolean isChangeDeletable(ChangeResource rsrc) {
+    Change change = rsrc.getChange();
+    if (change.isMerged()) {
       // Merged changes should never be deleted.
       return false;
     }
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteChangeEdit.java b/java/com/google/gerrit/server/restapi/change/DeleteChangeEdit.java
index d49a804..f7f808a 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteChangeEdit.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteChangeEdit.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.edit.ChangeEdit;
 import com.google.gerrit.server.edit.ChangeEditUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -41,7 +40,7 @@
 
   @Override
   public Response<?> apply(ChangeResource rsrc, Input input)
-      throws AuthException, ResourceNotFoundException, IOException, OrmException {
+      throws AuthException, ResourceNotFoundException, IOException {
     Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getNotes(), rsrc.getUser());
     if (edit.isPresent()) {
       editUtil.delete(edit.get());
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java b/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java
index 881d106..0fb8e18 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java
@@ -28,7 +28,6 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountLoader;
@@ -44,7 +43,6 @@
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -58,7 +56,6 @@
         ChangeMessageResource, DeleteChangeMessageInput, Response<ChangeMessageInfo>> {
 
   private final Provider<CurrentUser> userProvider;
-  private final Provider<ReviewDb> dbProvider;
   private final PermissionBackend permissionBackend;
   private final ChangeMessagesUtil changeMessagesUtil;
   private final AccountLoader.Factory accountLoaderFactory;
@@ -67,7 +64,6 @@
   @Inject
   public DeleteChangeMessage(
       Provider<CurrentUser> userProvider,
-      Provider<ReviewDb> dbProvider,
       PermissionBackend permissionBackend,
       ChangeMessagesUtil changeMessagesUtil,
       AccountLoader.Factory accountLoaderFactory,
@@ -75,7 +71,6 @@
       RetryHelper retryHelper) {
     super(retryHelper);
     this.userProvider = userProvider;
-    this.dbProvider = dbProvider;
     this.permissionBackend = permissionBackend;
     this.changeMessagesUtil = changeMessagesUtil;
     this.accountLoaderFactory = accountLoaderFactory;
@@ -87,18 +82,16 @@
       BatchUpdate.Factory updateFactory,
       ChangeMessageResource resource,
       DeleteChangeMessageInput input)
-      throws RestApiException, PermissionBackendException, OrmException, UpdateException,
-          IOException {
+      throws RestApiException, PermissionBackendException, UpdateException, IOException {
     CurrentUser user = userProvider.get();
     permissionBackend.user(user).check(GlobalPermission.ADMINISTRATE_SERVER);
 
     String newChangeMessage =
         createNewChangeMessage(user.asIdentifiedUser().getName(), input.reason);
     DeleteChangeMessageOp deleteChangeMessageOp =
-        new DeleteChangeMessageOp(resource.getChangeMessageIndex(), newChangeMessage);
+        new DeleteChangeMessageOp(resource.getChangeMessageId(), newChangeMessage);
     try (BatchUpdate batchUpdate =
-        updateFactory.create(
-            dbProvider.get(), resource.getChangeResource().getProject(), user, TimeUtil.nowTs())) {
+        updateFactory.create(resource.getChangeResource().getProject(), user, TimeUtil.nowTs())) {
       batchUpdate.addOp(resource.getChangeId(), deleteChangeMessageOp).execute();
     }
 
@@ -108,9 +101,8 @@
   }
 
   private ChangeMessageInfo createUpdatedChangeMessageInfo(Change.Id id, int targetIdx)
-      throws OrmException, PermissionBackendException {
-    List<ChangeMessage> messages =
-        changeMessagesUtil.byChange(dbProvider.get(), notesFactory.createChecked(id));
+      throws PermissionBackendException {
+    List<ChangeMessage> messages = changeMessagesUtil.byChange(notesFactory.createChecked(id));
     ChangeMessage updatedChangeMessage = messages.get(targetIdx);
     AccountLoader accountLoader = accountLoaderFactory.create(true);
     ChangeMessageInfo info = createChangeMessageInfo(updatedChangeMessage, accountLoader);
@@ -136,19 +128,18 @@
   }
 
   private class DeleteChangeMessageOp implements BatchUpdateOp {
-    private final int targetMessageIdx;
+    private final String targetMessageId;
     private final String newMessage;
 
-    DeleteChangeMessageOp(int targetMessageIdx, String newMessage) {
-      this.targetMessageIdx = targetMessageIdx;
+    DeleteChangeMessageOp(String targetMessageIdx, String newMessage) {
+      this.targetMessageId = targetMessageIdx;
       this.newMessage = newMessage;
     }
 
     @Override
-    public boolean updateChange(ChangeContext ctx) throws OrmException {
+    public boolean updateChange(ChangeContext ctx) {
       PatchSet.Id psId = ctx.getChange().currentPatchSetId();
-      changeMessagesUtil.replaceChangeMessage(
-          ctx.getDb(), ctx.getUpdate(psId), targetMessageIdx, newMessage);
+      changeMessagesUtil.replaceChangeMessage(ctx.getUpdate(psId), targetMessageId, newMessage);
       return true;
     }
   }
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteComment.java b/java/com/google/gerrit/server/restapi/change/DeleteComment.java
index 2ddf359..30a8efd7 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteComment.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteComment.java
@@ -22,7 +22,6 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.CommentResource;
@@ -37,7 +36,6 @@
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -51,7 +49,6 @@
     extends RetryingRestModifyView<CommentResource, DeleteCommentInput, CommentInfo> {
 
   private final Provider<CurrentUser> userProvider;
-  private final Provider<ReviewDb> dbProvider;
   private final PermissionBackend permissionBackend;
   private final CommentsUtil commentsUtil;
   private final Provider<CommentJson> commentJson;
@@ -60,7 +57,6 @@
   @Inject
   public DeleteComment(
       Provider<CurrentUser> userProvider,
-      Provider<ReviewDb> dbProvider,
       PermissionBackend permissionBackend,
       RetryHelper retryHelper,
       CommentsUtil commentsUtil,
@@ -68,7 +64,6 @@
       ChangeNotes.Factory notesFactory) {
     super(retryHelper);
     this.userProvider = userProvider;
-    this.dbProvider = dbProvider;
     this.permissionBackend = permissionBackend;
     this.commentsUtil = commentsUtil;
     this.commentJson = commentJson;
@@ -78,8 +73,8 @@
   @Override
   public CommentInfo applyImpl(
       BatchUpdate.Factory batchUpdateFactory, CommentResource rsrc, DeleteCommentInput input)
-      throws RestApiException, IOException, ConfigInvalidException, OrmException,
-          PermissionBackendException, UpdateException {
+      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException,
+          UpdateException {
     CurrentUser user = userProvider.get();
     permissionBackend.user(user).check(GlobalPermission.ADMINISTRATE_SERVER);
 
@@ -91,13 +86,13 @@
     DeleteCommentOp deleteCommentOp = new DeleteCommentOp(rsrc, newMessage);
     try (BatchUpdate batchUpdate =
         batchUpdateFactory.create(
-            dbProvider.get(), rsrc.getRevisionResource().getProject(), user, TimeUtil.nowTs())) {
+            rsrc.getRevisionResource().getProject(), user, TimeUtil.nowTs())) {
       batchUpdate.addOp(rsrc.getRevisionResource().getChange().getId(), deleteCommentOp).execute();
     }
 
     ChangeNotes updatedNotes =
         notesFactory.createChecked(rsrc.getRevisionResource().getChange().getId());
-    List<Comment> changeComments = commentsUtil.publishedByChange(dbProvider.get(), updatedNotes);
+    List<Comment> changeComments = commentsUtil.publishedByChange(updatedNotes);
     Optional<Comment> updatedComment =
         changeComments.stream().filter(c -> c.key.equals(rsrc.getComment().key)).findFirst();
     if (!updatedComment.isPresent()) {
@@ -127,14 +122,10 @@
 
     @Override
     public boolean updateChange(ChangeContext ctx)
-        throws ResourceConflictException, OrmException, ResourceNotFoundException {
+        throws ResourceConflictException, ResourceNotFoundException {
       PatchSet.Id psId = ctx.getChange().currentPatchSetId();
       commentsUtil.deleteCommentByRewritingHistory(
-          ctx.getDb(),
-          ctx.getUpdate(psId),
-          rsrc.getComment().key,
-          rsrc.getPatchSet().getId(),
-          newMessage);
+          ctx.getUpdate(psId), rsrc.getComment().key, newMessage);
       return true;
     }
   }
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java b/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java
index f8e3add..de04d36 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java
@@ -23,7 +23,6 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.DraftCommentResource;
@@ -36,9 +35,7 @@
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.util.Collections;
 import java.util.Optional;
@@ -47,20 +44,17 @@
 public class DeleteDraftComment
     extends RetryingRestModifyView<DraftCommentResource, Input, Response<CommentInfo>> {
 
-  private final Provider<ReviewDb> db;
   private final CommentsUtil commentsUtil;
   private final PatchSetUtil psUtil;
   private final PatchListCache patchListCache;
 
   @Inject
   DeleteDraftComment(
-      Provider<ReviewDb> db,
       CommentsUtil commentsUtil,
       PatchSetUtil psUtil,
       RetryHelper retryHelper,
       PatchListCache patchListCache) {
     super(retryHelper);
-    this.db = db;
     this.commentsUtil = commentsUtil;
     this.psUtil = psUtil;
     this.patchListCache = patchListCache;
@@ -71,8 +65,7 @@
       BatchUpdate.Factory updateFactory, DraftCommentResource rsrc, Input input)
       throws RestApiException, UpdateException {
     try (BatchUpdate bu =
-        updateFactory.create(
-            db.get(), rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+        updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
       Op op = new Op(rsrc.getComment().key);
       bu.addOp(rsrc.getChange().getId(), op);
       bu.execute();
@@ -89,21 +82,20 @@
 
     @Override
     public boolean updateChange(ChangeContext ctx)
-        throws ResourceNotFoundException, OrmException, PatchListNotAvailableException {
+        throws ResourceNotFoundException, PatchListNotAvailableException {
       Optional<Comment> maybeComment =
-          commentsUtil.getDraft(ctx.getDb(), ctx.getNotes(), ctx.getIdentifiedUser(), key);
+          commentsUtil.getDraft(ctx.getNotes(), ctx.getIdentifiedUser(), key);
       if (!maybeComment.isPresent()) {
         return false; // Nothing to do.
       }
       PatchSet.Id psId = new PatchSet.Id(ctx.getChange().getId(), key.patchSetId);
-      PatchSet ps = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
+      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);
-      commentsUtil.deleteComments(ctx.getDb(), ctx.getUpdate(psId), Collections.singleton(c));
-      ctx.dontBumpLastUpdatedOn();
+      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 9ecd56e..8601e68 100644
--- a/java/com/google/gerrit/server/restapi/change/DeletePrivate.java
+++ b/java/com/google/gerrit/server/restapi/change/DeletePrivate.java
@@ -22,8 +22,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.server.ReviewDb;
-import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.SetPrivateOp;
 import com.google.gerrit.server.permissions.GlobalPermission;
@@ -34,27 +32,20 @@
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 @Singleton
 public class DeletePrivate
     extends RetryingRestModifyView<ChangeResource, SetPrivateOp.Input, Response<String>> {
-  private final ChangeMessagesUtil cmUtil;
-  private final Provider<ReviewDb> dbProvider;
   private final PermissionBackend permissionBackend;
   private final SetPrivateOp.Factory setPrivateOpFactory;
 
   @Inject
   DeletePrivate(
-      Provider<ReviewDb> dbProvider,
       RetryHelper retryHelper,
-      ChangeMessagesUtil cmUtil,
       PermissionBackend permissionBackend,
       SetPrivateOp.Factory setPrivateOpFactory) {
     super(retryHelper);
-    this.dbProvider = dbProvider;
-    this.cmUtil = cmUtil;
     this.permissionBackend = permissionBackend;
     this.setPrivateOpFactory = setPrivateOpFactory;
   }
@@ -71,10 +62,9 @@
       throw new ResourceConflictException("change is not private");
     }
 
-    SetPrivateOp op = setPrivateOpFactory.create(cmUtil, false, input);
+    SetPrivateOp op = setPrivateOpFactory.create(false, input);
     try (BatchUpdate u =
-        updateFactory.create(
-            dbProvider.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+        updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
       u.addOp(rsrc.getId(), op).execute();
     }
 
diff --git a/java/com/google/gerrit/server/restapi/change/DeletePrivateByPost.java b/java/com/google/gerrit/server/restapi/change/DeletePrivateByPost.java
index f73705b..c86d0ca 100644
--- a/java/com/google/gerrit/server/restapi/change/DeletePrivateByPost.java
+++ b/java/com/google/gerrit/server/restapi/change/DeletePrivateByPost.java
@@ -17,26 +17,21 @@
 import static com.google.gerrit.extensions.conditions.BooleanCondition.and;
 
 import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.SetPrivateOp;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 @Singleton
 public class DeletePrivateByPost extends DeletePrivate implements UiAction<ChangeResource> {
   @Inject
   DeletePrivateByPost(
-      Provider<ReviewDb> dbProvider,
       RetryHelper retryHelper,
-      ChangeMessagesUtil cmUtil,
       PermissionBackend permissionBackend,
       SetPrivateOp.Factory setPrivateOpFactory) {
-    super(dbProvider, retryHelper, cmUtil, permissionBackend, setPrivateOpFactory);
+    super(retryHelper, permissionBackend, setPrivateOpFactory);
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteReviewer.java b/java/com/google/gerrit/server/restapi/change/DeleteReviewer.java
index 83ae7b3..12dbcdd 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteReviewer.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteReviewer.java
@@ -15,11 +15,13 @@
 package com.google.gerrit.server.restapi.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.server.ReviewDb;
+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;
 import com.google.gerrit.server.change.ReviewerResource;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
@@ -28,25 +30,21 @@
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 @Singleton
 public class DeleteReviewer
     extends RetryingRestModifyView<ReviewerResource, DeleteReviewerInput, Response<?>> {
 
-  private final Provider<ReviewDb> dbProvider;
   private final DeleteReviewerOp.Factory deleteReviewerOpFactory;
   private final DeleteReviewerByEmailOp.Factory deleteReviewerByEmailOpFactory;
 
   @Inject
   DeleteReviewer(
-      Provider<ReviewDb> dbProvider,
       RetryHelper retryHelper,
       DeleteReviewerOp.Factory deleteReviewerOpFactory,
       DeleteReviewerByEmailOp.Factory deleteReviewerByEmailOpFactory) {
     super(retryHelper);
-    this.dbProvider = dbProvider;
     this.deleteReviewerOpFactory = deleteReviewerOpFactory;
     this.deleteReviewerByEmailOpFactory = deleteReviewerByEmailOpFactory;
   }
@@ -61,13 +59,13 @@
 
     try (BatchUpdate bu =
         updateFactory.create(
-            dbProvider.get(),
             rsrc.getChangeResource().getProject(),
             rsrc.getChangeResource().getUser(),
             TimeUtil.nowTs())) {
+      bu.setNotify(getNotify(rsrc.getChange(), input));
       BatchUpdateOp op;
       if (rsrc.isByEmail()) {
-        op = deleteReviewerByEmailOpFactory.create(rsrc.getReviewerByEmail(), input);
+        op = deleteReviewerByEmailOpFactory.create(rsrc.getReviewerByEmail());
       } else {
         op = deleteReviewerOpFactory.create(rsrc.getReviewerUser().state(), input);
       }
@@ -76,4 +74,12 @@
     }
     return Response.none();
   }
+
+  private static NotifyResolver.Result getNotify(Change change, DeleteReviewerInput input) {
+    NotifyHandling notifyHandling = input.notify;
+    if (notifyHandling == null) {
+      notifyHandling = change.isWorkInProgress() ? NotifyHandling.NONE : NotifyHandling.ALL;
+    }
+    return NotifyResolver.Result.create(notifyHandling);
+  }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteVote.java b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
index 57649fad..77894fb 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteVote.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.common.base.MoreObjects.firstNonNull;
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.flogger.FluentLogger;
@@ -29,16 +30,14 @@
 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.LabelId;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.change.NotifyUtil;
+import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.change.ReviewerResource;
 import com.google.gerrit.server.change.VoteResource;
 import com.google.gerrit.server.extensions.events.VoteDeleted;
@@ -57,33 +56,29 @@
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.LabelVote;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
 public class DeleteVote extends RetryingRestModifyView<VoteResource, DeleteVoteInput, Response<?>> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  private final Provider<ReviewDb> db;
   private final ApprovalsUtil approvalsUtil;
   private final PatchSetUtil psUtil;
   private final ChangeMessagesUtil cmUtil;
   private final IdentifiedUser.GenericFactory userFactory;
   private final VoteDeleted voteDeleted;
   private final DeleteVoteSender.Factory deleteVoteSenderFactory;
-  private final NotifyUtil notifyUtil;
+  private final NotifyResolver notifyResolver;
   private final RemoveReviewerControl removeReviewerControl;
   private final ProjectCache projectCache;
 
   @Inject
   DeleteVote(
-      Provider<ReviewDb> db,
       RetryHelper retryHelper,
       ApprovalsUtil approvalsUtil,
       PatchSetUtil psUtil,
@@ -91,18 +86,17 @@
       IdentifiedUser.GenericFactory userFactory,
       VoteDeleted voteDeleted,
       DeleteVoteSender.Factory deleteVoteSenderFactory,
-      NotifyUtil notifyUtil,
+      NotifyResolver notifyResolver,
       RemoveReviewerControl removeReviewerControl,
       ProjectCache projectCache) {
     super(retryHelper);
-    this.db = db;
     this.approvalsUtil = approvalsUtil;
     this.psUtil = psUtil;
     this.cmUtil = cmUtil;
     this.userFactory = userFactory;
     this.voteDeleted = voteDeleted;
     this.deleteVoteSenderFactory = deleteVoteSenderFactory;
-    this.notifyUtil = notifyUtil;
+    this.notifyResolver = notifyResolver;
     this.removeReviewerControl = removeReviewerControl;
     this.projectCache = projectCache;
   }
@@ -110,7 +104,7 @@
   @Override
   protected Response<?> applyImpl(
       BatchUpdate.Factory updateFactory, VoteResource rsrc, DeleteVoteInput input)
-      throws RestApiException, UpdateException, IOException {
+      throws RestApiException, UpdateException, IOException, ConfigInvalidException {
     if (input == null) {
       input = new DeleteVoteInput();
     }
@@ -129,7 +123,10 @@
 
     try (BatchUpdate bu =
         updateFactory.create(
-            db.get(), change.getProject(), r.getChangeResource().getUser(), TimeUtil.nowTs())) {
+            change.getProject(), r.getChangeResource().getUser(), TimeUtil.nowTs())) {
+      bu.setNotify(
+          notifyResolver.resolve(
+              firstNonNull(input.notify, NotifyHandling.ALL), input.notifyDetails));
       bu.addOp(
           change.getId(),
           new Op(
@@ -165,11 +162,10 @@
 
     @Override
     public boolean updateChange(ChangeContext ctx)
-        throws OrmException, AuthException, ResourceNotFoundException, IOException,
-            PermissionBackendException {
+        throws AuthException, ResourceNotFoundException, IOException, PermissionBackendException {
       change = ctx.getChange();
       PatchSet.Id psId = change.currentPatchSetId();
-      ps = psUtil.current(db.get(), ctx.getNotes());
+      ps = psUtil.current(ctx.getNotes());
 
       boolean found = false;
       LabelTypes labelTypes = projectState.getLabelTypes(ctx.getNotes());
@@ -178,12 +174,7 @@
 
       for (PatchSetApproval a :
           approvalsUtil.byPatchSetUser(
-              ctx.getDb(),
-              ctx.getNotes(),
-              psId,
-              accountId,
-              ctx.getRevWalk(),
-              ctx.getRepoView().getConfig())) {
+              ctx.getNotes(), psId, accountId, ctx.getRevWalk(), ctx.getRepoView().getConfig())) {
         if (labelTypes.byLabel(a.getLabelId()) == null) {
           continue; // Ignore undefined labels.
         } else if (!a.getLabel().equals(label)) {
@@ -210,7 +201,6 @@
       }
 
       ctx.getUpdate(psId).removeApprovalFor(accountId, label);
-      ctx.getDb().patchSetApprovals().upsert(Collections.singleton(deletedApproval(ctx)));
 
       StringBuilder msg = new StringBuilder();
       msg.append("Removed ");
@@ -218,22 +208,11 @@
       msg.append(" by ").append(userFactory.create(accountId).getNameEmail()).append("\n");
       changeMessage =
           ChangeMessagesUtil.newMessage(ctx, msg.toString(), ChangeMessagesUtil.TAG_DELETE_VOTE);
-      cmUtil.addChangeMessage(ctx.getDb(), ctx.getUpdate(psId), changeMessage);
+      cmUtil.addChangeMessage(ctx.getUpdate(psId), changeMessage);
 
       return true;
     }
 
-    private PatchSetApproval deletedApproval(ChangeContext ctx) {
-      // Set the effective user to the account we're trying to remove, and don't
-      // set the real user; this preserves the calling user as the NoteDb
-      // committer.
-      return new PatchSetApproval(
-          new PatchSetApproval.Key(
-              ps.getId(), accountState.getAccount().getId(), new LabelId(label)),
-          (short) 0,
-          ctx.getWhen());
-    }
-
     @Override
     public void postUpdate(Context ctx) {
       if (changeMessage == null) {
@@ -241,17 +220,17 @@
       }
 
       IdentifiedUser user = ctx.getIdentifiedUser();
-      if (NotifyUtil.shouldNotify(input.notify, input.notifyDetails)) {
-        try {
+      try {
+        NotifyResolver.Result notify = ctx.getNotify(change.getId());
+        if (notify.shouldNotify()) {
           ReplyToChangeSender cm = deleteVoteSenderFactory.create(ctx.getProject(), change.getId());
           cm.setFrom(user.getAccountId());
           cm.setChangeMessage(changeMessage.getMessage(), ctx.getWhen());
-          cm.setNotify(input.notify);
-          cm.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
+          cm.setNotify(notify);
           cm.send();
-        } catch (Exception e) {
-          logger.atSevere().withCause(e).log("Cannot email update for change %s", change.getId());
         }
+      } catch (Exception e) {
+        logger.atSevere().withCause(e).log("Cannot email update for change %s", change.getId());
       }
 
       voteDeleted.fire(
diff --git a/java/com/google/gerrit/server/restapi/change/DownloadContent.java b/java/com/google/gerrit/server/restapi/change/DownloadContent.java
index b6564c0..1022cad 100644
--- a/java/com/google/gerrit/server/restapi/change/DownloadContent.java
+++ b/java/com/google/gerrit/server/restapi/change/DownloadContent.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import org.eclipse.jgit.lib.ObjectId;
@@ -43,7 +42,7 @@
 
   @Override
   public BinaryResult apply(FileResource rsrc)
-      throws ResourceNotFoundException, IOException, NoSuchChangeException, OrmException {
+      throws ResourceNotFoundException, IOException, NoSuchChangeException {
     String path = rsrc.getPatchKey().get();
     RevisionResource rev = rsrc.getRevision();
     ObjectId revstr = ObjectId.fromString(rev.getPatchSet().getRevision().get());
diff --git a/java/com/google/gerrit/server/restapi/change/DraftComments.java b/java/com/google/gerrit/server/restapi/change/DraftComments.java
index b8e24a5..dd61ca0 100644
--- a/java/com/google/gerrit/server/restapi/change/DraftComments.java
+++ b/java/com/google/gerrit/server/restapi/change/DraftComments.java
@@ -21,12 +21,10 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.DraftCommentResource;
 import com.google.gerrit.server.change.RevisionResource;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -36,7 +34,6 @@
   private final DynamicMap<RestView<DraftCommentResource>> views;
   private final Provider<CurrentUser> user;
   private final ListRevisionDrafts list;
-  private final Provider<ReviewDb> dbProvider;
   private final CommentsUtil commentsUtil;
 
   @Inject
@@ -44,12 +41,10 @@
       DynamicMap<RestView<DraftCommentResource>> views,
       Provider<CurrentUser> user,
       ListRevisionDrafts list,
-      Provider<ReviewDb> dbProvider,
       CommentsUtil commentsUtil) {
     this.views = views;
     this.user = user;
     this.list = list;
-    this.dbProvider = dbProvider;
     this.commentsUtil = commentsUtil;
   }
 
@@ -66,12 +61,12 @@
 
   @Override
   public DraftCommentResource parse(RevisionResource rev, IdString id)
-      throws ResourceNotFoundException, OrmException, AuthException {
+      throws ResourceNotFoundException, AuthException {
     checkIdentifiedUser();
     String uuid = id.get();
     for (Comment c :
         commentsUtil.draftByPatchSetAuthor(
-            dbProvider.get(), rev.getPatchSet().getId(), rev.getAccountId(), rev.getNotes())) {
+            rev.getPatchSet().getId(), 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 84283e7..d892810 100644
--- a/java/com/google/gerrit/server/restapi/change/Files.java
+++ b/java/com/google/gerrit/server/restapi/change/Files.java
@@ -18,6 +18,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.common.hash.Hasher;
 import com.google.common.hash.Hashing;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.common.FileInfo;
 import com.google.gerrit.extensions.registration.DynamicMap;
@@ -34,7 +35,6 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.AccountPatchReviewStore;
@@ -50,7 +50,6 @@
 import com.google.gerrit.server.patch.PatchListObjectTooLargeException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.plugincontext.PluginItemContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -114,7 +113,6 @@
     @Option(name = "-q")
     String query;
 
-    private final Provider<ReviewDb> db;
     private final Provider<CurrentUser> self;
     private final FileInfoJson fileInfoJson;
     private final Revisions revisions;
@@ -126,7 +124,6 @@
 
     @Inject
     ListFiles(
-        Provider<ReviewDb> db,
         Provider<CurrentUser> self,
         FileInfoJson fileInfoJson,
         Revisions revisions,
@@ -135,7 +132,6 @@
         PatchSetUtil psUtil,
         PluginItemContext<AccountPatchReviewStore> accountPatchReviewStore,
         GerritApi gApi) {
-      this.db = db;
       this.self = self;
       this.fileInfoJson = fileInfoJson;
       this.revisions = revisions;
@@ -153,7 +149,7 @@
 
     @Override
     public Response<?> apply(RevisionResource resource)
-        throws RestApiException, OrmException, RepositoryNotFoundException, IOException,
+        throws RestApiException, RepositoryNotFoundException, IOException,
             PatchListNotAvailableException, PermissionBackendException {
       checkOptions();
       if (reviewed) {
@@ -239,8 +235,7 @@
       }
     }
 
-    private Collection<String> reviewed(RevisionResource resource)
-        throws AuthException, OrmException {
+    private Collection<String> reviewed(RevisionResource resource) throws AuthException {
       CurrentUser user = self.get();
       if (!(user.isIdentifiedUser())) {
         throw new AuthException("Authentication required");
@@ -249,9 +244,7 @@
       Account.Id userId = user.getAccountId();
       PatchSet patchSetId = resource.getPatchSet();
       Optional<PatchSetWithReviewedFiles> o;
-      o =
-          accountPatchReviewStore.call(
-              s -> s.findReviewed(patchSetId.getId(), userId), OrmException.class);
+      o = accountPatchReviewStore.call(s -> s.findReviewed(patchSetId.getId(), userId));
 
       if (o.isPresent()) {
         PatchSetWithReviewedFiles res = o.get();
@@ -273,14 +266,14 @@
 
     private List<String> copy(
         Set<String> paths, PatchSet.Id old, RevisionResource resource, Account.Id userId)
-        throws IOException, PatchListNotAvailableException, OrmException {
+        throws IOException, PatchListNotAvailableException {
       Project.NameKey project = resource.getChange().getProject();
       try (Repository git = gitManager.openRepository(project);
           ObjectReader reader = git.newObjectReader();
           RevWalk rw = new RevWalk(reader);
           TreeWalk tw = new TreeWalk(reader)) {
         Change change = resource.getChange();
-        PatchSet patchSet = psUtil.get(db.get(), resource.getNotes(), old);
+        PatchSet patchSet = psUtil.get(resource.getNotes(), old);
         if (patchSet == null) {
           throw new PatchListNotAvailableException(
               String.format(
@@ -334,8 +327,7 @@
         }
 
         accountPatchReviewStore.run(
-            s -> s.markReviewed(resource.getPatchSet().getId(), userId, pathList),
-            OrmException.class);
+            s -> s.markReviewed(resource.getPatchSet().getId(), userId, pathList));
         return pathList;
       }
     }
@@ -345,7 +337,7 @@
       return this;
     }
 
-    public ListFiles setBase(String base) {
+    public ListFiles setBase(@Nullable String base) {
       this.base = base;
       return this;
     }
diff --git a/java/com/google/gerrit/server/restapi/change/Fixes.java b/java/com/google/gerrit/server/restapi/change/Fixes.java
index 1d8726d..855d1f4 100644
--- a/java/com/google/gerrit/server/restapi/change/Fixes.java
+++ b/java/com/google/gerrit/server/restapi/change/Fixes.java
@@ -25,7 +25,6 @@
 import com.google.gerrit.server.change.FixResource;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.List;
@@ -50,7 +49,7 @@
 
   @Override
   public FixResource parse(RevisionResource revisionResource, IdString id)
-      throws ResourceNotFoundException, OrmException {
+      throws ResourceNotFoundException {
     String fixId = id.get();
     ChangeNotes changeNotes = revisionResource.getNotes();
 
diff --git a/java/com/google/gerrit/server/restapi/change/GetAssignee.java b/java/com/google/gerrit/server/restapi/change/GetAssignee.java
index e95f8d8..f89fe1b 100644
--- a/java/com/google/gerrit/server/restapi/change/GetAssignee.java
+++ b/java/com/google/gerrit/server/restapi/change/GetAssignee.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.Optional;
@@ -36,8 +35,7 @@
   }
 
   @Override
-  public Response<AccountInfo> apply(ChangeResource rsrc)
-      throws OrmException, PermissionBackendException {
+  public Response<AccountInfo> apply(ChangeResource rsrc) throws PermissionBackendException {
     Optional<Account.Id> assignee = Optional.ofNullable(rsrc.getChange().getAssignee());
     if (assignee.isPresent()) {
       return Response.ok(accountLoaderFactory.create(true).fillOne(assignee.get()));
diff --git a/java/com/google/gerrit/server/restapi/change/GetBlame.java b/java/com/google/gerrit/server/restapi/change/GetBlame.java
index c7a8015..98a3a8a 100644
--- a/java/com/google/gerrit/server/restapi/change/GetBlame.java
+++ b/java/com/google/gerrit/server/restapi/change/GetBlame.java
@@ -32,7 +32,6 @@
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gitiles.blame.cache.BlameCache;
 import com.google.gitiles.blame.cache.Region;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -77,9 +76,14 @@
     this.autoMerger = autoMerger;
   }
 
+  public GetBlame setBase(boolean base) {
+    this.base = base;
+    return this;
+  }
+
   @Override
   public Response<List<BlameInfo>> apply(FileResource resource)
-      throws RestApiException, OrmException, IOException, InvalidChangeOperationException {
+      throws RestApiException, IOException, InvalidChangeOperationException {
     Project.NameKey project = resource.getRevision().getChange().getProject();
     try (Repository repository = repoManager.openRepository(project);
         ObjectInserter ins = repository.newObjectInserter();
diff --git a/java/com/google/gerrit/server/restapi/change/GetChange.java b/java/com/google/gerrit/server/restapi/change/GetChange.java
index a8f8bbb..c28741b 100644
--- a/java/com/google/gerrit/server/restapi/change/GetChange.java
+++ b/java/com/google/gerrit/server/restapi/change/GetChange.java
@@ -14,43 +14,79 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Streams;
 import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.client.ListOption;
 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.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.DynamicOptions;
+import com.google.gerrit.server.DynamicOptions.DynamicBean;
+import com.google.gerrit.server.change.ChangeAttributeFactory;
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.PluginDefinedAttributesFactories;
 import com.google.gerrit.server.change.RevisionResource;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
 import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Map;
 import org.kohsuke.args4j.Option;
 
-public class GetChange implements RestReadView<ChangeResource> {
+public class GetChange
+    implements RestReadView<ChangeResource>,
+        DynamicOptions.BeanReceiver,
+        DynamicOptions.BeanProvider {
   private final ChangeJson.Factory json;
+  private final DynamicSet<ChangeAttributeFactory> attrFactories;
   private final EnumSet<ListChangesOption> options = EnumSet.noneOf(ListChangesOption.class);
+  private final Map<String, DynamicBean> dynamicBeans = new HashMap<>();
 
   @Option(name = "-o", usage = "Output options")
-  void addOption(ListChangesOption o) {
+  public void addOption(ListChangesOption o) {
     options.add(o);
   }
 
   @Option(name = "-O", usage = "Output option flags, in hex")
   void setOptionFlagsHex(String hex) {
-    options.addAll(ListChangesOption.fromBits(Integer.parseInt(hex, 16)));
+    options.addAll(ListOption.fromBits(ListChangesOption.class, Integer.parseInt(hex, 16)));
   }
 
   @Inject
-  GetChange(ChangeJson.Factory json) {
+  GetChange(ChangeJson.Factory json, DynamicSet<ChangeAttributeFactory> attrFactories) {
     this.json = json;
+    this.attrFactories = attrFactories;
   }
 
   @Override
-  public Response<ChangeInfo> apply(ChangeResource rsrc) throws OrmException {
-    return Response.withMustRevalidate(json.create(options).format(rsrc));
+  public void setDynamicBean(String plugin, DynamicOptions.DynamicBean dynamicBean) {
+    dynamicBeans.put(plugin, dynamicBean);
   }
 
-  Response<ChangeInfo> apply(RevisionResource rsrc) throws OrmException {
-    return Response.withMustRevalidate(json.create(options).format(rsrc));
+  @Override
+  public DynamicBean getDynamicBean(String plugin) {
+    return dynamicBeans.get(plugin);
+  }
+
+  @Override
+  public Response<ChangeInfo> apply(ChangeResource rsrc) {
+    return Response.withMustRevalidate(newChangeJson().format(rsrc));
+  }
+
+  Response<ChangeInfo> apply(RevisionResource rsrc) {
+    return Response.withMustRevalidate(newChangeJson().format(rsrc));
+  }
+
+  private ChangeJson newChangeJson() {
+    return json.create(options, this::buildPluginInfo);
+  }
+
+  private ImmutableList<PluginDefinedInfo> buildPluginInfo(ChangeData cd) {
+    return PluginDefinedAttributesFactories.createAll(
+        cd, this, Streams.stream(attrFactories.entries()));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/GetComment.java b/java/com/google/gerrit/server/restapi/change/GetComment.java
index d067dff..0109c95 100644
--- a/java/com/google/gerrit/server/restapi/change/GetComment.java
+++ b/java/com/google/gerrit/server/restapi/change/GetComment.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.change.CommentResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -34,7 +33,7 @@
   }
 
   @Override
-  public CommentInfo apply(CommentResource rsrc) throws OrmException, PermissionBackendException {
+  public CommentInfo apply(CommentResource rsrc) throws PermissionBackendException {
     return commentJson.get().newCommentFormatter().format(rsrc.getComment());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/GetContent.java b/java/com/google/gerrit/server/restapi/change/GetContent.java
index 6b9bf17..1d35ab5 100644
--- a/java/com/google/gerrit/server/restapi/change/GetContent.java
+++ b/java/com/google/gerrit/server/restapi/change/GetContent.java
@@ -21,7 +21,6 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.FileContentUtil;
 import com.google.gerrit.server.change.FileResource;
@@ -31,9 +30,7 @@
 import com.google.gerrit.server.patch.Text;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import java.io.IOException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.ObjectId;
@@ -43,7 +40,6 @@
 import org.kohsuke.args4j.Option;
 
 public class GetContent implements RestReadView<FileResource> {
-  private final Provider<ReviewDb> db;
   private final GitRepositoryManager gitManager;
   private final PatchSetUtil psUtil;
   private final FileContentUtil fileContentUtil;
@@ -54,12 +50,10 @@
 
   @Inject
   GetContent(
-      Provider<ReviewDb> db,
       GitRepositoryManager gitManager,
       PatchSetUtil psUtil,
       FileContentUtil fileContentUtil,
       ProjectCache projectCache) {
-    this.db = db;
     this.gitManager = gitManager;
     this.psUtil = psUtil;
     this.fileContentUtil = fileContentUtil;
@@ -68,7 +62,7 @@
 
   @Override
   public BinaryResult apply(FileResource rsrc)
-      throws ResourceNotFoundException, IOException, BadRequestException, OrmException {
+      throws ResourceNotFoundException, IOException, BadRequestException {
     String path = rsrc.getPatchKey().get();
     if (Patch.COMMIT_MSG.equals(path)) {
       String msg = getMessage(rsrc.getRevision().getChangeResource().getNotes());
@@ -88,9 +82,9 @@
         parent);
   }
 
-  private String getMessage(ChangeNotes notes) throws OrmException, IOException {
+  private String getMessage(ChangeNotes notes) throws IOException {
     Change.Id changeId = notes.getChangeId();
-    PatchSet ps = psUtil.current(db.get(), notes);
+    PatchSet ps = psUtil.current(notes);
     if (ps == null) {
       throw new NoSuchChangeException(changeId);
     }
@@ -104,9 +98,9 @@
     }
   }
 
-  private byte[] getMergeList(ChangeNotes notes) throws OrmException, IOException {
+  private byte[] getMergeList(ChangeNotes notes) throws IOException {
     Change.Id changeId = notes.getChangeId();
-    PatchSet ps = psUtil.current(db.get(), notes);
+    PatchSet ps = psUtil.current(notes);
     if (ps == null) {
       throw new NoSuchChangeException(changeId);
     }
diff --git a/java/com/google/gerrit/server/restapi/change/GetDetail.java b/java/com/google/gerrit/server/restapi/change/GetDetail.java
index ab75ab7..e31d84b 100644
--- a/java/com/google/gerrit/server/restapi/change/GetDetail.java
+++ b/java/com/google/gerrit/server/restapi/change/GetDetail.java
@@ -18,12 +18,13 @@
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.DynamicOptions;
+import com.google.gerrit.server.DynamicOptions.DynamicBean;
 import com.google.gerrit.server.change.ChangeResource;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import org.kohsuke.args4j.Option;
 
-public class GetDetail implements RestReadView<ChangeResource> {
+public class GetDetail implements RestReadView<ChangeResource>, DynamicOptions.BeanReceiver {
   private final GetChange delegate;
 
   @Option(name = "-o", usage = "Output options")
@@ -47,7 +48,17 @@
   }
 
   @Override
-  public Response<ChangeInfo> apply(ChangeResource rsrc) throws OrmException {
+  public void setDynamicBean(String plugin, DynamicBean dynamicBean) {
+    delegate.setDynamicBean(plugin, dynamicBean);
+  }
+
+  @Override
+  public Class<? extends DynamicOptions.BeanReceiver> getExportedBeanReceiver() {
+    return delegate.getExportedBeanReceiver();
+  }
+
+  @Override
+  public Response<ChangeInfo> apply(ChangeResource rsrc) {
     return delegate.apply(rsrc);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/GetDiff.java b/java/com/google/gerrit/server/restapi/change/GetDiff.java
index 5590da9..41254ed 100644
--- a/java/com/google/gerrit/server/restapi/change/GetDiff.java
+++ b/java/com/google/gerrit/server/restapi/change/GetDiff.java
@@ -41,6 +41,7 @@
 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.jgit.diff.ReplaceEdit;
 import com.google.gerrit.prettify.common.SparseFileContent;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -57,14 +58,12 @@
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.diff.Edit;
-import org.eclipse.jgit.diff.ReplaceEdit;
 import org.kohsuke.args4j.CmdLineException;
 import org.kohsuke.args4j.CmdLineParser;
 import org.kohsuke.args4j.NamedOptionDef;
@@ -112,9 +111,6 @@
   @Option(name = "--intraline")
   boolean intraline;
 
-  @Option(name = "--weblinks-only")
-  boolean webLinksOnly;
-
   @Inject
   GetDiff(
       ProjectCache projectCache,
@@ -129,7 +125,7 @@
 
   @Override
   public Response<DiffInfo> apply(FileResource resource)
-      throws ResourceConflictException, ResourceNotFoundException, OrmException, AuthException,
+      throws ResourceConflictException, ResourceNotFoundException, AuthException,
           InvalidChangeOperationException, IOException, PermissionBackendException {
     DiffPreferencesInfo prefs = new DiffPreferencesInfo();
     if (whitespace != null) {
@@ -217,52 +213,50 @@
               ps.getNewName());
       result.webLinks = links.isEmpty() ? null : links;
 
-      if (!webLinksOnly) {
-        if (ps.isBinary()) {
-          result.binary = true;
-        }
-        if (ps.getDisplayMethodA() != DisplayMethod.NONE) {
-          result.metaA = new FileMeta();
-          result.metaA.name = MoreObjects.firstNonNull(ps.getOldName(), ps.getNewName());
-          result.metaA.contentType =
-              FileContentUtil.resolveContentType(
-                  state, result.metaA.name, ps.getFileModeA(), ps.getMimeTypeA());
-          result.metaA.lines = ps.getA().size();
-          result.metaA.webLinks = getFileWebLinks(state.getProject(), revA, result.metaA.name);
-          result.metaA.commitId = content.commitIdA;
-        }
-
-        if (ps.getDisplayMethodB() != DisplayMethod.NONE) {
-          result.metaB = new FileMeta();
-          result.metaB.name = ps.getNewName();
-          result.metaB.contentType =
-              FileContentUtil.resolveContentType(
-                  state, result.metaB.name, ps.getFileModeB(), ps.getMimeTypeB());
-          result.metaB.lines = ps.getB().size();
-          result.metaB.webLinks = getFileWebLinks(state.getProject(), revB, result.metaB.name);
-          result.metaB.commitId = content.commitIdB;
-        }
-
-        if (intraline) {
-          if (ps.hasIntralineTimeout()) {
-            result.intralineStatus = IntraLineStatus.TIMEOUT;
-          } else if (ps.hasIntralineFailure()) {
-            result.intralineStatus = IntraLineStatus.FAILURE;
-          } else {
-            result.intralineStatus = IntraLineStatus.OK;
-          }
-        }
-
-        result.changeType = CHANGE_TYPE.get(ps.getChangeType());
-        if (result.changeType == null) {
-          throw new IllegalStateException("unknown change type: " + ps.getChangeType());
-        }
-
-        if (ps.getPatchHeader().size() > 0) {
-          result.diffHeader = ps.getPatchHeader();
-        }
-        result.content = content.lines;
+      if (ps.isBinary()) {
+        result.binary = true;
       }
+      if (ps.getDisplayMethodA() != DisplayMethod.NONE) {
+        result.metaA = new FileMeta();
+        result.metaA.name = MoreObjects.firstNonNull(ps.getOldName(), ps.getNewName());
+        result.metaA.contentType =
+            FileContentUtil.resolveContentType(
+                state, result.metaA.name, ps.getFileModeA(), ps.getMimeTypeA());
+        result.metaA.lines = ps.getA().size();
+        result.metaA.webLinks = getFileWebLinks(state.getProject(), revA, result.metaA.name);
+        result.metaA.commitId = content.commitIdA;
+      }
+
+      if (ps.getDisplayMethodB() != DisplayMethod.NONE) {
+        result.metaB = new FileMeta();
+        result.metaB.name = ps.getNewName();
+        result.metaB.contentType =
+            FileContentUtil.resolveContentType(
+                state, result.metaB.name, ps.getFileModeB(), ps.getMimeTypeB());
+        result.metaB.lines = ps.getB().size();
+        result.metaB.webLinks = getFileWebLinks(state.getProject(), revB, result.metaB.name);
+        result.metaB.commitId = content.commitIdB;
+      }
+
+      if (intraline) {
+        if (ps.hasIntralineTimeout()) {
+          result.intralineStatus = IntraLineStatus.TIMEOUT;
+        } else if (ps.hasIntralineFailure()) {
+          result.intralineStatus = IntraLineStatus.FAILURE;
+        } else {
+          result.intralineStatus = IntraLineStatus.OK;
+        }
+      }
+
+      result.changeType = CHANGE_TYPE.get(ps.getChangeType());
+      if (result.changeType == null) {
+        throw new IllegalStateException("unknown change type: " + ps.getChangeType());
+      }
+
+      if (ps.getPatchHeader().size() > 0) {
+        result.diffHeader = ps.getPatchHeader();
+      }
+      result.content = content.lines;
 
       Response<DiffInfo> r = Response.ok(result);
       if (resource.isCacheable()) {
diff --git a/java/com/google/gerrit/server/restapi/change/GetDraftComment.java b/java/com/google/gerrit/server/restapi/change/GetDraftComment.java
index 6049607..ca5b56f 100644
--- a/java/com/google/gerrit/server/restapi/change/GetDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/GetDraftComment.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.change.DraftCommentResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -34,8 +33,7 @@
   }
 
   @Override
-  public CommentInfo apply(DraftCommentResource rsrc)
-      throws OrmException, PermissionBackendException {
+  public CommentInfo apply(DraftCommentResource rsrc) throws PermissionBackendException {
     return commentJson.get().newCommentFormatter().format(rsrc.getComment());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/GetHashtags.java b/java/com/google/gerrit/server/restapi/change/GetHashtags.java
index 8369acf..aff3a44 100644
--- a/java/com/google/gerrit/server/restapi/change/GetHashtags.java
+++ b/java/com/google/gerrit/server/restapi/change/GetHashtags.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.Collections;
@@ -30,7 +29,7 @@
 public class GetHashtags implements RestReadView<ChangeResource> {
   @Override
   public Response<Set<String>> apply(ChangeResource req)
-      throws AuthException, OrmException, IOException, BadRequestException {
+      throws AuthException, IOException, BadRequestException {
     ChangeNotes notes = req.getNotes().load();
     Set<String> hashtags = notes.getHashtags();
     if (hashtags == null) {
diff --git a/java/com/google/gerrit/server/restapi/change/GetMergeList.java b/java/com/google/gerrit/server/restapi/change/GetMergeList.java
index 8e7e693..0c18a8f 100644
--- a/java/com/google/gerrit/server/restapi/change/GetMergeList.java
+++ b/java/com/google/gerrit/server/restapi/change/GetMergeList.java
@@ -76,7 +76,7 @@
       }
 
       if (commit.getParentCount() < 2) {
-        return createResponse(rsrc, ImmutableList.<CommitInfo>of());
+        return createResponse(rsrc, ImmutableList.of());
       }
 
       List<RevCommit> commits = MergeListBuilder.build(rw, commit, uninterestingParent);
diff --git a/java/com/google/gerrit/server/restapi/change/GetPastAssignees.java b/java/com/google/gerrit/server/restapi/change/GetPastAssignees.java
index 279cfe3..1d56669 100644
--- a/java/com/google/gerrit/server/restapi/change/GetPastAssignees.java
+++ b/java/com/google/gerrit/server/restapi/change/GetPastAssignees.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.Collections;
@@ -40,8 +39,7 @@
   }
 
   @Override
-  public Response<List<AccountInfo>> apply(ChangeResource rsrc)
-      throws OrmException, PermissionBackendException {
+  public Response<List<AccountInfo>> apply(ChangeResource rsrc) throws PermissionBackendException {
 
     Set<Account.Id> pastAssignees = rsrc.getNotes().load().getPastAssignees();
     if (pastAssignees == null) {
diff --git a/java/com/google/gerrit/server/restapi/change/GetPureRevert.java b/java/com/google/gerrit/server/restapi/change/GetPureRevert.java
index 75019af..fa5cc36 100644
--- a/java/com/google/gerrit/server/restapi/change/GetPureRevert.java
+++ b/java/com/google/gerrit/server/restapi/change/GetPureRevert.java
@@ -22,9 +22,9 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.PureRevert;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
+import java.util.Optional;
 import org.kohsuke.args4j.Option;
 
 public class GetPureRevert implements RestReadView<ChangeResource> {
@@ -47,8 +47,8 @@
 
   @Override
   public PureRevertInfo apply(ChangeResource rsrc)
-      throws ResourceConflictException, IOException, BadRequestException, OrmException,
-          AuthException {
-    return pureRevert.get(rsrc.getNotes(), claimedOriginal);
+      throws ResourceConflictException, IOException, BadRequestException, AuthException {
+    boolean isPureRevert = pureRevert.get(rsrc.getNotes(), Optional.ofNullable(claimedOriginal));
+    return 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 3eea736..332cc4d 100644
--- a/java/com/google/gerrit/server/restapi/change/GetRelated.java
+++ b/java/com/google/gerrit/server/restapi/change/GetRelated.java
@@ -27,7 +27,7 @@
 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.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CommonConverters;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.RevisionResource;
@@ -36,7 +36,6 @@
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -44,13 +43,13 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Locale;
 import java.util.Set;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.revwalk.RevCommit;
 
 @Singleton
 public class GetRelated implements RestReadView<RevisionResource> {
-  private final Provider<ReviewDb> db;
   private final Provider<InternalChangeQuery> queryProvider;
   private final PatchSetUtil psUtil;
   private final RelatedChangesSorter sorter;
@@ -58,12 +57,10 @@
 
   @Inject
   GetRelated(
-      Provider<ReviewDb> db,
       Provider<InternalChangeQuery> queryProvider,
       PatchSetUtil psUtil,
       RelatedChangesSorter sorter,
       IndexConfig indexConfig) {
-    this.db = db;
     this.queryProvider = queryProvider;
     this.psUtil = psUtil;
     this.sorter = sorter;
@@ -72,7 +69,7 @@
 
   @Override
   public RelatedChangesInfo apply(RevisionResource rsrc)
-      throws RepositoryNotFoundException, IOException, OrmException, NoSuchProjectException,
+      throws RepositoryNotFoundException, IOException, NoSuchProjectException,
           PermissionBackendException {
     RelatedChangesInfo relatedChangesInfo = new RelatedChangesInfo();
     relatedChangesInfo.changes = getRelated(rsrc);
@@ -80,8 +77,8 @@
   }
 
   private List<RelatedChangeAndCommitInfo> getRelated(RevisionResource rsrc)
-      throws OrmException, IOException, PermissionBackendException {
-    Set<String> groups = getAllGroups(rsrc.getNotes(), db.get(), psUtil);
+      throws IOException, PermissionBackendException {
+    Set<String> groups = getAllGroups(rsrc.getNotes(), psUtil);
     if (groups.isEmpty()) {
       return Collections.emptyList();
     }
@@ -125,14 +122,11 @@
   }
 
   @VisibleForTesting
-  public static Set<String> getAllGroups(ChangeNotes notes, ReviewDb db, PatchSetUtil psUtil)
-      throws OrmException {
-    return psUtil.byChange(db, notes).stream()
-        .flatMap(ps -> ps.getGroups().stream())
-        .collect(toSet());
+  public static Set<String> getAllGroups(ChangeNotes notes, PatchSetUtil psUtil) {
+    return psUtil.byChange(notes).stream().flatMap(ps -> ps.getGroups().stream()).collect(toSet());
   }
 
-  private void reloadChangeIfStale(List<ChangeData> cds, PatchSet wantedPs) throws OrmException {
+  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) {
@@ -153,7 +147,7 @@
       info._revisionNumber = ps != null ? ps.getPatchSetId() : null;
       PatchSet.Id curr = change.currentPatchSetId();
       info._currentRevisionNumber = curr != null ? curr.get() : null;
-      info.status = change.getStatus().asChangeStatus().toString();
+      info.status = ChangeUtil.status(change).toUpperCase(Locale.US);
     }
 
     info.commit = new CommitInfo();
diff --git a/java/com/google/gerrit/server/restapi/change/GetReview.java b/java/com/google/gerrit/server/restapi/change/GetReview.java
index 40e132d..8d941ab 100644
--- a/java/com/google/gerrit/server/restapi/change/GetReview.java
+++ b/java/com/google/gerrit/server/restapi/change/GetReview.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.change.RevisionResource;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -35,7 +34,7 @@
   }
 
   @Override
-  public Response<ChangeInfo> apply(RevisionResource rsrc) throws OrmException {
+  public Response<ChangeInfo> apply(RevisionResource rsrc) {
     return delegate.apply(rsrc);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/GetReviewer.java b/java/com/google/gerrit/server/restapi/change/GetReviewer.java
index a11380b..73760da 100644
--- a/java/com/google/gerrit/server/restapi/change/GetReviewer.java
+++ b/java/com/google/gerrit/server/restapi/change/GetReviewer.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.server.change.ReviewerJson;
 import com.google.gerrit.server.change.ReviewerResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.List;
@@ -34,8 +33,7 @@
   }
 
   @Override
-  public List<ReviewerInfo> apply(ReviewerResource rsrc)
-      throws OrmException, PermissionBackendException {
+  public List<ReviewerInfo> apply(ReviewerResource rsrc) throws PermissionBackendException {
     return json.format(rsrc);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/GetRevisionActions.java b/java/com/google/gerrit/server/restapi/change/GetRevisionActions.java
index 03b95a6..c4da3b6 100644
--- a/java/com/google/gerrit/server/restapi/change/GetRevisionActions.java
+++ b/java/com/google/gerrit/server/restapi/change/GetRevisionActions.java
@@ -16,10 +16,10 @@
 
 import com.google.common.hash.Hasher;
 import com.google.common.hash.Hashing;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.common.ActionInfo;
 import com.google.gerrit.extensions.restapi.ETagView;
 import com.google.gerrit.extensions.restapi.Response;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.ActionJson;
 import com.google.gerrit.server.change.ChangeResource;
@@ -29,8 +29,6 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.submit.ChangeSet;
 import com.google.gerrit.server.submit.MergeSuperSet;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.OrmRuntimeException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -42,26 +40,23 @@
 public class GetRevisionActions implements ETagView<RevisionResource> {
   private final ActionJson delegate;
   private final Config config;
-  private final Provider<ReviewDb> dbProvider;
   private final Provider<MergeSuperSet> mergeSuperSet;
   private final ChangeResource.Factory changeResourceFactory;
 
   @Inject
   GetRevisionActions(
       ActionJson delegate,
-      Provider<ReviewDb> dbProvider,
       Provider<MergeSuperSet> mergeSuperSet,
       ChangeResource.Factory changeResourceFactory,
       @GerritServerConfig Config config) {
     this.delegate = delegate;
-    this.dbProvider = dbProvider;
     this.mergeSuperSet = mergeSuperSet;
     this.changeResourceFactory = changeResourceFactory;
     this.config = config;
   }
 
   @Override
-  public Response<Map<String, ActionInfo>> apply(RevisionResource rsrc) throws OrmException {
+  public Response<Map<String, ActionInfo>> apply(RevisionResource rsrc) {
     return Response.withMustRevalidate(delegate.format(rsrc));
   }
 
@@ -72,14 +67,13 @@
     try {
       rsrc.getChangeResource().prepareETag(h, user);
       h.putBoolean(MergeSuperSet.wholeTopicEnabled(config));
-      ReviewDb db = dbProvider.get();
-      ChangeSet cs = mergeSuperSet.get().completeChangeSet(db, rsrc.getChange(), user);
+      ChangeSet cs = mergeSuperSet.get().completeChangeSet(rsrc.getChange(), user);
       for (ChangeData cd : cs.changes()) {
         changeResourceFactory.create(cd.notes(), user).prepareETag(h, user);
       }
       h.putBoolean(cs.furtherHiddenChanges());
-    } catch (IOException | OrmException | PermissionBackendException e) {
-      throw new OrmRuntimeException(e);
+    } catch (IOException | PermissionBackendException e) {
+      throw new StorageException(e);
     }
     return h.hash().toString();
   }
diff --git a/java/com/google/gerrit/server/restapi/change/GetRobotComment.java b/java/com/google/gerrit/server/restapi/change/GetRobotComment.java
index 0197068..75d994d 100644
--- a/java/com/google/gerrit/server/restapi/change/GetRobotComment.java
+++ b/java/com/google/gerrit/server/restapi/change/GetRobotComment.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.change.RobotCommentResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -34,8 +33,7 @@
   }
 
   @Override
-  public RobotCommentInfo apply(RobotCommentResource rsrc)
-      throws OrmException, PermissionBackendException {
+  public RobotCommentInfo apply(RobotCommentResource rsrc) throws PermissionBackendException {
     return commentJson.get().newRobotCommentFormatter().format(rsrc.getComment());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/Ignore.java b/java/com/google/gerrit/server/restapi/change/Ignore.java
index e319451..25cf311 100644
--- a/java/com/google/gerrit/server/restapi/change/Ignore.java
+++ b/java/com/google/gerrit/server/restapi/change/Ignore.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -26,7 +27,6 @@
 import com.google.gerrit.server.StarredChangesUtil.IllegalLabelException;
 import com.google.gerrit.server.StarredChangesUtil.MutuallyExclusiveLabelsException;
 import com.google.gerrit.server.change.ChangeResource;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -51,7 +51,7 @@
 
   @Override
   public Response<String> apply(ChangeResource rsrc, Input input)
-      throws RestApiException, OrmException, IllegalLabelException {
+      throws RestApiException, IllegalLabelException {
     try {
       if (rsrc.isUserOwner()) {
         throw new BadRequestException("cannot ignore own change");
@@ -73,7 +73,7 @@
   private boolean isIgnored(ChangeResource rsrc) {
     try {
       return stars.isIgnored(rsrc);
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("failed to check ignored star");
     }
     return false;
diff --git a/java/com/google/gerrit/server/restapi/change/Index.java b/java/com/google/gerrit/server/restapi/change/Index.java
index a5dd868..90dad98 100644
--- a/java/com/google/gerrit/server/restapi/change/Index.java
+++ b/java/com/google/gerrit/server/restapi/change/Index.java
@@ -17,7 +17,6 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.index.change.ChangeIndexer;
 import com.google.gerrit.server.permissions.GlobalPermission;
@@ -26,27 +25,18 @@
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryingRestModifyView;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 
 @Singleton
 public class Index extends RetryingRestModifyView<ChangeResource, Input, Response<?>> {
-
-  private final Provider<ReviewDb> db;
   private final PermissionBackend permissionBackend;
   private final ChangeIndexer indexer;
 
   @Inject
-  Index(
-      Provider<ReviewDb> db,
-      RetryHelper retryHelper,
-      PermissionBackend permissionBackend,
-      ChangeIndexer indexer) {
+  Index(RetryHelper retryHelper, PermissionBackend permissionBackend, ChangeIndexer indexer) {
     super(retryHelper);
-    this.db = db;
     this.permissionBackend = permissionBackend;
     this.indexer = indexer;
   }
@@ -54,9 +44,9 @@
   @Override
   protected Response<?> applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input)
-      throws IOException, AuthException, OrmException, PermissionBackendException {
+      throws IOException, AuthException, PermissionBackendException {
     permissionBackend.currentUser().check(GlobalPermission.MAINTAIN_SERVER);
-    indexer.index(db.get(), rsrc.getChange());
+    indexer.index(rsrc.getChange());
     return Response.none();
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/ListChangeComments.java b/java/com/google/gerrit/server/restapi/change/ListChangeComments.java
index 72c5589..403922d 100644
--- a/java/com/google/gerrit/server/restapi/change/ListChangeComments.java
+++ b/java/com/google/gerrit/server/restapi/change/ListChangeComments.java
@@ -15,31 +15,27 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 @Singleton
 public class ListChangeComments extends ListChangeDrafts {
-
   @Inject
   ListChangeComments(
-      Provider<ReviewDb> db,
       ChangeData.Factory changeDataFactory,
       Provider<CommentJson> commentJson,
       CommentsUtil commentsUtil) {
-    super(db, changeDataFactory, commentJson, commentsUtil);
+    super(changeDataFactory, commentJson, commentsUtil);
   }
 
   @Override
-  protected Iterable<Comment> listComments(ChangeResource rsrc) throws OrmException {
-    ChangeData cd = changeDataFactory.create(db.get(), rsrc.getNotes());
-    return commentsUtil.publishedByChange(db.get(), cd.notes());
+  protected Iterable<Comment> listComments(ChangeResource rsrc) {
+    ChangeData cd = changeDataFactory.create(rsrc.getNotes());
+    return commentsUtil.publishedByChange(cd.notes());
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/restapi/change/ListChangeDrafts.java b/java/com/google/gerrit/server/restapi/change/ListChangeDrafts.java
index 93e9284..f8fda73 100644
--- a/java/com/google/gerrit/server/restapi/change/ListChangeDrafts.java
+++ b/java/com/google/gerrit/server/restapi/change/ListChangeDrafts.java
@@ -18,13 +18,11 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.restapi.change.CommentJson.CommentFormatter;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -33,26 +31,23 @@
 
 @Singleton
 public class ListChangeDrafts implements RestReadView<ChangeResource> {
-  protected final Provider<ReviewDb> db;
   protected final ChangeData.Factory changeDataFactory;
   protected final Provider<CommentJson> commentJson;
   protected final CommentsUtil commentsUtil;
 
   @Inject
   ListChangeDrafts(
-      Provider<ReviewDb> db,
       ChangeData.Factory changeDataFactory,
       Provider<CommentJson> commentJson,
       CommentsUtil commentsUtil) {
-    this.db = db;
     this.changeDataFactory = changeDataFactory;
     this.commentJson = commentJson;
     this.commentsUtil = commentsUtil;
   }
 
-  protected Iterable<Comment> listComments(ChangeResource rsrc) throws OrmException {
-    ChangeData cd = changeDataFactory.create(db.get(), rsrc.getNotes());
-    return commentsUtil.draftByChangeAuthor(db.get(), cd.notes(), rsrc.getUser().getAccountId());
+  protected Iterable<Comment> listComments(ChangeResource rsrc) {
+    ChangeData cd = changeDataFactory.create(rsrc.getNotes());
+    return commentsUtil.draftByChangeAuthor(cd.notes(), rsrc.getUser().getAccountId());
   }
 
   protected boolean includeAuthorInfo() {
@@ -65,7 +60,7 @@
 
   @Override
   public Map<String, List<CommentInfo>> apply(ChangeResource rsrc)
-      throws AuthException, OrmException, PermissionBackendException {
+      throws AuthException, PermissionBackendException {
     if (requireAuthentication() && !rsrc.getUser().isIdentifiedUser()) {
       throw new AuthException("Authentication required");
     }
@@ -73,7 +68,7 @@
   }
 
   public List<CommentInfo> getComments(ChangeResource rsrc)
-      throws AuthException, OrmException, PermissionBackendException {
+      throws AuthException, PermissionBackendException {
     if (requireAuthentication() && !rsrc.getUser().isIdentifiedUser()) {
       throw new AuthException("Authentication required");
     }
diff --git a/java/com/google/gerrit/server/restapi/change/ListChangeMessages.java b/java/com/google/gerrit/server/restapi/change/ListChangeMessages.java
index 9ce2157..a2e3d4b 100644
--- a/java/com/google/gerrit/server/restapi/change/ListChangeMessages.java
+++ b/java/com/google/gerrit/server/restapi/change/ListChangeMessages.java
@@ -19,39 +19,30 @@
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.util.List;
 import java.util.stream.Collectors;
 
 @Singleton
 public class ListChangeMessages implements RestReadView<ChangeResource> {
-  private final Provider<ReviewDb> dbProvider;
   private final ChangeMessagesUtil changeMessagesUtil;
   private final AccountLoader accountLoader;
 
   @Inject
   public ListChangeMessages(
-      Provider<ReviewDb> dbProvider,
-      ChangeMessagesUtil changeMessagesUtil,
-      AccountLoader.Factory accountLoaderFactory) {
-    this.dbProvider = dbProvider;
+      ChangeMessagesUtil changeMessagesUtil, AccountLoader.Factory accountLoaderFactory) {
     this.changeMessagesUtil = changeMessagesUtil;
     this.accountLoader = accountLoaderFactory.create(true);
   }
 
   @Override
-  public List<ChangeMessageInfo> apply(ChangeResource resource)
-      throws OrmException, PermissionBackendException {
-    List<ChangeMessage> messages =
-        changeMessagesUtil.byChange(dbProvider.get(), resource.getNotes());
+  public List<ChangeMessageInfo> apply(ChangeResource resource) throws PermissionBackendException {
+    List<ChangeMessage> messages = changeMessagesUtil.byChange(resource.getNotes());
     List<ChangeMessageInfo> messageInfos =
         messages.stream()
             .map(m -> createChangeMessageInfo(m, accountLoader))
diff --git a/java/com/google/gerrit/server/restapi/change/ListChangeRobotComments.java b/java/com/google/gerrit/server/restapi/change/ListChangeRobotComments.java
index dc92ced..e5840fd 100644
--- a/java/com/google/gerrit/server/restapi/change/ListChangeRobotComments.java
+++ b/java/com/google/gerrit/server/restapi/change/ListChangeRobotComments.java
@@ -17,30 +17,25 @@
 import com.google.gerrit.extensions.common.RobotCommentInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.util.List;
 import java.util.Map;
 
 public class ListChangeRobotComments implements RestReadView<ChangeResource> {
-  private final Provider<ReviewDb> db;
   private final ChangeData.Factory changeDataFactory;
   private final Provider<CommentJson> commentJson;
   private final CommentsUtil commentsUtil;
 
   @Inject
   ListChangeRobotComments(
-      Provider<ReviewDb> db,
       ChangeData.Factory changeDataFactory,
       Provider<CommentJson> commentJson,
       CommentsUtil commentsUtil) {
-    this.db = db;
     this.changeDataFactory = changeDataFactory;
     this.commentJson = commentJson;
     this.commentsUtil = commentsUtil;
@@ -48,8 +43,8 @@
 
   @Override
   public Map<String, List<RobotCommentInfo>> apply(ChangeResource rsrc)
-      throws AuthException, OrmException, PermissionBackendException {
-    ChangeData cd = changeDataFactory.create(db.get(), rsrc.getNotes());
+      throws AuthException, PermissionBackendException {
+    ChangeData cd = changeDataFactory.create(rsrc.getNotes());
     return commentJson
         .get()
         .setFillAccounts(true)
diff --git a/java/com/google/gerrit/server/restapi/change/ListReviewers.java b/java/com/google/gerrit/server/restapi/change/ListReviewers.java
index 3fb4b02..12732ff 100644
--- a/java/com/google/gerrit/server/restapi/change/ListReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/ListReviewers.java
@@ -18,15 +18,12 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.ReviewerJson;
 import com.google.gerrit.server.change.ReviewerResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -34,29 +31,22 @@
 
 @Singleton
 public class ListReviewers implements RestReadView<ChangeResource> {
-  private final Provider<ReviewDb> dbProvider;
   private final ApprovalsUtil approvalsUtil;
   private final ReviewerJson json;
   private final ReviewerResource.Factory resourceFactory;
 
   @Inject
   ListReviewers(
-      Provider<ReviewDb> dbProvider,
-      ApprovalsUtil approvalsUtil,
-      ReviewerResource.Factory resourceFactory,
-      ReviewerJson json) {
-    this.dbProvider = dbProvider;
+      ApprovalsUtil approvalsUtil, ReviewerResource.Factory resourceFactory, ReviewerJson json) {
     this.approvalsUtil = approvalsUtil;
     this.resourceFactory = resourceFactory;
     this.json = json;
   }
 
   @Override
-  public List<ReviewerInfo> apply(ChangeResource rsrc)
-      throws OrmException, PermissionBackendException {
+  public List<ReviewerInfo> apply(ChangeResource rsrc) throws PermissionBackendException {
     Map<String, ReviewerResource> reviewers = new LinkedHashMap<>();
-    ReviewDb db = dbProvider.get();
-    for (Account.Id accountId : approvalsUtil.getReviewers(db, rsrc.getNotes()).all()) {
+    for (Account.Id accountId : approvalsUtil.getReviewers(rsrc.getNotes()).all()) {
       if (!reviewers.containsKey(accountId.toString())) {
         reviewers.put(accountId.toString(), resourceFactory.create(rsrc, accountId));
       }
diff --git a/java/com/google/gerrit/server/restapi/change/ListRevisionComments.java b/java/com/google/gerrit/server/restapi/change/ListRevisionComments.java
index 964e560..b39ba63 100644
--- a/java/com/google/gerrit/server/restapi/change/ListRevisionComments.java
+++ b/java/com/google/gerrit/server/restapi/change/ListRevisionComments.java
@@ -15,11 +15,9 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -27,9 +25,8 @@
 @Singleton
 public class ListRevisionComments extends ListRevisionDrafts {
   @Inject
-  ListRevisionComments(
-      Provider<ReviewDb> db, Provider<CommentJson> commentJson, CommentsUtil commentsUtil) {
-    super(db, commentJson, commentsUtil);
+  ListRevisionComments(Provider<CommentJson> commentJson, CommentsUtil commentsUtil) {
+    super(commentJson, commentsUtil);
   }
 
   @Override
@@ -38,8 +35,8 @@
   }
 
   @Override
-  protected Iterable<Comment> listComments(RevisionResource rsrc) throws OrmException {
+  protected Iterable<Comment> listComments(RevisionResource rsrc) {
     ChangeNotes notes = rsrc.getNotes();
-    return commentsUtil.publishedByPatchSet(db.get(), notes, rsrc.getPatchSet().getId());
+    return commentsUtil.publishedByPatchSet(notes, rsrc.getPatchSet().getId());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/ListRevisionDrafts.java b/java/com/google/gerrit/server/restapi/change/ListRevisionDrafts.java
index dbd0ccf..a46bd6c 100644
--- a/java/com/google/gerrit/server/restapi/change/ListRevisionDrafts.java
+++ b/java/com/google/gerrit/server/restapi/change/ListRevisionDrafts.java
@@ -18,11 +18,9 @@
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -31,21 +29,18 @@
 
 @Singleton
 public class ListRevisionDrafts implements RestReadView<RevisionResource> {
-  protected final Provider<ReviewDb> db;
   protected final Provider<CommentJson> commentJson;
   protected final CommentsUtil commentsUtil;
 
   @Inject
-  ListRevisionDrafts(
-      Provider<ReviewDb> db, Provider<CommentJson> commentJson, CommentsUtil commentsUtil) {
-    this.db = db;
+  ListRevisionDrafts(Provider<CommentJson> commentJson, CommentsUtil commentsUtil) {
     this.commentJson = commentJson;
     this.commentsUtil = commentsUtil;
   }
 
-  protected Iterable<Comment> listComments(RevisionResource rsrc) throws OrmException {
+  protected Iterable<Comment> listComments(RevisionResource rsrc) {
     return commentsUtil.draftByPatchSetAuthor(
-        db.get(), rsrc.getPatchSet().getId(), rsrc.getAccountId(), rsrc.getNotes());
+        rsrc.getPatchSet().getId(), rsrc.getAccountId(), rsrc.getNotes());
   }
 
   protected boolean includeAuthorInfo() {
@@ -54,7 +49,7 @@
 
   @Override
   public Map<String, List<CommentInfo>> apply(RevisionResource rsrc)
-      throws OrmException, PermissionBackendException {
+      throws PermissionBackendException {
     return commentJson
         .get()
         .setFillAccounts(includeAuthorInfo())
@@ -63,7 +58,7 @@
   }
 
   public ImmutableList<CommentInfo> getComments(RevisionResource rsrc)
-      throws OrmException, PermissionBackendException {
+      throws PermissionBackendException {
     return commentJson
         .get()
         .setFillAccounts(includeAuthorInfo())
diff --git a/java/com/google/gerrit/server/restapi/change/ListRevisionReviewers.java b/java/com/google/gerrit/server/restapi/change/ListRevisionReviewers.java
index 7add548..920cde9 100644
--- a/java/com/google/gerrit/server/restapi/change/ListRevisionReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/ListRevisionReviewers.java
@@ -19,15 +19,12 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.change.ReviewerJson;
 import com.google.gerrit.server.change.ReviewerResource;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -35,18 +32,13 @@
 
 @Singleton
 class ListRevisionReviewers implements RestReadView<RevisionResource> {
-  private final Provider<ReviewDb> dbProvider;
   private final ApprovalsUtil approvalsUtil;
   private final ReviewerJson json;
   private final ReviewerResource.Factory resourceFactory;
 
   @Inject
   ListRevisionReviewers(
-      Provider<ReviewDb> dbProvider,
-      ApprovalsUtil approvalsUtil,
-      ReviewerResource.Factory resourceFactory,
-      ReviewerJson json) {
-    this.dbProvider = dbProvider;
+      ApprovalsUtil approvalsUtil, ReviewerResource.Factory resourceFactory, ReviewerJson json) {
     this.approvalsUtil = approvalsUtil;
     this.resourceFactory = resourceFactory;
     this.json = json;
@@ -54,14 +46,13 @@
 
   @Override
   public List<ReviewerInfo> apply(RevisionResource rsrc)
-      throws OrmException, MethodNotAllowedException, PermissionBackendException {
+      throws MethodNotAllowedException, PermissionBackendException {
     if (!rsrc.isCurrent()) {
       throw new MethodNotAllowedException("Cannot list reviewers on non-current patch set");
     }
 
     Map<String, ReviewerResource> reviewers = new LinkedHashMap<>();
-    ReviewDb db = dbProvider.get();
-    for (Account.Id accountId : approvalsUtil.getReviewers(db, rsrc.getNotes()).all()) {
+    for (Account.Id accountId : approvalsUtil.getReviewers(rsrc.getNotes()).all()) {
       if (!reviewers.containsKey(accountId.toString())) {
         reviewers.put(accountId.toString(), resourceFactory.create(rsrc, accountId));
       }
diff --git a/java/com/google/gerrit/server/restapi/change/ListRobotComments.java b/java/com/google/gerrit/server/restapi/change/ListRobotComments.java
index 99366aa..bbf46a3 100644
--- a/java/com/google/gerrit/server/restapi/change/ListRobotComments.java
+++ b/java/com/google/gerrit/server/restapi/change/ListRobotComments.java
@@ -18,11 +18,9 @@
 import com.google.gerrit.extensions.common.RobotCommentInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.RobotComment;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -31,21 +29,18 @@
 
 @Singleton
 public class ListRobotComments implements RestReadView<RevisionResource> {
-  protected final Provider<ReviewDb> db;
   protected final Provider<CommentJson> commentJson;
   protected final CommentsUtil commentsUtil;
 
   @Inject
-  ListRobotComments(
-      Provider<ReviewDb> db, Provider<CommentJson> commentJson, CommentsUtil commentsUtil) {
-    this.db = db;
+  ListRobotComments(Provider<CommentJson> commentJson, CommentsUtil commentsUtil) {
     this.commentJson = commentJson;
     this.commentsUtil = commentsUtil;
   }
 
   @Override
   public Map<String, List<RobotCommentInfo>> apply(RevisionResource rsrc)
-      throws OrmException, PermissionBackendException {
+      throws PermissionBackendException {
     return commentJson
         .get()
         .setFillAccounts(true)
@@ -54,7 +49,7 @@
   }
 
   public ImmutableList<RobotCommentInfo> getComments(RevisionResource rsrc)
-      throws OrmException, PermissionBackendException {
+      throws PermissionBackendException {
     return commentJson
         .get()
         .setFillAccounts(true)
@@ -62,7 +57,7 @@
         .formatAsList(listComments(rsrc));
   }
 
-  private Iterable<RobotComment> listComments(RevisionResource rsrc) throws OrmException {
+  private Iterable<RobotComment> listComments(RevisionResource rsrc) {
     return commentsUtil.robotCommentsByPatchSet(rsrc.getNotes(), rsrc.getPatchSet().getId());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/MarkAsReviewed.java b/java/com/google/gerrit/server/restapi/change/MarkAsReviewed.java
index 7c9ba73..4c942d2 100644
--- a/java/com/google/gerrit/server/restapi/change/MarkAsReviewed.java
+++ b/java/com/google/gerrit/server/restapi/change/MarkAsReviewed.java
@@ -15,19 +15,17 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.StarredChangesUtil.IllegalLabelException;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 @Singleton
@@ -35,16 +33,11 @@
     implements RestModifyView<ChangeResource, Input>, UiAction<ChangeResource> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  private final Provider<ReviewDb> dbProvider;
   private final ChangeData.Factory changeDataFactory;
   private final StarredChangesUtil stars;
 
   @Inject
-  MarkAsReviewed(
-      Provider<ReviewDb> dbProvider,
-      ChangeData.Factory changeDataFactory,
-      StarredChangesUtil stars) {
-    this.dbProvider = dbProvider;
+  MarkAsReviewed(ChangeData.Factory changeDataFactory, StarredChangesUtil stars) {
     this.changeDataFactory = changeDataFactory;
     this.stars = stars;
   }
@@ -59,7 +52,7 @@
 
   @Override
   public Response<String> apply(ChangeResource rsrc, Input input)
-      throws RestApiException, OrmException, IllegalLabelException {
+      throws RestApiException, IllegalLabelException {
     stars.markAsReviewed(rsrc);
     return Response.ok("");
   }
@@ -67,9 +60,9 @@
   private boolean isReviewed(ChangeResource rsrc) {
     try {
       return changeDataFactory
-          .create(dbProvider.get(), rsrc.getNotes())
+          .create(rsrc.getNotes())
           .isReviewedBy(rsrc.getUser().asIdentifiedUser().getAccountId());
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("failed to check if change is reviewed");
     }
     return false;
diff --git a/java/com/google/gerrit/server/restapi/change/MarkAsUnreviewed.java b/java/com/google/gerrit/server/restapi/change/MarkAsUnreviewed.java
index 6e15dcc..5945b14 100644
--- a/java/com/google/gerrit/server/restapi/change/MarkAsUnreviewed.java
+++ b/java/com/google/gerrit/server/restapi/change/MarkAsUnreviewed.java
@@ -15,18 +15,16 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.StarredChangesUtil.IllegalLabelException;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 @Singleton
@@ -34,16 +32,11 @@
     implements RestModifyView<ChangeResource, Input>, UiAction<ChangeResource> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  private final Provider<ReviewDb> dbProvider;
   private final ChangeData.Factory changeDataFactory;
   private final StarredChangesUtil stars;
 
   @Inject
-  MarkAsUnreviewed(
-      Provider<ReviewDb> dbProvider,
-      ChangeData.Factory changeDataFactory,
-      StarredChangesUtil stars) {
-    this.dbProvider = dbProvider;
+  MarkAsUnreviewed(ChangeData.Factory changeDataFactory, StarredChangesUtil stars) {
     this.changeDataFactory = changeDataFactory;
     this.stars = stars;
   }
@@ -57,8 +50,7 @@
   }
 
   @Override
-  public Response<String> apply(ChangeResource rsrc, Input input)
-      throws OrmException, IllegalLabelException {
+  public Response<String> apply(ChangeResource rsrc, Input input) throws IllegalLabelException {
     stars.markAsUnreviewed(rsrc);
     return Response.ok("");
   }
@@ -66,9 +58,9 @@
   private boolean isReviewed(ChangeResource rsrc) {
     try {
       return changeDataFactory
-          .create(dbProvider.get(), rsrc.getNotes())
+          .create(rsrc.getNotes())
           .isReviewedBy(rsrc.getUser().asIdentifiedUser().getAccountId());
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("failed to check if change is reviewed");
     }
     return false;
diff --git a/java/com/google/gerrit/server/restapi/change/Mergeable.java b/java/com/google/gerrit/server/restapi/change/Mergeable.java
index b196347..ece8938 100644
--- a/java/com/google/gerrit/server/restapi/change/Mergeable.java
+++ b/java/com/google/gerrit/server/restapi/change/Mergeable.java
@@ -16,6 +16,7 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.SubmitTypeRecord;
+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;
@@ -24,8 +25,6 @@
 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.reviewdb.server.ReviewDb;
-import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.change.MergeabilityCache;
 import com.google.gerrit.server.change.RevisionResource;
@@ -38,12 +37,9 @@
 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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.Future;
@@ -66,7 +62,6 @@
   private final ProjectCache projectCache;
   private final MergeUtil.Factory mergeUtilFactory;
   private final ChangeData.Factory changeDataFactory;
-  private final Provider<ReviewDb> db;
   private final ChangeIndexer indexer;
   private final MergeabilityCache cache;
   private final SubmitRuleEvaluator submitRuleEvaluator;
@@ -77,7 +72,6 @@
       ProjectCache projectCache,
       MergeUtil.Factory mergeUtilFactory,
       ChangeData.Factory changeDataFactory,
-      Provider<ReviewDb> db,
       ChangeIndexer indexer,
       MergeabilityCache cache,
       SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory) {
@@ -85,7 +79,6 @@
     this.projectCache = projectCache;
     this.mergeUtilFactory = mergeUtilFactory;
     this.changeDataFactory = changeDataFactory;
-    this.db = db;
     this.indexer = indexer;
     this.cache = cache;
     submitRuleEvaluator = submitRuleEvaluatorFactory.create(SubmitRuleOptions.defaults());
@@ -97,20 +90,19 @@
 
   @Override
   public MergeableInfo apply(RevisionResource resource)
-      throws AuthException, ResourceConflictException, BadRequestException, OrmException,
-          IOException {
+      throws AuthException, ResourceConflictException, BadRequestException, IOException {
     Change change = resource.getChange();
     PatchSet ps = resource.getPatchSet();
     MergeableInfo result = new MergeableInfo();
 
-    if (!change.getStatus().isOpen()) {
+    if (!change.isNew()) {
       throw new ResourceConflictException("change is " + ChangeUtil.status(change));
     } else if (!ps.getId().equals(change.currentPatchSetId())) {
       // Only the current revision is mergeable. Others always fail.
       return result;
     }
 
-    ChangeData cd = changeDataFactory.create(db.get(), resource.getNotes());
+    ChangeData cd = changeDataFactory.create(resource.getNotes());
     result.submitType = getSubmitType(cd);
 
     try (Repository git = gitManager.openRepository(change.getProject())) {
@@ -143,10 +135,10 @@
     return result;
   }
 
-  private SubmitType getSubmitType(ChangeData cd) throws OrmException {
+  private SubmitType getSubmitType(ChangeData cd) {
     SubmitTypeRecord rec = submitRuleEvaluator.getSubmitType(cd);
     if (rec.status != SubmitTypeRecord.Status.OK) {
-      throw new OrmException("Submit type rule failed: " + rec);
+      throw new StorageException("Submit type rule failed: " + rec);
     }
     return rec.type;
   }
@@ -157,8 +149,7 @@
       ObjectId commit,
       Ref ref,
       SubmitType submitType,
-      String strategy)
-      throws OrmException {
+      String strategy) {
     if (commit == null) {
       return false;
     }
@@ -186,25 +177,14 @@
       SubmitType type,
       String strategy,
       Repository git,
-      Boolean old)
-      throws OrmException {
-    final boolean mergeable = cache.get(commit, ref, type, strategy, change.getDest(), git);
+      Boolean old) {
+    boolean mergeable = cache.get(commit, ref, type, strategy, change.getDest(), git);
+    // TODO(dborowitz): Include something else in the change ETag that it's possible to bump here,
+    // such as cache or secondary index update time.
     if (!Objects.equals(mergeable, old)) {
-      invalidateETag(change.getId(), db.get());
-
       @SuppressWarnings("unused")
       Future<?> possiblyIgnoredError = indexer.indexAsync(change.getProject(), change.getId());
     }
     return mergeable;
   }
-
-  private static void invalidateETag(Change.Id id, ReviewDb db) throws OrmException {
-    // Empty update of Change to bump rowVersion, changing its ETag.
-    // TODO(dborowitz): Include cache info in ETag somehow instead.
-    db = ReviewDbUtil.unwrapDb(db);
-    Change c = db.changes().get(id);
-    if (c != null) {
-      db.changes().update(Collections.singleton(c));
-    }
-  }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/Module.java b/java/com/google/gerrit/server/restapi/change/Module.java
index 8824247..a57bd64 100644
--- a/java/com/google/gerrit/server/restapi/change/Module.java
+++ b/java/com/google/gerrit/server/restapi/change/Module.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.server.change.AddReviewersOp;
 import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.DeleteChangeOp;
 import com.google.gerrit.server.change.DeleteReviewerByEmailOp;
 import com.google.gerrit.server.change.DeleteReviewerOp;
 import com.google.gerrit.server.change.EmailReviewComments;
@@ -99,7 +100,6 @@
     get(CHANGE_KIND, "submitted_together").to(SubmittedTogether.class);
     post(CHANGE_KIND, "rebase").to(Rebase.CurrentRevision.class);
     post(CHANGE_KIND, "index").to(Index.class);
-    post(CHANGE_KIND, "rebuild.notedb").to(Rebuild.class);
     post(CHANGE_KIND, "move").to(Move.class);
     post(CHANGE_KIND, "private").to(PostPrivate.class);
     post(CHANGE_KIND, "private.delete").to(DeletePrivateByPost.class);
@@ -190,6 +190,7 @@
     factory(AccountLoader.Factory.class);
     factory(ChangeInserter.Factory.class);
     factory(ChangeResource.Factory.class);
+    factory(DeleteChangeOp.Factory.class);
     factory(DeleteReviewerByEmailOp.Factory.class);
     factory(DeleteReviewerOp.Factory.class);
     factory(EmailReviewComments.Factory.class);
diff --git a/java/com/google/gerrit/server/restapi/change/Move.java b/java/com/google/gerrit/server/restapi/change/Move.java
index 013d3e9..f2335b1 100644
--- a/java/com/google/gerrit/server/restapi/change/Move.java
+++ b/java/com/google/gerrit/server/restapi/change/Move.java
@@ -23,23 +23,23 @@
 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.api.changes.MoveInput;
 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.MethodNotAllowedException;
 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.Branch;
 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.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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
@@ -47,6 +47,7 @@
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -61,13 +62,13 @@
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -79,7 +80,6 @@
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final PermissionBackend permissionBackend;
-  private final Provider<ReviewDb> dbProvider;
   private final ChangeJson.Factory json;
   private final GitRepositoryManager repoManager;
   private final Provider<InternalChangeQuery> queryProvider;
@@ -87,11 +87,11 @@
   private final PatchSetUtil psUtil;
   private final ApprovalsUtil approvalsUtil;
   private final ProjectCache projectCache;
+  private final boolean moveEnabled;
 
   @Inject
   Move(
       PermissionBackend permissionBackend,
-      Provider<ReviewDb> dbProvider,
       ChangeJson.Factory json,
       GitRepositoryManager repoManager,
       Provider<InternalChangeQuery> queryProvider,
@@ -99,10 +99,10 @@
       RetryHelper retryHelper,
       PatchSetUtil psUtil,
       ApprovalsUtil approvalsUtil,
-      ProjectCache projectCache) {
+      ProjectCache projectCache,
+      @GerritServerConfig Config gerritConfig) {
     super(retryHelper);
     this.permissionBackend = permissionBackend;
-    this.dbProvider = dbProvider;
     this.json = json;
     this.repoManager = repoManager;
     this.queryProvider = queryProvider;
@@ -110,13 +110,19 @@
     this.psUtil = psUtil;
     this.approvalsUtil = approvalsUtil;
     this.projectCache = projectCache;
+    this.moveEnabled = gerritConfig.getBoolean("change", null, "move", true);
   }
 
   @Override
   protected ChangeInfo applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, MoveInput input)
-      throws RestApiException, OrmException, UpdateException, PermissionBackendException,
-          IOException {
+      throws RestApiException, UpdateException, PermissionBackendException, IOException {
+    if (!moveEnabled) {
+      // This will be removed with the above config once we reach consensus for the move change
+      // behavior. See: https://bugs.chromium.org/p/gerrit/issues/detail?id=9877
+      throw new MethodNotAllowedException("move changes endpoint is disabled");
+    }
+
     Change change = rsrc.getChange();
     Project.NameKey project = rsrc.getProject();
     IdentifiedUser caller = rsrc.getUser().asIdentifiedUser();
@@ -125,7 +131,7 @@
     }
     input.destinationBranch = RefNames.fullName(input.destinationBranch);
 
-    if (change.getStatus().isClosed()) {
+    if (!change.isNew()) {
       throw new ResourceConflictException("Change is " + ChangeUtil.status(change));
     }
 
@@ -139,16 +145,15 @@
 
     // Move requires abandoning this change, and creating a new change.
     try {
-      rsrc.permissions().database(dbProvider).check(ABANDON);
-      permissionBackend.user(caller).database(dbProvider).ref(newDest).check(CREATE_CHANGE);
+      rsrc.permissions().check(ABANDON);
+      permissionBackend.user(caller).ref(newDest).check(CREATE_CHANGE);
     } catch (AuthException denied) {
       throw new AuthException("move not permitted", denied);
     }
     projectCache.checkedGet(project).checkStatePermitsWrite();
 
     Op op = new Op(input);
-    try (BatchUpdate u =
-        updateFactory.create(dbProvider.get(), project, caller, TimeUtil.nowTs())) {
+    try (BatchUpdate u = updateFactory.create(project, caller, TimeUtil.nowTs())) {
       u.addOp(change.getId(), op);
       u.execute();
     }
@@ -171,10 +176,9 @@
     }
 
     @Override
-    public boolean updateChange(ChangeContext ctx)
-        throws OrmException, ResourceConflictException, IOException {
+    public boolean updateChange(ChangeContext ctx) throws ResourceConflictException, IOException {
       change = ctx.getChange();
-      if (change.getStatus() != Status.NEW) {
+      if (!change.isNew()) {
         throw new ResourceConflictException("Change is " + ChangeUtil.status(change));
       }
 
@@ -190,8 +194,7 @@
           RevWalk revWalk = new RevWalk(repo)) {
         RevCommit currPatchsetRevCommit =
             revWalk.parseCommit(
-                ObjectId.fromString(
-                    psUtil.current(ctx.getDb(), ctx.getNotes()).getRevision().get()));
+                ObjectId.fromString(psUtil.current(ctx.getNotes()).getRevision().get()));
         if (currPatchsetRevCommit.getParentCount() > 1) {
           throw new ResourceConflictException("Merge commit cannot be moved");
         }
@@ -240,7 +243,7 @@
       }
       ChangeMessage cmsg =
           ChangeMessagesUtil.newMessage(ctx, msgBuf.toString(), ChangeMessagesUtil.TAG_MOVE);
-      cmUtil.addChangeMessage(ctx.getDb(), update, cmsg);
+      cmUtil.addChangeMessage(update, cmsg);
 
       return true;
     }
@@ -253,11 +256,11 @@
      */
     private void updateApprovals(
         ChangeContext ctx, ChangeUpdate update, PatchSet.Id psId, Project.NameKey project)
-        throws IOException, OrmException {
+        throws IOException {
       List<PatchSetApproval> approvals = new ArrayList<>();
       for (PatchSetApproval psa :
           approvalsUtil.byPatchSet(
-              ctx.getDb(), ctx.getNotes(), psId, ctx.getRevWalk(), ctx.getRepoView().getConfig())) {
+              ctx.getNotes(), psId, ctx.getRevWalk(), ctx.getRepoView().getConfig())) {
         ProjectState projectState = projectCache.checkedGet(project);
         LabelType type = projectState.getLabelTypes(ctx.getNotes()).byLabel(psa.getLabelId());
         // Only keep veto votes, defined as votes where:
@@ -275,8 +278,6 @@
                 (short) 0,
                 ctx.getWhen()));
       }
-      // Remove votes from ReviewDb.
-      ctx.getDb().patchSetApprovals().upsert(approvals);
     }
   }
 
@@ -289,7 +290,7 @@
             .setVisible(false);
 
     Change change = rsrc.getChange();
-    if (!change.getStatus().isOpen()) {
+    if (!change.isNew()) {
       return description;
     }
 
@@ -307,7 +308,7 @@
       if (psUtil.isPatchSetLocked(rsrc.getNotes())) {
         return description;
       }
-    } catch (OrmException | IOException e) {
+    } catch (StorageException | IOException e) {
       logger.atSevere().withCause(e).log(
           "Failed to check if the current patch set of change %s is locked", change.getId());
       return description;
@@ -316,6 +317,6 @@
     return description.setVisible(
         and(
             permissionBackend.user(rsrc.getUser()).ref(change.getDest()).testCond(CREATE_CHANGE),
-            rsrc.permissions().database(dbProvider).testCond(ABANDON)));
+            rsrc.permissions().testCond(ABANDON)));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/PostHashtags.java b/java/com/google/gerrit/server/restapi/change/PostHashtags.java
index c67fb67..da499a9 100644
--- a/java/com/google/gerrit/server/restapi/change/PostHashtags.java
+++ b/java/com/google/gerrit/server/restapi/change/PostHashtags.java
@@ -19,7 +19,6 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.SetHashtagsOp;
 import com.google.gerrit.server.permissions.ChangePermission;
@@ -30,7 +29,6 @@
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 @Singleton
@@ -38,14 +36,11 @@
     extends RetryingRestModifyView<
         ChangeResource, HashtagsInput, Response<ImmutableSortedSet<String>>>
     implements UiAction<ChangeResource> {
-  private final Provider<ReviewDb> db;
   private final SetHashtagsOp.Factory hashtagsFactory;
 
   @Inject
-  PostHashtags(
-      Provider<ReviewDb> db, RetryHelper retryHelper, SetHashtagsOp.Factory hashtagsFactory) {
+  PostHashtags(RetryHelper retryHelper, SetHashtagsOp.Factory hashtagsFactory) {
     super(retryHelper);
-    this.db = db;
     this.hashtagsFactory = hashtagsFactory;
   }
 
@@ -56,12 +51,11 @@
     req.permissions().check(ChangePermission.EDIT_HASHTAGS);
 
     try (BatchUpdate bu =
-        updateFactory.create(
-            db.get(), req.getChange().getProject(), req.getUser(), TimeUtil.nowTs())) {
+        updateFactory.create(req.getChange().getProject(), req.getUser(), TimeUtil.nowTs())) {
       SetHashtagsOp op = hashtagsFactory.create(input);
       bu.addOp(req.getId(), op);
       bu.execute();
-      return Response.<ImmutableSortedSet<String>>ok(op.getUpdatedHashtags());
+      return Response.ok(op.getUpdatedHashtags());
     }
   }
 
diff --git a/java/com/google/gerrit/server/restapi/change/PostPrivate.java b/java/com/google/gerrit/server/restapi/change/PostPrivate.java
index a506624..5aa2ecc 100644
--- a/java/com/google/gerrit/server/restapi/change/PostPrivate.java
+++ b/java/com/google/gerrit/server/restapi/change/PostPrivate.java
@@ -24,8 +24,6 @@
 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.server.ReviewDb;
-import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.SetPrivateOp;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -37,7 +35,6 @@
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import org.eclipse.jgit.lib.Config;
 
@@ -45,23 +42,17 @@
 public class PostPrivate
     extends RetryingRestModifyView<ChangeResource, SetPrivateOp.Input, Response<String>>
     implements UiAction<ChangeResource> {
-  private final ChangeMessagesUtil cmUtil;
-  private final Provider<ReviewDb> dbProvider;
   private final PermissionBackend permissionBackend;
   private final SetPrivateOp.Factory setPrivateOpFactory;
   private final boolean disablePrivateChanges;
 
   @Inject
   PostPrivate(
-      Provider<ReviewDb> dbProvider,
       RetryHelper retryHelper,
-      ChangeMessagesUtil cmUtil,
       PermissionBackend permissionBackend,
       SetPrivateOp.Factory setPrivateOpFactory,
       @GerritServerConfig Config config) {
     super(retryHelper);
-    this.dbProvider = dbProvider;
-    this.cmUtil = cmUtil;
     this.permissionBackend = permissionBackend;
     this.setPrivateOpFactory = setPrivateOpFactory;
     this.disablePrivateChanges = config.getBoolean("change", null, "disablePrivateChanges", false);
@@ -83,10 +74,9 @@
       return Response.ok("");
     }
 
-    SetPrivateOp op = setPrivateOpFactory.create(cmUtil, true, input);
+    SetPrivateOp op = setPrivateOpFactory.create(true, input);
     try (BatchUpdate u =
-        updateFactory.create(
-            dbProvider.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+        updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
       u.addOp(rsrc.getId(), op).execute();
     }
 
@@ -99,13 +89,16 @@
     return new UiAction.Description()
         .setLabel("Mark private")
         .setTitle("Mark change as private")
-        .setVisible(and(!disablePrivateChanges && !change.isPrivate(), canSetPrivate(rsrc)));
+        .setVisible(
+            and(
+                !disablePrivateChanges && !change.isPrivate() && change.isNew(),
+                canSetPrivate(rsrc)));
   }
 
   private BooleanCondition canSetPrivate(ChangeResource rsrc) {
     PermissionBackend.WithUser user = permissionBackend.user(rsrc.getUser());
     return or(
-        rsrc.isUserOwner() && rsrc.getChange().getStatus() != Change.Status.MERGED,
+        rsrc.isUserOwner() && !rsrc.getChange().isMerged(),
         user.testCond(GlobalPermission.ADMINISTRATE_SERVER));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 4867dd2..4a98a9e 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -29,7 +29,6 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.base.Strings;
-import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Ordering;
@@ -42,7 +41,6 @@
 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.api.changes.RecipientType;
 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;
@@ -58,12 +56,12 @@
 import com.google.gerrit.extensions.common.FixSuggestionInfo;
 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.UnprocessableEntityException;
 import com.google.gerrit.extensions.restapi.Url;
+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;
@@ -77,14 +75,12 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.OutputFormat;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.PublishCommentUtil;
 import com.google.gerrit.server.ReviewerSet;
@@ -92,7 +88,7 @@
 import com.google.gerrit.server.change.AddReviewersEmail;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.EmailReviewComments;
-import com.google.gerrit.server.change.NotifyUtil;
+import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.change.ReviewerAdder;
 import com.google.gerrit.server.change.ReviewerAdder.ReviewerAddition;
 import com.google.gerrit.server.change.RevisionResource;
@@ -101,7 +97,6 @@
 import com.google.gerrit.server.extensions.events.CommentAdded;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.patch.DiffSummary;
 import com.google.gerrit.server.patch.DiffSummaryKey;
 import com.google.gerrit.server.patch.PatchListCache;
@@ -124,9 +119,7 @@
 import com.google.gerrit.server.util.LabelVote;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gson.Gson;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
@@ -160,7 +153,6 @@
   private static final Gson GSON = OutputFormat.JSON_COMPACT.newGson();
   private static final int DEFAULT_ROBOT_COMMENT_SIZE_LIMIT_IN_BYTES = 1024 * 1024;
 
-  private final Provider<ReviewDb> db;
   private final ChangeResource.Factory changeResourceFactory;
   private final ChangeData.Factory changeDataFactory;
   private final ApprovalsUtil approvalsUtil;
@@ -174,8 +166,7 @@
   private final CommentAdded commentAdded;
   private final ReviewerAdder reviewerAdder;
   private final AddReviewersEmail addReviewersEmail;
-  private final NotesMigration migration;
-  private final NotifyUtil notifyUtil;
+  private final NotifyResolver notifyResolver;
   private final Config gerritConfig;
   private final WorkInProgressOp.Factory workInProgressOpFactory;
   private final ProjectCache projectCache;
@@ -184,7 +175,6 @@
 
   @Inject
   PostReview(
-      Provider<ReviewDb> db,
       RetryHelper retryHelper,
       ChangeResource.Factory changeResourceFactory,
       ChangeData.Factory changeDataFactory,
@@ -199,14 +189,12 @@
       CommentAdded commentAdded,
       ReviewerAdder reviewerAdder,
       AddReviewersEmail addReviewersEmail,
-      NotesMigration migration,
-      NotifyUtil notifyUtil,
+      NotifyResolver notifyResolver,
       @GerritServerConfig Config gerritConfig,
       WorkInProgressOp.Factory workInProgressOpFactory,
       ProjectCache projectCache,
       PermissionBackend permissionBackend) {
     super(retryHelper);
-    this.db = db;
     this.changeResourceFactory = changeResourceFactory;
     this.changeDataFactory = changeDataFactory;
     this.commentsUtil = commentsUtil;
@@ -220,8 +208,7 @@
     this.commentAdded = commentAdded;
     this.reviewerAdder = reviewerAdder;
     this.addReviewersEmail = addReviewersEmail;
-    this.migration = migration;
-    this.notifyUtil = notifyUtil;
+    this.notifyResolver = notifyResolver;
     this.gerritConfig = gerritConfig;
     this.workInProgressOpFactory = workInProgressOpFactory;
     this.projectCache = projectCache;
@@ -232,15 +219,15 @@
   @Override
   protected Response<ReviewResult> applyImpl(
       BatchUpdate.Factory updateFactory, RevisionResource revision, ReviewInput input)
-      throws RestApiException, UpdateException, OrmException, IOException,
-          PermissionBackendException, ConfigInvalidException, PatchListNotAvailableException {
+      throws RestApiException, UpdateException, IOException, PermissionBackendException,
+          ConfigInvalidException, PatchListNotAvailableException {
     return apply(updateFactory, revision, input, TimeUtil.nowTs());
   }
 
   public Response<ReviewResult> apply(
       BatchUpdate.Factory updateFactory, RevisionResource revision, ReviewInput input, Timestamp ts)
-      throws RestApiException, UpdateException, OrmException, IOException,
-          PermissionBackendException, ConfigInvalidException, PatchListNotAvailableException {
+      throws RestApiException, UpdateException, IOException, PermissionBackendException,
+          ConfigInvalidException, PatchListNotAvailableException {
     // Respect timestamp, but truncate at change created-on time.
     ts = Ordering.natural().max(ts, revision.getChange().getCreatedOn());
     if (revision.getEdit().isPresent()) {
@@ -260,20 +247,13 @@
       checkComments(revision, input.comments);
     }
     if (input.robotComments != null) {
-      if (!migration.readChanges()) {
-        throw new MethodNotAllowedException("robot comments not supported");
-      }
       checkRobotComments(revision, input.robotComments);
     }
 
-    NotifyHandling reviewerNotify = input.notify;
     if (input.notify == null) {
       input.notify = defaultNotify(revision.getChange(), input);
     }
 
-    ListMultimap<RecipientType, Account.Id> accountsToNotify =
-        notifyUtil.resolveAccounts(input.notifyDetails);
-
     Map<String, AddReviewerResult> reviewerJsonResults = null;
     List<ReviewerAddition> reviewerResults = Lists.newArrayList();
     boolean hasError = false;
@@ -281,15 +261,8 @@
     if (input.reviewers != null) {
       reviewerJsonResults = Maps.newHashMap();
       for (AddReviewerInput reviewerInput : input.reviewers) {
-        // Prevent individual AddReviewersOps from sending one email each. Instead, we call
-        // batchEmailReviewers at the very end to send out a single email.
-        // TODO(dborowitz): I think this still sends out separate emails if any of input.reviewers
-        // specifies explicit accountsToNotify. Unclear whether that's a good thing.
-        reviewerInput.notify = NotifyHandling.NONE;
-
         ReviewerAddition result =
-            reviewerAdder.prepare(
-                db.get(), revision.getNotes(), revision.getUser(), reviewerInput, true);
+            reviewerAdder.prepare(revision.getNotes(), revision.getUser(), reviewerInput, true);
         reviewerJsonResults.put(reviewerInput.reviewer, result.result);
         if (result.result.error != null) {
           hasError = true;
@@ -312,7 +285,7 @@
     output.labels = input.labels;
 
     try (BatchUpdate bu =
-        updateFactory.create(db.get(), revision.getChange().getProject(), revision.getUser(), ts)) {
+        updateFactory.create(revision.getChange().getProject(), revision.getUser(), ts)) {
       Account.Id id = revision.getUser().getAccountId();
       boolean ccOrReviewer = false;
       if (input.labels != null && !input.labels.isEmpty()) {
@@ -322,7 +295,7 @@
       if (!ccOrReviewer) {
         // Check if user was already CCed or reviewing prior to this review.
         ReviewerSet currentReviewers =
-            approvalsUtil.getReviewers(db.get(), revision.getChangeResource().getNotes());
+            approvalsUtil.getReviewers(revision.getChangeResource().getNotes());
         ccOrReviewer = currentReviewers.all().contains(id);
       }
 
@@ -330,6 +303,7 @@
       // updated set of reviewers. Also keep track of whether the user added
       // themselves as a reviewer or to the CC list.
       for (ReviewerAddition reviewerResult : reviewerResults) {
+        reviewerResult.op.suppressEmail(); // Send a single batch email below.
         bu.addOp(revision.getChange().getId(), reviewerResult.op);
         if (!ccOrReviewer && reviewerResult.result.reviewers != null) {
           for (ReviewerInfo reviewerInfo : reviewerResult.result.reviewers) {
@@ -354,6 +328,7 @@
         // isn't being explicitly added, and isn't voting on any label.
         // Automatically CC them on this change so they receive replies.
         ReviewerAddition selfAddition = reviewerAdder.ccCurrentUser(revision.getUser(), revision);
+        selfAddition.op.suppressEmail();
         bu.addOp(revision.getChange().getId(), selfAddition.op);
       }
 
@@ -364,51 +339,57 @@
           return Response.withStatusCode(SC_BAD_REQUEST, output);
         }
 
-        WorkInProgressOp.checkPermissions(
-            permissionBackend, revision.getUser(), revision.getChange());
+        revision
+            .getChangeResource()
+            .permissions()
+            .check(ChangePermission.TOGGLE_WORK_IN_PROGRESS_STATE);
 
         if (input.ready) {
           output.ready = true;
         }
 
-        // Suppress notifications in WorkInProgressOp, we'll take care of
-        // them in this endpoint.
-        WorkInProgressOp.Input wipIn = new WorkInProgressOp.Input();
-        wipIn.notify = NotifyHandling.NONE;
-        bu.addOp(
-            revision.getChange().getId(),
-            workInProgressOpFactory.create(input.workInProgress, wipIn));
+        WorkInProgressOp wipOp =
+            workInProgressOpFactory.create(input.workInProgress, new WorkInProgressOp.Input());
+        wipOp.suppressEmail();
+        bu.addOp(revision.getChange().getId(), wipOp);
       }
 
       // Add the review op.
       bu.addOp(
           revision.getChange().getId(),
-          new Op(projectState, revision.getPatchSet().getId(), input, accountsToNotify));
+          new Op(projectState, revision.getPatchSet().getId(), input));
+
+      // Notify based on ReviewInput, ignoring the notify settings from any AddReviewerInputs.
+      NotifyResolver.Result notify =
+          notifyResolver.resolve(getNotifyHandling(input, output, revision), input.notifyDetails);
+      bu.setNotify(notify);
 
       bu.execute();
 
       // Re-read change to take into account results of the update.
-      ChangeData cd =
-          changeDataFactory.create(db.get(), revision.getProject(), revision.getChange().getId());
+      ChangeData cd = changeDataFactory.create(revision.getProject(), revision.getChange().getId());
       for (ReviewerAddition reviewerResult : reviewerResults) {
         reviewerResult.gatherResults(cd);
       }
 
-      boolean readyForReview =
-          (output.ready != null && output.ready) || !revision.getChange().isWorkInProgress();
       // Sending from AddReviewersOp was suppressed so we can send a single batch email here.
-      batchEmailReviewers(
-          revision.getUser(),
-          revision.getChange(),
-          reviewerResults,
-          reviewerNotify,
-          accountsToNotify,
-          readyForReview);
+      batchEmailReviewers(revision.getUser(), revision.getChange(), reviewerResults, notify);
     }
 
     return Response.ok(output);
   }
 
+  private NotifyHandling getNotifyHandling(
+      ReviewInput input, ReviewResult output, RevisionResource revision) {
+    if (input.notify != null) {
+      return input.notify;
+    }
+    if ((output.ready != null && output.ready) || !revision.getChange().isWorkInProgress()) {
+      return NotifyHandling.ALL;
+    }
+    return NotifyHandling.NONE;
+  }
+
   private NotifyHandling defaultNotify(Change c, ReviewInput in) {
     boolean workInProgress = c.isWorkInProgress();
     if (in.workInProgress) {
@@ -424,11 +405,12 @@
     }
 
     if (workInProgress && !c.hasReviewStarted()) {
-      // If review hasn't started we want to minimize recipients, no matter who
-      // the author is.
-      return NotifyHandling.OWNER;
+      // If review hasn't started we want to eliminate notifications, no matter who the author is.
+      return NotifyHandling.NONE;
     }
 
+    // Otherwise, it's either a non-WIP change, or a WIP change where review has started. Notify
+    // everyone.
     return NotifyHandling.ALL;
   }
 
@@ -436,9 +418,7 @@
       CurrentUser user,
       Change change,
       List<ReviewerAddition> reviewerAdditions,
-      @Nullable NotifyHandling notify,
-      ListMultimap<RecipientType, Account.Id> accountsToNotify,
-      boolean readyForReview) {
+      NotifyResolver.Result notify) {
     List<Account.Id> to = new ArrayList<>();
     List<Account.Id> cc = new ArrayList<>();
     List<Address> toByEmail = new ArrayList<>();
@@ -453,19 +433,11 @@
       }
     }
     addReviewersEmail.emailReviewers(
-        user.asIdentifiedUser(),
-        change,
-        to,
-        cc,
-        toByEmail,
-        ccByEmail,
-        notify,
-        accountsToNotify,
-        readyForReview);
+        user.asIdentifiedUser(), change, to, cc, toByEmail, ccByEmail, notify);
   }
 
   private RevisionResource onBehalfOf(RevisionResource rev, LabelTypes labelTypes, ReviewInput in)
-      throws BadRequestException, AuthException, UnprocessableEntityException, OrmException,
+      throws BadRequestException, AuthException, UnprocessableEntityException,
           PermissionBackendException, IOException, ConfigInvalidException {
     if (in.labels == null || in.labels.isEmpty()) {
       throw new AuthException(
@@ -476,7 +448,7 @@
     }
 
     CurrentUser caller = rev.getUser();
-    PermissionBackend.ForChange perm = rev.permissions().database(db);
+    PermissionBackend.ForChange perm = rev.permissions();
     Iterator<Map.Entry<String, Short>> itr = in.labels.entrySet().iterator();
     while (itr.hasNext()) {
       Map.Entry<String, Short> ent = itr.next();
@@ -507,13 +479,9 @@
           String.format("label required to post review on behalf of \"%s\"", in.onBehalfOf));
     }
 
-    IdentifiedUser reviewer = accountResolver.parseOnBehalfOf(caller, in.onBehalfOf);
+    IdentifiedUser reviewer = accountResolver.resolve(in.onBehalfOf).asUniqueUserOnBehalfOf(caller);
     try {
-      permissionBackend
-          .user(reviewer)
-          .database(db)
-          .change(rev.getNotes())
-          .check(ChangePermission.READ);
+      permissionBackend.user(reviewer).change(rev.getNotes()).check(ChangePermission.READ);
     } catch (AuthException e) {
       throw new UnprocessableEntityException(
           String.format("on_behalf_of account %s cannot see change", reviewer.getAccountId()), e);
@@ -868,7 +836,6 @@
     private final ProjectState projectState;
     private final PatchSet.Id psId;
     private final ReviewInput in;
-    private final ListMultimap<RecipientType, Account.Id> accountsToNotify;
 
     private IdentifiedUser user;
     private ChangeNotes notes;
@@ -879,24 +846,19 @@
     private Map<String, Short> approvals = new HashMap<>();
     private Map<String, Short> oldApprovals = new HashMap<>();
 
-    private Op(
-        ProjectState projectState,
-        PatchSet.Id psId,
-        ReviewInput in,
-        ListMultimap<RecipientType, Account.Id> accountsToNotify) {
+    private Op(ProjectState projectState, PatchSet.Id psId, ReviewInput in) {
       this.projectState = projectState;
       this.psId = psId;
       this.in = in;
-      this.accountsToNotify = requireNonNull(accountsToNotify);
     }
 
     @Override
     public boolean updateChange(ChangeContext ctx)
-        throws OrmException, ResourceConflictException, UnprocessableEntityException, IOException,
+        throws ResourceConflictException, UnprocessableEntityException, IOException,
             PatchListNotAvailableException {
       user = ctx.getIdentifiedUser();
       notes = ctx.getNotes();
-      ps = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
+      ps = psUtil.get(ctx.getNotes(), psId);
       boolean dirty = false;
       dirty |= insertComments(ctx);
       dirty |= insertRobotComments(ctx);
@@ -906,22 +868,14 @@
     }
 
     @Override
-    public void postUpdate(Context ctx) throws OrmException {
+    public void postUpdate(Context ctx) {
       if (message == null) {
         return;
       }
-      if (in.notify.compareTo(NotifyHandling.NONE) > 0 || !accountsToNotify.isEmpty()) {
+      NotifyResolver.Result notify = ctx.getNotify(notes.getChangeId());
+      if (notify.shouldNotify()) {
         email
-            .create(
-                in.notify,
-                accountsToNotify,
-                notes,
-                ps,
-                user,
-                message,
-                comments,
-                in.message,
-                labelDelta)
+            .create(notify, notes, ps, user, message, comments, in.message, labelDelta)
             .sendAsync();
       }
       commentAdded.fire(
@@ -935,7 +889,7 @@
     }
 
     private boolean insertComments(ChangeContext ctx)
-        throws OrmException, UnprocessableEntityException, PatchListNotAvailableException {
+        throws UnprocessableEntityException, PatchListNotAvailableException {
       Map<String, List<CommentInput>> map = in.comments;
       if (map == null) {
         map = Collections.emptyMap();
@@ -990,13 +944,12 @@
           break;
       }
       ChangeUpdate u = ctx.getUpdate(psId);
-      commentsUtil.putComments(ctx.getDb(), u, Status.PUBLISHED, toPublish);
+      commentsUtil.putComments(u, Status.PUBLISHED, toPublish);
       comments.addAll(toPublish);
       return !toPublish.isEmpty();
     }
 
-    private boolean insertRobotComments(ChangeContext ctx)
-        throws OrmException, PatchListNotAvailableException {
+    private boolean insertRobotComments(ChangeContext ctx) throws PatchListNotAvailableException {
       if (in.robotComments == null) {
         return false;
       }
@@ -1008,7 +961,7 @@
     }
 
     private List<RobotComment> getNewRobotComments(ChangeContext ctx)
-        throws OrmException, PatchListNotAvailableException {
+        throws PatchListNotAvailableException {
       List<RobotComment> toAdd = new ArrayList<>(in.robotComments.size());
 
       Set<CommentSetEntry> existingIds =
@@ -1077,33 +1030,31 @@
       return new FixReplacement(fixReplacementInfo.path, range, fixReplacementInfo.replacement);
     }
 
-    private Set<CommentSetEntry> readExistingComments(ChangeContext ctx) throws OrmException {
-      return commentsUtil.publishedByChange(ctx.getDb(), ctx.getNotes()).stream()
+    private Set<CommentSetEntry> readExistingComments(ChangeContext ctx) {
+      return commentsUtil.publishedByChange(ctx.getNotes()).stream()
           .map(CommentSetEntry::create)
           .collect(toSet());
     }
 
-    private Set<CommentSetEntry> readExistingRobotComments(ChangeContext ctx) throws OrmException {
+    private Set<CommentSetEntry> readExistingRobotComments(ChangeContext ctx) {
       return commentsUtil.robotCommentsByChange(ctx.getNotes()).stream()
           .map(CommentSetEntry::create)
           .collect(toSet());
     }
 
-    private Map<String, Comment> changeDrafts(ChangeContext ctx) throws OrmException {
+    private Map<String, Comment> changeDrafts(ChangeContext ctx) {
       Map<String, Comment> drafts = new HashMap<>();
-      for (Comment c :
-          commentsUtil.draftByChangeAuthor(ctx.getDb(), ctx.getNotes(), user.getAccountId())) {
+      for (Comment c : commentsUtil.draftByChangeAuthor(ctx.getNotes(), user.getAccountId())) {
         c.tag = in.tag;
         drafts.put(c.key.uuid, c);
       }
       return drafts;
     }
 
-    private Map<String, Comment> patchSetDrafts(ChangeContext ctx) throws OrmException {
+    private Map<String, Comment> patchSetDrafts(ChangeContext ctx) {
       Map<String, Comment> drafts = new HashMap<>();
       for (Comment c :
-          commentsUtil.draftByPatchSetAuthor(
-              ctx.getDb(), psId, user.getAccountId(), ctx.getNotes())) {
+          commentsUtil.draftByPatchSetAuthor(psId, user.getAccountId(), ctx.getNotes())) {
         drafts.put(c.key.uuid, c);
       }
       return drafts;
@@ -1148,11 +1099,11 @@
       return previous;
     }
 
-    private boolean isReviewer(ChangeContext ctx) throws OrmException {
+    private boolean isReviewer(ChangeContext ctx) {
       if (ctx.getAccountId().equals(ctx.getChange().getOwner())) {
         return true;
       }
-      ChangeData cd = changeDataFactory.create(db.get(), ctx.getNotes());
+      ChangeData cd = changeDataFactory.create(ctx.getNotes());
       ReviewerSet reviewers = cd.reviewers();
       if (reviewers.byState(REVIEWER).contains(ctx.getAccountId())) {
         return true;
@@ -1161,13 +1112,13 @@
     }
 
     private boolean updateLabels(ProjectState projectState, ChangeContext ctx)
-        throws OrmException, ResourceConflictException, IOException {
+        throws ResourceConflictException, IOException {
       Map<String, Short> inLabels = firstNonNull(in.labels, Collections.emptyMap());
 
       // If no labels were modified and change is closed, abort early.
       // This avoids trying to record a modified label caused by a user
       // losing access to a label after the change was submitted.
-      if (inLabels.isEmpty() && ctx.getChange().getStatus().isClosed()) {
+      if (inLabels.isEmpty() && ctx.getChange().isClosed()) {
         return false;
       }
 
@@ -1236,14 +1187,6 @@
 
       forceCallerAsReviewer(projectState, ctx, current, ups, del);
 
-      if (!del.isEmpty()) {
-        ctx.getDb().patchSetApprovals().delete(del);
-      }
-
-      if (!ups.isEmpty()) {
-        ctx.getDb().patchSetApprovals().upsert(ups);
-      }
-
       return !del.isEmpty() || !ups.isEmpty();
     }
 
@@ -1254,11 +1197,11 @@
         List<PatchSetApproval> ups,
         List<PatchSetApproval> del)
         throws ResourceConflictException {
-      if (ctx.getChange().getStatus().isOpen()) {
+      if (ctx.getChange().isNew()) {
         return; // Not closed, nothing to validate.
       } else if (del.isEmpty() && ups.isEmpty()) {
         return; // No new votes.
-      } else if (ctx.getChange().getStatus() != Change.Status.MERGED) {
+      } else if (!ctx.getChange().isMerged()) {
         throw new ResourceConflictException("change is closed");
       }
 
@@ -1294,11 +1237,8 @@
         checkState(prev != psa.getValue()); // Should be filtered out above.
         if (prev > psa.getValue()) {
           reduced.add(psa);
-        } else {
-          // Set postSubmit bit in ReviewDb; not required for NoteDb, which sets
-          // it automatically.
-          psa.setPostSubmit(true);
         }
+        // No need to set postSubmit bit, which is set automatically when parsing from NoteDb.
       }
 
       if (!disallowed.isEmpty()) {
@@ -1356,13 +1296,12 @@
 
     private Map<String, PatchSetApproval> scanLabels(
         ProjectState projectState, ChangeContext ctx, List<PatchSetApproval> del)
-        throws OrmException, IOException {
+        throws IOException {
       LabelTypes labelTypes = projectState.getLabelTypes(ctx.getNotes());
       Map<String, PatchSetApproval> current = new HashMap<>();
 
       for (PatchSetApproval a :
           approvalsUtil.byPatchSetUser(
-              ctx.getDb(),
               ctx.getNotes(),
               psId,
               user.getAccountId(),
@@ -1382,7 +1321,7 @@
       return current;
     }
 
-    private boolean insertMessage(ChangeContext ctx) throws OrmException {
+    private boolean insertMessage(ChangeContext ctx) {
       String msg = Strings.nullToEmpty(in.message).trim();
 
       StringBuilder buf = new StringBuilder();
@@ -1406,7 +1345,7 @@
       message =
           ChangeMessagesUtil.newMessage(
               psId, user, ctx.getWhen(), "Patch Set " + psId.get() + ":" + buf, in.tag);
-      cmUtil.addChangeMessage(ctx.getDb(), ctx.getUpdate(psId), message);
+      cmUtil.addChangeMessage(ctx.getUpdate(psId), message);
       return true;
     }
 
diff --git a/java/com/google/gerrit/server/restapi/change/PostReviewers.java b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
index 3e66189..8abd964 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
@@ -16,11 +16,12 @@
 
 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.RestApiException;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.change.ReviewerAdder;
 import com.google.gerrit.server.change.ReviewerAdder.ReviewerAddition;
 import com.google.gerrit.server.change.ReviewerResource;
@@ -31,9 +32,7 @@
 import com.google.gerrit.server.update.RetryingRestCollectionModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -43,47 +42,55 @@
     extends RetryingRestCollectionModifyView<
         ChangeResource, ReviewerResource, AddReviewerInput, AddReviewerResult> {
 
-  private final Provider<ReviewDb> dbProvider;
   private final ChangeData.Factory changeDataFactory;
+  private final NotifyResolver notifyResolver;
   private final ReviewerAdder reviewerAdder;
 
   @Inject
   PostReviewers(
-      Provider<ReviewDb> db,
       ChangeData.Factory changeDataFactory,
       RetryHelper retryHelper,
+      NotifyResolver notifyResolver,
       ReviewerAdder reviewerAdder) {
     super(retryHelper);
-    this.dbProvider = db;
     this.changeDataFactory = changeDataFactory;
+    this.notifyResolver = notifyResolver;
     this.reviewerAdder = reviewerAdder;
   }
 
   @Override
   protected AddReviewerResult applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, AddReviewerInput input)
-      throws IOException, OrmException, RestApiException, UpdateException,
-          PermissionBackendException, ConfigInvalidException {
+      throws IOException, RestApiException, UpdateException, PermissionBackendException,
+          ConfigInvalidException {
     if (input.reviewer == null) {
       throw new BadRequestException("missing reviewer field");
     }
 
-    ReviewerAddition addition =
-        reviewerAdder.prepare(dbProvider.get(), rsrc.getNotes(), rsrc.getUser(), input, true);
+    ReviewerAddition addition = reviewerAdder.prepare(rsrc.getNotes(), rsrc.getUser(), input, true);
     if (addition.op == null) {
       return addition.result;
     }
     try (BatchUpdate bu =
-        updateFactory.create(
-            dbProvider.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+        updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+      bu.setNotify(resolveNotify(rsrc, input));
       Change.Id id = rsrc.getChange().getId();
       bu.addOp(id, addition.op);
       bu.execute();
     }
 
     // Re-read change to take into account results of the update.
-    addition.gatherResults(
-        changeDataFactory.create(dbProvider.get(), rsrc.getProject(), rsrc.getId()));
+    addition.gatherResults(changeDataFactory.create(rsrc.getProject(), rsrc.getId()));
     return addition.result;
   }
+
+  private NotifyResolver.Result resolveNotify(ChangeResource rsrc, AddReviewerInput input)
+      throws BadRequestException, ConfigInvalidException, IOException {
+    NotifyHandling notifyHandling = input.notify;
+    if (notifyHandling == null) {
+      notifyHandling =
+          rsrc.getChange().isWorkInProgress() ? NotifyHandling.NONE : NotifyHandling.ALL;
+    }
+    return notifyResolver.resolve(notifyHandling, input.notifyDetails);
+  }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/PreviewSubmit.java b/java/com/google/gerrit/server/restapi/change/PreviewSubmit.java
index c0c434c..c3cee0e 100644
--- a/java/com/google/gerrit/server/restapi/change/PreviewSubmit.java
+++ b/java/com/google/gerrit/server/restapi/change/PreviewSubmit.java
@@ -26,7 +26,6 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.ArchiveFormat;
@@ -40,7 +39,6 @@
 import com.google.gerrit.server.submit.MergeOpRepoManager;
 import com.google.gerrit.server.submit.MergeOpRepoManager.OpenRepo;
 import com.google.gerrit.server.update.UpdateException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -61,7 +59,6 @@
 public class PreviewSubmit implements RestReadView<RevisionResource> {
   private static final int MAX_DEFAULT_BUNDLE_SIZE = 100 * 1024 * 1024;
 
-  private final Provider<ReviewDb> dbProvider;
   private final Provider<MergeOp> mergeOpProvider;
   private final AllowedFormats allowedFormats;
   private int maxBundleSize;
@@ -74,11 +71,9 @@
 
   @Inject
   PreviewSubmit(
-      Provider<ReviewDb> dbProvider,
       Provider<MergeOp> mergeOpProvider,
       AllowedFormats allowedFormats,
       @GerritServerConfig Config cfg) {
-    this.dbProvider = dbProvider;
     this.mergeOpProvider = mergeOpProvider;
     this.allowedFormats = allowedFormats;
     this.maxBundleSize = cfg.getInt("download", "maxBundleSize", MAX_DEFAULT_BUNDLE_SIZE);
@@ -86,7 +81,7 @@
 
   @Override
   public BinaryResult apply(RevisionResource rsrc)
-      throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException,
+      throws RestApiException, UpdateException, IOException, ConfigInvalidException,
           PermissionBackendException {
     if (Strings.isNullOrEmpty(format)) {
       throw new BadRequestException("format is not specified");
@@ -103,7 +98,7 @@
     }
 
     Change change = rsrc.getChange();
-    if (!change.getStatus().isOpen()) {
+    if (!change.isNew()) {
       throw new PreconditionFailedException("change is " + ChangeUtil.status(change));
     }
     if (!rsrc.getUser().isIdentifiedUser()) {
@@ -114,23 +109,21 @@
   }
 
   private BinaryResult getBundles(RevisionResource rsrc, ArchiveFormat f)
-      throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException,
+      throws RestApiException, UpdateException, IOException, ConfigInvalidException,
           PermissionBackendException {
-    ReviewDb db = dbProvider.get();
     IdentifiedUser caller = rsrc.getUser().asIdentifiedUser();
     Change change = rsrc.getChange();
 
     @SuppressWarnings("resource") // Returned BinaryResult takes ownership and handles closing.
     MergeOp op = mergeOpProvider.get();
     try {
-      op.merge(db, change, caller, false, new SubmitInput(), true);
+      op.merge(change, caller, false, new SubmitInput(), true);
       BinaryResult bin = new SubmitPreviewResult(op, f, maxBundleSize);
       bin.disableGzip()
           .setContentType(f.getMimeType())
           .setAttachmentName("submit-preview-" + change.getChangeId() + "." + format);
       return bin;
-    } catch (OrmException
-        | RestApiException
+    } catch (RestApiException
         | UpdateException
         | IOException
         | ConfigInvalidException
diff --git a/java/com/google/gerrit/server/restapi/change/PublishChangeEdit.java b/java/com/google/gerrit/server/restapi/change/PublishChangeEdit.java
index 3d401c4..a47037c 100644
--- a/java/com/google/gerrit/server/restapi/change/PublishChangeEdit.java
+++ b/java/com/google/gerrit/server/restapi/change/PublishChangeEdit.java
@@ -14,12 +14,15 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.common.base.MoreObjects.firstNonNull;
+
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.PublishChangeEditInput;
 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.server.change.ChangeResource;
-import com.google.gerrit.server.change.NotifyUtil;
+import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.edit.ChangeEdit;
 import com.google.gerrit.server.edit.ChangeEditUtil;
 import com.google.gerrit.server.project.ContributorAgreementsChecker;
@@ -28,7 +31,6 @@
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -39,25 +41,25 @@
 public class PublishChangeEdit
     extends RetryingRestModifyView<ChangeResource, PublishChangeEditInput, Response<?>> {
   private final ChangeEditUtil editUtil;
-  private final NotifyUtil notifyUtil;
+  private final NotifyResolver notifyResolver;
   private final ContributorAgreementsChecker contributorAgreementsChecker;
 
   @Inject
   PublishChangeEdit(
       RetryHelper retryHelper,
       ChangeEditUtil editUtil,
-      NotifyUtil notifyUtil,
+      NotifyResolver notifyResolver,
       ContributorAgreementsChecker contributorAgreementsChecker) {
     super(retryHelper);
     this.editUtil = editUtil;
-    this.notifyUtil = notifyUtil;
+    this.notifyResolver = notifyResolver;
     this.contributorAgreementsChecker = contributorAgreementsChecker;
   }
 
   @Override
   protected Response<?> applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, PublishChangeEditInput in)
-      throws IOException, OrmException, RestApiException, UpdateException, ConfigInvalidException,
+      throws IOException, RestApiException, UpdateException, ConfigInvalidException,
           NoSuchProjectException {
     contributorAgreementsChecker.check(rsrc.getProject(), rsrc.getUser());
     Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getNotes(), rsrc.getUser());
@@ -73,8 +75,7 @@
         rsrc.getNotes(),
         rsrc.getUser(),
         edit.get(),
-        in.notify,
-        notifyUtil.resolveAccounts(in.notifyDetails));
+        notifyResolver.resolve(firstNonNull(in.notify, NotifyHandling.ALL), in.notifyDetails));
     return Response.none();
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/PutAssignee.java b/java/com/google/gerrit/server/restapi/change/PutAssignee.java
index d247d5e..a9a6f12 100644
--- a/java/com/google/gerrit/server/restapi/change/PutAssignee.java
+++ b/java/com/google/gerrit/server/restapi/change/PutAssignee.java
@@ -23,9 +23,7 @@
 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.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.account.AccountResolver;
@@ -41,9 +39,7 @@
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -54,7 +50,6 @@
 
   private final AccountResolver accountResolver;
   private final SetAssigneeOp.Factory assigneeFactory;
-  private final Provider<ReviewDb> db;
   private final ReviewerAdder reviewerAdder;
   private final AccountLoader.Factory accountLoaderFactory;
   private final PermissionBackend permissionBackend;
@@ -64,14 +59,12 @@
       AccountResolver accountResolver,
       SetAssigneeOp.Factory assigneeFactory,
       RetryHelper retryHelper,
-      Provider<ReviewDb> db,
       ReviewerAdder reviewerAdder,
       AccountLoader.Factory accountLoaderFactory,
       PermissionBackend permissionBackend) {
     super(retryHelper);
     this.accountResolver = accountResolver;
     this.assigneeFactory = assigneeFactory;
-    this.db = db;
     this.reviewerAdder = reviewerAdder;
     this.accountLoaderFactory = accountLoaderFactory;
     this.permissionBackend = permissionBackend;
@@ -80,8 +73,8 @@
   @Override
   protected AccountInfo applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, AssigneeInput input)
-      throws RestApiException, UpdateException, OrmException, IOException,
-          PermissionBackendException, ConfigInvalidException {
+      throws RestApiException, UpdateException, IOException, PermissionBackendException,
+          ConfigInvalidException {
     rsrc.permissions().check(ChangePermission.EDIT_ASSIGNEE);
 
     input.assignee = Strings.nullToEmpty(input.assignee).trim();
@@ -89,14 +82,10 @@
       throw new BadRequestException("missing assignee field");
     }
 
-    IdentifiedUser assignee = accountResolver.parse(input.assignee);
-    if (!assignee.getAccount().isActive()) {
-      throw new UnprocessableEntityException(input.assignee + " is not active");
-    }
+    IdentifiedUser assignee = accountResolver.resolve(input.assignee).asUniqueUser();
     try {
       permissionBackend
           .absentUser(assignee.getAccountId())
-          .database(db)
           .change(rsrc.getNotes())
           .check(ChangePermission.READ);
     } catch (AuthException e) {
@@ -104,12 +93,12 @@
     }
 
     try (BatchUpdate bu =
-        updateFactory.create(
-            db.get(), rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+        updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
       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);
 
       bu.execute();
@@ -118,13 +107,13 @@
   }
 
   private ReviewerAddition addAssigneeAsCC(ChangeResource rsrc, String assignee)
-      throws OrmException, IOException, PermissionBackendException, ConfigInvalidException {
+      throws IOException, PermissionBackendException, ConfigInvalidException {
     AddReviewerInput reviewerInput = new AddReviewerInput();
     reviewerInput.reviewer = assignee;
     reviewerInput.state = ReviewerState.CC;
     reviewerInput.confirmed = true;
     reviewerInput.notify = NotifyHandling.NONE;
-    return reviewerAdder.prepare(db.get(), rsrc.getNotes(), rsrc.getUser(), reviewerInput, false);
+    return reviewerAdder.prepare(rsrc.getNotes(), rsrc.getUser(), reviewerInput, false);
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/restapi/change/PutDescription.java b/java/com/google/gerrit/server/restapi/change/PutDescription.java
index 3b5edb2..0ec38e1 100644
--- a/java/com/google/gerrit/server/restapi/change/PutDescription.java
+++ b/java/com/google/gerrit/server/restapi/change/PutDescription.java
@@ -21,7 +21,6 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.RevisionResource;
@@ -35,28 +34,19 @@
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import java.util.Collections;
 
 @Singleton
 public class PutDescription
     extends RetryingRestModifyView<RevisionResource, DescriptionInput, Response<String>>
     implements UiAction<RevisionResource> {
-  private final Provider<ReviewDb> dbProvider;
   private final ChangeMessagesUtil cmUtil;
   private final PatchSetUtil psUtil;
 
   @Inject
-  PutDescription(
-      Provider<ReviewDb> dbProvider,
-      ChangeMessagesUtil cmUtil,
-      RetryHelper retryHelper,
-      PatchSetUtil psUtil) {
+  PutDescription(ChangeMessagesUtil cmUtil, RetryHelper retryHelper, PatchSetUtil psUtil) {
     super(retryHelper);
-    this.dbProvider = dbProvider;
     this.cmUtil = cmUtil;
     this.psUtil = psUtil;
   }
@@ -69,8 +59,7 @@
 
     Op op = new Op(input != null ? input : new DescriptionInput(), rsrc.getPatchSet().getId());
     try (BatchUpdate u =
-        updateFactory.create(
-            dbProvider.get(), rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+        updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
       u.addOp(rsrc.getChange().getId(), op);
       u.execute();
     }
@@ -92,11 +81,10 @@
     }
 
     @Override
-    public boolean updateChange(ChangeContext ctx) throws OrmException {
-      PatchSet ps = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
+    public boolean updateChange(ChangeContext ctx) {
       ChangeUpdate update = ctx.getUpdate(psId);
       newDescription = Strings.nullToEmpty(input.description);
-      oldDescription = Strings.nullToEmpty(ps.getDescription());
+      oldDescription = Strings.nullToEmpty(psUtil.get(ctx.getNotes(), psId).getDescription());
       if (oldDescription.equals(newDescription)) {
         return false;
       }
@@ -109,15 +97,12 @@
         summary = "Description changed to \"" + newDescription + "\"";
       }
 
-      ps.setDescription(newDescription);
       update.setPsDescription(newDescription);
 
-      ctx.getDb().patchSets().update(Collections.singleton(ps));
-
       ChangeMessage cmsg =
           ChangeMessagesUtil.newMessage(
               psId, ctx.getUser(), ctx.getWhen(), summary, ChangeMessagesUtil.TAG_SET_DESCRIPTION);
-      cmUtil.addChangeMessage(ctx.getDb(), update, cmsg);
+      cmUtil.addChangeMessage(update, cmsg);
       return true;
     }
   }
diff --git a/java/com/google/gerrit/server/restapi/change/PutDraftComment.java b/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
index 72358bd..f6c9abe 100644
--- a/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
@@ -26,7 +26,6 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.DraftCommentResource;
@@ -41,7 +40,6 @@
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -53,7 +51,6 @@
 public class PutDraftComment
     extends RetryingRestModifyView<DraftCommentResource, DraftInput, Response<CommentInfo>> {
 
-  private final Provider<ReviewDb> db;
   private final DeleteDraftComment delete;
   private final CommentsUtil commentsUtil;
   private final PatchSetUtil psUtil;
@@ -62,7 +59,6 @@
 
   @Inject
   PutDraftComment(
-      Provider<ReviewDb> db,
       DeleteDraftComment delete,
       CommentsUtil commentsUtil,
       PatchSetUtil psUtil,
@@ -70,7 +66,6 @@
       Provider<CommentJson> commentJson,
       PatchListCache patchListCache) {
     super(retryHelper);
-    this.db = db;
     this.delete = delete;
     this.commentsUtil = commentsUtil;
     this.psUtil = psUtil;
@@ -81,7 +76,7 @@
   @Override
   protected Response<CommentInfo> applyImpl(
       BatchUpdate.Factory updateFactory, DraftCommentResource rsrc, DraftInput in)
-      throws RestApiException, UpdateException, OrmException, PermissionBackendException {
+      throws RestApiException, UpdateException, PermissionBackendException {
     if (in == null || in.message == null || in.message.trim().isEmpty()) {
       return delete.applyImpl(updateFactory, rsrc, null);
     } else if (in.id != null && !rsrc.getId().equals(in.id)) {
@@ -93,8 +88,7 @@
     }
 
     try (BatchUpdate bu =
-        updateFactory.create(
-            db.get(), rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+        updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
       Op op = new Op(rsrc.getComment().key, in);
       bu.addOp(rsrc.getChange().getId(), op);
       bu.execute();
@@ -116,9 +110,9 @@
 
     @Override
     public boolean updateChange(ChangeContext ctx)
-        throws ResourceNotFoundException, OrmException, PatchListNotAvailableException {
+        throws ResourceNotFoundException, PatchListNotAvailableException {
       Optional<Comment> maybeComment =
-          commentsUtil.getDraft(ctx.getDb(), ctx.getNotes(), ctx.getIdentifiedUser(), key);
+          commentsUtil.getDraft(ctx.getNotes(), ctx.getIdentifiedUser(), key);
       if (!maybeComment.isPresent()) {
         // Disappeared out from under us. Can't easily fall back to insert,
         // because the input might be missing required fields. Just give up.
@@ -133,7 +127,7 @@
       PatchSet.Id psId = new PatchSet.Id(ctx.getChange().getId(), origComment.key.patchSetId);
       ChangeUpdate update = ctx.getUpdate(psId);
 
-      PatchSet ps = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
+      PatchSet ps = psUtil.get(ctx.getNotes(), psId);
       if (ps == null) {
         throw new ResourceNotFoundException("patch set not found: " + psId);
       }
@@ -141,16 +135,12 @@
         // Updating the path alters the primary key, which isn't possible.
         // Delete then recreate the comment instead of an update.
 
-        commentsUtil.deleteComments(ctx.getDb(), update, Collections.singleton(origComment));
+        commentsUtil.deleteComments(update, Collections.singleton(origComment));
         comment.key.filename = in.path;
       }
       setCommentRevId(comment, patchListCache, ctx.getChange(), ps);
       commentsUtil.putComments(
-          ctx.getDb(),
-          update,
-          Status.DRAFT,
-          Collections.singleton(update(comment, in, ctx.getWhen())));
-      ctx.dontBumpLastUpdatedOn();
+          update, 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 bcd0e9e..c542164 100644
--- a/java/com/google/gerrit/server/restapi/change/PutMessage.java
+++ b/java/com/google/gerrit/server/restapi/change/PutMessage.java
@@ -24,13 +24,12 @@
 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.reviewdb.server.ReviewDb;
 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.change.ChangeResource;
-import com.google.gerrit.server.change.NotifyUtil;
+import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.change.PatchSetInserter;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeNotes;
@@ -44,7 +43,6 @@
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.CommitMessageUtil;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -68,12 +66,11 @@
 
   private final GitRepositoryManager repositoryManager;
   private final Provider<CurrentUser> userProvider;
-  private final Provider<ReviewDb> db;
   private final TimeZone tz;
   private final PatchSetInserter.Factory psInserterFactory;
   private final PermissionBackend permissionBackend;
   private final PatchSetUtil psUtil;
-  private final NotifyUtil notifyUtil;
+  private final NotifyResolver notifyResolver;
   private final ProjectCache projectCache;
 
   @Inject
@@ -81,22 +78,20 @@
       RetryHelper retryHelper,
       GitRepositoryManager repositoryManager,
       Provider<CurrentUser> userProvider,
-      Provider<ReviewDb> db,
       PatchSetInserter.Factory psInserterFactory,
       PermissionBackend permissionBackend,
       @GerritPersonIdent PersonIdent gerritIdent,
       PatchSetUtil psUtil,
-      NotifyUtil notifyUtil,
+      NotifyResolver notifyResolver,
       ProjectCache projectCache) {
     super(retryHelper);
     this.repositoryManager = repositoryManager;
     this.userProvider = userProvider;
-    this.db = db;
     this.psInserterFactory = psInserterFactory;
     this.tz = gerritIdent.getTimeZone();
     this.permissionBackend = permissionBackend;
     this.psUtil = psUtil;
-    this.notifyUtil = notifyUtil;
+    this.notifyResolver = notifyResolver;
     this.projectCache = projectCache;
   }
 
@@ -104,8 +99,8 @@
   protected Response<String> applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource resource, CommitMessageInput input)
       throws IOException, RestApiException, UpdateException, PermissionBackendException,
-          OrmException, ConfigInvalidException {
-    PatchSet ps = psUtil.current(db.get(), resource.getNotes());
+          ConfigInvalidException {
+    PatchSet ps = psUtil.current(resource.getNotes());
     if (ps == null) {
       throw new ResourceConflictException("current revision is missing");
     }
@@ -121,11 +116,6 @@
         resource.getChange().getKey().get(),
         sanitizedCommitMessage);
 
-    NotifyHandling notify = input.notify;
-    if (notify == null) {
-      notify = resource.getChange().isWorkInProgress() ? NotifyHandling.OWNER : NotifyHandling.ALL;
-    }
-
     try (Repository repository = repositoryManager.openRepository(resource.getProject());
         RevWalk revWalk = new RevWalk(repository);
         ObjectInserter objectInserter = repository.newObjectInserter()) {
@@ -138,8 +128,7 @@
 
       Timestamp ts = TimeUtil.nowTs();
       try (BatchUpdate bu =
-          updateFactory.create(
-              db.get(), resource.getChange().getProject(), userProvider.get(), ts)) {
+          updateFactory.create(resource.getChange().getProject(), userProvider.get(), ts)) {
         // Ensure that BatchUpdate will update the same repo
         bu.setRepository(repository, new RevWalk(objectInserter.newReader()), objectInserter);
 
@@ -150,8 +139,7 @@
         inserter.setMessage(
             String.format("Patch Set %s: Commit message was updated.", psId.getId()));
         inserter.setDescription("Edit commit message");
-        inserter.setNotify(notify);
-        inserter.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
+        bu.setNotify(resolveNotify(input, resource));
         bu.addOp(resource.getChange().getId(), inserter);
         bu.execute();
       }
@@ -159,6 +147,16 @@
     return Response.ok("ok");
   }
 
+  private NotifyResolver.Result resolveNotify(CommitMessageInput input, ChangeResource resource)
+      throws BadRequestException, ConfigInvalidException, IOException {
+    NotifyHandling notifyHandling = input.notify;
+    if (notifyHandling == null) {
+      notifyHandling =
+          resource.getChange().isWorkInProgress() ? NotifyHandling.OWNER : NotifyHandling.ALL;
+    }
+    return notifyResolver.resolve(notifyHandling, input.notifyDetails);
+  }
+
   private ObjectId createCommit(
       ObjectInserter objectInserter,
       RevCommit basePatchSetCommit,
@@ -177,8 +175,7 @@
   }
 
   private void ensureCanEditCommitMessage(ChangeNotes changeNotes)
-      throws AuthException, PermissionBackendException, IOException, ResourceConflictException,
-          OrmException {
+      throws AuthException, PermissionBackendException, IOException, ResourceConflictException {
     if (!userProvider.get().isIdentifiedUser()) {
       throw new AuthException("Authentication required");
     }
@@ -188,7 +185,6 @@
     try {
       permissionBackend
           .user(userProvider.get())
-          .database(db.get())
           .change(changeNotes)
           .check(ChangePermission.ADD_PATCH_SET);
       projectCache.checkedGet(changeNotes.getProjectName()).checkStatePermitsWrite();
diff --git a/java/com/google/gerrit/server/restapi/change/PutTopic.java b/java/com/google/gerrit/server/restapi/change/PutTopic.java
index 7f56c91..abfa49b 100644
--- a/java/com/google/gerrit/server/restapi/change/PutTopic.java
+++ b/java/com/google/gerrit/server/restapi/change/PutTopic.java
@@ -22,7 +22,6 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.change.ChangeResource;
@@ -38,26 +37,18 @@
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 @Singleton
 public class PutTopic extends RetryingRestModifyView<ChangeResource, TopicInput, Response<String>>
     implements UiAction<ChangeResource> {
-  private final Provider<ReviewDb> dbProvider;
   private final ChangeMessagesUtil cmUtil;
   private final TopicEdited topicEdited;
 
   @Inject
-  PutTopic(
-      Provider<ReviewDb> dbProvider,
-      ChangeMessagesUtil cmUtil,
-      RetryHelper retryHelper,
-      TopicEdited topicEdited) {
+  PutTopic(ChangeMessagesUtil cmUtil, RetryHelper retryHelper, TopicEdited topicEdited) {
     super(retryHelper);
-    this.dbProvider = dbProvider;
     this.cmUtil = cmUtil;
     this.topicEdited = topicEdited;
   }
@@ -82,8 +73,7 @@
 
     Op op = new Op(sanitizedInput);
     try (BatchUpdate u =
-        updateFactory.create(
-            dbProvider.get(), req.getChange().getProject(), req.getUser(), TimeUtil.nowTs())) {
+        updateFactory.create(req.getChange().getProject(), req.getUser(), TimeUtil.nowTs())) {
       u.addOp(req.getId(), op);
       u.execute();
     }
@@ -102,7 +92,7 @@
     }
 
     @Override
-    public boolean updateChange(ChangeContext ctx) throws OrmException {
+    public boolean updateChange(ChangeContext ctx) {
       change = ctx.getChange();
       ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId());
       newTopicName = Strings.nullToEmpty(input.topic);
@@ -123,7 +113,7 @@
 
       ChangeMessage cmsg =
           ChangeMessagesUtil.newMessage(ctx, summary, ChangeMessagesUtil.TAG_SET_TOPIC);
-      cmUtil.addChangeMessage(ctx.getDb(), update, cmsg);
+      cmUtil.addChangeMessage(update, cmsg);
       return true;
     }
 
diff --git a/java/com/google/gerrit/server/restapi/change/QueryChanges.java b/java/com/google/gerrit/server/restapi/change/QueryChanges.java
index 082d27d..e1ed8d6 100644
--- a/java/com/google/gerrit/server/restapi/change/QueryChanges.java
+++ b/java/com/google/gerrit/server/restapi/change/QueryChanges.java
@@ -17,6 +17,7 @@
 import com.google.common.collect.Iterables;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.client.ListOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -31,7 +32,6 @@
 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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -70,7 +70,7 @@
 
   @Option(name = "-O", usage = "Output option flags, in hex")
   void setOptionFlagsHex(String hex) {
-    options.addAll(ListChangesOption.fromBits(Integer.parseInt(hex, 16)));
+    options.addAll(ListOption.fromBits(ListChangesOption.class, Integer.parseInt(hex, 16)));
   }
 
   @Option(
@@ -82,6 +82,11 @@
     imp.setStart(start);
   }
 
+  @Option(name = "--no-limit", usage = "Return all results, overriding the default limit")
+  public void setNoLimit(boolean on) {
+    imp.setNoLimit(on);
+  }
+
   @Override
   public void setDynamicBean(String plugin, DynamicOptions.DynamicBean dynamicBean) {
     imp.setDynamicBean(plugin, dynamicBean);
@@ -109,7 +114,7 @@
 
   @Override
   public List<?> apply(TopLevelResource rsrc)
-      throws BadRequestException, AuthException, OrmException, PermissionBackendException {
+      throws BadRequestException, AuthException, PermissionBackendException {
     List<List<ChangeInfo>> out;
     try {
       out = query();
@@ -122,8 +127,7 @@
     return out.size() == 1 ? out.get(0) : out;
   }
 
-  private List<List<ChangeInfo>> query()
-      throws OrmException, QueryParseException, PermissionBackendException {
+  private List<List<ChangeInfo>> query() throws QueryParseException, PermissionBackendException {
     if (imp.isDisabled()) {
       throw new QueryParseException("query disabled");
     }
@@ -137,7 +141,8 @@
 
     int cnt = queries.size();
     List<QueryResult<ChangeData>> results = imp.query(qb.parse(queries));
-    List<List<ChangeInfo>> res = json.create(options, this.imp).format(results);
+    List<List<ChangeInfo>> res =
+        json.create(options, this.imp.getAttributesFactory()).format(results);
     for (int n = 0; n < cnt; n++) {
       List<ChangeInfo> info = res.get(n);
       if (results.get(n).more() && !info.isEmpty()) {
diff --git a/java/com/google/gerrit/server/restapi/change/Rebase.java b/java/com/google/gerrit/server/restapi/change/Rebase.java
index 3165436..782b91aa 100644
--- a/java/com/google/gerrit/server/restapi/change/Rebase.java
+++ b/java/com/google/gerrit/server/restapi/change/Rebase.java
@@ -17,6 +17,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
+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;
@@ -28,13 +29,12 @@
 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.Change.Status;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.change.RebaseChangeOp;
 import com.google.gerrit.server.change.RebaseUtil;
 import com.google.gerrit.server.change.RebaseUtil.Base;
@@ -50,9 +50,7 @@
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import org.eclipse.jgit.lib.ObjectId;
@@ -75,7 +73,6 @@
   private final RebaseChangeOp.Factory rebaseFactory;
   private final RebaseUtil rebaseUtil;
   private final ChangeJson.Factory json;
-  private final Provider<ReviewDb> dbProvider;
   private final PermissionBackend permissionBackend;
   private final ProjectCache projectCache;
   private final PatchSetUtil patchSetUtil;
@@ -87,7 +84,6 @@
       RebaseChangeOp.Factory rebaseFactory,
       RebaseUtil rebaseUtil,
       ChangeJson.Factory json,
-      Provider<ReviewDb> dbProvider,
       PermissionBackend permissionBackend,
       ProjectCache projectCache,
       PatchSetUtil patchSetUtil) {
@@ -96,7 +92,6 @@
     this.rebaseFactory = rebaseFactory;
     this.rebaseUtil = rebaseUtil;
     this.json = json;
-    this.dbProvider = dbProvider;
     this.permissionBackend = permissionBackend;
     this.projectCache = projectCache;
     this.patchSetUtil = patchSetUtil;
@@ -105,12 +100,11 @@
   @Override
   protected ChangeInfo applyImpl(
       BatchUpdate.Factory updateFactory, RevisionResource rsrc, RebaseInput input)
-      throws OrmException, UpdateException, RestApiException, IOException,
-          PermissionBackendException {
+      throws UpdateException, RestApiException, IOException, PermissionBackendException {
     // Not allowed to rebase if the current patch set is locked.
     patchSetUtil.checkPatchSetNotLocked(rsrc.getNotes());
 
-    rsrc.permissions().database(dbProvider).check(ChangePermission.REBASE);
+    rsrc.permissions().check(ChangePermission.REBASE);
     projectCache.checkedGet(rsrc.getProject()).checkStatePermitsWrite();
 
     Change change = rsrc.getChange();
@@ -119,14 +113,15 @@
         ObjectReader reader = oi.newReader();
         RevWalk rw = new RevWalk(reader);
         BatchUpdate bu =
-            updateFactory.create(
-                dbProvider.get(), change.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
-      if (!change.getStatus().isOpen()) {
+            updateFactory.create(change.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+      if (!change.isNew()) {
         throw new ResourceConflictException("change is " + ChangeUtil.status(change));
       } else if (!hasOneParent(rw, rsrc.getPatchSet())) {
         throw new ResourceConflictException(
             "cannot rebase merge commits or commit with no ancestor");
       }
+      // TODO(dborowitz): Why no notification? This seems wrong; dig up blame.
+      bu.setNotify(NotifyResolver.Result.none());
       bu.setRepository(repo, rw, oi);
       bu.addOp(
           change.getId(),
@@ -141,7 +136,7 @@
 
   private ObjectId findBaseRev(
       Repository repo, RevWalk rw, RevisionResource rsrc, RebaseInput input)
-      throws RestApiException, OrmException, IOException, NoSuchChangeException, AuthException,
+      throws RestApiException, IOException, NoSuchChangeException, AuthException,
           PermissionBackendException {
     Branch.NameKey destRefKey = rsrc.getChange().getDest();
     if (input == null || input.base == null) {
@@ -164,7 +159,8 @@
     try {
       base = rebaseUtil.parseBase(rsrc, str);
       if (base == null) {
-        throw new ResourceConflictException("base revision is missing: " + str);
+        throw new ResourceConflictException(
+            "base revision is missing from the destination branch: " + str);
       }
     } catch (NoSuchChangeException e) {
       throw new UnprocessableEntityException(
@@ -176,11 +172,7 @@
       throw new ResourceConflictException("cannot rebase change onto itself");
     }
 
-    permissionBackend
-        .user(rsrc.getUser())
-        .database(dbProvider)
-        .change(base.notes())
-        .check(ChangePermission.READ);
+    permissionBackend.user(rsrc.getUser()).change(base.notes()).check(ChangePermission.READ);
 
     Change baseChange = base.notes().getChange();
     if (!baseChange.getProject().equals(change.getProject())) {
@@ -189,7 +181,7 @@
     } else if (!baseChange.getDest().equals(change.getDest())) {
       throw new ResourceConflictException(
           "base change is targeting wrong branch: " + baseChange.getDest());
-    } else if (baseChange.getStatus() == Status.ABANDONED) {
+    } else if (baseChange.isAbandoned()) {
       throw new ResourceConflictException("base change is abandoned: " + baseChange.getKey());
     } else if (isMergedInto(rw, rsrc.getPatchSet(), base.patchSet())) {
       throw new ResourceConflictException(
@@ -221,7 +213,7 @@
             .setVisible(false);
 
     Change change = rsrc.getChange();
-    if (!(change.getStatus().isOpen() && rsrc.isCurrent())) {
+    if (!(change.isNew() && rsrc.isCurrent())) {
       return description;
     }
 
@@ -239,7 +231,7 @@
       if (patchSetUtil.isPatchSetLocked(rsrc.getNotes())) {
         return description;
       }
-    } catch (OrmException | IOException e) {
+    } catch (StorageException | IOException e) {
       logger.atSevere().withCause(e).log(
           "Failed to check if the current patch set of change %s is locked", change.getId());
       return description;
@@ -251,13 +243,17 @@
       if (hasOneParent(rw, rsrc.getPatchSet())) {
         enabled = rebaseUtil.canRebase(rsrc.getPatchSet(), change.getDest(), repo, rw);
       }
-    } catch (IOException e) {
+    } catch (Exception e) {
+      // Be generous here with the exceptions that we log and swallow. RebaseUtil#canRebase uses the
+      // change index and this UI action is on the critical path of rendering a change details page.
+      // If the index is broken, we log and disable the UI action, but still show the page to the
+      // user.
       logger.atSevere().withCause(e).log(
           "Failed to check if patch set can be rebased: %s", rsrc.getPatchSet());
       return description;
     }
 
-    if (rsrc.permissions().database(dbProvider).testOrFalse(ChangePermission.REBASE)) {
+    if (rsrc.permissions().testOrFalse(ChangePermission.REBASE)) {
       return description.setVisible(true).setEnabled(enabled);
     }
     return description;
@@ -278,9 +274,8 @@
     @Override
     protected ChangeInfo applyImpl(
         BatchUpdate.Factory updateFactory, ChangeResource rsrc, RebaseInput input)
-        throws OrmException, UpdateException, RestApiException, IOException,
-            PermissionBackendException {
-      PatchSet ps = psUtil.current(rebase.dbProvider.get(), rsrc.getNotes());
+        throws UpdateException, RestApiException, IOException, PermissionBackendException {
+      PatchSet ps = psUtil.current(rsrc.getNotes());
       if (ps == null) {
         throw new ResourceConflictException("current revision is missing");
       }
diff --git a/java/com/google/gerrit/server/restapi/change/RebaseChangeEdit.java b/java/com/google/gerrit/server/restapi/change/RebaseChangeEdit.java
index 6020e95..81294ed 100644
--- a/java/com/google/gerrit/server/restapi/change/RebaseChangeEdit.java
+++ b/java/com/google/gerrit/server/restapi/change/RebaseChangeEdit.java
@@ -27,7 +27,6 @@
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryingRestModifyView;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -50,8 +49,7 @@
 
   @Override
   protected Response<?> applyImpl(BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input in)
-      throws AuthException, ResourceConflictException, IOException, OrmException,
-          PermissionBackendException {
+      throws AuthException, ResourceConflictException, IOException, PermissionBackendException {
     Project.NameKey project = rsrc.getProject();
     try (Repository repository = repositoryManager.openRepository(project)) {
       editModifier.rebaseEdit(repository, rsrc.getNotes());
diff --git a/java/com/google/gerrit/server/restapi/change/Rebuild.java b/java/com/google/gerrit/server/restapi/change/Rebuild.java
deleted file mode 100644
index ec17598..0000000
--- a/java/com/google/gerrit/server/restapi/change/Rebuild.java
+++ /dev/null
@@ -1,109 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.restapi.change;
-
-import static java.util.stream.Collectors.joining;
-
-import com.google.gerrit.extensions.common.Input;
-import com.google.gerrit.extensions.restapi.BinaryResult;
-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.RestModifyView;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.reviewdb.server.ReviewDbUtil;
-import com.google.gerrit.server.CommentsUtil;
-import com.google.gerrit.server.change.ChangeResource;
-import com.google.gerrit.server.notedb.ChangeBundle;
-import com.google.gerrit.server.notedb.ChangeBundleReader;
-import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.notedb.NotesMigration;
-import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder;
-import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gwtorm.server.OrmException;
-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;
-
-@Singleton
-public class Rebuild implements RestModifyView<ChangeResource, Input> {
-
-  private final Provider<ReviewDb> db;
-  private final NotesMigration migration;
-  private final ChangeRebuilder rebuilder;
-  private final ChangeBundleReader bundleReader;
-  private final CommentsUtil commentsUtil;
-  private final ChangeNotes.Factory notesFactory;
-
-  @Inject
-  Rebuild(
-      Provider<ReviewDb> db,
-      NotesMigration migration,
-      ChangeRebuilder rebuilder,
-      ChangeBundleReader bundleReader,
-      CommentsUtil commentsUtil,
-      ChangeNotes.Factory notesFactory) {
-    this.db = db;
-    this.migration = migration;
-    this.rebuilder = rebuilder;
-    this.bundleReader = bundleReader;
-    this.commentsUtil = commentsUtil;
-    this.notesFactory = notesFactory;
-  }
-
-  @Override
-  public BinaryResult apply(ChangeResource rsrc, Input input)
-      throws ResourceNotFoundException, IOException, OrmException, ConfigInvalidException,
-          ResourceConflictException {
-    if (!migration.commitChangeWrites()) {
-      throw new ResourceNotFoundException();
-    }
-    if (!migration.readChanges()) {
-      // ChangeBundle#fromNotes currently doesn't work if reading isn't enabled,
-      // so don't attempt a diff.
-      rebuild(rsrc);
-      return BinaryResult.create("Rebuilt change successfully");
-    }
-
-    // Not the same transaction as the rebuild, so may result in spurious diffs
-    // in the case of races. This should be easy enough to detect by rerunning.
-    ChangeBundle reviewDbBundle =
-        bundleReader.fromReviewDb(ReviewDbUtil.unwrapDb(db.get()), rsrc.getId());
-    if (reviewDbBundle == null) {
-      throw new ResourceConflictException("change is missing in ReviewDb");
-    }
-    rebuild(rsrc);
-    ChangeNotes notes = notesFactory.create(db.get(), rsrc.getChange().getProject(), rsrc.getId());
-    ChangeBundle noteDbBundle = ChangeBundle.fromNotes(commentsUtil, notes);
-    List<String> diffs = reviewDbBundle.differencesFrom(noteDbBundle);
-    if (diffs.isEmpty()) {
-      return BinaryResult.create("No differences between ReviewDb and NoteDb");
-    }
-    return BinaryResult.create(
-        diffs.stream().collect(joining("\n", "Differences between ReviewDb and NoteDb:\n", "\n")));
-  }
-
-  private void rebuild(ChangeResource rsrc)
-      throws ResourceNotFoundException, OrmException, IOException {
-    try {
-      rebuilder.rebuild(db.get(), rsrc.getId());
-    } catch (NoSuchChangeException e) {
-      throw new ResourceNotFoundException(IdString.fromDecoded(rsrc.getId().toString()), e);
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java b/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java
index 51f70bd..937ab56 100644
--- a/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java
+++ b/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java
@@ -28,7 +28,6 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -36,9 +35,7 @@
 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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.ArrayDeque;
@@ -61,23 +58,20 @@
 class RelatedChangesSorter {
   private final GitRepositoryManager repoManager;
   private final PermissionBackend permissionBackend;
-  private final Provider<ReviewDb> dbProvider;
   private final ProjectCache projectCache;
 
   @Inject
   RelatedChangesSorter(
       GitRepositoryManager repoManager,
       PermissionBackend permissionBackend,
-      Provider<ReviewDb> dbProvider,
       ProjectCache projectCache) {
     this.repoManager = repoManager;
     this.permissionBackend = permissionBackend;
-    this.dbProvider = dbProvider;
     this.projectCache = projectCache;
   }
 
   public List<PatchSetData> sort(List<ChangeData> in, PatchSet startPs)
-      throws OrmException, IOException, PermissionBackendException {
+      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);
@@ -118,8 +112,7 @@
     return result;
   }
 
-  private Map<String, PatchSetData> collectById(List<ChangeData> in)
-      throws OrmException, IOException {
+  private Map<String, PatchSetData> collectById(List<ChangeData> in) throws IOException {
     Project.NameKey project = in.get(0).change().getProject();
     Map<String, PatchSetData> result = Maps.newHashMapWithExpectedSize(in.size() * 3);
     try (Repository repo = repoManager.openRepository(project);
@@ -235,7 +228,7 @@
   }
 
   private boolean isVisible(PatchSetData psd) throws PermissionBackendException, IOException {
-    PermissionBackend.WithUser perm = permissionBackend.currentUser().database(dbProvider);
+    PermissionBackend.WithUser perm = permissionBackend.currentUser();
     try {
       perm.change(psd.data()).check(ChangePermission.READ);
     } catch (AuthException e) {
diff --git a/java/com/google/gerrit/server/restapi/change/Restore.java b/java/com/google/gerrit/server/restapi/change/Restore.java
index d6f9e2b..5f56cdb 100644
--- a/java/com/google/gerrit/server/restapi/change/Restore.java
+++ b/java/com/google/gerrit/server/restapi/change/Restore.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.flogger.FluentLogger;
+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;
@@ -25,7 +26,6 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.PatchSetUtil;
@@ -46,9 +46,7 @@
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 
@@ -58,7 +56,6 @@
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final RestoredSender.Factory restoredSenderFactory;
-  private final Provider<ReviewDb> dbProvider;
   private final ChangeJson.Factory json;
   private final ChangeMessagesUtil cmUtil;
   private final PatchSetUtil psUtil;
@@ -68,7 +65,6 @@
   @Inject
   Restore(
       RestoredSender.Factory restoredSenderFactory,
-      Provider<ReviewDb> dbProvider,
       ChangeJson.Factory json,
       ChangeMessagesUtil cmUtil,
       PatchSetUtil psUtil,
@@ -77,7 +73,6 @@
       ProjectCache projectCache) {
     super(retryHelper);
     this.restoredSenderFactory = restoredSenderFactory;
-    this.dbProvider = dbProvider;
     this.json = json;
     this.cmUtil = cmUtil;
     this.psUtil = psUtil;
@@ -88,18 +83,16 @@
   @Override
   protected ChangeInfo applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, RestoreInput input)
-      throws RestApiException, UpdateException, OrmException, PermissionBackendException,
-          IOException {
+      throws RestApiException, UpdateException, PermissionBackendException, IOException {
     // Not allowed to restore if the current patch set is locked.
     psUtil.checkPatchSetNotLocked(rsrc.getNotes());
 
-    rsrc.permissions().database(dbProvider).check(ChangePermission.RESTORE);
+    rsrc.permissions().check(ChangePermission.RESTORE);
     projectCache.checkedGet(rsrc.getProject()).checkStatePermitsWrite();
 
     Op op = new Op(input);
     try (BatchUpdate u =
-        updateFactory.create(
-            dbProvider.get(), rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+        updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
       u.addOp(rsrc.getId(), op).execute();
     }
     return json.noOptions().format(op.change);
@@ -117,20 +110,20 @@
     }
 
     @Override
-    public boolean updateChange(ChangeContext ctx) throws OrmException, ResourceConflictException {
+    public boolean updateChange(ChangeContext ctx) throws ResourceConflictException {
       change = ctx.getChange();
-      if (change == null || change.getStatus() != Status.ABANDONED) {
+      if (change == null || !change.isAbandoned()) {
         throw new ResourceConflictException("change is " + ChangeUtil.status(change));
       }
       PatchSet.Id psId = change.currentPatchSetId();
       ChangeUpdate update = ctx.getUpdate(psId);
-      patchSet = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
+      patchSet = psUtil.get(ctx.getNotes(), psId);
       change.setStatus(Status.NEW);
       change.setLastUpdatedOn(ctx.getWhen());
       update.setStatus(change.getStatus());
 
       message = newMessage(ctx);
-      cmUtil.addChangeMessage(ctx.getDb(), update, message);
+      cmUtil.addChangeMessage(update, message);
       return true;
     }
 
@@ -145,7 +138,7 @@
     }
 
     @Override
-    public void postUpdate(Context ctx) throws OrmException {
+    public void postUpdate(Context ctx) {
       try {
         ReplyToChangeSender cm = restoredSenderFactory.create(ctx.getProject(), change.getId());
         cm.setFrom(ctx.getAccountId());
@@ -168,7 +161,7 @@
             .setVisible(false);
 
     Change change = rsrc.getChange();
-    if (change.getStatus() != Status.ABANDONED) {
+    if (!change.isAbandoned()) {
       return description;
     }
 
@@ -186,13 +179,13 @@
       if (psUtil.isPatchSetLocked(rsrc.getNotes())) {
         return description;
       }
-    } catch (OrmException | IOException e) {
+    } catch (StorageException | IOException e) {
       logger.atSevere().withCause(e).log(
           "Failed to check if the current patch set of change %s is locked", change.getId());
       return description;
     }
 
-    boolean visible = rsrc.permissions().database(dbProvider).testOrFalse(ChangePermission.RESTORE);
+    boolean visible = rsrc.permissions().testOrFalse(ChangePermission.RESTORE);
     return description.setVisible(visible);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/Revert.java b/java/com/google/gerrit/server/restapi/change/Revert.java
index 7309fde..3fe6770 100644
--- a/java/com/google/gerrit/server/restapi/change/Revert.java
+++ b/java/com/google/gerrit/server/restapi/change/Revert.java
@@ -14,14 +14,13 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.common.base.MoreObjects.firstNonNull;
 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.collect.ListMultimap;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.api.changes.RevertInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -33,7 +32,6 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
@@ -41,17 +39,17 @@
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.ReviewerSet;
-import com.google.gerrit.server.Sequences;
 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.NotifyUtil;
+import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.extensions.events.ChangeReverted;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.mail.send.RevertedSender;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
+import com.google.gerrit.server.notedb.Sequences;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ContributorAgreementsChecker;
@@ -66,7 +64,6 @@
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -92,7 +89,6 @@
     implements UiAction<ChangeResource> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  private final Provider<ReviewDb> db;
   private final PermissionBackend permissionBackend;
   private final GitRepositoryManager repoManager;
   private final ChangeInserter.Factory changeInserterFactory;
@@ -106,11 +102,10 @@
   private final ChangeReverted changeReverted;
   private final ContributorAgreementsChecker contributorAgreements;
   private final ProjectCache projectCache;
-  private final NotifyUtil notifyUtil;
+  private final NotifyResolver notifyResolver;
 
   @Inject
   Revert(
-      Provider<ReviewDb> db,
       PermissionBackend permissionBackend,
       GitRepositoryManager repoManager,
       ChangeInserter.Factory changeInserterFactory,
@@ -125,9 +120,8 @@
       ChangeReverted changeReverted,
       ContributorAgreementsChecker contributorAgreements,
       ProjectCache projectCache,
-      NotifyUtil notifyUtil) {
+      NotifyResolver notifyResolver) {
     super(retryHelper);
-    this.db = db;
     this.permissionBackend = permissionBackend;
     this.repoManager = repoManager;
     this.changeInserterFactory = changeInserterFactory;
@@ -141,16 +135,16 @@
     this.changeReverted = changeReverted;
     this.contributorAgreements = contributorAgreements;
     this.projectCache = projectCache;
-    this.notifyUtil = notifyUtil;
+    this.notifyResolver = notifyResolver;
   }
 
   @Override
   public ChangeInfo applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, RevertInput input)
-      throws IOException, OrmException, RestApiException, UpdateException, NoSuchChangeException,
+      throws IOException, RestApiException, UpdateException, NoSuchChangeException,
           PermissionBackendException, NoSuchProjectException, ConfigInvalidException {
     Change change = rsrc.getChange();
-    if (change.getStatus() != Change.Status.MERGED) {
+    if (!change.isMerged()) {
       throw new ResourceConflictException("change is " + ChangeUtil.status(change));
     }
 
@@ -164,11 +158,11 @@
 
   private Change.Id revert(
       BatchUpdate.Factory updateFactory, ChangeNotes notes, CurrentUser user, RevertInput input)
-      throws OrmException, IOException, RestApiException, UpdateException, ConfigInvalidException {
+      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(db.get(), notes, patchSetId);
+    PatchSet patch = psUtil.get(notes, patchSetId);
     if (patch == null) {
       throw new ResourceNotFoundException(changeIdToRevert.toString());
     }
@@ -220,18 +214,17 @@
       ObjectId id = oi.insert(revertCommitBuilder);
       RevCommit revertCommit = revWalk.parseCommit(id);
 
-      ListMultimap<RecipientType, Account.Id> accountsToNotify =
-          notifyUtil.resolveAccounts(input.notifyDetails);
+      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());
       ins.setMessage("Uploaded patch set 1.");
-      ins.setNotify(input.notify);
-      ins.setAccountsToNotify(accountsToNotify);
 
-      ReviewerSet reviewerSet = approvalsUtil.getReviewers(db.get(), notes);
+      ReviewerSet reviewerSet = approvalsUtil.getReviewers(notes);
 
       Set<Account.Id> reviewers = new HashSet<>();
       reviewers.add(changeToRevert.getOwner());
@@ -242,10 +235,11 @@
       ins.setReviewersAndCcs(reviewers, ccs);
       ins.setRevertOf(changeIdToRevert);
 
-      try (BatchUpdate bu = updateFactory.create(db.get(), project, user, now)) {
+      try (BatchUpdate bu = updateFactory.create(project, user, now)) {
         bu.setRepository(git, revWalk, oi);
+        bu.setNotify(notify);
         bu.insertChange(ins);
-        bu.addOp(changeId, new NotifyOp(changeToRevert, ins, input.notify, accountsToNotify));
+        bu.addOp(changeId, new NotifyOp(changeToRevert, ins));
         bu.addOp(changeToRevert.getId(), new PostRevertedMessageOp(computedChangeId));
         bu.execute();
       }
@@ -270,7 +264,7 @@
         .setTitle("Revert the change")
         .setVisible(
             and(
-                change.getStatus() == Change.Status.MERGED && projectStatePermitsWrite,
+                change.isMerged() && projectStatePermitsWrite,
                 permissionBackend
                     .user(rsrc.getUser())
                     .ref(change.getDest())
@@ -280,18 +274,10 @@
   private class NotifyOp implements BatchUpdateOp {
     private final Change change;
     private final ChangeInserter ins;
-    private final NotifyHandling notifyHandling;
-    private final ListMultimap<RecipientType, Account.Id> accountsToNotify;
 
-    NotifyOp(
-        Change change,
-        ChangeInserter ins,
-        NotifyHandling notifyHandling,
-        ListMultimap<RecipientType, Account.Id> accountsToNotify) {
+    NotifyOp(Change change, ChangeInserter ins) {
       this.change = change;
       this.ins = ins;
-      this.notifyHandling = notifyHandling;
-      this.accountsToNotify = accountsToNotify;
     }
 
     @Override
@@ -300,8 +286,7 @@
       try {
         RevertedSender cm = revertedSenderFactory.create(ctx.getProject(), change.getId());
         cm.setFrom(ctx.getAccountId());
-        cm.setNotify(notifyHandling);
-        cm.setAccountsToNotify(accountsToNotify);
+        cm.setNotify(ctx.getNotify(change.getId()));
         cm.send();
       } catch (Exception err) {
         logger.atSevere().withCause(err).log(
@@ -318,7 +303,7 @@
     }
 
     @Override
-    public boolean updateChange(ChangeContext ctx) throws Exception {
+    public boolean updateChange(ChangeContext ctx) {
       Change change = ctx.getChange();
       PatchSet.Id patchSetId = change.currentPatchSetId();
       ChangeMessage changeMessage =
@@ -326,7 +311,7 @@
               ctx,
               "Created a revert of this change as I" + computedChangeId.name(),
               ChangeMessagesUtil.TAG_REVERT);
-      cmUtil.addChangeMessage(ctx.getDb(), ctx.getUpdate(patchSetId), changeMessage);
+      cmUtil.addChangeMessage(ctx.getUpdate(patchSetId), changeMessage);
       return true;
     }
   }
diff --git a/java/com/google/gerrit/server/restapi/change/Reviewed.java b/java/com/google/gerrit/server/restapi/change/Reviewed.java
index accc355..4594503 100644
--- a/java/com/google/gerrit/server/restapi/change/Reviewed.java
+++ b/java/com/google/gerrit/server/restapi/change/Reviewed.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.server.change.AccountPatchReviewStore;
 import com.google.gerrit.server.change.FileResource;
 import com.google.gerrit.server.plugincontext.PluginItemContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -36,15 +35,14 @@
     }
 
     @Override
-    public Response<String> apply(FileResource resource, Input input) throws OrmException {
+    public Response<String> apply(FileResource resource, Input input) {
       boolean reviewFlagUpdated =
           accountPatchReviewStore.call(
               s ->
                   s.markReviewed(
                       resource.getPatchKey().getParentKey(),
                       resource.getAccountId(),
-                      resource.getPatchKey().getFileName()),
-              OrmException.class);
+                      resource.getPatchKey().getFileName()));
       return reviewFlagUpdated ? Response.created("") : Response.ok("");
     }
   }
@@ -59,14 +57,13 @@
     }
 
     @Override
-    public Response<?> apply(FileResource resource, Input input) throws OrmException {
+    public Response<?> apply(FileResource resource, Input input) {
       accountPatchReviewStore.run(
           s ->
               s.clearReviewed(
                   resource.getPatchKey().getParentKey(),
                   resource.getAccountId(),
-                  resource.getPatchKey().getFileName()),
-          OrmException.class);
+                  resource.getPatchKey().getFileName()));
       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 ece52bf..ed487f7 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
@@ -27,7 +27,6 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.FanOutExecutor;
 import com.google.gerrit.server.change.ReviewerSuggestion;
@@ -40,7 +39,6 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -51,7 +49,6 @@
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
 import java.util.Set;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
@@ -80,7 +77,6 @@
   private final PluginMapContext<ReviewerSuggestion> reviewerSuggestionPluginMap;
   private final Provider<InternalChangeQuery> queryProvider;
   private final ExecutorService executor;
-  private final Provider<ReviewDb> dbProvider;
   private final ApprovalsUtil approvalsUtil;
 
   @Inject
@@ -89,7 +85,6 @@
       PluginMapContext<ReviewerSuggestion> reviewerSuggestionPluginMap,
       Provider<InternalChangeQuery> queryProvider,
       @FanOutExecutor ExecutorService executor,
-      Provider<ReviewDb> dbProvider,
       ApprovalsUtil approvalsUtil,
       @GerritServerConfig Config config) {
     this.changeQueryBuilder = changeQueryBuilder;
@@ -97,7 +92,6 @@
     this.queryProvider = queryProvider;
     this.reviewerSuggestionPluginMap = reviewerSuggestionPluginMap;
     this.executor = executor;
-    this.dbProvider = dbProvider;
     this.approvalsUtil = approvalsUtil;
   }
 
@@ -106,7 +100,7 @@
       SuggestReviewers suggestReviewers,
       ProjectState projectState,
       List<Account.Id> candidateList)
-      throws OrmException, IOException, ConfigInvalidException {
+      throws IOException, ConfigInvalidException {
     logger.atFine().log("Candidates %s", candidateList);
 
     String query = suggestReviewers.getQuery();
@@ -183,7 +177,7 @@
 
       // Remove existing reviewers
       approvalsUtil
-          .getReviewers(dbProvider.get(), changeNotes)
+          .getReviewers(changeNotes)
           .byState(REVIEWER)
           .forEach(
               r -> {
@@ -194,7 +188,7 @@
     }
 
     // Sort results
-    Stream<Entry<Account.Id, MutableDouble>> sorted =
+    Stream<Map.Entry<Account.Id, MutableDouble>> sorted =
         reviewerScores.entrySet().stream()
             .sorted(Collections.reverseOrder(Map.Entry.comparingByValue()));
     List<Account.Id> sortedSuggestions = sorted.map(Map.Entry::getKey).collect(toList());
@@ -203,7 +197,7 @@
   }
 
   private Map<Account.Id, MutableDouble> baseRankingForEmptyQuery(double baseWeight)
-      throws OrmException, IOException, ConfigInvalidException {
+      throws IOException, ConfigInvalidException {
     // Get the user's last 25 changes, check approvals
     try {
       List<ChangeData> result =
@@ -233,7 +227,7 @@
 
   private Map<Account.Id, MutableDouble> baseRankingForCandidateList(
       List<Account.Id> candidates, ProjectState projectState, double baseWeight)
-      throws OrmException, IOException, ConfigInvalidException {
+      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).
diff --git a/java/com/google/gerrit/server/restapi/change/Reviewers.java b/java/com/google/gerrit/server/restapi/change/Reviewers.java
index f0aef13..546ca01 100644
--- a/java/com/google/gerrit/server/restapi/change/Reviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/Reviewers.java
@@ -23,14 +23,11 @@
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.ReviewerResource;
 import com.google.gerrit.server.restapi.account.AccountsCollection;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.Collection;
@@ -39,7 +36,6 @@
 @Singleton
 public class Reviewers implements ChildCollection<ChangeResource, ReviewerResource> {
   private final DynamicMap<RestView<ReviewerResource>> views;
-  private final Provider<ReviewDb> dbProvider;
   private final ApprovalsUtil approvalsUtil;
   private final AccountsCollection accounts;
   private final ReviewerResource.Factory resourceFactory;
@@ -47,13 +43,11 @@
 
   @Inject
   Reviewers(
-      Provider<ReviewDb> dbProvider,
       ApprovalsUtil approvalsUtil,
       AccountsCollection accounts,
       ReviewerResource.Factory resourceFactory,
       DynamicMap<RestView<ReviewerResource>> views,
       ListReviewers list) {
-    this.dbProvider = dbProvider;
     this.approvalsUtil = approvalsUtil;
     this.accounts = accounts;
     this.resourceFactory = resourceFactory;
@@ -73,8 +67,7 @@
 
   @Override
   public ReviewerResource parse(ChangeResource rsrc, IdString id)
-      throws OrmException, ResourceNotFoundException, AuthException, IOException,
-          ConfigInvalidException {
+      throws ResourceNotFoundException, AuthException, IOException, ConfigInvalidException {
     Address address = Address.tryParse(id.get());
 
     Account.Id accountId = null;
@@ -98,7 +91,7 @@
     throw new ResourceNotFoundException(id);
   }
 
-  private Collection<Account.Id> fetchAccountIds(ChangeResource rsrc) throws OrmException {
-    return approvalsUtil.getReviewers(dbProvider.get(), rsrc.getNotes()).all();
+  private Collection<Account.Id> fetchAccountIds(ChangeResource rsrc) {
+    return approvalsUtil.getReviewers(rsrc.getNotes()).all();
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
index ff628db..f5907e7 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
@@ -32,6 +32,7 @@
 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.metrics.Description;
 import com.google.gerrit.metrics.Description.Units;
 import com.google.gerrit.metrics.MetricMaker;
@@ -54,8 +55,6 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.account.AccountPredicates;
 import com.google.gerrit.server.query.account.AccountQueryBuilder;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -114,9 +113,9 @@
     }
   }
 
-  // Generate a candidate list at 2x the size of what the user wants to see to
+  // 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 = 2;
+  private static final int CANDIDATE_LIST_MULTIPLIER = 3;
 
   private final AccountLoader.Factory accountLoaderFactory;
   private final AccountQueryBuilder accountQueryBuilder;
@@ -154,7 +153,7 @@
   }
 
   public interface VisibilityControl {
-    boolean isVisibleTo(Account.Id account) throws OrmException;
+    boolean isVisibleTo(Account.Id account);
   }
 
   public List<SuggestedReviewerInfo> suggestReviewers(
@@ -163,7 +162,7 @@
       ProjectState projectState,
       VisibilityControl visibilityControl,
       boolean excludeGroups)
-      throws IOException, OrmException, ConfigInvalidException, PermissionBackendException {
+      throws IOException, ConfigInvalidException, PermissionBackendException {
     CurrentUser currentUser = self.get();
     if (changeNotes != null) {
       logger.atFine().log(
@@ -222,7 +221,7 @@
     return suggestedReviewers;
   }
 
-  private List<Account.Id> suggestAccounts(SuggestReviewers suggestReviewers) throws OrmException {
+  private List<Account.Id> suggestAccounts(SuggestReviewers suggestReviewers) {
     try (Timer0.Context ctx = metrics.queryAccountsLatency.start()) {
       try {
         // For performance reasons we don't use AccountQueryProvider as it would always load the
@@ -262,7 +261,7 @@
       VisibilityControl visibilityControl,
       boolean excludeGroups,
       List<Account.Id> filteredRecommendations)
-      throws OrmException, PermissionBackendException, IOException {
+      throws PermissionBackendException, IOException {
     List<SuggestedReviewerInfo> suggestedReviewers = loadAccounts(filteredRecommendations);
 
     int limit = suggestReviewers.getLimit();
@@ -291,7 +290,7 @@
       SuggestReviewers suggestReviewers,
       ProjectState projectState,
       List<Account.Id> candidateList)
-      throws OrmException, IOException, ConfigInvalidException {
+      throws IOException, ConfigInvalidException {
     try (Timer0.Context ctx = metrics.recommendAccountsLatency.start()) {
       return reviewerRecommender.suggestReviewers(
           changeNotes, suggestReviewers, projectState, candidateList);
@@ -327,7 +326,7 @@
       ProjectState projectState,
       VisibilityControl visibilityControl,
       int limit)
-      throws OrmException, IOException {
+      throws IOException {
     try (Timer0.Context ctx = metrics.queryGroupsLatency.start()) {
       List<SuggestedReviewerInfo> groups = new ArrayList<>();
       for (GroupReference g : suggestAccountGroups(suggestReviewers, projectState)) {
@@ -372,7 +371,7 @@
       Project project,
       GroupReference group,
       VisibilityControl visibilityControl)
-      throws OrmException, IOException {
+      throws IOException {
     GroupAsReviewer result = new GroupAsReviewer();
     int maxAllowed = suggestReviewers.getMaxAllowed();
     int maxAllowedWithoutConfirmation = suggestReviewers.getMaxAllowedWithoutConfirmation();
@@ -396,7 +395,8 @@
         return result;
       }
 
-      boolean needsConfirmation = result.size > maxAllowedWithoutConfirmation;
+      boolean needsConfirmation =
+          maxAllowedWithoutConfirmation > 0 && result.size > maxAllowedWithoutConfirmation;
       if (needsConfirmation) {
         logger.atFine().log(
             "group %s needs confirmation to be added as reviewer, it has %d members",
diff --git a/java/com/google/gerrit/server/restapi/change/RevisionReviewers.java b/java/com/google/gerrit/server/restapi/change/RevisionReviewers.java
index b9b7a4f..a41143c 100644
--- a/java/com/google/gerrit/server/restapi/change/RevisionReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/RevisionReviewers.java
@@ -24,14 +24,11 @@
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.change.ReviewerResource;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.restapi.account.AccountsCollection;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.Collection;
@@ -40,7 +37,6 @@
 @Singleton
 public class RevisionReviewers implements ChildCollection<RevisionResource, ReviewerResource> {
   private final DynamicMap<RestView<ReviewerResource>> views;
-  private final Provider<ReviewDb> dbProvider;
   private final ApprovalsUtil approvalsUtil;
   private final AccountsCollection accounts;
   private final ReviewerResource.Factory resourceFactory;
@@ -48,13 +44,11 @@
 
   @Inject
   RevisionReviewers(
-      Provider<ReviewDb> dbProvider,
       ApprovalsUtil approvalsUtil,
       AccountsCollection accounts,
       ReviewerResource.Factory resourceFactory,
       DynamicMap<RestView<ReviewerResource>> views,
       ListRevisionReviewers list) {
-    this.dbProvider = dbProvider;
     this.approvalsUtil = approvalsUtil;
     this.accounts = accounts;
     this.resourceFactory = resourceFactory;
@@ -74,8 +68,8 @@
 
   @Override
   public ReviewerResource parse(RevisionResource rsrc, IdString id)
-      throws OrmException, ResourceNotFoundException, AuthException, MethodNotAllowedException,
-          IOException, ConfigInvalidException {
+      throws ResourceNotFoundException, AuthException, MethodNotAllowedException, IOException,
+          ConfigInvalidException {
     if (!rsrc.isCurrent()) {
       throw new MethodNotAllowedException("Cannot access on non-current patch set");
     }
@@ -89,8 +83,7 @@
         throw e;
       }
     }
-    Collection<Account.Id> reviewers =
-        approvalsUtil.getReviewers(dbProvider.get(), rsrc.getNotes()).all();
+    Collection<Account.Id> reviewers = approvalsUtil.getReviewers(rsrc.getNotes()).all();
     // See if the id exists as a reviewer for this change
     if (reviewers.contains(accountId)) {
       return resourceFactory.create(rsrc, accountId);
diff --git a/java/com/google/gerrit/server/restapi/change/Revisions.java b/java/com/google/gerrit/server/restapi/change/Revisions.java
index 557d77a..c5cce4f 100644
--- a/java/com/google/gerrit/server/restapi/change/Revisions.java
+++ b/java/com/google/gerrit/server/restapi/change/Revisions.java
@@ -24,7 +24,6 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.RevisionResource;
@@ -34,9 +33,7 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -48,7 +45,6 @@
 @Singleton
 public class Revisions implements ChildCollection<ChangeResource, RevisionResource> {
   private final DynamicMap<RestView<RevisionResource>> views;
-  private final Provider<ReviewDb> dbProvider;
   private final ChangeEditUtil editUtil;
   private final PatchSetUtil psUtil;
   private final PermissionBackend permissionBackend;
@@ -57,13 +53,11 @@
   @Inject
   Revisions(
       DynamicMap<RestView<RevisionResource>> views,
-      Provider<ReviewDb> dbProvider,
       ChangeEditUtil editUtil,
       PatchSetUtil psUtil,
       PermissionBackend permissionBackend,
       ProjectCache projectCache) {
     this.views = views;
-    this.dbProvider = dbProvider;
     this.editUtil = editUtil;
     this.psUtil = psUtil;
     this.permissionBackend = permissionBackend;
@@ -82,12 +76,11 @@
 
   @Override
   public RevisionResource parse(ChangeResource change, IdString id)
-      throws ResourceNotFoundException, AuthException, OrmException, IOException,
-          PermissionBackendException {
+      throws ResourceNotFoundException, AuthException, IOException, PermissionBackendException {
     if (id.get().equals("current")) {
-      PatchSet ps = psUtil.current(dbProvider.get(), change.getNotes());
+      PatchSet ps = psUtil.current(change.getNotes());
       if (ps != null && visible(change)) {
-        return RevisionResource.createNonCachable(change, ps);
+        return RevisionResource.createNonCacheable(change, ps);
       }
       throw new ResourceNotFoundException(id);
     }
@@ -114,7 +107,6 @@
       permissionBackend
           .user(change.getUser())
           .change(change.getNotes())
-          .database(dbProvider)
           .check(ChangePermission.READ);
       return projectCache.checkedGet(change.getProject()).statePermitsRead();
     } catch (AuthException e) {
@@ -123,7 +115,7 @@
   }
 
   private List<RevisionResource> find(ChangeResource change, String id)
-      throws OrmException, IOException, AuthException {
+      throws IOException, AuthException {
     if (id.equals("0") || id.equals("edit")) {
       return loadEdit(change, null);
     } else if (id.length() < 6 && id.matches("^[1-9][0-9]{0,4}$")) {
@@ -135,7 +127,7 @@
       return Collections.emptyList();
     } else {
       List<RevisionResource> out = new ArrayList<>();
-      for (PatchSet ps : psUtil.byChange(dbProvider.get(), change.getNotes())) {
+      for (PatchSet ps : psUtil.byChange(change.getNotes())) {
         if (ps.getRevision() != null && ps.getRevision().get().startsWith(id)) {
           out.add(new RevisionResource(change, ps));
         }
@@ -148,13 +140,9 @@
     }
   }
 
-  private List<RevisionResource> byLegacyPatchSetId(ChangeResource change, String id)
-      throws OrmException {
+  private List<RevisionResource> byLegacyPatchSetId(ChangeResource change, String id) {
     PatchSet ps =
-        psUtil.get(
-            dbProvider.get(),
-            change.getNotes(),
-            new PatchSet.Id(change.getId(), Integer.parseInt(id)));
+        psUtil.get(change.getNotes(), new PatchSet.Id(change.getId(), Integer.parseInt(id)));
     if (ps != null) {
       return Collections.singletonList(new RevisionResource(change, ps));
     }
diff --git a/java/com/google/gerrit/server/restapi/change/RobotComments.java b/java/com/google/gerrit/server/restapi/change/RobotComments.java
index 6570ae0..1aa8c2a 100644
--- a/java/com/google/gerrit/server/restapi/change/RobotComments.java
+++ b/java/com/google/gerrit/server/restapi/change/RobotComments.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.change.RobotCommentResource;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -56,7 +55,7 @@
 
   @Override
   public RobotCommentResource parse(RevisionResource rev, IdString id)
-      throws ResourceNotFoundException, OrmException {
+      throws ResourceNotFoundException {
     String uuid = id.get();
     ChangeNotes notes = rev.getNotes();
 
diff --git a/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java b/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
index f911147..aacf58b 100644
--- a/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
+++ b/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
@@ -14,64 +14,49 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.gerrit.extensions.conditions.BooleanCondition.and;
-import static com.google.gerrit.extensions.conditions.BooleanCondition.or;
 
+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.reviewdb.client.Change.Status;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.change.WorkInProgressOp;
 import com.google.gerrit.server.change.WorkInProgressOp.Input;
-import com.google.gerrit.server.permissions.GlobalPermission;
-import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 @Singleton
 public class SetReadyForReview extends RetryingRestModifyView<ChangeResource, Input, Response<?>>
     implements UiAction<ChangeResource> {
   private final WorkInProgressOp.Factory opFactory;
-  private final Provider<ReviewDb> db;
-  private final PermissionBackend permissionBackend;
-  private final Provider<CurrentUser> user;
 
   @Inject
-  SetReadyForReview(
-      RetryHelper retryHelper,
-      WorkInProgressOp.Factory opFactory,
-      Provider<ReviewDb> db,
-      PermissionBackend permissionBackend,
-      Provider<CurrentUser> user) {
+  SetReadyForReview(RetryHelper retryHelper, WorkInProgressOp.Factory opFactory) {
     super(retryHelper);
     this.opFactory = opFactory;
-    this.db = db;
-    this.permissionBackend = permissionBackend;
-    this.user = user;
   }
 
   @Override
   protected Response<?> applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input)
       throws RestApiException, UpdateException, PermissionBackendException {
-    WorkInProgressOp.checkPermissions(permissionBackend, user.get(), rsrc.getChange());
+    rsrc.permissions().check(ChangePermission.TOGGLE_WORK_IN_PROGRESS_STATE);
 
     Change change = rsrc.getChange();
-    if (change.getStatus() != Status.NEW) {
+    if (!change.isNew()) {
       throw new ResourceConflictException("change is " + ChangeUtil.status(change));
     }
 
@@ -80,7 +65,8 @@
     }
 
     try (BatchUpdate bu =
-        updateFactory.create(db.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+        updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+      bu.setNotify(NotifyResolver.Result.create(firstNonNull(input.notify, NotifyHandling.ALL)));
       bu.addOp(rsrc.getChange().getId(), opFactory.create(false, input));
       bu.execute();
       return Response.ok("");
@@ -94,16 +80,7 @@
         .setTitle("Set Ready For Review")
         .setVisible(
             and(
-                rsrc.getChange().getStatus() == Status.NEW && rsrc.getChange().isWorkInProgress(),
-                or(
-                    rsrc.isUserOwner(),
-                    or(
-                        permissionBackend
-                            .currentUser()
-                            .testCond(GlobalPermission.ADMINISTRATE_SERVER),
-                        permissionBackend
-                            .currentUser()
-                            .project(rsrc.getProject())
-                            .testCond(ProjectPermission.WRITE_CONFIG)))));
+                rsrc.getChange().isNew() && rsrc.getChange().isWorkInProgress(),
+                rsrc.permissions().testCond(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 da5d8bb..852813e 100644
--- a/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java
+++ b/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java
@@ -14,64 +14,49 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.gerrit.extensions.conditions.BooleanCondition.and;
-import static com.google.gerrit.extensions.conditions.BooleanCondition.or;
 
+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.reviewdb.client.Change.Status;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.change.WorkInProgressOp;
 import com.google.gerrit.server.change.WorkInProgressOp.Input;
-import com.google.gerrit.server.permissions.GlobalPermission;
-import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 @Singleton
 public class SetWorkInProgress extends RetryingRestModifyView<ChangeResource, Input, Response<?>>
     implements UiAction<ChangeResource> {
   private final WorkInProgressOp.Factory opFactory;
-  private final Provider<ReviewDb> db;
-  private final PermissionBackend permissionBackend;
-  private final Provider<CurrentUser> user;
 
   @Inject
-  SetWorkInProgress(
-      WorkInProgressOp.Factory opFactory,
-      RetryHelper retryHelper,
-      Provider<ReviewDb> db,
-      PermissionBackend permissionBackend,
-      Provider<CurrentUser> user) {
+  SetWorkInProgress(WorkInProgressOp.Factory opFactory, RetryHelper retryHelper) {
     super(retryHelper);
     this.opFactory = opFactory;
-    this.db = db;
-    this.permissionBackend = permissionBackend;
-    this.user = user;
   }
 
   @Override
   protected Response<?> applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input)
       throws RestApiException, UpdateException, PermissionBackendException {
-    WorkInProgressOp.checkPermissions(permissionBackend, user.get(), rsrc.getChange());
+    rsrc.permissions().check(ChangePermission.TOGGLE_WORK_IN_PROGRESS_STATE);
 
     Change change = rsrc.getChange();
-    if (change.getStatus() != Status.NEW) {
+    if (!change.isNew()) {
       throw new ResourceConflictException("change is " + ChangeUtil.status(change));
     }
 
@@ -80,7 +65,8 @@
     }
 
     try (BatchUpdate bu =
-        updateFactory.create(db.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+        updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+      bu.setNotify(NotifyResolver.Result.create(firstNonNull(input.notify, NotifyHandling.NONE)));
       bu.addOp(rsrc.getChange().getId(), opFactory.create(true, input));
       bu.execute();
       return Response.ok("");
@@ -94,16 +80,7 @@
         .setTitle("Set Work In Progress")
         .setVisible(
             and(
-                rsrc.getChange().getStatus() == Status.NEW && !rsrc.getChange().isWorkInProgress(),
-                or(
-                    rsrc.isUserOwner(),
-                    or(
-                        permissionBackend
-                            .currentUser()
-                            .testCond(GlobalPermission.ADMINISTRATE_SERVER),
-                        permissionBackend
-                            .currentUser()
-                            .project(rsrc.getProject())
-                            .testCond(ProjectPermission.WRITE_CONFIG)))));
+                rsrc.getChange().isNew() && !rsrc.getChange().isWorkInProgress(),
+                rsrc.permissions().testCond(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 a775c93..51e2dfb 100644
--- a/java/com/google/gerrit/server/restapi/change/Submit.java
+++ b/java/com/google/gerrit/server/restapi/change/Submit.java
@@ -23,6 +23,7 @@
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.ParameterizedString;
+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;
@@ -36,7 +37,6 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -60,8 +60,6 @@
 import com.google.gerrit.server.submit.MergeOp;
 import com.google.gerrit.server.submit.MergeSuperSet;
 import com.google.gerrit.server.update.UpdateException;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.OrmRuntimeException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -70,7 +68,6 @@
 import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -107,7 +104,6 @@
     }
   }
 
-  private final Provider<ReviewDb> dbProvider;
   private final GitRepositoryManager repoManager;
   private final PermissionBackend permissionBackend;
   private final ChangeData.Factory changeDataFactory;
@@ -128,7 +124,6 @@
 
   @Inject
   Submit(
-      Provider<ReviewDb> dbProvider,
       GitRepositoryManager repoManager,
       PermissionBackend permissionBackend,
       ChangeData.Factory changeDataFactory,
@@ -140,7 +135,6 @@
       Provider<InternalChangeQuery> queryProvider,
       PatchSetUtil psUtil,
       ProjectCache projectCache) {
-    this.dbProvider = dbProvider;
     this.repoManager = repoManager;
     this.permissionBackend = permissionBackend;
     this.changeDataFactory = changeDataFactory;
@@ -180,8 +174,8 @@
 
   @Override
   public Output apply(RevisionResource rsrc, SubmitInput input)
-      throws RestApiException, RepositoryNotFoundException, IOException, OrmException,
-          PermissionBackendException, UpdateException, ConfigInvalidException {
+      throws RestApiException, RepositoryNotFoundException, IOException, PermissionBackendException,
+          UpdateException, ConfigInvalidException {
     input.onBehalfOf = Strings.emptyToNull(input.onBehalfOf);
     IdentifiedUser submitter;
     if (input.onBehalfOf != null) {
@@ -196,10 +190,10 @@
   }
 
   public Change mergeChange(RevisionResource rsrc, IdentifiedUser submitter, SubmitInput input)
-      throws OrmException, RestApiException, IOException, UpdateException, ConfigInvalidException,
+      throws RestApiException, IOException, UpdateException, ConfigInvalidException,
           PermissionBackendException {
     Change change = rsrc.getChange();
-    if (!change.getStatus().isOpen()) {
+    if (!change.isNew()) {
       throw new ResourceConflictException("change is " + ChangeUtil.status(change));
     } else if (!ProjectUtil.branchExists(repoManager, change.getDest())) {
       throw new ResourceConflictException(
@@ -212,26 +206,24 @@
     }
 
     try (MergeOp op = mergeOpProvider.get()) {
-      ReviewDb db = dbProvider.get();
-      op.merge(db, change, submitter, true, input, false);
-      try {
-        change =
-            changeNotesFactory.createChecked(db, change.getProject(), change.getId()).getChange();
-      } catch (NoSuchChangeException e) {
-        throw new ResourceConflictException("change is deleted", e);
-      }
+      op.merge(change, submitter, true, input, false);
     }
 
-    switch (change.getStatus()) {
-      case MERGED:
-        return change;
-      case NEW:
-        throw new RestApiException(
-            "change unexpectedly had status " + change.getStatus() + " after submit attempt");
-      case ABANDONED:
-      default:
-        throw new ResourceConflictException("change is " + ChangeUtil.status(change));
+    // Read the ChangeNotes only after MergeOp is fully done (including MergeOp#close) to be sure
+    // to have the correct state of the repo.
+    try {
+      change = changeNotesFactory.createChecked(change.getProject(), change.getId()).getChange();
+    } catch (NoSuchChangeException e) {
+      throw new ResourceConflictException("change is deleted", e);
     }
+
+    if (change.isMerged()) {
+      return change;
+    }
+    if (change.isNew()) {
+      throw new RestApiException("change unexpectedly had status NEW after submit attempt");
+    }
+    throw new ResourceConflictException("change is " + ChangeUtil.status(change));
   }
 
   /**
@@ -257,7 +249,6 @@
         Set<ChangePermission> can =
             permissionBackend
                 .user(user)
-                .database(dbProvider)
                 .change(c)
                 .test(EnumSet.of(ChangePermission.READ, ChangePermission.SUBMIT));
         if (!can.contains(ChangePermission.READ)) {
@@ -292,9 +283,9 @@
         return "Problems with change(s): "
             + unmergeable.stream().map(c -> c.getId().toString()).collect(joining(", "));
       }
-    } catch (PermissionBackendException | OrmException | IOException e) {
+    } catch (PermissionBackendException | IOException e) {
       logger.atSevere().withCause(e).log("Error checking if change is submittable");
-      throw new OrmRuntimeException("Could not determine problems for the change", e);
+      throw new StorageException("Could not determine problems for the change", e);
     }
     return null;
   }
@@ -302,7 +293,7 @@
   @Override
   public UiAction.Description getDescription(RevisionResource resource) {
     Change change = resource.getChange();
-    if (!change.getStatus().isOpen() || !resource.isCurrent()) {
+    if (!change.isNew() || !resource.isCurrent()) {
       return null; // submit not visible
     }
 
@@ -312,51 +303,39 @@
       }
     } catch (IOException e) {
       logger.atSevere().withCause(e).log("Error checking if change is submittable");
-      throw new OrmRuntimeException("Could not determine problems for the change", e);
+      throw new StorageException("Could not determine problems for the change", e);
     }
 
-    ReviewDb db = dbProvider.get();
-    ChangeData cd = changeDataFactory.create(db, resource.getNotes());
+    ChangeData cd = changeDataFactory.create(resource.getNotes());
     try {
       MergeOp.checkSubmitRule(cd, false);
     } catch (ResourceConflictException e) {
       return null; // submit not visible
-    } catch (OrmException e) {
-      logger.atSevere().withCause(e).log("Error checking if change is submittable");
-      throw new OrmRuntimeException("Could not determine problems for the change", e);
     }
 
     ChangeSet cs;
     try {
-      cs = mergeSuperSet.get().completeChangeSet(db, cd.change(), resource.getUser());
-    } catch (OrmException | IOException | PermissionBackendException e) {
-      throw new OrmRuntimeException(
-          "Could not determine complete set of changes to be submitted", e);
+      cs = mergeSuperSet.get().completeChangeSet(cd.change(), resource.getUser());
+    } catch (IOException | PermissionBackendException e) {
+      throw new StorageException("Could not determine complete set of changes to be submitted", e);
     }
 
     String topic = change.getTopic();
     int topicSize = 0;
     if (!Strings.isNullOrEmpty(topic)) {
-      topicSize = getChangesByTopic(topic).size();
+      topicSize = queryProvider.get().noFields().byTopicOpen(topic).size();
     }
     boolean treatWithTopic = submitWholeTopic && !Strings.isNullOrEmpty(topic) && topicSize > 1;
 
     String submitProblems = problemsForSubmittingChangeset(cd, cs, resource.getUser());
 
-    Boolean enabled;
-    try {
-      // Recheck mergeability rather than using value stored in the index,
-      // which may be stale.
-      // TODO(dborowitz): This is ugly; consider providing a way to not read
-      // stored fields from the index in the first place.
-      // cd.setMergeable(null);
-      // That was done in unmergeableChanges which was called by
-      // problemsForSubmittingChangeset, so now it is safe to read from
-      // the cache, as it yields the same result.
-      enabled = cd.isMergeable();
-    } catch (OrmException e) {
-      throw new OrmRuntimeException("Could not determine mergeability", e);
-    }
+    // Recheck mergeability rather than using value stored in the index, which may be stale.
+    // TODO(dborowitz): This is ugly; consider providing a way to not read stored fields from the
+    // index in the first place.
+    // cd.setMergeable(null);
+    // That was done in unmergeableChanges which was called by problemsForSubmittingChangeset, so
+    // now it is safe to read from the cache, as it yields the same result.
+    Boolean enabled = cd.isMergeable();
 
     if (submitProblems != null) {
       return new UiAction.Description()
@@ -392,7 +371,7 @@
         .setEnabled(Boolean.TRUE.equals(enabled));
   }
 
-  public Collection<ChangeData> unmergeableChanges(ChangeSet cs) throws OrmException, IOException {
+  public Collection<ChangeData> unmergeableChanges(ChangeSet cs) throws IOException {
     Set<ChangeData> mergeabilityMap = new HashSet<>();
     for (ChangeData change : cs.changes()) {
       mergeabilityMap.add(change);
@@ -441,15 +420,14 @@
   }
 
   private HashMap<Change.Id, RevCommit> findCommits(
-      Collection<ChangeData> changes, Project.NameKey project) throws IOException, OrmException {
+      Collection<ChangeData> changes, Project.NameKey project) throws IOException {
     HashMap<Change.Id, RevCommit> commits = new HashMap<>();
     try (Repository repo = repoManager.openRepository(project);
         RevWalk walk = new RevWalk(repo)) {
       for (ChangeData change : changes) {
         RevCommit commit =
             walk.parseCommit(
-                ObjectId.fromString(
-                    psUtil.current(dbProvider.get(), change.notes()).getRevision().get()));
+                ObjectId.fromString(psUtil.current(change.notes()).getRevision().get()));
         commits.put(change.getId(), commit);
       }
     }
@@ -457,20 +435,17 @@
   }
 
   private IdentifiedUser onBehalfOf(RevisionResource rsrc, SubmitInput in)
-      throws AuthException, UnprocessableEntityException, OrmException, PermissionBackendException,
-          IOException, ConfigInvalidException {
-    PermissionBackend.ForChange perm = rsrc.permissions().database(dbProvider);
+      throws AuthException, UnprocessableEntityException, PermissionBackendException, IOException,
+          ConfigInvalidException {
+    PermissionBackend.ForChange perm = rsrc.permissions();
     perm.check(ChangePermission.SUBMIT);
     perm.check(ChangePermission.SUBMIT_AS);
 
     CurrentUser caller = rsrc.getUser();
-    IdentifiedUser submitter = accountResolver.parseOnBehalfOf(caller, in.onBehalfOf);
+    IdentifiedUser submitter =
+        accountResolver.resolve(in.onBehalfOf).asUniqueUserOnBehalfOf(caller);
     try {
-      permissionBackend
-          .user(submitter)
-          .database(dbProvider)
-          .change(rsrc.getNotes())
-          .check(ChangePermission.READ);
+      permissionBackend.user(submitter).change(rsrc.getNotes()).check(ChangePermission.READ);
     } catch (AuthException e) {
       throw new UnprocessableEntityException(
           String.format("on_behalf_of account %s cannot see change", submitter.getAccountId()), e);
@@ -478,27 +453,13 @@
     return submitter;
   }
 
-  private List<ChangeData> getChangesByTopic(String topic) {
-    try {
-      return queryProvider.get().byTopicOpen(topic);
-    } catch (OrmException e) {
-      throw new OrmRuntimeException(e);
-    }
-  }
-
   public static class CurrentRevision implements RestModifyView<ChangeResource, SubmitInput> {
-    private final Provider<ReviewDb> dbProvider;
     private final Submit submit;
     private final ChangeJson.Factory json;
     private final PatchSetUtil psUtil;
 
     @Inject
-    CurrentRevision(
-        Provider<ReviewDb> dbProvider,
-        Submit submit,
-        ChangeJson.Factory json,
-        PatchSetUtil psUtil) {
-      this.dbProvider = dbProvider;
+    CurrentRevision(Submit submit, ChangeJson.Factory json, PatchSetUtil psUtil) {
       this.submit = submit;
       this.json = json;
       this.psUtil = psUtil;
@@ -506,9 +467,9 @@
 
     @Override
     public ChangeInfo apply(ChangeResource rsrc, SubmitInput input)
-        throws RestApiException, RepositoryNotFoundException, IOException, OrmException,
+        throws RestApiException, RepositoryNotFoundException, IOException,
             PermissionBackendException, UpdateException, ConfigInvalidException {
-      PatchSet ps = psUtil.current(dbProvider.get(), rsrc.getNotes());
+      PatchSet ps = psUtil.current(rsrc.getNotes());
       if (ps == null) {
         throw new ResourceConflictException("current revision is missing");
       }
diff --git a/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java b/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java
index fa40329..efefc3e 100644
--- a/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java
+++ b/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java
@@ -19,16 +19,15 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.SubmittedTogetherInfo;
 import com.google.gerrit.extensions.api.changes.SubmittedTogetherOption;
-import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.ListChangesOption;
 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.RestReadView;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.WalkSorter;
@@ -38,7 +37,6 @@
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.server.submit.ChangeSet;
 import com.google.gerrit.server.submit.MergeSuperSet;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -62,7 +60,6 @@
       Comparator.comparing(ChangeData::project).thenComparing(cd -> cd.getId().id, reverseOrder());
 
   private final ChangeJson.Factory json;
-  private final Provider<ReviewDb> dbProvider;
   private final Provider<InternalChangeQuery> queryProvider;
   private final Provider<MergeSuperSet> mergeSuperSet;
   private final Provider<WalkSorter> sorter;
@@ -89,12 +86,10 @@
   @Inject
   SubmittedTogether(
       ChangeJson.Factory json,
-      Provider<ReviewDb> dbProvider,
       Provider<InternalChangeQuery> queryProvider,
       Provider<MergeSuperSet> mergeSuperSet,
       Provider<WalkSorter> sorter) {
     this.json = json;
-    this.dbProvider = dbProvider;
     this.queryProvider = queryProvider;
     this.mergeSuperSet = mergeSuperSet;
     this.sorter = sorter;
@@ -113,7 +108,7 @@
   @Override
   public Object apply(ChangeResource resource)
       throws AuthException, BadRequestException, ResourceConflictException, IOException,
-          OrmException, PermissionBackendException {
+          PermissionBackendException {
     SubmittedTogetherInfo info = applyInfo(resource);
     if (options.isEmpty()) {
       return info.changes;
@@ -122,18 +117,17 @@
   }
 
   public SubmittedTogetherInfo applyInfo(ChangeResource resource)
-      throws AuthException, IOException, OrmException, PermissionBackendException {
+      throws AuthException, IOException, PermissionBackendException {
     Change c = resource.getChange();
     try {
       List<ChangeData> cds;
       int hidden;
 
-      if (c.getStatus().isOpen()) {
-        ChangeSet cs =
-            mergeSuperSet.get().completeChangeSet(dbProvider.get(), c, resource.getUser());
+      if (c.isNew()) {
+        ChangeSet cs = mergeSuperSet.get().completeChangeSet(c, resource.getUser());
         cds = ensureRequiredDataIsLoaded(cs.changes().asList());
         hidden = cs.nonVisibleChanges().size();
-      } else if (c.getStatus().asChangeStatus() == ChangeStatus.MERGED) {
+      } else if (c.isMerged()) {
         cds = queryProvider.get().bySubmissionId(c.getSubmissionId());
         hidden = 0;
       } else {
@@ -150,13 +144,13 @@
       info.changes = json.create(jsonOpt).format(cds);
       info.nonVisibleChanges = hidden;
       return info;
-    } catch (OrmException | IOException e) {
+    } catch (StorageException | IOException e) {
       logger.atSevere().withCause(e).log("Error on getting a ChangeSet");
       throw e;
     }
   }
 
-  private List<ChangeData> sort(List<ChangeData> cds, int hidden) throws OrmException, IOException {
+  private List<ChangeData> sort(List<ChangeData> cds, int hidden) throws IOException {
     if (cds.size() <= 1 && hidden == 0) {
       // Skip sorting for singleton lists, to avoid WalkSorter opening the
       // repo just to fill out the commit field in PatchSetData.
@@ -181,8 +175,7 @@
     return sorted;
   }
 
-  private static List<ChangeData> ensureRequiredDataIsLoaded(List<ChangeData> cds)
-      throws OrmException {
+  private static List<ChangeData> ensureRequiredDataIsLoaded(List<ChangeData> cds) {
     // TODO(hiesel): Instead of calling these manually, either implement a helper that brings a
     // database-backed change on-par with an index-backed change in terms of the populated fields in
     // ChangeData or check if any of the ChangeDatas was loaded from the database and allow
diff --git a/java/com/google/gerrit/server/restapi/change/SuggestChangeReviewers.java b/java/com/google/gerrit/server/restapi/change/SuggestChangeReviewers.java
index 0ce5750..f5a2751 100644
--- a/java/com/google/gerrit/server/restapi/change/SuggestChangeReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/SuggestChangeReviewers.java
@@ -19,8 +19,6 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -29,7 +27,6 @@
 import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.restapi.change.ReviewersUtil.VisibilityControl;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -54,13 +51,12 @@
   @Inject
   SuggestChangeReviewers(
       AccountVisibility av,
-      Provider<ReviewDb> dbProvider,
       PermissionBackend permissionBackend,
       Provider<CurrentUser> self,
       @GerritServerConfig Config cfg,
       ReviewersUtil reviewersUtil,
       ProjectCache projectCache) {
-    super(av, dbProvider, cfg, reviewersUtil);
+    super(av, cfg, reviewersUtil);
     this.permissionBackend = permissionBackend;
     this.self = self;
     this.projectCache = projectCache;
@@ -68,7 +64,7 @@
 
   @Override
   public List<SuggestedReviewerInfo> apply(ChangeResource rsrc)
-      throws AuthException, BadRequestException, OrmException, IOException, ConfigInvalidException,
+      throws AuthException, BadRequestException, IOException, ConfigInvalidException,
           PermissionBackendException {
     if (!self.get().isIdentifiedUser()) {
       throw new AuthException("Authentication required");
@@ -83,17 +79,13 @@
 
   private VisibilityControl getVisibility(ChangeResource rsrc) {
 
-    return new VisibilityControl() {
-      @Override
-      public boolean isVisibleTo(Account.Id account) {
-        // Use the destination reference, not the change, as private changes deny anyone who is not
-        // already a reviewer.
-        return permissionBackend
-            .absentUser(account)
-            .database(dbProvider)
-            .ref(rsrc.getChange().getDest())
-            .testOrFalse(RefPermission.READ);
-      }
+    return account -> {
+      // Use the destination reference, not the change, as private changes deny anyone who is not
+      // already a reviewer.
+      return permissionBackend
+          .absentUser(account)
+          .ref(rsrc.getChange().getDest())
+          .testOrFalse(RefPermission.READ);
     };
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/SuggestReviewers.java b/java/com/google/gerrit/server/restapi/change/SuggestReviewers.java
index 6e94218..e071c89 100644
--- a/java/com/google/gerrit/server/restapi/change/SuggestReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/SuggestReviewers.java
@@ -18,13 +18,11 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.common.AccountVisibility;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.change.ReviewerAdder;
 import com.google.gerrit.server.config.ConfigKey;
 import com.google.gerrit.server.config.GerritConfigListener;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import org.eclipse.jgit.lib.Config;
 import org.kohsuke.args4j.Option;
 
@@ -33,7 +31,6 @@
 
   private static final int DEFAULT_MAX_SUGGESTED = 10;
 
-  protected final Provider<ReviewDb> dbProvider;
   protected final ReviewersUtil reviewersUtil;
 
   private final boolean suggestAccounts;
@@ -83,11 +80,7 @@
 
   @Inject
   public SuggestReviewers(
-      AccountVisibility av,
-      Provider<ReviewDb> dbProvider,
-      @GerritServerConfig Config cfg,
-      ReviewersUtil reviewersUtil) {
-    this.dbProvider = dbProvider;
+      AccountVisibility av, @GerritServerConfig Config cfg, ReviewersUtil reviewersUtil) {
     this.reviewersUtil = reviewersUtil;
     this.maxSuggestedReviewers =
         cfg.getInt("suggest", "maxSuggestedReviewers", DEFAULT_MAX_SUGGESTED);
diff --git a/java/com/google/gerrit/server/restapi/change/TestSubmitRule.java b/java/com/google/gerrit/server/restapi/change/TestSubmitRule.java
index c7eb781..4904da7 100644
--- a/java/com/google/gerrit/server/restapi/change/TestSubmitRule.java
+++ b/java/com/google/gerrit/server/restapi/change/TestSubmitRule.java
@@ -25,7 +25,6 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -36,15 +35,12 @@
 import com.google.gerrit.server.rules.DefaultSubmitRule;
 import com.google.gerrit.server.rules.PrologRule;
 import com.google.gerrit.server.rules.RulesCache;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import java.util.LinkedHashMap;
 import java.util.List;
 import org.kohsuke.args4j.Option;
 
 public class TestSubmitRule implements RestModifyView<RevisionResource, TestSubmitRuleInput> {
-  private final Provider<ReviewDb> db;
   private final ChangeData.Factory changeDataFactory;
   private final RulesCache rules;
   private final AccountLoader.Factory accountInfoFactory;
@@ -57,14 +53,12 @@
 
   @Inject
   TestSubmitRule(
-      Provider<ReviewDb> db,
       ChangeData.Factory changeDataFactory,
       RulesCache rules,
       AccountLoader.Factory infoFactory,
       ProjectCache projectCache,
       DefaultSubmitRule defaultSubmitRule,
       PrologRule prologRule) {
-    this.db = db;
     this.changeDataFactory = changeDataFactory;
     this.rules = rules;
     this.accountInfoFactory = infoFactory;
@@ -75,7 +69,7 @@
 
   @Override
   public List<TestSubmitRuleInfo> apply(RevisionResource rsrc, TestSubmitRuleInput input)
-      throws AuthException, OrmException, PermissionBackendException, BadRequestException {
+      throws AuthException, PermissionBackendException, BadRequestException {
     if (input == null) {
       input = new TestSubmitRuleInput();
     }
@@ -95,7 +89,7 @@
     if (projectState == null) {
       throw new BadRequestException("project not found");
     }
-    ChangeData cd = changeDataFactory.create(db.get(), rsrc.getNotes());
+    ChangeData cd = changeDataFactory.create(rsrc.getNotes());
     List<SubmitRecord> records;
     if (projectState.hasPrologRules() || input.rule != null) {
       records = ImmutableList.copyOf(prologRule.evaluate(cd, opts));
diff --git a/java/com/google/gerrit/server/restapi/change/TestSubmitType.java b/java/com/google/gerrit/server/restapi/change/TestSubmitType.java
index c1be1ce..46dbad6 100644
--- a/java/com/google/gerrit/server/restapi/change/TestSubmitType.java
+++ b/java/com/google/gerrit/server/restapi/change/TestSubmitType.java
@@ -23,19 +23,15 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 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.RulesCache;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import org.kohsuke.args4j.Option;
 
 public class TestSubmitType implements RestModifyView<RevisionResource, TestSubmitRuleInput> {
-  private final Provider<ReviewDb> db;
   private final ChangeData.Factory changeDataFactory;
   private final RulesCache rules;
   private final SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory;
@@ -45,11 +41,9 @@
 
   @Inject
   TestSubmitType(
-      Provider<ReviewDb> db,
       ChangeData.Factory changeDataFactory,
       RulesCache rules,
       SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory) {
-    this.db = db;
     this.changeDataFactory = changeDataFactory;
     this.rules = rules;
     this.submitRuleEvaluatorFactory = submitRuleEvaluatorFactory;
@@ -57,7 +51,7 @@
 
   @Override
   public SubmitType apply(RevisionResource rsrc, TestSubmitRuleInput input)
-      throws AuthException, BadRequestException, OrmException {
+      throws AuthException, BadRequestException {
     if (input == null) {
       input = new TestSubmitRuleInput();
     }
@@ -74,7 +68,7 @@
             .build();
 
     SubmitRuleEvaluator evaluator = submitRuleEvaluatorFactory.create(opts);
-    ChangeData cd = changeDataFactory.create(db.get(), rsrc.getNotes());
+    ChangeData cd = changeDataFactory.create(rsrc.getNotes());
     SubmitTypeRecord rec = evaluator.getSubmitType(cd);
 
     if (rec.status != SubmitTypeRecord.Status.OK) {
@@ -93,8 +87,7 @@
     }
 
     @Override
-    public SubmitType apply(RevisionResource resource)
-        throws AuthException, BadRequestException, OrmException {
+    public SubmitType apply(RevisionResource resource) throws AuthException, BadRequestException {
       return test.apply(resource, null);
     }
   }
diff --git a/java/com/google/gerrit/server/restapi/change/Unignore.java b/java/com/google/gerrit/server/restapi/change/Unignore.java
index 6f2144a..26d3233 100644
--- a/java/com/google/gerrit/server/restapi/change/Unignore.java
+++ b/java/com/google/gerrit/server/restapi/change/Unignore.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -22,7 +23,6 @@
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.StarredChangesUtil.IllegalLabelException;
 import com.google.gerrit.server.change.ChangeResource;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -46,8 +46,7 @@
   }
 
   @Override
-  public Response<String> apply(ChangeResource rsrc, Input input)
-      throws OrmException, IllegalLabelException {
+  public Response<String> apply(ChangeResource rsrc, Input input) throws IllegalLabelException {
     if (isIgnored(rsrc)) {
       stars.unignore(rsrc);
     }
@@ -57,7 +56,7 @@
   private boolean isIgnored(ChangeResource rsrc) {
     try {
       return stars.isIgnored(rsrc);
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("failed to check ignored star");
     }
     return false;
diff --git a/java/com/google/gerrit/server/restapi/change/Votes.java b/java/com/google/gerrit/server/restapi/change/Votes.java
index 3b2548c..31efe54 100644
--- a/java/com/google/gerrit/server/restapi/change/Votes.java
+++ b/java/com/google/gerrit/server/restapi/change/Votes.java
@@ -23,13 +23,10 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.change.ReviewerResource;
 import com.google.gerrit.server.change.VoteResource;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.util.Map;
 import java.util.TreeMap;
@@ -57,7 +54,7 @@
 
   @Override
   public VoteResource parse(ReviewerResource reviewer, IdString id)
-      throws ResourceNotFoundException, OrmException, AuthException, MethodNotAllowedException {
+      throws ResourceNotFoundException, AuthException, MethodNotAllowedException {
     if (reviewer.getRevisionResource() != null && !reviewer.getRevisionResource().isCurrent()) {
       throw new MethodNotAllowedException("Cannot access on non-current patch set");
     }
@@ -66,18 +63,15 @@
 
   @Singleton
   public static class List implements RestReadView<ReviewerResource> {
-    private final Provider<ReviewDb> db;
     private final ApprovalsUtil approvalsUtil;
 
     @Inject
-    List(Provider<ReviewDb> db, ApprovalsUtil approvalsUtil) {
-      this.db = db;
+    List(ApprovalsUtil approvalsUtil) {
       this.approvalsUtil = approvalsUtil;
     }
 
     @Override
-    public Map<String, Short> apply(ReviewerResource rsrc)
-        throws OrmException, MethodNotAllowedException {
+    public 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,7 +79,6 @@
       Map<String, Short> votes = new TreeMap<>();
       Iterable<PatchSetApproval> byPatchSetUser =
           approvalsUtil.byPatchSetUser(
-              db.get(),
               rsrc.getChangeResource().getNotes(),
               rsrc.getChange().currentPatchSetId(),
               rsrc.getReviewerUser().getAccountId(),
diff --git a/java/com/google/gerrit/server/restapi/config/AgreementJson.java b/java/com/google/gerrit/server/restapi/config/AgreementJson.java
index 02e5f68..d5c085b 100644
--- a/java/com/google/gerrit/server/restapi/config/AgreementJson.java
+++ b/java/com/google/gerrit/server/restapi/config/AgreementJson.java
@@ -17,7 +17,8 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.common.AgreementInfo;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -25,7 +26,6 @@
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.group.GroupJson;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -61,7 +61,7 @@
         GroupControl gc = genericGroupControlFactory.controlFor(user, autoVerifyGroup.getUUID());
         GroupResource group = new GroupResource(gc);
         info.autoVerifyGroup = groupJson.format(group);
-      } catch (NoSuchGroupException | OrmException e) {
+      } catch (NoSuchGroupException | StorageException e) {
         logger.atWarning().log(
             "autoverify group \"%s\" does not exist, referenced in CLA \"%s\"",
             autoVerifyGroup.getName(), ca.getName());
diff --git a/java/com/google/gerrit/server/restapi/config/CheckConsistency.java b/java/com/google/gerrit/server/restapi/config/CheckConsistency.java
index a16736b..61d5c79 100644
--- a/java/com/google/gerrit/server/restapi/config/CheckConsistency.java
+++ b/java/com/google/gerrit/server/restapi/config/CheckConsistency.java
@@ -29,7 +29,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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -56,8 +55,7 @@
 
   @Override
   public ConsistencyCheckInfo apply(ConfigResource resource, ConsistencyCheckInput input)
-      throws RestApiException, IOException, OrmException, PermissionBackendException,
-          ConfigInvalidException {
+      throws RestApiException, IOException, PermissionBackendException, ConfigInvalidException {
     permissionBackend.currentUser().check(GlobalPermission.ACCESS_DATABASE);
 
     if (input == null
diff --git a/java/com/google/gerrit/server/restapi/config/ConfirmEmail.java b/java/com/google/gerrit/server/restapi/config/ConfirmEmail.java
index d16a379..b6dcd35 100644
--- a/java/com/google/gerrit/server/restapi/config/ConfirmEmail.java
+++ b/java/com/google/gerrit/server/restapi/config/ConfirmEmail.java
@@ -26,7 +26,6 @@
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.mail.EmailTokenVerifier;
 import com.google.gerrit.server.restapi.config.ConfirmEmail.Input;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -55,8 +54,7 @@
 
   @Override
   public Response<?> apply(ConfigResource rsrc, Input input)
-      throws AuthException, UnprocessableEntityException, OrmException, IOException,
-          ConfigInvalidException {
+      throws AuthException, UnprocessableEntityException, IOException, ConfigInvalidException {
     CurrentUser user = self.get();
     if (!user.isIdentifiedUser()) {
       throw new AuthException("Authentication required");
diff --git a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
index eca26c3..aa0e350 100644
--- a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
+++ b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
@@ -20,7 +20,6 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.ContributorAgreement;
-import com.google.gerrit.extensions.client.UiType;
 import com.google.gerrit.extensions.common.AccountsInfo;
 import com.google.gerrit.extensions.common.AuthInfo;
 import com.google.gerrit.extensions.common.ChangeConfigInfo;
@@ -49,13 +48,11 @@
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.config.ConfigUtil;
-import com.google.gerrit.server.config.GerritOptions;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.documentation.QueryDocumentationExecutor;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.index.change.ChangeIndexCollection;
-import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.plugincontext.PluginItemContext;
 import com.google.gerrit.server.plugincontext.PluginMapContext;
@@ -67,18 +64,12 @@
 import java.nio.file.Files;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.EnumSet;
 import java.util.HashMap;
-import java.util.Map;
 import java.util.Optional;
 import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.lib.Config;
 
 public class GetServerInfo implements RestReadView<ConfigResource> {
-  private static final String URL_ALIAS = "urlAlias";
-  private static final String KEY_MATCH = "match";
-  private static final String KEY_TOKEN = "token";
-
   private final Config config;
   private final AccountVisibilityProvider accountVisibilityProvider;
   private final AuthConfig authConfig;
@@ -94,10 +85,8 @@
   private final PluginItemContext<AvatarProvider> avatar;
   private final boolean enableSignedPush;
   private final QueryDocumentationExecutor docSearcher;
-  private final NotesMigration migration;
   private final ProjectCache projectCache;
   private final AgreementJson agreementJson;
-  private final GerritOptions gerritOptions;
   private final ChangeIndexCollection indexes;
   private final SitePaths sitePaths;
 
@@ -118,10 +107,8 @@
       PluginItemContext<AvatarProvider> avatar,
       @EnableSignedPush boolean enableSignedPush,
       QueryDocumentationExecutor docSearcher,
-      NotesMigration migration,
       ProjectCache projectCache,
       AgreementJson agreementJson,
-      GerritOptions gerritOptions,
       ChangeIndexCollection indexes,
       SitePaths sitePaths) {
     this.config = config;
@@ -139,10 +126,8 @@
     this.avatar = avatar;
     this.enableSignedPush = enableSignedPush;
     this.docSearcher = docSearcher;
-    this.migration = migration;
     this.projectCache = projectCache;
     this.agreementJson = agreementJson;
-    this.gerritOptions = gerritOptions;
     this.indexes = indexes;
     this.sitePaths = sitePaths;
   }
@@ -155,15 +140,12 @@
     info.change = getChangeInfo();
     info.download = getDownloadInfo();
     info.gerrit = getGerritInfo();
-    info.noteDbEnabled = toBoolean(isNoteDbEnabled());
+    info.noteDbEnabled = true;
     info.plugin = getPluginInfo();
     info.defaultTheme = getDefaultTheme();
     info.sshd = getSshdInfo();
     info.suggest = getSuggestInfo();
 
-    Map<String, String> urlAliases = getUrlAliasesInfo();
-    info.urlAliases = !urlAliases.isEmpty() ? urlAliases : null;
-
     info.user = getUserInfo();
     info.receive = getReceiveInfo();
     return info;
@@ -303,16 +285,10 @@
     info.allProjects = allProjectsName.get();
     info.allUsers = allUsersName.get();
     info.reportBugUrl = config.getString("gerrit", null, "reportBugUrl");
-    info.reportBugText = config.getString("gerrit", null, "reportBugText");
     info.docUrl = getDocUrl();
     info.docSearch = docSearcher.isAvailable();
     info.editGpgKeys =
         toBoolean(enableSignedPush && config.getBoolean("gerrit", null, "editGpgKeys", true));
-    info.webUis = EnumSet.noneOf(UiType.class);
-    info.webUis.add(UiType.POLYGERRIT);
-    if (gerritOptions.enableGwtUi()) {
-      info.webUis.add(UiType.GWT);
-    }
     info.primaryWeblinkName = config.getString("gerrit", null, "primaryWeblinkName");
     return info;
   }
@@ -325,10 +301,6 @@
     return CharMatcher.is('/').trimTrailingFrom(docUrl) + '/';
   }
 
-  private boolean isNoteDbEnabled() {
-    return migration.readChanges();
-  }
-
   private PluginConfigInfo getPluginInfo() {
     PluginConfigInfo info = new PluginConfigInfo();
     info.hasAvatars = toBoolean(avatar.hasImplementation());
@@ -364,16 +336,6 @@
     return null;
   }
 
-  private Map<String, String> getUrlAliasesInfo() {
-    Map<String, String> urlAliases = new HashMap<>();
-    for (String subsection : config.getSubsections(URL_ALIAS)) {
-      urlAliases.put(
-          config.getString(URL_ALIAS, subsection, KEY_MATCH),
-          config.getString(URL_ALIAS, subsection, KEY_TOKEN));
-    }
-    return urlAliases;
-  }
-
   private SshdInfo getSshdInfo() {
     String[] addr = config.getStringList("sshd", null, "listenAddress");
     if (addr.length == 1 && isOff(addr[0])) {
diff --git a/java/com/google/gerrit/server/restapi/config/GetVersion.java b/java/com/google/gerrit/server/restapi/config/GetVersion.java
index 8135719..ee206d6 100644
--- a/java/com/google/gerrit/server/restapi/config/GetVersion.java
+++ b/java/com/google/gerrit/server/restapi/config/GetVersion.java
@@ -15,19 +15,22 @@
 package com.google.gerrit.server.restapi.config;
 
 import com.google.gerrit.common.Version;
+import com.google.gerrit.extensions.restapi.CacheControl;
 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.ConfigResource;
 import com.google.inject.Singleton;
+import java.util.concurrent.TimeUnit;
 
 @Singleton
 public class GetVersion implements RestReadView<ConfigResource> {
   @Override
-  public String apply(ConfigResource resource) throws ResourceNotFoundException {
+  public Response<String> apply(ConfigResource resource) throws ResourceNotFoundException {
     String version = Version.getVersion();
     if (version == null) {
       throw new ResourceNotFoundException();
     }
-    return version;
+    return Response.ok(version).caching(CacheControl.PRIVATE(30, TimeUnit.SECONDS));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/config/IndexChanges.java b/java/com/google/gerrit/server/restapi/config/IndexChanges.java
index b178118..6ba93e8 100644
--- a/java/com/google/gerrit/server/restapi/config/IndexChanges.java
+++ b/java/com/google/gerrit/server/restapi/config/IndexChanges.java
@@ -19,18 +19,14 @@
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.change.ChangeFinder;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.index.change.ChangeIndexer;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.restapi.config.IndexChanges.Input;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import java.io.IOException;
 import java.util.Set;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
@@ -43,38 +39,27 @@
   }
 
   private final ChangeFinder changeFinder;
-  private final SchemaFactory<ReviewDb> schemaFactory;
   private final ChangeData.Factory changeDataFactory;
   private final ChangeIndexer indexer;
 
   @Inject
   IndexChanges(
-      ChangeFinder changeFinder,
-      SchemaFactory<ReviewDb> schemaFactory,
-      ChangeData.Factory changeDataFactory,
-      ChangeIndexer indexer) {
+      ChangeFinder changeFinder, ChangeData.Factory changeDataFactory, ChangeIndexer indexer) {
     this.changeFinder = changeFinder;
-    this.schemaFactory = schemaFactory;
     this.changeDataFactory = changeDataFactory;
     this.indexer = indexer;
   }
 
   @Override
-  public Object apply(ConfigResource resource, Input input) throws OrmException {
+  public Object apply(ConfigResource resource, Input input) {
     if (input == null || input.changes == null) {
       return Response.ok("Nothing to index");
     }
 
-    try (ReviewDb db = schemaFactory.open()) {
-      for (String id : input.changes) {
-        for (ChangeNotes n : changeFinder.find(id)) {
-          try {
-            indexer.index(changeDataFactory.create(db, n));
-            logger.atFine().log("Indexed change %s", id);
-          } catch (IOException e) {
-            logger.atSevere().withCause(e).log("Failed to index change %s", id);
-          }
-        }
+    for (String id : input.changes) {
+      for (ChangeNotes n : changeFinder.find(id)) {
+        indexer.index(changeDataFactory.create(n));
+        logger.atFine().log("Indexed change %s", id);
       }
     }
 
diff --git a/java/com/google/gerrit/server/restapi/config/ListCapabilities.java b/java/com/google/gerrit/server/restapi/config/ListCapabilities.java
index fa9bfde..cacbbf5 100644
--- a/java/com/google/gerrit/server/restapi/config/ListCapabilities.java
+++ b/java/com/google/gerrit/server/restapi/config/ListCapabilities.java
@@ -14,38 +14,32 @@
 
 package com.google.gerrit.server.restapi.config;
 
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
+
 import com.google.common.collect.ImmutableMap;
-import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.extensions.config.CapabilityDefinition;
-import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.config.CapabilityConstants;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PluginPermissionsUtil;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.regex.Pattern;
 
 /** List capabilities visible to the calling user. */
 @Singleton
 public class ListCapabilities implements RestReadView<ConfigResource> {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
-  private static final Pattern PLUGIN_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9-]+$");
-
   private final PermissionBackend permissionBackend;
-  private final DynamicMap<CapabilityDefinition> pluginCapabilities;
+  private final PluginPermissionsUtil pluginPermissionsUtil;
 
   @Inject
   public ListCapabilities(
-      PermissionBackend permissionBackend, DynamicMap<CapabilityDefinition> pluginCapabilities) {
+      PermissionBackend permissionBackend, PluginPermissionsUtil pluginPermissionsUtil) {
     this.permissionBackend = permissionBackend;
-    this.pluginCapabilities = pluginCapabilities;
+    this.pluginPermissionsUtil = pluginPermissionsUtil;
   }
 
   @Override
@@ -59,21 +53,14 @@
   }
 
   public Map<String, CapabilityInfo> collectPluginCapabilities() {
-    Map<String, CapabilityInfo> output = new HashMap<>();
-    for (String pluginName : pluginCapabilities.plugins()) {
-      if (!PLUGIN_NAME_PATTERN.matcher(pluginName).matches()) {
-        logger.atWarning().log(
-            "Plugin name '%s' must match '%s' to use capabilities; rename the plugin",
-            pluginName, PLUGIN_NAME_PATTERN.pattern());
-        continue;
-      }
-      for (Map.Entry<String, Provider<CapabilityDefinition>> entry :
-          pluginCapabilities.byPlugin(pluginName).entrySet()) {
-        String id = String.format("%s-%s", pluginName, entry.getKey());
-        output.put(id, new CapabilityInfo(id, entry.getValue().get().getDescription()));
-      }
-    }
-    return output;
+    return convertToPermissionInfos(pluginPermissionsUtil.collectPluginCapabilities());
+  }
+
+  private static ImmutableMap<String, CapabilityInfo> convertToPermissionInfos(
+      ImmutableMap<String, String> permissionIdNames) {
+    return permissionIdNames.entrySet().stream()
+        .collect(
+            toImmutableMap(Map.Entry::getKey, e -> new CapabilityInfo(e.getKey(), e.getValue())));
   }
 
   private Map<String, CapabilityInfo> collectCoreCapabilities()
diff --git a/java/com/google/gerrit/server/restapi/config/ListTopMenus.java b/java/com/google/gerrit/server/restapi/config/ListTopMenus.java
index 7a85bcd..01c273c 100644
--- a/java/com/google/gerrit/server/restapi/config/ListTopMenus.java
+++ b/java/com/google/gerrit/server/restapi/config/ListTopMenus.java
@@ -14,30 +14,30 @@
 
 package com.google.gerrit.server.restapi.config;
 
-import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.webui.TopMenu;
+import com.google.gerrit.extensions.webui.TopMenu.MenuEntry;
 import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.ArrayList;
 import java.util.List;
 
 @Singleton
-class ListTopMenus implements RestReadView<ConfigResource> {
-  private final DynamicSet<TopMenu> extensions;
+public class ListTopMenus implements RestReadView<ConfigResource> {
+  private final PluginSetContext<TopMenu> extensions;
 
   @Inject
-  ListTopMenus(DynamicSet<TopMenu> extensions) {
+  ListTopMenus(PluginSetContext<TopMenu> extensions) {
     this.extensions = extensions;
   }
 
   @Override
-  public List<TopMenu.MenuEntry> apply(ConfigResource resource) {
+  public Response<List<MenuEntry>> apply(ConfigResource resource) {
     List<TopMenu.MenuEntry> entries = new ArrayList<>();
-    for (TopMenu extension : extensions) {
-      entries.addAll(extension.getEntries());
-    }
-    return entries;
+    extensions.runEach(extension -> entries.addAll(extension.getEntries()));
+    return Response.ok(entries).caching(ConfigResource.DEFAULT_CACHE_CONTROL);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/group/AddMembers.java b/java/com/google/gerrit/server/restapi/group/AddMembers.java
index 5283518..b2b14a1 100644
--- a/java/com/google/gerrit/server/restapi/group/AddMembers.java
+++ b/java/com/google/gerrit/server/restapi/group/AddMembers.java
@@ -18,7 +18,7 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+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;
@@ -37,6 +37,7 @@
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.GroupControl;
@@ -48,7 +49,6 @@
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.group.AddMembers.Input;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -112,9 +112,8 @@
 
   @Override
   public List<AccountInfo> apply(GroupResource resource, Input input)
-      throws AuthException, NotInternalGroupException, UnprocessableEntityException, OrmException,
-          IOException, ConfigInvalidException, ResourceNotFoundException,
-          PermissionBackendException {
+      throws AuthException, NotInternalGroupException, UnprocessableEntityException, IOException,
+          ConfigInvalidException, ResourceNotFoundException, PermissionBackendException {
     GroupDescription.Internal internalGroup =
         resource.asInternalGroup().orElseThrow(NotInternalGroupException::new);
     input = Input.init(input);
@@ -144,19 +143,18 @@
   }
 
   Account findAccount(String nameOrEmailOrId)
-      throws AuthException, UnprocessableEntityException, OrmException, IOException,
-          ConfigInvalidException {
+      throws UnprocessableEntityException, IOException, ConfigInvalidException {
+    AccountResolver.Result result = accountResolver.resolve(nameOrEmailOrId);
     try {
-      return accountResolver.parse(nameOrEmailOrId).getAccount();
-    } catch (UnprocessableEntityException e) {
-      // might be because the account does not exist or because the account is
-      // not visible
+      return result.asUnique().getAccount();
+    } catch (UnresolvableAccountException e) {
       switch (authType) {
         case HTTP_LDAP:
         case CLIENT_SSL_CERT_LDAP:
         case LDAP:
-          if (accountResolver.find(nameOrEmailOrId) == null) {
-            // account does not exist, try to create it
+          if (!e.isSelf() && result.asList().isEmpty()) {
+            // Account does not exist, try to create it. This may leak account existence, since we
+            // can't distinguish between a nonexistent account and one that the caller can't see.
             Optional<Account> a = createAccountByLdap(nameOrEmailOrId);
             if (a.isPresent()) {
               return a.get();
@@ -177,7 +175,7 @@
   }
 
   public void addMembers(AccountGroup.UUID groupUuid, Set<Account.Id> newMemberIds)
-      throws OrmException, IOException, NoSuchGroupException, ConfigInvalidException {
+      throws IOException, NoSuchGroupException, ConfigInvalidException {
     InternalGroupUpdate groupUpdate =
         InternalGroupUpdate.builder()
             .setMemberModification(memberIds -> Sets.union(memberIds, newMemberIds))
@@ -224,8 +222,8 @@
 
     @Override
     public AccountInfo apply(GroupResource resource, IdString id, Input input)
-        throws AuthException, MethodNotAllowedException, ResourceNotFoundException, OrmException,
-            IOException, ConfigInvalidException, PermissionBackendException {
+        throws AuthException, MethodNotAllowedException, ResourceNotFoundException, IOException,
+            ConfigInvalidException, PermissionBackendException {
       AddMembers.Input in = new AddMembers.Input();
       in._oneMember = id.get();
       try {
@@ -251,7 +249,7 @@
 
     @Override
     public AccountInfo apply(MemberResource resource, Input input)
-        throws OrmException, PermissionBackendException {
+        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 02c4a29..5c879e7 100644
--- a/java/com/google/gerrit/server/restapi/group/AddSubgroups.java
+++ b/java/com/google/gerrit/server/restapi/group/AddSubgroups.java
@@ -19,7 +19,7 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+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;
@@ -39,7 +39,6 @@
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.group.AddSubgroups.Input;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -93,7 +92,7 @@
 
   @Override
   public List<GroupInfo> apply(GroupResource resource, Input input)
-      throws NotInternalGroupException, AuthException, UnprocessableEntityException, OrmException,
+      throws NotInternalGroupException, AuthException, UnprocessableEntityException,
           ResourceNotFoundException, IOException, ConfigInvalidException,
           PermissionBackendException {
     GroupDescription.Internal group =
@@ -124,7 +123,7 @@
 
   private void addSubgroups(
       AccountGroup.UUID parentGroupUuid, Set<AccountGroup.UUID> newSubgroupUuids)
-      throws OrmException, NoSuchGroupException, IOException, ConfigInvalidException {
+      throws NoSuchGroupException, IOException, ConfigInvalidException {
     InternalGroupUpdate groupUpdate =
         InternalGroupUpdate.builder()
             .setSubgroupModification(subgroupUuids -> Sets.union(subgroupUuids, newSubgroupUuids))
@@ -144,8 +143,8 @@
 
     @Override
     public GroupInfo apply(GroupResource resource, IdString id, Input input)
-        throws AuthException, MethodNotAllowedException, ResourceNotFoundException, OrmException,
-            IOException, ConfigInvalidException, PermissionBackendException {
+        throws AuthException, MethodNotAllowedException, ResourceNotFoundException, IOException,
+            ConfigInvalidException, PermissionBackendException {
       AddSubgroups.Input in = new AddSubgroups.Input();
       in.groups = ImmutableList.of(id.get());
       try {
@@ -171,7 +170,7 @@
 
     @Override
     public GroupInfo apply(SubgroupResource resource, Input input)
-        throws OrmException, PermissionBackendException {
+        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 4a2375b..eaedcd7 100644
--- a/java/com/google/gerrit/server/restapi/group/CreateGroup.java
+++ b/java/com/google/gerrit/server/restapi/group/CreateGroup.java
@@ -19,6 +19,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.exceptions.DuplicateKeyException;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.api.groups.GroupInput;
 import com.google.gerrit.extensions.client.ListGroupsOption;
@@ -36,7 +37,6 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.UserInitiated;
 import com.google.gerrit.server.account.CreateGroupArgs;
 import com.google.gerrit.server.account.GroupCache;
@@ -50,12 +50,11 @@
 import com.google.gerrit.server.group.db.GroupsUpdate;
 import com.google.gerrit.server.group.db.InternalGroupCreation;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
+import com.google.gerrit.server.notedb.Sequences;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.gerrit.server.validators.GroupCreationValidationListener;
 import com.google.gerrit.server.validators.ValidationException;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -125,8 +124,8 @@
   @Override
   public GroupInfo apply(TopLevelResource resource, IdString id, GroupInput input)
       throws AuthException, BadRequestException, UnprocessableEntityException,
-          ResourceConflictException, OrmException, IOException, ConfigInvalidException,
-          ResourceNotFoundException, PermissionBackendException {
+          ResourceConflictException, IOException, ConfigInvalidException, ResourceNotFoundException,
+          PermissionBackendException {
     String name = id.get();
     if (input == null) {
       input = new GroupInput();
@@ -156,7 +155,7 @@
       args.initialMembers =
           ownerUuid == null
               ? Collections.singleton(self.get().getAccountId())
-              : Collections.<Account.Id>emptySet();
+              : Collections.emptySet();
     }
 
     try {
@@ -178,7 +177,7 @@
   }
 
   private InternalGroup createGroup(CreateGroupArgs createGroupArgs)
-      throws OrmException, ResourceConflictException, IOException, ConfigInvalidException {
+      throws ResourceConflictException, IOException, ConfigInvalidException {
 
     String nameLower = createGroupArgs.getGroupName().toLowerCase(Locale.US);
 
@@ -218,7 +217,7 @@
         members -> ImmutableSet.copyOf(createGroupArgs.initialMembers));
     try {
       return groupsUpdateProvider.get().createGroup(groupCreation, groupUpdateBuilder.build());
-    } catch (OrmDuplicateKeyException e) {
+    } catch (DuplicateKeyException e) {
       throw new ResourceConflictException(
           "group '" + createGroupArgs.getGroupName() + "' already exists", e);
     }
diff --git a/java/com/google/gerrit/server/restapi/group/DeleteMembers.java b/java/com/google/gerrit/server/restapi/group/DeleteMembers.java
index 82dc7d9..b5771a3 100644
--- a/java/com/google/gerrit/server/restapi/group/DeleteMembers.java
+++ b/java/com/google/gerrit/server/restapi/group/DeleteMembers.java
@@ -16,7 +16,7 @@
 
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -33,7 +33,6 @@
 import com.google.gerrit.server.group.db.GroupsUpdate;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
 import com.google.gerrit.server.restapi.group.AddMembers.Input;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -56,8 +55,8 @@
 
   @Override
   public Response<?> apply(GroupResource resource, Input input)
-      throws AuthException, NotInternalGroupException, UnprocessableEntityException, OrmException,
-          IOException, ConfigInvalidException, ResourceNotFoundException {
+      throws AuthException, NotInternalGroupException, UnprocessableEntityException, IOException,
+          ConfigInvalidException, ResourceNotFoundException {
     GroupDescription.Internal internalGroup =
         resource.asInternalGroup().orElseThrow(NotInternalGroupException::new);
     input = Input.init(input);
@@ -69,8 +68,7 @@
 
     Set<Account.Id> membersToRemove = new HashSet<>();
     for (String nameOrEmail : input.members) {
-      Account a = accountResolver.parse(nameOrEmail).getAccount();
-      membersToRemove.add(a.getId());
+      membersToRemove.add(accountResolver.resolve(nameOrEmail).asUnique().getAccount().getId());
     }
     AccountGroup.UUID groupUuid = internalGroup.getGroupUUID();
     try {
@@ -83,7 +81,7 @@
   }
 
   private void removeGroupMembers(AccountGroup.UUID groupUuid, Set<Account.Id> accountIds)
-      throws OrmException, IOException, NoSuchGroupException, ConfigInvalidException {
+      throws IOException, NoSuchGroupException, ConfigInvalidException {
     InternalGroupUpdate groupUpdate =
         InternalGroupUpdate.builder()
             .setMemberModification(memberIds -> Sets.difference(memberIds, accountIds))
@@ -103,8 +101,8 @@
 
     @Override
     public Response<?> apply(MemberResource resource, Input input)
-        throws AuthException, MethodNotAllowedException, UnprocessableEntityException, OrmException,
-            IOException, ConfigInvalidException, ResourceNotFoundException {
+        throws AuthException, MethodNotAllowedException, UnprocessableEntityException, IOException,
+            ConfigInvalidException, ResourceNotFoundException {
       AddMembers.Input in = new AddMembers.Input();
       in._oneMember = resource.getMember().getAccountId().toString();
       return delete.get().apply(resource, in);
diff --git a/java/com/google/gerrit/server/restapi/group/DeleteSubgroups.java b/java/com/google/gerrit/server/restapi/group/DeleteSubgroups.java
index 307f874..9ecfa1f 100644
--- a/java/com/google/gerrit/server/restapi/group/DeleteSubgroups.java
+++ b/java/com/google/gerrit/server/restapi/group/DeleteSubgroups.java
@@ -17,7 +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.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -33,7 +33,6 @@
 import com.google.gerrit.server.group.db.GroupsUpdate;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
 import com.google.gerrit.server.restapi.group.AddSubgroups.Input;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -56,7 +55,7 @@
 
   @Override
   public Response<?> apply(GroupResource resource, Input input)
-      throws AuthException, NotInternalGroupException, UnprocessableEntityException, OrmException,
+      throws AuthException, NotInternalGroupException, UnprocessableEntityException,
           ResourceNotFoundException, IOException, ConfigInvalidException {
     GroupDescription.Internal internalGroup =
         resource.asInternalGroup().orElseThrow(NotInternalGroupException::new);
@@ -86,7 +85,7 @@
 
   private void removeSubgroups(
       AccountGroup.UUID parentGroupUuid, Set<AccountGroup.UUID> removedSubgroupUuids)
-      throws OrmException, NoSuchGroupException, IOException, ConfigInvalidException {
+      throws NoSuchGroupException, IOException, ConfigInvalidException {
     InternalGroupUpdate groupUpdate =
         InternalGroupUpdate.builder()
             .setSubgroupModification(
@@ -107,7 +106,7 @@
 
     @Override
     public Response<?> apply(SubgroupResource resource, Input input)
-        throws AuthException, MethodNotAllowedException, UnprocessableEntityException, OrmException,
+        throws AuthException, MethodNotAllowedException, UnprocessableEntityException,
             ResourceNotFoundException, IOException, ConfigInvalidException {
       AddSubgroups.Input in = new AddSubgroups.Input();
       in.groups = ImmutableList.of(resource.getMember().get());
diff --git a/java/com/google/gerrit/server/restapi/group/GetAuditLog.java b/java/com/google/gerrit/server/restapi/group/GetAuditLog.java
index dcdd8a8..1a781d9 100644
--- a/java/com/google/gerrit/server/restapi/group/GetAuditLog.java
+++ b/java/com/google/gerrit/server/restapi/group/GetAuditLog.java
@@ -36,7 +36,6 @@
 import com.google.gerrit.server.group.InternalGroupDescription;
 import com.google.gerrit.server.group.db.Groups;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -76,8 +75,8 @@
 
   @Override
   public List<? extends GroupAuditEventInfo> apply(GroupResource rsrc)
-      throws AuthException, NotInternalGroupException, OrmException, IOException,
-          ConfigInvalidException, PermissionBackendException {
+      throws AuthException, NotInternalGroupException, IOException, ConfigInvalidException,
+          PermissionBackendException {
     GroupDescription.Internal group =
         rsrc.asInternalGroup().orElseThrow(NotInternalGroupException::new);
     if (!rsrc.getControl().isOwner()) {
diff --git a/java/com/google/gerrit/server/restapi/group/GetDetail.java b/java/com/google/gerrit/server/restapi/group/GetDetail.java
index 75d1e34..c757383 100644
--- a/java/com/google/gerrit/server/restapi/group/GetDetail.java
+++ b/java/com/google/gerrit/server/restapi/group/GetDetail.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -33,7 +32,7 @@
   }
 
   @Override
-  public GroupInfo apply(GroupResource rsrc) throws OrmException, PermissionBackendException {
+  public GroupInfo apply(GroupResource rsrc) throws PermissionBackendException {
     return 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 c6cddb6..3ae447b 100644
--- a/java/com/google/gerrit/server/restapi/group/GetGroup.java
+++ b/java/com/google/gerrit/server/restapi/group/GetGroup.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -32,7 +31,7 @@
   }
 
   @Override
-  public GroupInfo apply(GroupResource resource) throws OrmException, PermissionBackendException {
+  public GroupInfo apply(GroupResource resource) throws PermissionBackendException {
     return 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 95063de..63a8a1b 100644
--- a/java/com/google/gerrit/server/restapi/group/GetMember.java
+++ b/java/com/google/gerrit/server/restapi/group/GetMember.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.group.MemberResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -33,7 +32,7 @@
   }
 
   @Override
-  public AccountInfo apply(MemberResource rsrc) throws OrmException, PermissionBackendException {
+  public AccountInfo apply(MemberResource rsrc) throws PermissionBackendException {
     AccountLoader loader = infoFactory.create(true);
     AccountInfo info = loader.get(rsrc.getMember().getAccountId());
     loader.fill();
diff --git a/java/com/google/gerrit/server/restapi/group/GetOwner.java b/java/com/google/gerrit/server/restapi/group/GetOwner.java
index 6a97d01..10e1b23 100644
--- a/java/com/google/gerrit/server/restapi/group/GetOwner.java
+++ b/java/com/google/gerrit/server/restapi/group/GetOwner.java
@@ -15,14 +15,13 @@
 package com.google.gerrit.server.restapi.group;
 
 import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+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.RestReadView;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -40,8 +39,7 @@
 
   @Override
   public GroupInfo apply(GroupResource resource)
-      throws NotInternalGroupException, ResourceNotFoundException, OrmException,
-          PermissionBackendException {
+      throws NotInternalGroupException, ResourceNotFoundException, PermissionBackendException {
     GroupDescription.Internal group =
         resource.asInternalGroup().orElseThrow(NotInternalGroupException::new);
     try {
diff --git a/java/com/google/gerrit/server/restapi/group/GetSubgroup.java b/java/com/google/gerrit/server/restapi/group/GetSubgroup.java
index 16e2739..4466180 100644
--- a/java/com/google/gerrit/server/restapi/group/GetSubgroup.java
+++ b/java/com/google/gerrit/server/restapi/group/GetSubgroup.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.group.SubgroupResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -32,7 +31,7 @@
   }
 
   @Override
-  public GroupInfo apply(SubgroupResource rsrc) throws OrmException, PermissionBackendException {
+  public GroupInfo apply(SubgroupResource rsrc) throws PermissionBackendException {
     return 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 a51fad2..12b9d61 100644
--- a/java/com/google/gerrit/server/restapi/group/GroupJson.java
+++ b/java/com/google/gerrit/server/restapi/group/GroupJson.java
@@ -29,7 +29,6 @@
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.util.Collection;
@@ -76,18 +75,17 @@
     return this;
   }
 
-  public GroupInfo format(GroupResource rsrc) throws OrmException, PermissionBackendException {
+  public GroupInfo format(GroupResource rsrc) throws PermissionBackendException {
     return createGroupInfo(rsrc.getGroup(), rsrc::getControl);
   }
 
-  public GroupInfo format(GroupDescription.Basic group)
-      throws OrmException, PermissionBackendException {
+  public GroupInfo format(GroupDescription.Basic group) throws PermissionBackendException {
     return createGroupInfo(group, Suppliers.memoize(() -> groupControlFactory.controlFor(group)));
   }
 
   private GroupInfo createGroupInfo(
       GroupDescription.Basic group, Supplier<GroupControl> groupControlSupplier)
-      throws OrmException, PermissionBackendException {
+      throws PermissionBackendException {
     GroupInfo info = createBasicGroupInfo(group);
 
     if (group instanceof GroupDescription.Internal) {
@@ -110,7 +108,7 @@
       GroupInfo info,
       GroupDescription.Internal internalGroup,
       Supplier<GroupControl> groupControlSupplier)
-      throws OrmException, PermissionBackendException {
+      throws PermissionBackendException {
     info.description = Strings.emptyToNull(internalGroup.getDescription());
     info.groupId = internalGroup.getId().get();
 
diff --git a/java/com/google/gerrit/server/restapi/group/ListGroups.java b/java/com/google/gerrit/server/restapi/group/ListGroups.java
index 367bc71..38b525b 100644
--- a/java/com/google/gerrit/server/restapi/group/ListGroups.java
+++ b/java/com/google/gerrit/server/restapi/group/ListGroups.java
@@ -23,8 +23,9 @@
 import com.google.common.collect.Streams;
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+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.RestApiException;
@@ -45,7 +46,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.restapi.account.GetGroups;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -202,7 +202,7 @@
 
   @Option(name = "-O", usage = "Output option flags, in hex")
   void setOptionFlagsHex(String hex) {
-    options.addAll(ListGroupsOption.fromBits(Integer.parseInt(hex, 16)));
+    options.addAll(ListOption.fromBits(ListGroupsOption.class, Integer.parseInt(hex, 16)));
   }
 
   @Option(name = "--owned-by", usage = "list groups owned by the given group uuid")
@@ -248,8 +248,7 @@
 
   @Override
   public SortedMap<String, GroupInfo> apply(TopLevelResource resource)
-      throws OrmException, RestApiException, IOException, ConfigInvalidException,
-          PermissionBackendException {
+      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
     SortedMap<String, GroupInfo> output = new TreeMap<>();
     for (GroupInfo info : get()) {
       output.put(MoreObjects.firstNonNull(info.name, "Group " + Url.decode(info.id)), info);
@@ -259,8 +258,7 @@
   }
 
   public List<GroupInfo> get()
-      throws OrmException, RestApiException, IOException, ConfigInvalidException,
-          PermissionBackendException {
+      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
     if (!Strings.isNullOrEmpty(suggest)) {
       return suggestGroups();
     }
@@ -285,7 +283,7 @@
   }
 
   private List<GroupInfo> getAllGroups()
-      throws OrmException, IOException, ConfigInvalidException, PermissionBackendException {
+      throws IOException, ConfigInvalidException, PermissionBackendException {
     Pattern pattern = getRegexPattern();
     Stream<GroupDescription.Internal> existingGroups =
         getAllExistingGroups()
@@ -316,8 +314,7 @@
     return groups.getAllGroupReferences();
   }
 
-  private List<GroupInfo> suggestGroups()
-      throws OrmException, BadRequestException, PermissionBackendException {
+  private List<GroupInfo> suggestGroups() throws BadRequestException, PermissionBackendException {
     if (conflictingSuggestParameters()) {
       throw new BadRequestException(
           "You should only have no more than one --project and -n with --suggest");
@@ -372,7 +369,7 @@
   }
 
   private List<GroupInfo> filterGroupsOwnedBy(Predicate<GroupDescription.Internal> filter)
-      throws OrmException, IOException, ConfigInvalidException, PermissionBackendException {
+      throws IOException, ConfigInvalidException, PermissionBackendException {
     Pattern pattern = getRegexPattern();
     Stream<? extends GroupDescription.Internal> foundGroups =
         groups
@@ -400,14 +397,13 @@
   }
 
   private List<GroupInfo> getGroupsOwnedBy(String id)
-      throws OrmException, RestApiException, IOException, ConfigInvalidException,
-          PermissionBackendException {
+      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
     String uuid = groupResolver.parse(id).getGroupUUID().get();
     return filterGroupsOwnedBy(group -> group.getOwnerGroupUUID().get().equals(uuid));
   }
 
   private List<GroupInfo> getGroupsOwnedBy(IdentifiedUser user)
-      throws OrmException, IOException, ConfigInvalidException, PermissionBackendException {
+      throws IOException, ConfigInvalidException, PermissionBackendException {
     return filterGroupsOwnedBy(group -> isOwner(user, group));
   }
 
diff --git a/java/com/google/gerrit/server/restapi/group/ListMembers.java b/java/com/google/gerrit/server/restapi/group/ListMembers.java
index 40db2b7..75be44c 100644
--- a/java/com/google/gerrit/server/restapi/group/ListMembers.java
+++ b/java/com/google/gerrit/server/restapi/group/ListMembers.java
@@ -33,7 +33,6 @@
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.group.InternalGroupDescription;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -67,7 +66,7 @@
 
   @Override
   public List<AccountInfo> apply(GroupResource resource)
-      throws NotInternalGroupException, OrmException, PermissionBackendException {
+      throws NotInternalGroupException, PermissionBackendException {
     GroupDescription.Internal group =
         resource.asInternalGroup().orElseThrow(NotInternalGroupException::new);
     if (recursive) {
diff --git a/java/com/google/gerrit/server/restapi/group/ListSubgroups.java b/java/com/google/gerrit/server/restapi/group/ListSubgroups.java
index 864b01b..bb72a10 100644
--- a/java/com/google/gerrit/server/restapi/group/ListSubgroups.java
+++ b/java/com/google/gerrit/server/restapi/group/ListSubgroups.java
@@ -19,14 +19,13 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.common.GroupInfo;
 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;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.ArrayList;
@@ -47,7 +46,7 @@
 
   @Override
   public List<GroupInfo> apply(GroupResource rsrc)
-      throws NotInternalGroupException, OrmException, PermissionBackendException {
+      throws NotInternalGroupException, PermissionBackendException {
     GroupDescription.Internal group =
         rsrc.asInternalGroup().orElseThrow(NotInternalGroupException::new);
 
@@ -56,7 +55,7 @@
 
   public List<GroupInfo> getDirectSubgroups(
       GroupDescription.Internal group, GroupControl groupControl)
-      throws OrmException, PermissionBackendException {
+      throws PermissionBackendException {
     boolean ownerOfParent = groupControl.isOwner();
     List<GroupInfo> included = new ArrayList<>();
     for (AccountGroup.UUID subgroupUuid : group.getSubgroups()) {
diff --git a/java/com/google/gerrit/server/restapi/group/MembersCollection.java b/java/com/google/gerrit/server/restapi/group/MembersCollection.java
index fec1443..6dfb2b6 100644
--- a/java/com/google/gerrit/server/restapi/group/MembersCollection.java
+++ b/java/com/google/gerrit/server/restapi/group/MembersCollection.java
@@ -26,7 +26,6 @@
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.group.MemberResource;
 import com.google.gerrit.server.restapi.account.AccountsCollection;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -56,8 +55,8 @@
 
   @Override
   public MemberResource parse(GroupResource parent, IdString id)
-      throws NotInternalGroupException, AuthException, ResourceNotFoundException, OrmException,
-          IOException, ConfigInvalidException {
+      throws NotInternalGroupException, AuthException, ResourceNotFoundException, IOException,
+          ConfigInvalidException {
     GroupDescription.Internal group =
         parent.asInternalGroup().orElseThrow(NotInternalGroupException::new);
 
diff --git a/java/com/google/gerrit/server/restapi/group/Module.java b/java/com/google/gerrit/server/restapi/group/Module.java
index 741c3da..45ac411 100644
--- a/java/com/google/gerrit/server/restapi/group/Module.java
+++ b/java/com/google/gerrit/server/restapi/group/Module.java
@@ -82,7 +82,7 @@
   @Provides
   @ServerInitiated
   GroupsUpdate provideServerInitiatedGroupsUpdate(GroupsUpdate.Factory groupsUpdateFactory) {
-    return groupsUpdateFactory.create(null);
+    return groupsUpdateFactory.createWithServerIdent();
   }
 
   @Provides
diff --git a/java/com/google/gerrit/server/restapi/group/PutDescription.java b/java/com/google/gerrit/server/restapi/group/PutDescription.java
index 9ce180f4..c4e6f09 100644
--- a/java/com/google/gerrit/server/restapi/group/PutDescription.java
+++ b/java/com/google/gerrit/server/restapi/group/PutDescription.java
@@ -16,7 +16,7 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+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;
@@ -27,7 +27,6 @@
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.group.db.GroupsUpdate;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -46,8 +45,8 @@
 
   @Override
   public Response<String> apply(GroupResource resource, DescriptionInput input)
-      throws AuthException, NotInternalGroupException, ResourceNotFoundException, OrmException,
-          IOException, ConfigInvalidException {
+      throws AuthException, NotInternalGroupException, ResourceNotFoundException, IOException,
+          ConfigInvalidException {
     if (input == null) {
       input = new DescriptionInput(); // Delete would set description to null.
     }
@@ -72,7 +71,7 @@
     }
 
     return Strings.isNullOrEmpty(input.description)
-        ? Response.<String>none()
+        ? Response.none()
         : Response.ok(input.description);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/group/PutName.java b/java/com/google/gerrit/server/restapi/group/PutName.java
index 83757d0..adc20d5 100644
--- a/java/com/google/gerrit/server/restapi/group/PutName.java
+++ b/java/com/google/gerrit/server/restapi/group/PutName.java
@@ -16,7 +16,8 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.DuplicateKeyException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.common.NameInput;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -28,7 +29,6 @@
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.group.db.GroupsUpdate;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -79,7 +79,7 @@
       groupsUpdateProvider.get().updateGroup(groupUuid, groupUpdate);
     } catch (NoSuchGroupException e) {
       throw new ResourceNotFoundException(String.format("Group %s not found", groupUuid), e);
-    } catch (OrmDuplicateKeyException e) {
+    } catch (DuplicateKeyException e) {
       throw new ResourceConflictException("group with name " + newName + " already exists", e);
     }
   }
diff --git a/java/com/google/gerrit/server/restapi/group/PutOptions.java b/java/com/google/gerrit/server/restapi/group/PutOptions.java
index 8698ee6..747d899 100644
--- a/java/com/google/gerrit/server/restapi/group/PutOptions.java
+++ b/java/com/google/gerrit/server/restapi/group/PutOptions.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.restapi.group;
 
 import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+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;
@@ -26,7 +26,6 @@
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.group.db.GroupsUpdate;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -45,7 +44,7 @@
   @Override
   public GroupOptionsInfo apply(GroupResource resource, GroupOptionsInfo input)
       throws NotInternalGroupException, AuthException, BadRequestException,
-          ResourceNotFoundException, OrmException, IOException, ConfigInvalidException {
+          ResourceNotFoundException, IOException, ConfigInvalidException {
     GroupDescription.Internal internalGroup =
         resource.asInternalGroup().orElseThrow(NotInternalGroupException::new);
     if (!resource.getControl().isOwner()) {
diff --git a/java/com/google/gerrit/server/restapi/group/PutOwner.java b/java/com/google/gerrit/server/restapi/group/PutOwner.java
index 0b007da..f766a84 100644
--- a/java/com/google/gerrit/server/restapi/group/PutOwner.java
+++ b/java/com/google/gerrit/server/restapi/group/PutOwner.java
@@ -16,7 +16,7 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+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;
@@ -31,7 +31,6 @@
 import com.google.gerrit.server.group.db.GroupsUpdate;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -57,8 +56,8 @@
   @Override
   public GroupInfo apply(GroupResource resource, OwnerInput input)
       throws ResourceNotFoundException, NotInternalGroupException, AuthException,
-          BadRequestException, UnprocessableEntityException, OrmException, IOException,
-          ConfigInvalidException, PermissionBackendException {
+          BadRequestException, UnprocessableEntityException, IOException, ConfigInvalidException,
+          PermissionBackendException {
     GroupDescription.Internal internalGroup =
         resource.asInternalGroup().orElseThrow(NotInternalGroupException::new);
     if (!resource.getControl().isOwner()) {
diff --git a/java/com/google/gerrit/server/restapi/group/QueryGroups.java b/java/com/google/gerrit/server/restapi/group/QueryGroups.java
index b116646..b8dd28d 100644
--- a/java/com/google/gerrit/server/restapi/group/QueryGroups.java
+++ b/java/com/google/gerrit/server/restapi/group/QueryGroups.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 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.MethodNotAllowedException;
@@ -29,7 +30,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.query.group.GroupQueryBuilder;
 import com.google.gerrit.server.query.group.GroupQueryProcessor;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.util.ArrayList;
@@ -83,7 +83,7 @@
 
   @Option(name = "-O", usage = "Output option flags, in hex")
   public void setOptionFlagsHex(String hex) {
-    options.addAll(ListGroupsOption.fromBits(Integer.parseInt(hex, 16)));
+    options.addAll(ListOption.fromBits(ListGroupsOption.class, Integer.parseInt(hex, 16)));
   }
 
   @Inject
@@ -98,8 +98,7 @@
 
   @Override
   public List<GroupInfo> apply(TopLevelResource resource)
-      throws BadRequestException, MethodNotAllowedException, OrmException,
-          PermissionBackendException {
+      throws BadRequestException, MethodNotAllowedException, PermissionBackendException {
     if (Strings.isNullOrEmpty(query)) {
       throw new BadRequestException("missing query field");
     }
diff --git a/java/com/google/gerrit/server/restapi/project/CheckAccess.java b/java/com/google/gerrit/server/restapi/project/CheckAccess.java
index 1664635..67b68b5 100644
--- a/java/com/google/gerrit/server/restapi/project/CheckAccess.java
+++ b/java/com/google/gerrit/server/restapi/project/CheckAccess.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 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.reviewdb.client.Branch;
 import com.google.gerrit.server.account.AccountResolver;
@@ -35,7 +34,6 @@
 import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.project.ProjectResource;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -62,8 +60,7 @@
 
   @Override
   public AccessCheckInfo apply(ProjectResource rsrc, AccessCheckInput input)
-      throws OrmException, PermissionBackendException, RestApiException, IOException,
-          ConfigInvalidException {
+      throws PermissionBackendException, RestApiException, IOException, ConfigInvalidException {
     permissionBackend.user(rsrc.getUser()).check(GlobalPermission.VIEW_ACCESS);
 
     rsrc.getProjectState().checkStatePermitsRead();
@@ -75,20 +72,16 @@
       throw new BadRequestException("input requires 'account'");
     }
 
-    Account match = accountResolver.find(input.account);
-    if (match == null) {
-      throw new UnprocessableEntityException(
-          String.format("cannot find account %s", input.account));
-    }
+    Account.Id match = accountResolver.resolve(input.account).asUnique().getAccount().getId();
 
     AccessCheckInfo info = new AccessCheckInfo();
     try {
       permissionBackend
-          .absentUser(match.getId())
+          .absentUser(match)
           .project(rsrc.getNameKey())
           .check(ProjectPermission.ACCESS);
     } catch (AuthException e) {
-      info.message = String.format("user %s cannot see project %s", match.getId(), rsrc.getName());
+      info.message = String.format("user %s cannot see project %s", match, rsrc.getName());
       info.status = HttpServletResponse.SC_FORBIDDEN;
       return info;
     }
@@ -112,7 +105,7 @@
     if (!Strings.isNullOrEmpty(input.ref)) {
       try {
         permissionBackend
-            .absentUser(match.getId())
+            .absentUser(match)
             .ref(new Branch.NameKey(rsrc.getNameKey(), input.ref))
             .check(refPerm);
       } catch (AuthException e) {
@@ -120,7 +113,7 @@
         info.message =
             String.format(
                 "user %s lacks permission %s for %s in project %s",
-                match.getId(), input.permission, input.ref, rsrc.getName());
+                match, input.permission, input.ref, rsrc.getName());
         return info;
       }
     } else {
diff --git a/java/com/google/gerrit/server/restapi/project/CheckAccessReadView.java b/java/com/google/gerrit/server/restapi/project/CheckAccessReadView.java
index 0aab0e1..770e8c3 100644
--- a/java/com/google/gerrit/server/restapi/project/CheckAccessReadView.java
+++ b/java/com/google/gerrit/server/restapi/project/CheckAccessReadView.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectResource;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -50,8 +49,7 @@
 
   @Override
   public AccessCheckInfo apply(ProjectResource rsrc)
-      throws OrmException, PermissionBackendException, RestApiException, IOException,
-          ConfigInvalidException {
+      throws PermissionBackendException, RestApiException, IOException, ConfigInvalidException {
 
     AccessCheckInput input = new AccessCheckInput();
     input.ref = refName;
diff --git a/java/com/google/gerrit/server/restapi/project/CommitIncludedIn.java b/java/com/google/gerrit/server/restapi/project/CommitIncludedIn.java
index 3855b78..8cc8298 100644
--- a/java/com/google/gerrit/server/restapi/project/CommitIncludedIn.java
+++ b/java/com/google/gerrit/server/restapi/project/CommitIncludedIn.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.change.IncludedIn;
 import com.google.gerrit.server.project.CommitResource;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -36,8 +35,7 @@
   }
 
   @Override
-  public IncludedInInfo apply(CommitResource rsrc)
-      throws RestApiException, OrmException, IOException {
+  public IncludedInInfo apply(CommitResource rsrc) throws RestApiException, IOException {
     RevCommit commit = rsrc.getCommit();
     Project.NameKey project = rsrc.getProjectState().getNameKey();
     return 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 dc53923..d6c4469 100644
--- a/java/com/google/gerrit/server/restapi/project/CommitsCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/CommitsCollection.java
@@ -14,7 +14,8 @@
 
 package com.google.gerrit.server.restapi.project;
 
-import com.google.common.flogger.FluentLogger;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.ChildCollection;
 import com.google.gerrit.extensions.restapi.IdString;
@@ -22,6 +23,7 @@
 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;
@@ -30,7 +32,6 @@
 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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -39,14 +40,13 @@
 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;
 
 @Singleton
 public class CommitsCollection implements ChildCollection<ProjectResource, CommitResource> {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
   private final DynamicMap<RestView<CommitResource>> views;
   private final GitRepositoryManager repoManager;
   private final ChangeIndexCollection indexes;
@@ -105,23 +105,27 @@
   }
 
   /** @return true if {@code commit} is visible to the caller. */
-  public boolean canRead(ProjectState state, Repository repo, RevCommit commit) {
+  public boolean canRead(ProjectState state, Repository repo, RevCommit commit) throws IOException {
     Project.NameKey project = state.getNameKey();
-
-    // Look for changes associated with the commit.
-    if (indexes.getSearchIndex() != null) {
-      try {
-        List<ChangeData> changes =
-            queryProvider.get().enforceVisibility(true).byProjectCommit(project, commit);
-        if (!changes.isEmpty()) {
-          return true;
-        }
-      } catch (OrmException e) {
-        logger.atSevere().withCause(e).log(
-            "Cannot look up change for commit %s in %s", commit.name(), project);
-      }
+    if (indexes.getSearchIndex() == null) {
+      // No index in slaves, fall back to scanning refs.
+      return reachable.fromRefs(project, repo, commit, repo.getRefDatabase().getRefs());
     }
 
-    return reachable.fromRefs(project, repo, commit, repo.getAllRefs());
+    // 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);
+    if (!changes.isEmpty()) {
+      return true;
+    }
+
+    // If we have already checked change refs using the change index, spare any further checks for
+    // changes.
+    List<Ref> refs =
+        repo.getRefDatabase().getRefs().stream()
+            .filter(r -> !r.getName().startsWith(RefNames.REFS_CHANGES))
+            .collect(toImmutableList());
+    return reachable.fromRefs(project, repo, commit, refs);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/ConfigInfoImpl.java b/java/com/google/gerrit/server/restapi/project/ConfigInfoImpl.java
index e179896..37bc265 100644
--- a/java/com/google/gerrit/server/restapi/project/ConfigInfoImpl.java
+++ b/java/com/google/gerrit/server/restapi/project/ConfigInfoImpl.java
@@ -101,7 +101,6 @@
     for (UiAction.Description d : uiActions.from(views, new ProjectResource(projectState, user))) {
       actions.put(d.getId(), new ActionInfo(d));
     }
-    this.theme = projectState.getTheme();
 
     this.extensionPanelNames = projectState.getConfig().getExtensionPanelSections();
   }
diff --git a/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java b/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
index 66a8de2..0741377 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
@@ -16,7 +16,7 @@
 
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.errors.InvalidNameException;
+import com.google.gerrit.exceptions.InvalidNameException;
 import com.google.gerrit.extensions.api.access.ProjectAccessInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -28,12 +28,11 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
-import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.notedb.Sequences;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
@@ -44,7 +43,6 @@
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -64,10 +62,10 @@
   private final ChangeInserter.Factory changeInserterFactory;
   private final BatchUpdate.Factory updateFactory;
   private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
-  private final Provider<ReviewDb> db;
   private final SetAccessUtil setAccess;
   private final ChangeJson.Factory jsonFactory;
   private final ProjectCache projectCache;
+  private final ProjectConfig.Factory projectConfigFactory;
 
   @Inject
   CreateAccessChange(
@@ -76,25 +74,25 @@
       BatchUpdate.Factory updateFactory,
       Sequences seq,
       Provider<MetaDataUpdate.User> metaDataUpdateFactory,
-      Provider<ReviewDb> db,
       SetAccessUtil accessUtil,
       ChangeJson.Factory jsonFactory,
-      ProjectCache projectCache) {
+      ProjectCache projectCache,
+      ProjectConfig.Factory projectConfigFactory) {
     this.permissionBackend = permissionBackend;
     this.seq = seq;
     this.changeInserterFactory = changeInserterFactory;
     this.updateFactory = updateFactory;
     this.metaDataUpdateFactory = metaDataUpdateFactory;
-    this.db = db;
     this.setAccess = accessUtil;
     this.jsonFactory = jsonFactory;
     this.projectCache = projectCache;
+    this.projectConfigFactory = projectConfigFactory;
   }
 
   @Override
   public Response<ChangeInfo> apply(ProjectResource rsrc, ProjectAccessInput input)
       throws PermissionBackendException, AuthException, IOException, ConfigInvalidException,
-          OrmException, InvalidNameException, UpdateException, RestApiException {
+          InvalidNameException, UpdateException, RestApiException {
     PermissionBackend.ForProject forProject =
         permissionBackend.user(rsrc.getUser()).project(rsrc.getNameKey());
     if (!check(forProject, ProjectPermission.READ_CONFIG)) {
@@ -117,7 +115,7 @@
         input.parent == null ? null : new Project.NameKey(input.parent);
 
     try (MetaDataUpdate md = metaDataUpdateUser.create(rsrc.getNameKey())) {
-      ProjectConfig config = ProjectConfig.read(md);
+      ProjectConfig config = projectConfigFactory.read(md);
       ObjectId oldCommit = config.getRevision();
       String oldCommitSha1 = oldCommit == null ? null : oldCommit.getName();
 
@@ -150,7 +148,7 @@
           ObjectReader objReader = objInserter.newReader();
           RevWalk rw = new RevWalk(objReader);
           BatchUpdate bu =
-              updateFactory.create(db.get(), rsrc.getNameKey(), rsrc.getUser(), TimeUtil.nowTs())) {
+              updateFactory.create(rsrc.getNameKey(), rsrc.getUser(), TimeUtil.nowTs())) {
         bu.setRepository(md.getRepository(), rw, objInserter);
         ChangeInserter ins = newInserter(changeId, commit);
         bu.insertChange(ins);
diff --git a/java/com/google/gerrit/server/restapi/project/CreateProject.java b/java/com/google/gerrit/server/restapi/project/CreateProject.java
index eacce18..8b81f10 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateProject.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateProject.java
@@ -19,21 +19,13 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.ProjectUtil;
-import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ProjectInfo;
-import com.google.gerrit.extensions.events.NewProjectCreatedListener;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -41,30 +33,18 @@
 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.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.UsedAt;
-import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.ProjectUtil;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.ProjectOwnerGroupsProvider;
-import com.google.gerrit.server.config.RepositoryConfig;
-import com.google.gerrit.server.extensions.events.AbstractNoNotifyEvent;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.RepositoryCaseMismatchException;
-import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.group.GroupResolver;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.plugincontext.PluginItemContext;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
 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.gerrit.server.project.ProjectJson;
 import com.google.gerrit.server.project.ProjectNameLockManager;
 import com.google.gerrit.server.project.ProjectResource;
@@ -80,77 +60,43 @@
 import java.util.List;
 import java.util.concurrent.locks.Lock;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-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.PersonIdent;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.RefUpdate.Result;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.transport.ReceiveCommand;
 
 @RequiresCapability(GlobalCapability.CREATE_PROJECT)
 @Singleton
 public class CreateProject
     implements RestCollectionCreateView<TopLevelResource, ProjectResource, ProjectInput> {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
   private final Provider<ProjectsCollection> projectsCollection;
   private final Provider<GroupResolver> groupResolver;
   private final PluginSetContext<ProjectCreationValidationListener>
       projectCreationValidationListeners;
   private final ProjectJson json;
-  private final GitRepositoryManager repoManager;
-  private final PluginSetContext<NewProjectCreatedListener> createdListeners;
-  private final ProjectCache projectCache;
-  private final GroupBackend groupBackend;
   private final ProjectOwnerGroupsProvider.Factory projectOwnerGroups;
-  private final MetaDataUpdate.User metaDataUpdateFactory;
-  private final GitReferenceUpdated referenceUpdated;
-  private final RepositoryConfig repositoryCfg;
-  private final Provider<PersonIdent> serverIdent;
-  private final Provider<IdentifiedUser> identifiedUser;
   private final Provider<PutConfig> putConfig;
   private final AllProjectsName allProjects;
   private final AllUsersName allUsers;
   private final PluginItemContext<ProjectNameLockManager> lockManager;
+  private final ProjectCreator projectCreator;
 
   @Inject
   CreateProject(
+      ProjectCreator projectCreator,
       Provider<ProjectsCollection> projectsCollection,
       Provider<GroupResolver> groupResolver,
       ProjectJson json,
       PluginSetContext<ProjectCreationValidationListener> projectCreationValidationListeners,
-      GitRepositoryManager repoManager,
-      PluginSetContext<NewProjectCreatedListener> createdListeners,
-      ProjectCache projectCache,
-      GroupBackend groupBackend,
       ProjectOwnerGroupsProvider.Factory projectOwnerGroups,
-      MetaDataUpdate.User metaDataUpdateFactory,
-      GitReferenceUpdated referenceUpdated,
-      RepositoryConfig repositoryCfg,
-      @GerritPersonIdent Provider<PersonIdent> serverIdent,
-      Provider<IdentifiedUser> identifiedUser,
       Provider<PutConfig> putConfig,
       AllProjectsName allProjects,
       AllUsersName allUsers,
       PluginItemContext<ProjectNameLockManager> lockManager) {
     this.projectsCollection = projectsCollection;
+    this.projectCreator = projectCreator;
     this.groupResolver = groupResolver;
     this.projectCreationValidationListeners = projectCreationValidationListeners;
     this.json = json;
-    this.repoManager = repoManager;
-    this.createdListeners = createdListeners;
-    this.projectCache = projectCache;
-    this.groupBackend = groupBackend;
     this.projectOwnerGroups = projectOwnerGroups;
-    this.metaDataUpdateFactory = metaDataUpdateFactory;
-    this.referenceUpdated = referenceUpdated;
-    this.repositoryCfg = repositoryCfg;
-    this.serverIdent = serverIdent;
-    this.identifiedUser = identifiedUser;
     this.putConfig = putConfig;
     this.allProjects = allProjects;
     this.allUsers = allUsers;
@@ -225,7 +171,7 @@
         throw new ResourceConflictException(e.getMessage(), e);
       }
 
-      ProjectState projectState = createProject(args);
+      ProjectState projectState = projectCreator.createProject(args);
       requireNonNull(
           projectState,
           () -> String.format("failed to create project %s", args.getProject().get()));
@@ -233,6 +179,7 @@
       if (input.pluginConfigValues != null) {
         ConfigInput in = new ConfigInput();
         in.pluginConfigValues = input.pluginConfigValues;
+        in.description = args.projectDescription;
         putConfig.get().apply(projectState, in);
       }
       return Response.created(json.format(projectState));
@@ -241,95 +188,6 @@
     }
   }
 
-  @UsedAt(UsedAt.Project.COLLABNET)
-  public ProjectState createProject(CreateProjectArgs args)
-      throws BadRequestException, ResourceConflictException, IOException, ConfigInvalidException {
-    final Project.NameKey nameKey = args.getProject();
-    try {
-      final String head = args.permissionsOnly ? RefNames.REFS_CONFIG : args.branch.get(0);
-      try (Repository repo = repoManager.openRepository(nameKey)) {
-        if (repo.getObjectDatabase().exists()) {
-          throw new ResourceConflictException("project \"" + nameKey + "\" exists");
-        }
-      } catch (RepositoryNotFoundException e) {
-        // It does not exist, safe to ignore.
-      }
-      try (Repository repo = repoManager.createRepository(nameKey)) {
-        RefUpdate u = repo.updateRef(Constants.HEAD);
-        u.disableRefLog();
-        u.link(head);
-
-        createProjectConfig(args);
-
-        if (!args.permissionsOnly && args.createEmptyCommit) {
-          createEmptyCommits(repo, nameKey, args.branch);
-        }
-
-        fire(nameKey, head);
-
-        return projectCache.get(nameKey);
-      }
-    } catch (RepositoryCaseMismatchException e) {
-      throw new ResourceConflictException(
-          "Cannot create "
-              + nameKey.get()
-              + " because the name is already occupied by another project."
-              + " The other project has the same name, only spelled in a"
-              + " different case.",
-          e);
-    } catch (RepositoryNotFoundException badName) {
-      throw new BadRequestException("invalid project name: " + nameKey, badName);
-    } catch (ConfigInvalidException e) {
-      String msg = "Cannot create " + nameKey;
-      logger.atSevere().withCause(e).log(msg);
-      throw e;
-    }
-  }
-
-  private void createProjectConfig(CreateProjectArgs args)
-      throws IOException, ConfigInvalidException {
-    try (MetaDataUpdate md = metaDataUpdateFactory.create(args.getProject())) {
-      ProjectConfig config = ProjectConfig.read(md);
-
-      Project newProject = config.getProject();
-      newProject.setDescription(args.projectDescription);
-      newProject.setSubmitType(
-          MoreObjects.firstNonNull(
-              args.submitType, repositoryCfg.getDefaultSubmitType(args.getProject())));
-      newProject.setBooleanConfig(
-          BooleanProjectConfig.USE_CONTRIBUTOR_AGREEMENTS, args.contributorAgreements);
-      newProject.setBooleanConfig(BooleanProjectConfig.USE_SIGNED_OFF_BY, args.signedOffBy);
-      newProject.setBooleanConfig(BooleanProjectConfig.USE_CONTENT_MERGE, args.contentMerge);
-      newProject.setBooleanConfig(
-          BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET,
-          args.newChangeForAllNotInTarget);
-      newProject.setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, args.changeIdRequired);
-      newProject.setBooleanConfig(BooleanProjectConfig.REJECT_EMPTY_COMMIT, args.rejectEmptyCommit);
-      newProject.setMaxObjectSizeLimit(args.maxObjectSizeLimit);
-      newProject.setBooleanConfig(BooleanProjectConfig.ENABLE_SIGNED_PUSH, args.enableSignedPush);
-      newProject.setBooleanConfig(BooleanProjectConfig.REQUIRE_SIGNED_PUSH, args.requireSignedPush);
-      if (args.newParent != null) {
-        newProject.setParentName(args.newParent);
-      }
-
-      if (!args.ownerIds.isEmpty()) {
-        AccessSection all = config.getAccessSection(AccessSection.ALL, true);
-        for (AccountGroup.UUID ownerId : args.ownerIds) {
-          GroupDescription.Basic g = groupBackend.get(ownerId);
-          if (g != null) {
-            GroupReference group = config.resolve(GroupReference.forGroup(g));
-            all.getPermission(Permission.OWNER, true).add(new PermissionRule(group));
-          }
-        }
-      }
-
-      md.setMessage("Created project\n");
-      config.commit(md);
-      md.getRepository().setGitwebDescription(args.projectDescription);
-    }
-    projectCache.onCreateProject(args.getProject());
-  }
-
   private List<String> normalizeBranchNames(List<String> branches) throws BadRequestException {
     if (branches == null || branches.isEmpty()) {
       return Collections.singletonList(Constants.R_HEADS + Constants.MASTER);
@@ -350,78 +208,4 @@
     }
     return normalizedBranches;
   }
-
-  private void createEmptyCommits(Repository repo, Project.NameKey project, List<String> refs)
-      throws IOException {
-    try (ObjectInserter oi = repo.newObjectInserter()) {
-      CommitBuilder cb = new CommitBuilder();
-      cb.setTreeId(oi.insert(Constants.OBJ_TREE, new byte[] {}));
-      cb.setAuthor(metaDataUpdateFactory.getUserPersonIdent());
-      cb.setCommitter(serverIdent.get());
-      cb.setMessage("Initial empty repository\n");
-
-      ObjectId id = oi.insert(cb);
-      oi.flush();
-
-      for (String ref : refs) {
-        RefUpdate ru = repo.updateRef(ref);
-        ru.setNewObjectId(id);
-        Result result = ru.update();
-        switch (result) {
-          case NEW:
-            referenceUpdated.fire(
-                project, ru, ReceiveCommand.Type.CREATE, identifiedUser.get().state());
-            break;
-          case FAST_FORWARD:
-          case FORCED:
-          case IO_FAILURE:
-          case LOCK_FAILURE:
-          case NOT_ATTEMPTED:
-          case NO_CHANGE:
-          case REJECTED:
-          case REJECTED_CURRENT_BRANCH:
-          case RENAMED:
-          case REJECTED_MISSING_OBJECT:
-          case REJECTED_OTHER_REASON:
-          default:
-            {
-              throw new IOException(
-                  String.format("Failed to create ref \"%s\": %s", ref, result.name()));
-            }
-        }
-      }
-    } catch (IOException e) {
-      logger.atSevere().withCause(e).log("Cannot create empty commit for %s", project.get());
-      throw e;
-    }
-  }
-
-  private void fire(Project.NameKey name, String head) {
-    if (createdListeners.isEmpty()) {
-      return;
-    }
-
-    Event event = new Event(name, head);
-    createdListeners.runEach(l -> l.onNewProjectCreated(event));
-  }
-
-  static class Event extends AbstractNoNotifyEvent implements NewProjectCreatedListener.Event {
-    private final Project.NameKey name;
-    private final String head;
-
-    Event(Project.NameKey name, String head) {
-      this.name = name;
-      this.head = head;
-    }
-
-    @Override
-    public String getProjectName() {
-      return name.get();
-    }
-
-    @Override
-    public String getHeadName() {
-      return head;
-    }
-  }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteBranch.java b/java/com/google/gerrit/server/restapi/project/DeleteBranch.java
index 1d327ca..b94fb51 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteBranch.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteBranch.java
@@ -27,7 +27,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.BranchResource;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -47,7 +46,7 @@
 
   @Override
   public Response<?> apply(BranchResource rsrc, Input input)
-      throws RestApiException, OrmException, IOException, PermissionBackendException {
+      throws RestApiException, IOException, PermissionBackendException {
     if (RefNames.HEAD.equals(rsrc.getBranchKey().get())) {
       throw new MethodNotAllowedException("not allowed to delete HEAD");
     } else if (isConfigRef(rsrc.getBranchKey().get())) {
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteBranches.java b/java/com/google/gerrit/server/restapi/project/DeleteBranches.java
index 28c0501..ba25e33 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteBranches.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteBranches.java
@@ -26,7 +26,6 @@
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectResource;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -42,7 +41,7 @@
 
   @Override
   public Response<?> apply(ProjectResource project, DeleteBranchesInput input)
-      throws OrmException, IOException, RestApiException, PermissionBackendException {
+      throws IOException, RestApiException, PermissionBackendException {
     if (input == null || input.branches == null || input.branches.isEmpty()) {
       throw new BadRequestException("branches must be specified");
     }
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteRef.java b/java/com/google/gerrit/server/restapi/project/DeleteRef.java
index 5df102c..dae759a 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteRef.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteRef.java
@@ -25,6 +25,7 @@
 import com.google.common.collect.Iterables;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+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;
@@ -38,7 +39,6 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.RefValidationHelper;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -157,7 +157,7 @@
           break;
 
         case REJECTED_CURRENT_BRANCH:
-          logger.atSevere().log("Cannot delete %s: %s", ref, result.name());
+          logger.atFine().log("Cannot delete current branch %s: %s", ref, result.name());
           throw new ResourceConflictException("cannot delete current branch");
 
         case IO_FAILURE:
@@ -168,8 +168,7 @@
         case REJECTED_MISSING_OBJECT:
         case REJECTED_OTHER_REASON:
         default:
-          logger.atSevere().log("Cannot delete %s: %s", ref, result.name());
-          throw new ResourceConflictException("cannot delete: " + result.name());
+          throw new StorageException(String.format("Cannot delete %s: %s", ref, result.name()));
       }
     }
   }
@@ -180,15 +179,13 @@
    * @param projectState the {@code ProjectState} of the project whose refs are to be deleted.
    * @param refsToDelete the refs to be deleted.
    * @param prefix the prefix of the refs.
-   * @throws OrmException
    * @throws IOException
    * @throws ResourceConflictException
    * @throws PermissionBackendException
    */
   public void deleteMultipleRefs(
       ProjectState projectState, ImmutableSet<String> refsToDelete, String prefix)
-      throws OrmException, IOException, ResourceConflictException, PermissionBackendException,
-          AuthException {
+      throws IOException, ResourceConflictException, PermissionBackendException, AuthException {
     if (refsToDelete.isEmpty()) {
       return;
     }
@@ -229,8 +226,8 @@
 
   private ReceiveCommand createDeleteCommand(
       ProjectState projectState, Repository r, String refName)
-      throws OrmException, IOException, ResourceConflictException, PermissionBackendException {
-    Ref ref = r.getRefDatabase().getRef(refName);
+      throws IOException, ResourceConflictException, PermissionBackendException {
+    Ref ref = r.getRefDatabase().exactRef(refName);
     ReceiveCommand command;
     if (ref == null) {
       command = new ReceiveCommand(ObjectId.zeroId(), ObjectId.zeroId(), refName);
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteTag.java b/java/com/google/gerrit/server/restapi/project/DeleteTag.java
index f7cce11..33955ee 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteTag.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteTag.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.RefUtil;
 import com.google.gerrit.server.project.TagResource;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -41,7 +40,7 @@
 
   @Override
   public Response<?> apply(TagResource resource, Input input)
-      throws OrmException, RestApiException, IOException, PermissionBackendException {
+      throws RestApiException, IOException, PermissionBackendException {
     String tag = RefUtil.normalizeTagRef(resource.getTagInfo().ref);
 
     if (isConfigRef(tag)) {
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteTags.java b/java/com/google/gerrit/server/restapi/project/DeleteTags.java
index bf2c524..6e8ec37 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteTags.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteTags.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectResource;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -40,7 +39,7 @@
 
   @Override
   public Response<?> apply(ProjectResource project, DeleteTagsInput input)
-      throws OrmException, RestApiException, IOException, PermissionBackendException {
+      throws RestApiException, IOException, PermissionBackendException {
     if (input == null || input.tags == null || input.tags.isEmpty()) {
       throw new BadRequestException("tags must be specified");
     }
diff --git a/java/com/google/gerrit/server/restapi/project/GetAccess.java b/java/com/google/gerrit/server/restapi/project/GetAccess.java
index 4ec187a..d6c07dd 100644
--- a/java/com/google/gerrit/server/restapi/project/GetAccess.java
+++ b/java/com/google/gerrit/server/restapi/project/GetAccess.java
@@ -29,14 +29,11 @@
 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.common.data.RefConfigSection;
-import com.google.gerrit.common.data.WebLinkInfoCommon;
 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.common.GroupInfo;
-import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -95,6 +92,7 @@
   private final Provider<MetaDataUpdate.Server> metaDataUpdateFactory;
   private final GroupBackend groupBackend;
   private final WebLinks webLinks;
+  private final ProjectConfig.Factory projectConfigFactory;
 
   @Inject
   public GetAccess(
@@ -105,7 +103,8 @@
       Provider<MetaDataUpdate.Server> metaDataUpdateFactory,
       ProjectJson projectJson,
       GroupBackend groupBackend,
-      WebLinks webLinks) {
+      WebLinks webLinks,
+      ProjectConfig.Factory projectConfigFactory) {
     this.user = self;
     this.permissionBackend = permissionBackend;
     this.allProjectsName = allProjectsName;
@@ -114,6 +113,7 @@
     this.metaDataUpdateFactory = metaDataUpdateFactory;
     this.groupBackend = groupBackend;
     this.webLinks = webLinks;
+    this.projectConfigFactory = projectConfigFactory;
   }
 
   public ProjectAccessInfo apply(Project.NameKey nameKey)
@@ -141,18 +141,14 @@
 
     ProjectConfig config;
     try (MetaDataUpdate md = metaDataUpdateFactory.get().create(projectName)) {
-      config = ProjectConfig.read(md);
+      config = projectConfigFactory.read(md);
       info.configWebLinks = new ArrayList<>();
 
       // config may have a null revision if the repo doesn't have its own refs/meta/config.
       if (config.getRevision() != null) {
-        // WebLinks operates in terms of the data types used in the GWT UI. Once the GWT UI is
-        // gone, WebLinks should be fixed to use the extension data types.
-        for (WebLinkInfoCommon wl :
+        info.configWebLinks.addAll(
             webLinks.getFileHistoryLinks(
-                projectName.get(), config.getRevision().getName(), ProjectConfig.PROJECT_CONFIG)) {
-          info.configWebLinks.add(new WebLinkInfo(wl.name, wl.imageUrl, wl.url, wl.target));
-        }
+                projectName.get(), config.getRevision().getName(), ProjectConfig.PROJECT_CONFIG));
       }
 
       if (config.updateGroupNames(groupBackend)) {
@@ -199,7 +195,7 @@
           info.local.put(section.getName(), createAccessSection(groups, section));
         }
 
-      } else if (RefConfigSection.isValid(name)) {
+      } else if (AccessSection.isValidRefSectionName(name)) {
         if (check(perm, name, WRITE_CONFIG)) {
           info.local.put(name, createAccessSection(groups, section));
           info.ownerOf.add(name);
diff --git a/java/com/google/gerrit/server/restapi/project/Index.java b/java/com/google/gerrit/server/restapi/project/Index.java
index a346aed..bc58b23 100644
--- a/java/com/google/gerrit/server/restapi/project/Index.java
+++ b/java/com/google/gerrit/server/restapi/project/Index.java
@@ -16,7 +16,6 @@
 
 import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
 
-import com.google.common.flogger.FluentLogger;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
@@ -30,7 +29,6 @@
 import com.google.gerrit.server.index.IndexExecutor;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectResource;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -40,8 +38,6 @@
 @RequiresCapability(GlobalCapability.MAINTAIN_SERVER)
 @Singleton
 public class Index implements RestModifyView<ProjectResource, IndexProjectInput> {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
   private final ProjectIndexer indexer;
   private final ListeningExecutorService executor;
   private final Provider<ListChildProjects> listChildProjectsProvider;
@@ -58,7 +54,7 @@
 
   @Override
   public Response.Accepted apply(ProjectResource rsrc, IndexProjectInput input)
-      throws IOException, OrmException, PermissionBackendException, RestApiException {
+      throws IOException, PermissionBackendException, RestApiException {
     String response = "Project " + rsrc.getName() + " submitted for reindexing";
 
     reindex(rsrc.getNameKey(), input.async);
@@ -72,18 +68,10 @@
     return Response.accepted(response);
   }
 
-  private void reindex(Project.NameKey project, Boolean async) throws IOException {
+  private void reindex(Project.NameKey project, Boolean async) {
     if (Boolean.TRUE.equals(async)) {
       @SuppressWarnings("unused")
-      Future<?> possiblyIgnoredError =
-          executor.submit(
-              () -> {
-                try {
-                  indexer.index(project);
-                } catch (IOException e) {
-                  logger.atWarning().withCause(e).log("reindexing project %s failed", project);
-                }
-              });
+      Future<?> possiblyIgnoredError = executor.submit(() -> indexer.index(project));
     } else {
       indexer.index(project);
     }
diff --git a/java/com/google/gerrit/server/restapi/project/ListChildProjects.java b/java/com/google/gerrit/server/restapi/project/ListChildProjects.java
index ea54a4a..3879720 100644
--- a/java/com/google/gerrit/server/restapi/project/ListChildProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/ListChildProjects.java
@@ -27,7 +27,6 @@
 import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.project.ChildProjects;
 import com.google.gerrit.server.project.ProjectResource;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.util.List;
@@ -67,7 +66,7 @@
 
   @Override
   public List<ProjectInfo> apply(ProjectResource rsrc)
-      throws PermissionBackendException, OrmException, RestApiException {
+      throws PermissionBackendException, RestApiException {
     if (limit < 0) {
       throw new BadRequestException("limit must be a positive number");
     }
@@ -82,8 +81,7 @@
     return directChildProjects(rsrc.getNameKey());
   }
 
-  private List<ProjectInfo> directChildProjects(Project.NameKey parent)
-      throws OrmException, RestApiException {
+  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(
diff --git a/java/com/google/gerrit/server/restapi/project/ListProjects.java b/java/com/google/gerrit/server/restapi/project/ListProjects.java
index eccf29e..4f3dbb7 100644
--- a/java/com/google/gerrit/server/restapi/project/ListProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/ListProjects.java
@@ -27,7 +27,8 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -37,11 +38,11 @@
 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.OutputFormat;
 import com.google.gerrit.server.WebLinks;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -57,7 +58,6 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.util.TreeFormatter;
 import com.google.gson.reflect.TypeToken;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.BufferedWriter;
@@ -105,17 +105,6 @@
         return true;
       }
     },
-    PARENT_CANDIDATES {
-      @Override
-      boolean matches(Repository git) {
-        return true;
-      }
-
-      @Override
-      boolean useMatch() {
-        return false;
-      }
-    },
     PERMISSIONS {
       @Override
       boolean matches(Repository git) throws IOException {
@@ -369,7 +358,7 @@
           .collect(
               ImmutableSortedMap.toImmutableSortedMap(
                   natural(), p -> p.name, p -> showDescription ? p : nullifyDescription(p)));
-    } catch (OrmException | MethodNotAllowedException e) {
+    } catch (StorageException | MethodNotAllowedException e) {
       logger.atWarning().withCause(e).log(
           "Internal error while processing the query '%s' request", query);
       throw new BadRequestException("Internal error while processing the query request");
@@ -389,7 +378,7 @@
         newProjectsNamesStream(query).forEach(out::println);
       }
       out.flush();
-    } catch (OrmException | MethodNotAllowedException e) {
+    } catch (StorageException | MethodNotAllowedException e) {
       logger.atWarning().withCause(e).log(
           "Internal error while processing the query '%s' request", query);
       throw new BadRequestException("Internal error while processing the query request");
@@ -397,7 +386,7 @@
   }
 
   private Stream<String> newProjectsNamesStream(String query)
-      throws OrmException, MethodNotAllowedException, BadRequestException {
+      throws MethodNotAllowedException, BadRequestException {
     Stream<String> projects =
         queryProjectsProvider.get().withQuery(query).apply().stream().map(p -> p.name).skip(start);
     if (limit > 0) {
@@ -436,11 +425,6 @@
       }
     }
 
-    if (type == FilterType.PARENT_CANDIDATES) {
-      // Historically, PARENT_CANDIDATES implied showDescription.
-      showDescription = true;
-    }
-
     int foundIndex = 0;
     int found = 0;
     TreeMap<String, ProjectInfo> output = new TreeMap<>();
@@ -449,10 +433,10 @@
     PermissionBackend.WithUser perm = permissionBackend.user(currentUser);
     final TreeMap<Project.NameKey, ProjectNode> treeMap = new TreeMap<>();
     try {
-      Iterable<Project.NameKey> projectNames = filter(perm)::iterator;
-      for (Project.NameKey projectName : projectNames) {
-        final ProjectState e = projectCache.get(projectName);
-        if (e == null || (e.getProject().getState() == HIDDEN && !all && state != HIDDEN)) {
+      Iterable<ProjectState> projectStatesIt = filter(perm)::iterator;
+      for (ProjectState e : projectStatesIt) {
+        Project.NameKey projectName = e.getNameKey();
+        if (e.getProject().getState() == HIDDEN && !all && state != HIDDEN) {
           // If we can't get it from the cache, pretend it's not present.
           // If all wasn't selected, and it's HIDDEN, pretend it's not present.
           // If state HIDDEN wasn't selected, and it's HIDDEN, pretend it's not present.
@@ -469,26 +453,22 @@
           continue;
         }
 
-        ProjectInfo info = new ProjectInfo();
         if (showTree && !format.isJson()) {
           treeMap.put(projectName, projectNodeFactory.create(e.getProject(), true));
           continue;
         }
 
+        if (foundIndex++ < start) {
+          continue;
+        }
+        if (limit > 0 && ++found > limit) {
+          break;
+        }
+
+        ProjectInfo info = new ProjectInfo();
         info.name = projectName.get();
         if (showTree && format.isJson()) {
-          ProjectState parent = Iterables.getFirst(e.parents(), null);
-          if (parent != null) {
-            if (isParentAccessible(accessibleParents, perm, parent)) {
-              info.parent = parent.getName();
-            } else {
-              info.parent = hiddenNames.get(parent.getName());
-              if (info.parent == null) {
-                info.parent = "?-" + (hiddenNames.size() + 1);
-                hiddenNames.put(parent.getName(), info.parent);
-              }
-            }
-          }
+          addParentProjectInfo(hiddenNames, accessibleParents, perm, e, info);
         }
 
         if (showDescription) {
@@ -503,32 +483,12 @@
                 continue;
               }
 
-              boolean canReadAllRefs = e.statePermitsRead();
-              if (canReadAllRefs) {
-                try {
-                  permissionBackend
-                      .user(currentUser)
-                      .project(e.getNameKey())
-                      .check(ProjectPermission.READ);
-                } catch (AuthException exp) {
-                  canReadAllRefs = false;
-                }
-              }
-
-              List<Ref> refs = getBranchRefs(projectName, canReadAllRefs);
+              List<Ref> refs = retieveBranchRefs(e);
               if (!hasValidRef(refs)) {
                 continue;
               }
 
-              for (int i = 0; i < showBranch.size(); i++) {
-                Ref ref = refs.get(i);
-                if (ref != null && ref.getObjectId() != null) {
-                  if (info.branches == null) {
-                    info.branches = new LinkedHashMap<>();
-                  }
-                  info.branches.put(showBranch.get(i), ref.getObjectId().name());
-                }
-              }
+              addProjectBranchesInfo(info, refs);
             }
           } else if (!showTree && type.useMatch()) {
             try (Repository git = repoManager.openRepository(projectName)) {
@@ -545,17 +505,8 @@
           continue;
         }
 
-        if (type != FilterType.PARENT_CANDIDATES) {
-          List<WebLinkInfo> links = webLinks.getProjectLinks(projectName.get());
-          info.webLinks = links.isEmpty() ? null : links;
-        }
-
-        if (foundIndex++ < start) {
-          continue;
-        }
-        if (limit > 0 && ++found > limit) {
-          break;
-        }
+        List<WebLinkInfo> links = webLinks.getProjectLinks(projectName.get());
+        info.webLinks = links.isEmpty() ? null : links;
 
         if (stdout == null || format.isJson()) {
           output.put(info.name, info);
@@ -563,15 +514,7 @@
         }
 
         if (!showBranch.isEmpty()) {
-          for (String name : showBranch) {
-            String ref = info.branches != null ? info.branches.get(name) : null;
-            if (ref == null) {
-              // Print stub (forty '-' symbols)
-              ref = "----------------------------------------";
-            }
-            stdout.print(ref);
-            stdout.print(' ');
-          }
+          printProjectBranches(stdout, info);
         }
         stdout.print(info.name);
 
@@ -604,28 +547,79 @@
     }
   }
 
-  private Stream<Project.NameKey> filter(PermissionBackend.WithUser perm)
-      throws BadRequestException {
-    Stream<Project.NameKey> matches = StreamSupport.stream(scan().spliterator(), false);
-    if (type == FilterType.PARENT_CANDIDATES) {
-      matches =
-          matches.map(projectCache::get).map(this::parentOf).filter(Objects::nonNull).sorted();
+  private void printProjectBranches(PrintWriter stdout, ProjectInfo info) {
+    for (String name : showBranch) {
+      String ref = info.branches != null ? info.branches.get(name) : null;
+      if (ref == null) {
+        // Print stub (forty '-' symbols)
+        ref = "----------------------------------------";
+      }
+      stdout.print(ref);
+      stdout.print(' ');
     }
-    return matches.filter(p -> perm.project(p).testOrFalse(ProjectPermission.ACCESS));
   }
 
-  private Project.NameKey parentOf(ProjectState ps) {
-    if (ps == null) {
-      return null;
-    }
-    Project.NameKey parent = ps.getProject().getParent();
-    if (parent != null) {
-      if (projectCache.get(parent) != null) {
-        return parent;
+  private void addProjectBranchesInfo(ProjectInfo info, List<Ref> refs) {
+    for (int i = 0; i < showBranch.size(); i++) {
+      Ref ref = refs.get(i);
+      if (ref != null && ref.getObjectId() != null) {
+        if (info.branches == null) {
+          info.branches = new LinkedHashMap<>();
+        }
+        info.branches.put(showBranch.get(i), ref.getObjectId().name());
       }
-      logger.atWarning().log("parent project %s of project %s not found", ps.getName());
     }
-    return null;
+  }
+
+  private List<Ref> retieveBranchRefs(ProjectState e) throws PermissionBackendException {
+    boolean canReadAllRefs = e.statePermitsRead();
+    if (canReadAllRefs) {
+      try {
+        permissionBackend.user(currentUser).project(e.getNameKey()).check(ProjectPermission.READ);
+      } catch (AuthException exp) {
+        canReadAllRefs = false;
+      }
+    }
+
+    return getBranchRefs(e.getNameKey(), canReadAllRefs);
+  }
+
+  private void addParentProjectInfo(
+      Map<String, String> hiddenNames,
+      Map<Project.NameKey, Boolean> accessibleParents,
+      PermissionBackend.WithUser perm,
+      ProjectState e,
+      ProjectInfo info)
+      throws PermissionBackendException {
+    ProjectState parent = Iterables.getFirst(e.parents(), null);
+    if (parent != null) {
+      if (isParentAccessible(accessibleParents, perm, parent)) {
+        info.parent = parent.getName();
+      } else {
+        info.parent = hiddenNames.get(parent.getName());
+        if (info.parent == null) {
+          info.parent = "?-" + (hiddenNames.size() + 1);
+          hiddenNames.put(parent.getName(), info.parent);
+        }
+      }
+    }
+  }
+
+  private Stream<ProjectState> filter(PermissionBackend.WithUser perm) throws BadRequestException {
+    return StreamSupport.stream(scan().spliterator(), false)
+        .map(projectCache::get)
+        .filter(Objects::nonNull)
+        .filter(p -> permissionCheck(p, perm));
+  }
+
+  private boolean permissionCheck(ProjectState state, PermissionBackend.WithUser perm) {
+    // Hidden projects(permitsRead = false) should only be accessible by the project owners.
+    // READ_CONFIG is checked here because it's only allowed to project owners(ACCESS may also
+    // be allowed for other users). Allowing project owners to access here will help them to view
+    // and update the config of hidden projects easily.
+    return perm.project(state.getNameKey())
+        .testOrFalse(
+            state.statePermitsRead() ? ProjectPermission.ACCESS : ProjectPermission.READ_CONFIG);
   }
 
   private boolean isParentAccessible(
diff --git a/java/com/google/gerrit/server/restapi/project/ListTags.java b/java/com/google/gerrit/server/restapi/project/ListTags.java
index 5fbfcf6..08b9b84 100644
--- a/java/com/google/gerrit/server/restapi/project/ListTags.java
+++ b/java/com/google/gerrit/server/restapi/project/ListTags.java
@@ -17,7 +17,7 @@
 import static com.google.gerrit.reviewdb.client.RefNames.isConfigRef;
 import static java.util.Comparator.comparing;
 
-import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.extensions.api.projects.ProjectApi.ListRefsRequest;
 import com.google.gerrit.extensions.api.projects.TagInfo;
 import com.google.gerrit.extensions.common.WebLinkInfo;
@@ -127,7 +127,8 @@
     try (Repository repo = getRepository(resource.getNameKey());
         RevWalk rw = new RevWalk(repo)) {
       Map<String, Ref> all =
-          visibleTags(resource.getNameKey(), repo, repo.getRefDatabase().getRefs(Constants.R_TAGS));
+          visibleTags(
+              resource.getNameKey(), repo, repo.getRefDatabase().getRefsByPrefix(Constants.R_TAGS));
       for (Ref ref : all.values()) {
         tags.add(
             createTagInfo(perm.ref(ref.getName()), ref, rw, resource.getProjectState(), links));
@@ -154,8 +155,7 @@
       }
       Ref ref = repo.getRefDatabase().exactRef(tagName);
       if (ref != null
-          && !visibleTags(resource.getNameKey(), repo, ImmutableMap.of(ref.getName(), ref))
-              .isEmpty()) {
+          && !visibleTags(resource.getNameKey(), repo, ImmutableList.of(ref)).isEmpty()) {
         return createTagInfo(
             permissionBackend
                 .user(resource.getUser())
@@ -221,15 +221,11 @@
     }
   }
 
-  private Map<String, Ref> visibleTags(
-      Project.NameKey project, Repository repo, Map<String, Ref> tags)
+  private Map<String, Ref> visibleTags(Project.NameKey project, Repository repo, List<Ref> tags)
       throws PermissionBackendException {
     return permissionBackend
         .currentUser()
         .project(project)
-        .filter(
-            tags,
-            repo,
-            RefFilterOptions.builder().setFilterMeta(true).setFilterTagsSeparately(true).build());
+        .filter(tags, repo, RefFilterOptions.builder().setFilterMeta(true).build());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/Module.java b/java/com/google/gerrit/server/restapi/project/Module.java
index 8c8ab49..de5661d 100644
--- a/java/com/google/gerrit/server/restapi/project/Module.java
+++ b/java/com/google/gerrit/server/restapi/project/Module.java
@@ -55,7 +55,6 @@
     get(PROJECT_KIND, "access").to(GetAccess.class);
     post(PROJECT_KIND, "access").to(SetAccess.class);
     put(PROJECT_KIND, "access:review").to(CreateAccessChange.class);
-    post(PROJECT_KIND, "check.access").to(CheckAccess.class);
     get(PROJECT_KIND, "check.access").to(CheckAccessReadView.class);
 
     post(PROJECT_KIND, "check").to(Check.class);
diff --git a/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java b/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java
index ed317aa..31c90e5 100644
--- a/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java
@@ -17,7 +17,6 @@
 import com.google.common.collect.ListMultimap;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.ProjectUtil;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -30,9 +29,10 @@
 import com.google.gerrit.extensions.restapi.RestView;
 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.OutputFormat;
+import com.google.gerrit.server.ProjectUtil;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
diff --git a/java/com/google/gerrit/server/restapi/project/PutConfig.java b/java/com/google/gerrit/server/restapi/project/PutConfig.java
index e0b443b..56ddbb4 100644
--- a/java/com/google/gerrit/server/restapi/project/PutConfig.java
+++ b/java/com/google/gerrit/server/restapi/project/PutConfig.java
@@ -47,6 +47,7 @@
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.project.ProjectState.Factory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -54,7 +55,6 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
 import java.util.regex.Pattern;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -77,20 +77,22 @@
   private final DynamicMap<RestView<ProjectResource>> views;
   private final Provider<CurrentUser> user;
   private final PermissionBackend permissionBackend;
+  private final ProjectConfig.Factory projectConfigFactory;
 
   @Inject
   PutConfig(
       @EnableSignedPush boolean serverEnableSignedPush,
       Provider<MetaDataUpdate.User> metaDataUpdateFactory,
       ProjectCache projectCache,
-      ProjectState.Factory projectStateFactory,
+      Factory projectStateFactory,
       DynamicMap<ProjectConfigEntry> pluginConfigEntries,
       PluginConfigFactory cfgFactory,
       AllProjectsName allProjects,
       UiActions uiActions,
       DynamicMap<RestView<ProjectResource>> views,
       Provider<CurrentUser> user,
-      PermissionBackend permissionBackend) {
+      PermissionBackend permissionBackend,
+      ProjectConfig.Factory projectConfigFactory) {
     this.serverEnableSignedPush = serverEnableSignedPush;
     this.metaDataUpdateFactory = metaDataUpdateFactory;
     this.projectCache = projectCache;
@@ -102,6 +104,7 @@
     this.views = views;
     this.user = user;
     this.permissionBackend = permissionBackend;
+    this.projectConfigFactory = projectConfigFactory;
   }
 
   @Override
@@ -122,7 +125,7 @@
     }
 
     try (MetaDataUpdate md = metaDataUpdateFactory.get().create(projectName)) {
-      ProjectConfig projectConfig = ProjectConfig.read(md);
+      ProjectConfig projectConfig = projectConfigFactory.read(md);
       Project p = projectConfig.getProject();
 
       p.setDescription(Strings.emptyToNull(input.description));
@@ -164,7 +167,7 @@
         throw new ResourceConflictException("Cannot update " + projectName);
       }
 
-      ProjectState state = projectStateFactory.create(ProjectConfig.read(md));
+      ProjectState state = projectStateFactory.create(projectConfigFactory.read(md));
       return new ConfigInfoImpl(
           serverEnableSignedPush,
           state,
@@ -188,10 +191,10 @@
       ProjectConfig projectConfig,
       Map<String, Map<String, ConfigValue>> pluginConfigValues)
       throws BadRequestException {
-    for (Entry<String, Map<String, ConfigValue>> e : pluginConfigValues.entrySet()) {
+    for (Map.Entry<String, Map<String, ConfigValue>> e : pluginConfigValues.entrySet()) {
       String pluginName = e.getKey();
       PluginConfig cfg = projectConfig.getPluginConfig(pluginName);
-      for (Entry<String, ConfigValue> v : e.getValue().entrySet()) {
+      for (Map.Entry<String, ConfigValue> v : e.getValue().entrySet()) {
         ProjectConfigEntry projectConfigEntry = pluginConfigEntries.get(pluginName, v.getKey());
         if (projectConfigEntry != null) {
           if (!PARAMETER_NAME_PATTERN.matcher(v.getKey()).matches()) {
diff --git a/java/com/google/gerrit/server/restapi/project/PutDescription.java b/java/com/google/gerrit/server/restapi/project/PutDescription.java
index f3366e5..c3a063d 100644
--- a/java/com/google/gerrit/server/restapi/project/PutDescription.java
+++ b/java/com/google/gerrit/server/restapi/project/PutDescription.java
@@ -43,15 +43,18 @@
   private final ProjectCache cache;
   private final Provider<MetaDataUpdate.Server> updateFactory;
   private final PermissionBackend permissionBackend;
+  private final ProjectConfig.Factory projectConfigFactory;
 
   @Inject
   PutDescription(
       ProjectCache cache,
       Provider<MetaDataUpdate.Server> updateFactory,
-      PermissionBackend permissionBackend) {
+      PermissionBackend permissionBackend,
+      ProjectConfig.Factory projectConfigFactory) {
     this.cache = cache;
     this.updateFactory = updateFactory;
     this.permissionBackend = permissionBackend;
+    this.projectConfigFactory = projectConfigFactory;
   }
 
   @Override
@@ -69,7 +72,7 @@
         .check(ProjectPermission.WRITE_CONFIG);
 
     try (MetaDataUpdate md = updateFactory.get().create(resource.getNameKey())) {
-      ProjectConfig config = ProjectConfig.read(md);
+      ProjectConfig config = projectConfigFactory.read(md);
       Project project = config.getProject();
       project.setDescription(Strings.emptyToNull(input.description));
 
@@ -86,7 +89,7 @@
       md.getRepository().setGitwebDescription(project.getDescription());
 
       return Strings.isNullOrEmpty(project.getDescription())
-          ? Response.<String>none()
+          ? Response.none()
           : Response.ok(project.getDescription());
     } catch (RepositoryNotFoundException notFound) {
       throw new ResourceNotFoundException(resource.getName(), notFound);
diff --git a/java/com/google/gerrit/server/restapi/project/QueryProjects.java b/java/com/google/gerrit/server/restapi/project/QueryProjects.java
index 44432aa..8727df3 100644
--- a/java/com/google/gerrit/server/restapi/project/QueryProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/QueryProjects.java
@@ -29,7 +29,6 @@
 import com.google.gerrit.server.project.ProjectJson;
 import com.google.gerrit.server.query.project.ProjectQueryBuilder;
 import com.google.gerrit.server.query.project.ProjectQueryProcessor;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.util.ArrayList;
 import java.util.List;
@@ -88,12 +87,11 @@
 
   @Override
   public List<ProjectInfo> apply(TopLevelResource resource)
-      throws BadRequestException, MethodNotAllowedException, OrmException {
+      throws BadRequestException, MethodNotAllowedException {
     return apply();
   }
 
-  public List<ProjectInfo> apply()
-      throws BadRequestException, MethodNotAllowedException, OrmException {
+  public List<ProjectInfo> apply() throws BadRequestException, MethodNotAllowedException {
     if (Strings.isNullOrEmpty(query)) {
       throw new BadRequestException("missing query field");
     }
diff --git a/java/com/google/gerrit/server/restapi/project/RepositoryStatistics.java b/java/com/google/gerrit/server/restapi/project/RepositoryStatistics.java
index 2a2fc866..9f686eb 100644
--- a/java/com/google/gerrit/server/restapi/project/RepositoryStatistics.java
+++ b/java/com/google/gerrit/server/restapi/project/RepositoryStatistics.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.restapi.project;
 
 import com.google.common.base.CaseFormat;
-import java.util.Map.Entry;
+import java.util.Map;
 import java.util.Properties;
 import java.util.TreeMap;
 
@@ -23,7 +23,7 @@
   private static final long serialVersionUID = 1L;
 
   RepositoryStatistics(Properties p) {
-    for (Entry<Object, Object> e : p.entrySet()) {
+    for (Map.Entry<Object, Object> e : p.entrySet()) {
       put(
           CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, e.getKey().toString()),
           e.getValue());
diff --git a/java/com/google/gerrit/server/restapi/project/SetAccess.java b/java/com/google/gerrit/server/restapi/project/SetAccess.java
index 4cefa4b..95bc75f7 100644
--- a/java/com/google/gerrit/server/restapi/project/SetAccess.java
+++ b/java/com/google/gerrit/server/restapi/project/SetAccess.java
@@ -17,7 +17,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.errors.InvalidNameException;
+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;
@@ -38,7 +38,6 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.project.ProjectResource;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -56,6 +55,7 @@
   private final Provider<IdentifiedUser> identifiedUser;
   private final SetAccessUtil accessUtil;
   private final CreateGroupPermissionSyncer createGroupPermissionSyncer;
+  private final ProjectConfig.Factory projectConfigFactory;
 
   @Inject
   private SetAccess(
@@ -66,7 +66,8 @@
       GetAccess getAccess,
       Provider<IdentifiedUser> identifiedUser,
       SetAccessUtil accessUtil,
-      CreateGroupPermissionSyncer createGroupPermissionSyncer) {
+      CreateGroupPermissionSyncer createGroupPermissionSyncer,
+      ProjectConfig.Factory projectConfigFactory) {
     this.groupBackend = groupBackend;
     this.permissionBackend = permissionBackend;
     this.metaDataUpdateFactory = metaDataUpdateFactory;
@@ -75,13 +76,13 @@
     this.identifiedUser = identifiedUser;
     this.accessUtil = accessUtil;
     this.createGroupPermissionSyncer = createGroupPermissionSyncer;
+    this.projectConfigFactory = projectConfigFactory;
   }
 
   @Override
   public ProjectAccessInfo apply(ProjectResource rsrc, ProjectAccessInput input)
       throws ResourceNotFoundException, ResourceConflictException, IOException, AuthException,
-          BadRequestException, UnprocessableEntityException, OrmException,
-          PermissionBackendException {
+          BadRequestException, UnprocessableEntityException, PermissionBackendException {
     MetaDataUpdate.User metaDataUpdateUser = metaDataUpdateFactory.get();
 
     ProjectConfig config;
@@ -89,7 +90,7 @@
     List<AccessSection> removals = accessUtil.getAccessSections(input.remove);
     List<AccessSection> additions = accessUtil.getAccessSections(input.add);
     try (MetaDataUpdate md = metaDataUpdateUser.create(rsrc.getNameKey())) {
-      config = ProjectConfig.read(md);
+      config = projectConfigFactory.read(md);
 
       // Check that the user has the right permissions.
       boolean checkedAdmin = false;
diff --git a/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java b/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
index c8857a2..e206319 100644
--- a/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
+++ b/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
@@ -21,7 +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.common.errors.InvalidNameException;
+import com.google.gerrit.exceptions.InvalidNameException;
 import com.google.gerrit.extensions.api.access.AccessSectionInfo;
 import com.google.gerrit.extensions.api.access.PermissionInfo;
 import com.google.gerrit.extensions.api.access.PermissionRuleInfo;
@@ -146,7 +146,7 @@
       boolean isGlobalCapabilities = AccessSection.GLOBAL_CAPABILITIES.equals(name);
 
       if (!isGlobalCapabilities) {
-        if (!AccessSection.isValid(name)) {
+        if (!AccessSection.isValidRefSectionName(name)) {
           throw new BadRequestException("invalid section name");
         }
         RefPattern.validate(name);
diff --git a/java/com/google/gerrit/server/restapi/project/SetDefaultDashboard.java b/java/com/google/gerrit/server/restapi/project/SetDefaultDashboard.java
index 374f3b9..3917bee 100644
--- a/java/com/google/gerrit/server/restapi/project/SetDefaultDashboard.java
+++ b/java/com/google/gerrit/server/restapi/project/SetDefaultDashboard.java
@@ -47,6 +47,7 @@
   private final DashboardsCollection dashboards;
   private final Provider<GetDashboard> get;
   private final PermissionBackend permissionBackend;
+  private final ProjectConfig.Factory projectConfigFactory;
 
   @Option(name = "--inherited", usage = "set dashboard inherited by children")
   boolean inherited;
@@ -57,12 +58,14 @@
       MetaDataUpdate.Server updateFactory,
       DashboardsCollection dashboards,
       Provider<GetDashboard> get,
-      PermissionBackend permissionBackend) {
+      PermissionBackend permissionBackend,
+      ProjectConfig.Factory projectConfigFactory) {
     this.cache = cache;
     this.updateFactory = updateFactory;
     this.dashboards = dashboards;
     this.get = get;
     this.permissionBackend = permissionBackend;
+    this.projectConfigFactory = projectConfigFactory;
   }
 
   @Override
@@ -93,7 +96,7 @@
     }
 
     try (MetaDataUpdate md = updateFactory.create(rsrc.getProjectState().getNameKey())) {
-      ProjectConfig config = ProjectConfig.read(md);
+      ProjectConfig config = projectConfigFactory.read(md);
       Project project = config.getProject();
       if (inherited) {
         project.setDefaultDashboard(input.id);
diff --git a/java/com/google/gerrit/server/restapi/project/SetParent.java b/java/com/google/gerrit/server/restapi/project/SetParent.java
index 134a74e..2d9ef42 100644
--- a/java/com/google/gerrit/server/restapi/project/SetParent.java
+++ b/java/com/google/gerrit/server/restapi/project/SetParent.java
@@ -62,6 +62,7 @@
   private final Provider<MetaDataUpdate.Server> updateFactory;
   private final AllProjectsName allProjects;
   private final AllUsersName allUsers;
+  private final ProjectConfig.Factory projectConfigFactory;
   private volatile boolean allowProjectOwnersToChangeParent;
 
   @Inject
@@ -71,12 +72,14 @@
       Provider<MetaDataUpdate.Server> updateFactory,
       AllProjectsName allProjects,
       AllUsersName allUsers,
+      ProjectConfig.Factory projectConfigFactory,
       @GerritServerConfig Config config) {
     this.cache = cache;
     this.permissionBackend = permissionBackend;
     this.updateFactory = updateFactory;
     this.allProjects = allProjects;
     this.allUsers = allUsers;
+    this.projectConfigFactory = projectConfigFactory;
     this.allowProjectOwnersToChangeParent =
         config.getBoolean("receive", "allowProjectOwnersToChangeParent", false);
   }
@@ -98,7 +101,7 @@
         MoreObjects.firstNonNull(Strings.emptyToNull(input.parent), allProjects.get());
     validateParentUpdate(rsrc.getProjectState().getNameKey(), user, parentName, checkIfAdmin);
     try (MetaDataUpdate md = updateFactory.get().create(rsrc.getNameKey())) {
-      ProjectConfig config = ProjectConfig.read(md);
+      ProjectConfig config = projectConfigFactory.read(md);
       Project project = config.getProject();
       project.setParentName(parentName);
 
diff --git a/java/com/google/gerrit/server/rules/DefaultSubmitRule.java b/java/com/google/gerrit/server/rules/DefaultSubmitRule.java
index b70858d..2be6c19 100644
--- a/java/com/google/gerrit/server/rules/DefaultSubmitRule.java
+++ b/java/com/google/gerrit/server/rules/DefaultSubmitRule.java
@@ -20,6 +20,7 @@
 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.exceptions.StorageException;
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
@@ -27,7 +28,6 @@
 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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.ArrayList;
@@ -80,7 +80,7 @@
     try {
       labelTypes = cd.getLabelTypes().getLabelTypes();
       approvals = cd.currentApprovals();
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log(
           "Unable to fetch labels and approvals for change %s", cd.getId());
 
diff --git a/java/com/google/gerrit/server/rules/IgnoreSelfApprovalRule.java b/java/com/google/gerrit/server/rules/IgnoreSelfApprovalRule.java
index f7c25a4..592c269c 100644
--- a/java/com/google/gerrit/server/rules/IgnoreSelfApprovalRule.java
+++ b/java/com/google/gerrit/server/rules/IgnoreSelfApprovalRule.java
@@ -23,13 +23,13 @@
 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.exceptions.StorageException;
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.config.FactoryModule;
 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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.ArrayList;
@@ -66,12 +66,12 @@
     try {
       labelTypes = cd.getLabelTypes().getLabelTypes();
       approvals = cd.currentApprovals();
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atWarning().withCause(e).log(E_UNABLE_TO_FETCH_LABELS);
       return singletonRuleError(E_UNABLE_TO_FETCH_LABELS);
     }
 
-    boolean shouldIgnoreSelfApproval = labelTypes.stream().anyMatch(l -> l.ignoreSelfApproval());
+    boolean shouldIgnoreSelfApproval = labelTypes.stream().anyMatch(LabelType::ignoreSelfApproval);
     if (!shouldIgnoreSelfApproval) {
       // Shortcut to avoid further processing if no label should ignore uploader approvals
       return ImmutableList.of();
@@ -80,7 +80,7 @@
     Account.Id uploader;
     try {
       uploader = cd.currentPatchSet().getUploader();
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atWarning().withCause(e).log(E_UNABLE_TO_FETCH_UPLOADER);
       return singletonRuleError(E_UNABLE_TO_FETCH_UPLOADER);
     }
diff --git a/java/com/google/gerrit/server/rules/PrologEnvironment.java b/java/com/google/gerrit/server/rules/PrologEnvironment.java
index 0b702c7..ba78724 100644
--- a/java/com/google/gerrit/server/rules/PrologEnvironment.java
+++ b/java/com/google/gerrit/server/rules/PrologEnvironment.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.account.Emails;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
@@ -171,6 +172,7 @@
     private final ProjectCache projectCache;
     private final PermissionBackend permissionBackend;
     private final GitRepositoryManager repositoryManager;
+    private final PluginConfigFactory pluginConfigFactory;
     private final PatchListCache patchListCache;
     private final PatchSetInfoFactory patchSetInfoFactory;
     private final IdentifiedUser.GenericFactory userFactory;
@@ -185,6 +187,7 @@
         ProjectCache projectCache,
         PermissionBackend permissionBackend,
         GitRepositoryManager repositoryManager,
+        PluginConfigFactory pluginConfigFactory,
         PatchListCache patchListCache,
         PatchSetInfoFactory patchSetInfoFactory,
         IdentifiedUser.GenericFactory userFactory,
@@ -195,6 +198,7 @@
       this.projectCache = projectCache;
       this.permissionBackend = permissionBackend;
       this.repositoryManager = repositoryManager;
+      this.pluginConfigFactory = pluginConfigFactory;
       this.patchListCache = patchListCache;
       this.patchSetInfoFactory = patchSetInfoFactory;
       this.userFactory = userFactory;
@@ -238,6 +242,10 @@
       return repositoryManager;
     }
 
+    public PluginConfigFactory getPluginConfigFactory() {
+      return pluginConfigFactory;
+    }
+
     public PatchListCache getPatchListCache() {
       return patchListCache;
     }
diff --git a/java/com/google/gerrit/server/rules/PrologRuleEvaluator.java b/java/com/google/gerrit/server/rules/PrologRuleEvaluator.java
index e61fc90..9cde54c 100644
--- a/java/com/google/gerrit/server/rules/PrologRuleEvaluator.java
+++ b/java/com/google/gerrit/server/rules/PrologRuleEvaluator.java
@@ -24,6 +24,7 @@
 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.exceptions.StorageException;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
@@ -36,7 +37,6 @@
 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.gwtorm.server.OrmException;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 import com.googlecode.prolog_cafe.exceptions.CompileException;
@@ -149,17 +149,17 @@
     try {
       change = cd.change();
       if (change == null) {
-        throw new OrmException("No change found");
+        throw new StorageException("No change found");
       }
 
       if (projectState == null) {
         throw new NoSuchProjectException(cd.project());
       }
-    } catch (OrmException | NoSuchProjectException e) {
+    } catch (StorageException | NoSuchProjectException e) {
       return ruleError("Error looking up change " + cd.getId(), e);
     }
 
-    if (!opts.allowClosed() && change.getStatus().isClosed()) {
+    if (!opts.allowClosed() && change.isClosed()) {
       SubmitRecord rec = new SubmitRecord();
       rec.status = SubmitRecord.Status.CLOSED;
       return Collections.singletonList(rec);
@@ -487,7 +487,6 @@
     env.set(StoredValues.ACCOUNTS, accounts);
     env.set(StoredValues.ACCOUNT_CACHE, accountCache);
     env.set(StoredValues.EMAILS, emails);
-    env.set(StoredValues.REVIEW_DB, cd.db());
     env.set(StoredValues.CHANGE_DATA, cd);
     env.set(StoredValues.PROJECT_STATE, projectState);
     return env;
diff --git a/java/com/google/gerrit/server/rules/StoredValues.java b/java/com/google/gerrit/server/rules/StoredValues.java
index ed70629..30d329d 100644
--- a/java/com/google/gerrit/server/rules/StoredValues.java
+++ b/java/com/google/gerrit/server/rules/StoredValues.java
@@ -16,18 +16,19 @@
 
 import static com.google.gerrit.server.rules.StoredValue.create;
 
+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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.Accounts;
 import com.google.gerrit.server.account.Emails;
+import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListCache;
@@ -36,7 +37,6 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import com.googlecode.prolog_cafe.exceptions.SystemException;
 import com.googlecode.prolog_cafe.lang.Prolog;
 import java.io.IOException;
@@ -50,7 +50,6 @@
   public static final StoredValue<Accounts> ACCOUNTS = create(Accounts.class);
   public static final StoredValue<AccountCache> ACCOUNT_CACHE = create(AccountCache.class);
   public static final StoredValue<Emails> EMAILS = create(Emails.class);
-  public static final StoredValue<ReviewDb> REVIEW_DB = create(ReviewDb.class);
   public static final StoredValue<ChangeData> CHANGE_DATA = create(ChangeData.class);
   public static final StoredValue<ProjectState> PROJECT_STATE = create(ProjectState.class);
 
@@ -58,7 +57,7 @@
     ChangeData cd = CHANGE_DATA.get(engine);
     try {
       return cd.change();
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       throw new SystemException(
           String.format("Cannot load change %s: %s", cd.getId(), e.getMessage()));
     }
@@ -68,7 +67,7 @@
     ChangeData cd = CHANGE_DATA.get(engine);
     try {
       return cd.currentPatchSet();
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       throw new SystemException(e.getMessage());
     }
   }
@@ -111,6 +110,27 @@
         }
       };
 
+  // Accessing GitRepositoryManager could be slow.
+  // It should be minimized or cached to reduce pause time
+  // when evaluating Prolog submit rules.
+  public static final StoredValue<GitRepositoryManager> REPO_MANAGER =
+      new StoredValue<GitRepositoryManager>() {
+        @Override
+        public GitRepositoryManager createValue(Prolog engine) {
+          PrologEnvironment env = (PrologEnvironment) engine.control;
+          return env.getArgs().getGitRepositoryManager();
+        }
+      };
+
+  public static final StoredValue<PluginConfigFactory> PLUGIN_CONFIG_FACTORY =
+      new StoredValue<PluginConfigFactory>() {
+        @Override
+        public PluginConfigFactory createValue(Prolog engine) {
+          PrologEnvironment env = (PrologEnvironment) engine.control;
+          return env.getArgs().getPluginConfigFactory();
+        }
+      };
+
   public static final StoredValue<Repository> REPOSITORY =
       new StoredValue<Repository>() {
         @Override
diff --git a/java/com/google/gerrit/server/schema/AbstractDisabledAccess.java b/java/com/google/gerrit/server/schema/AbstractDisabledAccess.java
deleted file mode 100644
index 17eb56e..0000000
--- a/java/com/google/gerrit/server/schema/AbstractDisabledAccess.java
+++ /dev/null
@@ -1,141 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import static com.google.common.base.Preconditions.checkState;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.util.concurrent.Futures;
-import com.google.gerrit.reviewdb.server.ReviewDbWrapper;
-import com.google.gwtorm.client.Key;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.AtomicUpdate;
-import com.google.gwtorm.server.ListResultSet;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
-import java.util.Map;
-import java.util.function.Function;
-
-abstract class AbstractDisabledAccess<T, K extends Key<?>> implements Access<T, K> {
-  private static <T> ResultSet<T> empty() {
-    return new ListResultSet<>(ImmutableList.of());
-  }
-
-  @SuppressWarnings("deprecation")
-  private static <T>
-      com.google.common.util.concurrent.CheckedFuture<T, OrmException> emptyFuture() {
-    return Futures.immediateCheckedFuture(null);
-  }
-
-  // Don't even hold a reference to delegate, so it's not possible to use it
-  // accidentally.
-  private final ReviewDbWrapper wrapper;
-  private final String relationName;
-  private final int relationId;
-  private final Function<T, K> primaryKey;
-  private final Function<Iterable<T>, Map<K, T>> toMap;
-
-  AbstractDisabledAccess(ReviewDbWrapper wrapper, Access<T, K> delegate) {
-    this.wrapper = wrapper;
-    this.relationName = delegate.getRelationName();
-    this.relationId = delegate.getRelationID();
-    this.primaryKey = delegate::primaryKey;
-    this.toMap = delegate::toMap;
-  }
-
-  @Override
-  public final int getRelationID() {
-    return relationId;
-  }
-
-  @Override
-  public final String getRelationName() {
-    return relationName;
-  }
-
-  @Override
-  public final K primaryKey(T entity) {
-    return primaryKey.apply(entity);
-  }
-
-  @Override
-  public final Map<K, T> toMap(Iterable<T> iterable) {
-    return toMap.apply(iterable);
-  }
-
-  @Override
-  public final ResultSet<T> iterateAllEntities() {
-    return empty();
-  }
-
-  @SuppressWarnings("deprecation")
-  @Override
-  public final com.google.common.util.concurrent.CheckedFuture<T, OrmException> getAsync(K key) {
-    return emptyFuture();
-  }
-
-  @Override
-  public final ResultSet<T> get(Iterable<K> keys) {
-    return empty();
-  }
-
-  @Override
-  public final void insert(Iterable<T> instances) {
-    // Do nothing.
-  }
-
-  @Override
-  public final void update(Iterable<T> instances) {
-    // Do nothing.
-  }
-
-  @Override
-  public final void upsert(Iterable<T> instances) {
-    // Do nothing.
-  }
-
-  @Override
-  public final void deleteKeys(Iterable<K> keys) {
-    // Do nothing.
-  }
-
-  @Override
-  public final void delete(Iterable<T> instances) {
-    // Do nothing.
-  }
-
-  @Override
-  public final void beginTransaction(K key) {
-    // Keep track of when we've started a transaction so that we can avoid calling commit/rollback
-    // on the underlying ReviewDb. This is just a simple arm's-length approach, and may produce
-    // slightly different results from a native ReviewDb in corner cases like:
-    // * beginning transactions on different tables simultaneously
-    // * doing work between commit and rollback
-    // These kinds of things are already misuses of ReviewDb, and shouldn't be happening in current
-    // code anyway.
-    checkState(!wrapper.inTransaction(), "already in transaction");
-    wrapper.beginTransaction();
-  }
-
-  @Override
-  public final T atomicUpdate(K key, AtomicUpdate<T> update) {
-    return null;
-  }
-
-  @Override
-  public final T get(K id) {
-    return null;
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/AllProjectsCreator.java b/java/com/google/gerrit/server/schema/AllProjectsCreator.java
index 348f88c..9446b7c 100644
--- a/java/com/google/gerrit/server/schema/AllProjectsCreator.java
+++ b/java/com/google/gerrit/server/schema/AllProjectsCreator.java
@@ -22,39 +22,27 @@
 import static com.google.gerrit.server.schema.AclUtil.grant;
 import static com.google.gerrit.server.schema.AclUtil.rule;
 
-import com.google.common.base.MoreObjects;
-import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableList;
-import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.Version;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.LabelValue;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.data.PermissionRule.Action;
-import com.google.gerrit.extensions.client.InheritableBoolean;
-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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.Sequences;
-import com.google.gerrit.server.UsedAt;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.notedb.RepoSequence;
+import com.google.gerrit.server.notedb.Sequences;
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.inject.Inject;
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.BatchRefUpdate;
@@ -72,84 +60,39 @@
   private final GitRepositoryManager repositoryManager;
   private final AllProjectsName allProjectsName;
   private final PersonIdent serverUser;
-  private final NotesMigration notesMigration;
+  private final NoteDbSchemaVersionManager versionManager;
+  private final ProjectConfig.Factory projectConfigFactory;
   private final GroupReference anonymous;
   private final GroupReference registered;
   private final GroupReference owners;
 
-  @Nullable private GroupReference admin;
-  @Nullable private GroupReference batch;
-  private String message;
-  private int firstChangeId = ReviewDb.FIRST_CHANGE_ID;
-  private LabelType codeReviewLabel;
-  private List<LabelType> additionalLabelType;
-
   @Inject
   AllProjectsCreator(
       GitRepositoryManager repositoryManager,
       AllProjectsName allProjectsName,
       @GerritPersonIdent PersonIdent serverUser,
-      NotesMigration notesMigration,
-      SystemGroupBackend systemGroupBackend) {
+      NoteDbSchemaVersionManager versionManager,
+      SystemGroupBackend systemGroupBackend,
+      ProjectConfig.Factory projectConfigFactory) {
     this.repositoryManager = repositoryManager;
     this.allProjectsName = allProjectsName;
     this.serverUser = serverUser;
-    this.notesMigration = notesMigration;
+    this.versionManager = versionManager;
+    this.projectConfigFactory = projectConfigFactory;
 
     this.anonymous = systemGroupBackend.getGroup(ANONYMOUS_USERS);
     this.registered = systemGroupBackend.getGroup(REGISTERED_USERS);
     this.owners = systemGroupBackend.getGroup(PROJECT_OWNERS);
-    this.codeReviewLabel = getDefaultCodeReviewLabel();
-    this.additionalLabelType = new ArrayList<>();
   }
 
-  /** If called, grant default permissions to this admin group */
-  public AllProjectsCreator setAdministrators(GroupReference admin) {
-    this.admin = admin;
-    return this;
-  }
-
-  /** If called, grant stream-events permission and set appropriate priority for this group */
-  public AllProjectsCreator setBatchUsers(GroupReference batch) {
-    this.batch = batch;
-    return this;
-  }
-
-  public AllProjectsCreator setCommitMessage(String message) {
-    this.message = message;
-    return this;
-  }
-
-  @UsedAt(UsedAt.Project.GOOGLE)
-  public AllProjectsCreator setFirstChangeIdForNoteDb(int id) {
-    checkArgument(id > 0, "id must be positive: %s", id);
-    firstChangeId = id;
-    return this;
-  }
-
-  /** If called, the provided "Code-Review" label will be used rather than the default. */
-  @UsedAt(UsedAt.Project.GOOGLE)
-  public AllProjectsCreator setCodeReviewLabel(LabelType labelType) {
-    checkArgument(
-        labelType.getName().equals("Code-Review"), "label should have 'Code-Review' as its name");
-    this.codeReviewLabel = labelType;
-    return this;
-  }
-
-  @UsedAt(UsedAt.Project.GOOGLE)
-  public AllProjectsCreator addAdditionalLabel(LabelType labelType) {
-    additionalLabelType.add(labelType);
-    return this;
-  }
-
-  public void create() throws IOException, ConfigInvalidException {
+  public void create(AllProjectsInput input) throws IOException, ConfigInvalidException {
     try (Repository git = repositoryManager.openRepository(allProjectsName)) {
-      initAllProjects(git);
+      initAllProjects(git, input);
     } catch (RepositoryNotFoundException notFound) {
       // A repository may be missing if this project existed only to store
       // inheritable permissions. For example 'All-Projects'.
       try (Repository git = repositoryManager.createRepository(allProjectsName)) {
-        initAllProjects(git);
+        initAllProjects(git, input);
         RefUpdate u = git.updateRef(Constants.HEAD);
         u.link(RefNames.REFS_CONFIG);
       } catch (RepositoryNotFoundException err) {
@@ -159,103 +102,131 @@
     }
   }
 
-  private void initAllProjects(Repository git) throws IOException, ConfigInvalidException {
+  private void initAllProjects(Repository git, AllProjectsInput input)
+      throws ConfigInvalidException, IOException {
     BatchRefUpdate bru = git.getRefDatabase().newBatchUpdate();
     try (MetaDataUpdate md =
         new MetaDataUpdate(GitReferenceUpdated.DISABLED, allProjectsName, git, bru)) {
       md.getCommitBuilder().setAuthor(serverUser);
       md.getCommitBuilder().setCommitter(serverUser);
       md.setMessage(
-          MoreObjects.firstNonNull(
-              Strings.emptyToNull(message),
-              "Initialized Gerrit Code Review " + Version.getVersion()));
+          input.commitMessage().isPresent()
+              ? input.commitMessage().get()
+              : "Initialized Gerrit Code Review " + Version.getVersion());
 
-      ProjectConfig config = ProjectConfig.read(md);
+      // init basic project configs.
+      ProjectConfig config = projectConfigFactory.read(md);
       Project p = config.getProject();
-      p.setDescription("Access inherited by all other projects.");
-      p.setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, InheritableBoolean.TRUE);
-      p.setBooleanConfig(BooleanProjectConfig.USE_CONTENT_MERGE, InheritableBoolean.TRUE);
-      p.setBooleanConfig(BooleanProjectConfig.USE_CONTRIBUTOR_AGREEMENTS, InheritableBoolean.FALSE);
-      p.setBooleanConfig(BooleanProjectConfig.USE_SIGNED_OFF_BY, InheritableBoolean.FALSE);
-      p.setBooleanConfig(BooleanProjectConfig.ENABLE_SIGNED_PUSH, InheritableBoolean.FALSE);
+      p.setDescription(
+          input.projectDescription().orElse("Access inherited by all other projects."));
 
-      AccessSection cap = config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true);
-      AccessSection all = config.getAccessSection(AccessSection.ALL, true);
-      AccessSection heads = config.getAccessSection(AccessSection.HEADS, true);
-      AccessSection tags = config.getAccessSection("refs/tags/*", true);
-      AccessSection meta = config.getAccessSection(RefNames.REFS_CONFIG, true);
-      AccessSection refsFor = config.getAccessSection("refs/for/*", true);
-      AccessSection magic = config.getAccessSection("refs/for/" + AccessSection.ALL, true);
+      // init boolean project configs.
+      input.booleanProjectConfigs().forEach(p::setBooleanConfig);
 
-      grant(config, cap, GlobalCapability.ADMINISTRATE_SERVER, admin);
-      grant(config, all, Permission.READ, admin, anonymous);
-      grant(config, refsFor, Permission.ADD_PATCH_SET, registered);
+      // init labels.
+      input
+          .codeReviewLabel()
+          .ifPresent(
+              codeReviewLabel ->
+                  config.getLabelSections().put(codeReviewLabel.getName(), codeReviewLabel));
 
-      if (batch != null) {
-        Permission priority = cap.getPermission(GlobalCapability.PRIORITY, true);
-        PermissionRule r = rule(config, batch);
-        r.setAction(Action.BATCH);
-        priority.add(r);
-
-        Permission stream = cap.getPermission(GlobalCapability.STREAM_EVENTS, true);
-        stream.add(rule(config, batch));
+      if (input.initDefaultAcls()) {
+        // init access sections.
+        initDefaultAcls(config, input);
       }
 
-      initLabels(config);
-      grant(config, heads, codeReviewLabel, -1, 1, registered);
-
-      grant(config, heads, codeReviewLabel, -2, 2, admin, owners);
-      grant(config, heads, Permission.CREATE, admin, owners);
-      grant(config, heads, Permission.PUSH, admin, owners);
-      grant(config, heads, Permission.SUBMIT, admin, owners);
-      grant(config, heads, Permission.FORGE_AUTHOR, registered);
-      grant(config, heads, Permission.FORGE_COMMITTER, admin, owners);
-      grant(config, heads, Permission.EDIT_TOPIC_NAME, true, admin, owners);
-
-      grant(config, tags, Permission.CREATE, admin, owners);
-      grant(config, tags, Permission.CREATE_TAG, admin, owners);
-      grant(config, tags, Permission.CREATE_SIGNED_TAG, admin, owners);
-
-      grant(config, magic, Permission.PUSH, registered);
-      grant(config, magic, Permission.PUSH_MERGE, registered);
-
-      meta.getPermission(Permission.READ, true).setExclusiveGroup(true);
-      grant(config, meta, Permission.READ, admin, owners);
-      grant(config, meta, codeReviewLabel, -2, 2, admin, owners);
-      grant(config, meta, Permission.CREATE, admin, owners);
-      grant(config, meta, Permission.PUSH, admin, owners);
-      grant(config, meta, Permission.SUBMIT, admin, owners);
-
+      // commit all the above configs as a commit in "refs/meta/config" branch of the All-Projects.
       config.commitToNewRef(md, RefNames.REFS_CONFIG);
-      initSequences(git, bru);
+
+      // init sequence number.
+      initSequences(git, bru, input.firstChangeIdForNoteDb());
+
+      // init schema
+      versionManager.init();
+
       execute(git, bru);
     }
   }
 
-  @UsedAt(UsedAt.Project.GOOGLE)
-  public static LabelType getDefaultCodeReviewLabel() {
-    LabelType type =
-        new LabelType(
-            "Code-Review",
-            ImmutableList.of(
-                new LabelValue((short) 2, "Looks good to me, approved"),
-                new LabelValue((short) 1, "Looks good to me, but someone else must approve"),
-                new LabelValue((short) 0, "No score"),
-                new LabelValue((short) -1, "I would prefer this is not merged as is"),
-                new LabelValue((short) -2, "This shall not be merged")));
-    type.setCopyMinScore(true);
-    type.setCopyAllScoresOnTrivialRebase(true);
-    return type;
+  private void initDefaultAcls(ProjectConfig config, AllProjectsInput input) {
+    AccessSection capabilities = config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true);
+    AccessSection heads = config.getAccessSection(AccessSection.HEADS, true);
+
+    checkArgument(input.codeReviewLabel().isPresent());
+    LabelType codeReviewLabel = input.codeReviewLabel().get();
+
+    initDefaultAclsForRegisteredUsers(heads, codeReviewLabel, config);
+
+    input
+        .batchUsersGroup()
+        .ifPresent(
+            batchUsersGroup -> initDefaultAclsForBatchUsers(capabilities, config, batchUsersGroup));
+
+    input
+        .administratorsGroup()
+        .ifPresent(
+            adminsGroup ->
+                initDefaultAclsForAdmins(
+                    capabilities, config, heads, codeReviewLabel, adminsGroup));
   }
 
-  private void initLabels(ProjectConfig projectConfig) {
-    projectConfig.getLabelSections().put(codeReviewLabel.getName(), codeReviewLabel);
-    additionalLabelType.forEach(t -> projectConfig.getLabelSections().put(t.getName(), t));
+  private void initDefaultAclsForRegisteredUsers(
+      AccessSection heads, LabelType codeReviewLabel, ProjectConfig config) {
+    AccessSection refsFor = config.getAccessSection("refs/for/*", true);
+    AccessSection magic = config.getAccessSection("refs/for/" + AccessSection.ALL, true);
+
+    grant(config, refsFor, Permission.ADD_PATCH_SET, registered);
+    grant(config, heads, codeReviewLabel, -1, 1, registered);
+    grant(config, heads, Permission.FORGE_AUTHOR, registered);
+    grant(config, magic, Permission.PUSH, registered);
+    grant(config, magic, Permission.PUSH_MERGE, registered);
   }
 
-  private void initSequences(Repository git, BatchRefUpdate bru) throws IOException {
-    if (notesMigration.readChangeSequence()
-        && git.exactRef(REFS_SEQUENCES + Sequences.NAME_CHANGES) == null) {
+  private void initDefaultAclsForBatchUsers(
+      AccessSection capabilities, ProjectConfig config, GroupReference batchUsersGroup) {
+    Permission priority = capabilities.getPermission(GlobalCapability.PRIORITY, true);
+    PermissionRule r = rule(config, batchUsersGroup);
+    r.setAction(Action.BATCH);
+    priority.add(r);
+
+    Permission stream = capabilities.getPermission(GlobalCapability.STREAM_EVENTS, true);
+    stream.add(rule(config, batchUsersGroup));
+  }
+
+  private void initDefaultAclsForAdmins(
+      AccessSection capabilities,
+      ProjectConfig config,
+      AccessSection heads,
+      LabelType codeReviewLabel,
+      GroupReference adminsGroup) {
+    AccessSection all = config.getAccessSection(AccessSection.ALL, true);
+    AccessSection tags = config.getAccessSection("refs/tags/*", true);
+    AccessSection meta = config.getAccessSection(RefNames.REFS_CONFIG, true);
+
+    grant(config, capabilities, GlobalCapability.ADMINISTRATE_SERVER, adminsGroup);
+    grant(config, all, Permission.READ, adminsGroup, anonymous);
+    grant(config, heads, codeReviewLabel, -2, 2, adminsGroup, owners);
+    grant(config, heads, Permission.CREATE, adminsGroup, owners);
+    grant(config, heads, Permission.PUSH, adminsGroup, owners);
+    grant(config, heads, Permission.SUBMIT, adminsGroup, owners);
+    grant(config, heads, Permission.FORGE_COMMITTER, adminsGroup, owners);
+    grant(config, heads, Permission.EDIT_TOPIC_NAME, true, adminsGroup, owners);
+
+    grant(config, tags, Permission.CREATE, adminsGroup, owners);
+    grant(config, tags, Permission.CREATE_TAG, adminsGroup, owners);
+    grant(config, tags, Permission.CREATE_SIGNED_TAG, adminsGroup, owners);
+
+    meta.getPermission(Permission.READ, true).setExclusiveGroup(true);
+    grant(config, meta, Permission.READ, adminsGroup, owners);
+    grant(config, meta, codeReviewLabel, -2, 2, adminsGroup, owners);
+    grant(config, meta, Permission.CREATE, adminsGroup, owners);
+    grant(config, meta, Permission.PUSH, adminsGroup, owners);
+    grant(config, meta, Permission.SUBMIT, adminsGroup, owners);
+  }
+
+  private void initSequences(Repository git, BatchRefUpdate bru, int firstChangeId)
+      throws IOException {
+    if (git.exactRef(REFS_SEQUENCES + Sequences.NAME_CHANGES) == null) {
       // Can't easily reuse the inserter from MetaDataUpdate, but this shouldn't slow down site
       // initialization unduly.
       try (ObjectInserter ins = git.newObjectInserter()) {
diff --git a/java/com/google/gerrit/server/schema/AllProjectsInput.java b/java/com/google/gerrit/server/schema/AllProjectsInput.java
new file mode 100644
index 0000000..7231b18
--- /dev/null
+++ b/java/com/google/gerrit/server/schema/AllProjectsInput.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.server.schema;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.common.UsedAt;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
+import com.google.gerrit.server.notedb.Sequences;
+import java.util.Optional;
+
+@AutoValue
+public abstract class AllProjectsInput {
+
+  /** Default boolean configs set when initializing All-Projects. */
+  public static final ImmutableMap<BooleanProjectConfig, InheritableBoolean>
+      DEFAULT_BOOLEAN_PROJECT_CONFIGS =
+          ImmutableMap.of(
+              BooleanProjectConfig.REQUIRE_CHANGE_ID,
+              InheritableBoolean.TRUE,
+              BooleanProjectConfig.USE_CONTENT_MERGE,
+              InheritableBoolean.TRUE,
+              BooleanProjectConfig.USE_CONTRIBUTOR_AGREEMENTS,
+              InheritableBoolean.FALSE,
+              BooleanProjectConfig.USE_SIGNED_OFF_BY,
+              InheritableBoolean.FALSE,
+              BooleanProjectConfig.ENABLE_SIGNED_PUSH,
+              InheritableBoolean.FALSE);
+
+  @UsedAt(UsedAt.Project.GOOGLE)
+  public static LabelType getDefaultCodeReviewLabel() {
+    LabelType type =
+        new LabelType(
+            "Code-Review",
+            ImmutableList.of(
+                new LabelValue((short) 2, "Looks good to me, approved"),
+                new LabelValue((short) 1, "Looks good to me, but someone else must approve"),
+                new LabelValue((short) 0, "No score"),
+                new LabelValue((short) -1, "I would prefer this is not merged as is"),
+                new LabelValue((short) -2, "This shall not be merged")));
+    type.setCopyMinScore(true);
+    type.setCopyAllScoresOnTrivialRebase(true);
+    return type;
+  }
+
+  /** The administrator group which gets default permissions granted. */
+  public abstract Optional<GroupReference> administratorsGroup();
+
+  /** The group which gets stream-events permission granted and appropriate properties set. */
+  public abstract Optional<GroupReference> batchUsersGroup();
+
+  /** The commit message used when commit the project config change. */
+  public abstract Optional<String> commitMessage();
+
+  /** The first change-id used in this host. */
+  @UsedAt(UsedAt.Project.GOOGLE)
+  public abstract int firstChangeIdForNoteDb();
+
+  /** The "Code-Review" label to be defined in All-Projects. */
+  @UsedAt(UsedAt.Project.GOOGLE)
+  public abstract Optional<LabelType> codeReviewLabel();
+
+  /** Description for the All-Projects. */
+  public abstract Optional<String> projectDescription();
+
+  /** Boolean project configs to be set in All-Projects. */
+  public abstract ImmutableMap<BooleanProjectConfig, InheritableBoolean> booleanProjectConfigs();
+
+  /** Whether initializing default access sections in All-Projects. */
+  public abstract boolean initDefaultAcls();
+
+  public abstract Builder toBuilder();
+
+  public static Builder builder() {
+    Builder builder =
+        new AutoValue_AllProjectsInput.Builder()
+            .codeReviewLabel(getDefaultCodeReviewLabel())
+            .firstChangeIdForNoteDb(Sequences.FIRST_CHANGE_ID)
+            .initDefaultAcls(true);
+    DEFAULT_BOOLEAN_PROJECT_CONFIGS.forEach(builder::addBooleanProjectConfig);
+
+    return builder;
+  }
+
+  public static Builder builderWithNoDefault() {
+    return new AutoValue_AllProjectsInput.Builder();
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+    public abstract Builder administratorsGroup(GroupReference adminGroup);
+
+    public abstract Builder batchUsersGroup(GroupReference batchGroup);
+
+    public abstract Builder commitMessage(String commitMessage);
+
+    public abstract Builder firstChangeIdForNoteDb(int firstChangeId);
+
+    @UsedAt(UsedAt.Project.GOOGLE)
+    public abstract Builder codeReviewLabel(LabelType codeReviewLabel);
+
+    @UsedAt(UsedAt.Project.GOOGLE)
+    public abstract Builder projectDescription(String projectDescription);
+
+    public abstract ImmutableMap.Builder<BooleanProjectConfig, InheritableBoolean>
+        booleanProjectConfigsBuilder();
+
+    public Builder addBooleanProjectConfig(
+        BooleanProjectConfig booleanProjectConfig, InheritableBoolean inheritableBoolean) {
+      booleanProjectConfigsBuilder().put(booleanProjectConfig, inheritableBoolean);
+      return this;
+    }
+
+    @UsedAt(UsedAt.Project.GOOGLE)
+    public abstract Builder initDefaultAcls(boolean initDefaultACLs);
+
+    public abstract AllProjectsInput build();
+  }
+}
diff --git a/java/com/google/gerrit/server/schema/AllUsersCreator.java b/java/com/google/gerrit/server/schema/AllUsersCreator.java
index 3779d0d..d2f5ef1 100644
--- a/java/com/google/gerrit/server/schema/AllUsersCreator.java
+++ b/java/com/google/gerrit/server/schema/AllUsersCreator.java
@@ -17,9 +17,10 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.server.schema.AclUtil.grant;
-import static com.google.gerrit.server.schema.AllProjectsCreator.getDefaultCodeReviewLabel;
+import static com.google.gerrit.server.schema.AllProjectsInput.getDefaultCodeReviewLabel;
 
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.common.Version;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GroupReference;
@@ -28,13 +29,13 @@
 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.UsedAt;
 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.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.project.ProjectConfig;
+import com.google.gerrit.server.project.ProjectConfig.Factory;
 import com.google.gerrit.server.project.RefPattern;
 import com.google.inject.Inject;
 import java.io.IOException;
@@ -50,6 +51,7 @@
   private final GitRepositoryManager mgr;
   private final AllUsersName allUsersName;
   private final PersonIdent serverUser;
+  private final ProjectConfig.Factory projectConfigFactory;
   private final GroupReference registered;
 
   @Nullable private GroupReference admin;
@@ -60,11 +62,13 @@
       GitRepositoryManager mgr,
       AllUsersName allUsersName,
       SystemGroupBackend systemGroupBackend,
-      @GerritPersonIdent PersonIdent serverUser) {
+      @GerritPersonIdent PersonIdent serverUser,
+      Factory projectConfigFactory) {
     this.mgr = mgr;
     this.allUsersName = allUsersName;
     this.serverUser = serverUser;
     this.registered = systemGroupBackend.getGroup(REGISTERED_USERS);
+    this.projectConfigFactory = projectConfigFactory;
     this.codeReviewLabel = getDefaultCodeReviewLabel();
   }
 
@@ -107,7 +111,7 @@
       md.getCommitBuilder().setCommitter(serverUser);
       md.setMessage("Initialized Gerrit Code Review " + Version.getVersion());
 
-      ProjectConfig config = ProjectConfig.read(md);
+      ProjectConfig config = projectConfigFactory.read(md);
       Project project = config.getProject();
       project.setDescription("Individual user settings and preferences.");
 
diff --git a/java/com/google/gerrit/server/schema/BUILD b/java/com/google/gerrit/server/schema/BUILD
index 50b92d3..ee99c67 100644
--- a/java/com/google/gerrit/server/schema/BUILD
+++ b/java/com/google/gerrit/server/schema/BUILD
@@ -9,15 +9,16 @@
     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/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/org/eclipse/jgit:server",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
         "//lib/commons:dbcp",
diff --git a/java/com/google/gerrit/server/schema/BaseDataSourceType.java b/java/com/google/gerrit/server/schema/BaseDataSourceType.java
deleted file mode 100644
index 4b3a570..0000000
--- a/java/com/google/gerrit/server/schema/BaseDataSourceType.java
+++ /dev/null
@@ -1,63 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import java.io.IOException;
-import java.io.InputStream;
-
-public abstract class BaseDataSourceType implements DataSourceType {
-
-  private static final String DEFAULT_VALIDATION_QUERY = "select 1";
-  private final String driver;
-
-  protected BaseDataSourceType(String driver) {
-    this.driver = driver;
-  }
-
-  @Override
-  public final String getDriver() {
-    return driver;
-  }
-
-  @Override
-  public boolean usePool() {
-    return true;
-  }
-
-  @Override
-  public String getValidationQuery() {
-    return DEFAULT_VALIDATION_QUERY;
-  }
-
-  @Override
-  public ScriptRunner getIndexScript() throws IOException {
-    return getScriptRunner("index_generic.sql");
-  }
-
-  protected static final ScriptRunner getScriptRunner(String path) throws IOException {
-    if (path == null) {
-      return ScriptRunner.NOOP;
-    }
-    ScriptRunner runner;
-    try (InputStream in = ReviewDb.class.getResourceAsStream(path)) {
-      if (in == null) {
-        throw new IllegalStateException("SQL script " + path + " not found");
-      }
-      runner = new ScriptRunner(path, in);
-    }
-    return runner;
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/DB2.java b/java/com/google/gerrit/server/schema/DB2.java
deleted file mode 100644
index fcf8c1f..0000000
--- a/java/com/google/gerrit/server/schema/DB2.java
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import static com.google.gerrit.server.schema.JdbcUtil.hostname;
-import static com.google.gerrit.server.schema.JdbcUtil.port;
-
-import com.google.gerrit.server.config.ConfigSection;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.inject.Inject;
-import org.eclipse.jgit.lib.Config;
-
-public class DB2 extends BaseDataSourceType {
-  private Config cfg;
-
-  @Inject
-  public DB2(@GerritServerConfig Config cfg) {
-    super("com.ibm.db2.jcc.DB2Driver");
-    this.cfg = cfg;
-  }
-
-  @Override
-  public String getUrl() {
-    final StringBuilder b = new StringBuilder();
-    final ConfigSection dbc = new ConfigSection(cfg, "database");
-    b.append("jdbc:db2://");
-    b.append(hostname(dbc.optional("hostname")));
-    b.append(port(dbc.optional("port")));
-    b.append("/");
-    b.append(dbc.required("database"));
-    return b.toString();
-  }
-
-  @Override
-  public String getValidationQuery() {
-    return "SELECT 1 FROM SYSIBM.SYSDUMMY1";
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/DataSourceModule.java b/java/com/google/gerrit/server/schema/DataSourceModule.java
deleted file mode 100644
index ee57c8b..0000000
--- a/java/com/google/gerrit/server/schema/DataSourceModule.java
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.AbstractModule;
-import com.google.inject.name.Names;
-
-public class DataSourceModule extends AbstractModule {
-
-  @Override
-  protected void configure() {
-    bind(DataSourceType.class).annotatedWith(Names.named("db2")).to(DB2.class);
-    bind(DataSourceType.class).annotatedWith(Names.named("derby")).to(Derby.class);
-    bind(DataSourceType.class).annotatedWith(Names.named("h2")).to(H2.class);
-    bind(DataSourceType.class).annotatedWith(Names.named("jdbc")).to(JDBC.class);
-    bind(DataSourceType.class).annotatedWith(Names.named("mariadb")).to(MariaDb.class);
-    bind(DataSourceType.class).annotatedWith(Names.named("mysql")).to(MySql.class);
-    bind(DataSourceType.class).annotatedWith(Names.named("oracle")).to(Oracle.class);
-    bind(DataSourceType.class).annotatedWith(Names.named("postgresql")).to(PostgreSQL.class);
-    /*
-     * DatabaseMetaData.getDatabaseProductName() returns "sap db" for MaxDB.
-     * For auto-detection of the DB type (com.google.gerrit.pgm.util.SiteProgram#getDbType)
-     * we have to map "sap db" additionally to "maxdb", which is used for explicit configuration.
-     */
-    bind(DataSourceType.class).annotatedWith(Names.named("maxdb")).to(MaxDb.class);
-    bind(DataSourceType.class).annotatedWith(Names.named("sap db")).to(MaxDb.class);
-    bind(DataSourceType.class).annotatedWith(Names.named("hana")).to(HANA.class);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/DataSourceProvider.java b/java/com/google/gerrit/server/schema/DataSourceProvider.java
deleted file mode 100644
index d4cfaa6..0000000
--- a/java/com/google/gerrit/server/schema/DataSourceProvider.java
+++ /dev/null
@@ -1,206 +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.server.schema;
-
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
-import static java.util.concurrent.TimeUnit.SECONDS;
-
-import com.google.common.base.Strings;
-import com.google.gerrit.extensions.events.LifecycleListener;
-import com.google.gerrit.extensions.persistence.DataSourceInterceptor;
-import com.google.gerrit.metrics.CallbackMetric1;
-import com.google.gerrit.metrics.Description;
-import com.google.gerrit.metrics.Field;
-import com.google.gerrit.metrics.MetricMaker;
-import com.google.gerrit.server.config.ConfigSection;
-import com.google.gerrit.server.config.ConfigUtil;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.config.ThreadSettingsConfig;
-import com.google.gwtorm.jdbc.SimpleDataSource;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
-import com.google.inject.Singleton;
-import java.lang.reflect.Constructor;
-import java.lang.reflect.InvocationTargetException;
-import java.sql.SQLException;
-import java.util.Properties;
-import javax.sql.DataSource;
-import org.apache.commons.dbcp.BasicDataSource;
-import org.eclipse.jgit.lib.Config;
-
-/** Provides access to the DataSource. */
-@Singleton
-public class DataSourceProvider implements Provider<DataSource>, LifecycleListener {
-  private static final String DATABASE_KEY = "database";
-
-  private final Config cfg;
-  private final MetricMaker metrics;
-  private final Context ctx;
-  private final DataSourceType dst;
-  private final ThreadSettingsConfig threadSettingsConfig;
-  private DataSource ds;
-
-  @Inject
-  protected DataSourceProvider(
-      @GerritServerConfig Config cfg,
-      MetricMaker metrics,
-      ThreadSettingsConfig threadSettingsConfig,
-      Context ctx,
-      DataSourceType dst) {
-    this.cfg = cfg;
-    this.metrics = metrics;
-    this.threadSettingsConfig = threadSettingsConfig;
-    this.ctx = ctx;
-    this.dst = dst;
-  }
-
-  @Override
-  public synchronized DataSource get() {
-    if (ds == null) {
-      ds = open(cfg, ctx, dst);
-    }
-    return ds;
-  }
-
-  @Override
-  public void start() {}
-
-  @Override
-  public synchronized void stop() {
-    if (ds instanceof BasicDataSource) {
-      try {
-        ((BasicDataSource) ds).close();
-      } catch (SQLException e) {
-        // Ignore the close failure.
-      }
-    }
-  }
-
-  public enum Context {
-    SINGLE_USER,
-    MULTI_USER
-  }
-
-  private DataSource open(Config cfg, Context context, DataSourceType dst) {
-    ConfigSection dbs = new ConfigSection(cfg, DATABASE_KEY);
-    String driver = dbs.optional("driver");
-    if (Strings.isNullOrEmpty(driver)) {
-      driver = dst.getDriver();
-    }
-
-    String url = dbs.optional("url");
-    if (Strings.isNullOrEmpty(url)) {
-      url = dst.getUrl();
-    }
-
-    String username = dbs.optional("username");
-    String password = dbs.optional("password");
-    String interceptor = dbs.optional("dataSourceInterceptorClass");
-
-    boolean usePool;
-    if (context == Context.SINGLE_USER) {
-      usePool = false;
-    } else {
-      usePool = cfg.getBoolean(DATABASE_KEY, "connectionpool", dst.usePool());
-    }
-
-    if (usePool) {
-      final BasicDataSource lds = new BasicDataSource();
-      lds.setDriverClassName(driver);
-      lds.setUrl(url);
-      if (username != null && !username.isEmpty()) {
-        lds.setUsername(username);
-      }
-      if (password != null && !password.isEmpty()) {
-        lds.setPassword(password);
-      }
-      int poolLimit = threadSettingsConfig.getDatabasePoolLimit();
-      lds.setMaxActive(poolLimit);
-      lds.setMinIdle(cfg.getInt(DATABASE_KEY, "poolminidle", 4));
-      lds.setMaxIdle(cfg.getInt(DATABASE_KEY, "poolmaxidle", Math.min(poolLimit, 16)));
-      lds.setMaxWait(
-          ConfigUtil.getTimeUnit(
-              cfg,
-              DATABASE_KEY,
-              null,
-              "poolmaxwait",
-              MILLISECONDS.convert(30, SECONDS),
-              MILLISECONDS));
-      lds.setInitialSize(lds.getMinIdle());
-      long evictIdleTimeMs = 1000L * 60;
-      lds.setMinEvictableIdleTimeMillis(evictIdleTimeMs);
-      lds.setTimeBetweenEvictionRunsMillis(evictIdleTimeMs / 2);
-      lds.setTestOnBorrow(true);
-      lds.setTestOnReturn(true);
-      lds.setValidationQuery(dst.getValidationQuery());
-      lds.setValidationQueryTimeout(5);
-      exportPoolMetrics(lds);
-      return intercept(interceptor, lds);
-    }
-    // Don't use the connection pool.
-    //
-    try {
-      final Properties p = new Properties();
-      p.setProperty("driver", driver);
-      p.setProperty("url", url);
-      if (username != null) {
-        p.setProperty("user", username);
-      }
-      if (password != null) {
-        p.setProperty("password", password);
-      }
-      return intercept(interceptor, new SimpleDataSource(p));
-    } catch (SQLException se) {
-      throw new ProvisionException("Database unavailable", se);
-    }
-  }
-
-  private void exportPoolMetrics(BasicDataSource pool) {
-    CallbackMetric1<Boolean, Integer> cnt =
-        metrics.newCallbackMetric(
-            "sql/connection_pool/connections",
-            Integer.class,
-            new Description("SQL database connections").setGauge().setUnit("connections"),
-            Field.ofBoolean("active"));
-    metrics.newTrigger(
-        cnt,
-        () -> {
-          synchronized (pool) {
-            cnt.set(true, pool.getNumActive());
-            cnt.set(false, pool.getNumIdle());
-          }
-        });
-  }
-
-  private DataSource intercept(String interceptor, DataSource ds) {
-    if (interceptor == null) {
-      return ds;
-    }
-    try {
-      Constructor<?> c = Class.forName(interceptor).getConstructor();
-      DataSourceInterceptor datasourceInterceptor = (DataSourceInterceptor) c.newInstance();
-      return datasourceInterceptor.intercept("reviewDb", ds);
-    } catch (ClassNotFoundException
-        | SecurityException
-        | NoSuchMethodException
-        | IllegalArgumentException
-        | InstantiationException
-        | IllegalAccessException
-        | InvocationTargetException e) {
-      throw new ProvisionException("Cannot intercept datasource", e);
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/DataSourceType.java b/java/com/google/gerrit/server/schema/DataSourceType.java
deleted file mode 100644
index cbdcf0f..0000000
--- a/java/com/google/gerrit/server/schema/DataSourceType.java
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import java.io.IOException;
-
-/** Abstraction of a supported database platform */
-public interface DataSourceType {
-
-  String getDriver();
-
-  String getUrl();
-
-  String getValidationQuery();
-
-  boolean usePool();
-
-  /**
-   * Return a ScriptRunner that runs the index script. Must not return {@code null}, but may return
-   * a ScriptRunner that does nothing.
-   *
-   * @throws IOException
-   */
-  ScriptRunner getIndexScript() throws IOException;
-}
diff --git a/java/com/google/gerrit/server/schema/DatabaseModule.java b/java/com/google/gerrit/server/schema/DatabaseModule.java
deleted file mode 100644
index 38a7751..0000000
--- a/java/com/google/gerrit/server/schema/DatabaseModule.java
+++ /dev/null
@@ -1,41 +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.server.schema;
-
-import static com.google.inject.Scopes.SINGLETON;
-
-import com.google.gerrit.extensions.config.FactoryModule;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.notedb.ChangeBundleReader;
-import com.google.gerrit.server.notedb.GwtormChangeBundleReader;
-import com.google.gwtorm.jdbc.Database;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Key;
-import com.google.inject.TypeLiteral;
-
-/** Loads the database with standard dependencies. */
-public class DatabaseModule extends FactoryModule {
-  @Override
-  protected void configure() {
-    TypeLiteral<SchemaFactory<ReviewDb>> schemaFactory =
-        new TypeLiteral<SchemaFactory<ReviewDb>>() {};
-    TypeLiteral<Database<ReviewDb>> database = new TypeLiteral<Database<ReviewDb>>() {};
-
-    bind(schemaFactory).to(NotesMigrationSchemaFactory.class);
-    bind(Key.get(schemaFactory, ReviewDbFactory.class)).to(database).in(SINGLETON);
-    bind(database).toProvider(ReviewDbDatabaseProvider.class);
-    bind(ChangeBundleReader.class).to(GwtormChangeBundleReader.class);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Derby.java b/java/com/google/gerrit/server/schema/Derby.java
deleted file mode 100644
index 9fb761d..0000000
--- a/java/com/google/gerrit/server/schema/Derby.java
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.inject.Inject;
-import org.eclipse.jgit.lib.Config;
-
-class Derby extends BaseDataSourceType {
-
-  protected final Config cfg;
-  private final SitePaths site;
-
-  @Inject
-  Derby(@GerritServerConfig Config cfg, SitePaths site) {
-    super("org.apache.derby.jdbc.EmbeddedDriver");
-    this.cfg = cfg;
-    this.site = site;
-  }
-
-  @Override
-  public String getUrl() {
-    String database = cfg.getString("database", null, "database");
-    if (database == null || database.isEmpty()) {
-      database = "db/ReviewDB";
-    }
-    return "jdbc:derby:" + site.resolve(database).toString() + ";create=true";
-  }
-
-  @Override
-  public String getValidationQuery() {
-    return "values 1";
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/GroupBundle.java b/java/com/google/gerrit/server/schema/GroupBundle.java
deleted file mode 100644
index 4b8bdcc..0000000
--- a/java/com/google/gerrit/server/schema/GroupBundle.java
+++ /dev/null
@@ -1,768 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.collect.ImmutableList.toImmutableList;
-import static com.google.common.collect.ImmutableSet.toImmutableSet;
-import static com.google.gerrit.reviewdb.server.ReviewDbUtil.checkColumns;
-import static java.util.Comparator.naturalOrder;
-import static java.util.Comparator.nullsLast;
-import static java.util.stream.Collectors.toList;
-
-import com.google.auto.value.AutoValue;
-import com.google.common.base.Strings;
-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.common.collect.Multimaps;
-import com.google.common.collect.Streams;
-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.AccountGroupById;
-import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
-import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.reviewdb.server.ReviewDbWrapper;
-import com.google.gerrit.server.group.InternalGroup;
-import com.google.gerrit.server.group.db.AuditLogReader;
-import com.google.gerrit.server.group.db.GroupConfig;
-import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import java.io.IOException;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.sql.Timestamp;
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.function.Function;
-import java.util.stream.Stream;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.Repository;
-
-/**
- * A bundle of all entities rooted at a single {@link AccountGroup} entity.
- *
- * <p>Used primarily during the migration process. Most callers should prefer {@link InternalGroup}
- * instead.
- */
-@AutoValue
-abstract class GroupBundle {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
-  static {
-    // Initialization-time checks that the column set hasn't changed since the
-    // last time this file was updated.
-    checkColumns(AccountGroup.NameKey.class, 1);
-    checkColumns(AccountGroup.UUID.class, 1);
-    checkColumns(AccountGroup.Id.class, 1);
-    checkColumns(AccountGroup.class, 1, 2, 4, 7, 9, 10, 11);
-
-    checkColumns(AccountGroupById.Key.class, 1, 2);
-    checkColumns(AccountGroupById.class, 1);
-
-    checkColumns(AccountGroupByIdAud.Key.class, 1, 2, 3);
-    checkColumns(AccountGroupByIdAud.class, 1, 2, 3, 4);
-
-    checkColumns(AccountGroupMember.Key.class, 1, 2);
-    checkColumns(AccountGroupMember.class, 1);
-
-    checkColumns(AccountGroupMemberAudit.Key.class, 1, 2, 3);
-    checkColumns(AccountGroupMemberAudit.class, 1, 2, 3, 4);
-  }
-
-  public enum Source {
-    REVIEW_DB("ReviewDb"),
-    NOTE_DB("NoteDb");
-
-    private final String name;
-
-    private Source(String name) {
-      this.name = name;
-    }
-
-    @Override
-    public String toString() {
-      return name;
-    }
-  }
-
-  @Singleton
-  public static class Factory {
-    private final AuditLogReader auditLogReader;
-
-    @Inject
-    Factory(AuditLogReader auditLogReader) {
-      this.auditLogReader = auditLogReader;
-    }
-
-    public GroupBundle fromNoteDb(
-        Project.NameKey projectName, Repository repo, AccountGroup.UUID uuid)
-        throws ConfigInvalidException, IOException {
-      GroupConfig groupConfig = GroupConfig.loadForGroup(projectName, repo, uuid);
-      InternalGroup internalGroup = groupConfig.getLoadedGroup().get();
-      AccountGroup.Id groupId = internalGroup.getId();
-
-      AccountGroup accountGroup =
-          new AccountGroup(
-              internalGroup.getNameKey(),
-              internalGroup.getId(),
-              internalGroup.getGroupUUID(),
-              internalGroup.getCreatedOn());
-      accountGroup.setDescription(internalGroup.getDescription());
-      accountGroup.setOwnerGroupUUID(internalGroup.getOwnerGroupUUID());
-      accountGroup.setVisibleToAll(internalGroup.isVisibleToAll());
-
-      return create(
-          Source.NOTE_DB,
-          accountGroup,
-          internalGroup.getMembers().stream()
-              .map(
-                  accountId ->
-                      new AccountGroupMember(new AccountGroupMember.Key(accountId, groupId)))
-              .collect(toImmutableSet()),
-          auditLogReader.getMembersAudit(repo, uuid),
-          internalGroup.getSubgroups().stream()
-              .map(
-                  subgroupUuid ->
-                      new AccountGroupById(new AccountGroupById.Key(groupId, subgroupUuid)))
-              .collect(toImmutableSet()),
-          auditLogReader.getSubgroupsAudit(repo, uuid));
-    }
-
-    public static GroupBundle fromReviewDb(ReviewDb db, AccountGroup.UUID groupUuid)
-        throws OrmException {
-      JdbcSchema jdbcSchema = ReviewDbWrapper.unwrapJbdcSchema(db);
-      AccountGroup group = readAccountGroupFromReviewDb(jdbcSchema, groupUuid);
-      AccountGroup.Id groupId = group.getId();
-
-      return create(
-          Source.REVIEW_DB,
-          group,
-          readAccountGroupMembersFromReviewDb(jdbcSchema, groupId),
-          readAccountGroupMemberAuditsFromReviewDb(jdbcSchema, groupId),
-          readAccountGroupSubgroupsFromReviewDb(jdbcSchema, groupId),
-          readAccountGroupSubgroupAuditsFromReviewDb(jdbcSchema, groupId));
-    }
-
-    private static AccountGroup readAccountGroupFromReviewDb(
-        JdbcSchema jdbcSchema, AccountGroup.UUID groupUuid) throws OrmException {
-      try (Statement stmt = jdbcSchema.getConnection().createStatement();
-          ResultSet rs =
-              stmt.executeQuery(
-                  "SELECT group_id,"
-                      + " name,"
-                      + " created_on,"
-                      + " description,"
-                      + " owner_group_uuid,"
-                      + " visible_to_all"
-                      + " FROM account_groups"
-                      + " WHERE group_uuid = '"
-                      + groupUuid.get()
-                      + "'")) {
-        if (!rs.next()) {
-          throw new OrmException(String.format("Group %s not found", groupUuid));
-        }
-
-        AccountGroup.Id groupId = new AccountGroup.Id(rs.getInt(1));
-        AccountGroup.NameKey groupName = new AccountGroup.NameKey(rs.getString(2));
-        Timestamp createdOn = rs.getTimestamp(3);
-        String description = rs.getString(4);
-        AccountGroup.UUID ownerGroupUuid = new AccountGroup.UUID(rs.getString(5));
-        boolean visibleToAll = "Y".equals(rs.getString(6));
-
-        AccountGroup group = new AccountGroup(groupName, groupId, groupUuid, createdOn);
-        group.setDescription(description);
-        group.setOwnerGroupUUID(ownerGroupUuid);
-        group.setVisibleToAll(visibleToAll);
-
-        if (rs.next()) {
-          throw new OrmException(String.format("Group UUID %s is ambiguous", groupUuid));
-        }
-
-        return group;
-      } catch (SQLException e) {
-        throw new OrmException(
-            String.format("Failed to read account group %s from ReviewDb", groupUuid.get()), e);
-      }
-    }
-
-    private static List<AccountGroupMember> readAccountGroupMembersFromReviewDb(
-        JdbcSchema jdbcSchema, AccountGroup.Id groupId) throws OrmException {
-      try (Statement stmt = jdbcSchema.getConnection().createStatement();
-          ResultSet rs =
-              stmt.executeQuery(
-                  "SELECT account_id"
-                      + " FROM account_group_members"
-                      + " WHERE group_id = '"
-                      + groupId.get()
-                      + "'")) {
-        List<AccountGroupMember> members = new ArrayList<>();
-        while (rs.next()) {
-          Account.Id accountId = new Account.Id(rs.getInt(1));
-          members.add(new AccountGroupMember(new AccountGroupMember.Key(accountId, groupId)));
-        }
-        return members;
-      } catch (SQLException e) {
-        throw new OrmException(
-            String.format(
-                "Failed to read members of account group %s from ReviewDb", groupId.get()),
-            e);
-      }
-    }
-
-    private static List<AccountGroupMemberAudit> readAccountGroupMemberAuditsFromReviewDb(
-        JdbcSchema jdbcSchema, AccountGroup.Id groupId) throws OrmException {
-      try (Statement stmt = jdbcSchema.getConnection().createStatement();
-          ResultSet rs =
-              stmt.executeQuery(
-                  "SELECT account_id, added_by, added_on, removed_by, removed_on"
-                      + " FROM account_group_members_audit"
-                      + " WHERE group_id = '"
-                      + groupId.get()
-                      + "'")) {
-        List<AccountGroupMemberAudit> audits = new ArrayList<>();
-        while (rs.next()) {
-          Account.Id accountId = new Account.Id(rs.getInt(1));
-
-          Account.Id addedBy = new Account.Id(rs.getInt(2));
-          Timestamp addedOn = rs.getTimestamp(3);
-
-          Timestamp removedOn = rs.getTimestamp(5);
-          Account.Id removedBy = removedOn != null ? new Account.Id(rs.getInt(4)) : null;
-
-          AccountGroupMemberAudit.Key key =
-              new AccountGroupMemberAudit.Key(accountId, groupId, addedOn);
-          AccountGroupMemberAudit audit = new AccountGroupMemberAudit(key, addedBy);
-          audit.removed(removedBy, removedOn);
-          audits.add(audit);
-        }
-        return audits;
-      } catch (SQLException e) {
-        throw new OrmException(
-            String.format(
-                "Failed to read member audits of account group %s from ReviewDb", groupId.get()),
-            e);
-      }
-    }
-
-    private static List<AccountGroupById> readAccountGroupSubgroupsFromReviewDb(
-        JdbcSchema jdbcSchema, AccountGroup.Id groupId) throws OrmException {
-      try (Statement stmt = jdbcSchema.getConnection().createStatement();
-          ResultSet rs =
-              stmt.executeQuery(
-                  "SELECT include_uuid"
-                      + " FROM account_group_by_id"
-                      + " WHERE group_id = '"
-                      + groupId.get()
-                      + "'")) {
-        List<AccountGroupById> subgroups = new ArrayList<>();
-        while (rs.next()) {
-          AccountGroup.UUID includedGroupUuid = new AccountGroup.UUID(rs.getString(1));
-          subgroups.add(new AccountGroupById(new AccountGroupById.Key(groupId, includedGroupUuid)));
-        }
-        return subgroups;
-      } catch (SQLException e) {
-        throw new OrmException(
-            String.format(
-                "Failed to read subgroups of account group %s from ReviewDb", groupId.get()),
-            e);
-      }
-    }
-
-    private static List<AccountGroupByIdAud> readAccountGroupSubgroupAuditsFromReviewDb(
-        JdbcSchema jdbcSchema, AccountGroup.Id groupId) throws OrmException {
-      try (Statement stmt = jdbcSchema.getConnection().createStatement();
-          ResultSet rs =
-              stmt.executeQuery(
-                  "SELECT include_uuid, added_by, added_on, removed_by, removed_on"
-                      + " FROM account_group_by_id_aud"
-                      + " WHERE group_id = '"
-                      + groupId.get()
-                      + "'")) {
-        List<AccountGroupByIdAud> audits = new ArrayList<>();
-        while (rs.next()) {
-          AccountGroup.UUID includedGroupUuid = new AccountGroup.UUID(rs.getString(1));
-
-          Account.Id addedBy = new Account.Id(rs.getInt(2));
-          Timestamp addedOn = rs.getTimestamp(3);
-
-          Timestamp removedOn = rs.getTimestamp(5);
-          Account.Id removedBy = removedOn != null ? new Account.Id(rs.getInt(4)) : null;
-
-          AccountGroupByIdAud.Key key =
-              new AccountGroupByIdAud.Key(groupId, includedGroupUuid, addedOn);
-          AccountGroupByIdAud audit = new AccountGroupByIdAud(key, addedBy);
-          audit.removed(removedBy, removedOn);
-          audits.add(audit);
-        }
-        return audits;
-      } catch (SQLException e) {
-        throw new OrmException(
-            String.format(
-                "Failed to read subgroup audits of account group %s from ReviewDb", groupId.get()),
-            e);
-      }
-    }
-  }
-
-  private static final Comparator<AccountGroupMember> ACCOUNT_GROUP_MEMBER_COMPARATOR =
-      Comparator.comparingInt((AccountGroupMember m) -> m.getAccountGroupId().get())
-          .thenComparingInt(m -> m.getAccountId().get());
-
-  private static final Comparator<AccountGroupMemberAudit> ACCOUNT_GROUP_MEMBER_AUDIT_COMPARATOR =
-      Comparator.comparingInt((AccountGroupMemberAudit a) -> a.getGroupId().get())
-          .thenComparing(AccountGroupMemberAudit::getAddedOn)
-          .thenComparingInt(a -> a.getAddedBy().get())
-          .thenComparingInt(a -> a.getMemberId().get())
-          .thenComparing(
-              a -> a.getRemovedBy() != null ? a.getRemovedBy().get() : null,
-              nullsLast(naturalOrder()))
-          .thenComparing(AccountGroupMemberAudit::getRemovedOn, nullsLast(naturalOrder()));
-
-  private static final Comparator<AccountGroupById> ACCOUNT_GROUP_BY_ID_COMPARATOR =
-      Comparator.comparingInt((AccountGroupById m) -> m.getGroupId().get())
-          .thenComparing(AccountGroupById::getIncludeUUID);
-
-  private static final Comparator<AccountGroupByIdAud> ACCOUNT_GROUP_BY_ID_AUD_COMPARATOR =
-      Comparator.comparingInt((AccountGroupByIdAud a) -> a.getGroupId().get())
-          .thenComparing(AccountGroupByIdAud::getAddedOn)
-          .thenComparingInt(a -> a.getAddedBy().get())
-          .thenComparing(AccountGroupByIdAud::getIncludeUUID)
-          .thenComparing(
-              a -> a.getRemovedBy() != null ? a.getRemovedBy().get() : null,
-              nullsLast(naturalOrder()))
-          .thenComparing(AccountGroupByIdAud::getRemovedOn, nullsLast(naturalOrder()));
-
-  private static final Comparator<AuditEntry> AUDIT_ENTRY_COMPARATOR =
-      Comparator.comparing(AuditEntry::getTimestamp)
-          .thenComparing(AuditEntry::getAction, Comparator.comparingInt(Action::getOrder));
-
-  public static GroupBundle create(
-      Source source,
-      AccountGroup group,
-      Iterable<AccountGroupMember> members,
-      Iterable<AccountGroupMemberAudit> memberAudit,
-      Iterable<AccountGroupById> byId,
-      Iterable<AccountGroupByIdAud> byIdAudit) {
-    AccountGroup.UUID uuid = group.getGroupUUID();
-    return new AutoValue_GroupBundle.Builder()
-        .source(source)
-        .group(group)
-        .members(
-            logIfNotUnique(
-                source, uuid, members, ACCOUNT_GROUP_MEMBER_COMPARATOR, AccountGroupMember.class))
-        .memberAudit(
-            logIfNotUnique(
-                source,
-                uuid,
-                memberAudit,
-                ACCOUNT_GROUP_MEMBER_AUDIT_COMPARATOR,
-                AccountGroupMemberAudit.class))
-        .byId(
-            logIfNotUnique(
-                source, uuid, byId, ACCOUNT_GROUP_BY_ID_COMPARATOR, AccountGroupById.class))
-        .byIdAudit(
-            logIfNotUnique(
-                source,
-                uuid,
-                byIdAudit,
-                ACCOUNT_GROUP_BY_ID_AUD_COMPARATOR,
-                AccountGroupByIdAud.class))
-        .build();
-  }
-
-  private static <T> ImmutableSet<T> logIfNotUnique(
-      Source source,
-      AccountGroup.UUID uuid,
-      Iterable<T> iterable,
-      Comparator<T> comparator,
-      Class<T> clazz) {
-    List<T> list = Streams.stream(iterable).sorted(comparator).collect(toList());
-    ImmutableSet<T> set = ImmutableSet.copyOf(list);
-    if (set.size() != list.size()) {
-      // One way this can happen is that distinct audit entities can compare equal, because
-      // AccountGroup{MemberAudit,ByIdAud}.Key does not include the addedOn timestamp in its
-      // members() list. However, this particular issue only applies to pure adds, since removedOn
-      // *is* included in equality. As a result, if this happens, it means the audit log is already
-      // corrupt, and it's not clear if we can programmatically repair it. For migrating to NoteDb,
-      // we'll try our best to recreate it, but no guarantees it will match the real sequence of
-      // attempted operations, which is in any case lost in the mists of time.
-      logger.atWarning().log(
-          "group %s in %s has duplicate %s entities: %s",
-          uuid, source, clazz.getSimpleName(), iterable);
-    }
-    return set;
-  }
-
-  static Builder builder() {
-    return new AutoValue_GroupBundle.Builder().members().memberAudit().byId().byIdAudit();
-  }
-
-  public static ImmutableList<String> compareWithAudits(
-      GroupBundle reviewDbBundle, GroupBundle noteDbBundle) {
-    return compare(reviewDbBundle, noteDbBundle, true);
-  }
-
-  public static ImmutableList<String> compareWithoutAudits(
-      GroupBundle reviewDbBundle, GroupBundle noteDbBundle) {
-    return compare(reviewDbBundle, noteDbBundle, false);
-  }
-
-  private static ImmutableList<String> compare(
-      GroupBundle reviewDbBundle, GroupBundle noteDbBundle, boolean compareAudits) {
-    // Normalize the ReviewDb bundle to what we expect in NoteDb. This means that values in error
-    // messages will not reflect the actual data in ReviewDb, but it will make it easier for humans
-    // to see the difference.
-    reviewDbBundle = reviewDbBundle.truncateToSecond();
-    AccountGroup reviewDbGroup = new AccountGroup(reviewDbBundle.group());
-    reviewDbGroup.setDescription(Strings.emptyToNull(reviewDbGroup.getDescription()));
-    reviewDbBundle = reviewDbBundle.toBuilder().group(reviewDbGroup).build();
-
-    checkArgument(
-        reviewDbBundle.source() == Source.REVIEW_DB,
-        "first bundle's source must be %s: %s",
-        Source.REVIEW_DB,
-        reviewDbBundle);
-    checkArgument(
-        noteDbBundle.source() == Source.NOTE_DB,
-        "second bundle's source must be %s: %s",
-        Source.NOTE_DB,
-        noteDbBundle);
-
-    ImmutableList.Builder<String> result = ImmutableList.builder();
-    if (!reviewDbBundle.group().equals(noteDbBundle.group())) {
-      result.add(
-          "AccountGroups differ\n"
-              + ("ReviewDb: " + reviewDbBundle.group() + "\n")
-              + ("NoteDb  : " + noteDbBundle.group()));
-    }
-    if (!reviewDbBundle.members().equals(noteDbBundle.members())) {
-      result.add(
-          "AccountGroupMembers differ\n"
-              + ("ReviewDb: " + reviewDbBundle.members() + "\n")
-              + ("NoteDb  : " + noteDbBundle.members()));
-    }
-    if (compareAudits
-        && !areMemberAuditsConsideredEqual(
-            reviewDbBundle.memberAudit(), noteDbBundle.memberAudit())) {
-      result.add(
-          "AccountGroupMemberAudits differ\n"
-              + ("ReviewDb: " + reviewDbBundle.memberAudit() + "\n")
-              + ("NoteDb  : " + noteDbBundle.memberAudit()));
-    }
-    if (!reviewDbBundle.byId().equals(noteDbBundle.byId())) {
-      result.add(
-          "AccountGroupByIds differ\n"
-              + ("ReviewDb: " + reviewDbBundle.byId() + "\n")
-              + ("NoteDb  : " + noteDbBundle.byId()));
-    }
-    if (compareAudits
-        && !areByIdAuditsConsideredEqual(reviewDbBundle.byIdAudit(), noteDbBundle.byIdAudit())) {
-      result.add(
-          "AccountGroupByIdAudits differ\n"
-              + ("ReviewDb: " + reviewDbBundle.byIdAudit() + "\n")
-              + ("NoteDb  : " + noteDbBundle.byIdAudit()));
-    }
-    return result.build();
-  }
-
-  private static boolean areMemberAuditsConsideredEqual(
-      ImmutableSet<AccountGroupMemberAudit> reviewDbMemberAudits,
-      ImmutableSet<AccountGroupMemberAudit> noteDbMemberAudits) {
-    ListMultimap<String, AuditEntry> reviewDbMemberAuditsByMemberId =
-        toMemberAuditEntriesByMemberId(reviewDbMemberAudits);
-    ListMultimap<String, AuditEntry> noteDbMemberAuditsByMemberId =
-        toMemberAuditEntriesByMemberId(noteDbMemberAudits);
-
-    return areConsideredEqual(reviewDbMemberAuditsByMemberId, noteDbMemberAuditsByMemberId);
-  }
-
-  private static boolean areByIdAuditsConsideredEqual(
-      ImmutableSet<AccountGroupByIdAud> reviewDbByIdAudits,
-      ImmutableSet<AccountGroupByIdAud> noteDbByIdAudits) {
-    ListMultimap<String, AuditEntry> reviewDbByIdAuditsById =
-        toByIdAuditEntriesById(reviewDbByIdAudits);
-    ListMultimap<String, AuditEntry> noteDbByIdAuditsById =
-        toByIdAuditEntriesById(noteDbByIdAudits);
-
-    return areConsideredEqual(reviewDbByIdAuditsById, noteDbByIdAuditsById);
-  }
-
-  private static ListMultimap<String, AuditEntry> toMemberAuditEntriesByMemberId(
-      ImmutableSet<AccountGroupMemberAudit> memberAudits) {
-    return memberAudits.stream()
-        .flatMap(GroupBundle::toAuditEntries)
-        .collect(
-            Multimaps.toMultimap(
-                AuditEntry::getTarget,
-                Function.identity(),
-                MultimapBuilder.hashKeys().arrayListValues()::build));
-  }
-
-  private static Stream<AuditEntry> toAuditEntries(AccountGroupMemberAudit memberAudit) {
-    AuditEntry additionAuditEntry =
-        AuditEntry.create(
-            Action.ADD,
-            memberAudit.getAddedBy(),
-            memberAudit.getMemberId(),
-            memberAudit.getAddedOn());
-    if (memberAudit.isActive()) {
-      return Stream.of(additionAuditEntry);
-    }
-
-    AuditEntry removalAuditEntry =
-        AuditEntry.create(
-            Action.REMOVE,
-            memberAudit.getRemovedBy(),
-            memberAudit.getMemberId(),
-            memberAudit.getRemovedOn());
-    return Stream.of(additionAuditEntry, removalAuditEntry);
-  }
-
-  private static ListMultimap<String, AuditEntry> toByIdAuditEntriesById(
-      ImmutableSet<AccountGroupByIdAud> byIdAudits) {
-    return byIdAudits.stream()
-        .flatMap(GroupBundle::toAuditEntries)
-        .collect(
-            Multimaps.toMultimap(
-                AuditEntry::getTarget,
-                Function.identity(),
-                MultimapBuilder.hashKeys().arrayListValues()::build));
-  }
-
-  private static Stream<AuditEntry> toAuditEntries(AccountGroupByIdAud byIdAudit) {
-    AuditEntry additionAuditEntry =
-        AuditEntry.create(
-            Action.ADD, byIdAudit.getAddedBy(), byIdAudit.getIncludeUUID(), byIdAudit.getAddedOn());
-    if (byIdAudit.isActive()) {
-      return Stream.of(additionAuditEntry);
-    }
-
-    AuditEntry removalAuditEntry =
-        AuditEntry.create(
-            Action.REMOVE,
-            byIdAudit.getRemovedBy(),
-            byIdAudit.getIncludeUUID(),
-            byIdAudit.getRemovedOn());
-    return Stream.of(additionAuditEntry, removalAuditEntry);
-  }
-
-  /**
-   * Determines whether the audit log entries are equal except for redundant entries. Entries of the
-   * same type (addition/removal) which follow directly on each other according to their timestamp
-   * are considered redundant.
-   */
-  private static boolean areConsideredEqual(
-      ListMultimap<String, AuditEntry> reviewDbMemberAuditsByTarget,
-      ListMultimap<String, AuditEntry> noteDbMemberAuditsByTarget) {
-    for (String target : reviewDbMemberAuditsByTarget.keySet()) {
-      ImmutableList<AuditEntry> reviewDbAuditEntries =
-          reviewDbMemberAuditsByTarget.get(target).stream()
-              .sorted(AUDIT_ENTRY_COMPARATOR)
-              .collect(toImmutableList());
-      ImmutableSet<AuditEntry> noteDbAuditEntries =
-          noteDbMemberAuditsByTarget.get(target).stream()
-              .sorted(AUDIT_ENTRY_COMPARATOR)
-              .collect(toImmutableSet());
-
-      int reviewDbIndex = 0;
-      for (AuditEntry noteDbAuditEntry : noteDbAuditEntries) {
-        Set<AuditEntry> redundantReviewDbAuditEntries = new HashSet<>();
-        while (reviewDbIndex < reviewDbAuditEntries.size()) {
-          AuditEntry reviewDbAuditEntry = reviewDbAuditEntries.get(reviewDbIndex);
-          if (!reviewDbAuditEntry.getAction().equals(noteDbAuditEntry.getAction())) {
-            break;
-          }
-          redundantReviewDbAuditEntries.add(reviewDbAuditEntry);
-          reviewDbIndex++;
-        }
-
-        // The order of the entries is not perfect as ReviewDb included milliseconds for timestamps
-        // and we cut off everything below seconds due to NoteDb/git. Consequently, we don't have a
-        // way to know in this method in which exact order additions/removals within the same second
-        // happened. The best we can do is to group all additions within the same second as
-        // redundant entries and the removals afterward. To compensate that we possibly group
-        // non-redundant additions/removals, we also accept NoteDb audit entries which just occur
-        // anywhere as ReviewDb audit entries.
-        if (!redundantReviewDbAuditEntries.contains(noteDbAuditEntry)
-            && !reviewDbAuditEntries.contains(noteDbAuditEntry)) {
-          return false;
-        }
-      }
-
-      if (reviewDbIndex < reviewDbAuditEntries.size()) {
-        // Some of the ReviewDb audit log entries aren't matched by NoteDb audit log entries.
-        return false;
-      }
-    }
-    return true;
-  }
-
-  public AccountGroup.Id id() {
-    return group().getId();
-  }
-
-  public AccountGroup.UUID uuid() {
-    return group().getGroupUUID();
-  }
-
-  public abstract Source source();
-
-  public abstract AccountGroup group();
-
-  public abstract ImmutableSet<AccountGroupMember> members();
-
-  public abstract ImmutableSet<AccountGroupMemberAudit> memberAudit();
-
-  public abstract ImmutableSet<AccountGroupById> byId();
-
-  public abstract ImmutableSet<AccountGroupByIdAud> byIdAudit();
-
-  public abstract Builder toBuilder();
-
-  public GroupBundle truncateToSecond() {
-    AccountGroup newGroup = new AccountGroup(group());
-    if (newGroup.getCreatedOn() != null) {
-      newGroup.setCreatedOn(TimeUtil.truncateToSecond(newGroup.getCreatedOn()));
-    }
-    return toBuilder()
-        .group(newGroup)
-        .memberAudit(
-            memberAudit().stream().map(GroupBundle::truncateToSecond).collect(toImmutableSet()))
-        .byIdAudit(
-            byIdAudit().stream().map(GroupBundle::truncateToSecond).collect(toImmutableSet()))
-        .build();
-  }
-
-  private static AccountGroupMemberAudit truncateToSecond(AccountGroupMemberAudit a) {
-    AccountGroupMemberAudit result =
-        new AccountGroupMemberAudit(
-            new AccountGroupMemberAudit.Key(
-                a.getKey().getParentKey(),
-                a.getKey().getGroupId(),
-                TimeUtil.truncateToSecond(a.getKey().getAddedOn())),
-            a.getAddedBy());
-    if (a.getRemovedOn() != null) {
-      result.removed(a.getRemovedBy(), TimeUtil.truncateToSecond(a.getRemovedOn()));
-    }
-    return result;
-  }
-
-  private static AccountGroupByIdAud truncateToSecond(AccountGroupByIdAud a) {
-    AccountGroupByIdAud result =
-        new AccountGroupByIdAud(
-            new AccountGroupByIdAud.Key(
-                a.getKey().getParentKey(),
-                a.getKey().getIncludeUUID(),
-                TimeUtil.truncateToSecond(a.getKey().getAddedOn())),
-            a.getAddedBy());
-    if (a.getRemovedOn() != null) {
-      result.removed(a.getRemovedBy(), TimeUtil.truncateToSecond(a.getRemovedOn()));
-    }
-    return result;
-  }
-
-  public InternalGroup toInternalGroup() {
-    return InternalGroup.create(
-        group(),
-        members().stream().map(AccountGroupMember::getAccountId).collect(toImmutableSet()),
-        byId().stream().map(AccountGroupById::getIncludeUUID).collect(toImmutableSet()));
-  }
-
-  @Override
-  public int hashCode() {
-    throw new UnsupportedOperationException(
-        "hashCode is not supported because equals is not supported");
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    throw new UnsupportedOperationException("Use GroupBundle.compare(a, b) instead of equals");
-  }
-
-  @AutoValue
-  abstract static class AuditEntry {
-    private static AuditEntry create(
-        Action action, Account.Id userId, Account.Id memberId, Timestamp timestamp) {
-      return new AutoValue_GroupBundle_AuditEntry(
-          action, userId, String.valueOf(memberId.get()), timestamp);
-    }
-
-    private static AuditEntry create(
-        Action action, Account.Id userId, AccountGroup.UUID subgroupId, Timestamp timestamp) {
-      return new AutoValue_GroupBundle_AuditEntry(action, userId, subgroupId.get(), timestamp);
-    }
-
-    abstract Action getAction();
-
-    abstract Account.Id getUserId();
-
-    abstract String getTarget();
-
-    abstract Timestamp getTimestamp();
-  }
-
-  enum Action {
-    ADD(1),
-    REMOVE(2);
-
-    private final int order;
-
-    Action(int order) {
-      this.order = order;
-    }
-
-    public int getOrder() {
-      return order;
-    }
-  }
-
-  @AutoValue.Builder
-  abstract static class Builder {
-    abstract Builder source(Source source);
-
-    abstract Builder group(AccountGroup group);
-
-    abstract Builder members(AccountGroupMember... member);
-
-    abstract Builder members(Iterable<AccountGroupMember> member);
-
-    abstract Builder memberAudit(AccountGroupMemberAudit... audit);
-
-    abstract Builder memberAudit(Iterable<AccountGroupMemberAudit> audit);
-
-    abstract Builder byId(AccountGroupById... byId);
-
-    abstract Builder byId(Iterable<AccountGroupById> byId);
-
-    abstract Builder byIdAudit(AccountGroupByIdAud... audit);
-
-    abstract Builder byIdAudit(Iterable<AccountGroupByIdAud> audit);
-
-    abstract GroupBundle build();
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/GroupRebuilder.java b/java/com/google/gerrit/server/schema/GroupRebuilder.java
deleted file mode 100644
index 0b4fd02..0000000
--- a/java/com/google/gerrit/server/schema/GroupRebuilder.java
+++ /dev/null
@@ -1,299 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-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 com.google.auto.value.AutoValue;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.ListMultimap;
-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.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupById;
-import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
-import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.meta.MetaDataUpdate;
-import com.google.gerrit.server.git.meta.VersionedMetaData.BatchMetaDataUpdate;
-import com.google.gerrit.server.group.db.AuditLogFormatter;
-import com.google.gerrit.server.group.db.GroupConfig;
-import com.google.gerrit.server.group.db.InternalGroupCreation;
-import com.google.gerrit.server.group.db.InternalGroupUpdate;
-import com.google.gerrit.server.group.db.InternalGroupUpdate.MemberModification;
-import com.google.gerrit.server.group.db.InternalGroupUpdate.SubgroupModification;
-import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
-import java.io.IOException;
-import java.sql.Timestamp;
-import java.util.Collection;
-import java.util.Comparator;
-import java.util.Map;
-import java.util.NavigableSet;
-import java.util.Optional;
-import java.util.function.Consumer;
-import java.util.stream.Stream;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.BatchRefUpdate;
-import org.eclipse.jgit.lib.CommitBuilder;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
-
-/** Helper for rebuilding an entire group's NoteDb refs. */
-class GroupRebuilder {
-  private final PersonIdent serverIdent;
-  private final AllUsersName allUsers;
-  private final AuditLogFormatter auditLogFormatter;
-
-  public GroupRebuilder(
-      PersonIdent serverIdent, AllUsersName allUsers, AuditLogFormatter auditLogFormatter) {
-    this.serverIdent = serverIdent;
-    this.allUsers = allUsers;
-    this.auditLogFormatter = auditLogFormatter;
-  }
-
-  public void rebuild(Repository allUsersRepo, GroupBundle bundle, @Nullable BatchRefUpdate bru)
-      throws IOException, ConfigInvalidException, OrmDuplicateKeyException {
-    AccountGroup group = bundle.group();
-    InternalGroupCreation groupCreation =
-        InternalGroupCreation.builder()
-            .setId(bundle.id())
-            .setNameKey(group.getNameKey())
-            .setGroupUUID(group.getGroupUUID())
-            .build();
-    GroupConfig groupConfig = GroupConfig.createForNewGroup(allUsers, allUsersRepo, groupCreation);
-    groupConfig.setAllowSaveEmptyName();
-
-    InternalGroupUpdate.Builder updateBuilder =
-        InternalGroupUpdate.builder()
-            .setOwnerGroupUUID(group.getOwnerGroupUUID())
-            .setVisibleToAll(group.isVisibleToAll())
-            .setUpdatedOn(group.getCreatedOn());
-    if (bundle.group().getDescription() != null) {
-      updateBuilder.setDescription(group.getDescription());
-    }
-    groupConfig.setGroupUpdate(updateBuilder.build(), auditLogFormatter);
-
-    Map<Key, Collection<Event>> events = toEvents(bundle).asMap();
-    PersonIdent nowServerIdent = getServerIdent(events);
-
-    MetaDataUpdate md = createMetaDataUpdate(allUsers, allUsersRepo, bru);
-
-    // Creation is done by the server (unlike later audit events).
-    PersonIdent created = new PersonIdent(nowServerIdent, group.getCreatedOn());
-    md.getCommitBuilder().setAuthor(created);
-    md.getCommitBuilder().setCommitter(created);
-
-    // Rebuild group ref.
-    try (BatchMetaDataUpdate batch = groupConfig.openUpdate(md)) {
-      batch.write(groupConfig, md.getCommitBuilder());
-
-      for (Map.Entry<Key, Collection<Event>> e : events.entrySet()) {
-        InternalGroupUpdate.Builder ub = InternalGroupUpdate.builder();
-        e.getValue().forEach(event -> event.update().accept(ub));
-        ub.setUpdatedOn(e.getKey().when());
-        groupConfig.setGroupUpdate(ub.build(), auditLogFormatter);
-
-        PersonIdent currServerIdent = new PersonIdent(nowServerIdent, e.getKey().when());
-        CommitBuilder cb = new CommitBuilder();
-        cb.setAuthor(
-            e.getKey()
-                .accountId()
-                .map(id -> auditLogFormatter.getParsableAuthorIdent(id, currServerIdent))
-                .orElse(currServerIdent));
-        cb.setCommitter(currServerIdent);
-        batch.write(groupConfig, cb);
-      }
-
-      batch.createRef(groupConfig.getRefName());
-    }
-  }
-
-  private ListMultimap<Key, Event> toEvents(GroupBundle bundle) {
-    ListMultimap<Key, Event> result =
-        MultimapBuilder.treeKeys(Key.COMPARATOR).arrayListValues(1).build();
-    Event e;
-
-    for (AccountGroupMemberAudit a : bundle.memberAudit()) {
-      checkArgument(
-          a.getKey().getGroupId().equals(bundle.id()),
-          "key %s does not match group %s",
-          a.getKey(),
-          bundle.id());
-      Account.Id accountId = a.getKey().getParentKey();
-      e = event(Type.ADD_MEMBER, a.getAddedBy(), a.getKey().getAddedOn(), addMember(accountId));
-      result.put(e.key(), e);
-      if (!a.isActive()) {
-        e = event(Type.REMOVE_MEMBER, a.getRemovedBy(), a.getRemovedOn(), removeMember(accountId));
-        result.put(e.key(), e);
-      }
-    }
-
-    for (AccountGroupByIdAud a : bundle.byIdAudit()) {
-      checkArgument(
-          a.getKey().getParentKey().equals(bundle.id()),
-          "key %s does not match group %s",
-          a.getKey(),
-          bundle.id());
-      AccountGroup.UUID uuid = a.getKey().getIncludeUUID();
-      e = event(Type.ADD_GROUP, a.getAddedBy(), a.getKey().getAddedOn(), addGroup(uuid));
-      result.put(e.key(), e);
-      if (!a.isActive()) {
-        e = event(Type.REMOVE_GROUP, a.getRemovedBy(), a.getRemovedOn(), removeGroup(uuid));
-        result.put(e.key(), e);
-      }
-    }
-
-    // Due to clock skew, audit events may be in the future relative to this machine. Ensure the
-    // fixup event happens after any other events, both for the purposes of sorting Keys correctly
-    // and to avoid non-monotonic timestamps in the commit history.
-    Timestamp maxTs =
-        Stream.concat(result.keySet().stream().map(Key::when), Stream.of(TimeUtil.nowTs()))
-            .max(Comparator.naturalOrder())
-            .get();
-    Timestamp fixupTs = new Timestamp(maxTs.getTime() + 1);
-    e = serverEvent(Type.FIXUP, fixupTs, setCurrentMembership(bundle));
-    result.put(e.key(), e);
-
-    return result;
-  }
-
-  private PersonIdent getServerIdent(Map<Key, Collection<Event>> events) {
-    // Created with MultimapBuilder.treeKeys, so the keySet is navigable.
-    Key lastKey = ((NavigableSet<Key>) events.keySet()).last();
-    checkState(lastKey.type() == Type.FIXUP);
-    return new PersonIdent(
-        serverIdent.getName(),
-        serverIdent.getEmailAddress(),
-        Iterables.getOnlyElement(events.get(lastKey)).when(),
-        serverIdent.getTimeZone());
-  }
-
-  private static MetaDataUpdate createMetaDataUpdate(
-      Project.NameKey projectName, Repository repository, @Nullable BatchRefUpdate batchRefUpdate) {
-    return new MetaDataUpdate(
-        GitReferenceUpdated.DISABLED, projectName, repository, batchRefUpdate);
-  }
-
-  private static Consumer<InternalGroupUpdate.Builder> addMember(Account.Id toAdd) {
-    return b -> {
-      MemberModification prev = b.getMemberModification();
-      b.setMemberModification(in -> Sets.union(prev.apply(in), ImmutableSet.of(toAdd)));
-    };
-  }
-
-  private static Consumer<InternalGroupUpdate.Builder> removeMember(Account.Id toRemove) {
-    return b -> {
-      MemberModification prev = b.getMemberModification();
-      b.setMemberModification(in -> Sets.difference(prev.apply(in), ImmutableSet.of(toRemove)));
-    };
-  }
-
-  private static Consumer<InternalGroupUpdate.Builder> addGroup(AccountGroup.UUID toAdd) {
-    return b -> {
-      SubgroupModification prev = b.getSubgroupModification();
-      b.setSubgroupModification(in -> Sets.union(prev.apply(in), ImmutableSet.of(toAdd)));
-    };
-  }
-
-  private static Consumer<InternalGroupUpdate.Builder> removeGroup(AccountGroup.UUID toRemove) {
-    return b -> {
-      SubgroupModification prev = b.getSubgroupModification();
-      b.setSubgroupModification(in -> Sets.difference(prev.apply(in), ImmutableSet.of(toRemove)));
-    };
-  }
-
-  private static Consumer<InternalGroupUpdate.Builder> setCurrentMembership(GroupBundle bundle) {
-    // Overwrite members and subgroups with the current values. The storage layer will do the
-    // set differences to compute the appropriate delta, if any.
-    return b ->
-        b.setMemberModification(
-                in ->
-                    bundle.members().stream()
-                        .map(AccountGroupMember::getAccountId)
-                        .collect(toImmutableSet()))
-            .setSubgroupModification(
-                in ->
-                    bundle.byId().stream()
-                        .map(AccountGroupById::getIncludeUUID)
-                        .collect(toImmutableSet()));
-  }
-
-  private static Event event(
-      Type type,
-      Account.Id accountId,
-      Timestamp when,
-      Consumer<InternalGroupUpdate.Builder> update) {
-    return new AutoValue_GroupRebuilder_Event(type, Optional.of(accountId), when, update);
-  }
-
-  private static Event serverEvent(
-      Type type, Timestamp when, Consumer<InternalGroupUpdate.Builder> update) {
-    return new AutoValue_GroupRebuilder_Event(type, Optional.empty(), when, update);
-  }
-
-  @AutoValue
-  abstract static class Event {
-    abstract Type type();
-
-    abstract Optional<Account.Id> accountId();
-
-    abstract Timestamp when();
-
-    abstract Consumer<InternalGroupUpdate.Builder> update();
-
-    Key key() {
-      return new AutoValue_GroupRebuilder_Key(accountId(), when(), type());
-    }
-  }
-
-  /**
-   * Distinct event types.
-   *
-   * <p>Events at the same time by the same user are batched together by type. The types should
-   * correspond to the possible batch operations supported by AuditService.
-   */
-  enum Type {
-    ADD_MEMBER,
-    REMOVE_MEMBER,
-    ADD_GROUP,
-    REMOVE_GROUP,
-    FIXUP;
-  }
-
-  @AutoValue
-  abstract static class Key {
-    static final Comparator<Key> COMPARATOR =
-        Comparator.comparing(Key::when)
-            .thenComparing(
-                k -> k.accountId().map(Account.Id::get).orElse(null),
-                Comparator.nullsFirst(Comparator.naturalOrder()))
-            .thenComparing(Key::type);
-
-    abstract Optional<Account.Id> accountId();
-
-    abstract Timestamp when();
-
-    abstract Type type();
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/H2.java b/java/com/google/gerrit/server/schema/H2.java
deleted file mode 100644
index 840eaf0..0000000
--- a/java/com/google/gerrit/server/schema/H2.java
+++ /dev/null
@@ -1,63 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.inject.Inject;
-import java.nio.file.Path;
-import org.eclipse.jgit.lib.Config;
-
-class H2 extends BaseDataSourceType {
-
-  protected final Config cfg;
-  private final SitePaths site;
-
-  @Inject
-  H2(SitePaths site, @GerritServerConfig Config cfg) {
-    super("org.h2.Driver");
-    this.cfg = cfg;
-    this.site = site;
-  }
-
-  @Override
-  public String getUrl() {
-    String database = cfg.getString("database", null, "database");
-    if (database == null || database.isEmpty()) {
-      database = "db/ReviewDB";
-    }
-    return appendUrlOptions(cfg, createUrl(site.resolve(database)));
-  }
-
-  public static String createUrl(Path path) {
-    return new StringBuilder().append("jdbc:h2:").append(path.toUri().toString()).toString();
-  }
-
-  public static String appendUrlOptions(Config cfg, String url) {
-    long h2CacheSize = cfg.getLong("database", "h2", "cacheSize", -1);
-    boolean h2AutoServer = cfg.getBoolean("database", "h2", "autoServer", false);
-
-    StringBuilder urlBuilder = new StringBuilder().append(url);
-
-    if (h2CacheSize >= 0) {
-      // H2 CACHE_SIZE is always given in KB
-      urlBuilder.append(";CACHE_SIZE=").append(h2CacheSize / 1024);
-    }
-    if (h2AutoServer) {
-      urlBuilder.append(";AUTO_SERVER=TRUE");
-    }
-    return urlBuilder.toString();
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/H2AccountPatchReviewStore.java b/java/com/google/gerrit/server/schema/H2AccountPatchReviewStore.java
index 5146140..3f74cda 100644
--- a/java/com/google/gerrit/server/schema/H2AccountPatchReviewStore.java
+++ b/java/com/google/gerrit/server/schema/H2AccountPatchReviewStore.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.server.schema;
 
+import com.google.gerrit.exceptions.DuplicateKeyException;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.config.ThreadSettingsConfig;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.sql.SQLException;
@@ -36,17 +36,17 @@
   }
 
   @Override
-  public OrmException convertError(String op, SQLException err) {
+  public StorageException convertError(String op, SQLException err) {
     switch (getSQLStateInt(err)) {
       case 23001: // UNIQUE CONSTRAINT VIOLATION
       case 23505: // DUPLICATE_KEY_1
-        return new OrmDuplicateKeyException("account_patch_reviews", err);
+        return new DuplicateKeyException("account_patch_reviews", err);
 
       default:
         if (err.getCause() == null && err.getNextException() != null) {
           err.initCause(err.getNextException());
         }
-        return new OrmException(op + " failure on account_patch_reviews", err);
+        return new StorageException(op + " failure on account_patch_reviews", err);
     }
   }
 }
diff --git a/java/com/google/gerrit/server/schema/HANA.java b/java/com/google/gerrit/server/schema/HANA.java
deleted file mode 100644
index f9811c6..0000000
--- a/java/com/google/gerrit/server/schema/HANA.java
+++ /dev/null
@@ -1,56 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import static com.google.gerrit.server.schema.JdbcUtil.hostname;
-import static com.google.gerrit.server.schema.JdbcUtil.port;
-
-import com.google.common.base.Strings;
-import com.google.gerrit.server.config.ConfigSection;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.inject.Inject;
-import java.io.IOException;
-import org.eclipse.jgit.lib.Config;
-
-class HANA extends BaseDataSourceType {
-
-  private Config cfg;
-
-  @Inject
-  HANA(@GerritServerConfig Config cfg) {
-    super("com.sap.db.jdbc.Driver");
-    this.cfg = cfg;
-  }
-
-  @Override
-  public String getUrl() {
-    final StringBuilder b = new StringBuilder();
-    final ConfigSection dbs = new ConfigSection(cfg, "database");
-    b.append("jdbc:sap://");
-    b.append(hostname(dbs.required("hostname")));
-    b.append(port(dbs.optional("port")));
-    String database = dbs.optional("database");
-    if (!Strings.isNullOrEmpty(database)) {
-      b.append("?databaseName=").append(database);
-    }
-    return b.toString();
-  }
-
-  @Override
-  public ScriptRunner getIndexScript() throws IOException {
-    // HANA uses column tables and should not require additional indices
-    return ScriptRunner.NOOP;
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/InMemoryAccountPatchReviewStore.java b/java/com/google/gerrit/server/schema/InMemoryAccountPatchReviewStore.java
deleted file mode 100644
index 35e81b2..0000000
--- a/java/com/google/gerrit/server/schema/InMemoryAccountPatchReviewStore.java
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.gerrit.extensions.registration.DynamicItem;
-import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.server.change.AccountPatchReviewStore;
-import com.google.gwtorm.jdbc.SimpleDataSource;
-import java.sql.SQLException;
-import java.util.Properties;
-import javax.sql.DataSource;
-
-public class InMemoryAccountPatchReviewStore extends JdbcAccountPatchReviewStore {
-  @VisibleForTesting
-  public static class Module extends LifecycleModule {
-    @Override
-    protected void configure() {
-      InMemoryAccountPatchReviewStore inMemoryStore = new InMemoryAccountPatchReviewStore();
-      DynamicItem.bind(binder(), AccountPatchReviewStore.class).toInstance(inMemoryStore);
-      listener().toInstance(inMemoryStore);
-    }
-  }
-
-  /**
-   * Creates an in-memory H2 database to store the reviewed flags. This should be used for tests
-   * only.
-   */
-  @VisibleForTesting
-  private InMemoryAccountPatchReviewStore() {
-    super(newDataSource());
-  }
-
-  private static synchronized DataSource newDataSource() {
-    final Properties p = new Properties();
-    p.setProperty("driver", "org.h2.Driver");
-    // DB_CLOSE_DELAY=-1: By default the content of an in-memory H2 database is lost at the moment
-    // the last connection is closed. This option keeps the content as long as the vm lives.
-    p.setProperty("url", "jdbc:h2:mem:account_patch_reviews;DB_CLOSE_DELAY=-1");
-    try {
-      return new SimpleDataSource(p);
-    } catch (SQLException e) {
-      throw new RuntimeException("Unable to create test datasource", e);
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/JDBC.java b/java/com/google/gerrit/server/schema/JDBC.java
deleted file mode 100644
index d188df4..0000000
--- a/java/com/google/gerrit/server/schema/JDBC.java
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.server.config.ConfigUtil;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.inject.Inject;
-import org.eclipse.jgit.lib.Config;
-
-class JDBC extends BaseDataSourceType {
-
-  protected final Config cfg;
-
-  @Inject
-  JDBC(@GerritServerConfig Config cfg) {
-    super(ConfigUtil.getRequired(cfg, "database", "driver"));
-    this.cfg = cfg;
-  }
-
-  @Override
-  public String getUrl() {
-    return ConfigUtil.getRequired(cfg, "database", "url");
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java b/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
index 83a0986..0612bb9 100644
--- a/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
+++ b/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
@@ -17,21 +17,24 @@
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
+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.gwtorm.server.OrmDuplicateKeyException;
-import com.google.gwtorm.server.OrmException;
+import java.nio.file.Path;
 import java.sql.Connection;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
@@ -47,6 +50,12 @@
     implements AccountPatchReviewStore, LifecycleListener {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
+  // DB_CLOSE_DELAY=-1: By default the content of an in-memory H2 database is lost at the moment the
+  // last connection is closed. This option keeps the content as long as the VM lives.
+  @VisibleForTesting
+  public static final String TEST_IN_MEMORY_URL =
+      "jdbc:h2:mem:account_patch_reviews;DB_CLOSE_DELAY=-1";
+
   private static final String ACCOUNT_PATCH_REVIEW_DB = "accountPatchReviewDb";
   private static final String H2_DB = "h2";
   private static final String MARIADB = "mariadb";
@@ -108,14 +117,10 @@
     this.ds = createDataSource(cfg, sitePaths, threadSettingsConfig);
   }
 
-  protected JdbcAccountPatchReviewStore(DataSource ds) {
-    this.ds = ds;
-  }
-
   private static String getUrl(@GerritServerConfig Config cfg, SitePaths sitePaths) {
     String url = cfg.getString(ACCOUNT_PATCH_REVIEW_DB, null, URL);
     if (url == null) {
-      return H2.createUrl(sitePaths.db_dir.resolve("account_patch_reviews"));
+      return createH2Url(sitePaths.db_dir.resolve("account_patch_reviews"));
     }
     return url;
   }
@@ -163,7 +168,7 @@
   public void start() {
     try {
       createTableIfNotExists();
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Failed to create table to store account patch reviews");
     }
   }
@@ -172,7 +177,7 @@
     return ds.getConnection();
   }
 
-  public void createTableIfNotExists() throws OrmException {
+  public void createTableIfNotExists() {
     try (Connection con = ds.getConnection();
         Statement stmt = con.createStatement()) {
       doCreateTable(stmt);
@@ -193,7 +198,7 @@
             + ")");
   }
 
-  public void dropTableIfExists() throws OrmException {
+  public void dropTableIfExists() {
     try (Connection con = ds.getConnection();
         Statement stmt = con.createStatement()) {
       stmt.executeUpdate("DROP TABLE IF EXISTS account_patch_reviews");
@@ -206,8 +211,7 @@
   public void stop() {}
 
   @Override
-  public boolean markReviewed(PatchSet.Id psId, Account.Id accountId, String path)
-      throws OrmException {
+  public boolean markReviewed(PatchSet.Id psId, Account.Id accountId, String path) {
     try (Connection con = ds.getConnection();
         PreparedStatement stmt =
             con.prepareStatement(
@@ -221,8 +225,8 @@
       stmt.executeUpdate();
       return true;
     } catch (SQLException e) {
-      OrmException ormException = convertError("insert", e);
-      if (ormException instanceof OrmDuplicateKeyException) {
+      StorageException ormException = convertError("insert", e);
+      if (ormException instanceof DuplicateKeyException) {
         return false;
       }
       throw ormException;
@@ -230,8 +234,7 @@
   }
 
   @Override
-  public void markReviewed(PatchSet.Id psId, Account.Id accountId, Collection<String> paths)
-      throws OrmException {
+  public void markReviewed(PatchSet.Id psId, Account.Id accountId, Collection<String> paths) {
     if (paths == null || paths.isEmpty()) {
       return;
     }
@@ -251,8 +254,8 @@
       }
       stmt.executeBatch();
     } catch (SQLException e) {
-      OrmException ormException = convertError("insert", e);
-      if (ormException instanceof OrmDuplicateKeyException) {
+      StorageException ormException = convertError("insert", e);
+      if (ormException instanceof DuplicateKeyException) {
         return;
       }
       throw ormException;
@@ -260,8 +263,7 @@
   }
 
   @Override
-  public void clearReviewed(PatchSet.Id psId, Account.Id accountId, String path)
-      throws OrmException {
+  public void clearReviewed(PatchSet.Id psId, Account.Id accountId, String path) {
     try (Connection con = ds.getConnection();
         PreparedStatement stmt =
             con.prepareStatement(
@@ -279,7 +281,7 @@
   }
 
   @Override
-  public void clearReviewed(PatchSet.Id psId) throws OrmException {
+  public void clearReviewed(PatchSet.Id psId) {
     try (Connection con = ds.getConnection();
         PreparedStatement stmt =
             con.prepareStatement(
@@ -294,8 +296,19 @@
   }
 
   @Override
-  public Optional<PatchSetWithReviewedFiles> findReviewed(PatchSet.Id psId, Account.Id accountId)
-      throws OrmException {
+  public void clearReviewed(Change.Id changeId) {
+    try (Connection con = ds.getConnection();
+        PreparedStatement stmt =
+            con.prepareStatement("DELETE FROM account_patch_reviews WHERE change_id = ?")) {
+      stmt.setInt(1, changeId.get());
+      stmt.executeUpdate();
+    } catch (SQLException e) {
+      throw convertError("delete", e);
+    }
+  }
+
+  @Override
+  public Optional<PatchSetWithReviewedFiles> findReviewed(PatchSet.Id psId, Account.Id accountId) {
     try (Connection con = ds.getConnection();
         PreparedStatement stmt =
             con.prepareStatement(
@@ -327,11 +340,11 @@
     }
   }
 
-  public OrmException convertError(String op, SQLException err) {
+  public StorageException convertError(String op, SQLException err) {
     if (err.getCause() == null && err.getNextException() != null) {
       err.initCause(err.getNextException());
     }
-    return new OrmException(op + " failure on account_patch_reviews", err);
+    return new StorageException(op + " failure on account_patch_reviews", err);
   }
 
   private static String getSQLState(SQLException err) {
@@ -352,4 +365,8 @@
     }
     return 0;
   }
+
+  private static String createH2Url(Path path) {
+    return new StringBuilder().append("jdbc:h2:").append(path.toUri().toString()).toString();
+  }
 }
diff --git a/java/com/google/gerrit/server/schema/JdbcUtil.java b/java/com/google/gerrit/server/schema/JdbcUtil.java
deleted file mode 100644
index dddf23a..0000000
--- a/java/com/google/gerrit/server/schema/JdbcUtil.java
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.server.UsedAt;
-
-public class JdbcUtil {
-
-  public static String hostname(String hostname) {
-    if (hostname == null || hostname.isEmpty()) {
-      hostname = "localhost";
-
-    } else if (hostname.contains(":") && !hostname.startsWith("[")) {
-      hostname = "[" + hostname + "]";
-    }
-    return hostname;
-  }
-
-  @UsedAt(UsedAt.Project.PLUGINS_ALL)
-  public static String port(String port) {
-    if (port != null && !port.isEmpty()) {
-      return ":" + port;
-    }
-    return "";
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/MariaDBAccountPatchReviewStore.java b/java/com/google/gerrit/server/schema/MariaDBAccountPatchReviewStore.java
index aa05a08..b0a3370 100644
--- a/java/com/google/gerrit/server/schema/MariaDBAccountPatchReviewStore.java
+++ b/java/com/google/gerrit/server/schema/MariaDBAccountPatchReviewStore.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.server.schema;
 
+import com.google.gerrit.exceptions.DuplicateKeyException;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.config.ThreadSettingsConfig;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.sql.SQLException;
@@ -36,18 +36,18 @@
   }
 
   @Override
-  public OrmException convertError(String op, SQLException err) {
+  public StorageException convertError(String op, SQLException err) {
     switch (getSQLStateInt(err)) {
       case 1022: // ER_DUP_KEY
       case 1062: // ER_DUP_ENTRY
       case 1169: // ER_DUP_UNIQUE;
-        return new OrmDuplicateKeyException("ACCOUNT_PATCH_REVIEWS", err);
+        return new DuplicateKeyException("ACCOUNT_PATCH_REVIEWS", err);
 
       default:
         if (err.getCause() == null && err.getNextException() != null) {
           err.initCause(err.getNextException());
         }
-        return new OrmException(op + " failure on ACCOUNT_PATCH_REVIEWS", err);
+        return new StorageException(op + " failure on ACCOUNT_PATCH_REVIEWS", err);
     }
   }
 }
diff --git a/java/com/google/gerrit/server/schema/MariaDb.java b/java/com/google/gerrit/server/schema/MariaDb.java
deleted file mode 100644
index 6c5dd35..0000000
--- a/java/com/google/gerrit/server/schema/MariaDb.java
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import static com.google.gerrit.server.schema.JdbcUtil.hostname;
-import static com.google.gerrit.server.schema.JdbcUtil.port;
-
-import com.google.gerrit.server.config.ConfigSection;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.inject.Inject;
-import org.eclipse.jgit.lib.Config;
-
-class MariaDb extends BaseDataSourceType {
-  private final Config cfg;
-
-  @Inject
-  MariaDb(@GerritServerConfig Config cfg) {
-    super("org.mariadb.jdbc.Driver");
-    this.cfg = cfg;
-  }
-
-  @Override
-  public String getUrl() {
-    StringBuilder b = new StringBuilder();
-    ConfigSection dbs = new ConfigSection(cfg, "database");
-    b.append("jdbc:mariadb://");
-    b.append(hostname(dbs.optional("hostname")));
-    b.append(port(dbs.optional("port")));
-    b.append("/");
-    b.append(dbs.required("database"));
-    b.append("?useBulkStmts=false");
-    return b.toString();
-  }
-
-  @Override
-  public boolean usePool() {
-    // MariaDB has given us trouble with the connection pool,
-    // sometimes the backend disconnects and the pool winds
-    // up with a stale connection. Fortunately opening up
-    // a new MariaDB connection is usually very fast.
-    return false;
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/MaxDb.java b/java/com/google/gerrit/server/schema/MaxDb.java
deleted file mode 100644
index d552eb65..0000000
--- a/java/com/google/gerrit/server/schema/MaxDb.java
+++ /dev/null
@@ -1,50 +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.schema;
-
-import static com.google.gerrit.server.schema.JdbcUtil.hostname;
-
-import com.google.gerrit.server.config.ConfigSection;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.inject.Inject;
-import java.io.IOException;
-import org.eclipse.jgit.lib.Config;
-
-class MaxDb extends BaseDataSourceType {
-
-  private Config cfg;
-
-  @Inject
-  MaxDb(@GerritServerConfig Config cfg) {
-    super("com.sap.dbtech.jdbc.DriverSapDB");
-    this.cfg = cfg;
-  }
-
-  @Override
-  public String getUrl() {
-    final StringBuilder b = new StringBuilder();
-    final ConfigSection dbs = new ConfigSection(cfg, "database");
-    b.append("jdbc:sapdb://");
-    b.append(hostname(dbs.optional("hostname")));
-    b.append("/");
-    b.append(dbs.required("database"));
-    return b.toString();
-  }
-
-  @Override
-  public ScriptRunner getIndexScript() throws IOException {
-    return getScriptRunner("index_maxdb.sql");
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/MySql.java b/java/com/google/gerrit/server/schema/MySql.java
deleted file mode 100644
index e5f59d7..0000000
--- a/java/com/google/gerrit/server/schema/MySql.java
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import static com.google.gerrit.server.schema.JdbcUtil.hostname;
-import static com.google.gerrit.server.schema.JdbcUtil.port;
-
-import com.google.gerrit.server.config.ConfigSection;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.inject.Inject;
-import org.eclipse.jgit.lib.Config;
-
-class MySql extends BaseDataSourceType {
-
-  private Config cfg;
-
-  @Inject
-  MySql(@GerritServerConfig Config cfg) {
-    super("com.mysql.jdbc.Driver");
-    this.cfg = cfg;
-  }
-
-  @Override
-  public String getUrl() {
-    final StringBuilder b = new StringBuilder();
-    final ConfigSection dbs = new ConfigSection(cfg, "database");
-    b.append("jdbc:mysql://");
-    b.append(hostname(dbs.optional("hostname")));
-    b.append(port(dbs.optional("port")));
-    b.append("/");
-    b.append(dbs.required("database"));
-    // See
-    // https://stackoverflow.com/questions/42084633/table-name-pattern-can-not-be-null-or-empty-in-java
-    b.append("?nullNamePatternMatchesAll=true");
-    return b.toString();
-  }
-
-  @Override
-  public boolean usePool() {
-    // MySQL has given us trouble with the connection pool,
-    // sometimes the backend disconnects and the pool winds
-    // up with a stale connection. Fortunately opening up
-    // a new MySQL connection is usually very fast.
-    return false;
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/MysqlAccountPatchReviewStore.java b/java/com/google/gerrit/server/schema/MysqlAccountPatchReviewStore.java
index cb8c707..677b2de 100644
--- a/java/com/google/gerrit/server/schema/MysqlAccountPatchReviewStore.java
+++ b/java/com/google/gerrit/server/schema/MysqlAccountPatchReviewStore.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.server.schema;
 
+import com.google.gerrit.exceptions.DuplicateKeyException;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.config.ThreadSettingsConfig;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.sql.SQLException;
@@ -37,18 +37,18 @@
   }
 
   @Override
-  public OrmException convertError(String op, SQLException err) {
+  public StorageException convertError(String op, SQLException err) {
     switch (err.getErrorCode()) {
       case 1022: // ER_DUP_KEY
       case 1062: // ER_DUP_ENTRY
       case 1169: // ER_DUP_UNIQUE;
-        return new OrmDuplicateKeyException("ACCOUNT_PATCH_REVIEWS", err);
+        return new DuplicateKeyException("ACCOUNT_PATCH_REVIEWS", err);
 
       default:
         if (err.getCause() == null && err.getNextException() != null) {
           err.initCause(err.getNextException());
         }
-        return new OrmException(op + " failure on ACCOUNT_PATCH_REVIEWS", err);
+        return new StorageException(op + " failure on ACCOUNT_PATCH_REVIEWS", err);
     }
   }
 
diff --git a/java/com/google/gerrit/server/schema/NoChangesReviewDbWrapper.java b/java/com/google/gerrit/server/schema/NoChangesReviewDbWrapper.java
deleted file mode 100644
index 7247490..0000000
--- a/java/com/google/gerrit/server/schema/NoChangesReviewDbWrapper.java
+++ /dev/null
@@ -1,214 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-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.ChangeMessage;
-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.server.ChangeAccess;
-import com.google.gerrit.reviewdb.server.ChangeMessageAccess;
-import com.google.gerrit.reviewdb.server.PatchLineCommentAccess;
-import com.google.gerrit.reviewdb.server.PatchSetAccess;
-import com.google.gerrit.reviewdb.server.PatchSetApprovalAccess;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.reviewdb.server.ReviewDbWrapper;
-import com.google.gwtorm.server.ListResultSet;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
-
-/**
- * Wrapper for ReviewDb that never calls the underlying change tables.
- *
- * <p>See {@link NotesMigrationSchemaFactory} for discussion.
- */
-class NoChangesReviewDbWrapper extends ReviewDbWrapper {
-  private static <T> ResultSet<T> empty() {
-    return new ListResultSet<>(ImmutableList.of());
-  }
-
-  private final ChangeAccess changes;
-  private final PatchSetApprovalAccess patchSetApprovals;
-  private final ChangeMessageAccess changeMessages;
-  private final PatchSetAccess patchSets;
-  private final PatchLineCommentAccess patchComments;
-
-  NoChangesReviewDbWrapper(ReviewDb db) {
-    super(db);
-    changes = new Changes(this, delegate);
-    patchSetApprovals = new PatchSetApprovals(this, delegate);
-    changeMessages = new ChangeMessages(this, delegate);
-    patchSets = new PatchSets(this, delegate);
-    patchComments = new PatchLineComments(this, delegate);
-  }
-
-  @Override
-  public ChangeAccess changes() {
-    return changes;
-  }
-
-  @Override
-  public PatchSetApprovalAccess patchSetApprovals() {
-    return patchSetApprovals;
-  }
-
-  @Override
-  public ChangeMessageAccess changeMessages() {
-    return changeMessages;
-  }
-
-  @Override
-  public PatchSetAccess patchSets() {
-    return patchSets;
-  }
-
-  @Override
-  public PatchLineCommentAccess patchComments() {
-    return patchComments;
-  }
-
-  private static class Changes extends AbstractDisabledAccess<Change, Change.Id>
-      implements ChangeAccess {
-    private Changes(NoChangesReviewDbWrapper wrapper, ReviewDb db) {
-      super(wrapper, db.changes());
-    }
-
-    @Override
-    public ResultSet<Change> all() {
-      return empty();
-    }
-  }
-
-  private static class ChangeMessages
-      extends AbstractDisabledAccess<ChangeMessage, ChangeMessage.Key>
-      implements ChangeMessageAccess {
-    private ChangeMessages(NoChangesReviewDbWrapper wrapper, ReviewDb db) {
-      super(wrapper, db.changeMessages());
-    }
-
-    @Override
-    public ResultSet<ChangeMessage> byChange(Change.Id id) throws OrmException {
-      return empty();
-    }
-
-    @Override
-    public ResultSet<ChangeMessage> byPatchSet(PatchSet.Id id) throws OrmException {
-      return empty();
-    }
-
-    @Override
-    public ResultSet<ChangeMessage> all() throws OrmException {
-      return empty();
-    }
-  }
-
-  private static class PatchSets extends AbstractDisabledAccess<PatchSet, PatchSet.Id>
-      implements PatchSetAccess {
-    private PatchSets(NoChangesReviewDbWrapper wrapper, ReviewDb db) {
-      super(wrapper, db.patchSets());
-    }
-
-    @Override
-    public ResultSet<PatchSet> byChange(Change.Id id) {
-      return empty();
-    }
-
-    @Override
-    public ResultSet<PatchSet> all() {
-      return empty();
-    }
-  }
-
-  private static class PatchSetApprovals
-      extends AbstractDisabledAccess<PatchSetApproval, PatchSetApproval.Key>
-      implements PatchSetApprovalAccess {
-    private PatchSetApprovals(NoChangesReviewDbWrapper wrapper, ReviewDb db) {
-      super(wrapper, db.patchSetApprovals());
-    }
-
-    @Override
-    public ResultSet<PatchSetApproval> byChange(Change.Id id) {
-      return empty();
-    }
-
-    @Override
-    public ResultSet<PatchSetApproval> byPatchSet(PatchSet.Id id) {
-      return empty();
-    }
-
-    @Override
-    public ResultSet<PatchSetApproval> byPatchSetUser(PatchSet.Id patchSet, Account.Id account) {
-      return empty();
-    }
-
-    @Override
-    public ResultSet<PatchSetApproval> all() {
-      return empty();
-    }
-  }
-
-  private static class PatchLineComments
-      extends AbstractDisabledAccess<PatchLineComment, PatchLineComment.Key>
-      implements PatchLineCommentAccess {
-    private PatchLineComments(NoChangesReviewDbWrapper wrapper, ReviewDb db) {
-      super(wrapper, db.patchComments());
-    }
-
-    @Override
-    public ResultSet<PatchLineComment> byChange(Change.Id id) {
-      return empty();
-    }
-
-    @Override
-    public ResultSet<PatchLineComment> byPatchSet(PatchSet.Id id) {
-      return empty();
-    }
-
-    @Override
-    public ResultSet<PatchLineComment> publishedByChangeFile(Change.Id id, String file) {
-      return empty();
-    }
-
-    @Override
-    public ResultSet<PatchLineComment> publishedByPatchSet(PatchSet.Id patchset) {
-      return empty();
-    }
-
-    @Override
-    public ResultSet<PatchLineComment> draftByPatchSetAuthor(
-        PatchSet.Id patchset, Account.Id author) {
-      return empty();
-    }
-
-    @Override
-    public ResultSet<PatchLineComment> draftByChangeFileAuthor(
-        Change.Id id, String file, Account.Id author) {
-      return empty();
-    }
-
-    @Override
-    public ResultSet<PatchLineComment> draftByAuthor(Account.Id author) {
-      return empty();
-    }
-
-    @Override
-    public ResultSet<PatchLineComment> all() {
-      return empty();
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/NoteDbSchemaUpdater.java b/java/com/google/gerrit/server/schema/NoteDbSchemaUpdater.java
new file mode 100644
index 0000000..df95ff7
--- /dev/null
+++ b/java/com/google/gerrit/server/schema/NoteDbSchemaUpdater.java
@@ -0,0 +1,191 @@
+// 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.schema;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSortedMap;
+import com.google.common.collect.ImmutableSortedSet;
+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;
+import com.google.gerrit.server.notedb.Sequences;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.util.stream.IntStream;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Repository;
+
+public class NoteDbSchemaUpdater {
+  private final Config cfg;
+  private final AllUsersName allUsersName;
+  private final GitRepositoryManager repoManager;
+  private final SchemaCreator schemaCreator;
+  private final NoteDbSchemaVersionManager versionManager;
+  private final NoteDbSchemaVersion.Arguments args;
+  private final ImmutableSortedMap<Integer, Class<? extends NoteDbSchemaVersion>> schemaVersions;
+
+  @Inject
+  NoteDbSchemaUpdater(
+      @GerritServerConfig Config cfg,
+      AllUsersName allUsersName,
+      GitRepositoryManager repoManager,
+      SchemaCreator schemaCreator,
+      NoteDbSchemaVersionManager versionManager,
+      NoteDbSchemaVersion.Arguments args) {
+    this(
+        cfg,
+        allUsersName,
+        repoManager,
+        schemaCreator,
+        versionManager,
+        args,
+        NoteDbSchemaVersions.ALL);
+  }
+
+  NoteDbSchemaUpdater(
+      Config cfg,
+      AllUsersName allUsersName,
+      GitRepositoryManager repoManager,
+      SchemaCreator schemaCreator,
+      NoteDbSchemaVersionManager versionManager,
+      NoteDbSchemaVersion.Arguments args,
+      ImmutableSortedMap<Integer, Class<? extends NoteDbSchemaVersion>> schemaVersions) {
+    this.cfg = cfg;
+    this.allUsersName = allUsersName;
+    this.repoManager = repoManager;
+    this.schemaCreator = schemaCreator;
+    this.versionManager = versionManager;
+    this.args = args;
+    this.schemaVersions = schemaVersions;
+  }
+
+  public void update(UpdateUI ui) {
+    ensureSchemaCreated();
+
+    int currentVersion = versionManager.read();
+    if (currentVersion == 0) {
+      // The only valid case where there is no refs/meta/version is when running 3.x init for the
+      // first time on a site that previously ran init on 2.16. A freshly created 3.x site will have
+      // seeded refs/meta/version during AllProjectsCreator, so it won't hit this block.
+      checkNoteDbConfigFor216();
+    }
+
+    for (int nextVersion : requiredUpgrades(currentVersion, schemaVersions.keySet())) {
+      try {
+        ui.message(String.format("Migrating data to schema %d ...", nextVersion));
+        NoteDbSchemaVersions.get(schemaVersions, nextVersion).upgrade(args, ui);
+        versionManager.increment(nextVersion - 1);
+      } catch (Exception e) {
+        throw new StorageException(
+            String.format("Failed to upgrade to schema version %d", nextVersion), e);
+      }
+    }
+  }
+
+  private void ensureSchemaCreated() {
+    try {
+      schemaCreator.ensureCreated();
+    } catch (IOException | ConfigInvalidException e) {
+      throw new StorageException("Cannot initialize Gerrit site", e);
+    }
+  }
+
+  // Config#getEnum requires this to be public, so give it an off-putting name.
+  public enum PrimaryStorageFor216Compatibility {
+    REVIEW_DB,
+    NOTE_DB
+  }
+
+  private void checkNoteDbConfigFor216() {
+    // Check that the NoteDb migration config matches what we expect from a site that both:
+    // * Completed the change migration to NoteDB.
+    // * Ran schema upgrades from a 2.16 final release.
+
+    if (!cfg.getBoolean("noteDb", "changes", "write", false)
+        || !cfg.getBoolean("noteDb", "changes", "read", false)
+        || cfg.getEnum(
+                "noteDb", "changes", "primaryStorage", PrimaryStorageFor216Compatibility.REVIEW_DB)
+            != PrimaryStorageFor216Compatibility.NOTE_DB
+        || !cfg.getBoolean("noteDb", "changes", "disableReviewDb", false)) {
+      throw new StorageException(
+          "You appear to be upgrading from a 2.x site, but the NoteDb change migration was"
+              + " not completed. See documentation:\n"
+              + "https://gerrit-review.googlesource.com/Documentation/note-db.html#migration");
+    }
+
+    // We don't have a direct way to check that 2.16 init was run; the most obvious side effect
+    // would be upgrading the *ReviewDb* schema to the latest 2.16 schema version. But in 3.x we can
+    // no longer access ReviewDb, so we can't check that directly.
+    //
+    // Instead, check for a NoteDb-specific side effect of the migration process: the presence of
+    // the NoteDb group sequence ref. This is created by the schema 163 migration, which was part of
+    // 2.16 and not 2.15.
+    //
+    // There are a few corner cases where we will proceed even if the schema is not fully up to
+    // date:
+    //  * If a user happened to run init from master after schema 163 was added but before 2.16
+    //    final. We assume that someone savvy enough to do that has followed the documented
+    //    requirement of upgrading to 2.16 final before 3.0.
+    //  * If a user ran init in 2.16.x and the upgrade to 163 succeeded but a later update failed.
+    //    In this case the server literally will not start under 2.16. We assume the user will fix
+    //    this and get 2.16 running rather than abandoning 2.16 and jumping to 3.0 at this point.
+    try (Repository allUsers = repoManager.openRepository(allUsersName)) {
+      if (allUsers.exactRef(RefNames.REFS_SEQUENCES + Sequences.NAME_GROUPS) == null) {
+        throw new StorageException(
+            "You appear to be upgrading to 3.x from a version prior to 2.16; you must upgrade to"
+                + " 2.16.x first");
+      }
+    } catch (IOException e) {
+      throw new StorageException("Failed to check NoteDb migration state", e);
+    }
+  }
+
+  @VisibleForTesting
+  static ImmutableList<Integer> requiredUpgrades(
+      int currentVersion, ImmutableSortedSet<Integer> allVersions) {
+    int firstVersion = allVersions.first();
+    int latestVersion = allVersions.last();
+    if (currentVersion == latestVersion) {
+      return ImmutableList.of();
+    } else if (currentVersion > latestVersion) {
+      throw new StorageException(
+          String.format(
+              "Cannot downgrade NoteDb schema from version %d to %d",
+              currentVersion, latestVersion));
+    }
+
+    int firstUpgradeVersion;
+    if (currentVersion == 0) {
+      // Bootstrap NoteDb version to minimum supported schema number.
+      firstUpgradeVersion = firstVersion;
+    } else {
+      if (currentVersion < firstVersion - 1) {
+        throw new StorageException(
+            String.format(
+                "Cannot skip NoteDb schema from version %d to %d", currentVersion, firstVersion));
+      }
+      firstUpgradeVersion = currentVersion + 1;
+    }
+    return IntStream.rangeClosed(firstUpgradeVersion, latestVersion)
+        .boxed()
+        .collect(toImmutableList());
+  }
+}
diff --git a/java/com/google/gerrit/server/schema/NoteDbSchemaVersion.java b/java/com/google/gerrit/server/schema/NoteDbSchemaVersion.java
new file mode 100644
index 0000000..b6a7a1c
--- /dev/null
+++ b/java/com/google/gerrit/server/schema/NoteDbSchemaVersion.java
@@ -0,0 +1,46 @@
+// 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.schema;
+
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+/**
+ * Schema upgrade implementation.
+ *
+ * <p>Implementations must have a single non-private constructor with no arguments (e.g. the default
+ * constructor).
+ */
+interface NoteDbSchemaVersion {
+  @Singleton
+  class Arguments {
+    final GitRepositoryManager repoManager;
+    final AllProjectsName allProjects;
+    final AllUsersName allUsers;
+
+    @Inject
+    Arguments(
+        GitRepositoryManager repoManager, AllProjectsName allProjects, AllUsersName allUsers) {
+      this.repoManager = repoManager;
+      this.allProjects = allProjects;
+      this.allUsers = allUsers;
+    }
+  }
+
+  void upgrade(Arguments args, UpdateUI ui) throws Exception;
+}
diff --git a/java/com/google/gerrit/server/schema/NoteDbSchemaVersionCheck.java b/java/com/google/gerrit/server/schema/NoteDbSchemaVersionCheck.java
new file mode 100644
index 0000000..33534fc
--- /dev/null
+++ b/java/com/google/gerrit/server/schema/NoteDbSchemaVersionCheck.java
@@ -0,0 +1,77 @@
+// 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.schema;
+
+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.SitePaths;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.ProvisionException;
+
+public class NoteDbSchemaVersionCheck implements LifecycleListener {
+  public static Module module() {
+    return new LifecycleModule() {
+      @Override
+      protected void configure() {
+        listener().to(NoteDbSchemaVersionCheck.class);
+      }
+    };
+  }
+
+  private final NoteDbSchemaVersionManager versionManager;
+  private final SitePaths sitePaths;
+
+  @Inject
+  NoteDbSchemaVersionCheck(NoteDbSchemaVersionManager versionManager, SitePaths sitePaths) {
+    this.versionManager = versionManager;
+    this.sitePaths = sitePaths;
+  }
+
+  @Override
+  public void start() {
+    try {
+      int current = versionManager.read();
+      if (current == 0) {
+        throw new ProvisionException(
+            String.format(
+                "Schema not yet initialized. Run init to initialize the schema:\n"
+                    + "$ java -jar gerrit.war init -d %s",
+                sitePaths.site_path.toAbsolutePath()));
+      }
+      int expected = NoteDbSchemaVersions.LATEST;
+      if (current != expected) {
+        String advice =
+            current > expected
+                ? "Downgrade is not supported"
+                : String.format(
+                    "Run init to upgrade:\n$ java -jar %s init -d %s",
+                    sitePaths.gerrit_war.toAbsolutePath(), sitePaths.site_path.toAbsolutePath());
+        throw new ProvisionException(
+            String.format(
+                "Unsupported schema version %d; expected schema version %d. %s",
+                current, expected, advice));
+      }
+    } catch (StorageException e) {
+      throw new ProvisionException("Failed to read NoteDb schema version", e);
+    }
+  }
+
+  @Override
+  public void stop() {
+    // Do nothing.
+  }
+}
diff --git a/java/com/google/gerrit/server/schema/NoteDbSchemaVersionManager.java b/java/com/google/gerrit/server/schema/NoteDbSchemaVersionManager.java
new file mode 100644
index 0000000..7ff0ea6
--- /dev/null
+++ b/java/com/google/gerrit/server/schema/NoteDbSchemaVersionManager.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.schema;
+
+import static com.google.gerrit.reviewdb.client.RefNames.REFS_VERSION;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.notedb.IntBlob;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.util.Optional;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+public class NoteDbSchemaVersionManager {
+  private final AllProjectsName allProjectsName;
+  private final GitRepositoryManager repoManager;
+
+  @Inject
+  @VisibleForTesting
+  public NoteDbSchemaVersionManager(
+      AllProjectsName allProjectsName, GitRepositoryManager repoManager) {
+    // Can't inject GitReferenceUpdated here because it has dependencies that are not always
+    // available in this injector (e.g. during init). This is ok for now since no other ref updates
+    // during init are available to plugins, and there are not any other use cases for listening for
+    // updates to the version ref.
+    this.allProjectsName = allProjectsName;
+    this.repoManager = repoManager;
+  }
+
+  public int read() {
+    try (Repository repo = repoManager.openRepository(allProjectsName)) {
+      return IntBlob.parse(repo, REFS_VERSION).map(IntBlob::value).orElse(0);
+    } catch (IOException e) {
+      throw new StorageException("Failed to read " + REFS_VERSION, e);
+    }
+  }
+
+  public void init() throws IOException {
+    try (Repository repo = repoManager.openRepository(allProjectsName);
+        RevWalk rw = new RevWalk(repo)) {
+      Optional<IntBlob> old = IntBlob.parse(repo, REFS_VERSION, rw);
+      if (old.isPresent()) {
+        throw new StorageException(
+            String.format(
+                "Expected no old version for %s, found %s", REFS_VERSION, old.get().value()));
+      }
+      IntBlob.store(
+          repo,
+          rw,
+          allProjectsName,
+          REFS_VERSION,
+          old.map(IntBlob::id).orElse(ObjectId.zeroId()),
+          NoteDbSchemaVersions.LATEST,
+          GitReferenceUpdated.DISABLED);
+    }
+  }
+
+  public void increment(int expectedOldVersion) throws IOException {
+    try (Repository repo = repoManager.openRepository(allProjectsName);
+        RevWalk rw = new RevWalk(repo)) {
+      Optional<IntBlob> old = IntBlob.parse(repo, REFS_VERSION, rw);
+      if (old.isPresent() && old.get().value() != expectedOldVersion) {
+        throw new StorageException(
+            String.format(
+                "Expected old version %d for %s, found %d",
+                expectedOldVersion, REFS_VERSION, old.get().value()));
+      }
+      IntBlob.store(
+          repo,
+          rw,
+          allProjectsName,
+          REFS_VERSION,
+          old.map(IntBlob::id).orElse(ObjectId.zeroId()),
+          expectedOldVersion + 1,
+          GitReferenceUpdated.DISABLED);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/schema/NoteDbSchemaVersions.java b/java/com/google/gerrit/server/schema/NoteDbSchemaVersions.java
new file mode 100644
index 0000000..02250f2
--- /dev/null
+++ b/java/com/google/gerrit/server/schema/NoteDbSchemaVersions.java
@@ -0,0 +1,60 @@
+// 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.schema;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.ImmutableSortedMap.toImmutableSortedMap;
+import static java.util.Comparator.naturalOrder;
+
+import com.google.common.collect.ImmutableSortedMap;
+import com.google.common.primitives.Ints;
+import com.google.gerrit.common.UsedAt;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+public class NoteDbSchemaVersions {
+  static final ImmutableSortedMap<Integer, Class<? extends NoteDbSchemaVersion>> ALL =
+      // List all supported NoteDb schema versions here.
+      Stream.of(Schema_180.class, Schema_181.class)
+          .collect(toImmutableSortedMap(naturalOrder(), v -> guessVersion(v).get(), v -> v));
+
+  public static final int FIRST = ALL.firstKey();
+  public static final int LATEST = ALL.lastKey();
+
+  // TODO(dborowitz): Migrate delete-project plugin to use this implementation.
+  @UsedAt(UsedAt.Project.PLUGIN_DELETE_PROJECT)
+  public static Optional<Integer> guessVersion(Class<?> c) {
+    String prefix = "Schema_";
+    if (!c.getSimpleName().startsWith(prefix)) {
+      return Optional.empty();
+    }
+    return Optional.ofNullable(Ints.tryParse(c.getSimpleName().substring(prefix.length())));
+  }
+
+  public static NoteDbSchemaVersion get(
+      ImmutableSortedMap<Integer, Class<? extends NoteDbSchemaVersion>> schemaVersions, int i) {
+    Class<? extends NoteDbSchemaVersion> clazz = schemaVersions.get(i);
+    checkArgument(clazz != null, "Schema version not found: %s", i);
+    try {
+      return clazz.getDeclaredConstructor().newInstance();
+    } catch (InstantiationException
+        | IllegalAccessException
+        | NoSuchMethodException
+        | InvocationTargetException e) {
+      throw new IllegalStateException("failed to invoke constructor on " + clazz.getName(), e);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/schema/NotesMigrationSchemaFactory.java b/java/com/google/gerrit/server/schema/NotesMigrationSchemaFactory.java
deleted file mode 100644
index 0d95610..0000000
--- a/java/com/google/gerrit/server/schema/NotesMigrationSchemaFactory.java
+++ /dev/null
@@ -1,78 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.server.DisallowReadFromChangesReviewDbWrapper;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.notedb.NotesMigration;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-
-@Singleton
-public class NotesMigrationSchemaFactory implements SchemaFactory<ReviewDb> {
-  private final SchemaFactory<ReviewDb> delegate;
-  private final NotesMigration migration;
-
-  @Inject
-  NotesMigrationSchemaFactory(
-      @ReviewDbFactory SchemaFactory<ReviewDb> delegate, NotesMigration migration) {
-    this.delegate = delegate;
-    this.migration = migration;
-  }
-
-  @Override
-  public ReviewDb open() throws OrmException {
-    // There are two levels at which this class disables access to Changes and related tables,
-    // corresponding to two phases of the NoteDb migration:
-    //
-    // 1. When changes are read from NoteDb but some changes might still have their primary storage
-    //    in ReviewDb, it is generally programmer error to read changes from ReviewDb. However,
-    //    since ReviewDb is still the primary storage for most or all changes, we still need to
-    //    support writing to ReviewDb. This behavior is accomplished by wrapping in a
-    //    DisallowReadFromChangesReviewDbWrapper.
-    //
-    //    Some codepaths might need to be able to read from ReviewDb if they really need to,
-    //    because they need to operate on the underlying source of truth, for example when reading
-    //    a change to determine its primary storage. To support this, ReviewDbUtil#unwrapDb can
-    //    detect and unwrap databases of this type.
-    //
-    // 2. After all changes have their primary storage in NoteDb, we can completely shut off access
-    //    to the change tables. At this point in the migration, we are by definition not using the
-    //    ReviewDb tables at all; we could even delete the tables at this point, and Gerrit would
-    //    continue to function.
-    //
-    //    This is accomplished by setting the delegate ReviewDb *underneath*
-    //    DisallowReadFromChanges to be a complete no-op, with NoChangesReviewDbWrapper. With this
-    //    wrapper, all read operations return no results, and write operations silently do nothing.
-    //    This wrapper is not a public class and nobody should ever attempt to unwrap it.
-
-    // First create the wrappers which can not be removed by ReviewDbUtil#unwrapDb(ReviewDb).
-    ReviewDb db = delegate.open();
-    if (migration.readChanges() && migration.disableChangeReviewDb()) {
-      // Disable writes to change tables in ReviewDb (ReviewDb access for changes are No-Ops).
-      db = new NoChangesReviewDbWrapper(db);
-    }
-
-    // Second create the wrappers which can be removed by ReviewDbUtil#unwrapDb(ReviewDb).
-    if (migration.readChanges()) {
-      // If reading changes from NoteDb is configured, changes should not be read from ReviewDb.
-      // Make sure that any attempt to read a change from ReviewDb anyway fails with an exception.
-      db = new DisallowReadFromChangesReviewDbWrapper(db);
-    }
-    return db;
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Oracle.java b/java/com/google/gerrit/server/schema/Oracle.java
deleted file mode 100644
index 4ff7243..0000000
--- a/java/com/google/gerrit/server/schema/Oracle.java
+++ /dev/null
@@ -1,50 +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.schema;
-
-import static com.google.gerrit.server.schema.JdbcUtil.hostname;
-import static com.google.gerrit.server.schema.JdbcUtil.port;
-
-import com.google.gerrit.server.config.ConfigSection;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.inject.Inject;
-import org.eclipse.jgit.lib.Config;
-
-public class Oracle extends BaseDataSourceType {
-  private Config cfg;
-
-  @Inject
-  public Oracle(@GerritServerConfig Config cfg) {
-    super("oracle.jdbc.driver.OracleDriver");
-    this.cfg = cfg;
-  }
-
-  @Override
-  public String getUrl() {
-    final StringBuilder b = new StringBuilder();
-    final ConfigSection dbc = new ConfigSection(cfg, "database");
-    b.append("jdbc:oracle:thin:@");
-    b.append(hostname(dbc.optional("hostname")));
-    b.append(port(dbc.optional("port")));
-    b.append(":");
-    b.append(dbc.required("instance"));
-    return b.toString();
-  }
-
-  @Override
-  public String getValidationQuery() {
-    return "select 1 from dual";
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/PostgreSQL.java b/java/com/google/gerrit/server/schema/PostgreSQL.java
deleted file mode 100644
index d6aee94..0000000
--- a/java/com/google/gerrit/server/schema/PostgreSQL.java
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import static com.google.gerrit.server.schema.JdbcUtil.hostname;
-import static com.google.gerrit.server.schema.JdbcUtil.port;
-
-import com.google.gerrit.server.config.ConfigSection;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.inject.Inject;
-import java.io.IOException;
-import org.eclipse.jgit.lib.Config;
-
-class PostgreSQL extends BaseDataSourceType {
-
-  private Config cfg;
-
-  @Inject
-  PostgreSQL(@GerritServerConfig Config cfg) {
-    super("org.postgresql.Driver");
-    this.cfg = cfg;
-  }
-
-  @Override
-  public String getUrl() {
-    final StringBuilder b = new StringBuilder();
-    final ConfigSection dbc = new ConfigSection(cfg, "database");
-    b.append("jdbc:postgresql://");
-    b.append(hostname(dbc.optional("hostname")));
-    b.append(port(dbc.optional("port")));
-    b.append("/");
-    b.append(dbc.required("database"));
-    return b.toString();
-  }
-
-  @Override
-  public ScriptRunner getIndexScript() throws IOException {
-    return getScriptRunner("index_postgres.sql");
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/PostgresqlAccountPatchReviewStore.java b/java/com/google/gerrit/server/schema/PostgresqlAccountPatchReviewStore.java
index 34f7dba..db68f2e 100644
--- a/java/com/google/gerrit/server/schema/PostgresqlAccountPatchReviewStore.java
+++ b/java/com/google/gerrit/server/schema/PostgresqlAccountPatchReviewStore.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.server.schema;
 
+import com.google.gerrit.exceptions.DuplicateKeyException;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.config.ThreadSettingsConfig;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.sql.SQLException;
@@ -36,10 +36,10 @@
   }
 
   @Override
-  public OrmException convertError(String op, SQLException err) {
+  public StorageException convertError(String op, SQLException err) {
     switch (getSQLStateInt(err)) {
       case 23505: // DUPLICATE_KEY_1
-        return new OrmDuplicateKeyException("ACCOUNT_PATCH_REVIEWS", err);
+        return new DuplicateKeyException("ACCOUNT_PATCH_REVIEWS", err);
 
       case 23514: // CHECK CONSTRAINT VIOLATION
       case 23503: // FOREIGN KEY CONSTRAINT VIOLATION
@@ -49,7 +49,7 @@
         if (err.getCause() == null && err.getNextException() != null) {
           err.initCause(err.getNextException());
         }
-        return new OrmException(op + " failure on ACCOUNT_PATCH_REVIEWS", err);
+        return new StorageException(op + " failure on ACCOUNT_PATCH_REVIEWS", err);
     }
   }
 }
diff --git a/java/com/google/gerrit/server/schema/ProjectConfigSchemaUpdate.java b/java/com/google/gerrit/server/schema/ProjectConfigSchemaUpdate.java
index c25b846..9e12807 100644
--- a/java/com/google/gerrit/server/schema/ProjectConfigSchemaUpdate.java
+++ b/java/com/google/gerrit/server/schema/ProjectConfigSchemaUpdate.java
@@ -17,12 +17,17 @@
 import static com.google.gerrit.server.project.ProjectConfig.ACCESS;
 import static java.util.stream.Collectors.toList;
 
+import com.google.common.annotations.VisibleForTesting;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.PermissionRule;
+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;
 import com.google.gerrit.server.git.meta.VersionedMetaData;
 import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.List;
@@ -31,22 +36,38 @@
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.StoredConfig;
 
 public class ProjectConfigSchemaUpdate extends VersionedMetaData {
+  public static class Factory {
+    private final SitePaths sitePaths;
+    private final AllProjectsName allProjectsName;
+
+    @Inject
+    Factory(SitePaths sitePaths, AllProjectsName allProjectsName) {
+      this.sitePaths = sitePaths;
+      this.allProjectsName = allProjectsName;
+    }
+
+    ProjectConfigSchemaUpdate read(MetaDataUpdate update)
+        throws IOException, ConfigInvalidException {
+      ProjectConfigSchemaUpdate r =
+          new ProjectConfigSchemaUpdate(
+              update,
+              ProjectConfig.Factory.getBaseConfig(sitePaths, allProjectsName, allProjectsName));
+      r.load(update);
+      return r;
+    }
+  }
 
   private final MetaDataUpdate update;
+  @Nullable private final StoredConfig baseConfig;
   private Config config;
   private boolean updated;
 
-  public static ProjectConfigSchemaUpdate read(MetaDataUpdate update)
-      throws IOException, ConfigInvalidException {
-    ProjectConfigSchemaUpdate r = new ProjectConfigSchemaUpdate(update);
-    r.load(update);
-    return r;
-  }
-
-  private ProjectConfigSchemaUpdate(MetaDataUpdate update) {
+  private ProjectConfigSchemaUpdate(MetaDataUpdate update, @Nullable StoredConfig baseConfig) {
     this.update = update;
+    this.baseConfig = baseConfig;
   }
 
   @Override
@@ -56,7 +77,15 @@
 
   @Override
   protected void onLoad() throws IOException, ConfigInvalidException {
-    config = readConfig(ProjectConfig.PROJECT_CONFIG);
+    if (baseConfig != null) {
+      baseConfig.load();
+    }
+    config = readConfig(ProjectConfig.PROJECT_CONFIG, baseConfig);
+  }
+
+  @VisibleForTesting
+  Config getConfig() {
+    return config;
   }
 
   public void removeForceFromPermission(String name) {
@@ -86,7 +115,7 @@
     return true;
   }
 
-  public void save(PersonIdent personIdent, String commitMessage) throws OrmException {
+  public void save(PersonIdent personIdent, String commitMessage) {
     if (!updated) {
       return;
     }
@@ -97,7 +126,7 @@
     try {
       commit(update);
     } catch (IOException e) {
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
   }
 
diff --git a/java/com/google/gerrit/server/schema/ReviewDbDatabaseProvider.java b/java/com/google/gerrit/server/schema/ReviewDbDatabaseProvider.java
deleted file mode 100644
index 0fbaeca..0000000
--- a/java/com/google/gerrit/server/schema/ReviewDbDatabaseProvider.java
+++ /dev/null
@@ -1,43 +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.server.schema;
-
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.jdbc.Database;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
-import com.google.inject.name.Named;
-import javax.sql.DataSource;
-
-/** Provides the {@code Database<ReviewDb>} database handle. */
-final class ReviewDbDatabaseProvider implements Provider<Database<ReviewDb>> {
-  private final DataSource datasource;
-
-  @Inject
-  ReviewDbDatabaseProvider(@Named("ReviewDb") final DataSource ds) {
-    datasource = ds;
-  }
-
-  @Override
-  public Database<ReviewDb> get() {
-    try {
-      return new Database<>(datasource, ReviewDb.class);
-    } catch (OrmException e) {
-      throw new ProvisionException("Cannot create ReviewDb", e);
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/ReviewDbFactory.java b/java/com/google/gerrit/server/schema/ReviewDbFactory.java
deleted file mode 100644
index 86f5d06..0000000
--- a/java/com/google/gerrit/server/schema/ReviewDbFactory.java
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import static java.lang.annotation.RetentionPolicy.RUNTIME;
-
-import com.google.inject.BindingAnnotation;
-import java.lang.annotation.Retention;
-
-/**
- * Marker on {@link com.google.gwtorm.server.SchemaFactory} implementation that talks to the
- * underlying traditional {@link com.google.gerrit.reviewdb.server.ReviewDb} database.
- *
- * <p>During the migration to NoteDb, the actual {@code ReviewDb} will be a wrapper with certain
- * tables enabled/disabled; this marker goes on the low-level implementation that has all tables.
- */
-@Retention(RUNTIME)
-@BindingAnnotation
-public @interface ReviewDbFactory {}
diff --git a/java/com/google/gerrit/server/schema/SchemaCreator.java b/java/com/google/gerrit/server/schema/SchemaCreator.java
index 13734c3..b78ce73 100644
--- a/java/com/google/gerrit/server/schema/SchemaCreator.java
+++ b/java/com/google/gerrit/server/schema/SchemaCreator.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2009 The Android Open Source Project
+// 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.
@@ -14,262 +14,29 @@
 
 package com.google.gerrit.server.schema;
 
-import com.google.common.collect.ImmutableSet;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.metrics.MetricMaker;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.Sequences;
-import com.google.gerrit.server.account.GroupUUID;
-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.config.GerritServerId;
-import com.google.gerrit.server.config.SitePath;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.meta.MetaDataUpdate;
-import com.google.gerrit.server.group.InternalGroup;
-import com.google.gerrit.server.group.db.AuditLogFormatter;
-import com.google.gerrit.server.group.db.GroupConfig;
-import com.google.gerrit.server.group.db.GroupNameNotes;
-import com.google.gerrit.server.group.db.InternalGroupCreation;
-import com.google.gerrit.server.group.db.InternalGroupUpdate;
-import com.google.gerrit.server.index.group.GroupIndex;
-import com.google.gerrit.server.index.group.GroupIndexCollection;
-import com.google.gerrit.server.notedb.NotesMigration;
-import com.google.gerrit.server.update.RefUpdateUtil;
-import com.google.gwtorm.jdbc.JdbcExecutor;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
 import java.io.IOException;
-import java.nio.file.Path;
-import java.util.Collections;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.BatchRefUpdate;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
 
-/** Creates the current database schema and populates initial code rows. */
-public class SchemaCreator {
-  @SitePath private final Path site_path;
+/** Populates initial NoteDb schema, {@code All-Projects} configuration, etc. */
+public interface SchemaCreator {
 
-  private final GitRepositoryManager repoManager;
-  private final AllProjectsCreator allProjectsCreator;
-  private final AllUsersCreator allUsersCreator;
-  private final AllUsersName allUsersName;
-  private final PersonIdent serverUser;
-  private final DataSourceType dataSourceType;
-  private final GroupIndexCollection indexCollection;
-  private final String serverId;
+  /**
+   * Create the schema, assuming it does not already exist.
+   *
+   * <p>Fails if the schema does exist.
+   *
+   * @throws IOException an error occurred.
+   * @throws ConfigInvalidException an error occurred.
+   */
+  void create() throws IOException, ConfigInvalidException;
 
-  private final Config config;
-  private final MetricMaker metricMaker;
-  private final NotesMigration migration;
-  private final AllProjectsName allProjectsName;
-
-  @Inject
-  public SchemaCreator(
-      SitePaths site,
-      GitRepositoryManager repoManager,
-      AllProjectsCreator ap,
-      AllUsersCreator auc,
-      AllUsersName allUsersName,
-      @GerritPersonIdent PersonIdent au,
-      DataSourceType dst,
-      GroupIndexCollection ic,
-      @GerritServerId String serverId,
-      @GerritServerConfig Config config,
-      MetricMaker metricMaker,
-      NotesMigration migration,
-      AllProjectsName apName) {
-    this(
-        site.site_path,
-        repoManager,
-        ap,
-        auc,
-        allUsersName,
-        au,
-        dst,
-        ic,
-        serverId,
-        config,
-        metricMaker,
-        migration,
-        apName);
-  }
-
-  public SchemaCreator(
-      @SitePath Path site,
-      GitRepositoryManager repoManager,
-      AllProjectsCreator ap,
-      AllUsersCreator auc,
-      AllUsersName allUsersName,
-      @GerritPersonIdent PersonIdent au,
-      DataSourceType dst,
-      GroupIndexCollection ic,
-      String serverId,
-      Config config,
-      MetricMaker metricMaker,
-      NotesMigration migration,
-      AllProjectsName apName) {
-    site_path = site;
-    this.repoManager = repoManager;
-    allProjectsCreator = ap;
-    allUsersCreator = auc;
-    this.allUsersName = allUsersName;
-    serverUser = au;
-    dataSourceType = dst;
-    indexCollection = ic;
-    this.serverId = serverId;
-
-    this.config = config;
-    this.allProjectsName = apName;
-    this.migration = migration;
-    this.metricMaker = metricMaker;
-  }
-
-  public void create(ReviewDb db) throws OrmException, IOException, ConfigInvalidException {
-    final JdbcSchema jdbc = (JdbcSchema) db;
-    try (JdbcExecutor e = new JdbcExecutor(jdbc)) {
-      jdbc.updateSchema(e);
-    }
-
-    final CurrentSchemaVersion sVer = CurrentSchemaVersion.create();
-    sVer.versionNbr = SchemaVersion.getBinaryVersion();
-    db.schemaVersion().insert(Collections.singleton(sVer));
-
-    GroupReference admins = createGroupReference("Administrators");
-    GroupReference batchUsers = createGroupReference("Non-Interactive Users");
-
-    allProjectsCreator.setAdministrators(admins).setBatchUsers(batchUsers).create();
-    // We have to create the All-Users repository before we can use it to store the groups in it.
-    allUsersCreator.setAdministrators(admins).create();
-
-    // Don't rely on injection to construct Sequences, as it requires ReviewDb.
-    Sequences seqs =
-        new Sequences(
-            config,
-            () -> db,
-            migration,
-            repoManager,
-            GitReferenceUpdated.DISABLED,
-            allProjectsName,
-            allUsersName,
-            metricMaker);
-    try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
-      createAdminsGroup(seqs, allUsersRepo, admins);
-      createBatchUsersGroup(seqs, allUsersRepo, batchUsers, admins.getUUID());
-    }
-
-    dataSourceType.getIndexScript().run(db);
-  }
-
-  private void createAdminsGroup(
-      Sequences seqs, Repository allUsersRepo, GroupReference groupReference)
-      throws OrmException, IOException, ConfigInvalidException {
-    InternalGroupCreation groupCreation = getGroupCreation(seqs, groupReference);
-    InternalGroupUpdate groupUpdate =
-        InternalGroupUpdate.builder().setDescription("Gerrit Site Administrators").build();
-
-    createGroup(allUsersRepo, groupCreation, groupUpdate);
-  }
-
-  private void createBatchUsersGroup(
-      Sequences seqs,
-      Repository allUsersRepo,
-      GroupReference groupReference,
-      AccountGroup.UUID adminsGroupUuid)
-      throws OrmException, IOException, ConfigInvalidException {
-    InternalGroupCreation groupCreation = getGroupCreation(seqs, groupReference);
-    InternalGroupUpdate groupUpdate =
-        InternalGroupUpdate.builder()
-            .setDescription("Users who perform batch actions on Gerrit")
-            .setOwnerGroupUUID(adminsGroupUuid)
-            .build();
-
-    createGroup(allUsersRepo, groupCreation, groupUpdate);
-  }
-
-  private void createGroup(
-      Repository allUsersRepo, InternalGroupCreation groupCreation, InternalGroupUpdate groupUpdate)
-      throws OrmException, ConfigInvalidException, IOException {
-    InternalGroup createdGroup = createGroupInNoteDb(allUsersRepo, groupCreation, groupUpdate);
-    index(createdGroup);
-  }
-
-  private InternalGroup createGroupInNoteDb(
-      Repository allUsersRepo, InternalGroupCreation groupCreation, InternalGroupUpdate groupUpdate)
-      throws ConfigInvalidException, IOException, OrmDuplicateKeyException {
-    // This method is only executed on a new server which doesn't have any accounts or groups.
-    AuditLogFormatter auditLogFormatter =
-        AuditLogFormatter.createBackedBy(ImmutableSet.of(), ImmutableSet.of(), serverId);
-
-    GroupConfig groupConfig =
-        GroupConfig.createForNewGroup(allUsersName, allUsersRepo, groupCreation);
-    groupConfig.setGroupUpdate(groupUpdate, auditLogFormatter);
-
-    AccountGroup.NameKey groupName = groupUpdate.getName().orElseGet(groupCreation::getNameKey);
-    GroupNameNotes groupNameNotes =
-        GroupNameNotes.forNewGroup(
-            allUsersName, allUsersRepo, groupCreation.getGroupUUID(), groupName);
-
-    commit(allUsersRepo, groupConfig, groupNameNotes);
-
-    return groupConfig
-        .getLoadedGroup()
-        .orElseThrow(() -> new IllegalStateException("Created group wasn't automatically loaded"));
-  }
-
-  private void commit(
-      Repository allUsersRepo, GroupConfig groupConfig, GroupNameNotes groupNameNotes)
-      throws IOException {
-    BatchRefUpdate batchRefUpdate = allUsersRepo.getRefDatabase().newBatchUpdate();
-    try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate(allUsersRepo, batchRefUpdate)) {
-      groupConfig.commit(metaDataUpdate);
-    }
-    // MetaDataUpdates unfortunately can't be reused. -> Create a new one.
-    try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate(allUsersRepo, batchRefUpdate)) {
-      groupNameNotes.commit(metaDataUpdate);
-    }
-    RefUpdateUtil.executeChecked(batchRefUpdate, allUsersRepo);
-  }
-
-  private MetaDataUpdate createMetaDataUpdate(
-      Repository allUsersRepo, @Nullable BatchRefUpdate batchRefUpdate) {
-    MetaDataUpdate metaDataUpdate =
-        new MetaDataUpdate(
-            GitReferenceUpdated.DISABLED, allUsersName, allUsersRepo, batchRefUpdate);
-    metaDataUpdate.getCommitBuilder().setAuthor(serverUser);
-    metaDataUpdate.getCommitBuilder().setCommitter(serverUser);
-    return metaDataUpdate;
-  }
-
-  private void index(InternalGroup group) throws IOException {
-    for (GroupIndex groupIndex : indexCollection.getWriteIndexes()) {
-      groupIndex.replace(group);
-    }
-  }
-
-  private GroupReference createGroupReference(String name) {
-    AccountGroup.UUID groupUuid = GroupUUID.make(name, serverUser);
-    return new GroupReference(groupUuid, name);
-  }
-
-  private InternalGroupCreation getGroupCreation(Sequences seqs, GroupReference groupReference)
-      throws OrmException {
-    int next = seqs.nextGroupId();
-    return InternalGroupCreation.builder()
-        .setNameKey(new AccountGroup.NameKey(groupReference.getName()))
-        .setId(new AccountGroup.Id(next))
-        .setGroupUUID(groupReference.getUUID())
-        .build();
-  }
+  /**
+   * Create the schema only if it does not already exist.
+   *
+   * <p>Succeeds if the schema does exist.
+   *
+   * @throws IOException an error occurred.
+   * @throws ConfigInvalidException an error occurred.
+   */
+  void ensureCreated() throws IOException, ConfigInvalidException;
 }
diff --git a/java/com/google/gerrit/server/schema/SchemaCreatorImpl.java b/java/com/google/gerrit/server/schema/SchemaCreatorImpl.java
new file mode 100644
index 0000000..0a5823a
--- /dev/null
+++ b/java/com/google/gerrit/server/schema/SchemaCreatorImpl.java
@@ -0,0 +1,226 @@
+// 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.server.schema;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.data.GroupReference;
+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;
+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.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.group.InternalGroup;
+import com.google.gerrit.server.group.db.AuditLogFormatter;
+import com.google.gerrit.server.group.db.GroupConfig;
+import com.google.gerrit.server.group.db.GroupNameNotes;
+import com.google.gerrit.server.group.db.InternalGroupCreation;
+import com.google.gerrit.server.group.db.InternalGroupUpdate;
+import com.google.gerrit.server.index.group.GroupIndex;
+import com.google.gerrit.server.index.group.GroupIndexCollection;
+import com.google.gerrit.server.notedb.Sequences;
+import com.google.inject.Inject;
+import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+
+// TODO(dborowitz): The current NoteDb implementation mirrors the old ReviewDb code: this class is
+// called to create the site early on in NoteDbSchemaUpdater#update. This logic is a little
+// confusing and could stand to be reworked. Another smell is that this is an interface only for
+// testing purposes.
+public class SchemaCreatorImpl implements SchemaCreator {
+  private final GitRepositoryManager repoManager;
+  private final AllProjectsCreator allProjectsCreator;
+  private final AllUsersCreator allUsersCreator;
+  private final AllUsersName allUsersName;
+  private final PersonIdent serverUser;
+  private final GroupIndexCollection indexCollection;
+  private final String serverId;
+
+  private final Config config;
+  private final MetricMaker metricMaker;
+  private final AllProjectsName allProjectsName;
+
+  @Inject
+  public SchemaCreatorImpl(
+      GitRepositoryManager repoManager,
+      AllProjectsCreator ap,
+      AllUsersCreator auc,
+      AllUsersName allUsersName,
+      @GerritPersonIdent PersonIdent au,
+      GroupIndexCollection ic,
+      String serverId,
+      Config config,
+      MetricMaker metricMaker,
+      AllProjectsName apName) {
+    this.repoManager = repoManager;
+    allProjectsCreator = ap;
+    allUsersCreator = auc;
+    this.allUsersName = allUsersName;
+    serverUser = au;
+    indexCollection = ic;
+    this.serverId = serverId;
+
+    this.config = config;
+    this.allProjectsName = apName;
+    this.metricMaker = metricMaker;
+  }
+
+  @Override
+  public void create() throws IOException, ConfigInvalidException {
+    GroupReference admins = createGroupReference("Administrators");
+    GroupReference batchUsers = createGroupReference("Non-Interactive Users");
+
+    AllProjectsInput allProjectsInput =
+        AllProjectsInput.builder().administratorsGroup(admins).batchUsersGroup(batchUsers).build();
+    allProjectsCreator.create(allProjectsInput);
+    // We have to create the All-Users repository before we can use it to store the groups in it.
+    allUsersCreator.setAdministrators(admins).create();
+
+    // Don't rely on injection to construct Sequences, as the default GitReferenceUpdated has a
+    // thick dependency stack which may not all be available at schema creation time.
+    Sequences seqs =
+        new Sequences(
+            config,
+            repoManager,
+            GitReferenceUpdated.DISABLED,
+            allProjectsName,
+            allUsersName,
+            metricMaker);
+    try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
+      createAdminsGroup(seqs, allUsersRepo, admins);
+      createBatchUsersGroup(seqs, allUsersRepo, batchUsers, admins.getUUID());
+    }
+  }
+
+  @Override
+  public void ensureCreated() throws IOException, ConfigInvalidException {
+    try {
+      repoManager.openRepository(allProjectsName).close();
+    } catch (RepositoryNotFoundException e) {
+      create();
+    }
+  }
+
+  private void createAdminsGroup(
+      Sequences seqs, Repository allUsersRepo, GroupReference groupReference)
+      throws IOException, ConfigInvalidException {
+    InternalGroupCreation groupCreation = getGroupCreation(seqs, groupReference);
+    InternalGroupUpdate groupUpdate =
+        InternalGroupUpdate.builder().setDescription("Gerrit Site Administrators").build();
+
+    createGroup(allUsersRepo, groupCreation, groupUpdate);
+  }
+
+  private void createBatchUsersGroup(
+      Sequences seqs,
+      Repository allUsersRepo,
+      GroupReference groupReference,
+      AccountGroup.UUID adminsGroupUuid)
+      throws IOException, ConfigInvalidException {
+    InternalGroupCreation groupCreation = getGroupCreation(seqs, groupReference);
+    InternalGroupUpdate groupUpdate =
+        InternalGroupUpdate.builder()
+            .setDescription("Users who perform batch actions on Gerrit")
+            .setOwnerGroupUUID(adminsGroupUuid)
+            .build();
+
+    createGroup(allUsersRepo, groupCreation, groupUpdate);
+  }
+
+  private void createGroup(
+      Repository allUsersRepo, InternalGroupCreation groupCreation, InternalGroupUpdate groupUpdate)
+      throws ConfigInvalidException, IOException {
+    InternalGroup createdGroup = createGroupInNoteDb(allUsersRepo, groupCreation, groupUpdate);
+    index(createdGroup);
+  }
+
+  private InternalGroup createGroupInNoteDb(
+      Repository allUsersRepo, InternalGroupCreation groupCreation, InternalGroupUpdate groupUpdate)
+      throws ConfigInvalidException, IOException, DuplicateKeyException {
+    // This method is only executed on a new server which doesn't have any accounts or groups.
+    AuditLogFormatter auditLogFormatter =
+        AuditLogFormatter.createBackedBy(ImmutableSet.of(), ImmutableSet.of(), serverId);
+
+    GroupConfig groupConfig =
+        GroupConfig.createForNewGroup(allUsersName, allUsersRepo, groupCreation);
+    groupConfig.setGroupUpdate(groupUpdate, auditLogFormatter);
+
+    AccountGroup.NameKey groupName = groupUpdate.getName().orElseGet(groupCreation::getNameKey);
+    GroupNameNotes groupNameNotes =
+        GroupNameNotes.forNewGroup(
+            allUsersName, allUsersRepo, groupCreation.getGroupUUID(), groupName);
+
+    commit(allUsersRepo, groupConfig, groupNameNotes);
+
+    return groupConfig
+        .getLoadedGroup()
+        .orElseThrow(() -> new IllegalStateException("Created group wasn't automatically loaded"));
+  }
+
+  private void commit(
+      Repository allUsersRepo, GroupConfig groupConfig, GroupNameNotes groupNameNotes)
+      throws IOException {
+    BatchRefUpdate batchRefUpdate = allUsersRepo.getRefDatabase().newBatchUpdate();
+    try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate(allUsersRepo, batchRefUpdate)) {
+      groupConfig.commit(metaDataUpdate);
+    }
+    // MetaDataUpdates unfortunately can't be reused. -> Create a new one.
+    try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate(allUsersRepo, batchRefUpdate)) {
+      groupNameNotes.commit(metaDataUpdate);
+    }
+    RefUpdateUtil.executeChecked(batchRefUpdate, allUsersRepo);
+  }
+
+  private MetaDataUpdate createMetaDataUpdate(
+      Repository allUsersRepo, @Nullable BatchRefUpdate batchRefUpdate) {
+    MetaDataUpdate metaDataUpdate =
+        new MetaDataUpdate(
+            GitReferenceUpdated.DISABLED, allUsersName, allUsersRepo, batchRefUpdate);
+    metaDataUpdate.getCommitBuilder().setAuthor(serverUser);
+    metaDataUpdate.getCommitBuilder().setCommitter(serverUser);
+    return metaDataUpdate;
+  }
+
+  private void index(InternalGroup group) {
+    for (GroupIndex groupIndex : indexCollection.getWriteIndexes()) {
+      groupIndex.replace(group);
+    }
+  }
+
+  private GroupReference createGroupReference(String name) {
+    AccountGroup.UUID groupUuid = GroupUUID.make(name, serverUser);
+    return new GroupReference(groupUuid, name);
+  }
+
+  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))
+        .setGroupUUID(groupReference.getUUID())
+        .build();
+  }
+}
diff --git a/java/com/google/gerrit/server/schema/SchemaModule.java b/java/com/google/gerrit/server/schema/SchemaModule.java
index 9ce19fe..ff2073d 100644
--- a/java/com/google/gerrit/server/schema/SchemaModule.java
+++ b/java/com/google/gerrit/server/schema/SchemaModule.java
@@ -27,9 +27,10 @@
 import com.google.gerrit.server.config.AnonymousCowardNameProvider;
 import com.google.gerrit.server.config.GerritServerId;
 import com.google.gerrit.server.config.GerritServerIdProvider;
+import com.google.gerrit.server.index.group.GroupIndexCollection;
 import org.eclipse.jgit.lib.PersonIdent;
 
-/** Validate the schema and connect to Git. */
+/** Bindings for low-level Gerrit schema data. */
 public class SchemaModule extends FactoryModule {
   @Override
   protected void configure() {
@@ -49,5 +50,11 @@
         .annotatedWith(GerritServerId.class)
         .toProvider(GerritServerIdProvider.class)
         .in(SINGLETON);
+
+    // It feels wrong to have this binding in a seemingly unrelated module, but it's a dependency of
+    // SchemaCreatorImpl, so it's needed.
+    // TODO(dborowitz): Is there any way to untangle this?
+    bind(GroupIndexCollection.class);
+    bind(SchemaCreator.class).to(SchemaCreatorImpl.class);
   }
 }
diff --git a/java/com/google/gerrit/server/schema/SchemaUpdater.java b/java/com/google/gerrit/server/schema/SchemaUpdater.java
deleted file mode 100644
index 5ead6aa..0000000
--- a/java/com/google/gerrit/server/schema/SchemaUpdater.java
+++ /dev/null
@@ -1,131 +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.server.schema;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.reviewdb.server.ReviewDbUtil;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.config.AnonymousCowardName;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.AbstractModule;
-import com.google.inject.Guice;
-import com.google.inject.Inject;
-import com.google.inject.Injector;
-import com.google.inject.Key;
-import com.google.inject.Provider;
-import com.google.inject.Stage;
-import java.io.IOException;
-import java.sql.SQLException;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.PersonIdent;
-
-/** Creates or updates the current database schema. */
-public class SchemaUpdater {
-  private final SchemaFactory<ReviewDb> schema;
-  private final SchemaCreator creator;
-  private final Provider<SchemaVersion> updater;
-
-  @Inject
-  SchemaUpdater(
-      @ReviewDbFactory SchemaFactory<ReviewDb> schema, SchemaCreator creator, Injector parent) {
-    this.schema = schema;
-    this.creator = creator;
-    this.updater = buildInjector(parent).getProvider(SchemaVersion.class);
-  }
-
-  private static Injector buildInjector(Injector parent) {
-    // Use DEVELOPMENT mode to allow lazy initialization of the
-    // graph. This avoids touching ancient schema versions that
-    // are behind this installation's current version.
-    return Guice.createInjector(
-        Stage.DEVELOPMENT,
-        new AbstractModule() {
-          @Override
-          protected void configure() {
-            bind(SchemaVersion.class).to(SchemaVersion.C);
-
-            for (Key<?> k :
-                new Key<?>[] {
-                  Key.get(PersonIdent.class, GerritPersonIdent.class),
-                  Key.get(String.class, AnonymousCowardName.class),
-                  Key.get(Config.class, GerritServerConfig.class),
-                }) {
-              rebind(parent, k);
-            }
-
-            for (Class<?> c :
-                new Class<?>[] {
-                  AllProjectsName.class,
-                  AllUsersCreator.class,
-                  AllUsersName.class,
-                  GitRepositoryManager.class,
-                  SitePaths.class,
-                  SystemGroupBackend.class,
-                }) {
-              rebind(parent, Key.get(c));
-            }
-          }
-
-          private <T> void rebind(Injector parent, Key<T> c) {
-            bind(c).toProvider(parent.getProvider(c));
-          }
-        });
-  }
-
-  public void update(UpdateUI ui) throws OrmException {
-    try (ReviewDb db = ReviewDbUtil.unwrapDb(schema.open())) {
-
-      final SchemaVersion u = updater.get();
-      final CurrentSchemaVersion version = getSchemaVersion(db);
-      if (version == null) {
-        try {
-          creator.create(db);
-        } catch (IOException | ConfigInvalidException e) {
-          throw new OrmException("Cannot initialize schema", e);
-        }
-
-      } else {
-        try {
-          u.check(ui, version, db);
-        } catch (SQLException e) {
-          throw new OrmException("Cannot upgrade schema", e);
-        }
-      }
-    }
-  }
-
-  @VisibleForTesting
-  public SchemaVersion getLatestSchemaVersion() {
-    return updater.get();
-  }
-
-  private CurrentSchemaVersion getSchemaVersion(ReviewDb db) {
-    try {
-      return db.schemaVersion().get(new CurrentSchemaVersion.Key());
-    } catch (OrmException e) {
-      return null;
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/SchemaVersion.java b/java/com/google/gerrit/server/schema/SchemaVersion.java
deleted file mode 100644
index 44533c9..0000000
--- a/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ /dev/null
@@ -1,216 +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.server.schema;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Stopwatch;
-import com.google.common.collect.Lists;
-import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.UsedAt;
-import com.google.gwtorm.jdbc.JdbcExecutor;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.StatementExecutor;
-import com.google.inject.Provider;
-import java.sql.PreparedStatement;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-
-/** A version of the database schema. */
-public abstract class SchemaVersion {
-  /** The current schema version. */
-  public static final Class<Schema_170> C = Schema_170.class;
-
-  public static int getBinaryVersion() {
-    return guessVersion(C);
-  }
-
-  private final Provider<? extends SchemaVersion> prior;
-  private final int versionNbr;
-
-  protected SchemaVersion(Provider<? extends SchemaVersion> prior) {
-    this.prior = prior;
-    this.versionNbr = guessVersion(getClass());
-  }
-
-  @UsedAt(UsedAt.Project.PLUGIN_DELETE_PROJECT)
-  public static int guessVersion(Class<?> c) {
-    String n = c.getName();
-    n = n.substring(n.lastIndexOf('_') + 1);
-    while (n.startsWith("0")) {
-      n = n.substring(1);
-    }
-    return Integer.parseInt(n);
-  }
-
-  /** @return the {@link CurrentSchemaVersion#versionNbr} this step targets. */
-  public final int getVersionNbr() {
-    return versionNbr;
-  }
-
-  @VisibleForTesting
-  public final SchemaVersion getPrior() {
-    return prior.get();
-  }
-
-  public final void check(UpdateUI ui, CurrentSchemaVersion curr, ReviewDb db)
-      throws OrmException, SQLException {
-    if (curr.versionNbr == versionNbr) {
-      // Nothing to do, we are at the correct schema.
-    } else if (curr.versionNbr > versionNbr) {
-      throw new OrmException(
-          "Cannot downgrade database schema from version "
-              + curr.versionNbr
-              + " to "
-              + versionNbr
-              + ".");
-    } else {
-      upgradeFrom(ui, curr, db);
-    }
-  }
-
-  /** Runs check on the prior schema version, and then upgrades. */
-  private void upgradeFrom(UpdateUI ui, CurrentSchemaVersion curr, ReviewDb db)
-      throws OrmException, SQLException {
-    List<SchemaVersion> pending = pending(curr.versionNbr);
-    updateSchema(pending, ui, db);
-    migrateData(pending, ui, curr, db);
-
-    JdbcSchema s = (JdbcSchema) db;
-    final List<String> pruneList = new ArrayList<>();
-    s.pruneSchema(
-        new StatementExecutor() {
-          @Override
-          public void execute(String sql) {
-            pruneList.add(sql);
-          }
-
-          @Override
-          public void close() {
-            // Do nothing.
-          }
-        });
-
-    try (JdbcExecutor e = new JdbcExecutor(s)) {
-      if (!pruneList.isEmpty()) {
-        ui.pruneSchema(e, pruneList);
-      }
-    }
-  }
-
-  private List<SchemaVersion> pending(int curr) {
-    List<SchemaVersion> r = Lists.newArrayListWithCapacity(versionNbr - curr);
-    for (SchemaVersion v = this; curr < v.getVersionNbr(); v = v.prior.get()) {
-      r.add(v);
-    }
-    Collections.reverse(r);
-    return r;
-  }
-
-  private void updateSchema(List<SchemaVersion> pending, UpdateUI ui, ReviewDb db)
-      throws OrmException, SQLException {
-    for (SchemaVersion v : pending) {
-      ui.message(String.format("Upgrading schema to %d ...", v.getVersionNbr()));
-      v.preUpdateSchema(db);
-    }
-
-    JdbcSchema s = (JdbcSchema) db;
-    try (JdbcExecutor e = new JdbcExecutor(s)) {
-      s.updateSchema(e);
-    }
-  }
-
-  /**
-   * Invoked before updateSchema adds new columns/tables.
-   *
-   * @param db open database handle.
-   * @throws OrmException if a Gerrit-specific exception occurred.
-   * @throws SQLException if an underlying SQL exception occurred.
-   */
-  protected void preUpdateSchema(ReviewDb db) throws OrmException, SQLException {}
-
-  private void migrateData(
-      List<SchemaVersion> pending, UpdateUI ui, CurrentSchemaVersion curr, ReviewDb db)
-      throws OrmException, SQLException {
-    for (SchemaVersion v : pending) {
-      Stopwatch sw = Stopwatch.createStarted();
-      ui.message(String.format("Migrating data to schema %d ...", v.getVersionNbr()));
-      v.migrateData(db, ui);
-      v.finish(curr, db);
-      ui.message(String.format("\t> Done (%.3f s)", sw.elapsed(TimeUnit.MILLISECONDS) / 1000d));
-    }
-  }
-
-  /**
-   * Invoked between updateSchema (adds new columns/tables) and pruneSchema (removes deleted
-   * columns/tables).
-   *
-   * @param db open database handle.
-   * @param ui interface for interacting with the user.
-   * @throws OrmException if a Gerrit-specific exception occurred.
-   * @throws SQLException if an underlying SQL exception occurred.
-   */
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {}
-
-  /** Mark the current schema version. */
-  protected void finish(CurrentSchemaVersion curr, ReviewDb db) throws OrmException {
-    curr.versionNbr = versionNbr;
-    db.schemaVersion().update(Collections.singleton(curr));
-  }
-
-  /** Rename an existing table. */
-  protected static void renameTable(ReviewDb db, String from, String to) throws OrmException {
-    JdbcSchema s = (JdbcSchema) db;
-    try (JdbcExecutor e = new JdbcExecutor(s)) {
-      s.renameTable(e, from, to);
-    }
-  }
-
-  /** Rename an existing column. */
-  protected static void renameColumn(ReviewDb db, String table, String from, String to)
-      throws OrmException {
-    JdbcSchema s = (JdbcSchema) db;
-    try (JdbcExecutor e = new JdbcExecutor(s)) {
-      s.renameColumn(e, table, from, to);
-    }
-  }
-
-  /** Execute an SQL statement. */
-  protected static void execute(ReviewDb db, String sql) throws SQLException {
-    try (Statement s = newStatement(db)) {
-      s.execute(sql);
-    }
-  }
-
-  /** Open a new single statement. */
-  protected static Statement newStatement(ReviewDb db) throws SQLException {
-    return ((JdbcSchema) db).getConnection().createStatement();
-  }
-
-  /** Open a new prepared statement. */
-  protected static PreparedStatement prepareStatement(ReviewDb db, String sql) throws SQLException {
-    return ((JdbcSchema) db).getConnection().prepareStatement(sql);
-  }
-
-  /** Open a new statement executor. */
-  protected static JdbcExecutor newExecutor(ReviewDb db) throws OrmException {
-    return new JdbcExecutor(((JdbcSchema) db).getConnection());
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/SchemaVersionCheck.java b/java/com/google/gerrit/server/schema/SchemaVersionCheck.java
deleted file mode 100644
index bdc15f4..0000000
--- a/java/com/google/gerrit/server/schema/SchemaVersionCheck.java
+++ /dev/null
@@ -1,95 +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.server.schema;
-
-import com.google.gerrit.extensions.events.LifecycleListener;
-import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-import com.google.inject.Module;
-import com.google.inject.ProvisionException;
-
-/** Validates the current schema version. */
-public class SchemaVersionCheck implements LifecycleListener {
-  public static Module module() {
-    return new LifecycleModule() {
-      @Override
-      protected void configure() {
-        listener().to(SchemaVersionCheck.class);
-      }
-    };
-  }
-
-  private final SchemaFactory<ReviewDb> schema;
-  private final SitePaths site;
-
-  @Inject
-  public SchemaVersionCheck(SchemaFactory<ReviewDb> schemaFactory, SitePaths site) {
-    this.schema = schemaFactory;
-    this.site = site;
-  }
-
-  @Override
-  public void start() {
-    try (ReviewDb db = schema.open()) {
-      final CurrentSchemaVersion currentVer = getSchemaVersion(db);
-      final int expectedVer = SchemaVersion.getBinaryVersion();
-
-      if (currentVer == null) {
-        throw new ProvisionException(
-            "Schema not yet initialized."
-                + "  Run init to initialize the schema:\n"
-                + "$ java -jar gerrit.war init -d "
-                + site.site_path.toAbsolutePath());
-      }
-      if (currentVer.versionNbr < expectedVer) {
-        throw new ProvisionException(
-            "Unsupported schema version "
-                + currentVer.versionNbr
-                + "; expected schema version "
-                + expectedVer
-                + ".  Run init to upgrade:\n"
-                + "$ java -jar "
-                + site.gerrit_war.toAbsolutePath()
-                + " init -d "
-                + site.site_path.toAbsolutePath());
-      } else if (currentVer.versionNbr > expectedVer) {
-        throw new ProvisionException(
-            "Unsupported schema version "
-                + currentVer.versionNbr
-                + "; expected schema version "
-                + expectedVer
-                + ". Downgrade is not supported.");
-      }
-    } catch (OrmException e) {
-      throw new ProvisionException("Cannot read schema_version", e);
-    }
-  }
-
-  @Override
-  public void stop() {}
-
-  private CurrentSchemaVersion getSchemaVersion(ReviewDb db) {
-    try {
-      return db.schemaVersion().get(new CurrentSchemaVersion.Key());
-    } catch (OrmException e) {
-      return null;
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_100.java b/java/com/google/gerrit/server/schema/Schema_100.java
deleted file mode 100644
index 0902194..0000000
--- a/java/com/google/gerrit/server/schema/Schema_100.java
+++ /dev/null
@@ -1,27 +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.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_100 extends SchemaVersion {
-  @Inject
-  Schema_100(Provider<Schema_99> prior) {
-    super(prior);
-  }
-
-  // No database migration; merges are rechecked on reindex.
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_101.java b/java/com/google/gerrit/server/schema/Schema_101.java
deleted file mode 100644
index ccbb2de..0000000
--- a/java/com/google/gerrit/server/schema/Schema_101.java
+++ /dev/null
@@ -1,144 +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.schema;
-
-import com.google.common.base.Joiner;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.jdbc.JdbcExecutor;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.schema.ColumnModel;
-import com.google.gwtorm.schema.RelationModel;
-import com.google.gwtorm.schema.java.JavaSchemaModel;
-import com.google.gwtorm.schema.sql.DialectPostgreSQL;
-import com.google.gwtorm.schema.sql.SqlDialect;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.StatementExecutor;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.sql.Connection;
-import java.sql.DatabaseMetaData;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.TreeMap;
-
-public class Schema_101 extends SchemaVersion {
-
-  private static class PrimaryKey {
-    String oldNameInDb;
-    List<String> cols;
-  }
-
-  private Connection conn;
-  private SqlDialect dialect;
-
-  @Inject
-  Schema_101(Provider<Schema_100> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    conn = ((JdbcSchema) db).getConnection();
-    dialect = ((JdbcSchema) db).getDialect();
-    Map<String, PrimaryKey> corrections = findPKUpdates();
-    if (corrections.isEmpty()) {
-      return;
-    }
-
-    ui.message("Wrong Primary Key Column Order Detected");
-    ui.message("The following tables are affected:");
-    ui.message(Joiner.on(", ").join(corrections.keySet()));
-    ui.message("fixing primary keys...");
-    try (JdbcExecutor executor = new JdbcExecutor(conn)) {
-      for (Map.Entry<String, PrimaryKey> c : corrections.entrySet()) {
-        ui.message(String.format("  table: %s ... ", c.getKey()));
-        recreatePK(executor, c.getKey(), c.getValue(), ui);
-        ui.message("done");
-      }
-      ui.message("done");
-    }
-  }
-
-  private Map<String, PrimaryKey> findPKUpdates() throws OrmException, SQLException {
-    Map<String, PrimaryKey> corrections = new TreeMap<>();
-    DatabaseMetaData meta = conn.getMetaData();
-    JavaSchemaModel jsm = new JavaSchemaModel(ReviewDb.class);
-    for (RelationModel rm : jsm.getRelations()) {
-      String tableName = rm.getRelationName();
-      List<String> expectedPKCols = relationPK(rm);
-      PrimaryKey actualPK = dbTablePK(meta, tableName);
-      if (!expectedPKCols.equals(actualPK.cols)) {
-        actualPK.cols = expectedPKCols;
-        corrections.put(tableName, actualPK);
-      }
-    }
-    return corrections;
-  }
-
-  private List<String> relationPK(RelationModel rm) {
-    Collection<ColumnModel> cols = rm.getPrimaryKeyColumns();
-    List<String> pk = new ArrayList<>(cols.size());
-    for (ColumnModel cm : cols) {
-      pk.add(cm.getColumnName().toLowerCase(Locale.US));
-    }
-    return pk;
-  }
-
-  private PrimaryKey dbTablePK(DatabaseMetaData meta, String tableName) throws SQLException {
-    if (meta.storesUpperCaseIdentifiers()) {
-      tableName = tableName.toUpperCase();
-    } else if (meta.storesLowerCaseIdentifiers()) {
-      tableName = tableName.toLowerCase();
-    }
-
-    try (ResultSet cols = meta.getPrimaryKeys(null, null, tableName)) {
-      PrimaryKey pk = new PrimaryKey();
-      Map<Short, String> seqToName = new TreeMap<>();
-      while (cols.next()) {
-        seqToName.put(cols.getShort("KEY_SEQ"), cols.getString("COLUMN_NAME"));
-        if (pk.oldNameInDb == null) {
-          pk.oldNameInDb = cols.getString("PK_NAME");
-        }
-      }
-
-      pk.cols = new ArrayList<>(seqToName.size());
-      for (String name : seqToName.values()) {
-        pk.cols.add(name.toLowerCase(Locale.US));
-      }
-      return pk;
-    }
-  }
-
-  private void recreatePK(StatementExecutor executor, String tableName, PrimaryKey pk, UpdateUI ui)
-      throws OrmException {
-    if (pk.oldNameInDb == null) {
-      ui.message(String.format("warning: primary key for table %s didn't exist ... ", tableName));
-    } else {
-      if (dialect instanceof DialectPostgreSQL) {
-        // postgresql doesn't support the ALTER TABLE foo DROP PRIMARY KEY form
-        executor.execute("ALTER TABLE " + tableName + " DROP CONSTRAINT " + pk.oldNameInDb);
-      } else {
-        executor.execute("ALTER TABLE " + tableName + " DROP PRIMARY KEY");
-      }
-    }
-    executor.execute(
-        "ALTER TABLE " + tableName + " ADD PRIMARY KEY(" + Joiner.on(",").join(pk.cols) + ")");
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_102.java b/java/com/google/gerrit/server/schema/Schema_102.java
deleted file mode 100644
index 1c1aa55..0000000
--- a/java/com/google/gerrit/server/schema/Schema_102.java
+++ /dev/null
@@ -1,70 +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.schema;
-
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.schema.sql.DialectPostgreSQL;
-import com.google.gwtorm.schema.sql.SqlDialect;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.StatementExecutor;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.sql.SQLException;
-import java.util.Set;
-import java.util.regex.Pattern;
-
-public class Schema_102 extends SchemaVersion {
-  @Inject
-  Schema_102(Provider<Schema_101> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    JdbcSchema schema = (JdbcSchema) db;
-    SqlDialect dialect = schema.getDialect();
-    try (StatementExecutor e = newExecutor(db)) {
-      // Drop left over indexes that were missed to be removed in schema 84.
-      // See "Delete SQL index support" commit for more details:
-      // d4ae3a16d5e1464574bd04f429a63eb9c02b3b43
-      Pattern pattern =
-          Pattern.compile("^changes_(allOpen|allClosed|byBranchClosed)$", Pattern.CASE_INSENSITIVE);
-      String table = "changes";
-      Set<String> listIndexes = dialect.listIndexes(schema.getConnection(), table);
-      for (String index : listIndexes) {
-        if (pattern.matcher(index).matches()) {
-          dialect.dropIndex(e, table, index);
-        }
-      }
-
-      dialect.dropIndex(e, table, "changes_byProjectOpen");
-      if (dialect instanceof DialectPostgreSQL) {
-        e.execute(
-            "CREATE INDEX changes_byProjectOpen"
-                + " ON "
-                + table
-                + " (dest_project_name, last_updated_on)"
-                + " WHERE open = 'Y'");
-      } else {
-        e.execute(
-            "CREATE INDEX changes_byProjectOpen"
-                + " ON "
-                + table
-                + " (open, dest_project_name, last_updated_on)");
-      }
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_103.java b/java/com/google/gerrit/server/schema/Schema_103.java
deleted file mode 100644
index 60a5213..0000000
--- a/java/com/google/gerrit/server/schema/Schema_103.java
+++ /dev/null
@@ -1,27 +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.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_103 extends SchemaVersion {
-  @Inject
-  Schema_103(Provider<Schema_102> prior) {
-    super(prior);
-  }
-
-  // Adds originalSubject column
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_104.java b/java/com/google/gerrit/server/schema/Schema_104.java
deleted file mode 100644
index bebdaca..0000000
--- a/java/com/google/gerrit/server/schema/Schema_104.java
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_104 extends SchemaVersion {
-  @Inject
-  Schema_104(Provider<Schema_103> prior) {
-    super(prior);
-  }
-
-  // Remove old change screen
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_105.java b/java/com/google/gerrit/server/schema/Schema_105.java
deleted file mode 100644
index dd5e71a7..0000000
--- a/java/com/google/gerrit/server/schema/Schema_105.java
+++ /dev/null
@@ -1,87 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Sets;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.schema.sql.SqlDialect;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.StatementExecutor;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.sql.SQLException;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-
-public class Schema_105 extends SchemaVersion {
-  private static final String TABLE = "changes";
-
-  @Inject
-  Schema_105(Provider<Schema_104> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException, OrmException {
-    JdbcSchema schema = (JdbcSchema) db;
-    SqlDialect dialect = schema.getDialect();
-
-    Map<String, OrmException> errors = new HashMap<>();
-    try (StatementExecutor e = newExecutor(db)) {
-      for (String index : listChangesIndexes(schema)) {
-        ui.message("Dropping index " + index + " on table " + TABLE);
-        try {
-          dialect.dropIndex(e, TABLE, index);
-        } catch (OrmException err) {
-          errors.put(index, err);
-        }
-      }
-    }
-
-    for (String index : listChangesIndexes(schema)) {
-      String msg = "Failed to drop index " + index;
-      OrmException err = errors.get(index);
-      if (err != null) {
-        msg += ": " + err.getMessage();
-      }
-      ui.message(msg);
-    }
-  }
-
-  private Set<String> listChangesIndexes(JdbcSchema schema) throws SQLException {
-    // List of all changes indexes ever created or dropped, found with the
-    // following command:
-    //   find g* -name \*.sql | xargs git log -i -p -S' index changes_' | grep -io ' index
-    // changes_\w*' | cut -d' ' -f3 | tr A-Z a-z | sort -u
-    // Used rather than listIndexes as we're not sure whether it might include
-    // primary key indexes.
-    Set<String> allChanges =
-        ImmutableSet.of(
-            "changes_allclosed",
-            "changes_allopen",
-            "changes_bybranchclosed",
-            "changes_byownerclosed",
-            "changes_byowneropen",
-            "changes_byproject",
-            "changes_byprojectopen",
-            "changes_key",
-            "changes_submitted");
-    return Sets.intersection(
-        schema.getDialect().listIndexes(schema.getConnection(), TABLE), allChanges);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_106.java b/java/com/google/gerrit/server/schema/Schema_106.java
deleted file mode 100644
index 7b6a30a..0000000
--- a/java/com/google/gerrit/server/schema/Schema_106.java
+++ /dev/null
@@ -1,160 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.LocalDiskRepositoryManager;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.io.File;
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.SortedSet;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
-
-public class Schema_106 extends SchemaVersion {
-  // we can use multiple threads per CPU as we can expect that threads will be
-  // waiting for IO
-  private static final int THREADS_PER_CPU = 4;
-  private final GitRepositoryManager repoManager;
-  private final PersonIdent serverUser;
-
-  @Inject
-  Schema_106(
-      Provider<Schema_105> prior,
-      GitRepositoryManager repoManager,
-      @GerritPersonIdent PersonIdent serverUser) {
-    super(prior);
-    this.repoManager = repoManager;
-    this.serverUser = serverUser;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
-    if (!(repoManager instanceof LocalDiskRepositoryManager)) {
-      return;
-    }
-
-    ui.message("listing all repositories ...");
-    SortedSet<Project.NameKey> repoList = repoManager.list();
-    ui.message("done");
-
-    ui.message(String.format("creating reflog files for %s branches ...", RefNames.REFS_CONFIG));
-
-    ExecutorService executorPool = createExecutor(ui, repoList.size());
-    List<Future<Void>> futures = new ArrayList<>();
-
-    for (Project.NameKey project : repoList) {
-      Callable<Void> callable = new ReflogCreator(project);
-      futures.add(executorPool.submit(callable));
-    }
-
-    executorPool.shutdown();
-    try {
-      for (Future<Void> future : futures) {
-        try {
-          future.get();
-        } catch (ExecutionException e) {
-          ui.message(e.getCause().getMessage());
-        }
-      }
-      ui.message("done");
-    } catch (InterruptedException ex) {
-      String msg =
-          String.format(
-              "Migration step 106 was interrupted. "
-                  + "Reflog created in %d of %d repositories only.",
-              countDone(futures), repoList.size());
-      ui.message(msg);
-    }
-  }
-
-  private static int countDone(List<Future<Void>> futures) {
-    int count = 0;
-    for (Future<Void> future : futures) {
-      if (future.isDone()) {
-        count++;
-      }
-    }
-
-    return count;
-  }
-
-  private ExecutorService createExecutor(UpdateUI ui, int repoCount) {
-    int procs = Runtime.getRuntime().availableProcessors();
-    int threads = Math.min(procs * THREADS_PER_CPU, repoCount);
-    ui.message(String.format("... using %d threads ...", threads));
-    return Executors.newFixedThreadPool(threads);
-  }
-
-  private class ReflogCreator implements Callable<Void> {
-    private final Project.NameKey project;
-
-    ReflogCreator(Project.NameKey project) {
-      this.project = project;
-    }
-
-    @Override
-    public Void call() throws IOException {
-      try (Repository repo = repoManager.openRepository(project)) {
-        File metaConfigLog = new File(repo.getDirectory(), "logs/" + RefNames.REFS_CONFIG);
-        if (metaConfigLog.exists()) {
-          return null;
-        }
-
-        if (!metaConfigLog.getParentFile().mkdirs() || !metaConfigLog.createNewFile()) {
-          throw new IOException();
-        }
-
-        ObjectId metaConfigId = repo.resolve(RefNames.REFS_CONFIG);
-        if (metaConfigId != null) {
-          try (PrintWriter writer = new PrintWriter(metaConfigLog, UTF_8.name())) {
-            writer.print(ObjectId.zeroId().name());
-            writer.print(" ");
-            writer.print(metaConfigId.name());
-            writer.print(" ");
-            writer.print(serverUser.toExternalString());
-            writer.print("\t");
-            writer.print("create reflog");
-            writer.println();
-          }
-        }
-        return null;
-      } catch (IOException e) {
-        throw new IOException(
-            String.format(
-                "ERROR: Failed to create reflog file for the %s branch in repository %s",
-                RefNames.REFS_CONFIG, project.get()),
-            e);
-      }
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_107.java b/java/com/google/gerrit/server/schema/Schema_107.java
deleted file mode 100644
index dd8868f..0000000
--- a/java/com/google/gerrit/server/schema/Schema_107.java
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.sql.SQLException;
-import java.sql.Statement;
-
-public class Schema_107 extends SchemaVersion {
-
-  @Inject
-  Schema_107(Provider<Schema_106> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException {
-    try (Statement stmt = ((JdbcSchema) db).getConnection().createStatement()) {
-      stmt.executeUpdate("UPDATE accounts set mute_common_path_prefixes = 'Y'");
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_108.java b/java/com/google/gerrit/server/schema/Schema_108.java
deleted file mode 100644
index 4e62460..0000000
--- a/java/com/google/gerrit/server/schema/Schema_108.java
+++ /dev/null
@@ -1,191 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.common.base.Joiner;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ListMultimap;
-import com.google.common.collect.MultimapBuilder;
-import com.google.common.collect.SetMultimap;
-import com.google.common.collect.Sets;
-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.gerrit.reviewdb.client.Project.NameKey;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.GroupCollector;
-import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.io.IOException;
-import java.util.Collection;
-import java.util.Map;
-import java.util.Set;
-import java.util.SortedSet;
-import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefDatabase;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevObject;
-import org.eclipse.jgit.revwalk.RevSort;
-import org.eclipse.jgit.revwalk.RevWalk;
-
-public class Schema_108 extends SchemaVersion {
-  private final GitRepositoryManager repoManager;
-
-  @Inject
-  Schema_108(Provider<Schema_107> prior, GitRepositoryManager repoManager) {
-    super(prior);
-    this.repoManager = repoManager;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
-    ui.message("Listing all changes ...");
-    SetMultimap<Project.NameKey, Change.Id> openByProject = getOpenChangesByProject(db, ui);
-    ui.message("done");
-
-    ui.message("Updating groups for open changes ...");
-    int i = 0;
-    for (Map.Entry<Project.NameKey, Collection<Change.Id>> e : openByProject.asMap().entrySet()) {
-      try (Repository repo = repoManager.openRepository(e.getKey());
-          RevWalk rw = new RevWalk(repo)) {
-        updateProjectGroups(db, repo, rw, (Set<Change.Id>) e.getValue(), ui);
-      } catch (IOException | NoSuchChangeException err) {
-        throw new OrmException(err);
-      }
-      if (++i % 100 == 0) {
-        ui.message("  done " + i + " projects ...");
-      }
-    }
-    ui.message("done");
-  }
-
-  private void updateProjectGroups(
-      ReviewDb db, Repository repo, RevWalk rw, Set<Change.Id> changes, UpdateUI ui)
-      throws OrmException, IOException {
-    // Match sorting in ReceiveCommits.
-    rw.reset();
-    rw.sort(RevSort.TOPO);
-    rw.sort(RevSort.REVERSE, true);
-
-    RefDatabase refdb = repo.getRefDatabase();
-    for (Ref ref : refdb.getRefsByPrefix(Constants.R_HEADS)) {
-      RevCommit c = maybeParseCommit(rw, ref.getObjectId(), ui);
-      if (c != null) {
-        rw.markUninteresting(c);
-      }
-    }
-
-    ListMultimap<ObjectId, Ref> changeRefsBySha =
-        MultimapBuilder.hashKeys().arrayListValues().build();
-    ListMultimap<ObjectId, PatchSet.Id> patchSetsBySha =
-        MultimapBuilder.hashKeys().arrayListValues().build();
-    for (Ref ref : refdb.getRefsByPrefix(RefNames.REFS_CHANGES)) {
-      ObjectId id = ref.getObjectId();
-      if (ref.getObjectId() == null) {
-        continue;
-      }
-      id = id.copy();
-      changeRefsBySha.put(id, ref);
-      PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName());
-      if (psId != null && changes.contains(psId.getParentKey())) {
-        patchSetsBySha.put(id, psId);
-        RevCommit c = maybeParseCommit(rw, id, ui);
-        if (c != null) {
-          rw.markStart(c);
-        }
-      }
-    }
-
-    GroupCollector collector = GroupCollector.createForSchemaUpgradeOnly(changeRefsBySha, db);
-    RevCommit c;
-    while ((c = rw.next()) != null) {
-      collector.visit(c);
-    }
-
-    updateGroups(db, collector, patchSetsBySha);
-  }
-
-  private static void updateGroups(
-      ReviewDb db, GroupCollector collector, ListMultimap<ObjectId, PatchSet.Id> patchSetsBySha)
-      throws OrmException {
-    Map<PatchSet.Id, PatchSet> patchSets =
-        db.patchSets().toMap(db.patchSets().get(patchSetsBySha.values()));
-    for (Map.Entry<ObjectId, Collection<String>> e : collector.getGroups().asMap().entrySet()) {
-      for (PatchSet.Id psId : patchSetsBySha.get(e.getKey())) {
-        PatchSet ps = patchSets.get(psId);
-        if (ps != null) {
-          ps.setGroups(ImmutableList.copyOf(e.getValue()));
-        }
-      }
-    }
-
-    db.patchSets().update(patchSets.values());
-  }
-
-  private SetMultimap<Project.NameKey, Change.Id> getOpenChangesByProject(ReviewDb db, UpdateUI ui)
-      throws OrmException {
-    SortedSet<NameKey> projects = repoManager.list();
-    SortedSet<NameKey> nonExistentProjects = Sets.newTreeSet();
-    SetMultimap<Project.NameKey, Change.Id> openByProject =
-        MultimapBuilder.hashKeys().hashSetValues().build();
-    for (Change c : db.changes().all()) {
-      Status status = c.getStatus();
-      if (status != null && status.isClosed()) {
-        continue;
-      }
-
-      NameKey projectKey = c.getProject();
-      if (!projects.contains(projectKey)) {
-        nonExistentProjects.add(projectKey);
-      } else {
-        // The old "submitted" state is not supported anymore
-        // (thus status is null) but it was an opened state and needs
-        // to be migrated as such
-        openByProject.put(projectKey, c.getId());
-      }
-    }
-
-    if (!nonExistentProjects.isEmpty()) {
-      ui.message("Detected open changes referring to the following non-existent projects:");
-      ui.message(Joiner.on(", ").join(nonExistentProjects));
-      ui.message(
-          "It is highly recommended to remove\n"
-              + "the obsolete open changes, comments and patch-sets from your DB.\n");
-    }
-    return openByProject;
-  }
-
-  private static RevCommit maybeParseCommit(RevWalk rw, ObjectId id, UpdateUI ui)
-      throws IOException {
-    if (id != null) {
-      try {
-        RevObject obj = rw.parseAny(id);
-        return (obj instanceof RevCommit) ? (RevCommit) obj : null;
-      } catch (MissingObjectException moe) {
-        ui.message("Missing object: " + id.getName() + "\n");
-      }
-    }
-    return null;
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_109.java b/java/com/google/gerrit/server/schema/Schema_109.java
deleted file mode 100644
index c5a6015..0000000
--- a/java/com/google/gerrit/server/schema/Schema_109.java
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.StatementExecutor;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_109 extends SchemaVersion {
-  @Inject
-  Schema_109(Provider<Schema_108> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
-    try (StatementExecutor e = newExecutor(db)) {
-      e.execute("UPDATE changes SET status = 'n', created_on = created_on WHERE status = 's'");
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_110.java b/java/com/google/gerrit/server/schema/Schema_110.java
deleted file mode 100644
index 9e0f112..0000000
--- a/java/com/google/gerrit/server/schema/Schema_110.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_110 extends SchemaVersion {
-  @Inject
-  Schema_110(Provider<Schema_109> prior) {
-    super(prior);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_111.java b/java/com/google/gerrit/server/schema/Schema_111.java
deleted file mode 100644
index 223fdb6..0000000
--- a/java/com/google/gerrit/server/schema/Schema_111.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_111 extends SchemaVersion {
-  @Inject
-  Schema_111(Provider<Schema_110> prior) {
-    super(prior);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_112.java b/java/com/google/gerrit/server/schema/Schema_112.java
deleted file mode 100644
index 3e879bd..0000000
--- a/java/com/google/gerrit/server/schema/Schema_112.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_112 extends SchemaVersion {
-  @Inject
-  Schema_112(Provider<Schema_111> prior) {
-    super(prior);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_113.java b/java/com/google/gerrit/server/schema/Schema_113.java
deleted file mode 100644
index 32d655e..0000000
--- a/java/com/google/gerrit/server/schema/Schema_113.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_113 extends SchemaVersion {
-  @Inject
-  Schema_113(Provider<Schema_112> prior) {
-    super(prior);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_114.java b/java/com/google/gerrit/server/schema/Schema_114.java
deleted file mode 100644
index 85c93d2..0000000
--- a/java/com/google/gerrit/server/schema/Schema_114.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_114 extends SchemaVersion {
-  @Inject
-  Schema_114(Provider<Schema_113> prior) {
-    super(prior);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_115.java b/java/com/google/gerrit/server/schema/Schema_115.java
deleted file mode 100644
index 70bc921..0000000
--- a/java/com/google/gerrit/server/schema/Schema_115.java
+++ /dev/null
@@ -1,209 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import static com.google.common.base.Preconditions.checkState;
-import static com.google.gerrit.server.config.ConfigUtil.storeSection;
-import static java.util.Objects.requireNonNull;
-
-import com.google.common.base.Strings;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
-import com.google.gerrit.extensions.client.Theme;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-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.git.UserConfigSections;
-import com.google.gerrit.server.git.meta.MetaDataUpdate;
-import com.google.gerrit.server.patch.PatchListKey;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.io.IOException;
-import java.sql.ResultSet;
-import java.sql.ResultSetMetaData;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.BatchRefUpdate;
-import org.eclipse.jgit.lib.NullProgressMonitor;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevWalk;
-
-public class Schema_115 extends SchemaVersion {
-  private final GitRepositoryManager mgr;
-  private final AllUsersName allUsersName;
-  private final PersonIdent serverUser;
-
-  @Inject
-  Schema_115(
-      Provider<Schema_114> prior,
-      GitRepositoryManager mgr,
-      AllUsersName allUsersName,
-      @GerritPersonIdent PersonIdent serverUser) {
-    super(prior);
-    this.mgr = mgr;
-    this.allUsersName = allUsersName;
-    this.serverUser = serverUser;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    Map<Account.Id, DiffPreferencesInfo> imports = new HashMap<>();
-    try (Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-        ResultSet rs = stmt.executeQuery("SELECT * FROM account_diff_preferences")) {
-      Set<String> availableColumns = getColumns(rs);
-      while (rs.next()) {
-        Account.Id accountId = new Account.Id(rs.getInt("id"));
-        DiffPreferencesInfo prefs = new DiffPreferencesInfo();
-        if (availableColumns.contains("context")) {
-          prefs.context = (int) rs.getShort("context");
-        }
-        if (availableColumns.contains("expand_all_comments")) {
-          prefs.expandAllComments = toBoolean(rs.getString("expand_all_comments"));
-        }
-        if (availableColumns.contains("hide_line_numbers")) {
-          prefs.hideLineNumbers = toBoolean(rs.getString("hide_line_numbers"));
-        }
-        if (availableColumns.contains("hide_top_menu")) {
-          prefs.hideTopMenu = toBoolean(rs.getString("hide_top_menu"));
-        }
-        if (availableColumns.contains("ignore_whitespace")) {
-          // Enum with char as value
-          prefs.ignoreWhitespace = toWhitespace(rs.getString("ignore_whitespace"));
-        }
-        if (availableColumns.contains("intraline_difference")) {
-          prefs.intralineDifference = toBoolean(rs.getString("intraline_difference"));
-        }
-        if (availableColumns.contains("line_length")) {
-          prefs.lineLength = rs.getInt("line_length");
-        }
-        if (availableColumns.contains("manual_review")) {
-          prefs.manualReview = toBoolean(rs.getString("manual_review"));
-        }
-        if (availableColumns.contains("render_entire_file")) {
-          prefs.renderEntireFile = toBoolean(rs.getString("render_entire_file"));
-        }
-        if (availableColumns.contains("retain_header")) {
-          prefs.retainHeader = toBoolean(rs.getString("retain_header"));
-        }
-        if (availableColumns.contains("show_line_endings")) {
-          prefs.showLineEndings = toBoolean(rs.getString("show_line_endings"));
-        }
-        if (availableColumns.contains("show_tabs")) {
-          prefs.showTabs = toBoolean(rs.getString("show_tabs"));
-        }
-        if (availableColumns.contains("show_whitespace_errors")) {
-          prefs.showWhitespaceErrors = toBoolean(rs.getString("show_whitespace_errors"));
-        }
-        if (availableColumns.contains("skip_deleted")) {
-          prefs.skipDeleted = toBoolean(rs.getString("skip_deleted"));
-        }
-        if (availableColumns.contains("skip_uncommented")) {
-          prefs.skipUncommented = toBoolean(rs.getString("skip_uncommented"));
-        }
-        if (availableColumns.contains("syntax_highlighting")) {
-          prefs.syntaxHighlighting = toBoolean(rs.getString("syntax_highlighting"));
-        }
-        if (availableColumns.contains("tab_size")) {
-          prefs.tabSize = rs.getInt("tab_size");
-        }
-        if (availableColumns.contains("theme")) {
-          // Enum with name as values; can be null
-          prefs.theme = toTheme(rs.getString("theme"));
-        }
-        if (availableColumns.contains("hide_empty_pane")) {
-          prefs.hideEmptyPane = toBoolean(rs.getString("hide_empty_pane"));
-        }
-        if (availableColumns.contains("auto_hide_diff_table_header")) {
-          prefs.autoHideDiffTableHeader = toBoolean(rs.getString("auto_hide_diff_table_header"));
-        }
-        imports.put(accountId, prefs);
-      }
-    }
-
-    if (imports.isEmpty()) {
-      return;
-    }
-
-    try (Repository git = mgr.openRepository(allUsersName);
-        RevWalk rw = new RevWalk(git)) {
-      BatchRefUpdate bru = git.getRefDatabase().newBatchUpdate();
-      for (Map.Entry<Account.Id, DiffPreferencesInfo> e : imports.entrySet()) {
-        try (MetaDataUpdate md =
-            new MetaDataUpdate(GitReferenceUpdated.DISABLED, allUsersName, git, bru)) {
-          md.getCommitBuilder().setAuthor(serverUser);
-          md.getCommitBuilder().setCommitter(serverUser);
-          VersionedAccountPreferences p = VersionedAccountPreferences.forUser(e.getKey());
-          p.load(md);
-          storeSection(
-              p.getConfig(),
-              UserConfigSections.DIFF,
-              null,
-              e.getValue(),
-              DiffPreferencesInfo.defaults());
-          p.commit(md);
-        }
-      }
-
-      bru.execute(rw, NullProgressMonitor.INSTANCE);
-    } catch (ConfigInvalidException | IOException ex) {
-      throw new OrmException(ex);
-    }
-  }
-
-  private Set<String> getColumns(ResultSet rs) throws SQLException {
-    ResultSetMetaData metaData = rs.getMetaData();
-    int columnCount = metaData.getColumnCount();
-    Set<String> columns = new HashSet<>(columnCount);
-    for (int i = 1; i <= columnCount; i++) {
-      columns.add(metaData.getColumnLabel(i).toLowerCase());
-    }
-    return columns;
-  }
-
-  private static Theme toTheme(String v) {
-    if (v == null) {
-      return Theme.DEFAULT;
-    }
-    return Theme.valueOf(v);
-  }
-
-  private static Whitespace toWhitespace(String v) {
-    requireNonNull(v);
-    if (v.isEmpty()) {
-      return Whitespace.IGNORE_NONE;
-    }
-    Whitespace r = PatchListKey.WHITESPACE_TYPES.inverse().get(v.charAt(0));
-    if (r == null) {
-      throw new IllegalArgumentException("Cannot find Whitespace type for: " + v);
-    }
-    return r;
-  }
-
-  private static boolean toBoolean(String v) {
-    checkState(!Strings.isNullOrEmpty(v));
-    return v.equals("Y");
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_116.java b/java/com/google/gerrit/server/schema/Schema_116.java
deleted file mode 100644
index 5b018a2..0000000
--- a/java/com/google/gerrit/server/schema/Schema_116.java
+++ /dev/null
@@ -1,25 +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.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_116 extends SchemaVersion {
-  @Inject
-  Schema_116(Provider<Schema_115> prior) {
-    super(prior);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_117.java b/java/com/google/gerrit/server/schema/Schema_117.java
deleted file mode 100644
index 35e6c8a..0000000
--- a/java/com/google/gerrit/server/schema/Schema_117.java
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.sql.Connection;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.Set;
-
-public class Schema_117 extends SchemaVersion {
-  @Inject
-  Schema_117(Provider<Schema_116> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void preUpdateSchema(ReviewDb db) throws OrmException, SQLException {
-    JdbcSchema schema = (JdbcSchema) db;
-    Connection connection = schema.getConnection();
-    String tableName = "patch_sets";
-    String oldColumnName = "push_certficate";
-    String newColumnName = "push_certificate";
-    Set<String> columns = schema.getDialect().listColumns(connection, tableName);
-    if (columns.contains(oldColumnName)) {
-      renameColumn(db, tableName, oldColumnName, newColumnName);
-    }
-    try (Statement stmt = schema.getConnection().createStatement()) {
-      stmt.execute("ALTER TABLE " + tableName + " MODIFY " + newColumnName + " clob");
-    } catch (SQLException e) {
-      // Ignore.  Type may have already been modified manually.
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_118.java b/java/com/google/gerrit/server/schema/Schema_118.java
deleted file mode 100644
index 8c2c740..0000000
--- a/java/com/google/gerrit/server/schema/Schema_118.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_118 extends SchemaVersion {
-  @Inject
-  Schema_118(Provider<Schema_117> prior) {
-    super(prior);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_119.java b/java/com/google/gerrit/server/schema/Schema_119.java
deleted file mode 100644
index e5a6405..0000000
--- a/java/com/google/gerrit/server/schema/Schema_119.java
+++ /dev/null
@@ -1,233 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import static com.google.common.base.Preconditions.checkState;
-import static com.google.gerrit.reviewdb.client.CoreDownloadSchemes.ANON_GIT;
-import static com.google.gerrit.reviewdb.client.CoreDownloadSchemes.ANON_HTTP;
-import static com.google.gerrit.reviewdb.client.CoreDownloadSchemes.HTTP;
-import static com.google.gerrit.reviewdb.client.CoreDownloadSchemes.REPO_DOWNLOAD;
-import static com.google.gerrit.reviewdb.client.CoreDownloadSchemes.SSH;
-import static com.google.gerrit.server.config.ConfigUtil.storeSection;
-
-import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableMap;
-import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
-import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DateFormat;
-import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DiffView;
-import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DownloadCommand;
-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.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-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.git.UserConfigSections;
-import com.google.gerrit.server.git.meta.MetaDataUpdate;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.io.IOException;
-import java.sql.Connection;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.BatchRefUpdate;
-import org.eclipse.jgit.lib.NullProgressMonitor;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevWalk;
-
-public class Schema_119 extends SchemaVersion {
-  private static final ImmutableMap<String, String> LEGACY_DISPLAYNAME_MAP =
-      ImmutableMap.<String, String>of(
-          "ANON_GIT", ANON_GIT,
-          "ANON_HTTP", ANON_HTTP,
-          "HTTP", HTTP,
-          "SSH", SSH,
-          "REPO_DOWNLOAD", REPO_DOWNLOAD);
-
-  private final GitRepositoryManager mgr;
-  private final AllUsersName allUsersName;
-  private final PersonIdent serverUser;
-
-  @Inject
-  Schema_119(
-      Provider<Schema_118> prior,
-      GitRepositoryManager mgr,
-      AllUsersName allUsersName,
-      @GerritPersonIdent PersonIdent serverUser) {
-    super(prior);
-    this.mgr = mgr;
-    this.allUsersName = allUsersName;
-    this.serverUser = serverUser;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    JdbcSchema schema = (JdbcSchema) db;
-    Connection connection = schema.getConnection();
-    String tableName = "accounts";
-    String emailStrategy = "email_strategy";
-    Set<String> columns = schema.getDialect().listColumns(connection, tableName);
-    Map<Account.Id, GeneralPreferencesInfo> imports = new HashMap<>();
-    try (Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-        ResultSet rs =
-            stmt.executeQuery(
-                "select "
-                    + "account_id, "
-                    + "maximum_page_size, "
-                    + "show_site_header, "
-                    + "use_flash_clipboard, "
-                    + "download_url, "
-                    + "download_command, "
-                    + (columns.contains(emailStrategy)
-                        ? emailStrategy + ", "
-                        : "copy_self_on_email, ")
-                    + "date_format, "
-                    + "time_format, "
-                    + "relative_date_in_change_table, "
-                    + "diff_view, "
-                    + "size_bar_in_change_table, "
-                    + "legacycid_in_change_table, "
-                    + "review_category_strategy, "
-                    + "mute_common_path_prefixes "
-                    + "from "
-                    + tableName)) {
-      while (rs.next()) {
-        GeneralPreferencesInfo p = new GeneralPreferencesInfo();
-        Account.Id accountId = new Account.Id(rs.getInt(1));
-        p.changesPerPage = (int) rs.getShort(2);
-        p.showSiteHeader = toBoolean(rs.getString(3));
-        p.useFlashClipboard = toBoolean(rs.getString(4));
-        p.downloadScheme = convertToModernNames(rs.getString(5));
-        p.downloadCommand = toDownloadCommand(rs.getString(6));
-        p.emailStrategy = toEmailStrategy(rs.getString(7), columns.contains(emailStrategy));
-        p.dateFormat = toDateFormat(rs.getString(8));
-        p.timeFormat = toTimeFormat(rs.getString(9));
-        p.relativeDateInChangeTable = toBoolean(rs.getString(10));
-        p.diffView = toDiffView(rs.getString(11));
-        p.sizeBarInChangeTable = toBoolean(rs.getString(12));
-        p.legacycidInChangeTable = toBoolean(rs.getString(13));
-        p.reviewCategoryStrategy = toReviewCategoryStrategy(rs.getString(14));
-        p.muteCommonPathPrefixes = toBoolean(rs.getString(15));
-        p.defaultBaseForMerges = GeneralPreferencesInfo.defaults().defaultBaseForMerges;
-        imports.put(accountId, p);
-      }
-    }
-
-    if (imports.isEmpty()) {
-      return;
-    }
-
-    try (Repository git = mgr.openRepository(allUsersName);
-        RevWalk rw = new RevWalk(git)) {
-      BatchRefUpdate bru = git.getRefDatabase().newBatchUpdate();
-      for (Map.Entry<Account.Id, GeneralPreferencesInfo> e : imports.entrySet()) {
-        try (MetaDataUpdate md =
-            new MetaDataUpdate(GitReferenceUpdated.DISABLED, allUsersName, git, bru)) {
-          md.getCommitBuilder().setAuthor(serverUser);
-          md.getCommitBuilder().setCommitter(serverUser);
-          VersionedAccountPreferences p = VersionedAccountPreferences.forUser(e.getKey());
-          p.load(md);
-          storeSection(
-              p.getConfig(),
-              UserConfigSections.GENERAL,
-              null,
-              e.getValue(),
-              GeneralPreferencesInfo.defaults());
-          p.commit(md);
-        }
-      }
-
-      bru.execute(rw, NullProgressMonitor.INSTANCE);
-    } catch (ConfigInvalidException | IOException ex) {
-      throw new OrmException(ex);
-    }
-  }
-
-  private String convertToModernNames(String s) {
-    return !Strings.isNullOrEmpty(s) && LEGACY_DISPLAYNAME_MAP.containsKey(s)
-        ? LEGACY_DISPLAYNAME_MAP.get(s)
-        : s;
-  }
-
-  private static DownloadCommand toDownloadCommand(String v) {
-    if (v == null) {
-      return DownloadCommand.CHECKOUT;
-    }
-    return DownloadCommand.valueOf(v);
-  }
-
-  private static DateFormat toDateFormat(String v) {
-    if (v == null) {
-      return DateFormat.STD;
-    }
-    return DateFormat.valueOf(v);
-  }
-
-  private static TimeFormat toTimeFormat(String v) {
-    if (v == null) {
-      return TimeFormat.HHMM_12;
-    }
-    return TimeFormat.valueOf(v);
-  }
-
-  private static DiffView toDiffView(String v) {
-    if (v == null) {
-      return DiffView.SIDE_BY_SIDE;
-    }
-    return DiffView.valueOf(v);
-  }
-
-  private static EmailStrategy toEmailStrategy(String v, boolean emailStrategyColumnExists)
-      throws OrmException {
-    if (v == null) {
-      return EmailStrategy.ENABLED;
-    }
-    if (emailStrategyColumnExists) {
-      return EmailStrategy.valueOf(v);
-    }
-    if (v.equals("N")) {
-      // EMAIL_STRATEGY='ENABLED' WHERE (COPY_SELF_ON_EMAIL='N')
-      return EmailStrategy.ENABLED;
-    } else if (v.equals("Y")) {
-      // EMAIL_STRATEGY='CC_ON_OWN_COMMENTS' WHERE (COPY_SELF_ON_EMAIL='Y')
-      return EmailStrategy.CC_ON_OWN_COMMENTS;
-    } else {
-      throw new OrmException("invalid value in accounts.copy_self_on_email: " + v);
-    }
-  }
-
-  private static ReviewCategoryStrategy toReviewCategoryStrategy(String v) {
-    if (v == null) {
-      return ReviewCategoryStrategy.NONE;
-    }
-    return ReviewCategoryStrategy.valueOf(v);
-  }
-
-  private static boolean toBoolean(String v) {
-    checkState(!Strings.isNullOrEmpty(v));
-    return v.equals("Y");
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_120.java b/java/com/google/gerrit/server/schema/Schema_120.java
deleted file mode 100644
index f2f3b99..0000000
--- a/java/com/google/gerrit/server/schema/Schema_120.java
+++ /dev/null
@@ -1,119 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.common.data.SubscribeSection;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.meta.MetaDataUpdate;
-import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.io.IOException;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.BatchRefUpdate;
-import org.eclipse.jgit.lib.NullProgressMonitor;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.RefSpec;
-
-public class Schema_120 extends SchemaVersion {
-
-  private final GitRepositoryManager mgr;
-  private final PersonIdent serverUser;
-
-  @Inject
-  Schema_120(
-      Provider<Schema_119> prior,
-      GitRepositoryManager mgr,
-      @GerritPersonIdent PersonIdent serverUser) {
-    super(prior);
-    this.mgr = mgr;
-    this.serverUser = serverUser;
-  }
-
-  private void allowSubmoduleSubscription(Branch.NameKey subbranch, Branch.NameKey superBranch)
-      throws OrmException {
-    try (Repository git = mgr.openRepository(subbranch.getParentKey());
-        RevWalk rw = new RevWalk(git)) {
-      BatchRefUpdate bru = git.getRefDatabase().newBatchUpdate();
-      try (MetaDataUpdate md =
-          new MetaDataUpdate(GitReferenceUpdated.DISABLED, subbranch.getParentKey(), git, bru)) {
-        md.getCommitBuilder().setAuthor(serverUser);
-        md.getCommitBuilder().setCommitter(serverUser);
-        md.setMessage("Added superproject subscription during upgrade");
-        ProjectConfig pc = ProjectConfig.read(md);
-
-        SubscribeSection s = null;
-        for (SubscribeSection s1 : pc.getSubscribeSections(subbranch)) {
-          if (s1.getProject().equals(superBranch.getParentKey())) {
-            s = s1;
-          }
-        }
-        if (s == null) {
-          s = new SubscribeSection(superBranch.getParentKey());
-          pc.addSubscribeSection(s);
-        }
-        RefSpec newRefSpec = new RefSpec(subbranch.get() + ":" + superBranch.get());
-
-        if (!s.getMatchingRefSpecs().contains(newRefSpec)) {
-          // For the migration we use only exact RefSpecs, we're not trying to
-          // generalize it.
-          s.addMatchingRefSpec(newRefSpec);
-        }
-
-        pc.commit(md);
-      }
-      bru.execute(rw, NullProgressMonitor.INSTANCE);
-    } catch (ConfigInvalidException | IOException e) {
-      throw new OrmException(e);
-    }
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    ui.message("Generating Superproject subscriptions table to submodule ACLs");
-
-    try (Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-        ResultSet rs =
-            stmt.executeQuery(
-                "SELECT "
-                    + "super_project_project_name, "
-                    + "super_project_branch_name, "
-                    + "submodule_project_name, "
-                    + "submodule_branch_name "
-                    + "FROM submodule_subscriptions")) {
-      while (rs.next()) {
-        Project.NameKey superproject = new Project.NameKey(rs.getString(1));
-        Branch.NameKey superbranch = new Branch.NameKey(superproject, rs.getString(2));
-
-        Project.NameKey submodule = new Project.NameKey(rs.getString(3));
-        Branch.NameKey subbranch = new Branch.NameKey(submodule, rs.getString(4));
-
-        allowSubmoduleSubscription(subbranch, superbranch);
-      }
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_121.java b/java/com/google/gerrit/server/schema/Schema_121.java
deleted file mode 100644
index 31b42fb..0000000
--- a/java/com/google/gerrit/server/schema/Schema_121.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_121 extends SchemaVersion {
-  @Inject
-  Schema_121(Provider<Schema_120> prior) {
-    super(prior);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_122.java b/java/com/google/gerrit/server/schema/Schema_122.java
deleted file mode 100644
index b5b799d..0000000
--- a/java/com/google/gerrit/server/schema/Schema_122.java
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_122 extends SchemaVersion {
-  @Inject
-  Schema_122(Provider<Schema_121> prior) {
-    super(prior);
-  }
-
-  // Adds tag column
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_123.java b/java/com/google/gerrit/server/schema/Schema_123.java
deleted file mode 100644
index 31cfd5d..0000000
--- a/java/com/google/gerrit/server/schema/Schema_123.java
+++ /dev/null
@@ -1,86 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.common.collect.ListMultimap;
-import com.google.common.collect.MultimapBuilder;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.StarredChangesUtil;
-import com.google.gerrit.server.StarredChangesUtil.IllegalLabelException;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.io.IOException;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.Map;
-import org.eclipse.jgit.lib.BatchRefUpdate;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.lib.TextProgressMonitor;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.ReceiveCommand;
-
-public class Schema_123 extends SchemaVersion {
-  private final GitRepositoryManager repoManager;
-  private final AllUsersName allUsersName;
-
-  @Inject
-  Schema_123(
-      Provider<Schema_122> prior, GitRepositoryManager repoManager, AllUsersName allUsersName) {
-    super(prior);
-    this.repoManager = repoManager;
-    this.allUsersName = allUsersName;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    ListMultimap<Account.Id, Change.Id> imports =
-        MultimapBuilder.hashKeys().arrayListValues().build();
-    try (Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-        ResultSet rs = stmt.executeQuery("SELECT account_id, change_id FROM starred_changes")) {
-      while (rs.next()) {
-        Account.Id accountId = new Account.Id(rs.getInt(1));
-        Change.Id changeId = new Change.Id(rs.getInt(2));
-        imports.put(accountId, changeId);
-      }
-    }
-
-    if (imports.isEmpty()) {
-      return;
-    }
-
-    try (Repository git = repoManager.openRepository(allUsersName);
-        RevWalk rw = new RevWalk(git)) {
-      BatchRefUpdate bru = git.getRefDatabase().newBatchUpdate();
-      ObjectId id = StarredChangesUtil.writeLabels(git, StarredChangesUtil.DEFAULT_LABELS);
-      for (Map.Entry<Account.Id, Change.Id> e : imports.entries()) {
-        bru.addCommand(
-            new ReceiveCommand(
-                ObjectId.zeroId(), id, RefNames.refsStarredChanges(e.getValue(), e.getKey())));
-      }
-      bru.execute(rw, new TextProgressMonitor());
-    } catch (IOException | IllegalLabelException ex) {
-      throw new OrmException(ex);
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_124.java b/java/com/google/gerrit/server/schema/Schema_124.java
deleted file mode 100644
index 6164fd1..0000000
--- a/java/com/google/gerrit/server/schema/Schema_124.java
+++ /dev/null
@@ -1,138 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import static java.util.Comparator.comparing;
-
-import com.google.common.base.Strings;
-import com.google.common.collect.ListMultimap;
-import com.google.common.collect.MultimapBuilder;
-import com.google.common.collect.Ordering;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.account.AccountSshKey;
-import com.google.gerrit.server.account.VersionedAuthorizedKeys;
-import com.google.gerrit.server.account.VersionedAuthorizedKeys.SimpleSshKeyCreator;
-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.git.meta.MetaDataUpdate;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.io.IOException;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.BatchRefUpdate;
-import org.eclipse.jgit.lib.NullProgressMonitor;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevWalk;
-
-public class Schema_124 extends SchemaVersion {
-  private final GitRepositoryManager repoManager;
-  private final AllUsersName allUsersName;
-  private final PersonIdent serverUser;
-
-  @Inject
-  Schema_124(
-      Provider<Schema_123> prior,
-      GitRepositoryManager repoManager,
-      AllUsersName allUsersName,
-      @GerritPersonIdent PersonIdent serverUser) {
-    super(prior);
-    this.repoManager = repoManager;
-    this.allUsersName = allUsersName;
-    this.serverUser = serverUser;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    ListMultimap<Account.Id, AccountSshKey> imports =
-        MultimapBuilder.hashKeys().arrayListValues().build();
-    try (Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-        ResultSet rs =
-            stmt.executeQuery(
-                "SELECT "
-                    + "account_id, "
-                    + "seq, "
-                    + "ssh_public_key, "
-                    + "valid "
-                    + "FROM account_ssh_keys")) {
-      while (rs.next()) {
-        Account.Id accountId = new Account.Id(rs.getInt(1));
-        int seq = rs.getInt(2);
-        String sshPublicKey = rs.getString(3);
-        boolean valid = toBoolean(rs.getString(4));
-        AccountSshKey key = AccountSshKey.create(accountId, seq, sshPublicKey, valid);
-        imports.put(accountId, key);
-      }
-    }
-
-    if (imports.isEmpty()) {
-      return;
-    }
-
-    try (Repository git = repoManager.openRepository(allUsersName);
-        RevWalk rw = new RevWalk(git)) {
-      BatchRefUpdate bru = git.getRefDatabase().newBatchUpdate();
-
-      for (Map.Entry<Account.Id, Collection<AccountSshKey>> e : imports.asMap().entrySet()) {
-        try (MetaDataUpdate md =
-            new MetaDataUpdate(GitReferenceUpdated.DISABLED, allUsersName, git, bru)) {
-          md.getCommitBuilder().setAuthor(serverUser);
-          md.getCommitBuilder().setCommitter(serverUser);
-
-          VersionedAuthorizedKeys authorizedKeys =
-              new VersionedAuthorizedKeys(new SimpleSshKeyCreator(), e.getKey());
-          authorizedKeys.load(md);
-          authorizedKeys.setKeys(fixInvalidSequenceNumbers(e.getValue()));
-          authorizedKeys.commit(md);
-        }
-      }
-
-      bru.execute(rw, NullProgressMonitor.INSTANCE);
-    } catch (ConfigInvalidException | IOException ex) {
-      throw new OrmException(ex);
-    }
-  }
-
-  private Collection<AccountSshKey> fixInvalidSequenceNumbers(Collection<AccountSshKey> keys) {
-    Ordering<AccountSshKey> o = Ordering.from(comparing(AccountSshKey::seq));
-    List<AccountSshKey> fixedKeys = new ArrayList<>(keys);
-    AccountSshKey minKey = o.min(keys);
-    while (minKey.seq() <= 0) {
-      AccountSshKey fixedKey =
-          AccountSshKey.create(
-              minKey.accountId(), Math.max(o.max(keys).seq() + 1, 1), minKey.sshPublicKey());
-      Collections.replaceAll(fixedKeys, minKey, fixedKey);
-      minKey = o.min(fixedKeys);
-    }
-    return fixedKeys;
-  }
-
-  private static boolean toBoolean(String v) {
-    return !Strings.isNullOrEmpty(v) && v.equalsIgnoreCase("Y");
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_125.java b/java/com/google/gerrit/server/schema/Schema_125.java
deleted file mode 100644
index 7aab7c7..0000000
--- a/java/com/google/gerrit/server/schema/Schema_125.java
+++ /dev/null
@@ -1,128 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static com.google.gerrit.server.schema.AclUtil.grant;
-
-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.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.config.AllProjectsName;
-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.git.meta.MetaDataUpdate;
-import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gerrit.server.project.RefPattern;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.io.IOException;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
-
-public class Schema_125 extends SchemaVersion {
-  private static final String COMMIT_MSG =
-      "Assign default permissions on user branches\n"
-          + "\n"
-          + "By default each user should be able to read and update the own user\n"
-          + "branch. Also the user should be able to approve and submit changes for\n"
-          + "the own user branch. Assign default permissions for this and remove the\n"
-          + "old exclusive read protection from the user branches.\n";
-
-  private final GitRepositoryManager repoManager;
-  private final AllUsersName allUsersName;
-  private final AllProjectsName allProjectsName;
-  private final SystemGroupBackend systemGroupBackend;
-  private final PersonIdent serverUser;
-
-  @Inject
-  Schema_125(
-      Provider<Schema_124> prior,
-      GitRepositoryManager repoManager,
-      AllUsersName allUsersName,
-      AllProjectsName allProjectsName,
-      SystemGroupBackend systemGroupBackend,
-      @GerritPersonIdent PersonIdent serverUser) {
-    super(prior);
-    this.repoManager = repoManager;
-    this.allUsersName = allUsersName;
-    this.allProjectsName = allProjectsName;
-    this.systemGroupBackend = systemGroupBackend;
-    this.serverUser = serverUser;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
-    try (Repository git = repoManager.openRepository(allUsersName);
-        MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, allUsersName, git)) {
-      ProjectConfig config = ProjectConfig.read(md);
-
-      config
-          .getAccessSection(RefNames.REFS_USERS + "*", true)
-          .remove(new Permission(Permission.READ));
-      GroupReference registered = systemGroupBackend.getGroup(REGISTERED_USERS);
-      AccessSection users =
-          config.getAccessSection(
-              RefNames.REFS_USERS + "${" + RefPattern.USERID_SHARDED + "}", true);
-      grant(config, users, Permission.READ, true, registered);
-      grant(config, users, Permission.PUSH, true, registered);
-      grant(config, users, Permission.SUBMIT, true, registered);
-
-      for (LabelType lt : getLabelTypes(config)) {
-        if ("Code-Review".equals(lt.getName()) || "Verified".equals(lt.getName())) {
-          grant(config, users, lt, lt.getMin().getValue(), lt.getMax().getValue(), registered);
-        }
-      }
-
-      md.getCommitBuilder().setAuthor(serverUser);
-      md.getCommitBuilder().setCommitter(serverUser);
-      md.setMessage(COMMIT_MSG);
-      config.commit(md);
-    } catch (ConfigInvalidException | IOException ex) {
-      throw new OrmException(ex);
-    }
-  }
-
-  private Collection<LabelType> getLabelTypes(ProjectConfig config)
-      throws IOException, ConfigInvalidException {
-    Map<String, LabelType> labelTypes = new HashMap<>(config.getLabelSections());
-    Project.NameKey parent = config.getProject().getParent(allProjectsName);
-    while (parent != null) {
-      try (Repository git = repoManager.openRepository(parent);
-          MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, parent, git)) {
-        ProjectConfig parentConfig = ProjectConfig.read(md);
-        for (LabelType lt : parentConfig.getLabelSections().values()) {
-          if (!labelTypes.containsKey(lt.getName())) {
-            labelTypes.put(lt.getName(), lt);
-          }
-        }
-        parent = parentConfig.getProject().getParent(allProjectsName);
-      }
-    }
-    return labelTypes.values();
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_126.java b/java/com/google/gerrit/server/schema/Schema_126.java
deleted file mode 100644
index 5dbda72..0000000
--- a/java/com/google/gerrit/server/schema/Schema_126.java
+++ /dev/null
@@ -1,86 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static com.google.gerrit.server.schema.AclUtil.grant;
-
-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.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-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.git.meta.MetaDataUpdate;
-import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gerrit.server.project.RefPattern;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.io.IOException;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
-
-public class Schema_126 extends SchemaVersion {
-  private static final String COMMIT_MSG = "Fix default permissions on user branches";
-
-  private final GitRepositoryManager repoManager;
-  private final AllUsersName allUsersName;
-  private final SystemGroupBackend systemGroupBackend;
-  private final PersonIdent serverUser;
-
-  @Inject
-  Schema_126(
-      Provider<Schema_125> prior,
-      GitRepositoryManager repoManager,
-      AllUsersName allUsersName,
-      SystemGroupBackend systemGroupBackend,
-      @GerritPersonIdent PersonIdent serverUser) {
-    super(prior);
-    this.repoManager = repoManager;
-    this.allUsersName = allUsersName;
-    this.systemGroupBackend = systemGroupBackend;
-    this.serverUser = serverUser;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
-    try (Repository git = repoManager.openRepository(allUsersName);
-        MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, allUsersName, git)) {
-      ProjectConfig config = ProjectConfig.read(md);
-
-      String refsUsersShardedId = RefNames.REFS_USERS + "${" + RefPattern.USERID_SHARDED + "}";
-      config.remove(config.getAccessSection(refsUsersShardedId));
-
-      GroupReference registered = systemGroupBackend.getGroup(REGISTERED_USERS);
-      AccessSection users = config.getAccessSection(refsUsersShardedId, true);
-      grant(config, users, Permission.READ, false, true, registered);
-      grant(config, users, Permission.PUSH, false, true, registered);
-      grant(config, users, Permission.SUBMIT, false, true, registered);
-
-      md.getCommitBuilder().setAuthor(serverUser);
-      md.getCommitBuilder().setCommitter(serverUser);
-      md.setMessage(COMMIT_MSG);
-      config.commit(md);
-    } catch (ConfigInvalidException | IOException ex) {
-      throw new OrmException(ex);
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_127.java b/java/com/google/gerrit/server/schema/Schema_127.java
deleted file mode 100644
index d246b75..0000000
--- a/java/com/google/gerrit/server/schema/Schema_127.java
+++ /dev/null
@@ -1,87 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.config.ThreadSettingsConfig;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.sql.Connection;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import org.eclipse.jgit.lib.Config;
-
-public class Schema_127 extends SchemaVersion {
-  private static final int MAX_BATCH_SIZE = 1000;
-
-  private final SitePaths sitePaths;
-  private final Config cfg;
-  private final ThreadSettingsConfig threadSettingsConfig;
-
-  @Inject
-  Schema_127(
-      Provider<Schema_126> prior,
-      SitePaths sitePaths,
-      @GerritServerConfig Config cfg,
-      ThreadSettingsConfig threadSettingsConfig) {
-    super(prior);
-    this.sitePaths = sitePaths;
-    this.cfg = cfg;
-    this.threadSettingsConfig = threadSettingsConfig;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
-    JdbcAccountPatchReviewStore jdbcAccountPatchReviewStore =
-        JdbcAccountPatchReviewStore.createAccountPatchReviewStore(
-            cfg, sitePaths, threadSettingsConfig);
-    jdbcAccountPatchReviewStore.dropTableIfExists();
-    jdbcAccountPatchReviewStore.createTableIfNotExists();
-    try (Connection con = jdbcAccountPatchReviewStore.getConnection();
-        PreparedStatement stmt =
-            con.prepareStatement(
-                "INSERT INTO account_patch_reviews "
-                    + "(account_id, change_id, patch_set_id, file_name) VALUES "
-                    + "(?, ?, ?, ?)")) {
-      int batchCount = 0;
-
-      try (Statement s = newStatement(db);
-          ResultSet rs = s.executeQuery("SELECT * from account_patch_reviews")) {
-        while (rs.next()) {
-          stmt.setInt(1, rs.getInt("account_id"));
-          stmt.setInt(2, rs.getInt("change_id"));
-          stmt.setInt(3, rs.getInt("patch_set_id"));
-          stmt.setString(4, rs.getString("file_name"));
-          stmt.addBatch();
-          batchCount++;
-          if (batchCount >= MAX_BATCH_SIZE) {
-            stmt.executeBatch();
-            batchCount = 0;
-          }
-        }
-      }
-      if (batchCount > 0) {
-        stmt.executeBatch();
-      }
-    } catch (SQLException e) {
-      throw jdbcAccountPatchReviewStore.convertError("insert", e);
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_128.java b/java/com/google/gerrit/server/schema/Schema_128.java
deleted file mode 100644
index bd6b76a..0000000
--- a/java/com/google/gerrit/server/schema/Schema_128.java
+++ /dev/null
@@ -1,80 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static com.google.gerrit.server.schema.AclUtil.grant;
-
-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.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.meta.MetaDataUpdate;
-import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.io.IOException;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
-
-public class Schema_128 extends SchemaVersion {
-  private static final String COMMIT_MSG = "Add addPatchSet permission to all projects";
-
-  private final GitRepositoryManager repoManager;
-  private final AllProjectsName allProjectsName;
-  private final SystemGroupBackend systemGroupBackend;
-  private final PersonIdent serverUser;
-
-  @Inject
-  Schema_128(
-      Provider<Schema_127> prior,
-      GitRepositoryManager repoManager,
-      AllProjectsName allProjectsName,
-      SystemGroupBackend systemGroupBackend,
-      @GerritPersonIdent PersonIdent serverUser) {
-    super(prior);
-    this.repoManager = repoManager;
-    this.allProjectsName = allProjectsName;
-    this.systemGroupBackend = systemGroupBackend;
-    this.serverUser = serverUser;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
-    try (Repository git = repoManager.openRepository(allProjectsName);
-        MetaDataUpdate md =
-            new MetaDataUpdate(GitReferenceUpdated.DISABLED, allProjectsName, git)) {
-      ProjectConfig config = ProjectConfig.read(md);
-
-      GroupReference registered = systemGroupBackend.getGroup(REGISTERED_USERS);
-      AccessSection refsFor = config.getAccessSection("refs/for/*", true);
-      grant(config, refsFor, Permission.ADD_PATCH_SET, false, false, registered);
-
-      md.getCommitBuilder().setAuthor(serverUser);
-      md.getCommitBuilder().setCommitter(serverUser);
-      md.setMessage(COMMIT_MSG);
-      config.commit(md);
-    } catch (ConfigInvalidException | IOException ex) {
-      throw new OrmException(ex);
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_129.java b/java/com/google/gerrit/server/schema/Schema_129.java
deleted file mode 100644
index 73ce3c3..0000000
--- a/java/com/google/gerrit/server/schema/Schema_129.java
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.sql.SQLException;
-import java.sql.Statement;
-
-public class Schema_129 extends SchemaVersion {
-
-  @Inject
-  Schema_129(Provider<Schema_128> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void preUpdateSchema(ReviewDb db) throws OrmException {
-    try (Statement stmt = ((JdbcSchema) db).getConnection().createStatement()) {
-      stmt.execute("ALTER TABLE patch_sets MODIFY groups clob");
-    } catch (SQLException e) {
-      // Ignore.  Type may have already been modified manually.
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_130.java b/java/com/google/gerrit/server/schema/Schema_130.java
deleted file mode 100644
index 91cdcda..0000000
--- a/java/com/google/gerrit/server/schema/Schema_130.java
+++ /dev/null
@@ -1,74 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import static java.util.stream.Collectors.joining;
-
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-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.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.util.SortedSet;
-import java.util.TreeSet;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
-
-public class Schema_130 extends SchemaVersion {
-  private static final String COMMIT_MSG =
-      "Remove force option from 'Push Annotated Tag' permission\n"
-          + "\n"
-          + "The force option on 'Push Annotated Tag' had no effect and is no longer\n"
-          + "supported.";
-
-  private final GitRepositoryManager repoManager;
-  private final PersonIdent serverUser;
-
-  @Inject
-  Schema_130(
-      Provider<Schema_129> prior,
-      GitRepositoryManager repoManager,
-      @GerritPersonIdent PersonIdent serverUser) {
-    super(prior);
-    this.repoManager = repoManager;
-    this.serverUser = serverUser;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
-    SortedSet<Project.NameKey> repoList = repoManager.list();
-    SortedSet<Project.NameKey> repoUpgraded = new TreeSet<>();
-    ui.message("\tMigrating " + repoList.size() + " repositories ...");
-    for (Project.NameKey projectName : repoList) {
-      try (Repository git = repoManager.openRepository(projectName);
-          MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, projectName, git)) {
-        ProjectConfigSchemaUpdate cfg = ProjectConfigSchemaUpdate.read(md);
-        cfg.removeForceFromPermission("pushTag");
-        if (cfg.isUpdated()) {
-          repoUpgraded.add(projectName);
-        }
-        cfg.save(serverUser, COMMIT_MSG);
-      } catch (Exception ex) {
-        throw new OrmException("Cannot migrate project " + projectName, ex);
-      }
-    }
-    ui.message("\tMigration completed:  " + repoUpgraded.size() + " repositories updated:");
-    ui.message("\t" + repoUpgraded.stream().map(Project.NameKey::get).collect(joining(" ")));
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_131.java b/java/com/google/gerrit/server/schema/Schema_131.java
deleted file mode 100644
index 3755211..0000000
--- a/java/com/google/gerrit/server/schema/Schema_131.java
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import static java.util.stream.Collectors.joining;
-
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.meta.MetaDataUpdate;
-import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.io.IOException;
-import java.util.SortedSet;
-import java.util.TreeSet;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
-
-public class Schema_131 extends SchemaVersion {
-  private static final String COMMIT_MSG =
-      "Rename 'Push Annotated/Signed Tag' permission to 'Create Annotated/Signed Tag'";
-
-  private final GitRepositoryManager repoManager;
-  private final PersonIdent serverUser;
-
-  @Inject
-  Schema_131(
-      Provider<Schema_130> prior,
-      GitRepositoryManager repoManager,
-      @GerritPersonIdent PersonIdent serverUser) {
-    super(prior);
-    this.repoManager = repoManager;
-    this.serverUser = serverUser;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
-    SortedSet<Project.NameKey> repoList = repoManager.list();
-    SortedSet<Project.NameKey> repoUpgraded = new TreeSet<>();
-    ui.message("\tMigrating " + repoList.size() + " repositories ...");
-    for (Project.NameKey projectName : repoList) {
-      try (Repository git = repoManager.openRepository(projectName);
-          MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, projectName, git)) {
-        ProjectConfig config = ProjectConfig.read(md);
-        if (config.hasLegacyPermissions()) {
-          md.getCommitBuilder().setAuthor(serverUser);
-          md.getCommitBuilder().setCommitter(serverUser);
-          md.setMessage(COMMIT_MSG);
-          config.commit(md);
-          repoUpgraded.add(projectName);
-        }
-      } catch (ConfigInvalidException | IOException ex) {
-        throw new OrmException("Cannot migrate project " + projectName, ex);
-      }
-    }
-    ui.message("\tMigration completed:  " + repoUpgraded.size() + " repositories updated:");
-    ui.message("\t" + repoUpgraded.stream().map(Project.NameKey::get).collect(joining(" ")));
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_132.java b/java/com/google/gerrit/server/schema/Schema_132.java
deleted file mode 100644
index 7c1cde8..0000000
--- a/java/com/google/gerrit/server/schema/Schema_132.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_132 extends SchemaVersion {
-  @Inject
-  Schema_132(Provider<Schema_131> prior) {
-    super(prior);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_133.java b/java/com/google/gerrit/server/schema/Schema_133.java
deleted file mode 100644
index 31d330b..0000000
--- a/java/com/google/gerrit/server/schema/Schema_133.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_133 extends SchemaVersion {
-  @Inject
-  Schema_133(Provider<Schema_132> prior) {
-    super(prior);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_134.java b/java/com/google/gerrit/server/schema/Schema_134.java
deleted file mode 100644
index fa01ff3..0000000
--- a/java/com/google/gerrit/server/schema/Schema_134.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_134 extends SchemaVersion {
-  @Inject
-  Schema_134(Provider<Schema_133> prior) {
-    super(prior);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_135.java b/java/com/google/gerrit/server/schema/Schema_135.java
deleted file mode 100644
index df3dcaf..0000000
--- a/java/com/google/gerrit/server/schema/Schema_135.java
+++ /dev/null
@@ -1,98 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import static com.google.gerrit.server.group.SystemGroupBackend.PROJECT_OWNERS;
-import static java.util.stream.Collectors.toSet;
-
-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.RefNames;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.meta.MetaDataUpdate;
-import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.io.IOException;
-import java.util.Set;
-import java.util.stream.Stream;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
-
-public class Schema_135 extends SchemaVersion {
-  private static final String COMMIT_MSG =
-      "Allow admins and project owners to create refs/meta/config";
-
-  private final GitRepositoryManager repoManager;
-  private final AllProjectsName allProjectsName;
-  private final SystemGroupBackend systemGroupBackend;
-  private final PersonIdent serverUser;
-
-  @Inject
-  Schema_135(
-      Provider<Schema_134> prior,
-      GitRepositoryManager repoManager,
-      AllProjectsName allProjectsName,
-      SystemGroupBackend systemGroupBackend,
-      @GerritPersonIdent PersonIdent serverUser) {
-    super(prior);
-    this.repoManager = repoManager;
-    this.allProjectsName = allProjectsName;
-    this.systemGroupBackend = systemGroupBackend;
-    this.serverUser = serverUser;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
-    try (Repository git = repoManager.openRepository(allProjectsName);
-        MetaDataUpdate md =
-            new MetaDataUpdate(GitReferenceUpdated.DISABLED, allProjectsName, git)) {
-      ProjectConfig config = ProjectConfig.read(md);
-
-      AccessSection meta = config.getAccessSection(RefNames.REFS_CONFIG, true);
-      Permission createRefsMetaConfigPermission = meta.getPermission(Permission.CREATE, true);
-
-      Set<GroupReference> groups =
-          Stream.concat(
-                  config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true)
-                      .getPermission(GlobalCapability.ADMINISTRATE_SERVER, true).getRules().stream()
-                      .map(PermissionRule::getGroup),
-                  Stream.of(systemGroupBackend.getGroup(PROJECT_OWNERS)))
-              .filter(g -> createRefsMetaConfigPermission.getRule(g) == null)
-              .collect(toSet());
-
-      for (GroupReference group : groups) {
-        createRefsMetaConfigPermission.add(new PermissionRule(config.resolve(group)));
-      }
-
-      md.getCommitBuilder().setAuthor(serverUser);
-      md.getCommitBuilder().setCommitter(serverUser);
-      md.setMessage(COMMIT_MSG);
-      config.commit(md);
-    } catch (ConfigInvalidException | IOException ex) {
-      throw new OrmException(ex);
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_136.java b/java/com/google/gerrit/server/schema/Schema_136.java
deleted file mode 100644
index a4b1c82..0000000
--- a/java/com/google/gerrit/server/schema/Schema_136.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_136 extends SchemaVersion {
-  @Inject
-  Schema_136(Provider<Schema_135> prior) {
-    super(prior);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_137.java b/java/com/google/gerrit/server/schema/Schema_137.java
deleted file mode 100644
index 1b4102f..0000000
--- a/java/com/google/gerrit/server/schema/Schema_137.java
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-/* change the type of SystemConfig#sitePath to CLOB */
-public class Schema_137 extends SchemaVersion {
-  @Inject
-  Schema_137(Provider<Schema_136> prior) {
-    super(prior);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_138.java b/java/com/google/gerrit/server/schema/Schema_138.java
deleted file mode 100644
index f824ee1..0000000
--- a/java/com/google/gerrit/server/schema/Schema_138.java
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-/* Add resolved field to PatchLineComment */
-public class Schema_138 extends SchemaVersion {
-  @Inject
-  Schema_138(Provider<Schema_137> prior) {
-    super(prior);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_139.java b/java/com/google/gerrit/server/schema/Schema_139.java
deleted file mode 100644
index cdde7e4..0000000
--- a/java/com/google/gerrit/server/schema/Schema_139.java
+++ /dev/null
@@ -1,212 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import static com.google.common.base.Preconditions.checkState;
-
-import com.google.auto.value.AutoValue;
-import com.google.common.base.Strings;
-import com.google.common.collect.ListMultimap;
-import com.google.common.collect.MultimapBuilder;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.account.AccountConfig;
-import com.google.gerrit.server.account.InternalAccountUpdate;
-import com.google.gerrit.server.account.ProjectWatches.NotifyType;
-import com.google.gerrit.server.account.ProjectWatches.ProjectWatchKey;
-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.git.meta.MetaDataUpdate;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.io.IOException;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.Collection;
-import java.util.EnumSet;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.BatchRefUpdate;
-import org.eclipse.jgit.lib.NullProgressMonitor;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevWalk;
-
-public class Schema_139 extends SchemaVersion {
-  private static final String MSG = "Migrate project watches to git";
-
-  private final GitRepositoryManager repoManager;
-  private final AllUsersName allUsersName;
-  private final PersonIdent serverUser;
-
-  @Inject
-  Schema_139(
-      Provider<Schema_138> prior,
-      GitRepositoryManager repoManager,
-      AllUsersName allUsersName,
-      @GerritPersonIdent PersonIdent serverUser) {
-    super(prior);
-    this.repoManager = repoManager;
-    this.allUsersName = allUsersName;
-    this.serverUser = serverUser;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    ListMultimap<Account.Id, ProjectWatch> imports =
-        MultimapBuilder.hashKeys().arrayListValues().build();
-    try (Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-        ResultSet rs =
-            stmt.executeQuery(
-                "SELECT "
-                    + "account_id, "
-                    + "project_name, "
-                    + "filter, "
-                    + "notify_abandoned_changes, "
-                    + "notify_all_comments, "
-                    + "notify_new_changes, "
-                    + "notify_new_patch_sets, "
-                    + "notify_submitted_changes "
-                    + "FROM account_project_watches")) {
-      while (rs.next()) {
-        Account.Id accountId = new Account.Id(rs.getInt(1));
-        ProjectWatch.Builder b =
-            ProjectWatch.builder()
-                .project(new Project.NameKey(rs.getString(2)))
-                .filter(rs.getString(3))
-                .notifyAbandonedChanges(toBoolean(rs.getString(4)))
-                .notifyAllComments(toBoolean(rs.getString(5)))
-                .notifyNewChanges(toBoolean(rs.getString(6)))
-                .notifyNewPatchSets(toBoolean(rs.getString(7)))
-                .notifySubmittedChanges(toBoolean(rs.getString(8)));
-        imports.put(accountId, b.build());
-      }
-    }
-
-    if (imports.isEmpty()) {
-      return;
-    }
-
-    try (Repository git = repoManager.openRepository(allUsersName);
-        RevWalk rw = new RevWalk(git)) {
-      BatchRefUpdate bru = git.getRefDatabase().newBatchUpdate();
-      bru.setRefLogIdent(serverUser);
-      bru.setRefLogMessage(MSG, false);
-
-      for (Map.Entry<Account.Id, Collection<ProjectWatch>> e : imports.asMap().entrySet()) {
-        Map<ProjectWatchKey, Set<NotifyType>> projectWatches = new HashMap<>();
-        for (ProjectWatch projectWatch : e.getValue()) {
-          ProjectWatchKey key =
-              ProjectWatchKey.create(projectWatch.project(), projectWatch.filter());
-          if (projectWatches.containsKey(key)) {
-            throw new OrmDuplicateKeyException(
-                "Duplicate key for watched project: " + key.toString());
-          }
-          Set<NotifyType> notifyValues = EnumSet.noneOf(NotifyType.class);
-          if (projectWatch.notifyAbandonedChanges()) {
-            notifyValues.add(NotifyType.ABANDONED_CHANGES);
-          }
-          if (projectWatch.notifyAllComments()) {
-            notifyValues.add(NotifyType.ALL_COMMENTS);
-          }
-          if (projectWatch.notifyNewChanges()) {
-            notifyValues.add(NotifyType.NEW_CHANGES);
-          }
-          if (projectWatch.notifyNewPatchSets()) {
-            notifyValues.add(NotifyType.NEW_PATCHSETS);
-          }
-          if (projectWatch.notifySubmittedChanges()) {
-            notifyValues.add(NotifyType.SUBMITTED_CHANGES);
-          }
-          projectWatches.put(key, notifyValues);
-        }
-
-        try (MetaDataUpdate md =
-            new MetaDataUpdate(GitReferenceUpdated.DISABLED, allUsersName, git, bru)) {
-          md.getCommitBuilder().setAuthor(serverUser);
-          md.getCommitBuilder().setCommitter(serverUser);
-          md.setMessage(MSG);
-
-          AccountConfig accountConfig = new AccountConfig(e.getKey(), allUsersName, git);
-          accountConfig.load(md);
-          accountConfig.setAccountUpdate(
-              InternalAccountUpdate.builder()
-                  .deleteProjectWatches(accountConfig.getProjectWatches().keySet())
-                  .updateProjectWatches(projectWatches)
-                  .build());
-          accountConfig.commit(md);
-        }
-      }
-      bru.execute(rw, NullProgressMonitor.INSTANCE);
-    } catch (IOException | ConfigInvalidException ex) {
-      throw new OrmException(ex);
-    }
-  }
-
-  @AutoValue
-  abstract static class ProjectWatch {
-    abstract Project.NameKey project();
-
-    abstract @Nullable String filter();
-
-    abstract boolean notifyAbandonedChanges();
-
-    abstract boolean notifyAllComments();
-
-    abstract boolean notifyNewChanges();
-
-    abstract boolean notifyNewPatchSets();
-
-    abstract boolean notifySubmittedChanges();
-
-    static Builder builder() {
-      return new AutoValue_Schema_139_ProjectWatch.Builder();
-    }
-
-    @AutoValue.Builder
-    abstract static class Builder {
-      abstract Builder project(Project.NameKey project);
-
-      abstract Builder filter(@Nullable String filter);
-
-      abstract Builder notifyAbandonedChanges(boolean notifyAbandonedChanges);
-
-      abstract Builder notifyAllComments(boolean notifyAllComments);
-
-      abstract Builder notifyNewChanges(boolean notifyNewChanges);
-
-      abstract Builder notifyNewPatchSets(boolean notifyNewPatchSets);
-
-      abstract Builder notifySubmittedChanges(boolean notifySubmittedChanges);
-
-      abstract ProjectWatch build();
-    }
-  }
-
-  private static boolean toBoolean(String v) {
-    checkState(!Strings.isNullOrEmpty(v));
-    return v.equals("Y");
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_140.java b/java/com/google/gerrit/server/schema/Schema_140.java
deleted file mode 100644
index bdc5f55..0000000
--- a/java/com/google/gerrit/server/schema/Schema_140.java
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-/** Remove ChangeMessage sequence. */
-public class Schema_140 extends SchemaVersion {
-  @Inject
-  Schema_140(Provider<Schema_139> prior) {
-    super(prior);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_141.java b/java/com/google/gerrit/server/schema/Schema_141.java
deleted file mode 100644
index c081ea9..0000000
--- a/java/com/google/gerrit/server/schema/Schema_141.java
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-/** Add status field to account. */
-public class Schema_141 extends SchemaVersion {
-  @Inject
-  Schema_141(Provider<Schema_140> prior) {
-    super(prior);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_142.java b/java/com/google/gerrit/server/schema/Schema_142.java
deleted file mode 100644
index e67ae2f..0000000
--- a/java/com/google/gerrit/server/schema/Schema_142.java
+++ /dev/null
@@ -1,77 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
-
-import com.google.common.base.Strings;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.account.HashedPassword;
-import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-
-public class Schema_142 extends SchemaVersion {
-  private static final int MAX_BATCH_SIZE = 1000;
-
-  @Inject
-  Schema_142(Provider<Schema_141> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    try (PreparedStatement updateStmt =
-        ((JdbcSchema) db)
-            .getConnection()
-            .prepareStatement(
-                "UPDATE account_external_ids " + "SET password = ? " + "WHERE external_id = ?")) {
-      int batchCount = 0;
-
-      try (Statement stmt = newStatement(db);
-          ResultSet rs =
-              stmt.executeQuery("SELECT external_id, password FROM account_external_ids")) {
-        while (rs.next()) {
-          String externalId = rs.getString("external_id");
-          String password = rs.getString("password");
-          if (!ExternalId.Key.parse(externalId).isScheme(SCHEME_USERNAME)
-              || Strings.isNullOrEmpty(password)) {
-            continue;
-          }
-
-          HashedPassword hashed = HashedPassword.fromPassword(password);
-          updateStmt.setString(1, hashed.encode());
-          updateStmt.setString(2, externalId);
-          updateStmt.addBatch();
-          batchCount++;
-          if (batchCount >= MAX_BATCH_SIZE) {
-            updateStmt.executeBatch();
-            batchCount = 0;
-          }
-        }
-      }
-
-      if (batchCount > 0) {
-        updateStmt.executeBatch();
-      }
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_143.java b/java/com/google/gerrit/server/schema/Schema_143.java
deleted file mode 100644
index b190b29..0000000
--- a/java/com/google/gerrit/server/schema/Schema_143.java
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-/** Add isPrivate field to change. */
-public class Schema_143 extends SchemaVersion {
-  @Inject
-  Schema_143(Provider<Schema_142> prior) {
-    super(prior);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_144.java b/java/com/google/gerrit/server/schema/Schema_144.java
deleted file mode 100644
index bb0cbca..0000000
--- a/java/com/google/gerrit/server/schema/Schema_144.java
+++ /dev/null
@@ -1,97 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.gerrit.server.account.externalids.ExternalIdNotes;
-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.git.meta.MetaDataUpdate;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.io.IOException;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.HashSet;
-import java.util.Set;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
-
-public class Schema_144 extends SchemaVersion {
-  private static final String COMMIT_MSG = "Import external IDs from ReviewDb";
-
-  private final GitRepositoryManager repoManager;
-  private final AllUsersName allUsersName;
-  private final PersonIdent serverIdent;
-
-  @Inject
-  Schema_144(
-      Provider<Schema_143> prior,
-      GitRepositoryManager repoManager,
-      AllUsersName allUsersName,
-      @GerritPersonIdent PersonIdent serverIdent) {
-    super(prior);
-    this.repoManager = repoManager;
-    this.allUsersName = allUsersName;
-    this.serverIdent = serverIdent;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    Set<ExternalId> toAdd = new HashSet<>();
-    try (Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-        ResultSet rs =
-            stmt.executeQuery(
-                "SELECT "
-                    + "account_id, "
-                    + "email_address, "
-                    + "password, "
-                    + "external_id "
-                    + "FROM account_external_ids")) {
-      while (rs.next()) {
-        Account.Id accountId = new Account.Id(rs.getInt(1));
-        String email = rs.getString(2);
-        String password = rs.getString(3);
-        String externalId = rs.getString(4);
-
-        toAdd.add(ExternalId.create(ExternalId.Key.parse(externalId), accountId, email, password));
-      }
-    }
-
-    try {
-      try (Repository repo = repoManager.openRepository(allUsersName)) {
-        ExternalIdNotes extIdNotes = ExternalIdNotes.loadNoCacheUpdate(allUsersName, repo);
-        extIdNotes.upsert(toAdd);
-        try (MetaDataUpdate metaDataUpdate =
-            new MetaDataUpdate(GitReferenceUpdated.DISABLED, allUsersName, repo)) {
-          metaDataUpdate.getCommitBuilder().setAuthor(serverIdent);
-          metaDataUpdate.getCommitBuilder().setCommitter(serverIdent);
-          metaDataUpdate.getCommitBuilder().setMessage(COMMIT_MSG);
-          extIdNotes.commit(metaDataUpdate);
-        }
-      }
-    } catch (IOException | ConfigInvalidException e) {
-      throw new OrmException("Failed to migrate external IDs to NoteDb", e);
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_145.java b/java/com/google/gerrit/server/schema/Schema_145.java
deleted file mode 100644
index 6ccb5d8..0000000
--- a/java/com/google/gerrit/server/schema/Schema_145.java
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.schema.sql.SqlDialect;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.StatementExecutor;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.sql.SQLException;
-
-/** Create account_external_ids_byEmail index. */
-public class Schema_145 extends SchemaVersion {
-
-  @Inject
-  Schema_145(Provider<Schema_144> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    JdbcSchema schema = (JdbcSchema) db;
-    SqlDialect dialect = schema.getDialect();
-    try (StatementExecutor e = newExecutor(db)) {
-      try {
-        dialect.dropIndex(e, "account_external_ids", "account_external_ids_byEmail");
-      } catch (OrmException ex) {
-        // Ignore.  The index did not exist.
-      }
-      e.execute(
-          "CREATE INDEX account_external_ids_byEmail"
-              + " ON account_external_ids"
-              + " (email_address)");
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_146.java b/java/com/google/gerrit/server/schema/Schema_146.java
deleted file mode 100644
index 64e9123..0000000
--- a/java/com/google/gerrit/server/schema/Schema_146.java
+++ /dev/null
@@ -1,303 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.common.base.Stopwatch;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Sets;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.sql.Timestamp;
-import java.text.ParseException;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.locks.ReentrantLock;
-import org.eclipse.jgit.internal.storage.file.FileRepository;
-import org.eclipse.jgit.internal.storage.file.GC;
-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.PersonIdent;
-import org.eclipse.jgit.lib.ProgressMonitor;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.RefUpdate.Result;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.lib.TextProgressMonitor;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevSort;
-import org.eclipse.jgit.revwalk.RevWalk;
-
-/**
- * Make sure that for every account a user branch exists that has an initial empty commit with the
- * registration date as commit time.
- *
- * <p>For accounts that don't have a user branch yet the user branch is created with an initial
- * empty commit that has the registration date as commit time.
- *
- * <p>For accounts that already have a user branch the user branch is rewritten and an initial empty
- * commit with the registration date as commit time is inserted (if such a commit doesn't exist
- * yet).
- */
-public class Schema_146 extends SchemaVersion {
-  private static final String CREATE_ACCOUNT_MSG = "Create Account";
-
-  private final GitRepositoryManager repoManager;
-  private final AllUsersName allUsersName;
-  private final PersonIdent serverIdent;
-  private AtomicInteger i = new AtomicInteger();
-  private Stopwatch sw = Stopwatch.createStarted();
-  ReentrantLock gcLock = new ReentrantLock();
-  private int size;
-
-  @Inject
-  Schema_146(
-      Provider<Schema_145> prior,
-      GitRepositoryManager repoManager,
-      AllUsersName allUsersName,
-      @GerritPersonIdent PersonIdent serverIdent) {
-    super(prior);
-    this.repoManager = repoManager;
-    this.allUsersName = allUsersName;
-    this.serverIdent = serverIdent;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    ui.message("Migrating accounts");
-    Set<Entry<Account.Id, Timestamp>> accounts = scanAccounts(db, ui).entrySet();
-    gc(ui);
-    Set<List<Entry<Account.Id, Timestamp>>> batches =
-        Sets.newHashSet(Iterables.partition(accounts, 500));
-    ExecutorService pool = createExecutor(ui);
-    try {
-      batches.stream().forEach(batch -> pool.submit(() -> processBatch(batch, ui)));
-      pool.shutdown();
-      pool.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);
-    } catch (InterruptedException e) {
-      throw new RuntimeException(e);
-    }
-    ui.message(
-        String.format("... (%.3f s) Migrated all %d accounts to schema 146", elapsed(), i.get()));
-  }
-
-  private ExecutorService createExecutor(UpdateUI ui) {
-    int threads;
-    try {
-      threads = Integer.parseInt(System.getProperty("threadcount"));
-    } catch (NumberFormatException e) {
-      threads = Runtime.getRuntime().availableProcessors();
-    }
-    ui.message(String.format("... using %d threads ...", threads));
-    return Executors.newFixedThreadPool(threads);
-  }
-
-  private void processBatch(List<Entry<Account.Id, Timestamp>> batch, UpdateUI ui) {
-    try (Repository repo = repoManager.openRepository(allUsersName);
-        RevWalk rw = new RevWalk(repo);
-        ObjectInserter oi = repo.newObjectInserter()) {
-      ObjectId emptyTree = emptyTree(oi);
-
-      for (Map.Entry<Account.Id, Timestamp> e : batch) {
-        String refName = RefNames.refsUsers(e.getKey());
-        Ref ref = repo.exactRef(refName);
-        if (ref != null) {
-          rewriteUserBranch(repo, rw, oi, emptyTree, ref, e.getValue());
-        } else {
-          createUserBranch(repo, oi, emptyTree, e.getKey(), e.getValue());
-        }
-        int count = i.incrementAndGet();
-        showProgress(ui, count);
-        if (count % 1000 == 0) {
-          gc(repo, true, ui);
-        }
-      }
-    } catch (IOException e) {
-      throw new UncheckedIOException(e);
-    }
-  }
-
-  private double elapsed() {
-    return sw.elapsed(TimeUnit.MILLISECONDS) / 1000d;
-  }
-
-  private void showProgress(UpdateUI ui, int count) {
-    if (count % 100 == 0) {
-      ui.message(
-          String.format(
-              "... (%.3f s) migrated %d%% (%d/%d) accounts",
-              elapsed(), Math.round(100.0 * count / size), count, size));
-    }
-  }
-
-  private void gc(UpdateUI ui) {
-    try (Repository repo = repoManager.openRepository(allUsersName)) {
-      gc(repo, false, ui);
-    } catch (IOException e) {
-      throw new UncheckedIOException(e);
-    }
-  }
-
-  private void gc(Repository repo, boolean refsOnly, UpdateUI ui) {
-    if (repo instanceof FileRepository && gcLock.tryLock()) {
-      ProgressMonitor pm = null;
-      try {
-        pm = new TextProgressMonitor();
-        FileRepository r = (FileRepository) repo;
-        GC gc = new GC(r);
-        gc.setProgressMonitor(pm);
-        pm.beginTask("gc", ProgressMonitor.UNKNOWN);
-        if (refsOnly) {
-          ui.message(String.format("... (%.3f s) pack refs", elapsed()));
-          gc.packRefs();
-        } else {
-          ui.message(String.format("... (%.3f s) gc --prune=now", elapsed()));
-          gc.setExpire(new Date());
-          gc.gc();
-        }
-      } catch (IOException | ParseException e) {
-        throw new RuntimeException(e);
-      } finally {
-        gcLock.unlock();
-        if (pm != null) {
-          pm.endTask();
-        }
-      }
-    }
-  }
-
-  private void rewriteUserBranch(
-      Repository repo,
-      RevWalk rw,
-      ObjectInserter oi,
-      ObjectId emptyTree,
-      Ref ref,
-      Timestamp registeredOn)
-      throws IOException {
-    ObjectId current = createInitialEmptyCommit(oi, emptyTree, registeredOn);
-
-    rw.reset();
-    rw.sort(RevSort.TOPO);
-    rw.sort(RevSort.REVERSE, true);
-    rw.markStart(rw.parseCommit(ref.getObjectId()));
-
-    RevCommit c;
-    while ((c = rw.next()) != null) {
-      if (isInitialEmptyCommit(emptyTree, c)) {
-        return;
-      }
-
-      CommitBuilder cb = new CommitBuilder();
-      cb.setParentId(current);
-      cb.setTreeId(c.getTree());
-      cb.setAuthor(c.getAuthorIdent());
-      cb.setCommitter(c.getCommitterIdent());
-      cb.setMessage(c.getFullMessage());
-      cb.setEncoding(c.getEncoding());
-      current = oi.insert(cb);
-    }
-
-    oi.flush();
-
-    RefUpdate ru = repo.updateRef(ref.getName());
-    ru.setExpectedOldObjectId(ref.getObjectId());
-    ru.setNewObjectId(current);
-    ru.setForceUpdate(true);
-    ru.setRefLogIdent(serverIdent);
-    ru.setRefLogMessage(getClass().getSimpleName(), true);
-    Result result = ru.update();
-    if (result != Result.FORCED) {
-      throw new IOException(
-          String.format("Failed to update ref %s: %s", ref.getName(), result.name()));
-    }
-  }
-
-  public void createUserBranch(
-      Repository repo,
-      ObjectInserter oi,
-      ObjectId emptyTree,
-      Account.Id accountId,
-      Timestamp registeredOn)
-      throws IOException {
-    ObjectId id = createInitialEmptyCommit(oi, emptyTree, registeredOn);
-
-    String refName = RefNames.refsUsers(accountId);
-    RefUpdate ru = repo.updateRef(refName);
-    ru.setExpectedOldObjectId(ObjectId.zeroId());
-    ru.setNewObjectId(id);
-    ru.setRefLogIdent(serverIdent);
-    ru.setRefLogMessage(CREATE_ACCOUNT_MSG, false);
-    Result result = ru.update();
-    if (result != Result.NEW) {
-      throw new IOException(String.format("Failed to update ref %s: %s", refName, result.name()));
-    }
-  }
-
-  private ObjectId createInitialEmptyCommit(
-      ObjectInserter oi, ObjectId emptyTree, Timestamp registrationDate) throws IOException {
-    PersonIdent ident = new PersonIdent(serverIdent, registrationDate);
-
-    CommitBuilder cb = new CommitBuilder();
-    cb.setTreeId(emptyTree);
-    cb.setCommitter(ident);
-    cb.setAuthor(ident);
-    cb.setMessage(CREATE_ACCOUNT_MSG);
-    return oi.insert(cb);
-  }
-
-  private boolean isInitialEmptyCommit(ObjectId emptyTree, RevCommit c) {
-    return c.getParentCount() == 0
-        && c.getTree().equals(emptyTree)
-        && c.getShortMessage().equals(CREATE_ACCOUNT_MSG);
-  }
-
-  private static ObjectId emptyTree(ObjectInserter oi) throws IOException {
-    return oi.insert(Constants.OBJ_TREE, new byte[] {});
-  }
-
-  private Map<Account.Id, Timestamp> scanAccounts(ReviewDb db, UpdateUI ui) throws SQLException {
-    ui.message(String.format("... (%.3f s) scan accounts", elapsed()));
-    try (Statement stmt = newStatement(db);
-        ResultSet rs = stmt.executeQuery("SELECT account_id, registered_on FROM accounts")) {
-      HashMap<Account.Id, Timestamp> m = new HashMap<>();
-      while (rs.next()) {
-        m.put(new Account.Id(rs.getInt(1)), rs.getTimestamp(2));
-      }
-      size = m.size();
-      return m;
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_147.java b/java/com/google/gerrit/server/schema/Schema_147.java
deleted file mode 100644
index d93daa1..0000000
--- a/java/com/google/gerrit/server/schema/Schema_147.java
+++ /dev/null
@@ -1,98 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import static java.util.stream.Collectors.toSet;
-
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.io.IOException;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.HashSet;
-import java.util.Objects;
-import java.util.Set;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.RefUpdate.Result;
-import org.eclipse.jgit.lib.Repository;
-
-/** Delete user branches for which no account exists. */
-public class Schema_147 extends SchemaVersion {
-  private final GitRepositoryManager repoManager;
-  private final AllUsersName allUsersName;
-
-  @Inject
-  Schema_147(
-      Provider<Schema_146> prior, GitRepositoryManager repoManager, AllUsersName allUsersName) {
-    super(prior);
-    this.repoManager = repoManager;
-    this.allUsersName = allUsersName;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    try (Repository repo = repoManager.openRepository(allUsersName)) {
-      Set<Account.Id> accountIdsFromReviewDb = scanAccounts(db);
-      Set<Account.Id> accountIdsFromUserBranches =
-          repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_USERS).stream()
-              .map(r -> Account.Id.fromRef(r.getName()))
-              .filter(Objects::nonNull)
-              .collect(toSet());
-      accountIdsFromUserBranches.removeAll(accountIdsFromReviewDb);
-      for (Account.Id accountId : accountIdsFromUserBranches) {
-        deleteUserBranch(repo, accountId);
-      }
-    } catch (IOException e) {
-      throw new OrmException("Failed to delete user branches for non-existing accounts.", e);
-    }
-  }
-
-  private Set<Account.Id> scanAccounts(ReviewDb db) throws SQLException {
-    try (Statement stmt = newStatement(db);
-        ResultSet rs = stmt.executeQuery("SELECT account_id FROM accounts")) {
-      Set<Account.Id> ids = new HashSet<>();
-      while (rs.next()) {
-        ids.add(new Account.Id(rs.getInt(1)));
-      }
-      return ids;
-    }
-  }
-
-  private void deleteUserBranch(Repository allUsersRepo, Account.Id accountId) throws IOException {
-    String refName = RefNames.refsUsers(accountId);
-    Ref ref = allUsersRepo.exactRef(refName);
-    if (ref == null) {
-      return;
-    }
-
-    RefUpdate ru = allUsersRepo.updateRef(refName);
-    ru.setExpectedOldObjectId(ref.getObjectId());
-    ru.setNewObjectId(ObjectId.zeroId());
-    ru.setForceUpdate(true);
-    Result result = ru.delete();
-    if (result != Result.FORCED) {
-      throw new IOException(String.format("Failed to delete ref %s: %s", refName, result.name()));
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_148.java b/java/com/google/gerrit/server/schema/Schema_148.java
deleted file mode 100644
index 9433da8..0000000
--- a/java/com/google/gerrit/server/schema/Schema_148.java
+++ /dev/null
@@ -1,82 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.common.primitives.Ints;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.gerrit.server.account.externalids.ExternalIdNotes;
-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.git.meta.MetaDataUpdate;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.io.IOException;
-import java.sql.SQLException;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
-
-public class Schema_148 extends SchemaVersion {
-  private static final String COMMIT_MSG = "Make account IDs of external IDs human-readable";
-
-  private final GitRepositoryManager repoManager;
-  private final AllUsersName allUsersName;
-  private final PersonIdent serverUser;
-
-  @Inject
-  Schema_148(
-      Provider<Schema_147> prior,
-      GitRepositoryManager repoManager,
-      AllUsersName allUsersName,
-      @GerritPersonIdent PersonIdent serverUser) {
-    super(prior);
-    this.repoManager = repoManager;
-    this.allUsersName = allUsersName;
-    this.serverUser = serverUser;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    try (Repository repo = repoManager.openRepository(allUsersName)) {
-      ExternalIdNotes extIdNotes = ExternalIdNotes.loadNoCacheUpdate(allUsersName, repo);
-      for (ExternalId extId : extIdNotes.all()) {
-        if (needsUpdate(extId)) {
-          extIdNotes.upsert(extId);
-        }
-      }
-
-      try (MetaDataUpdate metaDataUpdate =
-          new MetaDataUpdate(GitReferenceUpdated.DISABLED, allUsersName, repo)) {
-        metaDataUpdate.getCommitBuilder().setAuthor(serverUser);
-        metaDataUpdate.getCommitBuilder().setCommitter(serverUser);
-        metaDataUpdate.getCommitBuilder().setMessage(COMMIT_MSG);
-        extIdNotes.commit(metaDataUpdate);
-      }
-    } catch (IOException | ConfigInvalidException e) {
-      throw new OrmException("Failed to update external IDs", e);
-    }
-  }
-
-  private static boolean needsUpdate(ExternalId extId) {
-    Config cfg = new Config();
-    cfg.setInt("externalId", extId.key().get(), "accountId", extId.accountId().get());
-    return Ints.tryParse(cfg.getString("externalId", extId.key().get(), "accountId")) == null;
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_149.java b/java/com/google/gerrit/server/schema/Schema_149.java
deleted file mode 100644
index f1ccaa6..0000000
--- a/java/com/google/gerrit/server/schema/Schema_149.java
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-/** Add workInProgress field to change. */
-public class Schema_149 extends SchemaVersion {
-  @Inject
-  Schema_149(Provider<Schema_148> prior) {
-    super(prior);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_150.java b/java/com/google/gerrit/server/schema/Schema_150.java
deleted file mode 100644
index 456a01a..0000000
--- a/java/com/google/gerrit/server/schema/Schema_150.java
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-/** Drop ACCOUNT_EXTERNAL_IDS table. */
-public class Schema_150 extends SchemaVersion {
-  @Inject
-  Schema_150(Provider<Schema_149> prior) {
-    super(prior);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_151.java b/java/com/google/gerrit/server/schema/Schema_151.java
deleted file mode 100644
index 41d8a32..0000000
--- a/java/com/google/gerrit/server/schema/Schema_151.java
+++ /dev/null
@@ -1,80 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.sql.Timestamp;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Optional;
-
-/** A schema which adds the 'created on' field to groups. */
-public class Schema_151 extends SchemaVersion {
-  @Inject
-  protected Schema_151(Provider<Schema_150> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    try (PreparedStatement groupUpdate =
-            prepareStatement(db, "UPDATE account_groups SET created_on = ? WHERE group_id = ?");
-        PreparedStatement addedOnRetrieval =
-            prepareStatement(
-                db,
-                "SELECT added_on FROM account_group_members_audit WHERE group_id = ?"
-                    + " ORDER BY added_on ASC")) {
-      List<AccountGroup.Id> accountGroups = getAllGroupIds(db);
-      for (AccountGroup.Id groupId : accountGroups) {
-        Optional<Timestamp> firstTimeMentioned = getFirstTimeMentioned(addedOnRetrieval, groupId);
-        Timestamp createdOn = firstTimeMentioned.orElseGet(AccountGroup::auditCreationInstantTs);
-
-        groupUpdate.setTimestamp(1, createdOn);
-        groupUpdate.setInt(2, groupId.get());
-        groupUpdate.executeUpdate();
-      }
-    }
-  }
-
-  private static Optional<Timestamp> getFirstTimeMentioned(
-      PreparedStatement addedOnRetrieval, AccountGroup.Id groupId) throws SQLException {
-    addedOnRetrieval.setInt(1, groupId.get());
-    try (ResultSet resultSet = addedOnRetrieval.executeQuery()) {
-      if (resultSet.next()) {
-        return Optional.of(resultSet.getTimestamp(1));
-      }
-    }
-    return Optional.empty();
-  }
-
-  private static List<AccountGroup.Id> getAllGroupIds(ReviewDb db) throws SQLException {
-    try (Statement stmt = newStatement(db);
-        ResultSet rs = stmt.executeQuery("SELECT group_id FROM account_groups")) {
-      List<AccountGroup.Id> groupIds = new ArrayList<>();
-      while (rs.next()) {
-        groupIds.add(new AccountGroup.Id(rs.getInt(1)));
-      }
-      return groupIds;
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_152.java b/java/com/google/gerrit/server/schema/Schema_152.java
deleted file mode 100644
index c5150a0..0000000
--- a/java/com/google/gerrit/server/schema/Schema_152.java
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.schema.sql.SqlDialect;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.StatementExecutor;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.sql.SQLException;
-
-/** Drop unused indexes from accounts table. */
-public class Schema_152 extends SchemaVersion {
-  @Inject
-  Schema_152(Provider<Schema_151> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    JdbcSchema schema = (JdbcSchema) db;
-    SqlDialect dialect = schema.getDialect();
-    try (StatementExecutor e = newExecutor(db)) {
-      dialect.dropIndex(e, "accounts", "accounts_byFullName");
-    } catch (OrmException ex) {
-      // Ignore. The index did not exist.
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_153.java b/java/com/google/gerrit/server/schema/Schema_153.java
deleted file mode 100644
index 28aeb17..0000000
--- a/java/com/google/gerrit/server/schema/Schema_153.java
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.StatementExecutor;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-/** Add reviewStarted field to change. */
-public class Schema_153 extends SchemaVersion {
-  @Inject
-  Schema_153(Provider<Schema_152> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
-    try (StatementExecutor e = newExecutor(db)) {
-      // Initialize review_started to a sensible default value according to
-      // whether change is currently WIP. No migration is needed in NoteDb,
-      // where the value of review_started is always derived from the history
-      // of assignments to work_in_progress.
-      e.execute(
-          "UPDATE changes SET review_started = 'Y', created_on = created_on WHERE work_in_progress = 'N'");
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_154.java b/java/com/google/gerrit/server/schema/Schema_154.java
deleted file mode 100644
index 11fa89e..0000000
--- a/java/com/google/gerrit/server/schema/Schema_154.java
+++ /dev/null
@@ -1,150 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import static java.util.stream.Collectors.toMap;
-
-import com.google.common.collect.ImmutableMap;
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.account.AccountConfig;
-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.git.meta.MetaDataUpdate;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.io.IOException;
-import java.sql.Connection;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.ProgressMonitor;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.lib.TextProgressMonitor;
-
-/** Migrate accounts to NoteDb. */
-public class Schema_154 extends SchemaVersion {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
-  private static final String TABLE = "accounts";
-  private static final ImmutableMap<String, AccountSetter> ACCOUNT_FIELDS_MAP =
-      ImmutableMap.<String, AccountSetter>builder()
-          .put("full_name", (a, rs, field) -> a.setFullName(rs.getString(field)))
-          .put("preferred_email", (a, rs, field) -> a.setPreferredEmail(rs.getString(field)))
-          .put("status", (a, rs, field) -> a.setStatus(rs.getString(field)))
-          .put("inactive", (a, rs, field) -> a.setActive(rs.getString(field).equals("N")))
-          .build();
-
-  private final GitRepositoryManager repoManager;
-  private final AllUsersName allUsersName;
-  private final Provider<PersonIdent> serverIdent;
-
-  @Inject
-  Schema_154(
-      Provider<Schema_153> prior,
-      GitRepositoryManager repoManager,
-      AllUsersName allUsersName,
-      @GerritPersonIdent Provider<PersonIdent> serverIdent) {
-    super(prior);
-    this.repoManager = repoManager;
-    this.allUsersName = allUsersName;
-    this.serverIdent = serverIdent;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    try {
-      try (Repository repo = repoManager.openRepository(allUsersName)) {
-        ProgressMonitor pm = new TextProgressMonitor();
-        pm.beginTask("Collecting accounts", ProgressMonitor.UNKNOWN);
-        Set<Account> accounts = scanAccounts(db, pm);
-        pm.endTask();
-        pm.beginTask("Migrating accounts to NoteDb", accounts.size());
-        for (Account account : accounts) {
-          updateAccountInNoteDb(repo, account);
-          pm.update(1);
-        }
-        pm.endTask();
-      }
-    } catch (IOException | ConfigInvalidException e) {
-      throw new OrmException("Migrating accounts to NoteDb failed", e);
-    }
-  }
-
-  private Set<Account> scanAccounts(ReviewDb db, ProgressMonitor pm) throws SQLException {
-    Map<String, AccountSetter> fields = getFields(db);
-    if (fields.isEmpty()) {
-      logger.atWarning().log("Only account_id and registered_on fields are migrated for accounts");
-    }
-
-    List<String> queryFields = new ArrayList<>();
-    queryFields.add("account_id");
-    queryFields.add("registered_on");
-    queryFields.addAll(fields.keySet());
-    String query = "SELECT " + String.join(", ", queryFields) + String.format(" FROM %s", TABLE);
-    try (Statement stmt = newStatement(db);
-        ResultSet rs = stmt.executeQuery(query)) {
-      Set<Account> s = new HashSet<>();
-      while (rs.next()) {
-        Account a = new Account(new Account.Id(rs.getInt(1)), rs.getTimestamp(2));
-        for (Map.Entry<String, AccountSetter> field : fields.entrySet()) {
-          field.getValue().set(a, rs, field.getKey());
-        }
-        s.add(a);
-        pm.update(1);
-      }
-      return s;
-    }
-  }
-
-  private Map<String, AccountSetter> getFields(ReviewDb db) throws SQLException {
-    JdbcSchema schema = (JdbcSchema) db;
-    Connection connection = schema.getConnection();
-    Set<String> columns = schema.getDialect().listColumns(connection, TABLE);
-    return ACCOUNT_FIELDS_MAP.entrySet().stream()
-        .filter(e -> columns.contains(e.getKey()))
-        .collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
-  }
-
-  private void updateAccountInNoteDb(Repository allUsersRepo, Account account)
-      throws IOException, ConfigInvalidException {
-    MetaDataUpdate md =
-        new MetaDataUpdate(GitReferenceUpdated.DISABLED, allUsersName, allUsersRepo);
-    PersonIdent ident = serverIdent.get();
-    md.getCommitBuilder().setAuthor(ident);
-    md.getCommitBuilder().setCommitter(ident);
-    new AccountConfig(account.getId(), allUsersName, allUsersRepo)
-        .load()
-        .setAccount(account)
-        .commit(md);
-  }
-
-  @FunctionalInterface
-  private interface AccountSetter {
-    void set(Account a, ResultSet rs, String field) throws SQLException;
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_155.java b/java/com/google/gerrit/server/schema/Schema_155.java
deleted file mode 100644
index ec16e06..0000000
--- a/java/com/google/gerrit/server/schema/Schema_155.java
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.Sequences;
-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.notedb.RepoSequence;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.sql.SQLException;
-
-/** Create account sequence in NoteDb */
-public class Schema_155 extends SchemaVersion {
-  private final GitRepositoryManager repoManager;
-  private final AllUsersName allUsersName;
-
-  @Inject
-  Schema_155(
-      Provider<Schema_154> prior, GitRepositoryManager repoManager, AllUsersName allUsersName) {
-    super(prior);
-    this.repoManager = repoManager;
-    this.allUsersName = allUsersName;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    @SuppressWarnings("deprecation")
-    RepoSequence.Seed accountSeed = db::nextAccountId;
-    RepoSequence accountSeq =
-        new RepoSequence(
-            repoManager,
-            GitReferenceUpdated.DISABLED,
-            allUsersName,
-            Sequences.NAME_ACCOUNTS,
-            accountSeed,
-            1);
-
-    // consume one account ID to ensure that the account sequence is initialized in NoteDb
-    accountSeq.next();
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_156.java b/java/com/google/gerrit/server/schema/Schema_156.java
deleted file mode 100644
index fd8fc00..0000000
--- a/java/com/google/gerrit/server/schema/Schema_156.java
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-/** Add revertOf field to change. */
-public class Schema_156 extends SchemaVersion {
-  @Inject
-  Schema_156(Provider<Schema_155> prior) {
-    super(prior);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_157.java b/java/com/google/gerrit/server/schema/Schema_157.java
deleted file mode 100644
index f5c5b59..0000000
--- a/java/com/google/gerrit/server/schema/Schema_157.java
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.schema.sql.SqlDialect;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.StatementExecutor;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.sql.SQLException;
-
-/** Drop unused indexes from accounts table. */
-public class Schema_157 extends SchemaVersion {
-  @Inject
-  Schema_157(Provider<Schema_156> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    JdbcSchema schema = (JdbcSchema) db;
-    SqlDialect dialect = schema.getDialect();
-    try (StatementExecutor e = newExecutor(db)) {
-      dialect.dropIndex(e, "accounts", "accounts_byPreferredEmail");
-    } catch (OrmException ex) {
-      // Ignore. The index did not exist.
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_158.java b/java/com/google/gerrit/server/schema/Schema_158.java
deleted file mode 100644
index ea85444..0000000
--- a/java/com/google/gerrit/server/schema/Schema_158.java
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-/** Drop ACCOUNTS table. */
-public class Schema_158 extends SchemaVersion {
-  @Inject
-  Schema_158(Provider<Schema_157> prior) {
-    super(prior);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_159.java b/java/com/google/gerrit/server/schema/Schema_159.java
deleted file mode 100644
index d6e37d7..0000000
--- a/java/com/google/gerrit/server/schema/Schema_159.java
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.StatementExecutor;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-/** Migrate draft changes to private or wip changes. */
-public class Schema_159 extends SchemaVersion {
-
-  private enum DraftWorkflowMigrationStrategy {
-    PRIVATE,
-    WORK_IN_PROGRESS
-  }
-
-  @Inject
-  Schema_159(Provider<Schema_158> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
-    DraftWorkflowMigrationStrategy strategy = DraftWorkflowMigrationStrategy.WORK_IN_PROGRESS;
-    if (ui.yesno(false, "Migrate draft changes to private changes (default is work-in-progress)")) {
-      strategy = DraftWorkflowMigrationStrategy.PRIVATE;
-    }
-    ui.message(
-        String.format("Replace draft changes with %s changes ...", strategy.name().toLowerCase()));
-    try (StatementExecutor e = newExecutor(db)) {
-      String column =
-          strategy == DraftWorkflowMigrationStrategy.PRIVATE ? "is_private" : "work_in_progress";
-      // Mark changes private/WIP and NEW if either:
-      // * they have status DRAFT
-      // * they have status NEW and have any draft patch sets
-      e.execute(
-          String.format(
-              "UPDATE changes "
-                  + "SET %s = 'Y', "
-                  + "    status = 'n', "
-                  + "    created_on = created_on "
-                  + "WHERE status = 'd' "
-                  + "  OR (status = 'n' "
-                  + "      AND EXISTS "
-                  + "        (SELECT * "
-                  + "         FROM patch_sets "
-                  + "         WHERE patch_sets.change_id = changes.change_id "
-                  + "           AND patch_sets.draft = 'Y')) ",
-              column));
-    }
-    ui.message("done");
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_160.java b/java/com/google/gerrit/server/schema/Schema_160.java
deleted file mode 100644
index eb8b70f..0000000
--- a/java/com/google/gerrit/server/schema/Schema_160.java
+++ /dev/null
@@ -1,152 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import static com.google.gerrit.server.git.UserConfigSections.KEY_URL;
-import static com.google.gerrit.server.git.UserConfigSections.MY;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.ImmutableList;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.account.Accounts;
-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.git.meta.MetaDataUpdate;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.io.IOException;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.CommitBuilder;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.ProgressMonitor;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.lib.TextProgressMonitor;
-
-/**
- * Remove "My Drafts" menu items for all users and server-wide default preferences.
- *
- * <p>Since draft changes no longer exist, these menu items are obsolete.
- *
- * <p>Only matches menu items (with any name) where the URL exactly matches one of the following,
- * with or without leading {@code #}:
- *
- * <ul>
- *   <li>/q/is:draft
- *   <li>/q/owner:self+is:draft
- * </ul>
- *
- * In particular, this includes the <a
- * href="https://gerrit.googlesource.com/gerrit/+/v2.14.4/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneralPreferencesLoader.java#144">default
- * from version 2.14 and earlier</a>.
- *
- * <p>Other menus containing {@code is:draft} in other positions are not affected; this is still a
- * valid predicate that matches no changes.
- */
-public class Schema_160 extends SchemaVersion {
-  @VisibleForTesting static final ImmutableList<String> DEFAULT_DRAFT_ITEMS;
-
-  static {
-    String ownerSelfIsDraft = "/q/owner:self+is:draft";
-    String isDraft = "/q/is:draft";
-    DEFAULT_DRAFT_ITEMS =
-        ImmutableList.of(ownerSelfIsDraft, '#' + ownerSelfIsDraft, isDraft, '#' + isDraft);
-  }
-
-  private final GitRepositoryManager repoManager;
-  private final AllUsersName allUsersName;
-  private final Provider<PersonIdent> serverIdent;
-
-  @Inject
-  Schema_160(
-      Provider<Schema_159> prior,
-      GitRepositoryManager repoManager,
-      AllUsersName allUsersName,
-      @GerritPersonIdent Provider<PersonIdent> serverIdent) {
-    super(prior);
-    this.repoManager = repoManager;
-    this.allUsersName = allUsersName;
-    this.serverIdent = serverIdent;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
-    try {
-      try (Repository repo = repoManager.openRepository(allUsersName)) {
-        ProgressMonitor pm = new TextProgressMonitor();
-        pm.beginTask("Removing \"My Drafts\" menu items", ProgressMonitor.UNKNOWN);
-        for (Account.Id id : (Iterable<Account.Id>) Accounts.readUserRefs(repo)::iterator) {
-          removeMyDrafts(repo, RefNames.refsUsers(id), pm);
-        }
-        removeMyDrafts(repo, RefNames.REFS_USERS_DEFAULT, pm);
-        pm.endTask();
-      }
-    } catch (IOException | ConfigInvalidException e) {
-      throw new OrmException("Removing \"My Drafts\" menu items failed", e);
-    }
-  }
-
-  private void removeMyDrafts(Repository repo, String ref, ProgressMonitor pm)
-      throws IOException, ConfigInvalidException {
-    MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, allUsersName, repo);
-    PersonIdent ident = serverIdent.get();
-    md.getCommitBuilder().setAuthor(ident);
-    md.getCommitBuilder().setCommitter(ident);
-    Prefs prefs = new Prefs(ref);
-    prefs.load(allUsersName, repo);
-    prefs.removeMyDrafts();
-    prefs.commit(md);
-    if (prefs.dirty()) {
-      pm.update(1);
-    }
-  }
-
-  private static class Prefs extends VersionedAccountPreferences {
-    private boolean dirty;
-
-    Prefs(String ref) {
-      super(ref);
-    }
-
-    @Override
-    protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException {
-      if (!dirty) {
-        return false;
-      }
-      commit.setMessage("Remove \"My Drafts\" menu items");
-      return super.onSave(commit);
-    }
-
-    void removeMyDrafts() {
-      Config cfg = getConfig();
-      for (String item : cfg.getSubsections(MY)) {
-        String value = cfg.getString(MY, item, KEY_URL);
-        if (DEFAULT_DRAFT_ITEMS.contains(value)) {
-          cfg.unsetSection(MY, item);
-          dirty = true;
-        }
-      }
-    }
-
-    boolean dirty() {
-      return dirty;
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_161.java b/java/com/google/gerrit/server/schema/Schema_161.java
deleted file mode 100644
index 70010f8..0000000
--- a/java/com/google/gerrit/server/schema/Schema_161.java
+++ /dev/null
@@ -1,112 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import static java.util.stream.Collectors.toList;
-
-import com.google.common.primitives.Ints;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.StarredChangesUtil;
-import com.google.gerrit.server.StarredChangesUtil.IllegalLabelException;
-import com.google.gerrit.server.StarredChangesUtil.StarRef;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.io.IOException;
-import java.util.List;
-import java.util.Objects;
-import java.util.Set;
-import org.eclipse.jgit.lib.BatchRefUpdate;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.lib.TextProgressMonitor;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.ReceiveCommand;
-
-public class Schema_161 extends SchemaVersion {
-  private static final String MUTE_LABEL = "mute";
-
-  private final GitRepositoryManager repoManager;
-  private final AllUsersName allUsersName;
-
-  @Inject
-  Schema_161(
-      Provider<Schema_160> prior, GitRepositoryManager repoManager, AllUsersName allUsersName) {
-    super(prior);
-    this.repoManager = repoManager;
-    this.allUsersName = allUsersName;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
-    try (Repository git = repoManager.openRepository(allUsersName);
-        RevWalk rw = new RevWalk(git)) {
-      BatchRefUpdate bru = git.getRefDatabase().newBatchUpdate();
-      bru.setAllowNonFastForwards(true);
-
-      for (Ref ref : git.getRefDatabase().getRefsByPrefix(RefNames.REFS_STARRED_CHANGES)) {
-        StarRef starRef = StarredChangesUtil.readLabels(git, ref.getName());
-
-        Set<Integer> mutedPatchSets =
-            StarredChangesUtil.getStarredPatchSets(starRef.labels(), MUTE_LABEL);
-        if (mutedPatchSets.isEmpty()) {
-          continue;
-        }
-
-        Set<Integer> reviewedPatchSets =
-            StarredChangesUtil.getStarredPatchSets(
-                starRef.labels(), StarredChangesUtil.REVIEWED_LABEL);
-        Set<Integer> unreviewedPatchSets =
-            StarredChangesUtil.getStarredPatchSets(
-                starRef.labels(), StarredChangesUtil.UNREVIEWED_LABEL);
-
-        List<String> newLabels =
-            starRef.labels().stream()
-                .map(
-                    l -> {
-                      if (l.startsWith(MUTE_LABEL)) {
-                        Integer mutedPatchSet = Ints.tryParse(l.substring(MUTE_LABEL.length() + 1));
-                        if (mutedPatchSet == null) {
-                          // unexpected format of mute label, must be a label that was manually
-                          // set, just leave it alone
-                          return l;
-                        }
-                        if (!reviewedPatchSets.contains(mutedPatchSet)
-                            && !unreviewedPatchSets.contains(mutedPatchSet)) {
-                          // convert mute label to reviewed label
-                          return StarredChangesUtil.REVIEWED_LABEL + "/" + mutedPatchSet;
-                        }
-                        // else patch set is muted but has either reviewed or unreviewed label
-                        // -> just drop the mute label
-                        return null;
-                      }
-                      return l;
-                    })
-                .filter(Objects::nonNull)
-                .collect(toList());
-
-        ObjectId id = StarredChangesUtil.writeLabels(git, newLabels);
-        bru.addCommand(new ReceiveCommand(ref.getTarget().getObjectId(), id, ref.getName()));
-      }
-      bru.execute(rw, new TextProgressMonitor());
-    } catch (IOException | IllegalLabelException ex) {
-      throw new OrmException(ex);
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_162.java b/java/com/google/gerrit/server/schema/Schema_162.java
deleted file mode 100644
index 7406bc6..0000000
--- a/java/com/google/gerrit/server/schema/Schema_162.java
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.config.AllProjectsName;
-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.git.meta.MetaDataUpdate;
-import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.io.IOException;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
-
-public class Schema_162 extends SchemaVersion {
-  private final GitRepositoryManager repoManager;
-  private final AllProjectsName allProjectsName;
-  private final AllUsersName allUsersName;
-  private final PersonIdent serverUser;
-
-  @Inject
-  Schema_162(
-      Provider<Schema_161> prior,
-      GitRepositoryManager repoManager,
-      AllProjectsName allProjectsName,
-      AllUsersName allUsersName,
-      @GerritPersonIdent PersonIdent serverUser) {
-    super(prior);
-    this.repoManager = repoManager;
-    this.allProjectsName = allProjectsName;
-    this.allUsersName = allUsersName;
-    this.serverUser = serverUser;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
-    try (Repository git = repoManager.openRepository(allUsersName);
-        MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, allUsersName, git)) {
-      ProjectConfig cfg = ProjectConfig.read(md);
-      if (allProjectsName.equals(cfg.getProject().getParent(allProjectsName))) {
-        return;
-      }
-      cfg.getProject().setParentName(allProjectsName);
-      md.getCommitBuilder().setAuthor(serverUser);
-      md.getCommitBuilder().setCommitter(serverUser);
-      md.setMessage(
-          String.format("Make %s inherit from %s", allUsersName.get(), allProjectsName.get()));
-      cfg.commit(md);
-    } catch (ConfigInvalidException | IOException ex) {
-      throw new OrmException(ex);
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_163.java b/java/com/google/gerrit/server/schema/Schema_163.java
deleted file mode 100644
index ae05774..0000000
--- a/java/com/google/gerrit/server/schema/Schema_163.java
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.Sequences;
-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.notedb.RepoSequence;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.sql.SQLException;
-
-/** Create group sequence in NoteDb */
-public class Schema_163 extends SchemaVersion {
-  private final GitRepositoryManager repoManager;
-  private final AllUsersName allUsersName;
-
-  @Inject
-  Schema_163(
-      Provider<Schema_162> prior, GitRepositoryManager repoManager, AllUsersName allUsersName) {
-    super(prior);
-    this.repoManager = repoManager;
-    this.allUsersName = allUsersName;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    @SuppressWarnings("deprecation")
-    RepoSequence.Seed groupSeed = db::nextAccountGroupId;
-    RepoSequence groupSeq =
-        new RepoSequence(
-            repoManager,
-            GitReferenceUpdated.DISABLED,
-            allUsersName,
-            Sequences.NAME_GROUPS,
-            groupSeed,
-            1);
-
-    // consume one account ID to ensure that the group sequence is initialized in NoteDb
-    groupSeq.next();
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_164.java b/java/com/google/gerrit/server/schema/Schema_164.java
deleted file mode 100644
index 8525478..0000000
--- a/java/com/google/gerrit/server/schema/Schema_164.java
+++ /dev/null
@@ -1,85 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static com.google.gerrit.server.schema.AclUtil.grant;
-
-import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-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.git.meta.MetaDataUpdate;
-import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.io.IOException;
-import java.sql.SQLException;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
-
-/** Grant read on group branches */
-public class Schema_164 extends SchemaVersion {
-  private static final String COMMIT_MSG = "Grant read permissions on group branches";
-
-  private final GitRepositoryManager repoManager;
-  private final AllUsersName allUsersName;
-  private final SystemGroupBackend systemGroupBackend;
-  private final PersonIdent serverUser;
-
-  @Inject
-  Schema_164(
-      Provider<Schema_163> prior,
-      GitRepositoryManager repoManager,
-      AllUsersName allUsersName,
-      SystemGroupBackend systemGroupBackend,
-      @GerritPersonIdent PersonIdent serverUser) {
-    super(prior);
-    this.repoManager = repoManager;
-    this.allUsersName = allUsersName;
-    this.systemGroupBackend = systemGroupBackend;
-    this.serverUser = serverUser;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    try (Repository git = repoManager.openRepository(allUsersName);
-        MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, allUsersName, git)) {
-      md.getCommitBuilder().setAuthor(serverUser);
-      md.getCommitBuilder().setCommitter(serverUser);
-      md.setMessage(COMMIT_MSG);
-
-      ProjectConfig config = ProjectConfig.read(md);
-      AccessSection groups = config.getAccessSection(RefNames.REFS_GROUPS + "*", true);
-      grant(
-          config,
-          groups,
-          Permission.READ,
-          false,
-          true,
-          systemGroupBackend.getGroup(REGISTERED_USERS));
-      config.commit(md);
-    } catch (IOException | ConfigInvalidException e) {
-      throw new OrmException("Failed to grant read permissions on group branches", e);
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_165.java b/java/com/google/gerrit/server/schema/Schema_165.java
deleted file mode 100644
index cd6da55..0000000
--- a/java/com/google/gerrit/server/schema/Schema_165.java
+++ /dev/null
@@ -1,142 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-
-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.common.data.PermissionRule.Action;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-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.git.meta.MetaDataUpdate;
-import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gerrit.server.project.RefPattern;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.io.IOException;
-import java.sql.SQLException;
-import java.util.Optional;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
-
-/** Make default Label-Code-Review permission on user branches exclusive. */
-public class Schema_165 extends SchemaVersion {
-  private static final String COMMIT_MSG =
-      "Make default Label-Code-Review permission on user branches exclusive";
-
-  private final GitRepositoryManager repoManager;
-  private final AllUsersName allUsersName;
-  private final SystemGroupBackend systemGroupBackend;
-  private final PersonIdent serverUser;
-
-  @Inject
-  Schema_165(
-      Provider<Schema_164> prior,
-      GitRepositoryManager repoManager,
-      AllUsersName allUsersName,
-      SystemGroupBackend systemGroupBackend,
-      @GerritPersonIdent PersonIdent serverUser) {
-    super(prior);
-    this.repoManager = repoManager;
-    this.allUsersName = allUsersName;
-    this.systemGroupBackend = systemGroupBackend;
-    this.serverUser = serverUser;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    try (Repository git = repoManager.openRepository(allUsersName);
-        MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, allUsersName, git)) {
-      ProjectConfig config = ProjectConfig.read(md);
-      Optional<Permission> permission = findDefaultPermission(config);
-      if (!permission.isPresent()) {
-        // the default permission was not found, hence it cannot be fixed
-        return;
-      }
-
-      permission.get().setExclusiveGroup(true);
-
-      md.getCommitBuilder().setAuthor(serverUser);
-      md.getCommitBuilder().setCommitter(serverUser);
-      md.setMessage(COMMIT_MSG);
-      config.commit(md);
-    } catch (IOException | ConfigInvalidException e) {
-      throw new OrmException(
-          "Failed to make default Label-Code-Review permission on user branches exclusive", e);
-    }
-  }
-
-  /**
-   * Searches for the default "Label-Code-Review" permission on the user branch and returns it if it
-   * was found. If it was not found (e.g. because it was removed or modified) {@link
-   * Optional#empty()} is returned.
-   */
-  private Optional<Permission> findDefaultPermission(ProjectConfig config) {
-    AccessSection users =
-        config.getAccessSection(
-            RefNames.REFS_USERS + "${" + RefPattern.USERID_SHARDED + "}", false);
-    if (users == null) {
-      // default permission was removed
-      return Optional.empty();
-    }
-
-    Permission permission = users.getPermission(Permission.LABEL + "Code-Review", false);
-    return isDefaultPermissionUntouched(permission) ? Optional.of(permission) : Optional.empty();
-  }
-
-  /**
-   * Checks whether the given permission matches the default "Label-Code-Review" permission on the
-   * user branch that was initially setup by {@link AllUsersCreator}.
-   */
-  private boolean isDefaultPermissionUntouched(Permission permission) {
-    if (permission == null) {
-      // default permission was removed
-      return false;
-    } else if (permission.getExclusiveGroup()) {
-      // default permission was modified
-      return false;
-    }
-
-    if (permission.getRules().size() != 1) {
-      // default permission was modified
-      return false;
-    }
-
-    PermissionRule rule = permission.getRule(systemGroupBackend.getGroup(REGISTERED_USERS));
-    if (rule == null) {
-      // default permission was removed
-      return false;
-    }
-
-    if (rule.getAction() != Action.ALLOW
-        || rule.getForce()
-        || rule.getMin() != -2
-        || rule.getMax() != 2) {
-      // default permission was modified
-      return false;
-    }
-
-    return true;
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_166.java b/java/com/google/gerrit/server/schema/Schema_166.java
deleted file mode 100644
index aa6f4e6..0000000
--- a/java/com/google/gerrit/server/schema/Schema_166.java
+++ /dev/null
@@ -1,52 +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.schema;
-
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.io.IOException;
-import java.sql.SQLException;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.Repository;
-
-/** Set HEAD for All-Users to refs/meta/config. */
-public class Schema_166 extends SchemaVersion {
-  private final GitRepositoryManager repoManager;
-  private final AllUsersName allUsersName;
-
-  @Inject
-  Schema_166(
-      Provider<Schema_165> prior, GitRepositoryManager repoManager, AllUsersName allUsersName) {
-    super(prior);
-    this.repoManager = repoManager;
-    this.allUsersName = allUsersName;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    try (Repository git = repoManager.openRepository(allUsersName)) {
-      RefUpdate u = git.updateRef(Constants.HEAD);
-      u.link(RefNames.REFS_CONFIG);
-    } catch (IOException e) {
-      throw new OrmException(String.format("Failed to update HEAD for %s", allUsersName.get()), e);
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_167.java b/java/com/google/gerrit/server/schema/Schema_167.java
deleted file mode 100644
index a5066cc..0000000
--- a/java/com/google/gerrit/server/schema/Schema_167.java
+++ /dev/null
@@ -1,288 +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.schema;
-
-import static com.google.gerrit.server.notedb.NoteDbTable.GROUPS;
-import static com.google.gerrit.server.notedb.NotesMigration.DISABLE_REVIEW_DB;
-import static com.google.gerrit.server.notedb.NotesMigration.SECTION_NOTE_DB;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.reviewdb.server.ReviewDbWrapper;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.account.AccountConfig;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.config.GerritServerIdProvider;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.group.db.AuditLogFormatter;
-import com.google.gerrit.server.group.db.GroupNameNotes;
-import com.google.gerrit.server.update.RefUpdateUtil;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.io.IOException;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.BatchRefUpdate;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
-
-/** Migrate groups from ReviewDb to NoteDb. */
-public class Schema_167 extends SchemaVersion {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
-  private final GitRepositoryManager repoManager;
-  private final AllUsersName allUsersName;
-  private final Config gerritConfig;
-  private final SitePaths sitePaths;
-  private final PersonIdent serverIdent;
-  private final SystemGroupBackend systemGroupBackend;
-
-  @Inject
-  protected Schema_167(
-      Provider<Schema_166> prior,
-      GitRepositoryManager repoManager,
-      AllUsersName allUsersName,
-      @GerritServerConfig Config gerritConfig,
-      SitePaths sitePaths,
-      @GerritPersonIdent PersonIdent serverIdent,
-      SystemGroupBackend systemGroupBackend) {
-    super(prior);
-    this.repoManager = repoManager;
-    this.allUsersName = allUsersName;
-    this.gerritConfig = gerritConfig;
-    this.sitePaths = sitePaths;
-    this.serverIdent = serverIdent;
-    this.systemGroupBackend = systemGroupBackend;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    if (gerritConfig.getBoolean(SECTION_NOTE_DB, GROUPS.key(), DISABLE_REVIEW_DB, false)) {
-      // Groups in ReviewDb have already been disabled, nothing to do.
-      return;
-    }
-
-    try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
-      List<GroupReference> allGroupReferences = readGroupReferencesFromReviewDb(db);
-
-      BatchRefUpdate batchRefUpdate = allUsersRepo.getRefDatabase().newBatchUpdate();
-      writeAllGroupNamesToNoteDb(allUsersRepo, allGroupReferences, batchRefUpdate);
-
-      GroupRebuilder groupRebuilder = createGroupRebuilder(db, allUsersRepo);
-      for (GroupReference groupReference : allGroupReferences) {
-        migrateOneGroupToNoteDb(
-            db, allUsersRepo, groupRebuilder, groupReference.getUUID(), batchRefUpdate);
-      }
-
-      RefUpdateUtil.executeChecked(batchRefUpdate, allUsersRepo);
-    } catch (IOException | ConfigInvalidException e) {
-      throw new OrmException(
-          String.format("Failed to migrate groups to NoteDb for %s", allUsersName.get()), e);
-    }
-  }
-
-  private List<GroupReference> readGroupReferencesFromReviewDb(ReviewDb db) throws SQLException {
-    try (Statement stmt = ReviewDbWrapper.unwrapJbdcSchema(db).getConnection().createStatement();
-        ResultSet rs = stmt.executeQuery("SELECT group_uuid, name FROM account_groups")) {
-      List<GroupReference> allGroupReferences = new ArrayList<>();
-      while (rs.next()) {
-        AccountGroup.UUID groupUuid = new AccountGroup.UUID(rs.getString(1));
-        String groupName = rs.getString(2);
-        allGroupReferences.add(new GroupReference(groupUuid, groupName));
-      }
-      return allGroupReferences;
-    }
-  }
-
-  private void writeAllGroupNamesToNoteDb(
-      Repository allUsersRepo,
-      List<GroupReference> allGroupReferences,
-      BatchRefUpdate batchRefUpdate)
-      throws IOException {
-    try (ObjectInserter inserter = allUsersRepo.newObjectInserter()) {
-      GroupNameNotes.updateAllGroups(
-          allUsersRepo, inserter, batchRefUpdate, allGroupReferences, serverIdent);
-      inserter.flush();
-    }
-  }
-
-  private GroupRebuilder createGroupRebuilder(ReviewDb db, Repository allUsersRepo)
-      throws IOException, ConfigInvalidException {
-    AuditLogFormatter auditLogFormatter =
-        createAuditLogFormatter(db, allUsersRepo, gerritConfig, sitePaths);
-    return new GroupRebuilder(serverIdent, allUsersName, auditLogFormatter);
-  }
-
-  private AuditLogFormatter createAuditLogFormatter(
-      ReviewDb db, Repository allUsersRepo, Config gerritConfig, SitePaths sitePaths)
-      throws IOException, ConfigInvalidException {
-    String serverId = new GerritServerIdProvider(gerritConfig, sitePaths).get();
-    SimpleInMemoryAccountCache accountCache =
-        new SimpleInMemoryAccountCache(allUsersName, allUsersRepo);
-    SimpleInMemoryGroupCache groupCache = new SimpleInMemoryGroupCache(db);
-    return AuditLogFormatter.create(
-        accountCache::get,
-        uuid -> {
-          if (systemGroupBackend.handles(uuid)) {
-            return Optional.ofNullable(systemGroupBackend.get(uuid));
-          }
-          return groupCache.get(uuid);
-        },
-        serverId);
-  }
-
-  private static void migrateOneGroupToNoteDb(
-      ReviewDb db,
-      Repository allUsersRepo,
-      GroupRebuilder rebuilder,
-      AccountGroup.UUID uuid,
-      BatchRefUpdate batchRefUpdate)
-      throws ConfigInvalidException, IOException, OrmException {
-    GroupBundle reviewDbBundle = GroupBundle.Factory.fromReviewDb(db, uuid);
-    RefUpdateUtil.deleteChecked(allUsersRepo, RefNames.refsGroups(uuid));
-    rebuilder.rebuild(allUsersRepo, reviewDbBundle, batchRefUpdate);
-  }
-
-  // The regular account cache isn't available during init. -> Use a simple replacement which tries
-  // to load every account only once from disk.
-  private static class SimpleInMemoryAccountCache {
-    private final AllUsersName allUsersName;
-    private final Repository allUsersRepo;
-    private Map<Account.Id, Optional<Account>> accounts = new HashMap<>();
-
-    public SimpleInMemoryAccountCache(AllUsersName allUsersName, Repository allUsersRepo) {
-      this.allUsersName = allUsersName;
-      this.allUsersRepo = allUsersRepo;
-    }
-
-    public Optional<Account> get(Account.Id accountId) {
-      accounts.computeIfAbsent(accountId, this::load);
-      return accounts.get(accountId);
-    }
-
-    private Optional<Account> load(Account.Id accountId) {
-      try {
-        AccountConfig accountConfig =
-            new AccountConfig(accountId, allUsersName, allUsersRepo).load();
-        return accountConfig.getLoadedAccount();
-      } catch (IOException | ConfigInvalidException ignored) {
-        logger.atWarning().withCause(ignored).log(
-            "Failed to load account %s."
-                + " Cannot get account name for group audit log commit messages.",
-            accountId.get());
-        return Optional.empty();
-      }
-    }
-  }
-
-  // The regular GroupBackends (especially external GroupBackends) and our internal group cache
-  // aren't available during init. -> Use a simple replacement which tries to look up only internal
-  // groups and which loads every internal group only once from disc. (There's no way we can look up
-  // external groups during init. As we need those groups only for cosmetic aspects in
-  // AuditLogFormatter, it's safe to exclude them.)
-  private static class SimpleInMemoryGroupCache {
-    private final ReviewDb db;
-    private Map<AccountGroup.UUID, Optional<GroupDescription.Basic>> groups = new HashMap<>();
-
-    public SimpleInMemoryGroupCache(ReviewDb db) {
-      this.db = db;
-    }
-
-    public Optional<GroupDescription.Basic> get(AccountGroup.UUID groupUuid) {
-      groups.computeIfAbsent(groupUuid, this::load);
-      return groups.get(groupUuid);
-    }
-
-    private Optional<GroupDescription.Basic> load(AccountGroup.UUID groupUuid) {
-      if (!AccountGroup.isInternalGroup(groupUuid)) {
-        return Optional.empty();
-      }
-
-      List<GroupDescription.Basic> groupDescriptions = getGroupDescriptions(groupUuid);
-      if (groupDescriptions.size() == 1) {
-        return Optional.of(Iterables.getOnlyElement(groupDescriptions));
-      }
-      return Optional.empty();
-    }
-
-    private List<GroupDescription.Basic> getGroupDescriptions(AccountGroup.UUID groupUuid) {
-      try (Statement stmt = ReviewDbWrapper.unwrapJbdcSchema(db).getConnection().createStatement();
-          ResultSet rs =
-              stmt.executeQuery(
-                  "SELECT name FROM account_groups where group_uuid = '" + groupUuid + "'")) {
-        List<GroupDescription.Basic> groupDescriptions = new ArrayList<>();
-        while (rs.next()) {
-          String groupName = rs.getString(1);
-          groupDescriptions.add(toGroupDescription(groupUuid, groupName));
-        }
-        return groupDescriptions;
-      } catch (SQLException ignored) {
-        logger.atWarning().withCause(ignored).log(
-            "Failed to load group %s."
-                + " Cannot get group name for group audit log commit messages.",
-            groupUuid.get());
-        return ImmutableList.of();
-      }
-    }
-
-    private static GroupDescription.Basic toGroupDescription(
-        AccountGroup.UUID groupUuid, String groupName) {
-      return new GroupDescription.Basic() {
-        @Override
-        public AccountGroup.UUID getGroupUUID() {
-          return groupUuid;
-        }
-
-        @Override
-        public String getName() {
-          return groupName;
-        }
-
-        @Nullable
-        @Override
-        public String getEmailAddress() {
-          return null;
-        }
-
-        @Nullable
-        @Override
-        public String getUrl() {
-          return null;
-        }
-      };
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_168.java b/java/com/google/gerrit/server/schema/Schema_168.java
deleted file mode 100644
index 3ea8468..0000000
--- a/java/com/google/gerrit/server/schema/Schema_168.java
+++ /dev/null
@@ -1,26 +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.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-/** Drop group tables. */
-public class Schema_168 extends SchemaVersion {
-  @Inject
-  Schema_168(Provider<Schema_167> prior) {
-    super(prior);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_169.java b/java/com/google/gerrit/server/schema/Schema_169.java
deleted file mode 100644
index 2779d47..0000000
--- a/java/com/google/gerrit/server/schema/Schema_169.java
+++ /dev/null
@@ -1,92 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.notedb.CommentJsonMigrator;
-import com.google.gerrit.server.notedb.CommentJsonMigrator.ProjectMigrationResult;
-import com.google.gerrit.server.notedb.MutableNotesMigration;
-import com.google.gerrit.server.notedb.NotesMigration;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.io.IOException;
-import java.util.SortedSet;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.ProgressMonitor;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.lib.TextProgressMonitor;
-
-/** Migrate NoteDb inline comments to JSON format. */
-public class Schema_169 extends SchemaVersion {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-  private final CommentJsonMigrator migrator;
-  private final GitRepositoryManager repoManager;
-  private final NotesMigration notesMigration;
-
-  @Inject
-  Schema_169(
-      Provider<Schema_168> prior,
-      CommentJsonMigrator migrator,
-      GitRepositoryManager repoManager,
-      @GerritServerConfig Config config) {
-    super(prior);
-    this.migrator = migrator;
-    this.repoManager = repoManager;
-    this.notesMigration = MutableNotesMigration.fromConfig(config);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
-    migrateData(ui);
-  }
-
-  @VisibleForTesting
-  protected void migrateData(UpdateUI ui) throws OrmException {
-    //  If the migration hasn't started, no need to look for non-JSON
-    if (!notesMigration.commitChangeWrites()) {
-      return;
-    }
-
-    boolean ok = true;
-    ProgressMonitor pm = new TextProgressMonitor();
-    SortedSet<Project.NameKey> projects = repoManager.list();
-    pm.beginTask("Migrating projects", projects.size());
-    int skipped = 0;
-    for (Project.NameKey project : projects) {
-      try (Repository repo = repoManager.openRepository(project)) {
-        ProjectMigrationResult progress = migrator.migrateProject(project, repo, false);
-        skipped += progress.skipped;
-      } catch (IOException e) {
-        ok = false;
-        logger.atWarning().log("Error migrating project " + project, e);
-      }
-      pm.update(1);
-    }
-
-    pm.endTask();
-    ui.message(
-        "Skipped " + skipped + " project" + (skipped == 1 ? "" : "s") + " with no legacy comments");
-
-    if (!ok) {
-      throw new OrmException("Migration failed");
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_170.java b/java/com/google/gerrit/server/schema/Schema_170.java
deleted file mode 100644
index c87fa3e..0000000
--- a/java/com/google/gerrit/server/schema/Schema_170.java
+++ /dev/null
@@ -1,25 +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.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_170 extends SchemaVersion {
-  @Inject
-  Schema_170(Provider<Schema_169> prior) {
-    super(prior);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_180.java b/java/com/google/gerrit/server/schema/Schema_180.java
new file mode 100644
index 0000000..6912b3e
--- /dev/null
+++ b/java/com/google/gerrit/server/schema/Schema_180.java
@@ -0,0 +1,22 @@
+// 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.schema;
+
+public class Schema_180 implements NoteDbSchemaVersion {
+  @Override
+  public void upgrade(Arguments args, UpdateUI ui) {
+    // Do nothing; only used to populate the version ref, which is done by the caller.
+  }
+}
diff --git a/java/com/google/gerrit/server/schema/Schema_181.java b/java/com/google/gerrit/server/schema/Schema_181.java
new file mode 100644
index 0000000..5a238ef
--- /dev/null
+++ b/java/com/google/gerrit/server/schema/Schema_181.java
@@ -0,0 +1,29 @@
+// 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.schema;
+
+import com.google.gerrit.gpg.PublicKeyStore;
+import org.eclipse.jgit.lib.Repository;
+
+public class Schema_181 implements NoteDbSchemaVersion {
+  @Override
+  public void upgrade(Arguments args, UpdateUI ui) throws Exception {
+    ui.message("Rebuild GPG note map to build subkey to master key map");
+    try (Repository repo = args.repoManager.openRepository(args.allUsers);
+        PublicKeyStore store = new PublicKeyStore(repo)) {
+      store.rebuildSubkeyMasterKeyMap();
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/schema/Schema_83.java b/java/com/google/gerrit/server/schema/Schema_83.java
deleted file mode 100644
index decbfb1..0000000
--- a/java/com/google/gerrit/server/schema/Schema_83.java
+++ /dev/null
@@ -1,33 +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.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
-
-public class Schema_83 extends SchemaVersion {
-
-  @Inject
-  Schema_83() {
-    super(
-        new Provider<SchemaVersion>() {
-          @Override
-          public SchemaVersion get() {
-            throw new ProvisionException("Upgrade first to 2.8 or 2.9");
-          }
-        });
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_84.java b/java/com/google/gerrit/server/schema/Schema_84.java
deleted file mode 100644
index c96f650..0000000
--- a/java/com/google/gerrit/server/schema/Schema_84.java
+++ /dev/null
@@ -1,26 +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.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_84 extends SchemaVersion {
-
-  @Inject
-  Schema_84(Provider<Schema_83> prior) {
-    super(prior);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_85.java b/java/com/google/gerrit/server/schema/Schema_85.java
deleted file mode 100644
index e24e67c..0000000
--- a/java/com/google/gerrit/server/schema/Schema_85.java
+++ /dev/null
@@ -1,25 +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.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_85 extends SchemaVersion {
-  @Inject
-  Schema_85(Provider<Schema_84> prior) {
-    super(prior);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_86.java b/java/com/google/gerrit/server/schema/Schema_86.java
deleted file mode 100644
index d758189..0000000
--- a/java/com/google/gerrit/server/schema/Schema_86.java
+++ /dev/null
@@ -1,25 +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.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_86 extends SchemaVersion {
-  @Inject
-  Schema_86(Provider<Schema_85> prior) {
-    super(prior);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_87.java b/java/com/google/gerrit/server/schema/Schema_87.java
deleted file mode 100644
index 5865af0..0000000
--- a/java/com/google/gerrit/server/schema/Schema_87.java
+++ /dev/null
@@ -1,80 +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.schema;
-
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.HashSet;
-import java.util.Optional;
-import java.util.Set;
-
-public class Schema_87 extends SchemaVersion {
-  @Inject
-  Schema_87(Provider<Schema_86> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    try (PreparedStatement uuidRetrieval =
-            prepareStatement(db, "SELECT group_uuid FROM account_groups WHERE group_id = ?");
-        PreparedStatement groupDeletion =
-            prepareStatement(db, "DELETE FROM account_groups WHERE group_id = ?");
-        PreparedStatement groupNameDeletion =
-            prepareStatement(db, "DELETE FROM account_group_names WHERE group_id = ?")) {
-      for (AccountGroup.Id id : scanSystemGroups(db)) {
-        Optional<AccountGroup.UUID> groupUuid = getUuid(uuidRetrieval, id);
-        if (groupUuid.filter(SystemGroupBackend::isSystemGroup).isPresent()) {
-          groupDeletion.setInt(1, id.get());
-          groupDeletion.executeUpdate();
-
-          groupNameDeletion.setInt(1, id.get());
-          groupNameDeletion.executeUpdate();
-        }
-      }
-    }
-  }
-
-  private static Optional<AccountGroup.UUID> getUuid(
-      PreparedStatement uuidRetrieval, AccountGroup.Id id) throws SQLException {
-    uuidRetrieval.setInt(1, id.get());
-    try (ResultSet uuidResults = uuidRetrieval.executeQuery()) {
-      if (uuidResults.next()) {
-        Optional.of(new AccountGroup.UUID(uuidResults.getString(1)));
-      }
-    }
-    return Optional.empty();
-  }
-
-  private static Set<AccountGroup.Id> scanSystemGroups(ReviewDb db) throws SQLException {
-    try (Statement stmt = newStatement(db);
-        ResultSet rs =
-            stmt.executeQuery("SELECT group_id FROM account_groups WHERE group_type = 'SYSTEM'")) {
-      Set<AccountGroup.Id> ids = new HashSet<>();
-      while (rs.next()) {
-        ids.add(new AccountGroup.Id(rs.getInt(1)));
-      }
-      return ids;
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_88.java b/java/com/google/gerrit/server/schema/Schema_88.java
deleted file mode 100644
index 0a7f14c..0000000
--- a/java/com/google/gerrit/server/schema/Schema_88.java
+++ /dev/null
@@ -1,25 +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.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_88 extends SchemaVersion {
-  @Inject
-  Schema_88(Provider<Schema_87> prior) {
-    super(prior);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_89.java b/java/com/google/gerrit/server/schema/Schema_89.java
deleted file mode 100644
index de84993..0000000
--- a/java/com/google/gerrit/server/schema/Schema_89.java
+++ /dev/null
@@ -1,40 +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.schema;
-
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.schema.sql.SqlDialect;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.StatementExecutor;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.sql.SQLException;
-
-public class Schema_89 extends SchemaVersion {
-  @Inject
-  Schema_89(Provider<Schema_88> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    SqlDialect dialect = ((JdbcSchema) db).getDialect();
-    try (StatementExecutor e = newExecutor(db)) {
-      dialect.dropIndex(e, "patch_set_approvals", "patch_set_approvals_openByUser");
-      dialect.dropIndex(e, "patch_set_approvals", "patch_set_approvals_closedByU");
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_90.java b/java/com/google/gerrit/server/schema/Schema_90.java
deleted file mode 100644
index d8f02ae..0000000
--- a/java/com/google/gerrit/server/schema/Schema_90.java
+++ /dev/null
@@ -1,35 +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.schema;
-
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.sql.SQLException;
-import java.sql.Statement;
-
-public class Schema_90 extends SchemaVersion {
-  @Inject
-  Schema_90(Provider<Schema_89> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException {
-    try (Statement stmt = newStatement(db)) {
-      stmt.executeUpdate("UPDATE accounts set size_bar_in_change_table = 'Y'");
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_91.java b/java/com/google/gerrit/server/schema/Schema_91.java
deleted file mode 100644
index 173793e..0000000
--- a/java/com/google/gerrit/server/schema/Schema_91.java
+++ /dev/null
@@ -1,25 +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.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_91 extends SchemaVersion {
-  @Inject
-  Schema_91(Provider<Schema_90> prior) {
-    super(prior);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_92.java b/java/com/google/gerrit/server/schema/Schema_92.java
deleted file mode 100644
index 5f5c141..0000000
--- a/java/com/google/gerrit/server/schema/Schema_92.java
+++ /dev/null
@@ -1,25 +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.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_92 extends SchemaVersion {
-  @Inject
-  Schema_92(Provider<Schema_91> prior) {
-    super(prior);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_93.java b/java/com/google/gerrit/server/schema/Schema_93.java
deleted file mode 100644
index 3132aa4..0000000
--- a/java/com/google/gerrit/server/schema/Schema_93.java
+++ /dev/null
@@ -1,25 +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.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_93 extends SchemaVersion {
-  @Inject
-  Schema_93(Provider<Schema_92> prior) {
-    super(prior);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_94.java b/java/com/google/gerrit/server/schema/Schema_94.java
deleted file mode 100644
index d4a189f..0000000
--- a/java/com/google/gerrit/server/schema/Schema_94.java
+++ /dev/null
@@ -1,35 +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.schema;
-
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.sql.SQLException;
-import java.sql.Statement;
-
-public class Schema_94 extends SchemaVersion {
-  @Inject
-  Schema_94(Provider<Schema_93> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException {
-    try (Statement stmt = newStatement(db)) {
-      stmt.execute("CREATE INDEX patch_sets_byRevision ON patch_sets (revision)");
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_95.java b/java/com/google/gerrit/server/schema/Schema_95.java
deleted file mode 100644
index 0ce0294..0000000
--- a/java/com/google/gerrit/server/schema/Schema_95.java
+++ /dev/null
@@ -1,42 +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.schema;
-
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.io.IOException;
-import java.sql.SQLException;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-
-public class Schema_95 extends SchemaVersion {
-  private final AllUsersCreator allUsersCreator;
-
-  @Inject
-  Schema_95(Provider<Schema_94> prior, AllUsersCreator allUsersCreator) {
-    super(prior);
-    this.allUsersCreator = allUsersCreator;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    try {
-      allUsersCreator.create();
-    } catch (IOException | ConfigInvalidException e) {
-      throw new OrmException(e);
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_96.java b/java/com/google/gerrit/server/schema/Schema_96.java
deleted file mode 100644
index bf19213..0000000
--- a/java/com/google/gerrit/server/schema/Schema_96.java
+++ /dev/null
@@ -1,25 +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.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_96 extends SchemaVersion {
-  @Inject
-  Schema_96(Provider<Schema_95> prior) {
-    super(prior);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_97.java b/java/com/google/gerrit/server/schema/Schema_97.java
deleted file mode 100644
index 0670377..0000000
--- a/java/com/google/gerrit/server/schema/Schema_97.java
+++ /dev/null
@@ -1,25 +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.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_97 extends SchemaVersion {
-  @Inject
-  Schema_97(Provider<Schema_96> prior) {
-    super(prior);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_98.java b/java/com/google/gerrit/server/schema/Schema_98.java
deleted file mode 100644
index eec3c9f..0000000
--- a/java/com/google/gerrit/server/schema/Schema_98.java
+++ /dev/null
@@ -1,39 +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.schema;
-
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.sql.SQLException;
-import java.sql.Statement;
-
-public class Schema_98 extends SchemaVersion {
-  @Inject
-  Schema_98(Provider<Schema_97> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException {
-    ui.message("Migrate user preference showUserInReview to reviewCategoryStrategy");
-    try (Statement stmt = newStatement(db)) {
-      stmt.executeUpdate(
-          "UPDATE accounts SET "
-              + "REVIEW_CATEGORY_STRATEGY='NAME' "
-              + "WHERE (SHOW_USER_IN_REVIEW='Y')");
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/Schema_99.java b/java/com/google/gerrit/server/schema/Schema_99.java
deleted file mode 100644
index b7fab7f..0000000
--- a/java/com/google/gerrit/server/schema/Schema_99.java
+++ /dev/null
@@ -1,25 +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.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_99 extends SchemaVersion {
-  @Inject
-  Schema_99(Provider<Schema_98> prior) {
-    super(prior);
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/ScriptRunner.java b/java/com/google/gerrit/server/schema/ScriptRunner.java
deleted file mode 100644
index f4cba98..0000000
--- a/java/com/google/gerrit/server/schema/ScriptRunner.java
+++ /dev/null
@@ -1,125 +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.server.schema;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.common.base.CharMatcher;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.schema.sql.SqlDialect;
-import com.google.gwtorm.server.OrmException;
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.sql.Connection;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.ArrayList;
-import java.util.List;
-
-/** Parses an SQL script from a resource file and later runs it. */
-class ScriptRunner {
-  private final String name;
-  private final List<String> commands;
-
-  static final ScriptRunner NOOP =
-      new ScriptRunner(null, null) {
-        @Override
-        void run(ReviewDb db) {}
-      };
-
-  ScriptRunner(String scriptName, InputStream script) {
-    this.name = scriptName;
-    try {
-      this.commands = script != null ? parse(script) : null;
-    } catch (IOException e) {
-      throw new IllegalStateException("Cannot parse " + name, e);
-    }
-  }
-
-  void run(ReviewDb db) throws OrmException {
-    try {
-      final JdbcSchema schema = (JdbcSchema) db;
-      final Connection c = schema.getConnection();
-      final SqlDialect dialect = schema.getDialect();
-      try (Statement stmt = c.createStatement()) {
-        for (String sql : commands) {
-          try {
-            if (!dialect.isStatementDelimiterSupported()) {
-              sql = CharMatcher.is(';').trimTrailingFrom(sql);
-            }
-            stmt.execute(sql);
-          } catch (SQLException e) {
-            throw new OrmException("Error in " + name + ":\n" + sql, e);
-          }
-        }
-      }
-    } catch (SQLException e) {
-      throw new OrmException("Cannot run statements for " + name, e);
-    }
-  }
-
-  private List<String> parse(InputStream in) throws IOException {
-    try (BufferedReader br = new BufferedReader(new InputStreamReader(in, UTF_8))) {
-      String delimiter = ";";
-      List<String> commands = new ArrayList<>();
-      StringBuilder buffer = new StringBuilder();
-      String line;
-      while ((line = br.readLine()) != null) {
-        if (line.isEmpty()) {
-          continue;
-        }
-        if (line.startsWith("--")) {
-          continue;
-        }
-
-        if (buffer.length() == 0 && line.toLowerCase().startsWith("delimiter ")) {
-          delimiter = line.substring("delimiter ".length()).trim();
-          continue;
-        }
-
-        if (buffer.length() > 0) {
-          buffer.append('\n');
-        }
-        buffer.append(line);
-
-        if (isDone(delimiter, line, buffer)) {
-          String cmd = buffer.toString();
-          commands.add(cmd);
-          buffer = new StringBuilder();
-        }
-      }
-      if (buffer.length() > 0) {
-        commands.add(buffer.toString());
-      }
-      return commands;
-    }
-  }
-
-  private boolean isDone(String delimiter, String line, StringBuilder buffer) {
-    if (";".equals(delimiter)) {
-      return buffer.charAt(buffer.length() - 1) == ';';
-
-    } else if (line.equals(delimiter)) {
-      buffer.setLength(buffer.length() - delimiter.length());
-      return true;
-
-    } else {
-      return false;
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/UpdateUI.java b/java/com/google/gerrit/server/schema/UpdateUI.java
index 0c02607..b5a6b6e 100644
--- a/java/com/google/gerrit/server/schema/UpdateUI.java
+++ b/java/com/google/gerrit/server/schema/UpdateUI.java
@@ -14,9 +14,6 @@
 
 package com.google.gerrit.server.schema;
 
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.StatementExecutor;
-import java.util.List;
 import java.util.Set;
 
 public interface UpdateUI {
@@ -37,6 +34,4 @@
   String readString(String defaultValue, Set<String> allowedValues, String message);
 
   boolean isBatch();
-
-  void pruneSchema(StatementExecutor e, List<String> pruneList) throws OrmException;
 }
diff --git a/java/com/google/gerrit/server/schema/testing/AllProjectsCreatorTestUtil.java b/java/com/google/gerrit/server/schema/testing/AllProjectsCreatorTestUtil.java
new file mode 100644
index 0000000..63837b2
--- /dev/null
+++ b/java/com/google/gerrit/server/schema/testing/AllProjectsCreatorTestUtil.java
@@ -0,0 +1,177 @@
+// 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.schema.testing;
+
+import static com.google.common.truth.Truth.assertThat;
+
+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.server.config.AllProjectsName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import java.io.IOException;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.BlobBasedConfig;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+
+public class AllProjectsCreatorTestUtil {
+  private static final ImmutableList<String> DEFAULT_ALL_PROJECTS_PROJECT_SECTION =
+      ImmutableList.of("[project]", "  description = Access inherited by all other projects.");
+  private static final ImmutableList<String> DEFAULT_ALL_PROJECTS_RECEIVE_SECTION =
+      ImmutableList.of(
+          "[receive]",
+          "  requireContributorAgreement = false",
+          "  requireSignedOffBy = false",
+          "  requireChangeId = true",
+          "  enableSignedPush = false");
+  private static final ImmutableList<String> DEFAULT_ALL_PROJECTS_SUBMIT_SECTION =
+      ImmutableList.of("[submit]", "  mergeContent = true");
+  private static final ImmutableList<String> DEFAULT_ALL_PROJECTS_CAPABILITY_SECTION =
+      ImmutableList.of(
+          "[capability]",
+          "  administrateServer = group Administrators",
+          "  priority = batch group Non-Interactive Users",
+          "  streamEvents = group Non-Interactive Users");
+  private static final ImmutableList<String> DEFAULT_ALL_PROJECTS_ACCESS_SECTION =
+      ImmutableList.of(
+          "[access \"refs/*\"]",
+          "  read = group Administrators",
+          "  read = group Anonymous Users",
+          "[access \"refs/for/*\"]",
+          "  addPatchSet = group Registered Users",
+          "[access \"refs/for/refs/*\"]",
+          "  push = group Registered Users",
+          "  pushMerge = group Registered Users",
+          "[access \"refs/heads/*\"]",
+          "  create = group Administrators",
+          "  create = group Project Owners",
+          "  editTopicName = +force group Administrators",
+          "  editTopicName = +force group Project Owners",
+          "  forgeAuthor = group Registered Users",
+          "  forgeCommitter = group Administrators",
+          "  forgeCommitter = group Project Owners",
+          "  label-Code-Review = -2..+2 group Administrators",
+          "  label-Code-Review = -2..+2 group Project Owners",
+          "  label-Code-Review = -1..+1 group Registered Users",
+          "  push = group Administrators",
+          "  push = group Project Owners",
+          "  submit = group Administrators",
+          "  submit = group Project Owners",
+          "[access \"refs/meta/config\"]",
+          "  exclusiveGroupPermissions = read",
+          "  create = group Administrators",
+          "  create = group Project Owners",
+          "  label-Code-Review = -2..+2 group Administrators",
+          "  label-Code-Review = -2..+2 group Project Owners",
+          "  push = group Administrators",
+          "  push = group Project Owners",
+          "  read = group Administrators",
+          "  read = group Project Owners",
+          "  submit = group Administrators",
+          "  submit = group Project Owners",
+          "[access \"refs/tags/*\"]",
+          "  create = group Administrators",
+          "  create = group Project Owners",
+          "  createSignedTag = group Administrators",
+          "  createSignedTag = group Project Owners",
+          "  createTag = group Administrators",
+          "  createTag = group Project Owners");
+  private static final ImmutableList<String> DEFAULT_ALL_PROJECTS_LABEL_SECTION =
+      ImmutableList.of(
+          "[label \"Code-Review\"]",
+          "  function = MaxWithBlock",
+          "  defaultValue = 0",
+          "  copyMinScore = true",
+          "  copyAllScoresOnTrivialRebase = true",
+          "  value = -2 This shall not be merged",
+          "  value = -1 I would prefer this is not merged as is",
+          "  value = 0 No score",
+          "  value = +1 Looks good to me, but someone else must approve",
+          "  value = +2 Looks good to me, approved");
+
+  public static String getDefaultAllProjectsWithAllDefaultSections() {
+    return Streams.stream(
+            Iterables.concat(
+                DEFAULT_ALL_PROJECTS_PROJECT_SECTION,
+                DEFAULT_ALL_PROJECTS_RECEIVE_SECTION,
+                DEFAULT_ALL_PROJECTS_SUBMIT_SECTION,
+                DEFAULT_ALL_PROJECTS_CAPABILITY_SECTION,
+                DEFAULT_ALL_PROJECTS_ACCESS_SECTION,
+                DEFAULT_ALL_PROJECTS_LABEL_SECTION))
+        .collect(Collectors.joining("\n"));
+  }
+
+  public static String getAllProjectsWithoutDefaultAcls() {
+    return Streams.stream(
+            Iterables.concat(
+                DEFAULT_ALL_PROJECTS_PROJECT_SECTION,
+                DEFAULT_ALL_PROJECTS_RECEIVE_SECTION,
+                DEFAULT_ALL_PROJECTS_SUBMIT_SECTION,
+                DEFAULT_ALL_PROJECTS_LABEL_SECTION))
+        .collect(Collectors.joining("\n"));
+  }
+
+  // Loads the "project.config" from the All-Projects repo.
+  public static Config readAllProjectsConfig(
+      GitRepositoryManager repoManager, AllProjectsName allProjectsName)
+      throws IOException, ConfigInvalidException {
+    try (Repository repo = repoManager.openRepository(allProjectsName)) {
+      Ref configRef = repo.exactRef(RefNames.REFS_CONFIG);
+      return new BlobBasedConfig(null, repo, configRef.getObjectId(), "project.config");
+    }
+  }
+
+  public static void assertTwoConfigsEquivalent(Config config1, Config config2) {
+    Set<String> sections1 = config1.getSections();
+    Set<String> sections2 = config2.getSections();
+    assertThat(sections1).containsExactlyElementsIn(sections2);
+
+    sections1.forEach(s -> assertSectionEquivalent(config1, config2, s));
+  }
+
+  public static void assertSectionEquivalent(Config config1, Config config2, String section) {
+    assertSubsectionEquivalent(config1, config2, section, null);
+
+    Set<String> subsections1 = config1.getSubsections(section);
+    Set<String> subsections2 = config2.getSubsections(section);
+    assertThat(subsections1)
+        .named("section \"%s\"", section)
+        .containsExactlyElementsIn(subsections2);
+
+    subsections1.forEach(s -> assertSubsectionEquivalent(config1, config2, section, s));
+  }
+
+  private static void assertSubsectionEquivalent(
+      Config config1, Config config2, String section, String subsection) {
+    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);
+
+    subsectionNames1.forEach(
+        n ->
+            assertThat(config1.getStringList(section, subsection, n))
+                .named(name)
+                .asList()
+                .containsExactlyElementsIn(config2.getStringList(section, subsection, n)));
+  }
+
+  private AllProjectsCreatorTestUtil() {}
+}
diff --git a/java/com/google/gerrit/server/schema/testing/BUILD b/java/com/google/gerrit/server/schema/testing/BUILD
new file mode 100644
index 0000000..d641c47
--- /dev/null
+++ b/java/com/google/gerrit/server/schema/testing/BUILD
@@ -0,0 +1,16 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
+package(default_visibility = ["//visibility:public"])
+
+java_library(
+    name = "testing",
+    testonly = True,
+    srcs = glob(["*.java"]),
+    deps = [
+        "//java/com/google/gerrit/reviewdb:server",
+        "//java/com/google/gerrit/server",
+        "//lib:guava",
+        "//lib/jgit/org.eclipse.jgit:jgit",
+        "//lib/truth",
+    ],
+)
diff --git a/java/com/google/gerrit/server/securestore/SecureStoreClassName.java b/java/com/google/gerrit/server/securestore/SecureStoreClassName.java
index 0247fc1..2989af0 100644
--- a/java/com/google/gerrit/server/securestore/SecureStoreClassName.java
+++ b/java/com/google/gerrit/server/securestore/SecureStoreClassName.java
@@ -1,3 +1,17 @@
+// 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.securestore;
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
diff --git a/java/com/google/gerrit/server/securestore/testing/BUILD b/java/com/google/gerrit/server/securestore/testing/BUILD
new file mode 100644
index 0000000..c2582b9
--- /dev/null
+++ b/java/com/google/gerrit/server/securestore/testing/BUILD
@@ -0,0 +1,13 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
+package(default_testonly = True)
+
+java_library(
+    name = "testing",
+    srcs = glob(["*.java"]),
+    visibility = ["//visibility:public"],
+    deps = [
+        "//java/com/google/gerrit/server",
+        "//lib/jgit/org.eclipse.jgit:jgit",
+    ],
+)
diff --git a/java/com/google/gerrit/server/securestore/testing/InMemorySecureStore.java b/java/com/google/gerrit/server/securestore/testing/InMemorySecureStore.java
new file mode 100644
index 0000000..23894c1
--- /dev/null
+++ b/java/com/google/gerrit/server/securestore/testing/InMemorySecureStore.java
@@ -0,0 +1,59 @@
+// 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.securestore.testing;
+
+import com.google.gerrit.server.securestore.SecureStore;
+import java.util.List;
+import org.eclipse.jgit.lib.Config;
+
+public class InMemorySecureStore extends SecureStore {
+  private final Config cfg = new Config();
+
+  @Override
+  public String[] getList(String section, String subsection, String name) {
+    return cfg.getStringList(section, subsection, name);
+  }
+
+  @Override
+  public String[] getListForPlugin(
+      String pluginName, String section, String subsection, String name) {
+    throw new UnsupportedOperationException("not used by tests");
+  }
+
+  @Override
+  public void setList(String section, String subsection, String name, List<String> values) {
+    cfg.setStringList(section, subsection, name, values);
+  }
+
+  @Override
+  public void unset(String section, String subsection, String name) {
+    cfg.unset(section, subsection, name);
+  }
+
+  @Override
+  public Iterable<EntryKey> list() {
+    throw new UnsupportedOperationException("not used by tests");
+  }
+
+  @Override
+  public boolean isOutdated() {
+    throw new UnsupportedOperationException("not used by tests");
+  }
+
+  @Override
+  public void reload() {
+    throw new UnsupportedOperationException("not used by tests");
+  }
+}
diff --git a/java/com/google/gerrit/server/ssh/NoSshKeyCache.java b/java/com/google/gerrit/server/ssh/NoSshKeyCache.java
index 387242c..74bb50c 100644
--- a/java/com/google/gerrit/server/ssh/NoSshKeyCache.java
+++ b/java/com/google/gerrit/server/ssh/NoSshKeyCache.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.ssh;
 
-import com.google.gerrit.common.errors.InvalidSshKeyException;
+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;
diff --git a/java/com/google/gerrit/server/ssh/SshKeyCreator.java b/java/com/google/gerrit/server/ssh/SshKeyCreator.java
index 55ba5ed..beaf1ba 100644
--- a/java/com/google/gerrit/server/ssh/SshKeyCreator.java
+++ b/java/com/google/gerrit/server/ssh/SshKeyCreator.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.ssh;
 
-import com.google.gerrit.common.errors.InvalidSshKeyException;
+import com.google.gerrit.exceptions.InvalidSshKeyException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountSshKey;
 
diff --git a/java/com/google/gerrit/server/submit/ChangeSet.java b/java/com/google/gerrit/server/submit/ChangeSet.java
index 8c94a21..e721be41ab 100644
--- a/java/com/google/gerrit/server/submit/ChangeSet.java
+++ b/java/com/google/gerrit/server/submit/ChangeSet.java
@@ -24,7 +24,6 @@
 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.gwtorm.server.OrmException;
 import java.util.Collection;
 import java.util.LinkedHashMap;
 import java.util.Map;
@@ -61,14 +60,12 @@
   }
 
   public ChangeSet(Iterable<ChangeData> changes, Iterable<ChangeData> hiddenChanges) {
-    changeData = index(changes, ImmutableList.<Change.Id>of());
+    changeData = index(changes, ImmutableList.of());
     nonVisibleChanges = index(hiddenChanges, changeData.keySet());
   }
 
   public ChangeSet(ChangeData change, boolean visible) {
-    this(
-        visible ? ImmutableList.of(change) : ImmutableList.<ChangeData>of(),
-        ImmutableList.of(change));
+    this(visible ? ImmutableList.of(change) : ImmutableList.of(), ImmutableList.of(change));
   }
 
   public ImmutableSet<Change.Id> ids() {
@@ -79,7 +76,7 @@
     return changeData;
   }
 
-  public ListMultimap<Branch.NameKey, ChangeData> changesByBranch() throws OrmException {
+  public ListMultimap<Branch.NameKey, ChangeData> changesByBranch() {
     ListMultimap<Branch.NameKey, ChangeData> ret =
         MultimapBuilder.hashKeys().arrayListValues().build();
     for (ChangeData cd : changeData.values()) {
diff --git a/java/com/google/gerrit/server/submit/CherryPick.java b/java/com/google/gerrit/server/submit/CherryPick.java
index 182c22a..8ff3cc5 100644
--- a/java/com/google/gerrit/server/submit/CherryPick.java
+++ b/java/com/google/gerrit/server/submit/CherryPick.java
@@ -30,7 +30,6 @@
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.RepoContext;
-import com.google.gwtorm.server.OrmException;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -91,14 +90,14 @@
 
     @Override
     protected void updateRepoImpl(RepoContext ctx)
-        throws IntegrationException, IOException, OrmException, MethodNotAllowedException {
+        throws IntegrationException, IOException, MethodNotAllowedException {
       // If there is only one parent, a cherry-pick can be done by taking the
       // delta relative to that one parent and redoing that on the current merge
       // tip.
       args.rw.parseBody(toMerge);
       psId =
-          ChangeUtil.nextPatchSetIdFromChangeRefsMap(
-              ctx.getRepoView().getRefs(getId().toRefPrefix()),
+          ChangeUtil.nextPatchSetIdFromChangeRefs(
+              ctx.getRepoView().getRefs(getId().toRefPrefix()).keySet(),
               toMerge.change().currentPatchSetId());
       RevCommit mergeTip = args.mergeTip.getCurrentTip();
       args.rw.parseBody(mergeTip);
@@ -146,8 +145,7 @@
     }
 
     @Override
-    public PatchSet updateChangeImpl(ChangeContext ctx)
-        throws OrmException, NoSuchChangeException, IOException {
+    public PatchSet updateChangeImpl(ChangeContext ctx) throws NoSuchChangeException, IOException {
       if (newCommit == null && toMerge.getStatusCode() == SKIPPED_IDENTICAL_TREE) {
         return null;
       }
@@ -157,15 +155,14 @@
               String.format(
                   "no new commit produced by CherryPick of %s, expected to fail fast",
                   toMerge.change().getId()));
-      PatchSet prevPs = args.psUtil.current(ctx.getDb(), ctx.getNotes());
+      PatchSet prevPs = args.psUtil.current(ctx.getNotes());
       PatchSet newPs =
           args.psUtil.insert(
-              ctx.getDb(),
               ctx.getRevWalk(),
               ctx.getUpdate(psId),
               psId,
               newCommit,
-              prevPs != null ? prevPs.getGroups() : ImmutableList.<String>of(),
+              prevPs != null ? prevPs.getGroups() : 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 3163bd2..12172dd 100644
--- a/java/com/google/gerrit/server/submit/CommitMergeStatus.java
+++ b/java/com/google/gerrit/server/submit/CommitMergeStatus.java
@@ -16,10 +16,11 @@
 
 import static java.util.stream.Collectors.toSet;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 import java.util.List;
 import java.util.Optional;
@@ -88,15 +89,17 @@
   }
 
   public static String createMissingDependencyMessage(
-      Provider<InternalChangeQuery> queryProvider, String commit, String otherCommit)
-      throws OrmException {
+      @Nullable CurrentUser caller,
+      Provider<InternalChangeQuery> queryProvider,
+      String commit,
+      String otherCommit) {
     List<ChangeData> changes = queryProvider.get().enforceVisibility(true).byCommit(otherCommit);
 
     if (changes.isEmpty()) {
       return String.format(
           "Commit %s depends on commit %s which cannot be merged."
-              + " Is the change of this commit not visible or was it deleted?",
-          commit, otherCommit);
+              + " Is the change of this commit not visible to '%s' or was it deleted?",
+          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)) {
diff --git a/java/com/google/gerrit/server/submit/EmailMerge.java b/java/com/google/gerrit/server/submit/EmailMerge.java
index a6b73447..a1f56eb 100644
--- a/java/com/google/gerrit/server/submit/EmailMerge.java
+++ b/java/com/google/gerrit/server/submit/EmailMerge.java
@@ -14,27 +14,20 @@
 
 package com.google.gerrit.server.submit;
 
-import com.google.common.collect.ListMultimap;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
-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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.config.SendEmailExecutor;
 import com.google.gerrit.server.mail.send.MergedSender;
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.OutOfScopeException;
-import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
 import com.google.inject.assistedinject.Assisted;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
@@ -47,46 +40,37 @@
         Project.NameKey project,
         Change.Id changeId,
         Account.Id submitter,
-        NotifyHandling notifyHandling,
-        ListMultimap<RecipientType, Account.Id> accountsToNotify);
+        NotifyResolver.Result notify);
   }
 
   private final ExecutorService sendEmailsExecutor;
   private final MergedSender.Factory mergedSenderFactory;
-  private final SchemaFactory<ReviewDb> schemaFactory;
   private final ThreadLocalRequestContext requestContext;
   private final IdentifiedUser.GenericFactory identifiedUserFactory;
 
   private final Project.NameKey project;
   private final Change.Id changeId;
   private final Account.Id submitter;
-  private final NotifyHandling notifyHandling;
-  private final ListMultimap<RecipientType, Account.Id> accountsToNotify;
-
-  private ReviewDb db;
+  private final NotifyResolver.Result notify;
 
   @Inject
   EmailMerge(
       @SendEmailExecutor ExecutorService executor,
       MergedSender.Factory mergedSenderFactory,
-      SchemaFactory<ReviewDb> schemaFactory,
       ThreadLocalRequestContext requestContext,
       IdentifiedUser.GenericFactory identifiedUserFactory,
       @Assisted Project.NameKey project,
       @Assisted Change.Id changeId,
       @Assisted @Nullable Account.Id submitter,
-      @Assisted NotifyHandling notifyHandling,
-      @Assisted ListMultimap<RecipientType, Account.Id> accountsToNotify) {
+      @Assisted NotifyResolver.Result notify) {
     this.sendEmailsExecutor = executor;
     this.mergedSenderFactory = mergedSenderFactory;
-    this.schemaFactory = schemaFactory;
     this.requestContext = requestContext;
     this.identifiedUserFactory = identifiedUserFactory;
     this.project = project;
     this.changeId = changeId;
     this.submitter = submitter;
-    this.notifyHandling = notifyHandling;
-    this.accountsToNotify = accountsToNotify;
+    this.notify = notify;
   }
 
   void sendAsync() {
@@ -102,17 +86,12 @@
       if (submitter != null) {
         cm.setFrom(submitter);
       }
-      cm.setNotify(notifyHandling);
-      cm.setAccountsToNotify(accountsToNotify);
+      cm.setNotify(notify);
       cm.send();
     } catch (Exception e) {
       logger.atSevere().withCause(e).log("Cannot email merged notification for %s", changeId);
     } finally {
       requestContext.setContext(old);
-      if (db != null) {
-        db.close();
-        db = null;
-      }
     }
   }
 
@@ -128,21 +107,4 @@
     }
     throw new OutOfScopeException("No user on email thread");
   }
-
-  @Override
-  public Provider<ReviewDb> getReviewDbProvider() {
-    return new Provider<ReviewDb>() {
-      @Override
-      public ReviewDb get() {
-        if (db == null) {
-          try {
-            db = schemaFactory.open();
-          } catch (OrmException e) {
-            throw new ProvisionException("Cannot open ReviewDb", e);
-          }
-        }
-        return db;
-      }
-    };
-  }
 }
diff --git a/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java b/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java
index 9efb976..831a8b8 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.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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -37,9 +37,9 @@
 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.query.change.ChangeIsVisibleToPredicate;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.server.submit.MergeOpRepoManager.OpenRepo;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -87,26 +87,29 @@
 
   private final PermissionBackend permissionBackend;
   private final Provider<InternalChangeQuery> queryProvider;
-  private final Map<QueryKey, List<ChangeData>> queryCache;
+  private final Map<QueryKey, ImmutableList<ChangeData>> queryCache;
   private final Map<Branch.NameKey, Optional<RevCommit>> heads;
   private final ProjectCache projectCache;
+  private final ChangeIsVisibleToPredicate changeIsVisibleToPredicate;
 
   @Inject
   LocalMergeSuperSetComputation(
       PermissionBackend permissionBackend,
       Provider<InternalChangeQuery> queryProvider,
-      ProjectCache projectCache) {
+      ProjectCache projectCache,
+      ChangeIsVisibleToPredicate changeIsVisibleToPredicate) {
     this.projectCache = projectCache;
     this.permissionBackend = permissionBackend;
     this.queryProvider = queryProvider;
     this.queryCache = new HashMap<>();
     this.heads = new HashMap<>();
+    this.changeIsVisibleToPredicate = changeIsVisibleToPredicate;
   }
 
   @Override
   public ChangeSet completeWithoutTopic(
-      ReviewDb db, MergeOpRepoManager orm, ChangeSet changeSet, CurrentUser user)
-      throws OrmException, IOException, PermissionBackendException {
+      MergeOpRepoManager orm, ChangeSet changeSet, CurrentUser user)
+      throws IOException, PermissionBackendException {
     Collection<ChangeData> visibleChanges = new ArrayList<>();
     Collection<ChangeData> nonVisibleChanges = new ArrayList<>();
 
@@ -119,7 +122,7 @@
       List<RevCommit> visibleCommits = new ArrayList<>();
       List<RevCommit> nonVisibleCommits = new ArrayList<>();
       for (ChangeData cd : bc.get(b)) {
-        boolean visible = isVisible(db, changeSet, cd, user);
+        boolean visible = isVisible(changeSet, cd, user);
 
         if (submitType(cd) == SubmitType.CHERRY_PICK) {
           if (visible) {
@@ -147,17 +150,18 @@
 
       Set<String> visibleHashes =
           walkChangesByHashes(visibleCommits, Collections.emptySet(), or, b);
-      Iterables.addAll(visibleChanges, byCommitsOnBranchNotMerged(or, db, b, visibleHashes));
-
       Set<String> nonVisibleHashes = walkChangesByHashes(nonVisibleCommits, visibleHashes, or, b);
-      Iterables.addAll(nonVisibleChanges, byCommitsOnBranchNotMerged(or, db, b, nonVisibleHashes));
+
+      ChangeSet partialSet = byCommitsOnBranchNotMerged(or, b, visibleHashes, nonVisibleHashes);
+      Iterables.addAll(visibleChanges, partialSet.changes());
+      Iterables.addAll(nonVisibleChanges, partialSet.nonVisibleChanges());
     }
 
     return new ChangeSet(visibleChanges, nonVisibleChanges);
   }
 
   private static ImmutableListMultimap<Branch.NameKey, ChangeData> byBranch(
-      Iterable<ChangeData> changes) throws OrmException {
+      Iterable<ChangeData> changes) {
     ImmutableListMultimap.Builder<Branch.NameKey, ChangeData> builder =
         ImmutableListMultimap.builder();
     for (ChangeData cd : changes) {
@@ -176,7 +180,7 @@
     }
   }
 
-  private boolean isVisible(ReviewDb db, ChangeSet changeSet, ChangeData cd, CurrentUser user)
+  private boolean isVisible(ChangeSet changeSet, ChangeData cd, CurrentUser user)
       throws PermissionBackendException, IOException {
     ProjectState projectState = projectCache.checkedGet(cd.project());
     boolean visible =
@@ -188,7 +192,7 @@
     }
 
     try {
-      permissionBackend.user(user).change(cd).database(db).check(ChangePermission.READ);
+      permissionBackend.user(user).change(cd).check(ChangePermission.READ);
       return true;
     } catch (AuthException e) {
       // We thought the change was visible, but it isn't.
@@ -198,7 +202,7 @@
     }
   }
 
-  private SubmitType submitType(ChangeData cd) throws OrmException {
+  private SubmitType submitType(ChangeData cd) {
     SubmitTypeRecord str = cd.submitTypeRecord();
     if (!str.isOk()) {
       logErrorAndThrow("Failed to get submit type for " + cd.getId() + ": " + str.errorMessage);
@@ -206,24 +210,36 @@
     return str.type;
   }
 
-  private List<ChangeData> byCommitsOnBranchNotMerged(
-      OpenRepo or, ReviewDb db, Branch.NameKey branch, Set<String> hashes)
-      throws OrmException, IOException {
+  private ChangeSet byCommitsOnBranchNotMerged(
+      OpenRepo or, Branch.NameKey branch, Set<String> visibleHashes, Set<String> nonVisibleHashes)
+      throws IOException {
+    List<ChangeData> potentiallyVisibleChanges =
+        byCommitsOnBranchNotMerged(or, branch, visibleHashes);
+    List<ChangeData> invisibleChanges =
+        new ArrayList<>(byCommitsOnBranchNotMerged(or, branch, nonVisibleHashes));
+    List<ChangeData> visibleChanges = new ArrayList<>(potentiallyVisibleChanges.size());
+    for (ChangeData cd : potentiallyVisibleChanges) {
+      if (changeIsVisibleToPredicate.match(cd)) {
+        visibleChanges.add(cd);
+      } else {
+        invisibleChanges.add(cd);
+      }
+    }
+    return new ChangeSet(visibleChanges, invisibleChanges);
+  }
+
+  private ImmutableList<ChangeData> byCommitsOnBranchNotMerged(
+      OpenRepo or, Branch.NameKey branch, Set<String> hashes) throws IOException {
     if (hashes.isEmpty()) {
       return ImmutableList.of();
     }
     QueryKey k = QueryKey.create(branch, hashes);
-    List<ChangeData> cached = queryCache.get(k);
-    if (cached != null) {
-      return cached;
+    if (queryCache.containsKey(k)) {
+      return queryCache.get(k);
     }
-
-    List<ChangeData> result = new ArrayList<>();
-    Iterable<ChangeData> destChanges =
-        queryProvider.get().byCommitsOnBranchNotMerged(or.repo, db, branch, hashes);
-    for (ChangeData chd : destChanges) {
-      result.add(chd);
-    }
+    ImmutableList<ChangeData> result =
+        ImmutableList.copyOf(
+            queryProvider.get().byCommitsOnBranchNotMerged(or.repo, branch, hashes));
     queryCache.put(k, result);
     return result;
   }
@@ -265,8 +281,8 @@
     }
   }
 
-  private void logErrorAndThrow(String msg) throws OrmException {
+  private void logErrorAndThrow(String msg) {
     logger.atSevere().log(msg);
-    throw new OrmException(msg);
+    throw new StorageException(msg);
   }
 }
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index 9c841ca..63d60a6 100644
--- a/java/com/google/gerrit/server/submit/MergeOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.submit;
 
+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 java.util.Comparator.comparing;
@@ -35,29 +36,29 @@
 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.extensions.api.changes.RecipientType;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.client.SubmitType;
 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.RestApiException;
+import com.google.gerrit.git.LockFailureException;
 import com.google.gerrit.metrics.Counter0;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.MetricMaker;
-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.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.InternalUser;
-import com.google.gerrit.server.change.NotifyUtil;
+import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.git.CodeReviewCommit;
-import com.google.gerrit.server.git.LockFailureException;
 import com.google.gerrit.server.git.MergeTip;
 import com.google.gerrit.server.git.validators.MergeValidationException;
 import com.google.gerrit.server.git.validators.MergeValidators;
@@ -78,7 +79,6 @@
 import com.google.gerrit.server.update.RetryHelper.ActionType;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -124,7 +124,7 @@
     private final ListMultimap<Change.Id, String> problems;
     private final boolean allowClosed;
 
-    private CommitStatus(ChangeSet cs, boolean allowClosed) throws OrmException {
+    private CommitStatus(ChangeSet cs, boolean allowClosed) {
       checkArgument(
           !cs.furtherHiddenChanges(), "CommitStatus must not be called with hidden changes");
       changes = cs.changesById();
@@ -229,7 +229,7 @@
   private final SubmitStrategyFactory submitStrategyFactory;
   private final SubmoduleOp.Factory subOpFactory;
   private final Provider<MergeOpRepoManager> ormProvider;
-  private final NotifyUtil notifyUtil;
+  private final NotifyResolver notifyResolver;
   private final RetryHelper retryHelper;
   private final ChangeData.Factory changeDataFactory;
 
@@ -239,9 +239,8 @@
 
   private MergeOpRepoManager orm;
   private CommitStatus commitStatus;
-  private ReviewDb db;
   private SubmitInput submitInput;
-  private ListMultimap<RecipientType, Account.Id> accountsToNotify;
+  private NotifyResolver.Result notify;
   private Set<Project.NameKey> allProjects;
   private boolean dryrun;
   private TopicMetrics topicMetrics;
@@ -257,7 +256,7 @@
       SubmitStrategyFactory submitStrategyFactory,
       SubmoduleOp.Factory subOpFactory,
       Provider<MergeOpRepoManager> ormProvider,
-      NotifyUtil notifyUtil,
+      NotifyResolver notifyResolver,
       TopicMetrics topicMetrics,
       RetryHelper retryHelper,
       ChangeData.Factory changeDataFactory) {
@@ -270,7 +269,7 @@
     this.submitStrategyFactory = submitStrategyFactory;
     this.subOpFactory = subOpFactory;
     this.ormProvider = ormProvider;
-    this.notifyUtil = notifyUtil;
+    this.notifyResolver = notifyResolver;
     this.retryHelper = retryHelper;
     this.topicMetrics = topicMetrics;
     this.changeDataFactory = changeDataFactory;
@@ -284,7 +283,7 @@
   }
 
   public static void checkSubmitRule(ChangeData cd, boolean allowClosed)
-      throws ResourceConflictException, OrmException {
+      throws ResourceConflictException {
     PatchSet patchSet = cd.currentPatchSet();
     if (patchSet == null) {
       throw new ResourceConflictException("missing current patch set for change " + cd.getId());
@@ -333,7 +332,7 @@
     return cd.submitRecords(submitRuleOptions(allowClosed));
   }
 
-  private static String describeNotReady(ChangeData cd, SubmitRecord record) throws OrmException {
+  private static String describeNotReady(ChangeData cd, SubmitRecord record) {
     List<String> blockingConditions = new ArrayList<>();
     if (record.labels != null) {
       blockingConditions.add(describeLabels(cd, record.labels));
@@ -346,8 +345,7 @@
     return Joiner.on("; ").join(blockingConditions);
   }
 
-  private static String describeLabels(ChangeData cd, List<SubmitRecord.Label> labels)
-      throws OrmException {
+  private static String describeLabels(ChangeData cd, List<SubmitRecord.Label> labels) {
     List<String> labelResults = new ArrayList<>();
     for (SubmitRecord.Label lbl : labels) {
       switch (lbl.status) {
@@ -383,12 +381,10 @@
         !cs.furtherHiddenChanges(), "checkSubmitRulesAndState called for topic with hidden change");
     for (ChangeData cd : cs.changes()) {
       try {
-        Change.Status status = cd.change().getStatus();
-        if (status != Change.Status.NEW) {
-          if (!(status == Change.Status.MERGED && allowMerged)) {
+        if (!cd.change().isNew()) {
+          if (!(cd.change().isMerged() && allowMerged)) {
             commitStatus.problem(
-                cd.getId(),
-                "Change " + cd.getId() + " is " + cd.change().getStatus().toString().toLowerCase());
+                cd.getId(), "Change " + cd.getId() + " is " + ChangeUtil.status(cd.change()));
           }
         } else if (cd.change().isWorkInProgress()) {
           commitStatus.problem(cd.getId(), "Change " + cd.getId() + " is work in progress");
@@ -397,7 +393,7 @@
         }
       } catch (ResourceConflictException e) {
         commitStatus.problem(cd.getId(), e.getMessage());
-      } catch (OrmException e) {
+      } catch (StorageException e) {
         String msg = "Error checking submit rules for change";
         logger.atWarning().withCause(e).log("%s %s", msg, cd.getId());
         commitStatus.problem(cd.getId(), msg);
@@ -425,31 +421,29 @@
    * topic or via superproject subscriptions. All affected changes are integrated using the projects
    * integration strategy.
    *
-   * @param db the review database.
    * @param change the change to be merged.
    * @param caller the identity of the caller
    * @param checkSubmitRules whether the prolog submit rules should be evaluated
    * @param submitInput parameters regarding the merge
-   * @throws OrmException an error occurred reading or writing the database.
    * @throws RestApiException if an error occurred.
    * @throws PermissionBackendException if permissions can't be checked
    * @throws IOException an error occurred reading from NoteDb.
    */
   public void merge(
-      ReviewDb db,
       Change change,
       IdentifiedUser caller,
       boolean checkSubmitRules,
       SubmitInput submitInput,
       boolean dryrun)
-      throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException,
+      throws RestApiException, UpdateException, IOException, ConfigInvalidException,
           PermissionBackendException {
     this.submitInput = submitInput;
-    this.accountsToNotify = notifyUtil.resolveAccounts(submitInput.notifyDetails);
+    this.notify =
+        notifyResolver.resolve(
+            firstNonNull(submitInput.notify, NotifyHandling.ALL), submitInput.notifyDetails);
     this.dryrun = dryrun;
     this.caller = caller;
     this.ts = TimeUtil.nowTs();
-    this.db = db;
     this.submissionId = new RequestId(change.getId().toString());
 
     try (TraceContext traceContext =
@@ -459,7 +453,7 @@
       logger.atFine().log("Beginning integration of %s", change);
       try {
         ChangeSet indexBackedChangeSet =
-            mergeSuperSet.setMergeOpRepoManager(orm).completeChangeSet(db, change, caller);
+            mergeSuperSet.setMergeOpRepoManager(orm).completeChangeSet(change, caller);
         checkState(
             indexBackedChangeSet.ids().contains(change.getId()),
             "change %s missing from %s",
@@ -524,7 +518,7 @@
         }
       } catch (IOException e) {
         // Anything before the merge attempt is an error
-        throw new OrmException(e);
+        throw new StorageException(e);
       }
     }
   }
@@ -534,18 +528,16 @@
       orm.close();
     }
     orm = ormProvider.get();
-    orm.setContext(db, ts, caller);
+    orm.setContext(ts, caller, notify);
   }
 
   private ChangeSet reloadChanges(ChangeSet changeSet) {
     List<ChangeData> visible = new ArrayList<>(changeSet.changes().size());
     List<ChangeData> nonVisible = new ArrayList<>(changeSet.nonVisibleChanges().size());
-    changeSet
-        .changes()
-        .forEach(c -> visible.add(changeDataFactory.create(db, c.project(), c.getId())));
+    changeSet.changes().forEach(c -> visible.add(changeDataFactory.create(c.project(), c.getId())));
     changeSet
         .nonVisibleChanges()
-        .forEach(c -> nonVisible.add(changeDataFactory.create(db, c.project(), c.getId())));
+        .forEach(c -> nonVisible.add(changeDataFactory.create(c.project(), c.getId())));
     return new ChangeSet(visible, nonVisible);
   }
 
@@ -586,7 +578,7 @@
     ListMultimap<Branch.NameKey, ChangeData> cbb;
     try {
       cbb = cs.changesByBranch();
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       throw new IntegrationException("Error reading changes to submit", e);
     }
     Set<Branch.NameKey> branches = cbb.keySet();
@@ -605,7 +597,7 @@
       SubmoduleOp submoduleOp = subOpFactory.create(branches, orm);
       List<SubmitStrategy> strategies = getSubmitStrategies(toSubmit, submoduleOp, dryrun);
       this.allProjects = submoduleOp.getProjectsInOrder();
-      batchUpdateFactory.execute(
+      BatchUpdate.execute(
           orm.batchUpdates(allProjects),
           new SubmitStrategyListener(submitInput, strategies, commitStatus),
           dryrun);
@@ -669,7 +661,6 @@
         SubmitStrategy strategy =
             submitStrategyFactory.create(
                 submitting.submitType(),
-                db,
                 or.rw,
                 or.canMergeFlag,
                 getAlreadyAccepted(or, ob.oldTip),
@@ -680,7 +671,6 @@
                 commitStatus,
                 submissionId,
                 submitInput,
-                accountsToNotify,
                 submoduleOp,
                 dryrun);
         strategies.add(strategy);
@@ -750,7 +740,7 @@
         notes = cd.notes();
         chg = cd.change();
         st = getSubmitType(cd);
-      } catch (OrmException e) {
+      } catch (StorageException e) {
         commitStatus.logProblem(changeId, e);
         continue;
       }
@@ -782,7 +772,7 @@
       Branch.NameKey destBranch = chg.getDest();
       try {
         ps = cd.currentPatchSet();
-      } catch (OrmException e) {
+      } catch (StorageException e) {
         commitStatus.logProblem(changeId, e);
         continue;
       }
@@ -878,7 +868,7 @@
         revisions.put(e.getValue().getObjectId(), PatchSet.Id.fromRef(e.getKey()));
       }
       return revisions;
-    } catch (IOException | OrmException e) {
+    } catch (IOException | StorageException e) {
       throw new IntegrationException("Failed to validate changes", e);
     }
   }
@@ -904,14 +894,14 @@
     try {
       for (ChangeData cd : queryProvider.get().byProjectOpen(destProject)) {
         try (BatchUpdate bu =
-            batchUpdateFactory.create(db, destProject, internalUserFactory.create(), ts)) {
+            batchUpdateFactory.create(destProject, internalUserFactory.create(), ts)) {
           bu.addOp(
               cd.getId(),
               new BatchUpdateOp() {
                 @Override
-                public boolean updateChange(ChangeContext ctx) throws OrmException {
+                public boolean updateChange(ChangeContext ctx) {
                   Change change = ctx.getChange();
-                  if (!change.getStatus().isOpen()) {
+                  if (!change.isNew()) {
                     return false;
                   }
 
@@ -924,8 +914,7 @@
                           change.getLastUpdatedOn(),
                           "Project was deleted.",
                           ChangeMessagesUtil.TAG_MERGED);
-                  cmUtil.addChangeMessage(
-                      ctx.getDb(), ctx.getUpdate(change.currentPatchSetId()), msg);
+                  cmUtil.addChangeMessage(ctx.getUpdate(change.currentPatchSetId()), msg);
 
                   return true;
                 }
@@ -938,7 +927,7 @@
           }
         }
       }
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atWarning().withCause(e).log(
           "Cannot abandon changes for deleted project %s", destProject);
     }
diff --git a/java/com/google/gerrit/server/submit/MergeOpRepoManager.java b/java/com/google/gerrit/server/submit/MergeOpRepoManager.java
index 3f07ed7..764aca8 100644
--- a/java/com/google/gerrit/server/submit/MergeOpRepoManager.java
+++ b/java/com/google/gerrit/server/submit/MergeOpRepoManager.java
@@ -15,13 +15,14 @@
 package com.google.gerrit.server.submit;
 
 import static com.google.common.base.Preconditions.checkState;
+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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -105,12 +106,13 @@
     }
 
     public BatchUpdate getUpdate() {
-      checkState(db != null, "call setContext before getUpdate");
+      checkState(caller != null, "call setContext before getUpdate");
       if (update == null) {
         update =
             batchUpdateFactory
-                .create(db, getProjectName(), caller, ts)
+                .create(getProjectName(), caller, ts)
                 .setRepository(repo, rw, ins)
+                .setNotify(notify)
                 .setOnSubmitValidators(onSubmitValidatorsFactory.create());
       }
       return update;
@@ -157,9 +159,9 @@
   private final GitRepositoryManager repoManager;
   private final ProjectCache projectCache;
 
-  private ReviewDb db;
   private Timestamp ts;
   private IdentifiedUser caller;
+  private NotifyResolver.Result notify;
 
   @Inject
   MergeOpRepoManager(
@@ -175,10 +177,10 @@
     openRepos = new HashMap<>();
   }
 
-  public void setContext(ReviewDb db, Timestamp ts, IdentifiedUser caller) {
-    this.db = db;
-    this.ts = ts;
-    this.caller = caller;
+  public void setContext(Timestamp ts, IdentifiedUser caller, NotifyResolver.Result notify) {
+    this.ts = requireNonNull(ts);
+    this.caller = requireNonNull(caller);
+    this.notify = requireNonNull(notify);
   }
 
   public OpenRepo getRepo(Project.NameKey project) throws NoSuchProjectException, IOException {
@@ -203,7 +205,7 @@
       throws NoSuchProjectException, IOException {
     List<BatchUpdate> updates = new ArrayList<>(projects.size());
     for (Project.NameKey project : projects) {
-      updates.add(getRepo(project).getUpdate().setRefLogMessage("merged"));
+      updates.add(getRepo(project).getUpdate().setNotify(notify).setRefLogMessage("merged"));
     }
     return updates;
   }
diff --git a/java/com/google/gerrit/server/submit/MergeSorter.java b/java/com/google/gerrit/server/submit/MergeSorter.java
index 6dbec32..f2f6537 100644
--- a/java/com/google/gerrit/server/submit/MergeSorter.java
+++ b/java/com/google/gerrit/server/submit/MergeSorter.java
@@ -14,10 +14,11 @@
 
 package com.google.gerrit.server.submit;
 
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 import java.io.IOException;
 import java.util.Collection;
@@ -29,6 +30,7 @@
 import org.eclipse.jgit.revwalk.RevFlag;
 
 public class MergeSorter {
+  @Nullable private final CurrentUser caller;
   private final CodeReviewRevWalk rw;
   private final RevFlag canMergeFlag;
   private final Set<RevCommit> accepted;
@@ -36,11 +38,13 @@
   private final Set<CodeReviewCommit> incoming;
 
   public MergeSorter(
+      @Nullable CurrentUser caller,
       CodeReviewRevWalk rw,
       Set<RevCommit> alreadyAccepted,
       RevFlag canMergeFlag,
       Provider<InternalChangeQuery> queryProvider,
       Set<CodeReviewCommit> incoming) {
+    this.caller = caller;
     this.rw = rw;
     this.canMergeFlag = canMergeFlag;
     this.accepted = alreadyAccepted;
@@ -49,7 +53,7 @@
   }
 
   public Collection<CodeReviewCommit> sort(Collection<CodeReviewCommit> toMerge)
-      throws IOException, OrmException {
+      throws IOException {
     final Set<CodeReviewCommit> heads = new HashSet<>();
     final Set<CodeReviewCommit> sort = new HashSet<>(toMerge);
     while (!sort.isEmpty()) {
@@ -70,7 +74,8 @@
           //
           n.setStatusCode(CommitMergeStatus.MISSING_DEPENDENCY);
           n.setStatusMessage(
-              CommitMergeStatus.createMissingDependencyMessage(queryProvider, n.name(), c.name()));
+              CommitMergeStatus.createMissingDependencyMessage(
+                  caller, queryProvider, n.name(), c.name()));
           break;
         }
         contents.add(c);
diff --git a/java/com/google/gerrit/server/submit/MergeSuperSet.java b/java/com/google/gerrit/server/submit/MergeSuperSet.java
index f444fcb..d729833 100644
--- a/java/com/google/gerrit/server/submit/MergeSuperSet.java
+++ b/java/com/google/gerrit/server/submit/MergeSuperSet.java
@@ -22,7 +22,6 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.logging.TraceContext;
@@ -34,7 +33,6 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -93,8 +91,8 @@
     return this;
   }
 
-  public ChangeSet completeChangeSet(ReviewDb db, Change change, CurrentUser user)
-      throws IOException, OrmException, PermissionBackendException {
+  public ChangeSet completeChangeSet(Change change, CurrentUser user)
+      throws IOException, PermissionBackendException {
     try {
       if (orm == null) {
         orm = repoManagerProvider.get();
@@ -114,7 +112,7 @@
 
         if (projectState.statePermitsRead()) {
           try {
-            permissionBackend.user(user).change(cd).database(db).check(ChangePermission.READ);
+            permissionBackend.user(user).change(cd).check(ChangePermission.READ);
             visible = true;
           } catch (AuthException e) {
             // Do nothing.
@@ -124,10 +122,10 @@
 
       ChangeSet changeSet = new ChangeSet(cd, visible);
       if (wholeTopicEnabled(cfg)) {
-        return completeChangeSetIncludingTopics(db, changeSet, user);
+        return completeChangeSetIncludingTopics(changeSet, user);
       }
       try (TraceContext traceContext = PluginContext.newTrace(mergeSuperSetComputation)) {
-        return mergeSuperSetComputation.get().completeWithoutTopic(db, orm, changeSet, user);
+        return mergeSuperSetComputation.get().completeWithoutTopic(orm, changeSet, user);
       }
     } finally {
       if (closeOrm && orm != null) {
@@ -141,9 +139,8 @@
    * Completes {@code changeSet} with any additional changes from its topics
    *
    * <p>{@link #completeChangeSetIncludingTopics} calls this repeatedly, alternating with {@link
-   * MergeSuperSetComputation#completeWithoutTopic(ReviewDb, MergeOpRepoManager, ChangeSet,
-   * CurrentUser)}, to discover what additional changes should be submitted with a change until the
-   * set stops growing.
+   * MergeSuperSetComputation#completeWithoutTopic(MergeOpRepoManager, ChangeSet, CurrentUser)}, to
+   * discover what additional changes should be submitted with a change until the set stops growing.
    *
    * <p>{@code topicsSeen} and {@code visibleTopicsSeen} keep track of topics already explored to
    * avoid wasted work.
@@ -151,12 +148,8 @@
    * @return the resulting larger {@link ChangeSet}
    */
   private ChangeSet topicClosure(
-      ReviewDb db,
-      ChangeSet changeSet,
-      CurrentUser user,
-      Set<String> topicsSeen,
-      Set<String> visibleTopicsSeen)
-      throws OrmException, PermissionBackendException, IOException {
+      ChangeSet changeSet, CurrentUser user, Set<String> topicsSeen, Set<String> visibleTopicsSeen)
+      throws PermissionBackendException, IOException {
     List<ChangeData> visibleChanges = new ArrayList<>();
     List<ChangeData> nonVisibleChanges = new ArrayList<>();
 
@@ -167,7 +160,7 @@
         continue;
       }
       for (ChangeData topicCd : byTopicOpen(topic)) {
-        if (canRead(db, user, topicCd)) {
+        if (canRead(user, topicCd)) {
           visibleChanges.add(topicCd);
         } else {
           nonVisibleChanges.add(topicCd);
@@ -190,33 +183,32 @@
     return new ChangeSet(visibleChanges, nonVisibleChanges);
   }
 
-  private ChangeSet completeChangeSetIncludingTopics(
-      ReviewDb db, ChangeSet changeSet, CurrentUser user)
-      throws IOException, OrmException, PermissionBackendException {
+  private ChangeSet completeChangeSetIncludingTopics(ChangeSet changeSet, CurrentUser user)
+      throws IOException, PermissionBackendException {
     Set<String> topicsSeen = new HashSet<>();
     Set<String> visibleTopicsSeen = new HashSet<>();
     int oldSeen;
     int seen;
 
-    changeSet = topicClosure(db, changeSet, user, topicsSeen, visibleTopicsSeen);
+    changeSet = topicClosure(changeSet, user, topicsSeen, visibleTopicsSeen);
     seen = topicsSeen.size() + visibleTopicsSeen.size();
 
     do {
       oldSeen = seen;
       try (TraceContext traceContext = PluginContext.newTrace(mergeSuperSetComputation)) {
-        changeSet = mergeSuperSetComputation.get().completeWithoutTopic(db, orm, changeSet, user);
+        changeSet = mergeSuperSetComputation.get().completeWithoutTopic(orm, changeSet, user);
       }
-      changeSet = topicClosure(db, changeSet, user, topicsSeen, visibleTopicsSeen);
+      changeSet = topicClosure(changeSet, user, topicsSeen, visibleTopicsSeen);
       seen = topicsSeen.size() + visibleTopicsSeen.size();
     } while (seen != oldSeen);
     return changeSet;
   }
 
-  private List<ChangeData> byTopicOpen(String topic) throws OrmException {
+  private List<ChangeData> byTopicOpen(String topic) {
     return queryProvider.get().byTopicOpen(topic);
   }
 
-  private boolean canRead(ReviewDb db, CurrentUser user, ChangeData cd)
+  private boolean canRead(CurrentUser user, ChangeData cd)
       throws PermissionBackendException, IOException {
     ProjectState projectState = projectCache.checkedGet(cd.project());
     if (projectState == null || !projectState.statePermitsRead()) {
@@ -224,7 +216,7 @@
     }
 
     try {
-      permissionBackend.user(user).change(cd).database(db).check(ChangePermission.READ);
+      permissionBackend.user(user).change(cd).check(ChangePermission.READ);
       return true;
     } catch (AuthException e) {
       return false;
diff --git a/java/com/google/gerrit/server/submit/MergeSuperSetComputation.java b/java/com/google/gerrit/server/submit/MergeSuperSetComputation.java
index dd9ad9b..99239e3 100644
--- a/java/com/google/gerrit/server/submit/MergeSuperSetComputation.java
+++ b/java/com/google/gerrit/server/submit/MergeSuperSetComputation.java
@@ -15,10 +15,8 @@
 package com.google.gerrit.server.submit;
 
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import java.io.IOException;
 
 /**
@@ -41,13 +39,11 @@
    * <p>This method is invoked iteratively while new changes to be submitted together are discovered
    * by expanding the topics of the changes. This method must not do any topic expansion on its own.
    *
-   * @param db {@link ReviewDb} instance
    * @param orm {@link MergeOpRepoManager} that should be used to access repositories
    * @param changeSet A set of changes for which it is known that they should be submitted together
    * @param user The user for which the visibility checks should be performed
    * @return the completed set of changes that should be submitted together
    */
-  ChangeSet completeWithoutTopic(
-      ReviewDb db, MergeOpRepoManager orm, ChangeSet changeSet, CurrentUser user)
-      throws OrmException, IOException, PermissionBackendException;
+  ChangeSet completeWithoutTopic(MergeOpRepoManager orm, ChangeSet changeSet, CurrentUser user)
+      throws IOException, PermissionBackendException;
 }
diff --git a/java/com/google/gerrit/server/submit/RebaseSorter.java b/java/com/google/gerrit/server/submit/RebaseSorter.java
index 32f3558..829ee9c 100644
--- a/java/com/google/gerrit/server/submit/RebaseSorter.java
+++ b/java/com/google/gerrit/server/submit/RebaseSorter.java
@@ -15,13 +15,13 @@
 package com.google.gerrit.server.submit;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change.Status;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -37,6 +37,7 @@
 public class RebaseSorter {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
+  private final CurrentUser caller;
   private final CodeReviewRevWalk rw;
   private final RevFlag canMergeFlag;
   private final RevCommit initialTip;
@@ -45,12 +46,14 @@
   private final Set<CodeReviewCommit> incoming;
 
   public RebaseSorter(
+      CurrentUser caller,
       CodeReviewRevWalk rw,
       RevCommit initialTip,
       Set<RevCommit> alreadyAccepted,
       RevFlag canMergeFlag,
       Provider<InternalChangeQuery> queryProvider,
       Set<CodeReviewCommit> incoming) {
+    this.caller = caller;
     this.rw = rw;
     this.canMergeFlag = canMergeFlag;
     this.initialTip = initialTip;
@@ -59,8 +62,7 @@
     this.incoming = incoming;
   }
 
-  public List<CodeReviewCommit> sort(Collection<CodeReviewCommit> toSort)
-      throws IOException, OrmException {
+  public List<CodeReviewCommit> sort(Collection<CodeReviewCommit> toSort) throws IOException {
     final List<CodeReviewCommit> sorted = new ArrayList<>();
     final Set<CodeReviewCommit> sort = new HashSet<>(toSort);
     while (!sort.isEmpty()) {
@@ -85,7 +87,7 @@
             n.setStatusCode(CommitMergeStatus.MISSING_DEPENDENCY);
             n.setStatusMessage(
                 CommitMergeStatus.createMissingDependencyMessage(
-                    queryProvider, n.name(), c.name()));
+                    caller, queryProvider, n.name(), c.name()));
           }
           // Stop RevWalk because c is either a merged commit or a missing
           // dependency. Not need to walk further.
@@ -122,15 +124,14 @@
       // check if the commit associated change is merged in the same branch
       List<ChangeData> changes = queryProvider.get().byCommit(commit);
       for (ChangeData change : changes) {
-        if (change.change().getStatus() == Status.MERGED
-            && change.change().getDest().equals(dest)) {
+        if (change.change().isMerged() && change.change().getDest().equals(dest)) {
           logger.atFine().log(
               "Dependency %s associated with merged change %s.", commit.getName(), change.getId());
           return true;
         }
       }
       return false;
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       throw new IOException(e);
     }
   }
diff --git a/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java b/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
index 1ec7ca8..b2ad4e2 100644
--- a/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
+++ b/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
@@ -19,6 +19,7 @@
 import static com.google.gerrit.server.submit.CommitMergeStatus.SKIPPED_IDENTICAL_TREE;
 
 import com.google.common.collect.ImmutableList;
+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;
@@ -34,7 +35,6 @@
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
 import com.google.gerrit.server.update.RepoContext;
-import com.google.gwtorm.server.OrmException;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -59,7 +59,7 @@
     List<CodeReviewCommit> sorted;
     try {
       sorted = args.rebaseSorter.sort(toMerge);
-    } catch (IOException | OrmException e) {
+    } catch (IOException | StorageException e) {
       throw new IntegrationException("Commit sorting failed", e);
     }
     List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size());
@@ -119,7 +119,7 @@
     @Override
     public void updateRepoImpl(RepoContext ctx)
         throws IntegrationException, InvalidChangeOperationException, RestApiException, IOException,
-            OrmException, PermissionBackendException {
+            PermissionBackendException {
       if (args.mergeUtil.canFastForward(
           args.mergeSorter, args.mergeTip.getCurrentTip(), args.rw, toMerge)) {
         if (!rebaseAlways) {
@@ -137,8 +137,8 @@
         // RebaseAlways means we modify commit message.
         args.rw.parseBody(toMerge);
         newPatchSetId =
-            ChangeUtil.nextPatchSetIdFromChangeRefsMap(
-                ctx.getRepoView().getRefs(getId().toRefPrefix()),
+            ChangeUtil.nextPatchSetIdFromChangeRefs(
+                ctx.getRepoView().getRefs(getId().toRefPrefix()).keySet(),
                 toMerge.change().currentPatchSetId());
         RevCommit mergeTip = args.mergeTip.getCurrentTip();
         args.rw.parseBody(mergeTip);
@@ -171,14 +171,13 @@
         ctx.addRefUpdate(ObjectId.zeroId(), newCommit, newPatchSetId.toRefName());
       } else {
         // Stale read of patch set is ok; see comments in RebaseChangeOp.
-        PatchSet origPs = args.psUtil.get(ctx.getDb(), toMerge.getNotes(), toMerge.getPatchsetId());
+        PatchSet origPs = args.psUtil.get(toMerge.getNotes(), toMerge.getPatchsetId());
         rebaseOp =
             args.rebaseFactory
                 .create(toMerge.notes(), origPs, args.mergeTip.getCurrentTip())
                 .setFireRevisionCreated(false)
                 // Bypass approval copier since SubmitStrategyOp copy all approvals
                 // later anyway.
-                .setCopyApprovals(false)
                 .setValidate(false)
                 .setCheckAddPatchSetPermission(false)
                 // RebaseAlways should set always modify commit message like
@@ -187,6 +186,7 @@
                 // Do not post message after inserting new patchset because there
                 // will be one about change being merged already.
                 .setPostMessage(false)
+                .setSendEmail(false)
                 .setMatchAuthorToCommitterDate(
                     args.project.is(BooleanProjectConfig.MATCH_AUTHOR_TO_COMMITTER_DATE));
         try {
@@ -215,7 +215,7 @@
 
     @Override
     public PatchSet updateChangeImpl(ChangeContext ctx)
-        throws NoSuchChangeException, ResourceConflictException, OrmException, IOException {
+        throws NoSuchChangeException, ResourceConflictException, IOException {
       if (newCommit == null) {
         checkState(!rebaseAlways, "RebaseAlways must never fast forward");
         // otherwise, took the fast-forward option, nothing to do.
@@ -228,15 +228,14 @@
         newPs = rebaseOp.getPatchSet();
       } else {
         // CherryPick
-        PatchSet prevPs = args.psUtil.current(ctx.getDb(), ctx.getNotes());
+        PatchSet prevPs = args.psUtil.current(ctx.getNotes());
         newPs =
             args.psUtil.insert(
-                ctx.getDb(),
                 ctx.getRevWalk(),
                 ctx.getUpdate(newPatchSetId),
                 newPatchSetId,
                 newCommit,
-                prevPs != null ? prevPs.getGroups() : ImmutableList.<String>of(),
+                prevPs != null ? prevPs.getGroups() : ImmutableList.of(),
                 null,
                 null);
       }
@@ -248,7 +247,7 @@
     }
 
     @Override
-    public void postUpdateImpl(Context ctx) throws OrmException {
+    public void postUpdateImpl(Context ctx) {
       if (rebaseOp != null) {
         rebaseOp.postUpdate(ctx);
       }
diff --git a/java/com/google/gerrit/server/submit/SubmitDryRun.java b/java/com/google/gerrit/server/submit/SubmitDryRun.java
index e273652..391d956 100644
--- a/java/com/google/gerrit/server/submit/SubmitDryRun.java
+++ b/java/com/google/gerrit/server/submit/SubmitDryRun.java
@@ -19,8 +19,10 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Streams;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 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;
 import com.google.gerrit.server.git.MergeUtil;
@@ -106,6 +108,7 @@
   }
 
   public boolean run(
+      @Nullable CurrentUser caller,
       SubmitType submitType,
       Repository repo,
       CodeReviewRevWalk rw,
@@ -124,7 +127,12 @@
             rw,
             mergeUtilFactory.create(getProject(destBranch)),
             new MergeSorter(
-                rw, alreadyAccepted, canMerge, queryProvider, ImmutableSet.of(toMergeCommit)));
+                caller,
+                rw,
+                alreadyAccepted,
+                canMerge,
+                queryProvider,
+                ImmutableSet.of(toMergeCommit)));
 
     switch (submitType) {
       case CHERRY_PICK:
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategy.java b/java/com/google/gerrit/server/submit/SubmitStrategy.java
index 4cefd7d..dc221f8 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategy.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategy.java
@@ -16,16 +16,12 @@
 
 import static java.util.Objects.requireNonNull;
 
-import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Sets;
-import com.google.gerrit.extensions.api.changes.RecipientType;
 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.Account;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.GerritPersonIdent;
@@ -34,6 +30,7 @@
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.change.LabelNormalizer;
 import com.google.gerrit.server.change.RebaseChangeOp;
+import com.google.gerrit.server.change.SetPrivateOp;
 import com.google.gerrit.server.change.TestSubmitInput;
 import com.google.gerrit.server.extensions.events.ChangeMerged;
 import com.google.gerrit.server.git.CodeReviewCommit;
@@ -46,6 +43,7 @@
 import com.google.gerrit.server.logging.RequestId;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 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.query.change.InternalChangeQuery;
 import com.google.gerrit.server.submit.MergeOp.CommitStatus;
@@ -90,12 +88,10 @@
           IdentifiedUser caller,
           MergeTip mergeTip,
           RevFlag canMergeFlag,
-          ReviewDb db,
           Set<RevCommit> alreadyAccepted,
           Set<CodeReviewCommit> incoming,
           RequestId submissionId,
           SubmitInput submitInput,
-          ListMultimap<RecipientType, Account.Id> accountsToNotify,
           SubmoduleOp submoduleOp,
           boolean dryrun);
     }
@@ -115,6 +111,8 @@
     final OnSubmitValidators.Factory onSubmitValidatorsFactory;
     final TagCache tagCache;
     final Provider<InternalChangeQuery> queryProvider;
+    final ProjectConfig.Factory projectConfigFactory;
+    final SetPrivateOp.Factory setPrivateOpFactory;
 
     final Branch.NameKey destBranch;
     final CodeReviewRevWalk rw;
@@ -122,12 +120,10 @@
     final IdentifiedUser caller;
     final MergeTip mergeTip;
     final RevFlag canMergeFlag;
-    final ReviewDb db;
     final Set<RevCommit> alreadyAccepted;
     final RequestId submissionId;
     final SubmitType submitType;
     final SubmitInput submitInput;
-    final ListMultimap<RecipientType, Account.Id> accountsToNotify;
     final SubmoduleOp submoduleOp;
 
     final ProjectState project;
@@ -154,19 +150,19 @@
         OnSubmitValidators.Factory onSubmitValidatorsFactory,
         TagCache tagCache,
         Provider<InternalChangeQuery> queryProvider,
+        ProjectConfig.Factory projectConfigFactory,
+        SetPrivateOp.Factory setPrivateOpFactory,
         @Assisted Branch.NameKey destBranch,
         @Assisted CommitStatus commitStatus,
         @Assisted CodeReviewRevWalk rw,
         @Assisted IdentifiedUser caller,
         @Assisted MergeTip mergeTip,
         @Assisted RevFlag canMergeFlag,
-        @Assisted ReviewDb db,
         @Assisted Set<RevCommit> alreadyAccepted,
         @Assisted Set<CodeReviewCommit> incoming,
         @Assisted RequestId submissionId,
         @Assisted SubmitType submitType,
         @Assisted SubmitInput submitInput,
-        @Assisted ListMultimap<RecipientType, Account.Id> accountsToNotify,
         @Assisted SubmoduleOp submoduleOp,
         @Assisted boolean dryrun) {
       this.accountCache = accountCache;
@@ -176,12 +172,14 @@
       this.repoManager = repoManager;
       this.cmUtil = cmUtil;
       this.labelNormalizer = labelNormalizer;
+      this.projectConfigFactory = projectConfigFactory;
       this.patchSetInfoFactory = patchSetInfoFactory;
       this.psUtil = psUtil;
       this.projectCache = projectCache;
       this.rebaseFactory = rebaseFactory;
       this.tagCache = tagCache;
       this.queryProvider = queryProvider;
+      this.setPrivateOpFactory = setPrivateOpFactory;
 
       this.serverIdent = serverIdent;
       this.destBranch = destBranch;
@@ -190,12 +188,10 @@
       this.caller = caller;
       this.mergeTip = mergeTip;
       this.canMergeFlag = canMergeFlag;
-      this.db = db;
       this.alreadyAccepted = alreadyAccepted;
       this.submissionId = submissionId;
       this.submitType = submitType;
       this.submitInput = submitInput;
-      this.accountsToNotify = accountsToNotify;
       this.submoduleOp = submoduleOp;
       this.dryrun = dryrun;
 
@@ -204,10 +200,16 @@
               projectCache.get(destBranch.getParentKey()),
               () -> String.format("project not found: %s", destBranch.getParentKey()));
       this.mergeSorter =
-          new MergeSorter(rw, alreadyAccepted, canMergeFlag, queryProvider, incoming);
+          new MergeSorter(caller, rw, alreadyAccepted, canMergeFlag, queryProvider, incoming);
       this.rebaseSorter =
           new RebaseSorter(
-              rw, mergeTip.getInitialTip(), alreadyAccepted, canMergeFlag, queryProvider, incoming);
+              caller,
+              rw,
+              mergeTip.getInitialTip(),
+              alreadyAccepted,
+              canMergeFlag,
+              queryProvider,
+              incoming);
       this.mergeUtil = mergeUtilFactory.create(project);
       this.onSubmitValidatorsFactory = onSubmitValidatorsFactory;
     }
@@ -246,12 +248,14 @@
     Collections.reverse(difference);
     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));
       maybeAddTestHelperOp(bu, id);
     }
 
     // Then ops for explicitly merged changes
     for (SubmitStrategyOp op : ops) {
+      bu.addOp(op.getId(), args.setPrivateOpFactory.create(false, null));
       bu.addOp(op.getId(), op);
       maybeAddTestHelperOp(bu, op.getId());
     }
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategyFactory.java b/java/com/google/gerrit/server/submit/SubmitStrategyFactory.java
index 7f70f37..30326f7 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategyFactory.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategyFactory.java
@@ -14,14 +14,10 @@
 
 package com.google.gerrit.server.submit;
 
-import com.google.common.collect.ListMultimap;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.client.SubmitType;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
@@ -48,7 +44,6 @@
 
   public SubmitStrategy create(
       SubmitType submitType,
-      ReviewDb db,
       CodeReviewRevWalk rw,
       RevFlag canMergeFlag,
       Set<RevCommit> alreadyAccepted,
@@ -59,7 +54,6 @@
       CommitStatus commitStatus,
       RequestId submissionId,
       SubmitInput submitInput,
-      ListMultimap<RecipientType, Account.Id> accountsToNotify,
       SubmoduleOp submoduleOp,
       boolean dryrun)
       throws IntegrationException {
@@ -72,12 +66,10 @@
             caller,
             mergeTip,
             canMergeFlag,
-            db,
             alreadyAccepted,
             incoming,
             submissionId,
             submitInput,
-            accountsToNotify,
             submoduleOp,
             dryrun);
     switch (submitType) {
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategyListener.java b/java/com/google/gerrit/server/submit/SubmitStrategyListener.java
index d1f847b..782cd7b 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategyListener.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategyListener.java
@@ -99,7 +99,7 @@
           args.rw,
           args.canMergeFlag,
           args.mergeTip.getCurrentTip(),
-          initialTip == null ? ImmutableSet.<RevCommit>of() : ImmutableSet.of(initialTip));
+          initialTip == null ? ImmutableSet.of() : ImmutableSet.of(initialTip));
     }
   }
 
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
index 303c956..14522a2 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
@@ -17,12 +17,13 @@
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
+import static java.util.Comparator.comparing;
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.base.Function;
-import com.google.common.collect.Iterables;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.SubmitRecord;
+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;
@@ -32,8 +33,6 @@
 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.server.ReviewDb;
-import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.IdentifiedUser;
@@ -50,7 +49,6 @@
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
 import com.google.gerrit.server.update.RepoContext;
-import com.google.gwtorm.server.OrmException;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -145,7 +143,7 @@
     if (RefNames.REFS_CONFIG.equals(refName)) {
       logger.atFine().log("Loading new configuration from %s", RefNames.REFS_CONFIG);
       try {
-        ProjectConfig cfg = new ProjectConfig(getProject());
+        ProjectConfig cfg = args.projectConfigFactory.create(getProject());
         cfg.load(ctx.getRevWalk(), commit);
       } catch (Exception e) {
         throw new IntegrationException(
@@ -183,8 +181,7 @@
         continue; // Bogus ref, can't be merged into tip so we don't care.
       }
     }
-    commits.sort(
-        ReviewDbUtil.intKeyOrdering().reverse().onResultOf(CodeReviewCommit::getPatchsetId));
+    commits.sort(comparing((CodeReviewCommit c) -> c.getPatchsetId().get()).reversed());
     CodeReviewCommit result = MergeUtil.findAnyMergedInto(rw, commits, tip);
     if (result == null) {
       return null;
@@ -218,7 +215,7 @@
         "%s#updateChange for change %s", getClass().getSimpleName(), toMerge.change().getId());
     toMerge.setNotes(ctx.getNotes()); // Update change and notes from ctx.
 
-    if (ctx.getChange().getStatus() == Change.Status.MERGED) {
+    if (ctx.getChange().isMerged()) {
       // Either another thread won a race, or we are retrying a whole topic submission after one
       // repo failed with lock failure.
       if (alreadyMergedCommit == null) {
@@ -251,7 +248,7 @@
         // during the submit strategy.
         mergedPatchSet =
             requireNonNull(
-                args.psUtil.get(ctx.getDb(), ctx.getNotes(), oldPsId),
+                args.psUtil.get(ctx.getNotes(), oldPsId),
                 () -> String.format("missing old patch set %s", oldPsId));
       } else {
         PatchSet.Id n = newPatchSet.getId();
@@ -286,7 +283,7 @@
             : alreadyMergedCommit;
     try {
       setMerged(ctx, message(ctx, commit, s));
-    } catch (OrmException err) {
+    } catch (StorageException err) {
       String msg = "Error updating change status for " + id;
       logger.atSevere().withCause(err).log(msg);
       args.commitStatus.logProblem(id, msg);
@@ -297,16 +294,15 @@
     return true;
   }
 
-  private PatchSet getOrCreateAlreadyMergedPatchSet(ChangeContext ctx)
-      throws IOException, OrmException {
+  private PatchSet getOrCreateAlreadyMergedPatchSet(ChangeContext ctx) throws IOException {
     PatchSet.Id psId = alreadyMergedCommit.getPatchsetId();
     logger.atFine().log("Fixing up already-merged patch set %s", psId);
-    PatchSet prevPs = args.psUtil.current(ctx.getDb(), ctx.getNotes());
+    PatchSet prevPs = args.psUtil.current(ctx.getNotes());
     ctx.getRevWalk().parseBody(alreadyMergedCommit);
     ctx.getChange()
         .setCurrentPatchSet(
             psId, alreadyMergedCommit.getShortMessage(), ctx.getChange().getOriginalSubject());
-    PatchSet existing = args.psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
+    PatchSet existing = args.psUtil.get(ctx.getNotes(), psId);
     if (existing != null) {
       logger.atFine().log("Patch set row exists, only updating change");
       return existing;
@@ -317,18 +313,10 @@
     List<String> groups =
         prevPs != null ? prevPs.getGroups() : GroupCollector.getDefaultGroups(alreadyMergedCommit);
     return args.psUtil.insert(
-        ctx.getDb(),
-        ctx.getRevWalk(),
-        ctx.getUpdate(psId),
-        psId,
-        alreadyMergedCommit,
-        groups,
-        null,
-        null);
+        ctx.getRevWalk(), ctx.getUpdate(psId), psId, alreadyMergedCommit, groups, null, null);
   }
 
-  private void setApproval(ChangeContext ctx, IdentifiedUser user)
-      throws OrmException, IOException {
+  private void setApproval(ChangeContext ctx, IdentifiedUser user) throws IOException {
     Change.Id id = ctx.getChange().getId();
     List<SubmitRecord> records = args.commitStatus.getSubmitRecords(id);
     PatchSet.Id oldPsId = toMerge.getPatchsetId();
@@ -344,18 +332,18 @@
     // If the submit strategy created a new revision (rebase, cherry-pick), copy
     // approvals as well.
     if (!newPsId.equals(oldPsId)) {
-      saveApprovals(normalized, ctx, newPsUpdate, true);
+      saveApprovals(normalized, newPsUpdate, true);
       submitter = convertPatchSet(newPsId).apply(submitter);
     }
   }
 
   private LabelNormalizer.Result approve(ChangeContext ctx, ChangeUpdate update)
-      throws OrmException, IOException {
+      throws IOException {
     PatchSet.Id psId = update.getPatchSetId();
     Map<PatchSetApproval.Key, PatchSetApproval> byKey = new HashMap<>();
     for (PatchSetApproval psa :
         args.approvalsUtil.byPatchSet(
-            ctx.getDb(), ctx.getNotes(), psId, ctx.getRevWalk(), ctx.getRepoView().getConfig())) {
+            ctx.getNotes(), psId, ctx.getRevWalk(), ctx.getRepoView().getConfig())) {
       byKey.put(psa.getKey(), psa);
     }
 
@@ -371,19 +359,12 @@
     LabelNormalizer.Result normalized =
         args.labelNormalizer.normalize(ctx.getNotes(), byKey.values());
     update.putApproval(submitter.getLabel(), submitter.getValue());
-    saveApprovals(normalized, ctx, update, false);
+    saveApprovals(normalized, update, false);
     return normalized;
   }
 
   private void saveApprovals(
-      LabelNormalizer.Result normalized,
-      ChangeContext ctx,
-      ChangeUpdate update,
-      boolean includeUnchanged)
-      throws OrmException {
-    PatchSet.Id psId = update.getPatchSetId();
-    ctx.getDb().patchSetApprovals().upsert(convertPatchSet(normalized.getNormalized(), psId));
-    ctx.getDb().patchSetApprovals().upsert(zero(convertPatchSet(normalized.deleted(), psId)));
+      LabelNormalizer.Result normalized, ChangeUpdate update, boolean includeUnchanged) {
     for (PatchSetApproval psa : normalized.updated()) {
       update.putApprovalFor(psa.getAccountId(), psa.getLabel(), psa.getValue());
     }
@@ -411,21 +392,6 @@
     };
   }
 
-  private static Iterable<PatchSetApproval> convertPatchSet(
-      Iterable<PatchSetApproval> approvals, PatchSet.Id psId) {
-    return Iterables.transform(approvals, convertPatchSet(psId));
-  }
-
-  private static Iterable<PatchSetApproval> zero(Iterable<PatchSetApproval> approvals) {
-    return Iterables.transform(
-        approvals,
-        a -> {
-          PatchSetApproval copy = new PatchSetApproval(a.getPatchSetId(), a);
-          copy.setValue((short) 0);
-          return copy;
-        });
-  }
-
   private String getByAccountName() {
     requireNonNull(submitter, "getByAccountName called before submitter populated");
     Optional<Account> account =
@@ -436,8 +402,7 @@
     return "";
   }
 
-  private ChangeMessage message(ChangeContext ctx, CodeReviewCommit commit, CommitMergeStatus s)
-      throws OrmException {
+  private ChangeMessage message(ChangeContext ctx, CodeReviewCommit commit, CommitMergeStatus s) {
     requireNonNull(s, "CommitMergeStatus may not be null");
     String txt = s.getDescription();
     if (s == CommitMergeStatus.CLEAN_MERGE) {
@@ -483,9 +448,8 @@
         psId, ctx.getUser(), ctx.getWhen(), body, ChangeMessagesUtil.TAG_MERGED);
   }
 
-  private void setMerged(ChangeContext ctx, ChangeMessage msg) throws OrmException {
+  private void setMerged(ChangeContext ctx, ChangeMessage msg) {
     Change c = ctx.getChange();
-    ReviewDb db = ctx.getDb();
     logger.atFine().log("Setting change %s merged", c.getId());
     c.setStatus(Change.Status.MERGED);
     c.setSubmissionId(args.submissionId.toStringForStorage());
@@ -494,7 +458,7 @@
     // which is not the user from the update context. addMergedMessage was able
     // to do this in the past.
     if (msg != null) {
-      args.cmUtil.addChangeMessage(db, ctx.getUpdate(msg.getPatchSetId()), msg);
+      args.cmUtil.addChangeMessage(ctx.getUpdate(msg.getPatchSetId()), msg);
     }
   }
 
@@ -534,12 +498,7 @@
     // have failed fast in one of the other steps.
     try {
       args.mergedSenderFactory
-          .create(
-              ctx.getProject(),
-              getId(),
-              submitter.getAccountId(),
-              args.submitInput.notify,
-              args.accountsToNotify)
+          .create(ctx.getProject(), getId(), submitter.getAccountId(), ctx.getNotify(getId()))
           .sendAsync();
     } catch (Exception e) {
       logger.atSevere().withCause(e).log("Cannot email merged notification for %s", getId());
diff --git a/java/com/google/gerrit/server/submit/SubmoduleOp.java b/java/com/google/gerrit/server/submit/SubmoduleOp.java
index d3a578f..9b418db 100644
--- a/java/com/google/gerrit/server/submit/SubmoduleOp.java
+++ b/java/com/google/gerrit/server/submit/SubmoduleOp.java
@@ -21,6 +21,7 @@
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.SetMultimap;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.common.data.SubscribeSection;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -28,7 +29,6 @@
 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.UsedAt;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.VerboseSuperprojectUpdate;
 import com.google.gerrit.server.git.CodeReviewCommit;
@@ -101,39 +101,29 @@
     private final Provider<PersonIdent> serverIdent;
     private final Config cfg;
     private final ProjectCache projectCache;
-    private final BatchUpdate.Factory batchUpdateFactory;
 
     @Inject
     Factory(
         GitModules.Factory gitmodulesFactory,
         @GerritPersonIdent Provider<PersonIdent> serverIdent,
         @GerritServerConfig Config cfg,
-        ProjectCache projectCache,
-        BatchUpdate.Factory batchUpdateFactory) {
+        ProjectCache projectCache) {
       this.gitmodulesFactory = gitmodulesFactory;
       this.serverIdent = serverIdent;
       this.cfg = cfg;
       this.projectCache = projectCache;
-      this.batchUpdateFactory = batchUpdateFactory;
     }
 
     public SubmoduleOp create(Set<Branch.NameKey> updatedBranches, MergeOpRepoManager orm)
         throws SubmoduleException {
       return new SubmoduleOp(
-          gitmodulesFactory,
-          serverIdent.get(),
-          cfg,
-          projectCache,
-          batchUpdateFactory,
-          updatedBranches,
-          orm);
+          gitmodulesFactory, serverIdent.get(), cfg, projectCache, updatedBranches, orm);
     }
   }
 
   private final GitModules.Factory gitmodulesFactory;
   private final PersonIdent myIdent;
   private final ProjectCache projectCache;
-  private final BatchUpdate.Factory batchUpdateFactory;
   private final VerboseSuperprojectUpdate verboseSuperProject;
   private final boolean enableSuperProjectSubscriptions;
   private final long maxCombinedCommitMessageSize;
@@ -173,14 +163,12 @@
       PersonIdent myIdent,
       Config cfg,
       ProjectCache projectCache,
-      BatchUpdate.Factory batchUpdateFactory,
       Set<Branch.NameKey> updatedBranches,
       MergeOpRepoManager orm)
       throws SubmoduleException {
     this.gitmodulesFactory = gitmodulesFactory;
     this.myIdent = myIdent;
     this.projectCache = projectCache;
-    this.batchUpdateFactory = batchUpdateFactory;
     this.verboseSuperProject =
         cfg.getEnum("submodule", null, "verboseSuperprojectUpdate", VerboseSuperprojectUpdate.TRUE);
     this.enableSuperProjectSubscriptions =
@@ -420,7 +408,7 @@
           }
         }
       }
-      batchUpdateFactory.execute(orm.batchUpdates(superProjects), BatchUpdateListener.NONE, false);
+      BatchUpdate.execute(orm.batchUpdates(superProjects), BatchUpdateListener.NONE, false);
     } catch (RestApiException | UpdateException | IOException | NoSuchProjectException e) {
       throw new SubmoduleException("Cannot update gitlinks", e);
     }
diff --git a/java/com/google/gerrit/server/update/BatchUpdate.java b/java/com/google/gerrit/server/update/BatchUpdate.java
index b3472d2..0ca9f5e 100644
--- a/java/com/google/gerrit/server/update/BatchUpdate.java
+++ b/java/com/google/gerrit/server/update/BatchUpdate.java
@@ -17,7 +17,10 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.collect.ImmutableMultiset.toImmutableMultiset;
+import static com.google.common.flogger.LazyArgs.lazy;
+import static java.util.Comparator.comparing;
 import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.toSet;
 
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
@@ -26,27 +29,36 @@
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.Multiset;
 import com.google.common.flogger.FluentLogger;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.config.FactoryModule;
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.change.NotifyResolver;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.validators.OnSubmitValidators;
+import com.google.gerrit.server.index.change.ChangeIndexer;
 import com.google.gerrit.server.logging.RequestId;
-import com.google.gerrit.server.notedb.NotesMigration;
+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.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.NoSuchRefException;
 import com.google.inject.Inject;
 import com.google.inject.Module;
-import com.google.inject.Singleton;
+import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.ArrayList;
@@ -56,6 +68,7 @@
 import java.util.Map;
 import java.util.Optional;
 import java.util.TimeZone;
+import java.util.TreeMap;
 import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -65,135 +78,106 @@
 import org.eclipse.jgit.transport.ReceiveCommand;
 
 /**
- * Helper for a set of updates that should be applied for a site.
+ * Helper for a set of change updates that should be applied to the NoteDb database.
  *
  * <p>An update operation can be divided into three phases:
  *
  * <ol>
  *   <li>Git reference updates
- *   <li>Database updates
+ *   <li>Review metadata updates
  *   <li>Post-update steps
  *   <li>
  * </ol>
  *
  * A single conceptual operation, such as a REST API call or a merge operation, may make multiple
  * changes at each step, which all need to be serialized relative to each other. Moreover, for
- * consistency, <em>all</em> git ref updates must be performed before <em>any</em> database updates,
- * since database updates might refer to newly-created patch set refs. And all post-update steps,
- * such as hooks, should run only after all storage mutations have completed.
+ * consistency, the git ref updates must be visible to the review metadata updates, since for
+ * example the metadata might refer to newly-created patch set refs. In NoteDb, this is accomplished
+ * by combining these two phases into a single {@link BatchRefUpdate}.
  *
- * <p>Depending on the backend used, each step might support batching, for example in a {@code
- * BatchRefUpdate} or one or more database transactions. All operations in one phase must complete
- * successfully before proceeding to the next phase.
+ * <p>Similarly, all post-update steps, such as sending email, must run only after all storage
+ * mutations have completed.
  */
-public abstract class BatchUpdate implements AutoCloseable {
+public class BatchUpdate implements AutoCloseable {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static Module module() {
     return new FactoryModule() {
       @Override
       public void configure() {
-        factory(ReviewDbBatchUpdate.AssistedFactory.class);
-        factory(NoteDbBatchUpdate.AssistedFactory.class);
+        factory(BatchUpdate.Factory.class);
       }
     };
   }
 
-  @Singleton
-  public static class Factory {
-    private final NotesMigration migration;
-    private final ReviewDbBatchUpdate.AssistedFactory reviewDbBatchUpdateFactory;
-    private final NoteDbBatchUpdate.AssistedFactory noteDbBatchUpdateFactory;
+  // 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);
+  }
 
-    // TODO(dborowitz): Make this non-injectable to force all callers to use RetryHelper.
-    @Inject
-    Factory(
-        NotesMigration migration,
-        ReviewDbBatchUpdate.AssistedFactory reviewDbBatchUpdateFactory,
-        NoteDbBatchUpdate.AssistedFactory noteDbBatchUpdateFactory) {
-      this.migration = migration;
-      this.reviewDbBatchUpdateFactory = reviewDbBatchUpdateFactory;
-      this.noteDbBatchUpdateFactory = noteDbBatchUpdateFactory;
+  public static void execute(
+      Collection<BatchUpdate> updates, BatchUpdateListener listener, boolean dryrun)
+      throws UpdateException, RestApiException {
+    requireNonNull(listener);
+    if (updates.isEmpty()) {
+      return;
     }
 
-    public BatchUpdate create(
-        ReviewDb db, Project.NameKey project, CurrentUser user, Timestamp when) {
-      if (migration.disableChangeReviewDb()) {
-        return noteDbBatchUpdateFactory.create(db, project, user, when);
+    checkDifferentProject(updates);
+
+    try {
+      List<ListenableFuture<?>> indexFutures = new ArrayList<>();
+      List<ChangesHandle> handles = new ArrayList<>(updates.size());
+      try {
+        for (BatchUpdate u : updates) {
+          u.executeUpdateRepo();
+        }
+        listener.afterUpdateRepos();
+        for (BatchUpdate u : updates) {
+          handles.add(u.executeChangeOps(dryrun));
+        }
+        for (ChangesHandle h : handles) {
+          h.execute();
+          indexFutures.addAll(h.startIndexFutures());
+        }
+        listener.afterUpdateRefs();
+        listener.afterUpdateChanges();
+      } finally {
+        for (ChangesHandle h : handles) {
+          h.close();
+        }
       }
-      return reviewDbBatchUpdateFactory.create(db, project, user, when);
-    }
 
-    @SuppressWarnings({"rawtypes", "unchecked"})
-    public void execute(
-        Collection<BatchUpdate> updates, BatchUpdateListener listener, boolean dryRun)
-        throws UpdateException, RestApiException {
-      requireNonNull(listener);
-      checkDifferentProject(updates);
-      // It's safe to downcast all members of the input collection in this case, because the only
-      // way a caller could have gotten any BatchUpdates in the first place is to call the create
-      // method above, which always returns instances of the type we expect. Just to be safe,
-      // copy them into an ImmutableList so there is no chance the callee can pollute the input
-      // collection.
-      if (migration.disableChangeReviewDb()) {
-        ImmutableList<NoteDbBatchUpdate> noteDbUpdates =
-            (ImmutableList) ImmutableList.copyOf(updates);
-        NoteDbBatchUpdate.execute(noteDbUpdates, listener, dryRun);
-      } else {
-        ImmutableList<ReviewDbBatchUpdate> reviewDbUpdates =
-            (ImmutableList) ImmutableList.copyOf(updates);
-        ReviewDbBatchUpdate.execute(reviewDbUpdates, listener, dryRun);
+      ((ListenableFuture<?>) Futures.allAsList(indexFutures)).get();
+
+      // Fire ref update events only after all mutations are finished, since callers may assume a
+      // patch set ref being created means the change was created, or a branch advancing meaning
+      // some changes were closed.
+      updates.stream()
+          .filter(u -> u.batchRefUpdate != null)
+          .forEach(
+              u -> u.gitRefUpdated.fire(u.project, u.batchRefUpdate, u.getAccount().orElse(null)));
+
+      if (!dryrun) {
+        for (BatchUpdate u : updates) {
+          u.executePostOps();
+        }
       }
-    }
-
-    private static void checkDifferentProject(Collection<BatchUpdate> updates) {
-      Multiset<Project.NameKey> projectCounts =
-          updates.stream().map(u -> u.project).collect(toImmutableMultiset());
-      checkArgument(
-          projectCounts.entrySet().size() == updates.size(),
-          "updates must all be for different projects, got: %s",
-          projectCounts);
+    } catch (Exception e) {
+      wrapAndThrowException(e);
     }
   }
 
-  static Order getOrder(Collection<? extends BatchUpdate> updates, BatchUpdateListener listener) {
-    Order o = null;
-    for (BatchUpdate u : updates) {
-      if (o == null) {
-        o = u.order;
-      } else if (u.order != o) {
-        throw new IllegalArgumentException("cannot mix execution orders");
-      }
-    }
-    if (o != Order.REPO_BEFORE_DB) {
-      checkArgument(
-          listener == BatchUpdateListener.NONE,
-          "BatchUpdateListener not supported for order %s",
-          o);
-    }
-    return o;
-  }
-
-  static boolean getUpdateChangesInParallel(Collection<? extends BatchUpdate> updates) {
-    checkArgument(!updates.isEmpty());
-    Boolean p = null;
-    for (BatchUpdate u : updates) {
-      if (p == null) {
-        p = u.updateChangesInParallel;
-      } else if (u.updateChangesInParallel != p) {
-        throw new IllegalArgumentException("cannot mix parallel and non-parallel operations");
-      }
-    }
-    // Properly implementing this would involve hoisting the parallel loop up
-    // even further. As of this writing, the only user is ReceiveCommits,
-    // which only executes a single BatchUpdate at a time. So bail for now.
+  private static void checkDifferentProject(Collection<BatchUpdate> updates) {
+    Multiset<Project.NameKey> projectCounts =
+        updates.stream().map(u -> u.project).collect(toImmutableMultiset());
     checkArgument(
-        !p || updates.size() <= 1,
-        "cannot execute ChangeOps in parallel with more than 1 BatchUpdate");
-    return p;
+        projectCounts.entrySet().size() == updates.size(),
+        "updates must all be for different projects, got: %s",
+        projectCounts);
   }
 
-  static void wrapAndThrowException(Exception e) throws UpdateException, RestApiException {
+  private static void wrapAndThrowException(Exception e) throws UpdateException, RestApiException {
     Throwables.throwIfUnchecked(e);
 
     // Propagate REST API exceptions thrown by operations; they commonly throw exceptions like
@@ -215,39 +199,146 @@
     throw new UpdateException(e);
   }
 
-  protected GitRepositoryManager repoManager;
+  class ContextImpl implements Context {
+    @Override
+    public RepoView getRepoView() throws IOException {
+      return BatchUpdate.this.getRepoView();
+    }
 
-  protected final Project.NameKey project;
-  protected final CurrentUser user;
-  protected final Timestamp when;
-  protected final TimeZone tz;
+    @Override
+    public RevWalk getRevWalk() throws IOException {
+      return getRepoView().getRevWalk();
+    }
 
-  protected final ListMultimap<Change.Id, BatchUpdateOp> ops =
+    @Override
+    public Project.NameKey getProject() {
+      return project;
+    }
+
+    @Override
+    public Timestamp getWhen() {
+      return when;
+    }
+
+    @Override
+    public TimeZone getTimeZone() {
+      return tz;
+    }
+
+    @Override
+    public CurrentUser getUser() {
+      return user;
+    }
+
+    @Override
+    public NotifyResolver.Result getNotify(Change.Id changeId) {
+      NotifyHandling notifyHandling = perChangeNotifyHandling.get(changeId);
+      return notifyHandling != null ? notify.withHandling(notifyHandling) : notify;
+    }
+  }
+
+  private class RepoContextImpl extends ContextImpl implements RepoContext {
+    @Override
+    public ObjectInserter getInserter() throws IOException {
+      return getRepoView().getInserterWrapper();
+    }
+
+    @Override
+    public void addRefUpdate(ReceiveCommand cmd) throws IOException {
+      getRepoView().getCommands().add(cmd);
+    }
+  }
+
+  private class ChangeContextImpl extends ContextImpl implements ChangeContext {
+    private final ChangeNotes notes;
+    private final Map<PatchSet.Id, ChangeUpdate> updates;
+
+    private boolean deleted;
+
+    ChangeContextImpl(ChangeNotes notes) {
+      this.notes = requireNonNull(notes);
+      updates = new TreeMap<>(comparing(PatchSet.Id::get));
+    }
+
+    @Override
+    public ChangeUpdate getUpdate(PatchSet.Id psId) {
+      ChangeUpdate u = updates.get(psId);
+      if (u == null) {
+        u = changeUpdateFactory.create(notes, user, when);
+        if (newChanges.containsKey(notes.getChangeId())) {
+          u.setAllowWriteToNewRef(true);
+        }
+        u.setPatchSetId(psId);
+        updates.put(psId, u);
+      }
+      return u;
+    }
+
+    @Override
+    public ChangeNotes getNotes() {
+      return notes;
+    }
+
+    @Override
+    public void deleteChange() {
+      deleted = true;
+    }
+  }
+
+  /** Per-change result status from {@link #executeChangeOps}. */
+  private enum ChangeResult {
+    SKIPPED,
+    UPSERTED,
+    DELETED;
+  }
+
+  private final GitRepositoryManager repoManager;
+  private final ChangeNotes.Factory changeNotesFactory;
+  private final ChangeUpdate.Factory changeUpdateFactory;
+  private final NoteDbUpdateManager.Factory updateManagerFactory;
+  private final ChangeIndexer indexer;
+  private final GitReferenceUpdated gitRefUpdated;
+
+  private final Project.NameKey project;
+  private final CurrentUser user;
+  private final Timestamp when;
+  private final TimeZone tz;
+
+  private final ListMultimap<Change.Id, BatchUpdateOp> ops =
       MultimapBuilder.linkedHashKeys().arrayListValues().build();
-  protected final Map<Change.Id, Change> newChanges = new HashMap<>();
-  protected final List<RepoOnlyOp> repoOnlyOps = new ArrayList<>();
+  private final Map<Change.Id, Change> newChanges = new HashMap<>();
+  private final List<RepoOnlyOp> repoOnlyOps = new ArrayList<>();
+  private final Map<Change.Id, NotifyHandling> perChangeNotifyHandling = new HashMap<>();
 
-  protected RepoView repoView;
-  protected BatchRefUpdate batchRefUpdate;
-  protected Order order;
-  protected OnSubmitValidators onSubmitValidators;
-  protected PushCertificate pushCert;
-  protected String refLogMessage;
+  private RepoView repoView;
+  private BatchRefUpdate batchRefUpdate;
+  private OnSubmitValidators onSubmitValidators;
+  private PushCertificate pushCert;
+  private String refLogMessage;
+  private NotifyResolver.Result notify = NotifyResolver.Result.all();
 
-  private boolean updateChangesInParallel;
-
-  protected BatchUpdate(
+  @Inject
+  BatchUpdate(
       GitRepositoryManager repoManager,
-      PersonIdent serverIdent,
-      Project.NameKey project,
-      CurrentUser user,
-      Timestamp when) {
+      @GerritPersonIdent PersonIdent serverIdent,
+      ChangeNotes.Factory changeNotesFactory,
+      ChangeUpdate.Factory changeUpdateFactory,
+      NoteDbUpdateManager.Factory updateManagerFactory,
+      ChangeIndexer indexer,
+      GitReferenceUpdated gitRefUpdated,
+      @Assisted Project.NameKey project,
+      @Assisted CurrentUser user,
+      @Assisted Timestamp when) {
     this.repoManager = repoManager;
+    this.changeNotesFactory = changeNotesFactory;
+    this.changeUpdateFactory = changeUpdateFactory;
+    this.updateManagerFactory = updateManagerFactory;
+    this.indexer = indexer;
+    this.gitRefUpdated = gitRefUpdated;
     this.project = project;
     this.user = user;
     this.when = when;
     tz = serverIdent.getTimeZone();
-    order = Order.REPO_BEFORE_DB;
   }
 
   @Override
@@ -257,15 +348,14 @@
     }
   }
 
-  public abstract void execute(BatchUpdateListener listener)
-      throws UpdateException, RestApiException;
+  public void execute(BatchUpdateListener listener) throws UpdateException, RestApiException {
+    execute(ImmutableList.of(this), listener, false);
+  }
 
   public void execute() throws UpdateException, RestApiException {
     execute(BatchUpdateListener.NONE);
   }
 
-  protected abstract Context newContext();
-
   public BatchUpdate setRepository(Repository repo, RevWalk revWalk, ObjectInserter inserter) {
     checkState(this.repoView == null, "repo already set");
     repoView = new RepoView(repo, revWalk, inserter);
@@ -282,8 +372,29 @@
     return this;
   }
 
-  public BatchUpdate setOrder(Order order) {
-    this.order = order;
+  /**
+   * Set the default notification settings for all changes in the batch.
+   *
+   * @param notify notification settings.
+   * @return this.
+   */
+  public BatchUpdate setNotify(NotifyResolver.Result notify) {
+    this.notify = requireNonNull(notify);
+    return this;
+  }
+
+  /**
+   * Override the {@link NotifyHandling} on a per-change basis.
+   *
+   * <p>Only the handling enum can be overridden; all changes share the same value for {@link
+   * com.google.gerrit.server.change.NotifyResolver.Result#accounts()}.
+   *
+   * @param changeId change ID.
+   * @param notifyHandling notify handling.
+   * @return this.
+   */
+  public BatchUpdate setNotifyHandling(Change.Id changeId, NotifyHandling notifyHandling) {
+    this.perChangeNotifyHandling.put(changeId, requireNonNull(notifyHandling));
     return this;
   }
 
@@ -296,44 +407,23 @@
     return this;
   }
 
-  /**
-   * Execute {@link BatchUpdateOp#updateChange(ChangeContext)} in parallel for each change.
-   *
-   * <p>This improves performance of writing to multiple changes in separate ReviewDb transactions.
-   * When only NoteDb is used, updates to all changes are written in a single batch ref update, so
-   * parallelization is not used and this option is ignored.
-   */
-  public BatchUpdate updateChangesInParallel() {
-    this.updateChangesInParallel = true;
-    return this;
-  }
-
-  protected void initRepository() throws IOException {
+  private void initRepository() throws IOException {
     if (repoView == null) {
       repoView = new RepoView(repoManager, project);
     }
   }
 
-  protected RepoView getRepoView() throws IOException {
+  private RepoView getRepoView() throws IOException {
     initRepository();
     return repoView;
   }
 
-  protected CurrentUser getUser() {
-    return user;
-  }
-
-  protected Optional<AccountState> getAccount() {
+  private Optional<AccountState> getAccount() {
     return user.isIdentifiedUser()
         ? Optional.of(user.asIdentifiedUser().state())
         : Optional.empty();
   }
 
-  protected RevWalk getRevWalk() throws IOException {
-    initRepository();
-    return repoView.getRevWalk();
-  }
-
   public Map<String, ReceiveCommand> getRefUpdates() {
     return repoView != null ? repoView.getCommands().getCommands() : ImmutableMap.of();
   }
@@ -352,7 +442,7 @@
   }
 
   public BatchUpdate insertChange(InsertChangeOp op) throws IOException {
-    Context ctx = newContext();
+    Context ctx = new ContextImpl();
     Change c = op.createChange(ctx);
     checkArgument(
         !newChanges.containsKey(c.getId()), "only one op allowed to create change %s", c.getId());
@@ -361,16 +451,163 @@
     return this;
   }
 
-  protected static void logDebug(String msg, Throwable t) {
-    // Only log if there is a requestId assigned, since those are the
-    // expensive/complicated requests like MergeOp. Doing it every time would be
-    // noisy.
-    if (RequestId.isSet()) {
-      logger.atFine().withCause(t).log("%s", msg);
+  private void executeUpdateRepo() throws UpdateException, RestApiException {
+    try {
+      logDebug("Executing updateRepo on %d ops", ops.size());
+      RepoContextImpl ctx = new RepoContextImpl();
+      for (BatchUpdateOp op : ops.values()) {
+        op.updateRepo(ctx);
+      }
+
+      logDebug("Executing updateRepo on %d RepoOnlyOps", repoOnlyOps.size());
+      for (RepoOnlyOp op : repoOnlyOps) {
+        op.updateRepo(ctx);
+      }
+
+      if (onSubmitValidators != null && !getRefUpdates().isEmpty()) {
+        // Validation of refs has to take place here and not at the beginning of executeRefUpdates.
+        // Otherwise, failing validation in a second BatchUpdate object will happen *after* the
+        // first update's executeRefUpdates has finished, hence after first repo's refs have been
+        // updated, which is too late.
+        onSubmitValidators.validate(
+            project, ctx.getRevWalk().getObjectReader(), repoView.getCommands());
+      }
+    } catch (Exception e) {
+      Throwables.throwIfInstanceOf(e, RestApiException.class);
+      throw new UpdateException(e);
     }
   }
 
-  protected static void logDebug(String msg) {
+  private class ChangesHandle implements AutoCloseable {
+    private final NoteDbUpdateManager manager;
+    private final boolean dryrun;
+    private final Map<Change.Id, ChangeResult> results;
+
+    ChangesHandle(NoteDbUpdateManager manager, boolean dryrun) {
+      this.manager = manager;
+      this.dryrun = dryrun;
+      results = new HashMap<>();
+    }
+
+    @Override
+    public void close() {
+      manager.close();
+    }
+
+    void setResult(Change.Id id, ChangeResult result) {
+      ChangeResult old = results.putIfAbsent(id, result);
+      checkArgument(old == null, "result for change %s already set: %s", id, old);
+    }
+
+    void execute() throws IOException {
+      BatchUpdate.this.batchRefUpdate = manager.execute(dryrun);
+    }
+
+    List<ListenableFuture<?>> startIndexFutures() {
+      if (dryrun) {
+        return ImmutableList.of();
+      }
+      logDebug("Reindexing %d changes", results.size());
+      List<ListenableFuture<?>> indexFutures = new ArrayList<>(results.size());
+      for (Map.Entry<Change.Id, ChangeResult> e : results.entrySet()) {
+        Change.Id id = e.getKey();
+        switch (e.getValue()) {
+          case UPSERTED:
+            indexFutures.add(indexer.indexAsync(project, id));
+            break;
+          case DELETED:
+            indexFutures.add(indexer.deleteAsync(id));
+            break;
+          case SKIPPED:
+            break;
+          default:
+            throw new IllegalStateException("unexpected result: " + e.getValue());
+        }
+      }
+      return indexFutures;
+    }
+  }
+
+  private ChangesHandle executeChangeOps(boolean dryrun) throws Exception {
+    logDebug("Executing change ops");
+    initRepository();
+    Repository repo = repoView.getRepository();
+    checkState(
+        repo.getRefDatabase().performsAtomicTransactions(),
+        "cannot use NoteDb with a repository that does not support atomic batch ref updates: %s",
+        repo);
+
+    ChangesHandle handle =
+        new ChangesHandle(
+            updateManagerFactory
+                .create(project)
+                .setChangeRepo(
+                    repo, repoView.getRevWalk(), repoView.getInserter(), repoView.getCommands()),
+            dryrun);
+    if (user.isIdentifiedUser()) {
+      handle.manager.setRefLogIdent(user.asIdentifiedUser().newRefLogIdent(when, tz));
+    }
+    handle.manager.setRefLogMessage(refLogMessage);
+    handle.manager.setPushCertificate(pushCert);
+    for (Map.Entry<Change.Id, Collection<BatchUpdateOp>> e : ops.asMap().entrySet()) {
+      Change.Id id = e.getKey();
+      ChangeContextImpl ctx = newChangeContext(id);
+      boolean dirty = false;
+      logDebug(
+          "Applying %d ops for change %s: %s",
+          e.getValue().size(),
+          id,
+          lazy(() -> e.getValue().stream().map(op -> op.getClass().getName()).collect(toSet())));
+      for (BatchUpdateOp op : e.getValue()) {
+        dirty |= op.updateChange(ctx);
+      }
+      if (!dirty) {
+        logDebug("No ops reported dirty, short-circuiting");
+        handle.setResult(id, ChangeResult.SKIPPED);
+        continue;
+      }
+      for (ChangeUpdate u : ctx.updates.values()) {
+        handle.manager.add(u);
+      }
+      if (ctx.deleted) {
+        logDebug("Change %s was deleted", id);
+        handle.manager.deleteChange(id);
+        handle.setResult(id, ChangeResult.DELETED);
+      } else {
+        handle.setResult(id, ChangeResult.UPSERTED);
+      }
+    }
+    return handle;
+  }
+
+  private ChangeContextImpl newChangeContext(Change.Id id) {
+    logDebug("Opening change %s for update", id);
+    Change c = newChanges.get(id);
+    boolean isNew = c != null;
+    if (!isNew) {
+      // Pass a synthetic change into ChangeNotes.Factory, which will take care of checking for
+      // existence and populating columns from the parsed notes state.
+      // TODO(dborowitz): This dance made more sense when using Reviewdb; consider a nicer way.
+      c = ChangeNotes.Factory.newChange(project, id);
+    } else {
+      logDebug("Change %s is new", id);
+    }
+    ChangeNotes notes = changeNotesFactory.createForBatchUpdate(c, !isNew);
+    return new ChangeContextImpl(notes);
+  }
+
+  private void executePostOps() throws Exception {
+    ContextImpl ctx = new ContextImpl();
+    for (BatchUpdateOp op : ops.values()) {
+      op.postUpdate(ctx);
+    }
+
+    for (RepoOnlyOp op : repoOnlyOps) {
+      op.postUpdate(ctx);
+    }
+  }
+
+  private static void logDebug(String msg) {
     // Only log if there is a requestId assigned, since those are the
     // expensive/complicated requests like MergeOp. Doing it every time would be
     // noisy.
@@ -379,7 +616,7 @@
     }
   }
 
-  protected static void logDebug(String msg, @Nullable Object arg) {
+  private static void logDebug(String msg, @Nullable Object arg) {
     // Only log if there is a requestId assigned, since those are the
     // expensive/complicated requests like MergeOp. Doing it every time would be
     // noisy.
@@ -388,16 +625,7 @@
     }
   }
 
-  protected static void logDebug(String msg, @Nullable Object arg1, @Nullable Object arg2) {
-    // Only log if there is a requestId assigned, since those are the
-    // expensive/complicated requests like MergeOp. Doing it every time would be
-    // noisy.
-    if (RequestId.isSet()) {
-      logger.atFine().log(msg, arg1, arg2);
-    }
-  }
-
-  protected static void logDebug(
+  private static void logDebug(
       String msg, @Nullable Object arg1, @Nullable Object arg2, @Nullable Object arg3) {
     // Only log if there is a requestId assigned, since those are the
     // expensive/complicated requests like MergeOp. Doing it every time would be
diff --git a/java/com/google/gerrit/server/update/BatchUpdateListener.java b/java/com/google/gerrit/server/update/BatchUpdateListener.java
index 847a7ca..765bba1 100644
--- a/java/com/google/gerrit/server/update/BatchUpdateListener.java
+++ b/java/com/google/gerrit/server/update/BatchUpdateListener.java
@@ -19,8 +19,6 @@
  *
  * <p>When used during execution of multiple batch updates, the {@code after*} methods are called
  * after that phase has been completed for <em>all</em> updates.
- *
- * <p>Listeners are only supported for the {@link Order#REPO_BEFORE_DB} order.
  */
 public interface BatchUpdateListener {
   public static final BatchUpdateListener NONE = new BatchUpdateListener() {};
diff --git a/java/com/google/gerrit/server/update/BatchUpdateReviewDb.java b/java/com/google/gerrit/server/update/BatchUpdateReviewDb.java
deleted file mode 100644
index 7d99b44..0000000
--- a/java/com/google/gerrit/server/update/BatchUpdateReviewDb.java
+++ /dev/null
@@ -1,104 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.update;
-
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.server.ChangeAccess;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.reviewdb.server.ReviewDbUtil;
-import com.google.gerrit.reviewdb.server.ReviewDbWrapper;
-import com.google.gwtorm.server.AtomicUpdate;
-
-public class BatchUpdateReviewDb extends ReviewDbWrapper {
-  private final ChangeAccess changesWrapper;
-
-  BatchUpdateReviewDb(ReviewDb delegate) {
-    super(delegate);
-    changesWrapper = new BatchUpdateChanges(delegate.changes());
-  }
-
-  /** @return the underlying delegate. Supports BatchUpdateReviewDb too. */
-  public static ReviewDb unwrap(ReviewDb db) {
-    if (db instanceof BatchUpdateReviewDb) {
-      db = ((BatchUpdateReviewDb) db).unsafeGetDelegate();
-    }
-    return ReviewDbUtil.unwrapDb(db);
-  }
-
-  @Override
-  public ChangeAccess changes() {
-    return changesWrapper;
-  }
-
-  @Override
-  public void commit() {
-    throw new UnsupportedOperationException(
-        "do not call commit; BatchUpdate always manages transactions");
-  }
-
-  @Override
-  public void rollback() {
-    throw new UnsupportedOperationException(
-        "do not call rollback; BatchUpdate always manages transactions");
-  }
-
-  private static class BatchUpdateChanges extends ChangeAccessWrapper {
-    private BatchUpdateChanges(ChangeAccess delegate) {
-      super(delegate);
-    }
-
-    @Override
-    public void insert(Iterable<Change> instances) {
-      throw new UnsupportedOperationException(
-          "do not call insert; change is automatically inserted");
-    }
-
-    @Override
-    public void upsert(Iterable<Change> instances) {
-      throw new UnsupportedOperationException(
-          "do not call upsert; existing changes are updated automatically,"
-              + " or use InsertChangeOp for insertion");
-    }
-
-    @Override
-    public void update(Iterable<Change> instances) {
-      throw new UnsupportedOperationException(
-          "do not call update; change is updated automatically");
-    }
-
-    @Override
-    public void beginTransaction(Change.Id key) {
-      throw new UnsupportedOperationException("updateChange is always called within a transaction");
-    }
-
-    @Override
-    public void deleteKeys(Iterable<Change.Id> keys) {
-      throw new UnsupportedOperationException(
-          "do not call deleteKeys; use ChangeContext#deleteChange()");
-    }
-
-    @Override
-    public void delete(Iterable<Change> instances) {
-      throw new UnsupportedOperationException(
-          "do not call delete; use ChangeContext#deleteChange()");
-    }
-
-    @Override
-    public Change atomicUpdate(Change.Id key, AtomicUpdate<Change> update) {
-      throw new UnsupportedOperationException(
-          "do not call atomicUpdate; updateChange is always called within a transaction");
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/update/ChangeContext.java b/java/com/google/gerrit/server/update/ChangeContext.java
index 7b5ef0e..28674fc 100644
--- a/java/com/google/gerrit/server/update/ChangeContext.java
+++ b/java/com/google/gerrit/server/update/ChangeContext.java
@@ -53,16 +53,6 @@
   ChangeNotes getNotes();
 
   /**
-   * Don't bump the value of {@link Change#getLastUpdatedOn()}.
-   *
-   * <p>If called, don't bump the timestamp before storing to ReviewDb. Only has an effect in
-   * ReviewDb, and the only usage should be to match the behavior of NoteDb. Specifically, in NoteDb
-   * the timestamp is updated if and only if the change meta graph is updated, and is not updated
-   * when only drafts are modified.
-   */
-  void dontBumpLastUpdatedOn();
-
-  /**
    * Instruct {@link BatchUpdate} to delete this change.
    *
    * <p>If called, all other updates are ignored.
diff --git a/java/com/google/gerrit/server/update/Context.java b/java/com/google/gerrit/server/update/Context.java
index ffb0392..8704cf0 100644
--- a/java/com/google/gerrit/server/update/Context.java
+++ b/java/com/google/gerrit/server/update/Context.java
@@ -17,11 +17,12 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.change.NotifyResolver;
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.TimeZone;
@@ -77,16 +78,6 @@
   TimeZone getTimeZone();
 
   /**
-   * Get the ReviewDb database.
-   *
-   * <p>Callers should not manage transactions or call mutating methods on the Changes table.
-   * Mutations on other tables (including other entities in the change entity group) are fine.
-   *
-   * @return open database instance.
-   */
-  ReviewDb getDb();
-
-  /**
    * Get the user performing the update.
    *
    * <p>In the current implementation, this is always an {@link IdentifiedUser} or {@link
@@ -97,11 +88,16 @@
   CurrentUser getUser();
 
   /**
-   * Get the order in which operations are executed in this update.
+   * Get the notification settings configured by the caller.
    *
-   * @return order of operations.
+   * <p>If there are multiple changes in a batch, they may have different settings. For example, WIP
+   * changes may have reduced {@code NotifyHandling} levels, and may be in a batch with non-WIP
+   * changes.
+   *
+   * @param changeId change ID
+   * @return notification settings.
    */
-  Order getOrder();
+  NotifyResolver.Result getNotify(Change.Id changeId);
 
   /**
    * Get the identified user performing the update.
diff --git a/java/com/google/gerrit/server/update/NoteDbBatchUpdate.java b/java/com/google/gerrit/server/update/NoteDbBatchUpdate.java
deleted file mode 100644
index a0c425f..0000000
--- a/java/com/google/gerrit/server/update/NoteDbBatchUpdate.java
+++ /dev/null
@@ -1,456 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.update;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkState;
-import static com.google.common.flogger.LazyArgs.lazy;
-import static java.util.Comparator.comparing;
-import static java.util.Objects.requireNonNull;
-import static java.util.stream.Collectors.toSet;
-
-import com.google.common.base.Throwables;
-import com.google.common.collect.ImmutableList;
-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.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.index.change.ChangeIndexer;
-import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.notedb.NoteDbUpdateManager;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-import java.io.IOException;
-import java.sql.Timestamp;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.TimeZone;
-import java.util.TreeMap;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.ReceiveCommand;
-
-/**
- * {@link BatchUpdate} implementation using only NoteDb that updates code refs and meta refs in a
- * single {@link org.eclipse.jgit.lib.BatchRefUpdate}.
- *
- * <p>Used when {@code noteDb.changes.disableReviewDb=true}, at which point ReviewDb is not
- * consulted during updates.
- */
-public class NoteDbBatchUpdate extends BatchUpdate {
-  public interface AssistedFactory {
-    NoteDbBatchUpdate create(
-        ReviewDb db, Project.NameKey project, CurrentUser user, Timestamp when);
-  }
-
-  static void execute(
-      ImmutableList<NoteDbBatchUpdate> updates, BatchUpdateListener listener, boolean dryrun)
-      throws UpdateException, RestApiException {
-    if (updates.isEmpty()) {
-      return;
-    }
-
-    try {
-      @SuppressWarnings("deprecation")
-      List<com.google.common.util.concurrent.CheckedFuture<?, IOException>> indexFutures =
-          new ArrayList<>();
-      List<ChangesHandle> handles = new ArrayList<>(updates.size());
-      Order order = getOrder(updates, listener);
-      try {
-        switch (order) {
-          case REPO_BEFORE_DB:
-            for (NoteDbBatchUpdate u : updates) {
-              u.executeUpdateRepo();
-            }
-            listener.afterUpdateRepos();
-            for (NoteDbBatchUpdate u : updates) {
-              handles.add(u.executeChangeOps(dryrun));
-            }
-            for (ChangesHandle h : handles) {
-              h.execute();
-              indexFutures.addAll(h.startIndexFutures());
-            }
-            listener.afterUpdateRefs();
-            listener.afterUpdateChanges();
-            break;
-
-          case DB_BEFORE_REPO:
-            // Call updateChange for each op before updateRepo, but defer executing the
-            // NoteDbUpdateManager until after calling updateRepo. They share an inserter and
-            // BatchRefUpdate, so it will all execute as a single batch. But we have to let
-            // NoteDbUpdateManager actually execute the update, since it has to interleave it
-            // properly with All-Users updates.
-            //
-            // TODO(dborowitz): This may still result in multiple updates to All-Users, but that's
-            // currently not a big deal because multi-change batches generally aren't affecting
-            // drafts anyway.
-            for (NoteDbBatchUpdate u : updates) {
-              handles.add(u.executeChangeOps(dryrun));
-            }
-            for (NoteDbBatchUpdate u : updates) {
-              u.executeUpdateRepo();
-            }
-            for (ChangesHandle h : handles) {
-              // TODO(dborowitz): This isn't quite good enough: in theory updateRepo may want to
-              // see the results of change meta commands, but they aren't actually added to the
-              // BatchUpdate until the body of execute. To fix this, execute needs to be split up
-              // into a method that returns a BatchRefUpdate before execution. Not a big deal at the
-              // moment, because this order is only used for deleting changes, and those updateRepo
-              // implementations definitely don't need to observe the updated change meta refs.
-              h.execute();
-              indexFutures.addAll(h.startIndexFutures());
-            }
-            break;
-          default:
-            throw new IllegalStateException("invalid execution order: " + order);
-        }
-      } finally {
-        for (ChangesHandle h : handles) {
-          h.close();
-        }
-      }
-
-      ChangeIndexer.allAsList(indexFutures).get();
-
-      // Fire ref update events only after all mutations are finished, since callers may assume a
-      // patch set ref being created means the change was created, or a branch advancing meaning
-      // some changes were closed.
-      updates.stream()
-          .filter(u -> u.batchRefUpdate != null)
-          .forEach(
-              u -> u.gitRefUpdated.fire(u.project, u.batchRefUpdate, u.getAccount().orElse(null)));
-
-      if (!dryrun) {
-        for (NoteDbBatchUpdate u : updates) {
-          u.executePostOps();
-        }
-      }
-    } catch (Exception e) {
-      wrapAndThrowException(e);
-    }
-  }
-
-  class ContextImpl implements Context {
-    @Override
-    public RepoView getRepoView() throws IOException {
-      return NoteDbBatchUpdate.this.getRepoView();
-    }
-
-    @Override
-    public RevWalk getRevWalk() throws IOException {
-      return getRepoView().getRevWalk();
-    }
-
-    @Override
-    public Project.NameKey getProject() {
-      return project;
-    }
-
-    @Override
-    public Timestamp getWhen() {
-      return when;
-    }
-
-    @Override
-    public TimeZone getTimeZone() {
-      return tz;
-    }
-
-    @Override
-    public ReviewDb getDb() {
-      return db;
-    }
-
-    @Override
-    public CurrentUser getUser() {
-      return user;
-    }
-
-    @Override
-    public Order getOrder() {
-      return order;
-    }
-  }
-
-  private class RepoContextImpl extends ContextImpl implements RepoContext {
-    @Override
-    public ObjectInserter getInserter() throws IOException {
-      return getRepoView().getInserterWrapper();
-    }
-
-    @Override
-    public void addRefUpdate(ReceiveCommand cmd) throws IOException {
-      getRepoView().getCommands().add(cmd);
-    }
-  }
-
-  private class ChangeContextImpl extends ContextImpl implements ChangeContext {
-    private final ChangeNotes notes;
-    private final Map<PatchSet.Id, ChangeUpdate> updates;
-
-    private boolean deleted;
-
-    protected ChangeContextImpl(ChangeNotes notes) {
-      this.notes = requireNonNull(notes);
-      updates = new TreeMap<>(comparing(PatchSet.Id::get));
-    }
-
-    @Override
-    public ChangeUpdate getUpdate(PatchSet.Id psId) {
-      ChangeUpdate u = updates.get(psId);
-      if (u == null) {
-        u = changeUpdateFactory.create(notes, user, when);
-        if (newChanges.containsKey(notes.getChangeId())) {
-          u.setAllowWriteToNewRef(true);
-        }
-        u.setPatchSetId(psId);
-        updates.put(psId, u);
-      }
-      return u;
-    }
-
-    @Override
-    public ChangeNotes getNotes() {
-      return notes;
-    }
-
-    @Override
-    public void dontBumpLastUpdatedOn() {
-      // Do nothing; NoteDb effectively updates timestamp if and only if a commit was written to the
-      // change meta ref.
-    }
-
-    @Override
-    public void deleteChange() {
-      deleted = true;
-    }
-  }
-
-  /** Per-change result status from {@link #executeChangeOps}. */
-  private enum ChangeResult {
-    SKIPPED,
-    UPSERTED,
-    DELETED;
-  }
-
-  private final ChangeNotes.Factory changeNotesFactory;
-  private final ChangeUpdate.Factory changeUpdateFactory;
-  private final NoteDbUpdateManager.Factory updateManagerFactory;
-  private final ChangeIndexer indexer;
-  private final GitReferenceUpdated gitRefUpdated;
-  private final ReviewDb db;
-
-  @Inject
-  NoteDbBatchUpdate(
-      GitRepositoryManager repoManager,
-      @GerritPersonIdent PersonIdent serverIdent,
-      ChangeNotes.Factory changeNotesFactory,
-      ChangeUpdate.Factory changeUpdateFactory,
-      NoteDbUpdateManager.Factory updateManagerFactory,
-      ChangeIndexer indexer,
-      GitReferenceUpdated gitRefUpdated,
-      @Assisted ReviewDb db,
-      @Assisted Project.NameKey project,
-      @Assisted CurrentUser user,
-      @Assisted Timestamp when) {
-    super(repoManager, serverIdent, project, user, when);
-    this.changeNotesFactory = changeNotesFactory;
-    this.changeUpdateFactory = changeUpdateFactory;
-    this.updateManagerFactory = updateManagerFactory;
-    this.indexer = indexer;
-    this.gitRefUpdated = gitRefUpdated;
-    this.db = db;
-  }
-
-  @Override
-  public void execute(BatchUpdateListener listener) throws UpdateException, RestApiException {
-    execute(ImmutableList.of(this), listener, false);
-  }
-
-  @Override
-  protected Context newContext() {
-    return new ContextImpl();
-  }
-
-  private void executeUpdateRepo() throws UpdateException, RestApiException {
-    try {
-      logDebug("Executing updateRepo on %d ops", ops.size());
-      RepoContextImpl ctx = new RepoContextImpl();
-      for (BatchUpdateOp op : ops.values()) {
-        op.updateRepo(ctx);
-      }
-
-      logDebug("Executing updateRepo on %d RepoOnlyOps", repoOnlyOps.size());
-      for (RepoOnlyOp op : repoOnlyOps) {
-        op.updateRepo(ctx);
-      }
-
-      if (onSubmitValidators != null && !getRefUpdates().isEmpty()) {
-        // Validation of refs has to take place here and not at the beginning of executeRefUpdates.
-        // Otherwise, failing validation in a second BatchUpdate object will happen *after* the
-        // first update's executeRefUpdates has finished, hence after first repo's refs have been
-        // updated, which is too late.
-        onSubmitValidators.validate(
-            project, ctx.getRevWalk().getObjectReader(), repoView.getCommands());
-      }
-    } catch (Exception e) {
-      Throwables.throwIfInstanceOf(e, RestApiException.class);
-      throw new UpdateException(e);
-    }
-  }
-
-  private class ChangesHandle implements AutoCloseable {
-    private final NoteDbUpdateManager manager;
-    private final boolean dryrun;
-    private final Map<Change.Id, ChangeResult> results;
-
-    ChangesHandle(NoteDbUpdateManager manager, boolean dryrun) {
-      this.manager = manager;
-      this.dryrun = dryrun;
-      results = new HashMap<>();
-    }
-
-    @Override
-    public void close() {
-      manager.close();
-    }
-
-    void setResult(Change.Id id, ChangeResult result) {
-      ChangeResult old = results.putIfAbsent(id, result);
-      checkArgument(old == null, "result for change %s already set: %s", id, old);
-    }
-
-    void execute() throws OrmException, IOException {
-      NoteDbBatchUpdate.this.batchRefUpdate = manager.execute(dryrun);
-    }
-
-    @SuppressWarnings("deprecation")
-    List<com.google.common.util.concurrent.CheckedFuture<?, IOException>> startIndexFutures() {
-      if (dryrun) {
-        return ImmutableList.of();
-      }
-      logDebug("Reindexing %d changes", results.size());
-      List<com.google.common.util.concurrent.CheckedFuture<?, IOException>> indexFutures =
-          new ArrayList<>(results.size());
-      for (Map.Entry<Change.Id, ChangeResult> e : results.entrySet()) {
-        Change.Id id = e.getKey();
-        switch (e.getValue()) {
-          case UPSERTED:
-            indexFutures.add(indexer.indexAsync(project, id));
-            break;
-          case DELETED:
-            indexFutures.add(indexer.deleteAsync(id));
-            break;
-          case SKIPPED:
-            break;
-          default:
-            throw new IllegalStateException("unexpected result: " + e.getValue());
-        }
-      }
-      return indexFutures;
-    }
-  }
-
-  private ChangesHandle executeChangeOps(boolean dryrun) throws Exception {
-    logDebug("Executing change ops");
-    initRepository();
-    Repository repo = repoView.getRepository();
-    checkState(
-        repo.getRefDatabase().performsAtomicTransactions(),
-        "cannot use NoteDb with a repository that does not support atomic batch ref updates: %s",
-        repo);
-
-    ChangesHandle handle =
-        new ChangesHandle(
-            updateManagerFactory
-                .create(project)
-                .setChangeRepo(
-                    repo, repoView.getRevWalk(), repoView.getInserter(), repoView.getCommands()),
-            dryrun);
-    if (user.isIdentifiedUser()) {
-      handle.manager.setRefLogIdent(user.asIdentifiedUser().newRefLogIdent(when, tz));
-    }
-    handle.manager.setRefLogMessage(refLogMessage);
-    handle.manager.setPushCertificate(pushCert);
-    for (Map.Entry<Change.Id, Collection<BatchUpdateOp>> e : ops.asMap().entrySet()) {
-      Change.Id id = e.getKey();
-      ChangeContextImpl ctx = newChangeContext(id);
-      boolean dirty = false;
-      logDebug(
-          "Applying %d ops for change %s: %s",
-          e.getValue().size(),
-          id,
-          lazy(() -> e.getValue().stream().map(op -> op.getClass().getName()).collect(toSet())));
-      for (BatchUpdateOp op : e.getValue()) {
-        dirty |= op.updateChange(ctx);
-      }
-      if (!dirty) {
-        logDebug("No ops reported dirty, short-circuiting");
-        handle.setResult(id, ChangeResult.SKIPPED);
-        continue;
-      }
-      for (ChangeUpdate u : ctx.updates.values()) {
-        handle.manager.add(u);
-      }
-      if (ctx.deleted) {
-        logDebug("Change %s was deleted", id);
-        handle.manager.deleteChange(id);
-        handle.setResult(id, ChangeResult.DELETED);
-      } else {
-        handle.setResult(id, ChangeResult.UPSERTED);
-      }
-    }
-    return handle;
-  }
-
-  private ChangeContextImpl newChangeContext(Change.Id id) throws OrmException {
-    logDebug("Opening change %s for update", id);
-    Change c = newChanges.get(id);
-    boolean isNew = c != null;
-    if (!isNew) {
-      // Pass a synthetic change into ChangeNotes.Factory, which will take care of checking for
-      // existence and populating columns from the parsed notes state.
-      // TODO(dborowitz): This dance made more sense when using Reviewdb; consider a nicer way.
-      c = ChangeNotes.Factory.newNoteDbOnlyChange(project, id);
-    } else {
-      logDebug("Change %s is new", id);
-    }
-    ChangeNotes notes = changeNotesFactory.createForBatchUpdate(c, !isNew);
-    return new ChangeContextImpl(notes);
-  }
-
-  private void executePostOps() throws Exception {
-    ContextImpl ctx = new ContextImpl();
-    for (BatchUpdateOp op : ops.values()) {
-      op.postUpdate(ctx);
-    }
-
-    for (RepoOnlyOp op : repoOnlyOps) {
-      op.postUpdate(ctx);
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/update/Order.java b/java/com/google/gerrit/server/update/Order.java
deleted file mode 100644
index fb64b7b..0000000
--- a/java/com/google/gerrit/server/update/Order.java
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.update;
-
-/** Order of execution of the various phases of a {@link BatchUpdate}. */
-public enum Order {
-  /**
-   * Update the repository and execute all ref updates before touching the database.
-   *
-   * <p>The default and most common, as Gerrit does not behave well when a patch set has no
-   * corresponding ref in the repo.
-   */
-  REPO_BEFORE_DB,
-
-  /**
-   * Update the database before touching the repository.
-   *
-   * <p>Generally only used when deleting patch sets, which should be deleted first from the
-   * database (for the same reason as above.)
-   */
-  DB_BEFORE_REPO;
-}
diff --git a/java/com/google/gerrit/server/update/RefUpdateUtil.java b/java/com/google/gerrit/server/update/RefUpdateUtil.java
deleted file mode 100644
index 71eef9e..0000000
--- a/java/com/google/gerrit/server/update/RefUpdateUtil.java
+++ /dev/null
@@ -1,153 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.update;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.gerrit.server.git.LockFailureException;
-import java.io.IOException;
-import org.eclipse.jgit.internal.JGitText;
-import org.eclipse.jgit.lib.BatchRefUpdate;
-import org.eclipse.jgit.lib.NullProgressMonitor;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.ReceiveCommand;
-
-/** Static utilities for working with JGit's ref update APIs. */
-public class RefUpdateUtil {
-  /**
-   * Execute a batch ref update, throwing a checked exception if not all updates succeeded.
-   *
-   * <p>Creates a new {@link RevWalk} used only for this operation.
-   *
-   * @param bru batch update; should already have been executed.
-   * @param repo repository that created {@code bru}.
-   * @throws LockFailureException if the transaction was aborted due to lock failure; see {@link
-   *     #checkResults(BatchRefUpdate)} for details.
-   * @throws IOException if any result was not {@code OK}.
-   */
-  public static void executeChecked(BatchRefUpdate bru, Repository repo) throws IOException {
-    try (RevWalk rw = new RevWalk(repo)) {
-      executeChecked(bru, rw);
-    }
-  }
-
-  /**
-   * Execute a batch ref update, throwing a checked exception if not all updates succeeded.
-   *
-   * @param bru batch update; should already have been executed.
-   * @param rw walk for executing the update.
-   * @throws LockFailureException if the transaction was aborted due to lock failure; see {@link
-   *     #checkResults(BatchRefUpdate)} for details.
-   * @throws IOException if any result was not {@code OK}.
-   */
-  public static void executeChecked(BatchRefUpdate bru, RevWalk rw) throws IOException {
-    bru.execute(rw, NullProgressMonitor.INSTANCE);
-    checkResults(bru);
-  }
-
-  /**
-   * Check results of all commands in the update batch, reducing to a single exception if there was
-   * a failure.
-   *
-   * <p>Throws {@link LockFailureException} if at least one command failed with {@code
-   * LOCK_FAILURE}, and the entire transaction was aborted, i.e. any non-{@code LOCK_FAILURE}
-   * results, if there were any, failed with "transaction aborted".
-   *
-   * <p>In particular, if the underlying ref database does not {@link
-   * org.eclipse.jgit.lib.RefDatabase#performsAtomicTransactions() perform atomic transactions},
-   * then a combination of {@code LOCK_FAILURE} on one ref and {@code OK} or another result on other
-   * refs will <em>not</em> throw {@code LockFailureException}.
-   *
-   * @param bru batch update; should already have been executed.
-   * @throws LockFailureException if the transaction was aborted due to lock failure.
-   * @throws IOException if any result was not {@code OK}.
-   */
-  @VisibleForTesting
-  static void checkResults(BatchRefUpdate bru) throws IOException {
-    if (bru.getCommands().isEmpty()) {
-      return;
-    }
-
-    int lockFailure = 0;
-    int aborted = 0;
-    int failure = 0;
-
-    for (ReceiveCommand cmd : bru.getCommands()) {
-      if (cmd.getResult() != ReceiveCommand.Result.OK) {
-        failure++;
-      }
-      if (cmd.getResult() == ReceiveCommand.Result.LOCK_FAILURE) {
-        lockFailure++;
-      } else if (cmd.getResult() == ReceiveCommand.Result.REJECTED_OTHER_REASON
-          && JGitText.get().transactionAborted.equals(cmd.getMessage())) {
-        aborted++;
-      }
-    }
-
-    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);
-    }
-  }
-
-  /**
-   * Delete a single ref, throwing a checked exception on failure.
-   *
-   * <p>Does not require that the ref have any particular old value. Succeeds as a no-op if the ref
-   * did not exist.
-   *
-   * @param repo repository.
-   * @param refName ref name to delete.
-   * @throws LockFailureException if a low-level lock failure (e.g. compare-and-swap failure)
-   *     occurs.
-   * @throws IOException if an error occurred.
-   */
-  public static void deleteChecked(Repository repo, String refName) throws IOException {
-    RefUpdate ru = repo.updateRef(refName);
-    ru.setForceUpdate(true);
-    ru.setCheckConflicting(false);
-    switch (ru.delete()) {
-      case FORCED:
-        // Ref was deleted.
-        return;
-
-      case NEW:
-        // Ref didn't exist (yes, really).
-        return;
-
-      case LOCK_FAILURE:
-        throw new LockFailureException("Failed to delete " + refName + ": " + ru.getResult(), ru);
-
-        // Not really failures, but should not be the result of a deletion, so the best option is to
-        // throw.
-      case NO_CHANGE:
-      case FAST_FORWARD:
-      case RENAMED:
-      case NOT_ATTEMPTED:
-
-      case IO_FAILURE:
-      case REJECTED:
-      case REJECTED_CURRENT_BRANCH:
-      case REJECTED_MISSING_OBJECT:
-      case REJECTED_OTHER_REASON:
-      default:
-        throw new IOException("Failed to delete " + refName + ": " + ru.getResult());
-    }
-  }
-
-  private RefUpdateUtil() {}
-}
diff --git a/java/com/google/gerrit/server/update/RepoView.java b/java/com/google/gerrit/server/update/RepoView.java
index 9ce842f..73dd12f 100644
--- a/java/com/google/gerrit/server/update/RepoView.java
+++ b/java/com/google/gerrit/server/update/RepoView.java
@@ -40,8 +40,7 @@
  *
  * <p>Second, the read methods take into account any pending operations on the repository that
  * implementations have staged using the write methods on {@link RepoContext}. Callers do not have
- * to worry about whether operations have been performed yet, and the implementation details may
- * differ between ReviewDb and NoteDb, but callers just don't need to care.
+ * to worry about whether operations have been performed yet.
  */
 public class RepoView {
   private final Repository repo;
diff --git a/java/com/google/gerrit/server/update/RetryHelper.java b/java/com/google/gerrit/server/update/RetryHelper.java
index 10e3455..ae8ba53 100644
--- a/java/com/google/gerrit/server/update/RetryHelper.java
+++ b/java/com/google/gerrit/server/update/RetryHelper.java
@@ -34,14 +34,13 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.git.LockFailureException;
 import com.google.gerrit.metrics.Counter1;
 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.config.GerritServerConfig;
-import com.google.gerrit.server.git.LockFailureException;
-import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.time.Duration;
@@ -69,7 +68,8 @@
     ACCOUNT_UPDATE,
     CHANGE_UPDATE,
     GROUP_UPDATE,
-    INDEX_QUERY
+    INDEX_QUERY,
+    PLUGIN_UPDATE
   }
 
   /**
@@ -141,7 +141,6 @@
     return options().build();
   }
 
-  private final NotesMigration migration;
   private final Metrics metrics;
   private final BatchUpdate.Factory updateFactory;
   private final Map<ActionType, Duration> defaultTimeouts;
@@ -149,27 +148,18 @@
   @Nullable private final Consumer<RetryerBuilder<?>> overwriteDefaultRetryerStrategySetup;
 
   @Inject
-  RetryHelper(
-      @GerritServerConfig Config cfg,
-      Metrics metrics,
-      NotesMigration migration,
-      ReviewDbBatchUpdate.AssistedFactory reviewDbBatchUpdateFactory,
-      NoteDbBatchUpdate.AssistedFactory noteDbBatchUpdateFactory) {
-    this(cfg, metrics, migration, reviewDbBatchUpdateFactory, noteDbBatchUpdateFactory, null);
+  RetryHelper(@GerritServerConfig Config cfg, Metrics metrics, BatchUpdate.Factory updateFactory) {
+    this(cfg, metrics, updateFactory, null);
   }
 
   @VisibleForTesting
   public RetryHelper(
       @GerritServerConfig Config cfg,
       Metrics metrics,
-      NotesMigration migration,
-      ReviewDbBatchUpdate.AssistedFactory reviewDbBatchUpdateFactory,
-      NoteDbBatchUpdate.AssistedFactory noteDbBatchUpdateFactory,
+      BatchUpdate.Factory updateFactory,
       @Nullable Consumer<RetryerBuilder<?>> overwriteDefaultRetryerStrategySetup) {
     this.metrics = metrics;
-    this.migration = migration;
-    this.updateFactory =
-        new BatchUpdate.Factory(migration, reviewDbBatchUpdateFactory, noteDbBatchUpdateFactory);
+    this.updateFactory = updateFactory;
 
     Duration defaultTimeout =
         Duration.ofMillis(
@@ -229,16 +219,6 @@
   public <T> T execute(ChangeAction<T> changeAction, Options opts)
       throws RestApiException, UpdateException {
     try {
-      if (!migration.disableChangeReviewDb()) {
-        // Either we aren't full-NoteDb, or the underlying ref storage doesn't support atomic
-        // transactions. Either way, retrying a partially-failed operation is not idempotent, so
-        // don't do it automatically. Let the end user decide whether they want to retry.
-        return executeWithTimeoutCount(
-            ActionType.CHANGE_UPDATE,
-            () -> changeAction.call(updateFactory),
-            RetryerBuilder.<T>newBuilder().build());
-      }
-
       return execute(
           ActionType.CHANGE_UPDATE,
           () -> changeAction.call(updateFactory),
diff --git a/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java b/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java
deleted file mode 100644
index cab96e5..0000000
--- a/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java
+++ /dev/null
@@ -1,844 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.update;
-
-import static com.google.common.base.Preconditions.checkState;
-import static java.util.Comparator.comparing;
-import static java.util.Objects.requireNonNull;
-import static java.util.concurrent.TimeUnit.NANOSECONDS;
-import static java.util.stream.Collectors.toList;
-
-import com.google.common.base.Stopwatch;
-import com.google.common.base.Throwables;
-import com.google.common.collect.ImmutableList;
-import com.google.common.flogger.FluentLogger;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.ListeningExecutorService;
-import com.google.common.util.concurrent.MoreExecutors;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.RestApiException;
-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.Timer1;
-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.server.ReviewDb;
-import com.google.gerrit.reviewdb.server.ReviewDbWrapper;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.config.ChangeUpdateExecutor;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.InsertedObject;
-import com.google.gerrit.server.git.LockFailureException;
-import com.google.gerrit.server.index.change.ChangeIndexer;
-import com.google.gerrit.server.logging.TraceContext;
-import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.notedb.NoteDbChangeState;
-import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
-import com.google.gerrit.server.notedb.NoteDbUpdateManager;
-import com.google.gerrit.server.notedb.NoteDbUpdateManager.MismatchedStateException;
-import com.google.gerrit.server.notedb.NotesMigration;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import com.google.inject.assistedinject.Assisted;
-import java.io.IOException;
-import java.sql.Timestamp;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.TimeZone;
-import java.util.TreeMap;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
-import org.eclipse.jgit.lib.BatchRefUpdate;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.NullProgressMonitor;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.ReceiveCommand;
-
-/**
- * {@link BatchUpdate} implementation that supports mixed ReviewDb/NoteDb operations, depending on
- * the migration state specified in {@link NotesMigration}.
- *
- * <p>When performing change updates in a mixed ReviewDb/NoteDb environment with ReviewDb primary,
- * the order of operations is very subtle:
- *
- * <ol>
- *   <li>Stage NoteDb updates to get the new NoteDb state, but do not write to the repo.
- *   <li>Write the new state in the Change entity, and commit this to ReviewDb.
- *   <li>Update NoteDb, ignoring any write failures.
- * </ol>
- *
- * The implementation in this class is well-tested, and it is strongly recommended that you not
- * attempt to reimplement this logic. Use {@code BatchUpdate} if at all possible.
- */
-public class ReviewDbBatchUpdate extends BatchUpdate {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
-  public interface AssistedFactory {
-    ReviewDbBatchUpdate create(
-        ReviewDb db, Project.NameKey project, CurrentUser user, Timestamp when);
-  }
-
-  class ContextImpl implements Context {
-    @Override
-    public RepoView getRepoView() throws IOException {
-      return ReviewDbBatchUpdate.this.getRepoView();
-    }
-
-    @Override
-    public RevWalk getRevWalk() throws IOException {
-      return getRepoView().getRevWalk();
-    }
-
-    @Override
-    public Project.NameKey getProject() {
-      return project;
-    }
-
-    @Override
-    public Timestamp getWhen() {
-      return when;
-    }
-
-    @Override
-    public TimeZone getTimeZone() {
-      return tz;
-    }
-
-    @Override
-    public ReviewDb getDb() {
-      return db;
-    }
-
-    @Override
-    public CurrentUser getUser() {
-      return user;
-    }
-
-    @Override
-    public Order getOrder() {
-      return order;
-    }
-  }
-
-  private class RepoContextImpl extends ContextImpl implements RepoContext {
-    @Override
-    public ObjectInserter getInserter() throws IOException {
-      return getRepoView().getInserterWrapper();
-    }
-
-    @Override
-    public void addRefUpdate(ReceiveCommand cmd) throws IOException {
-      initRepository();
-      repoView.getCommands().add(cmd);
-    }
-  }
-
-  private class ChangeContextImpl extends ContextImpl implements ChangeContext {
-    private final ChangeNotes notes;
-    private final Map<PatchSet.Id, ChangeUpdate> updates;
-    private final ReviewDbWrapper dbWrapper;
-    private final Repository threadLocalRepo;
-    private final RevWalk threadLocalRevWalk;
-
-    private boolean deleted;
-    private boolean bumpLastUpdatedOn = true;
-
-    protected ChangeContextImpl(
-        ChangeNotes notes, ReviewDbWrapper dbWrapper, Repository repo, RevWalk rw) {
-      this.notes = requireNonNull(notes);
-      this.dbWrapper = dbWrapper;
-      this.threadLocalRepo = repo;
-      this.threadLocalRevWalk = rw;
-      updates = new TreeMap<>(comparing(PatchSet.Id::get));
-    }
-
-    @Override
-    public ReviewDb getDb() {
-      requireNonNull(dbWrapper);
-      return dbWrapper;
-    }
-
-    @Override
-    public RevWalk getRevWalk() {
-      return threadLocalRevWalk;
-    }
-
-    @Override
-    public ChangeUpdate getUpdate(PatchSet.Id psId) {
-      ChangeUpdate u = updates.get(psId);
-      if (u == null) {
-        u = changeUpdateFactory.create(notes, user, when);
-        if (newChanges.containsKey(notes.getChangeId())) {
-          u.setAllowWriteToNewRef(true);
-        }
-        u.setPatchSetId(psId);
-        updates.put(psId, u);
-      }
-      return u;
-    }
-
-    @Override
-    public ChangeNotes getNotes() {
-      return notes;
-    }
-
-    @Override
-    public void dontBumpLastUpdatedOn() {
-      bumpLastUpdatedOn = false;
-    }
-
-    @Override
-    public void deleteChange() {
-      deleted = true;
-    }
-  }
-
-  @Singleton
-  private static class Metrics {
-    final Timer1<Boolean> executeChangeOpsLatency;
-
-    @Inject
-    Metrics(MetricMaker metricMaker) {
-      executeChangeOpsLatency =
-          metricMaker.newTimer(
-              "batch_update/execute_change_ops",
-              new Description("BatchUpdate change update latency, excluding reindexing")
-                  .setCumulative()
-                  .setUnit(Units.MILLISECONDS),
-              Field.ofBoolean("success"));
-    }
-  }
-
-  static void execute(
-      ImmutableList<ReviewDbBatchUpdate> updates, BatchUpdateListener listener, boolean dryrun)
-      throws UpdateException, RestApiException {
-    if (updates.isEmpty()) {
-      return;
-    }
-    try {
-      Order order = getOrder(updates, listener);
-      boolean updateChangesInParallel = getUpdateChangesInParallel(updates);
-      switch (order) {
-        case REPO_BEFORE_DB:
-          for (ReviewDbBatchUpdate u : updates) {
-            u.executeUpdateRepo();
-          }
-          listener.afterUpdateRepos();
-          for (ReviewDbBatchUpdate u : updates) {
-            u.executeRefUpdates(dryrun);
-          }
-          listener.afterUpdateRefs();
-          for (ReviewDbBatchUpdate u : updates) {
-            u.reindexChanges(u.executeChangeOps(updateChangesInParallel, dryrun));
-          }
-          listener.afterUpdateChanges();
-          break;
-        case DB_BEFORE_REPO:
-          for (ReviewDbBatchUpdate u : updates) {
-            u.reindexChanges(u.executeChangeOps(updateChangesInParallel, dryrun));
-          }
-          for (ReviewDbBatchUpdate u : updates) {
-            u.executeUpdateRepo();
-          }
-          for (ReviewDbBatchUpdate u : updates) {
-            u.executeRefUpdates(dryrun);
-          }
-          break;
-        default:
-          throw new IllegalStateException("invalid execution order: " + order);
-      }
-
-      ChangeIndexer.allAsList(
-              updates.stream().flatMap(u -> u.indexFutures.stream()).collect(toList()))
-          .get();
-
-      // Fire ref update events only after all mutations are finished, since callers may assume a
-      // patch set ref being created means the change was created, or a branch advancing meaning
-      // some changes were closed.
-      updates.stream()
-          .filter(u -> u.batchRefUpdate != null)
-          .forEach(
-              u -> u.gitRefUpdated.fire(u.project, u.batchRefUpdate, u.getAccount().orElse(null)));
-
-      if (!dryrun) {
-        for (ReviewDbBatchUpdate u : updates) {
-          u.executePostOps();
-        }
-      }
-    } catch (Exception e) {
-      wrapAndThrowException(e);
-    }
-  }
-
-  private final AllUsersName allUsers;
-  private final ChangeIndexer indexer;
-  private final ChangeNotes.Factory changeNotesFactory;
-  private final ChangeUpdate.Factory changeUpdateFactory;
-  private final GitReferenceUpdated gitRefUpdated;
-  private final ListeningExecutorService changeUpdateExector;
-  private final Metrics metrics;
-  private final NoteDbUpdateManager.Factory updateManagerFactory;
-  private final NotesMigration notesMigration;
-  private final ReviewDb db;
-  private final SchemaFactory<ReviewDb> schemaFactory;
-  private final long skewMs;
-
-  @SuppressWarnings("deprecation")
-  private final List<com.google.common.util.concurrent.CheckedFuture<?, IOException>> indexFutures =
-      new ArrayList<>();
-
-  @Inject
-  ReviewDbBatchUpdate(
-      @GerritServerConfig Config cfg,
-      AllUsersName allUsers,
-      ChangeIndexer indexer,
-      ChangeNotes.Factory changeNotesFactory,
-      @ChangeUpdateExecutor ListeningExecutorService changeUpdateExector,
-      ChangeUpdate.Factory changeUpdateFactory,
-      @GerritPersonIdent PersonIdent serverIdent,
-      GitReferenceUpdated gitRefUpdated,
-      GitRepositoryManager repoManager,
-      Metrics metrics,
-      NoteDbUpdateManager.Factory updateManagerFactory,
-      NotesMigration notesMigration,
-      SchemaFactory<ReviewDb> schemaFactory,
-      @Assisted ReviewDb db,
-      @Assisted Project.NameKey project,
-      @Assisted CurrentUser user,
-      @Assisted Timestamp when) {
-    super(repoManager, serverIdent, project, user, when);
-    this.allUsers = allUsers;
-    this.changeNotesFactory = changeNotesFactory;
-    this.changeUpdateExector = changeUpdateExector;
-    this.changeUpdateFactory = changeUpdateFactory;
-    this.gitRefUpdated = gitRefUpdated;
-    this.indexer = indexer;
-    this.metrics = metrics;
-    this.notesMigration = notesMigration;
-    this.schemaFactory = schemaFactory;
-    this.updateManagerFactory = updateManagerFactory;
-    this.db = db;
-    skewMs = NoteDbChangeState.getReadOnlySkew(cfg);
-  }
-
-  @Override
-  public void execute(BatchUpdateListener listener) throws UpdateException, RestApiException {
-    execute(ImmutableList.of(this), listener, false);
-  }
-
-  @Override
-  protected Context newContext() {
-    return new ContextImpl();
-  }
-
-  private void executeUpdateRepo() throws UpdateException, RestApiException {
-    try {
-      logDebug("Executing updateRepo on %d ops", ops.size());
-      RepoContextImpl ctx = new RepoContextImpl();
-      for (BatchUpdateOp op : ops.values()) {
-        op.updateRepo(ctx);
-      }
-
-      logDebug("Executing updateRepo on %d RepoOnlyOps", repoOnlyOps.size());
-      for (RepoOnlyOp op : repoOnlyOps) {
-        op.updateRepo(ctx);
-      }
-
-      if (onSubmitValidators != null && !getRefUpdates().isEmpty()) {
-        // Validation of refs has to take place here and not at the beginning of executeRefUpdates.
-        // Otherwise, failing validation in a second BatchUpdate object will happen *after* the
-        // first update's executeRefUpdates has finished, hence after first repo's refs have been
-        // updated, which is too late.
-        onSubmitValidators.validate(
-            project, ctx.getRevWalk().getObjectReader(), repoView.getCommands());
-      }
-
-      if (repoView != null) {
-        logDebug("Flushing inserter");
-        repoView.getInserter().flush();
-      } else {
-        logDebug("No objects to flush");
-      }
-    } catch (Exception e) {
-      Throwables.throwIfInstanceOf(e, RestApiException.class);
-      throw new UpdateException(e);
-    }
-  }
-
-  private void executeRefUpdates(boolean dryrun) throws IOException, RestApiException {
-    if (getRefUpdates().isEmpty()) {
-      logDebug("No ref updates to execute");
-      return;
-    }
-    // May not be opened if the caller added ref updates but no new objects.
-    // TODO(dborowitz): Really?
-    initRepository();
-    batchRefUpdate = repoView.getRepository().getRefDatabase().newBatchUpdate();
-    batchRefUpdate.setPushCertificate(pushCert);
-    batchRefUpdate.setRefLogMessage(refLogMessage, true);
-    batchRefUpdate.setAllowNonFastForwards(true);
-    repoView.getCommands().addTo(batchRefUpdate);
-    if (user.isIdentifiedUser()) {
-      batchRefUpdate.setRefLogIdent(user.asIdentifiedUser().newRefLogIdent(when, tz));
-    }
-    logDebug("Executing batch of %d ref updates", batchRefUpdate.getCommands().size());
-    if (dryrun) {
-      return;
-    }
-
-    // Force BatchRefUpdate to read newly referenced objects using a new RevWalk, rather than one
-    // that might have access to unflushed objects.
-    try (RevWalk updateRw = new RevWalk(repoView.getRepository())) {
-      batchRefUpdate.execute(updateRw, NullProgressMonitor.INSTANCE);
-    }
-    boolean ok = true;
-    for (ReceiveCommand cmd : batchRefUpdate.getCommands()) {
-      if (cmd.getResult() != ReceiveCommand.Result.OK) {
-        ok = false;
-        break;
-      }
-    }
-    if (!ok) {
-      throw new RestApiException("BatchRefUpdate failed: " + batchRefUpdate);
-    }
-  }
-
-  private List<ChangeTask> executeChangeOps(boolean parallel, boolean dryrun)
-      throws UpdateException, RestApiException {
-    List<ChangeTask> tasks;
-    boolean success = false;
-    Stopwatch sw = Stopwatch.createStarted();
-    try {
-      logDebug("Executing change ops (parallel? %s)", parallel);
-      ListeningExecutorService executor =
-          parallel ? changeUpdateExector : MoreExecutors.newDirectExecutorService();
-
-      tasks = new ArrayList<>(ops.keySet().size());
-      try {
-        if (notesMigration.commitChangeWrites() && repoView != null) {
-          // A NoteDb change may have been rebuilt since the repo was originally
-          // opened, so make sure we see that.
-          logDebug("Preemptively scanning for repo changes");
-          repoView.getRepository().scanForRepoChanges();
-        }
-        if (!ops.isEmpty() && notesMigration.failChangeWrites()) {
-          // Fail fast before attempting any writes if changes are read-only, as
-          // this is a programmer error.
-          logDebug("Failing early due to read-only Changes table");
-          throw new OrmException(NoteDbUpdateManager.CHANGES_READ_ONLY);
-        }
-        List<ListenableFuture<?>> futures = new ArrayList<>(ops.keySet().size());
-        for (Map.Entry<Change.Id, Collection<BatchUpdateOp>> e : ops.asMap().entrySet()) {
-          ChangeTask task =
-              new ChangeTask(e.getKey(), e.getValue(), Thread.currentThread(), dryrun);
-          tasks.add(task);
-          if (!parallel) {
-            logDebug("Direct execution of task for ops: %s", ops);
-          }
-          futures.add(executor.submit(task));
-        }
-        if (parallel) {
-          logDebug(
-              "Waiting on futures for %d ops spanning %d changes", ops.size(), ops.keySet().size());
-        }
-        Futures.allAsList(futures).get();
-
-        if (notesMigration.commitChangeWrites()) {
-          if (!dryrun) {
-            executeNoteDbUpdates(tasks);
-          }
-        }
-        success = true;
-      } catch (ExecutionException | InterruptedException e) {
-        Throwables.throwIfInstanceOf(e.getCause(), UpdateException.class);
-        Throwables.throwIfInstanceOf(e.getCause(), RestApiException.class);
-        throw new UpdateException(e);
-      } catch (OrmException | IOException e) {
-        throw new UpdateException(e);
-      }
-    } finally {
-      metrics.executeChangeOpsLatency.record(success, sw.elapsed(NANOSECONDS), NANOSECONDS);
-    }
-    return tasks;
-  }
-
-  private void reindexChanges(List<ChangeTask> tasks) {
-    // Reindex changes.
-    for (ChangeTask task : tasks) {
-      if (task.deleted) {
-        indexFutures.add(indexer.deleteAsync(task.id));
-      } else if (task.dirty) {
-        indexFutures.add(indexer.indexAsync(project, task.id));
-      }
-    }
-  }
-
-  private void executeNoteDbUpdates(List<ChangeTask> tasks)
-      throws ResourceConflictException, IOException {
-    // Aggregate together all NoteDb ref updates from the ops we executed,
-    // possibly in parallel. Each task had its own NoteDbUpdateManager instance
-    // with its own thread-local copy of the repo(s), but each of those was just
-    // used for staging updates and was never executed.
-    //
-    // Use a new BatchRefUpdate as the original batchRefUpdate field is intended
-    // for use only by the updateRepo phase.
-    //
-    // See the comments in NoteDbUpdateManager#execute() for why we execute the
-    // updates on the change repo first.
-    logDebug("Executing NoteDb updates for %d changes", tasks.size());
-    try {
-      initRepository();
-      BatchRefUpdate changeRefUpdate = repoView.getRepository().getRefDatabase().newBatchUpdate();
-      boolean hasAllUsersCommands = false;
-      try (ObjectInserter ins = repoView.getRepository().newObjectInserter()) {
-        int objs = 0;
-        for (ChangeTask task : tasks) {
-          if (task.noteDbResult == null) {
-            logDebug("No-op update to %s", task.id);
-            continue;
-          }
-          for (ReceiveCommand cmd : task.noteDbResult.changeCommands()) {
-            changeRefUpdate.addCommand(cmd);
-          }
-          for (InsertedObject obj : task.noteDbResult.changeObjects()) {
-            objs++;
-            ins.insert(obj.type(), obj.data().toByteArray());
-          }
-          hasAllUsersCommands |= !task.noteDbResult.allUsersCommands().isEmpty();
-        }
-        logDebug(
-            "Collected %d objects and %d ref updates to change repo",
-            objs, changeRefUpdate.getCommands().size());
-        executeNoteDbUpdate(getRevWalk(), ins, changeRefUpdate);
-      }
-
-      if (hasAllUsersCommands) {
-        try (Repository allUsersRepo = repoManager.openRepository(allUsers);
-            RevWalk allUsersRw = new RevWalk(allUsersRepo);
-            ObjectInserter allUsersIns = allUsersRepo.newObjectInserter()) {
-          int objs = 0;
-          BatchRefUpdate allUsersRefUpdate = allUsersRepo.getRefDatabase().newBatchUpdate();
-          for (ChangeTask task : tasks) {
-            if (task.noteDbResult == null) {
-              continue;
-            }
-            for (ReceiveCommand cmd : task.noteDbResult.allUsersCommands()) {
-              allUsersRefUpdate.addCommand(cmd);
-            }
-            for (InsertedObject obj : task.noteDbResult.allUsersObjects()) {
-              allUsersIns.insert(obj.type(), obj.data().toByteArray());
-            }
-          }
-          logDebug(
-              "Collected %d objects and %d ref updates to All-Users",
-              objs, allUsersRefUpdate.getCommands().size());
-          executeNoteDbUpdate(allUsersRw, allUsersIns, allUsersRefUpdate);
-        }
-      } else {
-        logDebug("No All-Users updates");
-      }
-    } catch (IOException e) {
-      if (tasks.stream().allMatch(t -> t.storage == PrimaryStorage.REVIEW_DB)) {
-        // Ignore all errors trying to update NoteDb at this point. We've already written the
-        // NoteDbChangeStates to ReviewDb, which means if any state is out of date it will be
-        // rebuilt the next time it is needed.
-        //
-        // Always log even without RequestId.
-        logger.atFine().withCause(e).log("Ignoring NoteDb update error after ReviewDb write");
-
-        // Otherwise, we can't prove it's safe to ignore the error, either because some change had
-        // NOTE_DB primary, or a task failed before determining the primary storage.
-      } else if (e instanceof LockFailureException) {
-        // LOCK_FAILURE is a special case indicating there was a conflicting write to a meta ref,
-        // although it happened too late for us to produce anything but a generic error message.
-        throw new ResourceConflictException("Updating change failed due to conflicting write", e);
-      }
-      throw e;
-    }
-  }
-
-  private void executeNoteDbUpdate(RevWalk rw, ObjectInserter ins, BatchRefUpdate bru)
-      throws IOException {
-    if (bru.getCommands().isEmpty()) {
-      logDebug("No commands, skipping flush and ref update");
-      return;
-    }
-    ins.flush();
-    bru.setAllowNonFastForwards(true);
-    bru.execute(rw, NullProgressMonitor.INSTANCE);
-    for (ReceiveCommand cmd : bru.getCommands()) {
-      // TODO(dborowitz): LOCK_FAILURE for NoteDb primary should be retried.
-      if (cmd.getResult() != ReceiveCommand.Result.OK) {
-        throw new IOException("Update failed: " + bru);
-      }
-    }
-  }
-
-  private class ChangeTask implements Callable<Void> {
-    final Change.Id id;
-    private final Collection<BatchUpdateOp> changeOps;
-    private final Thread mainThread;
-    private final boolean dryrun;
-
-    PrimaryStorage storage;
-    NoteDbUpdateManager.StagedResult noteDbResult;
-    boolean dirty;
-    boolean deleted;
-
-    private ChangeTask(
-        Change.Id id, Collection<BatchUpdateOp> changeOps, Thread mainThread, boolean dryrun) {
-      this.id = id;
-      this.changeOps = changeOps;
-      this.mainThread = mainThread;
-      this.dryrun = dryrun;
-    }
-
-    @Override
-    public Void call() throws Exception {
-      try (TraceContext traceContext =
-          TraceContext.open()
-              .addTag("TASK_ID", id.toString() + "-" + Thread.currentThread().getId())) {
-        if (Thread.currentThread() == mainThread) {
-          initRepository();
-          Repository repo = repoView.getRepository();
-          try (RevWalk rw = new RevWalk(repo)) {
-            call(ReviewDbBatchUpdate.this.db, repo, rw);
-          }
-        } else {
-          // Possible optimization: allow Ops to declare whether they need to
-          // access the repo from updateChange, and don't open in this thread
-          // unless we need it. However, as of this writing the only operations
-          // that are executed in parallel are during ReceiveCommits, and they
-          // all need the repo open anyway. (The non-parallel case above does not
-          // reopen the repo.)
-          try (ReviewDb threadLocalDb = schemaFactory.open();
-              Repository repo = repoManager.openRepository(project);
-              RevWalk rw = new RevWalk(repo)) {
-            call(threadLocalDb, repo, rw);
-          }
-        }
-        return null;
-      }
-    }
-
-    private void call(ReviewDb db, Repository repo, RevWalk rw) throws Exception {
-      @SuppressWarnings("resource") // Not always opened.
-      NoteDbUpdateManager updateManager = null;
-      try {
-        db.changes().beginTransaction(id);
-        try {
-          ChangeContextImpl ctx = newChangeContext(db, repo, rw, id);
-          NoteDbChangeState oldState = NoteDbChangeState.parse(ctx.getChange());
-          NoteDbChangeState.checkNotReadOnly(oldState, skewMs);
-
-          storage = PrimaryStorage.of(oldState);
-          if (storage == PrimaryStorage.NOTE_DB && !notesMigration.readChanges()) {
-            throw new OrmException("must have NoteDb enabled to update change " + id);
-          }
-
-          // Call updateChange on each op.
-          logDebug("Calling updateChange on %s ops", changeOps.size());
-          for (BatchUpdateOp op : changeOps) {
-            dirty |= op.updateChange(ctx);
-          }
-          if (!dirty) {
-            logDebug("No ops reported dirty, short-circuiting");
-            return;
-          }
-          deleted = ctx.deleted;
-          if (deleted) {
-            logDebug("Change was deleted");
-          }
-
-          // Stage the NoteDb update and store its state in the Change.
-          if (notesMigration.commitChangeWrites()) {
-            updateManager = stageNoteDbUpdate(ctx, deleted);
-          }
-
-          if (storage == PrimaryStorage.REVIEW_DB) {
-            // If primary storage of this change is in ReviewDb, bump
-            // lastUpdatedOn or rowVersion and commit. Otherwise, don't waste
-            // time updating ReviewDb at all.
-            Iterable<Change> cs = changesToUpdate(ctx);
-            if (isNewChange(id)) {
-              // Insert rather than upsert in case of a race on change IDs.
-              logDebug("Inserting change");
-              db.changes().insert(cs);
-            } else if (deleted) {
-              logDebug("Deleting change");
-              db.changes().delete(cs);
-            } else {
-              logDebug("Updating change");
-              db.changes().update(cs);
-            }
-            if (!dryrun) {
-              db.commit();
-            }
-          } else {
-            logDebug("Skipping ReviewDb write since primary storage is %s", storage);
-          }
-        } finally {
-          db.rollback();
-        }
-
-        // Do not execute the NoteDbUpdateManager, as we don't want too much
-        // contention on the underlying repo, and we would rather use a single
-        // ObjectInserter/BatchRefUpdate later.
-        //
-        // TODO(dborowitz): May or may not be worth trying to batch together
-        // flushed inserters as well.
-        if (storage == PrimaryStorage.NOTE_DB) {
-          // Should have failed above if NoteDb is disabled.
-          checkState(notesMigration.commitChangeWrites());
-          noteDbResult = updateManager.stage().get(id);
-        } else if (notesMigration.commitChangeWrites()) {
-          try {
-            noteDbResult = updateManager.stage().get(id);
-          } catch (IOException ex) {
-            // Ignore all errors trying to update NoteDb at this point. We've
-            // already written the NoteDbChangeState to ReviewDb, which means
-            // if the state is out of date it will be rebuilt the next time it
-            // is needed.
-            logger.atFine().withCause(ex).log("Ignoring NoteDb update error after ReviewDb write");
-          }
-        }
-      } catch (Exception e) {
-        logDebug("Error updating change (should be rethrown)", e);
-        Throwables.propagateIfPossible(e, RestApiException.class);
-        throw new UpdateException(e);
-      } finally {
-        if (updateManager != null) {
-          updateManager.close();
-        }
-      }
-    }
-
-    private ChangeContextImpl newChangeContext(
-        ReviewDb db, Repository repo, RevWalk rw, Change.Id id) throws OrmException {
-      Change c = newChanges.get(id);
-      boolean isNew = c != null;
-      if (isNew) {
-        // New change: populate noteDbState.
-        checkState(c.getNoteDbState() == null, "noteDbState should not be filled in by callers");
-        if (notesMigration.changePrimaryStorage() == PrimaryStorage.NOTE_DB) {
-          c.setNoteDbState(NoteDbChangeState.NOTE_DB_PRIMARY_STATE);
-        }
-      } else {
-        // Existing change.
-        c = ChangeNotes.readOneReviewDbChange(db, id);
-        if (c == null) {
-          // Not in ReviewDb, but new changes are created with default primary
-          // storage as NOTE_DB, so we can assume that a missing change is
-          // NoteDb primary. Pass a synthetic change into ChangeNotes.Factory,
-          // which lets ChangeNotes take care of the existence check.
-          //
-          // TODO(dborowitz): This assumption is potentially risky, because
-          // it means once we turn this option on and start creating changes
-          // without writing anything to ReviewDb, we can't turn this option
-          // back off without making those changes inaccessible. The problem
-          // is we have no way of distinguishing a change that only exists in
-          // NoteDb because it only ever existed in NoteDb, from a change that
-          // only exists in NoteDb because it used to exist in ReviewDb and
-          // deleting from ReviewDb succeeded but deleting from NoteDb failed.
-          //
-          // TODO(dborowitz): We actually still have that problem anyway. Maybe
-          // we need a cutoff timestamp? Or maybe we need to start leaving
-          // tombstones in ReviewDb?
-          c = ChangeNotes.Factory.newNoteDbOnlyChange(project, id);
-        }
-        NoteDbChangeState.checkNotReadOnly(c, skewMs);
-      }
-      ChangeNotes notes = changeNotesFactory.createForBatchUpdate(c, !isNew);
-      return new ChangeContextImpl(notes, new BatchUpdateReviewDb(db), repo, rw);
-    }
-
-    private NoteDbUpdateManager stageNoteDbUpdate(ChangeContextImpl ctx, boolean deleted)
-        throws OrmException, IOException {
-      logDebug("Staging NoteDb update");
-      NoteDbUpdateManager updateManager =
-          updateManagerFactory
-              .create(ctx.getProject())
-              .setChangeRepo(
-                  ctx.threadLocalRepo,
-                  ctx.threadLocalRevWalk,
-                  null,
-                  new ChainedReceiveCommands(ctx.threadLocalRepo));
-      if (ctx.getUser().isIdentifiedUser()) {
-        updateManager.setRefLogIdent(
-            ctx.getUser().asIdentifiedUser().newRefLogIdent(ctx.getWhen(), tz));
-      }
-      for (ChangeUpdate u : ctx.updates.values()) {
-        updateManager.add(u);
-      }
-
-      Change c = ctx.getChange();
-      if (deleted) {
-        updateManager.deleteChange(c.getId());
-      }
-      try {
-        updateManager.stageAndApplyDelta(c);
-      } catch (MismatchedStateException ex) {
-        // Refused to apply update because NoteDb was out of sync, which can
-        // only happen if ReviewDb is the primary storage for this change.
-        //
-        // Go ahead with this ReviewDb update; it's still out of sync, but this
-        // is no worse than before, and it will eventually get rebuilt.
-        logDebug("Ignoring MismatchedStateException while staging");
-      }
-
-      return updateManager;
-    }
-
-    private boolean isNewChange(Change.Id id) {
-      return newChanges.containsKey(id);
-    }
-  }
-
-  private static Iterable<Change> changesToUpdate(ChangeContextImpl ctx) {
-    Change c = ctx.getChange();
-    if (ctx.bumpLastUpdatedOn && c.getLastUpdatedOn().before(ctx.getWhen())) {
-      c.setLastUpdatedOn(ctx.getWhen());
-    }
-    return Collections.singleton(c);
-  }
-
-  private void executePostOps() throws Exception {
-    ContextImpl ctx = new ContextImpl();
-    for (BatchUpdateOp op : ops.values()) {
-      op.postUpdate(ctx);
-    }
-
-    for (RepoOnlyOp op : repoOnlyOps) {
-      op.postUpdate(ctx);
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/util/CommitMessageUtil.java b/java/com/google/gerrit/server/util/CommitMessageUtil.java
index fa55597..e984f46 100644
--- a/java/com/google/gerrit/server/util/CommitMessageUtil.java
+++ b/java/com/google/gerrit/server/util/CommitMessageUtil.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.util;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 
 /** Utility functions to manipulate commit messages. */
@@ -23,18 +24,22 @@
   private CommitMessageUtil() {}
 
   /**
-   * Checks for null or empty commit messages and appends a newline character to the commit message.
+   * Checks for invalid (empty or containing \0) commit messages and appends a newline character to
+   * the commit message.
    *
    * @throws BadRequestException if the commit message is null or empty
    * @returns the trimmed message with a trailing newline character
    */
-  public static String checkAndSanitizeCommitMessage(String commitMessage)
+  public static String checkAndSanitizeCommitMessage(@Nullable String commitMessage)
       throws BadRequestException {
-    String wellFormedMessage = Strings.nullToEmpty(commitMessage).trim();
-    if (wellFormedMessage.isEmpty()) {
+    String trimmed = Strings.nullToEmpty(commitMessage).trim();
+    if (trimmed.isEmpty()) {
       throw new BadRequestException("Commit message cannot be null or empty");
     }
-    wellFormedMessage = wellFormedMessage + "\n";
-    return wellFormedMessage;
+    if (trimmed.indexOf(0) >= 0) {
+      throw new BadRequestException("Commit message cannot have NUL character");
+    }
+    trimmed = trimmed + "\n";
+    return trimmed;
   }
 }
diff --git a/java/com/google/gerrit/server/util/FallbackRequestContext.java b/java/com/google/gerrit/server/util/FallbackRequestContext.java
index de1555f..7a40dbf 100644
--- a/java/com/google/gerrit/server/util/FallbackRequestContext.java
+++ b/java/com/google/gerrit/server/util/FallbackRequestContext.java
@@ -14,12 +14,9 @@
 
 package com.google.gerrit.server.util;
 
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
 import com.google.inject.Singleton;
 
 /**
@@ -40,14 +37,4 @@
   public CurrentUser getUser() {
     return user;
   }
-
-  @Override
-  public Provider<ReviewDb> getReviewDbProvider() {
-    return new Provider<ReviewDb>() {
-      @Override
-      public ReviewDb get() {
-        throw new ProvisionException("Automatic ReviewDb only available in request scope");
-      }
-    };
-  }
 }
diff --git a/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java b/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java
index 6dd5543..99dd8bf 100644
--- a/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java
+++ b/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.server.RemotePeer;
 import com.google.gerrit.server.config.CanonicalWebUrl;
-import com.google.gerrit.server.config.RequestScopedReviewDbProvider;
 import com.google.inject.Inject;
 import com.google.inject.Key;
 import com.google.inject.Provider;
@@ -41,9 +40,8 @@
   GuiceRequestScopePropagator(
       @CanonicalWebUrl @Nullable Provider<String> urlProvider,
       @RemotePeer Provider<SocketAddress> remotePeerProvider,
-      ThreadLocalRequestContext local,
-      Provider<RequestScopedReviewDbProvider> dbProviderProvider) {
-    super(ServletScopes.REQUEST, local, dbProviderProvider);
+      ThreadLocalRequestContext local) {
+    super(ServletScopes.REQUEST, local);
     this.url = urlProvider != null ? urlProvider.get() : null;
     this.peer = remotePeerProvider.get();
   }
diff --git a/java/com/google/gerrit/server/util/MagicBranch.java b/java/com/google/gerrit/server/util/MagicBranch.java
index a049169..4e41be0 100644
--- a/java/com/google/gerrit/server/util/MagicBranch.java
+++ b/java/com/google/gerrit/server/util/MagicBranch.java
@@ -28,25 +28,19 @@
   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/";
-  // TODO(xchangcheng): remove after migrating tools which are using this magic branch.
-  public static final String NEW_PUBLISH_CHANGE = "refs/publish/";
 
   /** 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;
-    } else if (refName.startsWith(NEW_PUBLISH_CHANGE)) {
-      magicBranch = NEW_PUBLISH_CHANGE;
     }
     return refName.substring(magicBranch.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_PUBLISH_CHANGE)
-        || refName.startsWith(NEW_CHANGE);
+    return refName.startsWith(NEW_DRAFT_CHANGE) || refName.startsWith(NEW_CHANGE);
   }
 
   /** Returns the ref name prefix for a magic branch, {@code null} if the branch is not magic */
@@ -54,9 +48,6 @@
     if (refName.startsWith(NEW_DRAFT_CHANGE)) {
       return NEW_DRAFT_CHANGE;
     }
-    if (refName.startsWith(NEW_PUBLISH_CHANGE)) {
-      return NEW_PUBLISH_CHANGE;
-    }
     if (refName.startsWith(NEW_CHANGE)) {
       return NEW_CHANGE;
     }
@@ -80,11 +71,6 @@
     if (result != Capable.OK) {
       return result;
     }
-    result = checkMagicBranchRef(NEW_PUBLISH_CHANGE, repo, project);
-    if (result != Capable.OK) {
-      return result;
-    }
-
     return Capable.OK;
   }
 
diff --git a/java/com/google/gerrit/server/util/ManualRequestContext.java b/java/com/google/gerrit/server/util/ManualRequestContext.java
index 620a2bc..7790b5f 100644
--- a/java/com/google/gerrit/server/util/ManualRequestContext.java
+++ b/java/com/google/gerrit/server/util/ManualRequestContext.java
@@ -14,35 +14,23 @@
 
 package com.google.gerrit.server.util;
 
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Provider;
 import com.google.inject.util.Providers;
 
 /** Closeable version of a {@link RequestContext} with manually-specified providers. */
 public class ManualRequestContext implements RequestContext, AutoCloseable {
   private final Provider<CurrentUser> userProvider;
-  private final Provider<ReviewDb> db;
   private final ThreadLocalRequestContext requestContext;
   private final RequestContext old;
 
-  public ManualRequestContext(
-      CurrentUser user,
-      SchemaFactory<ReviewDb> schemaFactory,
-      ThreadLocalRequestContext requestContext)
-      throws OrmException {
-    this(Providers.of(user), schemaFactory, requestContext);
+  public ManualRequestContext(CurrentUser user, ThreadLocalRequestContext requestContext) {
+    this(Providers.of(user), requestContext);
   }
 
   public ManualRequestContext(
-      Provider<CurrentUser> userProvider,
-      SchemaFactory<ReviewDb> schemaFactory,
-      ThreadLocalRequestContext requestContext)
-      throws OrmException {
+      Provider<CurrentUser> userProvider, ThreadLocalRequestContext requestContext) {
     this.userProvider = userProvider;
-    this.db = Providers.of(schemaFactory.open());
     this.requestContext = requestContext;
     old = requestContext.setContext(this);
   }
@@ -53,13 +41,7 @@
   }
 
   @Override
-  public Provider<ReviewDb> getReviewDbProvider() {
-    return db;
-  }
-
-  @Override
   public void close() {
     requestContext.setContext(old);
-    db.get().close();
   }
 }
diff --git a/java/com/google/gerrit/server/util/MostSpecificComparator.java b/java/com/google/gerrit/server/util/MostSpecificComparator.java
index f243726..b22617c 100644
--- a/java/com/google/gerrit/server/util/MostSpecificComparator.java
+++ b/java/com/google/gerrit/server/util/MostSpecificComparator.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.util;
 
-import com.google.gerrit.common.data.RefConfigSection;
+import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.server.project.RefPattern;
 import java.util.Comparator;
 import org.apache.commons.lang.StringUtils;
@@ -40,7 +40,7 @@
  * are infinite, but refs/heads/[a-zA-Z]* has more transitions, which after all turns it more
  * specific.
  */
-public final class MostSpecificComparator implements Comparator<RefConfigSection> {
+public final class MostSpecificComparator implements Comparator<AccessSection> {
   private final String refName;
 
   public MostSpecificComparator(String refName) {
@@ -48,7 +48,7 @@
   }
 
   @Override
-  public int compare(RefConfigSection a, RefConfigSection b) {
+  public int compare(AccessSection a, AccessSection b) {
     return compare(a.getName(), b.getName());
   }
 
diff --git a/java/com/google/gerrit/server/util/OneOffRequestContext.java b/java/com/google/gerrit/server/util/OneOffRequestContext.java
index 28be669..1788343 100644
--- a/java/com/google/gerrit/server/util/OneOffRequestContext.java
+++ b/java/com/google/gerrit/server/util/OneOffRequestContext.java
@@ -15,48 +15,38 @@
 package com.google.gerrit.server.util;
 
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.InternalUser;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 /**
  * Helper to create one-off request contexts.
  *
- * <p>Each call to {@link #open()} opens a new {@link ReviewDb}, so this class should only be used
- * in a bounded try/finally block.
- *
  * <p>The user in the request context is {@link InternalUser} or the {@link IdentifiedUser}
  * associated to the userId passed as parameter.
  */
 @Singleton
 public class OneOffRequestContext {
   private final InternalUser.Factory userFactory;
-  private final SchemaFactory<ReviewDb> schemaFactory;
   private final ThreadLocalRequestContext requestContext;
   private final IdentifiedUser.GenericFactory identifiedUserFactory;
 
   @Inject
   OneOffRequestContext(
       InternalUser.Factory userFactory,
-      SchemaFactory<ReviewDb> schemaFactory,
       ThreadLocalRequestContext requestContext,
       IdentifiedUser.GenericFactory identifiedUserFactory) {
     this.userFactory = userFactory;
-    this.schemaFactory = schemaFactory;
     this.requestContext = requestContext;
     this.identifiedUserFactory = identifiedUserFactory;
   }
 
-  public ManualRequestContext open() throws OrmException {
-    return new ManualRequestContext(userFactory.create(), schemaFactory, requestContext);
+  public ManualRequestContext open() {
+    return new ManualRequestContext(userFactory.create(), requestContext);
   }
 
-  public ManualRequestContext openAs(Account.Id userId) throws OrmException {
-    return new ManualRequestContext(
-        identifiedUserFactory.create(userId), schemaFactory, requestContext);
+  public ManualRequestContext openAs(Account.Id userId) {
+    return new ManualRequestContext(identifiedUserFactory.create(userId), requestContext);
   }
 }
diff --git a/java/com/google/gerrit/server/util/PluginRequestContext.java b/java/com/google/gerrit/server/util/PluginRequestContext.java
index 3f3f647..ec15786 100644
--- a/java/com/google/gerrit/server/util/PluginRequestContext.java
+++ b/java/com/google/gerrit/server/util/PluginRequestContext.java
@@ -14,11 +14,8 @@
 
 package com.google.gerrit.server.util;
 
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.PluginUser;
-import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
 
 /** RequestContext active while plugins load or unload. */
 public class PluginRequestContext implements RequestContext {
@@ -32,14 +29,4 @@
   public CurrentUser getUser() {
     return user;
   }
-
-  @Override
-  public Provider<ReviewDb> getReviewDbProvider() {
-    return new Provider<ReviewDb>() {
-      @Override
-      public ReviewDb get() {
-        throw new ProvisionException("Automatic ReviewDb only available in request scope");
-      }
-    };
-  }
 }
diff --git a/java/com/google/gerrit/server/util/RequestContext.java b/java/com/google/gerrit/server/util/RequestContext.java
index 37fd7bc..04ee5c6 100644
--- a/java/com/google/gerrit/server/util/RequestContext.java
+++ b/java/com/google/gerrit/server/util/RequestContext.java
@@ -14,9 +14,7 @@
 
 package com.google.gerrit.server.util;
 
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
-import com.google.inject.Provider;
 
 /**
  * The RequestContext is an interface exposing the fields that are needed by the GerritGlobalModule
@@ -24,6 +22,4 @@
  */
 public interface RequestContext {
   CurrentUser getUser();
-
-  Provider<ReviewDb> getReviewDbProvider();
 }
diff --git a/java/com/google/gerrit/server/util/RequestScopePropagator.java b/java/com/google/gerrit/server/util/RequestScopePropagator.java
index e5b0eaf..789b9b1 100644
--- a/java/com/google/gerrit/server/util/RequestScopePropagator.java
+++ b/java/com/google/gerrit/server/util/RequestScopePropagator.java
@@ -18,13 +18,9 @@
 
 import com.google.common.base.Throwables;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.RequestCleanup;
-import com.google.gerrit.server.config.RequestScopedReviewDbProvider;
 import com.google.gerrit.server.git.ProjectRunnable;
 import com.google.inject.Key;
-import com.google.inject.Provider;
 import com.google.inject.Scope;
 import com.google.inject.servlet.ServletScopes;
 import java.util.concurrent.Callable;
@@ -47,15 +43,10 @@
 
   private final Scope scope;
   private final ThreadLocalRequestContext local;
-  private final Provider<RequestScopedReviewDbProvider> dbProviderProvider;
 
-  protected RequestScopePropagator(
-      Scope scope,
-      ThreadLocalRequestContext local,
-      Provider<RequestScopedReviewDbProvider> dbProviderProvider) {
+  protected RequestScopePropagator(Scope scope, ThreadLocalRequestContext local) {
     this.scope = scope;
     this.local = local;
-    this.dbProviderProvider = dbProviderProvider;
   }
 
   /**
@@ -174,19 +165,7 @@
 
   protected <T> Callable<T> context(RequestContext context, Callable<T> callable) {
     return () -> {
-      RequestContext old =
-          local.setContext(
-              new RequestContext() {
-                @Override
-                public CurrentUser getUser() {
-                  return context.getUser();
-                }
-
-                @Override
-                public Provider<ReviewDb> getReviewDbProvider() {
-                  return dbProviderProvider.get();
-                }
-              });
+      RequestContext old = local.setContext(context::getUser);
       try {
         return callable.call();
       } finally {
@@ -198,16 +177,7 @@
   protected <T> Callable<T> cleanup(Callable<T> callable) {
     return () -> {
       RequestCleanup cleanup =
-          scope
-              .scope(
-                  Key.get(RequestCleanup.class),
-                  new Provider<RequestCleanup>() {
-                    @Override
-                    public RequestCleanup get() {
-                      return new RequestCleanup();
-                    }
-                  })
-              .get();
+          scope.scope(Key.get(RequestCleanup.class), RequestCleanup::new).get();
       try {
         return callable.call();
       } finally {
diff --git a/java/com/google/gerrit/server/util/ServerRequestContext.java b/java/com/google/gerrit/server/util/ServerRequestContext.java
index af903c4..037645d 100644
--- a/java/com/google/gerrit/server/util/ServerRequestContext.java
+++ b/java/com/google/gerrit/server/util/ServerRequestContext.java
@@ -14,12 +14,9 @@
 
 package com.google.gerrit.server.util;
 
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.InternalUser;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
 
 /** RequestContext with an InternalUser making the internals visible. */
 public class ServerRequestContext implements RequestContext {
@@ -34,14 +31,4 @@
   public CurrentUser getUser() {
     return user;
   }
-
-  @Override
-  public Provider<ReviewDb> getReviewDbProvider() {
-    return new Provider<ReviewDb>() {
-      @Override
-      public ReviewDb get() {
-        throw new ProvisionException("Automatic ReviewDb only available in request scope");
-      }
-    };
-  }
 }
diff --git a/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java b/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
index e065c6b..afd699c 100644
--- a/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
+++ b/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
@@ -16,8 +16,7 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.errors.NotSignedInException;
-import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.exceptions.NotSignedInException;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.inject.AbstractModule;
@@ -63,11 +62,6 @@
         }
         throw new ProvisionException(NotSignedInException.MESSAGE, new NotSignedInException());
       }
-
-      @Provides
-      ReviewDb provideReviewDb(RequestContext ctx) {
-        return ctx.getReviewDbProvider().get();
-      }
     };
   }
 
diff --git a/java/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java b/java/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java
index 90fb994..92f5488 100644
--- a/java/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java
+++ b/java/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java
@@ -14,9 +14,7 @@
 
 package com.google.gerrit.server.util;
 
-import com.google.gerrit.server.config.RequestScopedReviewDbProvider;
 import com.google.inject.OutOfScopeException;
-import com.google.inject.Provider;
 import com.google.inject.Scope;
 import java.util.concurrent.Callable;
 
@@ -31,11 +29,8 @@
   private final ThreadLocal<C> threadLocal;
 
   protected ThreadLocalRequestScopePropagator(
-      Scope scope,
-      ThreadLocal<C> threadLocal,
-      ThreadLocalRequestContext local,
-      Provider<RequestScopedReviewDbProvider> dbProviderProvider) {
-    super(scope, local, dbProviderProvider);
+      Scope scope, ThreadLocal<C> threadLocal, ThreadLocalRequestContext local) {
+    super(scope, local);
     this.threadLocal = threadLocal;
   }
 
diff --git a/java/com/google/gerrit/sshd/BUILD b/java/com/google/gerrit/sshd/BUILD
index 99501a6..4ef70c7 100644
--- a/java/com/google/gerrit/sshd/BUILD
+++ b/java/com/google/gerrit/sshd/BUILD
@@ -7,7 +7,9 @@
     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/json",
         "//java/com/google/gerrit/lifecycle",
         "//java/com/google/gerrit/lucene",
         "//java/com/google/gerrit/metrics",
@@ -22,11 +24,9 @@
         "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/server/util/time",
         "//java/com/google/gerrit/util/cli",
-        "//java/org/eclipse/jgit:server",
         "//lib:args4j",
         "//lib:gson",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib:jsch",
         "//lib:servlet-api-3_1",
         "//lib/auto:auto-value",
diff --git a/java/com/google/gerrit/sshd/ChangeArgumentParser.java b/java/com/google/gerrit/sshd/ChangeArgumentParser.java
index ce3b8a9..ed6f87f 100644
--- a/java/com/google/gerrit/sshd/ChangeArgumentParser.java
+++ b/java/com/google/gerrit/sshd/ChangeArgumentParser.java
@@ -19,7 +19,6 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.change.ChangeFinder;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.notedb.ChangeNotes;
@@ -30,7 +29,6 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.restapi.change.ChangesCollection;
 import com.google.gerrit.sshd.BaseCommand.UnloggedFailure;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -41,7 +39,6 @@
 public class ChangeArgumentParser {
   private final ChangesCollection changesCollection;
   private final ChangeFinder changeFinder;
-  private final ReviewDb db;
   private final ChangeNotes.Factory changeNotesFactory;
   private final PermissionBackend permissionBackend;
 
@@ -49,24 +46,22 @@
   ChangeArgumentParser(
       ChangesCollection changesCollection,
       ChangeFinder changeFinder,
-      ReviewDb db,
       ChangeNotes.Factory changeNotesFactory,
       PermissionBackend permissionBackend) {
     this.changesCollection = changesCollection;
     this.changeFinder = changeFinder;
-    this.db = db;
     this.changeNotesFactory = changeNotesFactory;
     this.permissionBackend = permissionBackend;
   }
 
   public void addChange(String id, Map<Change.Id, ChangeResource> changes)
-      throws UnloggedFailure, OrmException, PermissionBackendException, IOException {
+      throws UnloggedFailure, PermissionBackendException, IOException {
     addChange(id, changes, null);
   }
 
   public void addChange(
       String id, Map<Change.Id, ChangeResource> changes, @Nullable ProjectState projectState)
-      throws UnloggedFailure, OrmException, PermissionBackendException, IOException {
+      throws UnloggedFailure, PermissionBackendException, IOException {
     addChange(id, changes, projectState, true);
   }
 
@@ -75,7 +70,7 @@
       Map<Change.Id, ChangeResource> changes,
       @Nullable ProjectState projectState,
       boolean useIndex)
-      throws UnloggedFailure, OrmException, PermissionBackendException, IOException {
+      throws UnloggedFailure, PermissionBackendException, IOException {
     List<ChangeNotes> matched = useIndex ? changeFinder.find(id) : changeFromNotesFactory(id);
     List<ChangeNotes> toAdd = new ArrayList<>(changes.size());
     boolean canMaintainServer;
@@ -98,7 +93,7 @@
         }
 
         try {
-          permissionBackend.currentUser().change(notes).database(db).check(ChangePermission.READ);
+          permissionBackend.currentUser().change(notes).check(ChangePermission.READ);
           toAdd.add(notes);
         } catch (AuthException e) {
           // Do nothing.
@@ -121,8 +116,8 @@
     changes.put(cId, changeResource);
   }
 
-  private List<ChangeNotes> changeFromNotesFactory(String id) throws OrmException, UnloggedFailure {
-    return changeNotesFactory.create(db, parseId(id));
+  private List<ChangeNotes> changeFromNotesFactory(String id) throws UnloggedFailure {
+    return changeNotesFactory.create(parseId(id));
   }
 
   private List<Change.Id> parseId(String id) throws UnloggedFailure {
diff --git a/java/com/google/gerrit/sshd/CommandFactoryProvider.java b/java/com/google/gerrit/sshd/CommandFactoryProvider.java
index 3fb2ed4..245dd60 100644
--- a/java/com/google/gerrit/sshd/CommandFactoryProvider.java
+++ b/java/com/google/gerrit/sshd/CommandFactoryProvider.java
@@ -19,12 +19,10 @@
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.registration.DynamicItem;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.logging.LoggingContextAwareExecutorService;
 import com.google.gerrit.sshd.SshScope.Context;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -57,7 +55,6 @@
   private final SshScope sshScope;
   private final ScheduledExecutorService startExecutor;
   private final ExecutorService destroyExecutor;
-  private final SchemaFactory<ReviewDb> schemaFactory;
   private final DynamicItem<SshCreateCommandInterceptor> createCommandInterceptor;
 
   @Inject
@@ -67,12 +64,10 @@
       WorkQueue workQueue,
       SshLog l,
       SshScope s,
-      SchemaFactory<ReviewDb> sf,
       DynamicItem<SshCreateCommandInterceptor> i) {
     dispatcher = d;
     log = l;
     sshScope = s;
-    schemaFactory = sf;
     createCommandInterceptor = i;
 
     int threads = cfg.getInt("sshd", "commandStartThreads", 2);
@@ -96,16 +91,13 @@
 
   @Override
   public CommandFactory get() {
-    return new CommandFactory() {
-      @Override
-      public Command createCommand(String requestCommand) {
-        String c = requestCommand;
-        SshCreateCommandInterceptor interceptor = createCommandInterceptor.get();
-        if (interceptor != null) {
-          c = interceptor.intercept(c);
-        }
-        return new Trampoline(c);
+    return requestCommand -> {
+      String c = requestCommand;
+      SshCreateCommandInterceptor interceptor = createCommandInterceptor.get();
+      if (interceptor != null) {
+        c = interceptor.intercept(c);
       }
+      return new Trampoline(c);
     };
   }
 
@@ -152,7 +144,7 @@
     @Override
     public void setSession(ServerSession session) {
       final SshSession s = session.getAttribute(SshSession.KEY);
-      this.ctx = sshScope.newContext(schemaFactory, s, commandLine);
+      this.ctx = sshScope.newContext(s, commandLine);
     }
 
     @Override
diff --git a/java/com/google/gerrit/sshd/DispatchCommandProvider.java b/java/com/google/gerrit/sshd/DispatchCommandProvider.java
index 7ff7224..2a65ed0 100644
--- a/java/com/google/gerrit/sshd/DispatchCommandProvider.java
+++ b/java/com/google/gerrit/sshd/DispatchCommandProvider.java
@@ -50,24 +50,14 @@
     if (m.putIfAbsent(name.value(), commandProvider) != null) {
       throw new IllegalArgumentException(name.value() + " exists");
     }
-    return new RegistrationHandle() {
-      @Override
-      public void remove() {
-        m.remove(name.value(), commandProvider);
-      }
-    };
+    return () -> m.remove(name.value(), commandProvider);
   }
 
   public RegistrationHandle replace(CommandName name, Provider<Command> cmd) {
     final ConcurrentMap<String, CommandProvider> m = getMap();
     final CommandProvider commandProvider = new CommandProvider(cmd, null);
     m.put(name.value(), commandProvider);
-    return new RegistrationHandle() {
-      @Override
-      public void remove() {
-        m.remove(name.value(), commandProvider);
-      }
-    };
+    return () -> m.remove(name.value(), commandProvider);
   }
 
   ConcurrentMap<String, CommandProvider> getMap() {
diff --git a/java/com/google/gerrit/sshd/NoShell.java b/java/com/google/gerrit/sshd/NoShell.java
index 0235554..5aaa647 100644
--- a/java/com/google/gerrit/sshd/NoShell.java
+++ b/java/com/google/gerrit/sshd/NoShell.java
@@ -15,13 +15,11 @@
 package com.google.gerrit.sshd;
 
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gerrit.sshd.SshScope.Context;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -59,7 +57,6 @@
 
   static class SendMessage implements Command, SessionAware {
     private final Provider<MessageFactory> messageFactory;
-    private final SchemaFactory<ReviewDb> schemaFactory;
     private final SshScope sshScope;
 
     private InputStream in;
@@ -69,10 +66,8 @@
     private Context context;
 
     @Inject
-    SendMessage(
-        Provider<MessageFactory> messageFactory, SchemaFactory<ReviewDb> sf, SshScope sshScope) {
+    SendMessage(Provider<MessageFactory> messageFactory, SshScope sshScope) {
       this.messageFactory = messageFactory;
-      this.schemaFactory = sf;
       this.sshScope = sshScope;
     }
 
@@ -99,7 +94,7 @@
     @Override
     public void setSession(ServerSession session) {
       SshSession s = session.getAttribute(SshSession.KEY);
-      this.context = sshScope.newContext(schemaFactory, s, "");
+      this.context = sshScope.newContext(s, "");
     }
 
     @Override
diff --git a/java/com/google/gerrit/sshd/SshCommand.java b/java/com/google/gerrit/sshd/SshCommand.java
index 47c6098..9828957 100644
--- a/java/com/google/gerrit/sshd/SshCommand.java
+++ b/java/com/google/gerrit/sshd/SshCommand.java
@@ -34,18 +34,15 @@
   @Override
   public void start(Environment env) throws IOException {
     startThread(
-        new CommandRunnable() {
-          @Override
-          public void run() throws Exception {
-            parseCommandLine();
-            stdout = toPrintWriter(out);
-            stderr = toPrintWriter(err);
-            try (TraceContext traceContext = enableTracing()) {
-              SshCommand.this.run();
-            } finally {
-              stdout.flush();
-              stderr.flush();
-            }
+        () -> {
+          parseCommandLine();
+          stdout = toPrintWriter(out);
+          stderr = toPrintWriter(err);
+          try (TraceContext traceContext = enableTracing()) {
+            SshCommand.this.run();
+          } finally {
+            stdout.flush();
+            stderr.flush();
           }
         },
         AccessPath.SSH_COMMAND);
diff --git a/java/com/google/gerrit/sshd/SshDaemon.java b/java/com/google/gerrit/sshd/SshDaemon.java
index 433e0e9..69176a2 100644
--- a/java/com/google/gerrit/sshd/SshDaemon.java
+++ b/java/com/google/gerrit/sshd/SshDaemon.java
@@ -66,14 +66,10 @@
 import org.apache.mina.transport.socket.SocketSessionConfig;
 import org.apache.sshd.common.BaseBuilder;
 import org.apache.sshd.common.NamedFactory;
-import org.apache.sshd.common.channel.RequestHandler;
 import org.apache.sshd.common.cipher.Cipher;
 import org.apache.sshd.common.compression.BuiltinCompressions;
 import org.apache.sshd.common.compression.Compression;
-import org.apache.sshd.common.file.FileSystemFactory;
 import org.apache.sshd.common.forward.DefaultForwarderFactory;
-import org.apache.sshd.common.future.CloseFuture;
-import org.apache.sshd.common.future.SshFutureListener;
 import org.apache.sshd.common.io.AbstractIoServiceFactory;
 import org.apache.sshd.common.io.IoAcceptor;
 import org.apache.sshd.common.io.IoServiceFactory;
@@ -87,7 +83,6 @@
 import org.apache.sshd.common.mac.Mac;
 import org.apache.sshd.common.random.Random;
 import org.apache.sshd.common.random.SingletonRandomFactory;
-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;
@@ -101,7 +96,6 @@
 import org.apache.sshd.server.auth.gss.UserAuthGSSFactory;
 import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator;
 import org.apache.sshd.server.auth.pubkey.UserAuthPublicKeyFactory;
-import org.apache.sshd.server.command.Command;
 import org.apache.sshd.server.command.CommandFactory;
 import org.apache.sshd.server.forward.ForwardingFilter;
 import org.apache.sshd.server.global.CancelTcpipForwardHandler;
@@ -276,14 +270,11 @@
             // Log a session close without authentication as a failure.
             //
             s.addCloseFutureListener(
-                new SshFutureListener<CloseFuture>() {
-                  @Override
-                  public void operationComplete(CloseFuture future) {
-                    connected.decrementAndGet();
-                    if (sd.isAuthenticationError()) {
-                      authFailures.increment();
-                      sshLog.onAuthFail(sd);
-                    }
+                future -> {
+                  connected.decrementAndGet();
+                  if (sd.isAuthenticationError()) {
+                    authFailures.increment();
+                    sshLog.onAuthFail(sd);
                   }
                 });
             return s;
@@ -295,7 +286,7 @@
           }
         });
     setGlobalRequestHandlers(
-        Arrays.<RequestHandler<ConnectionService>>asList(
+        Arrays.asList(
             new KeepAliveHandler(),
             new NoMoreSessionsHandler(),
             new TcpipForwardHandler(),
@@ -660,7 +651,7 @@
   }
 
   private void initSubsystems() {
-    setSubsystemFactories(Collections.<NamedFactory<Command>>emptyList());
+    setSubsystemFactories(Collections.emptyList());
   }
 
   private void initUserAuth(
@@ -727,10 +718,8 @@
 
   private void initFileSystemFactory() {
     setFileSystemFactory(
-        new FileSystemFactory() {
-          @Override
-          public FileSystem createFileSystem(Session session) throws IOException {
-            return new FileSystem() {
+        session ->
+            new FileSystem() {
               @Override
               public void close() throws IOException {}
 
@@ -788,8 +777,6 @@
               public Set<String> supportedFileAttributeViews() {
                 return null;
               }
-            };
-          }
-        });
+            });
   }
 }
diff --git a/java/com/google/gerrit/sshd/SshKeyCreatorImpl.java b/java/com/google/gerrit/sshd/SshKeyCreatorImpl.java
index e2fbdcb..b2853b9 100644
--- a/java/com/google/gerrit/sshd/SshKeyCreatorImpl.java
+++ b/java/com/google/gerrit/sshd/SshKeyCreatorImpl.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.sshd;
 
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.errors.InvalidSshKeyException;
+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;
diff --git a/java/com/google/gerrit/sshd/SshModule.java b/java/com/google/gerrit/sshd/SshModule.java
index df1f633..e4aa14c 100644
--- a/java/com/google/gerrit/sshd/SshModule.java
+++ b/java/com/google/gerrit/sshd/SshModule.java
@@ -20,10 +20,8 @@
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Splitter;
 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.lifecycle.LifecycleModule;
-import com.google.gerrit.server.DynamicOptions;
 import com.google.gerrit.server.PeerDaemonUser;
 import com.google.gerrit.server.RemotePeer;
 import com.google.gerrit.server.config.GerritConfigListener;
@@ -36,7 +34,6 @@
 import com.google.gerrit.server.plugins.StartPluginListener;
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gerrit.server.util.RequestScopePropagator;
-import com.google.gerrit.sshd.commands.QueryShell;
 import com.google.inject.Inject;
 import com.google.inject.internal.UniqueAnnotations;
 import com.google.inject.servlet.RequestScoped;
@@ -77,7 +74,6 @@
 
     bind(SshInfo.class).to(SshDaemon.class).in(SINGLETON);
     factory(DispatchCommand.Factory.class);
-    factory(QueryShell.Factory.class);
     factory(PeerDaemonUser.Factory.class);
 
     bind(DispatchCommandProvider.class)
@@ -104,7 +100,6 @@
         .annotatedWith(UniqueAnnotations.create())
         .to(SshPluginStarterCallback.class);
 
-    DynamicMap.mapOf(binder(), DynamicOptions.DynamicBean.class);
     DynamicItem.itemOf(binder(), SshCreateCommandInterceptor.class);
     DynamicSet.setOf(binder(), SshExecuteCommandInterceptor.class);
 
diff --git a/java/com/google/gerrit/sshd/SshScope.java b/java/com/google/gerrit/sshd/SshScope.java
index 99787d8..340b910 100644
--- a/java/com/google/gerrit/sshd/SshScope.java
+++ b/java/com/google/gerrit/sshd/SshScope.java
@@ -14,22 +14,18 @@
 
 package com.google.gerrit.sshd;
 
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.RequestCleanup;
-import com.google.gerrit.server.config.RequestScopedReviewDbProvider;
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestScopePropagator;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Key;
 import com.google.inject.OutOfScopeException;
 import com.google.inject.Provider;
 import com.google.inject.Scope;
-import com.google.inject.util.Providers;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -37,13 +33,9 @@
 public class SshScope {
   private static final Key<RequestCleanup> RC_KEY = Key.get(RequestCleanup.class);
 
-  private static final Key<RequestScopedReviewDbProvider> DB_KEY =
-      Key.get(RequestScopedReviewDbProvider.class);
-
   class Context implements RequestContext {
     private final RequestCleanup cleanup = new RequestCleanup();
     private final Map<Key<?>, Object> map = new HashMap<>();
-    private final SchemaFactory<ReviewDb> schemaFactory;
     private final SshSession session;
     private final String commandLine;
 
@@ -51,17 +43,15 @@
     volatile long started;
     volatile long finished;
 
-    private Context(SchemaFactory<ReviewDb> sf, SshSession s, String c, long at) {
-      schemaFactory = sf;
+    private Context(SshSession s, String c, long at) {
       session = s;
       commandLine = c;
       created = started = finished = at;
       map.put(RC_KEY, cleanup);
-      map.put(DB_KEY, new RequestScopedReviewDbProvider(schemaFactory, Providers.of(cleanup)));
     }
 
     private Context(Context p, SshSession s, String c) {
-      this(p.schemaFactory, s, c, p.created);
+      this(s, c, p.created);
       started = p.started;
       finished = p.finished;
     }
@@ -85,11 +75,6 @@
       return user;
     }
 
-    @Override
-    public Provider<ReviewDb> getReviewDbProvider() {
-      return (RequestScopedReviewDbProvider) map.get(DB_KEY);
-    }
-
     synchronized <T> T get(Key<T> key, Provider<T> creator) {
       @SuppressWarnings("unchecked")
       T t = (T) map.get(key);
@@ -125,11 +110,8 @@
     private final SshScope sshScope;
 
     @Inject
-    Propagator(
-        SshScope sshScope,
-        ThreadLocalRequestContext local,
-        Provider<RequestScopedReviewDbProvider> dbProviderProvider) {
-      super(REQUEST, current, local, dbProviderProvider);
+    Propagator(SshScope sshScope, ThreadLocalRequestContext local) {
+      super(REQUEST, current, local);
       this.sshScope = sshScope;
     }
 
@@ -160,8 +142,8 @@
     this.userFactory = userFactory;
   }
 
-  Context newContext(SchemaFactory<ReviewDb> sf, SshSession s, String cmd) {
-    return new Context(sf, s, cmd, TimeUtil.nowMs());
+  Context newContext(SshSession s, String cmd) {
+    return new Context(s, cmd, TimeUtil.nowMs());
   }
 
   private Context newContinuingContext(Context ctx) {
diff --git a/java/com/google/gerrit/sshd/SshUtil.java b/java/com/google/gerrit/sshd/SshUtil.java
index ce35422..670d64c 100644
--- a/java/com/google/gerrit/sshd/SshUtil.java
+++ b/java/com/google/gerrit/sshd/SshUtil.java
@@ -30,8 +30,6 @@
 import java.security.spec.InvalidKeySpecException;
 import org.apache.commons.codec.binary.Base64;
 import org.apache.sshd.common.SshException;
-import org.apache.sshd.common.future.CloseFuture;
-import org.apache.sshd.common.future.SshFutureListener;
 import org.apache.sshd.common.keyprovider.KeyPairProvider;
 import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
 import org.apache.sshd.server.session.ServerSession;
@@ -127,7 +125,7 @@
       // session, record a login event in the log and add
       // a close listener to record a logout event.
       //
-      Context ctx = sshScope.newContext(null, sd, null);
+      Context ctx = sshScope.newContext(sd, null);
       Context old = sshScope.set(ctx);
       try {
         sshLog.onLogin();
@@ -136,16 +134,13 @@
       }
 
       session.addCloseFutureListener(
-          new SshFutureListener<CloseFuture>() {
-            @Override
-            public void operationComplete(CloseFuture future) {
-              final Context ctx = sshScope.newContext(null, sd, null);
-              final Context old = sshScope.set(ctx);
-              try {
-                sshLog.onLogout();
-              } finally {
-                sshScope.set(old);
-              }
+          future -> {
+            final Context ctx1 = sshScope.newContext(sd, null);
+            final Context old1 = sshScope.set(ctx1);
+            try {
+              sshLog.onLogout();
+            } finally {
+              sshScope.set(old1);
             }
           });
     }
diff --git a/java/com/google/gerrit/sshd/commands/AdminQueryShell.java b/java/com/google/gerrit/sshd/commands/AdminQueryShell.java
deleted file mode 100644
index c520e79..0000000
--- a/java/com/google/gerrit/sshd/commands/AdminQueryShell.java
+++ /dev/null
@@ -1,61 +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.sshd.commands;
-
-import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.extensions.annotations.RequiresCapability;
-import com.google.gerrit.extensions.restapi.AuthException;
-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.sshd.AdminHighPriorityCommand;
-import com.google.gerrit.sshd.CommandMetaData;
-import com.google.gerrit.sshd.SshCommand;
-import com.google.inject.Inject;
-import org.kohsuke.args4j.Option;
-
-/** Opens a query processor. */
-@AdminHighPriorityCommand
-@RequiresCapability(GlobalCapability.ACCESS_DATABASE)
-@CommandMetaData(name = "gsql", description = "Administrative interface to active database")
-final class AdminQueryShell extends SshCommand {
-  @Inject private PermissionBackend permissionBackend;
-  @Inject private QueryShell.Factory factory;
-
-  @Option(name = "--format", usage = "Set output format")
-  private QueryShell.OutputFormat format = QueryShell.OutputFormat.PRETTY;
-
-  @Option(name = "-c", metaVar = "SQL QUERY", usage = "Query to execute")
-  private String query;
-
-  @Override
-  protected void run() throws Failure {
-    try {
-      permissionBackend.currentUser().check(GlobalPermission.ACCESS_DATABASE);
-    } catch (AuthException err) {
-      throw die(err.getMessage());
-    } catch (PermissionBackendException e) {
-      throw new Failure(1, "unavailable", e);
-    }
-
-    QueryShell shell = factory.create(in, out);
-    shell.setOutputFormat(format);
-    if (query != null) {
-      shell.execute(query);
-    } else {
-      shell.run();
-    }
-  }
-}
diff --git a/java/com/google/gerrit/sshd/commands/ApproveOption.java b/java/com/google/gerrit/sshd/commands/ApproveOption.java
deleted file mode 100644
index cda340d..0000000
--- a/java/com/google/gerrit/sshd/commands/ApproveOption.java
+++ /dev/null
@@ -1,170 +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.sshd.commands;
-
-import static com.google.gerrit.util.cli.Localizable.localizable;
-
-import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.LabelValue;
-import java.lang.annotation.Annotation;
-import java.lang.reflect.AnnotatedElement;
-import org.kohsuke.args4j.CmdLineException;
-import org.kohsuke.args4j.CmdLineParser;
-import org.kohsuke.args4j.Option;
-import org.kohsuke.args4j.OptionDef;
-import org.kohsuke.args4j.spi.FieldSetter;
-import org.kohsuke.args4j.spi.OneArgumentOptionHandler;
-import org.kohsuke.args4j.spi.OptionHandler;
-import org.kohsuke.args4j.spi.Setter;
-
-final class ApproveOption implements Option, Setter<Short> {
-  private final String name;
-  private final String usage;
-  private final LabelType type;
-
-  private Short value;
-
-  ApproveOption(String name, String usage, LabelType type) {
-    this.name = name;
-    this.usage = usage;
-    this.type = type;
-  }
-
-  @Override
-  public String[] aliases() {
-    return new String[0];
-  }
-
-  @Override
-  public String[] depends() {
-    return new String[] {};
-  }
-
-  @Override
-  public boolean hidden() {
-    return false;
-  }
-
-  @Override
-  public Class<? extends OptionHandler<Short>> handler() {
-    return Handler.class;
-  }
-
-  @Override
-  public String metaVar() {
-    return "N";
-  }
-
-  @Override
-  public String name() {
-    return name;
-  }
-
-  @Override
-  public boolean required() {
-    return false;
-  }
-
-  @Override
-  public String usage() {
-    return usage;
-  }
-
-  public Short value() {
-    return value;
-  }
-
-  @Override
-  public Class<? extends Annotation> annotationType() {
-    return null;
-  }
-
-  @Override
-  public FieldSetter asFieldSetter() {
-    throw new UnsupportedOperationException();
-  }
-
-  @Override
-  public AnnotatedElement asAnnotatedElement() {
-    throw new UnsupportedOperationException();
-  }
-
-  @Override
-  public void addValue(Short val) {
-    this.value = val;
-  }
-
-  @Override
-  public Class<Short> getType() {
-    return Short.class;
-  }
-
-  @Override
-  public boolean isMultiValued() {
-    return false;
-  }
-
-  @Override
-  public String[] forbids() {
-    return null;
-  }
-
-  @Override
-  public boolean help() {
-    return false;
-  }
-
-  String getLabelName() {
-    return type.getName();
-  }
-
-  public static class Handler extends OneArgumentOptionHandler<Short> {
-    private final ApproveOption cmdOption;
-
-    // CS IGNORE RedundantModifier FOR NEXT 1 LINES. REASON: needed by org.kohsuke.args4j.Option
-    public Handler(CmdLineParser parser, OptionDef option, Setter<Short> setter) {
-      super(parser, option, setter);
-      this.cmdOption = (ApproveOption) setter;
-    }
-
-    @Override
-    protected Short parse(String token) throws NumberFormatException, CmdLineException {
-      String argument = token;
-      if (argument.startsWith("+")) {
-        argument = argument.substring(1);
-      }
-
-      final short value = Short.parseShort(argument);
-      final LabelValue min = cmdOption.type.getMin();
-      final LabelValue max = cmdOption.type.getMax();
-
-      if (value < min.getValue() || value > max.getValue()) {
-        final String name = cmdOption.name();
-        final String e =
-            "\""
-                + token
-                + "\" must be in range "
-                + min.formatValue()
-                + ".."
-                + max.formatValue()
-                + " for \""
-                + name
-                + "\"";
-        throw new CmdLineException(owner, localizable(e));
-      }
-      return value;
-    }
-  }
-}
diff --git a/java/com/google/gerrit/sshd/commands/BaseTestPrologCommand.java b/java/com/google/gerrit/sshd/commands/BaseTestPrologCommand.java
index affb919..d70c153 100644
--- a/java/com/google/gerrit/sshd/commands/BaseTestPrologCommand.java
+++ b/java/com/google/gerrit/sshd/commands/BaseTestPrologCommand.java
@@ -19,7 +19,7 @@
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
-import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.json.OutputFormat;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.restapi.change.ChangesCollection;
 import com.google.gerrit.server.restapi.change.Revisions;
diff --git a/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java b/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
index 8a37cce..8875f07 100644
--- a/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
+++ b/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
@@ -28,7 +28,6 @@
 import com.google.gerrit.server.restapi.account.CreateAccount;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.BufferedReader;
 import java.io.IOException;
@@ -72,8 +71,7 @@
 
   @Override
   protected void run()
-      throws OrmException, IOException, ConfigInvalidException, UnloggedFailure,
-          PermissionBackendException {
+      throws IOException, ConfigInvalidException, UnloggedFailure, PermissionBackendException {
     AccountInput input = new AccountInput();
     input.username = username;
     input.email = email;
diff --git a/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java b/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
index 917c138..f9a04a0 100644
--- a/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
+++ b/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
@@ -33,7 +33,6 @@
 import com.google.gerrit.server.restapi.group.GroupsCollection;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.HashSet;
@@ -102,8 +101,7 @@
 
   @Override
   protected void run()
-      throws Failure, OrmException, IOException, ConfigInvalidException,
-          PermissionBackendException {
+      throws Failure, IOException, ConfigInvalidException, PermissionBackendException {
     try {
       GroupResource rsrc = createGroup();
 
@@ -120,8 +118,7 @@
   }
 
   private GroupResource createGroup()
-      throws RestApiException, OrmException, IOException, ConfigInvalidException,
-          PermissionBackendException {
+      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
     GroupInput input = new GroupInput();
     input.description = groupDescription;
     input.visibleToAll = visibleToAll;
@@ -136,8 +133,7 @@
   }
 
   private void addMembers(GroupResource rsrc)
-      throws RestApiException, OrmException, IOException, ConfigInvalidException,
-          PermissionBackendException {
+      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
     AddMembers.Input input =
         AddMembers.Input.fromMembers(
             initialMembers.stream().map(Object::toString).collect(toList()));
@@ -145,8 +141,7 @@
   }
 
   private void addSubgroups(GroupResource rsrc)
-      throws RestApiException, OrmException, IOException, ConfigInvalidException,
-          PermissionBackendException {
+      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
     AddSubgroups.Input input =
         AddSubgroups.Input.fromGroups(
             initialGroups.stream().map(AccountGroup.UUID::get).collect(toList()));
diff --git a/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java b/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
index 1c857e4..87b6f02 100644
--- a/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
+++ b/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
@@ -90,7 +90,6 @@
     command(gerrit, CreateGroupCommand.class);
     command(gerrit, CreateProjectCommand.class);
     command(gerrit, SetHeadCommand.class);
-    command(gerrit, AdminQueryShell.class);
 
     if (slaveMode) {
       command("git-receive-pack").to(ReceiveSlaveMode.class);
diff --git a/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java b/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
index fad74f5..5aa2ec8 100644
--- a/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
+++ b/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.sshd.commands;
 
+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;
@@ -22,7 +23,6 @@
 import com.google.gerrit.sshd.ChangeArgumentParser;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.LinkedHashMap;
@@ -44,7 +44,7 @@
   void addChange(String token) {
     try {
       changeArgumentParser.addChange(token, changes, null, false);
-    } catch (UnloggedFailure | OrmException | PermissionBackendException | IOException e) {
+    } catch (UnloggedFailure | StorageException | PermissionBackendException | IOException e) {
       writeError("warning", e.getMessage());
     }
   }
diff --git a/java/com/google/gerrit/sshd/commands/LsUserRefs.java b/java/com/google/gerrit/sshd/commands/LsUserRefs.java
index f68e34c..f26a4fa 100644
--- a/java/com/google/gerrit/sshd/commands/LsUserRefs.java
+++ b/java/com/google/gerrit/sshd/commands/LsUserRefs.java
@@ -15,10 +15,11 @@
 package com.google.gerrit.sshd.commands;
 
 import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
-import static org.eclipse.jgit.lib.RefDatabase.ALL;
 
 import com.google.gerrit.common.data.GlobalCapability;
+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;
@@ -32,7 +33,6 @@
 import com.google.gerrit.server.util.OneOffRequestContext;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.Map;
@@ -74,27 +74,26 @@
 
   @Override
   protected void run() throws Failure {
-    Account userAccount;
+    Account.Id userAccountId;
     try {
-      userAccount = accountResolver.find(userName);
-    } catch (OrmException | IOException | ConfigInvalidException e) {
-      throw die(e);
-    }
-    if (userAccount == null) {
-      stdout.print("No single user could be found when searching for: " + userName + '\n');
+      userAccountId = accountResolver.resolve(userName).asUnique().getAccount().getId();
+    } catch (UnprocessableEntityException e) {
+      stdout.println(e.getMessage());
       stdout.flush();
       return;
+    } catch (StorageException | IOException | ConfigInvalidException e) {
+      throw die(e);
     }
 
     Project.NameKey projectName = projectState.getNameKey();
     try (Repository repo = repoManager.openRepository(projectName);
-        ManualRequestContext ctx = requestContext.openAs(userAccount.getId())) {
+        ManualRequestContext ctx = requestContext.openAs(userAccountId)) {
       try {
         Map<String, Ref> refsMap =
             permissionBackend
                 .user(ctx.getUser())
                 .project(projectName)
-                .filter(repo.getRefDatabase().getRefs(ALL), repo, RefFilterOptions.defaults());
+                .filter(repo.getRefDatabase().getRefs(), repo, RefFilterOptions.defaults());
 
         for (String ref : refsMap.keySet()) {
           if (!onlyRefsHeads || ref.startsWith(RefNames.REFS_HEADS)) {
@@ -106,7 +105,7 @@
       }
     } catch (RepositoryNotFoundException e) {
       throw die("'" + projectName + "': not a git archive", e);
-    } catch (IOException | OrmException e) {
+    } catch (IOException e) {
       throw die("Error opening: '" + projectName, e);
     }
   }
diff --git a/java/com/google/gerrit/sshd/commands/PatchSetParser.java b/java/com/google/gerrit/sshd/commands/PatchSetParser.java
index 2dd035c..45210f6 100644
--- a/java/com/google/gerrit/sshd/commands/PatchSetParser.java
+++ b/java/com/google/gerrit/sshd/commands/PatchSetParser.java
@@ -19,7 +19,6 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.ChangeFinder;
 import com.google.gerrit.server.notedb.ChangeNotes;
@@ -28,7 +27,6 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.sshd.BaseCommand.UnloggedFailure;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -37,7 +35,6 @@
 
 @Singleton
 public class PatchSetParser {
-  private final Provider<ReviewDb> db;
   private final Provider<InternalChangeQuery> queryProvider;
   private final ChangeNotes.Factory notesFactory;
   private final PatchSetUtil psUtil;
@@ -45,12 +42,10 @@
 
   @Inject
   PatchSetParser(
-      Provider<ReviewDb> db,
       Provider<InternalChangeQuery> queryProvider,
       ChangeNotes.Factory notesFactory,
       PatchSetUtil psUtil,
       ChangeFinder changeFinder) {
-    this.db = db;
     this.queryProvider = queryProvider;
     this.notesFactory = notesFactory;
     this.psUtil = psUtil;
@@ -58,7 +53,7 @@
   }
 
   public PatchSet parsePatchSet(String token, ProjectState projectState, String branch)
-      throws UnloggedFailure, OrmException {
+      throws UnloggedFailure {
     // By commit?
     //
     if (token.matches("^([0-9a-fA-F]{4," + RevId.LEN + "})$")) {
@@ -107,7 +102,7 @@
         throw error("\"" + token + "\" is not a valid patch set", e);
       }
       ChangeNotes notes = getNotes(projectState, patchSetId.getParentKey());
-      PatchSet patchSet = psUtil.get(db.get(), notes, patchSetId);
+      PatchSet patchSet = psUtil.get(notes, patchSetId);
       if (patchSet == null) {
         throw error("\"" + token + "\" no such patch set");
       }
@@ -127,13 +122,13 @@
   }
 
   private ChangeNotes getNotes(@Nullable ProjectState projectState, Change.Id changeId)
-      throws OrmException, UnloggedFailure {
+      throws UnloggedFailure {
     if (projectState != null) {
-      return notesFactory.create(db.get(), projectState.getNameKey(), changeId);
+      return notesFactory.create(projectState.getNameKey(), changeId);
     }
     try {
       ChangeNotes notes = changeFinder.findOne(changeId);
-      return notesFactory.create(db.get(), notes.getProjectName(), changeId);
+      return notesFactory.create(notes.getProjectName(), changeId);
     } catch (NoSuchChangeException e) {
       throw error("\"" + changeId + "\" no such change", e);
     }
diff --git a/java/com/google/gerrit/sshd/commands/PluginLsCommand.java b/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
index ceb5f07..15142bd 100644
--- a/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
+++ b/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
@@ -21,7 +21,7 @@
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.common.PluginInfo;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
-import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.json.OutputFormat;
 import com.google.gerrit.server.plugins.ListPlugins;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
diff --git a/java/com/google/gerrit/sshd/commands/Query.java b/java/com/google/gerrit/sshd/commands/Query.java
index 4d8351e..78485d3 100644
--- a/java/com/google/gerrit/sshd/commands/Query.java
+++ b/java/com/google/gerrit/sshd/commands/Query.java
@@ -91,6 +91,11 @@
     processor.setStart(start);
   }
 
+  @Option(name = "--no-limit", usage = "Return all results, overriding the default limit")
+  void setNoLimit(boolean on) {
+    processor.setNoLimit(on);
+  }
+
   @Argument(
       index = 0,
       required = true,
diff --git a/java/com/google/gerrit/sshd/commands/QueryShell.java b/java/com/google/gerrit/sshd/commands/QueryShell.java
deleted file mode 100644
index fe102ab..0000000
--- a/java/com/google/gerrit/sshd/commands/QueryShell.java
+++ /dev/null
@@ -1,781 +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.sshd.commands;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.gerrit.common.Version;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.reviewdb.server.ReviewDbUtil;
-import com.google.gerrit.server.schema.ReviewDbFactory;
-import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gson.JsonArray;
-import com.google.gson.JsonObject;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.PrintWriter;
-import java.sql.Connection;
-import java.sql.DatabaseMetaData;
-import java.sql.ResultSet;
-import java.sql.ResultSetMetaData;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.TreeMap;
-
-/** Simple interactive SQL query tool. */
-public class QueryShell {
-  public interface Factory {
-    QueryShell create(@Assisted InputStream in, @Assisted OutputStream out);
-  }
-
-  public enum OutputFormat {
-    PRETTY,
-    JSON,
-    JSON_SINGLE
-  }
-
-  private final BufferedReader in;
-  private final PrintWriter out;
-  private final SchemaFactory<ReviewDb> dbFactory;
-  private OutputFormat outputFormat = OutputFormat.PRETTY;
-
-  private ReviewDb db;
-  private Connection connection;
-  private Statement statement;
-
-  @Inject
-  QueryShell(
-      @ReviewDbFactory final SchemaFactory<ReviewDb> dbFactory,
-      @Assisted final InputStream in,
-      @Assisted final OutputStream out) {
-    this.dbFactory = dbFactory;
-    this.in = new BufferedReader(new InputStreamReader(in, UTF_8));
-    this.out = new PrintWriter(new OutputStreamWriter(out, UTF_8));
-  }
-
-  public void setOutputFormat(OutputFormat fmt) {
-    outputFormat = fmt;
-  }
-
-  public void run() {
-    try {
-      db = ReviewDbUtil.unwrapDb(dbFactory.open());
-      try {
-        connection = ((JdbcSchema) db).getConnection();
-        connection.setAutoCommit(true);
-
-        statement = connection.createStatement();
-        try {
-          showBanner();
-          readEvalPrintLoop();
-        } finally {
-          statement.close();
-          statement = null;
-        }
-      } finally {
-        db.close();
-        db = null;
-      }
-    } catch (OrmException | SQLException err) {
-      out.println("fatal: Cannot open connection: " + err.getMessage());
-    } finally {
-      out.flush();
-    }
-  }
-
-  public void execute(String query) {
-    try {
-      db = dbFactory.open();
-      try {
-        connection = ((JdbcSchema) db).getConnection();
-        connection.setAutoCommit(true);
-
-        statement = connection.createStatement();
-        try {
-          executeStatement(query);
-        } finally {
-          statement.close();
-          statement = null;
-        }
-      } finally {
-        db.close();
-        db = null;
-      }
-    } catch (OrmException | SQLException err) {
-      out.println("fatal: Cannot open connection: " + err.getMessage());
-    } finally {
-      out.flush();
-    }
-  }
-
-  private void readEvalPrintLoop() {
-    final StringBuilder buffer = new StringBuilder();
-    boolean executed = false;
-    for (; ; ) {
-      if (outputFormat == OutputFormat.PRETTY) {
-        print(buffer.length() == 0 || executed ? "gerrit> " : "     -> ");
-      }
-      String line = readLine();
-      if (line == null) {
-        return;
-      }
-
-      if (line.startsWith("\\")) {
-        // Shell command, check the various cases we recognize
-        //
-        line = line.substring(1);
-        if (line.equals("h") || line.equals("?")) {
-          showHelp();
-
-        } else if (line.equals("q")) {
-          if (outputFormat == OutputFormat.PRETTY) {
-            println("Bye");
-          }
-          return;
-
-        } else if (line.equals("r")) {
-          buffer.setLength(0);
-          executed = false;
-
-        } else if (line.equals("p")) {
-          println(buffer.toString());
-
-        } else if (line.equals("g")) {
-          if (buffer.length() > 0) {
-            executeStatement(buffer.toString());
-            executed = true;
-          }
-
-        } else if (line.equals("d")) {
-          listTables();
-
-        } else if (line.startsWith("d ")) {
-          showTable(line.substring(2).trim());
-
-        } else {
-          final String msg = "'\\" + line + "' not supported";
-          switch (outputFormat) {
-            case JSON_SINGLE:
-            case JSON:
-              {
-                final JsonObject err = new JsonObject();
-                err.addProperty("type", "error");
-                err.addProperty("message", msg);
-                println(err.toString());
-                break;
-              }
-            case PRETTY:
-            default:
-              println("ERROR: " + msg);
-              println("");
-              showHelp();
-              break;
-          }
-        }
-        continue;
-      }
-
-      if (executed) {
-        buffer.setLength(0);
-        executed = false;
-      }
-      if (buffer.length() > 0) {
-        buffer.append('\n');
-      }
-      buffer.append(line);
-
-      if (buffer.length() > 0 && buffer.charAt(buffer.length() - 1) == ';') {
-        executeStatement(buffer.toString());
-        executed = true;
-      }
-    }
-  }
-
-  private void listTables() {
-    final DatabaseMetaData meta;
-    try {
-      meta = connection.getMetaData();
-    } catch (SQLException e) {
-      error(e);
-      return;
-    }
-
-    final String[] types = {"TABLE", "VIEW"};
-    try (ResultSet rs = meta.getTables(null, null, null, types)) {
-      if (outputFormat == OutputFormat.PRETTY) {
-        println("                     List of relations");
-      }
-      showResultSet(
-          rs,
-          false,
-          0,
-          Identity.create(rs, "TABLE_SCHEM"),
-          Identity.create(rs, "TABLE_NAME"),
-          Identity.create(rs, "TABLE_TYPE"));
-    } catch (SQLException e) {
-      error(e);
-    }
-
-    println("");
-  }
-
-  private void showTable(String tableName) {
-    final DatabaseMetaData meta;
-    try {
-      meta = connection.getMetaData();
-
-      if (meta.storesUpperCaseIdentifiers()) {
-        tableName = tableName.toUpperCase();
-      } else if (meta.storesLowerCaseIdentifiers()) {
-        tableName = tableName.toLowerCase();
-      }
-    } catch (SQLException e) {
-      error(e);
-      return;
-    }
-
-    try (ResultSet rs = meta.getColumns(null, null, tableName, null)) {
-      if (!rs.next()) {
-        throw new SQLException("Table " + tableName + " not found");
-      }
-
-      if (outputFormat == OutputFormat.PRETTY) {
-        println("                     Table " + tableName);
-      }
-      showResultSet(
-          rs,
-          true,
-          0,
-          Identity.create(rs, "COLUMN_NAME"),
-          new Function("TYPE") {
-            @Override
-            String apply(ResultSet rs) throws SQLException {
-              String type = rs.getString("TYPE_NAME");
-              switch (rs.getInt("DATA_TYPE")) {
-                case java.sql.Types.CHAR:
-                case java.sql.Types.VARCHAR:
-                  type += "(" + rs.getInt("COLUMN_SIZE") + ")";
-                  break;
-              }
-
-              String def = rs.getString("COLUMN_DEF");
-              if (def != null && !def.isEmpty()) {
-                type += " DEFAULT " + def;
-              }
-
-              int nullable = rs.getInt("NULLABLE");
-              if (nullable == DatabaseMetaData.columnNoNulls) {
-                type += " NOT NULL";
-              }
-              return type;
-            }
-          });
-    } catch (SQLException e) {
-      error(e);
-      return;
-    }
-
-    try (ResultSet rs = meta.getIndexInfo(null, null, tableName, false, true)) {
-      Map<String, IndexInfo> indexes = new TreeMap<>();
-      while (rs.next()) {
-        final String indexName = rs.getString("INDEX_NAME");
-        IndexInfo def = indexes.get(indexName);
-        if (def == null) {
-          def = new IndexInfo();
-          def.name = indexName;
-          indexes.put(indexName, def);
-        }
-
-        if (!rs.getBoolean("NON_UNIQUE")) {
-          def.unique = true;
-        }
-
-        final int pos = rs.getInt("ORDINAL_POSITION");
-        final String col = rs.getString("COLUMN_NAME");
-        String desc = rs.getString("ASC_OR_DESC");
-        if ("D".equals(desc)) {
-          desc = " DESC";
-        } else {
-          desc = "";
-        }
-        def.addColumn(pos, col + desc);
-
-        String filter = rs.getString("FILTER_CONDITION");
-        if (filter != null && !filter.isEmpty()) {
-          def.filter.append(filter);
-        }
-      }
-
-      if (outputFormat == OutputFormat.PRETTY) {
-        println("");
-        println("Indexes on " + tableName + ":");
-        for (IndexInfo def : indexes.values()) {
-          println("  " + def);
-        }
-      }
-    } catch (SQLException e) {
-      error(e);
-      return;
-    }
-
-    println("");
-  }
-
-  private void executeStatement(String sql) {
-    final long start = TimeUtil.nowMs();
-    final boolean hasResultSet;
-    try {
-      hasResultSet = statement.execute(sql);
-    } catch (SQLException e) {
-      error(e);
-      return;
-    }
-
-    try {
-      if (hasResultSet) {
-        try (ResultSet rs = statement.getResultSet()) {
-          showResultSet(rs, false, start);
-        }
-
-      } else {
-        final int updateCount = statement.getUpdateCount();
-        final long ms = TimeUtil.nowMs() - start;
-        switch (outputFormat) {
-          case JSON_SINGLE:
-          case JSON:
-            {
-              final JsonObject tail = new JsonObject();
-              tail.addProperty("type", "update-stats");
-              tail.addProperty("rowCount", updateCount);
-              tail.addProperty("runTimeMilliseconds", ms);
-              println(tail.toString());
-              break;
-            }
-
-          case PRETTY:
-          default:
-            println("UPDATE " + updateCount + "; " + ms + " ms");
-            break;
-        }
-      }
-    } catch (SQLException e) {
-      error(e);
-    }
-  }
-
-  /**
-   * Outputs a result set to stdout.
-   *
-   * @param rs ResultSet to show.
-   * @param alreadyOnRow true if rs is already on the first row. false otherwise.
-   * @param start Timestamp in milliseconds when executing the statement started. This timestamp is
-   *     used to compute statistics about the statement. If no statistics should be shown, set it to
-   *     0.
-   * @param show Functions to map columns
-   * @throws SQLException
-   */
-  private void showResultSet(ResultSet rs, boolean alreadyOnRow, long start, Function... show)
-      throws SQLException {
-    switch (outputFormat) {
-      case JSON_SINGLE:
-      case JSON:
-        showResultSetJson(rs, alreadyOnRow, start, show);
-        break;
-      case PRETTY:
-      default:
-        showResultSetPretty(rs, alreadyOnRow, start, show);
-        break;
-    }
-  }
-
-  /**
-   * Outputs a result set to stdout in Json format.
-   *
-   * @param rs ResultSet to show.
-   * @param alreadyOnRow true if rs is already on the first row. false otherwise.
-   * @param start Timestamp in milliseconds when executing the statement started. This timestamp is
-   *     used to compute statistics about the statement. If no statistics should be shown, set it to
-   *     0.
-   * @param show Functions to map columns
-   * @throws SQLException
-   */
-  private void showResultSetJson(
-      final ResultSet rs, boolean alreadyOnRow, long start, Function... show) throws SQLException {
-    JsonArray collector = new JsonArray();
-    final ResultSetMetaData meta = rs.getMetaData();
-    final Function[] columnMap;
-    if (show != null && 0 < show.length) {
-      columnMap = show;
-
-    } else {
-      final int colCnt = meta.getColumnCount();
-      columnMap = new Function[colCnt];
-      for (int colId = 0; colId < colCnt; colId++) {
-        final int p = colId + 1;
-        final String name = meta.getColumnLabel(p);
-        columnMap[colId] = new Identity(p, name);
-      }
-    }
-
-    int rowCnt = 0;
-    while (alreadyOnRow || rs.next()) {
-      final JsonObject row = new JsonObject();
-      final JsonObject cols = new JsonObject();
-      for (Function function : columnMap) {
-        String v = function.apply(rs);
-        if (v == null) {
-          continue;
-        }
-        cols.addProperty(function.name.toLowerCase(), v);
-      }
-      row.addProperty("type", "row");
-      row.add("columns", cols);
-      switch (outputFormat) {
-        case JSON:
-          println(row.toString());
-          break;
-        case JSON_SINGLE:
-          collector.add(row);
-          break;
-        case PRETTY:
-        default:
-          final JsonObject obj = new JsonObject();
-          obj.addProperty("type", "error");
-          obj.addProperty("message", "Unsupported Json variant");
-          println(obj.toString());
-          return;
-      }
-      alreadyOnRow = false;
-      rowCnt++;
-    }
-
-    JsonObject tail = null;
-    if (start != 0) {
-      tail = new JsonObject();
-      tail.addProperty("type", "query-stats");
-      tail.addProperty("rowCount", rowCnt);
-      final long ms = TimeUtil.nowMs() - start;
-      tail.addProperty("runTimeMilliseconds", ms);
-    }
-
-    switch (outputFormat) {
-      case JSON:
-        if (tail != null) {
-          println(tail.toString());
-        }
-        break;
-      case JSON_SINGLE:
-        if (tail != null) {
-          collector.add(tail);
-        }
-        println(collector.toString());
-        break;
-      case PRETTY:
-      default:
-        final JsonObject obj = new JsonObject();
-        obj.addProperty("type", "error");
-        obj.addProperty("message", "Unsupported Json variant");
-        println(obj.toString());
-    }
-  }
-
-  /**
-   * Outputs a result set to stdout in plain text format.
-   *
-   * @param rs ResultSet to show.
-   * @param alreadyOnRow true if rs is already on the first row. false otherwise.
-   * @param start Timestamp in milliseconds when executing the statement started. This timestamp is
-   *     used to compute statistics about the statement. If no statistics should be shown, set it to
-   *     0.
-   * @param show Functions to map columns
-   * @throws SQLException
-   */
-  private void showResultSetPretty(
-      final ResultSet rs, boolean alreadyOnRow, long start, Function... show) throws SQLException {
-    final ResultSetMetaData meta = rs.getMetaData();
-
-    final Function[] columnMap;
-    if (show != null && 0 < show.length) {
-      columnMap = show;
-
-    } else {
-      final int colCnt = meta.getColumnCount();
-      columnMap = new Function[colCnt];
-      for (int colId = 0; colId < colCnt; colId++) {
-        final int p = colId + 1;
-        final String name = meta.getColumnLabel(p);
-        columnMap[colId] = new Identity(p, name);
-      }
-    }
-
-    final int colCnt = columnMap.length;
-    final int[] widths = new int[colCnt];
-    for (int c = 0; c < colCnt; c++) {
-      widths[c] = columnMap[c].name.length();
-    }
-
-    final List<String[]> rows = new ArrayList<>();
-    while (alreadyOnRow || rs.next()) {
-      final String[] row = new String[columnMap.length];
-      for (int c = 0; c < colCnt; c++) {
-        row[c] = columnMap[c].apply(rs);
-        if (row[c] == null) {
-          row[c] = "NULL";
-        }
-        widths[c] = Math.max(widths[c], row[c].length());
-      }
-      rows.add(row);
-      alreadyOnRow = false;
-    }
-
-    final StringBuilder b = new StringBuilder();
-    for (int c = 0; c < colCnt; c++) {
-      if (0 < c) {
-        b.append(" | ");
-      }
-
-      String n = columnMap[c].name;
-      if (widths[c] < n.length()) {
-        n = n.substring(0, widths[c]);
-      }
-      b.append(n);
-
-      if (c < colCnt - 1) {
-        for (int pad = n.length(); pad < widths[c]; pad++) {
-          b.append(' ');
-        }
-      }
-    }
-    println(" " + b.toString());
-
-    b.setLength(0);
-    for (int c = 0; c < colCnt; c++) {
-      if (0 < c) {
-        b.append("-+-");
-      }
-      for (int pad = 0; pad < widths[c]; pad++) {
-        b.append('-');
-      }
-    }
-    println(" " + b.toString());
-
-    boolean dataTruncated = false;
-    for (String[] row : rows) {
-      b.setLength(0);
-      b.append(' ');
-
-      for (int c = 0; c < colCnt; c++) {
-        final int max = widths[c];
-        if (0 < c) {
-          b.append(" | ");
-        }
-
-        String s = row[c];
-        if (1 < colCnt && max < s.length()) {
-          s = s.substring(0, max);
-          dataTruncated = true;
-        }
-        b.append(s);
-
-        if (c < colCnt - 1) {
-          for (int pad = s.length(); pad < max; pad++) {
-            b.append(' ');
-          }
-        }
-      }
-      println(b.toString());
-    }
-
-    if (dataTruncated) {
-      warning("some column data was truncated");
-    }
-
-    if (start != 0) {
-      final int rowCount = rows.size();
-      final long ms = TimeUtil.nowMs() - start;
-      println("(" + rowCount + (rowCount == 1 ? " row" : " rows") + "; " + ms + " ms)");
-    }
-  }
-
-  private void warning(String msg) {
-    switch (outputFormat) {
-      case JSON_SINGLE:
-      case JSON:
-        {
-          final JsonObject obj = new JsonObject();
-          obj.addProperty("type", "warning");
-          obj.addProperty("message", msg);
-          println(obj.toString());
-          break;
-        }
-
-      case PRETTY:
-      default:
-        println("WARNING: " + msg);
-        break;
-    }
-  }
-
-  private void error(SQLException err) {
-    switch (outputFormat) {
-      case JSON_SINGLE:
-      case JSON:
-        {
-          final JsonObject obj = new JsonObject();
-          obj.addProperty("type", "error");
-          obj.addProperty("message", err.getMessage());
-          println(obj.toString());
-          break;
-        }
-
-      case PRETTY:
-      default:
-        println("ERROR: " + err.getMessage());
-        break;
-    }
-  }
-
-  private void print(String s) {
-    out.print(s);
-    out.flush();
-  }
-
-  private void println(String s) {
-    out.print(s);
-    out.print('\n');
-    out.flush();
-  }
-
-  private String readLine() {
-    try {
-      return in.readLine();
-    } catch (IOException e) {
-      return null;
-    }
-  }
-
-  private void showBanner() {
-    if (outputFormat == OutputFormat.PRETTY) {
-      println("Welcome to Gerrit Code Review " + Version.getVersion());
-      try {
-        print("(");
-        print(connection.getMetaData().getDatabaseProductName());
-        print(" ");
-        print(connection.getMetaData().getDatabaseProductVersion());
-        println(")");
-      } catch (SQLException err) {
-        error(err);
-      }
-      println("");
-      println("Type '\\h' for help.  Type '\\r' to clear the buffer.");
-      println("");
-    }
-  }
-
-  private void showHelp() {
-    final StringBuilder help = new StringBuilder();
-    help.append("General\n");
-    help.append("  \\q        quit\n");
-
-    help.append("\n");
-    help.append("Query Buffer\n");
-    help.append("  \\g        execute the query buffer\n");
-    help.append("  \\p        display the current buffer\n");
-    help.append("  \\r        clear the query buffer\n");
-
-    help.append("\n");
-    help.append("Informational\n");
-    help.append("  \\d        list all tables\n");
-    help.append("  \\d NAME   describe table\n");
-
-    help.append("\n");
-    print(help.toString());
-  }
-
-  private abstract static class Function {
-    final String name;
-
-    Function(String name) {
-      this.name = name;
-    }
-
-    abstract String apply(ResultSet rs) throws SQLException;
-  }
-
-  private static class Identity extends Function {
-    static Identity create(ResultSet rs, String name) throws SQLException {
-      return new Identity(rs.findColumn(name), name);
-    }
-
-    final int colId;
-
-    Identity(int colId, String name) {
-      super(name);
-      this.colId = colId;
-    }
-
-    @Override
-    String apply(ResultSet rs) throws SQLException {
-      return rs.getString(colId);
-    }
-  }
-
-  private static class IndexInfo {
-    String name;
-    boolean unique;
-    final Map<Integer, String> columns = new TreeMap<>();
-    final StringBuilder filter = new StringBuilder();
-
-    void addColumn(int pos, String column) {
-      columns.put(Integer.valueOf(pos), column);
-    }
-
-    @Override
-    public String toString() {
-      final StringBuilder r = new StringBuilder();
-      r.append(name);
-      if (unique) {
-        r.append(" UNIQUE");
-      }
-      r.append(" (");
-      boolean first = true;
-      for (String c : columns.values()) {
-        if (!first) {
-          r.append(", ");
-        }
-        r.append(c);
-        first = false;
-      }
-      r.append(")");
-      if (filter.length() > 0) {
-        r.append(" WHERE ");
-        r.append(filter);
-      }
-      return r.toString();
-    }
-  }
-}
diff --git a/java/com/google/gerrit/sshd/commands/ReviewCommand.java b/java/com/google/gerrit/sshd/commands/ReviewCommand.java
index 786048f..0af8936 100644
--- a/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -14,13 +14,16 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static com.google.gerrit.util.cli.Localizable.localizable;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Objects.requireNonNull;
 
 import com.google.common.base.Strings;
 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.exceptions.StorageException;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.changes.AbandonInput;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
@@ -30,8 +33,8 @@
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 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.OutputFormat;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
@@ -40,20 +43,26 @@
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gerrit.util.cli.CmdLineParser;
+import com.google.gerrit.util.cli.OptionUtil;
 import com.google.gson.JsonSyntaxException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.io.InputStreamReader;
-import java.util.ArrayList;
+import java.lang.reflect.AnnotatedElement;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.List;
+import java.util.LinkedHashMap;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.TreeMap;
 import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.CmdLineException;
 import org.kohsuke.args4j.Option;
+import org.kohsuke.args4j.OptionDef;
+import org.kohsuke.args4j.spi.FieldSetter;
+import org.kohsuke.args4j.spi.OneArgumentOptionHandler;
+import org.kohsuke.args4j.spi.Setter;
 
 @CommandMetaData(name = "review", description = "Apply reviews to one or more patch sets")
 public class ReviewCommand extends SshCommand {
@@ -61,10 +70,8 @@
 
   @Override
   protected final CmdLineParser newCmdLineParser(Object options) {
-    final CmdLineParser parser = super.newCmdLineParser(options);
-    for (ApproveOption c : optionList) {
-      parser.addOption(c, c);
-    }
+    CmdLineParser parser = super.newCmdLineParser(options);
+    optionMap.forEach((o, s) -> parser.addOption(s, o));
     return parser;
   }
 
@@ -82,7 +89,7 @@
       patchSets.add(ps);
     } catch (UnloggedFailure e) {
       throw new IllegalArgumentException(e.getMessage(), e);
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       throw new IllegalArgumentException("database error", e);
     }
   }
@@ -154,7 +161,7 @@
 
   @Inject private PatchSetParser psParser;
 
-  private List<ApproveOption> optionList;
+  private Map<Option, LabelSetter> optionMap;
   private Map<String, Short> customLabels;
 
   @Override
@@ -257,11 +264,8 @@
     review.notify = notify;
     review.labels = new TreeMap<>();
     review.drafts = ReviewInput.DraftHandling.PUBLISH;
-    for (ApproveOption ao : optionList) {
-      Short v = ao.value();
-      if (v != null) {
-        review.labels.put(ao.getLabelName(), v);
-      }
+    for (LabelSetter setter : optionMap.values()) {
+      setter.getValue().ifPresent(v -> review.labels.put(setter.getLabelName(), v));
     }
     review.labels.putAll(customLabels);
 
@@ -315,7 +319,7 @@
 
   @Override
   protected void parseCommandLine() throws UnloggedFailure {
-    optionList = new ArrayList<>();
+    optionMap = new LinkedHashMap<>();
     customLabels = new HashMap<>();
 
     ProjectState allProjectsState;
@@ -332,10 +336,111 @@
         usage.append(v.format()).append("\n");
       }
 
-      final String name = "--" + type.getName().toLowerCase();
-      optionList.add(new ApproveOption(name, usage.toString(), type));
+      optionMap.put(newApproveOption(type, usage.toString()), new LabelSetter(type));
     }
 
     super.parseCommandLine();
   }
+
+  private static String asOptionName(LabelType type) {
+    return "--" + type.getName().toLowerCase();
+  }
+
+  private static Option newApproveOption(LabelType type, String usage) {
+    return OptionUtil.newOption(
+        asOptionName(type),
+        new String[0],
+        usage,
+        "N",
+        false,
+        false,
+        false,
+        LabelHandler.class,
+        new String[0],
+        new String[0]);
+  }
+
+  private static class LabelSetter implements Setter<Short> {
+    private final LabelType type;
+    private Optional<Short> value;
+
+    LabelSetter(LabelType type) {
+      this.type = requireNonNull(type);
+      this.value = Optional.empty();
+    }
+
+    Optional<Short> getValue() {
+      return value;
+    }
+
+    LabelType getLabelType() {
+      return type;
+    }
+
+    String getLabelName() {
+      return type.getName();
+    }
+
+    @Override
+    public void addValue(Short value) {
+      this.value = Optional.of(value);
+    }
+
+    @Override
+    public Class<Short> getType() {
+      return Short.class;
+    }
+
+    @Override
+    public boolean isMultiValued() {
+      return false;
+    }
+
+    @Override
+    public FieldSetter asFieldSetter() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public AnnotatedElement asAnnotatedElement() {
+      throw new UnsupportedOperationException();
+    }
+  }
+
+  public static class LabelHandler extends OneArgumentOptionHandler<Short> {
+    private final LabelType type;
+
+    public LabelHandler(
+        org.kohsuke.args4j.CmdLineParser parser, OptionDef option, Setter<Short> setter) {
+      super(parser, option, setter);
+      this.type = ((LabelSetter) setter).getLabelType();
+    }
+
+    @Override
+    protected Short parse(String token) throws NumberFormatException, CmdLineException {
+      String argument = token;
+      if (argument.startsWith("+")) {
+        argument = argument.substring(1);
+      }
+
+      short value = Short.parseShort(argument);
+      LabelValue min = type.getMin();
+      LabelValue max = type.getMax();
+
+      if (value < min.getValue() || value > max.getValue()) {
+        String e =
+            "\""
+                + token
+                + "\" must be in range "
+                + min.formatValue()
+                + ".."
+                + max.formatValue()
+                + " for \""
+                + asOptionName(type)
+                + "\"";
+        throw new CmdLineException(owner, localizable(e));
+      }
+      return value;
+    }
+  }
 }
diff --git a/java/com/google/gerrit/sshd/commands/ScpCommand.java b/java/com/google/gerrit/sshd/commands/ScpCommand.java
index b3a9a16..5122b35 100644
--- a/java/com/google/gerrit/sshd/commands/ScpCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ScpCommand.java
@@ -27,7 +27,6 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.tools.ToolsCatalog;
-import com.google.gerrit.server.tools.ToolsCatalog.Entry;
 import com.google.gerrit.sshd.BaseCommand;
 import com.google.inject.Inject;
 import java.io.ByteArrayOutputStream;
@@ -104,14 +103,14 @@
           root = "";
         }
 
-        final Entry ent = toc.get(root);
+        final ToolsCatalog.Entry ent = toc.get(root);
         if (ent == null) {
           throw new IOException(root + " not found");
 
-        } else if (Entry.Type.FILE == ent.getType()) {
+        } else if (ToolsCatalog.Entry.Type.FILE == ent.getType()) {
           readFile(ent);
 
-        } else if (Entry.Type.DIR == ent.getType()) {
+        } else if (ToolsCatalog.Entry.Type.DIR == ent.getType()) {
           if (!opt_r) {
             throw new IOException(root + " not a regular file");
           }
@@ -156,7 +155,7 @@
     }
   }
 
-  private void readFile(Entry ent) throws IOException {
+  private void readFile(ToolsCatalog.Entry ent) throws IOException {
     byte[] data = ent.getBytes();
     if (data == null) {
       throw new FileNotFoundException(ent.getPath());
@@ -170,12 +169,12 @@
     readAck();
   }
 
-  private void readDir(Entry dir) throws IOException {
+  private void readDir(ToolsCatalog.Entry dir) throws IOException {
     header(dir, 0);
     readAck();
 
-    for (Entry e : dir.getChildren()) {
-      if (Entry.Type.DIR == e.getType()) {
+    for (ToolsCatalog.Entry e : dir.getChildren()) {
+      if (ToolsCatalog.Entry.Type.DIR == e.getType()) {
         readDir(e);
       } else {
         readFile(e);
@@ -187,7 +186,8 @@
     readAck();
   }
 
-  private void header(Entry dir, int len) throws IOException, UnsupportedEncodingException {
+  private void header(ToolsCatalog.Entry dir, int len)
+      throws IOException, UnsupportedEncodingException {
     final StringBuilder buf = new StringBuilder();
     switch (dir.getType()) {
       case DIR:
diff --git a/java/com/google/gerrit/sshd/commands/SetAccountCommand.java b/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
index 0ff19f7..8a72de6 100644
--- a/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
@@ -18,7 +18,7 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.RawInputUtil;
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.accounts.EmailInput;
 import com.google.gerrit.extensions.api.accounts.SshKeyInput;
 import com.google.gerrit.extensions.common.EmailInfo;
@@ -52,7 +52,6 @@
 import com.google.gerrit.server.restapi.account.PutPreferred;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.BufferedReader;
@@ -213,8 +212,7 @@
   }
 
   private void setAccount()
-      throws OrmException, IOException, UnloggedFailure, ConfigInvalidException,
-          PermissionBackendException {
+      throws IOException, UnloggedFailure, ConfigInvalidException, PermissionBackendException {
     user = genericUserFactory.create(id);
     rsrc = new AccountResource(user.asIdentifiedUser());
     try {
@@ -273,8 +271,7 @@
   }
 
   private void addSshKeys(List<String> sshKeys)
-      throws RestApiException, OrmException, IOException, ConfigInvalidException,
-          PermissionBackendException {
+      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
     for (String sshKey : sshKeys) {
       SshKeyInput in = new SshKeyInput();
       in.raw = RawInputUtil.create(sshKey.getBytes(UTF_8), "text/plain");
@@ -283,8 +280,8 @@
   }
 
   private void deleteSshKeys(List<String> sshKeys)
-      throws RestApiException, OrmException, RepositoryNotFoundException, IOException,
-          ConfigInvalidException, PermissionBackendException {
+      throws RestApiException, RepositoryNotFoundException, IOException, ConfigInvalidException,
+          PermissionBackendException {
     List<SshKeyInfo> infos = getSshKeys.apply(rsrc);
     if (sshKeys.contains("ALL")) {
       for (SshKeyInfo i : infos) {
@@ -302,14 +299,14 @@
   }
 
   private void deleteSshKey(SshKeyInfo i)
-      throws AuthException, OrmException, RepositoryNotFoundException, IOException,
-          ConfigInvalidException, PermissionBackendException {
+      throws AuthException, RepositoryNotFoundException, IOException, ConfigInvalidException,
+          PermissionBackendException {
     AccountSshKey sshKey = AccountSshKey.create(user.getAccountId(), i.seq, i.sshPublicKey);
     deleteSshKey.apply(new AccountResource.SshKey(user.asIdentifiedUser(), sshKey), null);
   }
 
   private void addEmail(String email)
-      throws UnloggedFailure, RestApiException, OrmException, IOException, ConfigInvalidException,
+      throws UnloggedFailure, RestApiException, IOException, ConfigInvalidException,
           PermissionBackendException {
     EmailInput in = new EmailInput();
     in.email = email;
@@ -322,8 +319,7 @@
   }
 
   private void deleteEmail(String email)
-      throws RestApiException, OrmException, IOException, ConfigInvalidException,
-          PermissionBackendException {
+      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
     if (email.equals("ALL")) {
       List<EmailInfo> emails = getEmails.apply(rsrc);
       for (EmailInfo e : emails) {
@@ -335,8 +331,7 @@
   }
 
   private void putPreferred(String email)
-      throws RestApiException, OrmException, IOException, PermissionBackendException,
-          ConfigInvalidException {
+      throws RestApiException, IOException, PermissionBackendException, ConfigInvalidException {
     for (EmailInfo e : getEmails.apply(rsrc)) {
       if (e.email.equals(email)) {
         putPreferred.apply(new AccountResource.Email(user.asIdentifiedUser(), email), null);
diff --git a/java/com/google/gerrit/sshd/commands/SetParentCommand.java b/java/com/google/gerrit/sshd/commands/SetParentCommand.java
index f2d8c4c..dfdf7f2 100644
--- a/java/com/google/gerrit/sshd/commands/SetParentCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetParentCommand.java
@@ -16,6 +16,7 @@
 
 import static java.util.stream.Collectors.toList;
 
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.projects.ParentInput;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -33,7 +34,6 @@
 import com.google.gerrit.server.restapi.project.SetParent;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -112,7 +112,7 @@
         childProjects.addAll(getChildrenForReparenting(oldParent));
       } catch (PermissionBackendException e) {
         throw new Failure(1, "permissions unavailable", e);
-      } catch (OrmException | RestApiException e) {
+      } catch (StorageException | RestApiException e) {
         throw new Failure(1, "failure in request", e);
       }
     }
@@ -149,7 +149,7 @@
    * reparenting.
    */
   private List<Project.NameKey> getChildrenForReparenting(ProjectState parent)
-      throws PermissionBackendException, OrmException, RestApiException {
+      throws PermissionBackendException, RestApiException {
     final List<Project.NameKey> childProjects = new ArrayList<>();
     final List<Project.NameKey> excluded = new ArrayList<>(excludedChildren.size());
     for (ProjectState excludedChild : excludedChildren) {
diff --git a/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java b/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
index a4a8ea8..30caa43 100644
--- a/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.sshd.commands;
 
 import com.google.common.flogger.FluentLogger;
+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;
@@ -29,7 +30,6 @@
 import com.google.gerrit.sshd.ChangeArgumentParser;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -75,7 +75,7 @@
       changeArgumentParser.addChange(token, changes, projectState);
     } catch (IOException | UnloggedFailure e) {
       throw new IllegalArgumentException(e.getMessage(), e);
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       throw new IllegalArgumentException("database is down", e);
     } catch (PermissionBackendException e) {
       throw new IllegalArgumentException("can't check permissions", e);
diff --git a/java/com/google/gerrit/sshd/commands/ShowCaches.java b/java/com/google/gerrit/sshd/commands/ShowCaches.java
index e4c14d8..be13a84 100644
--- a/java/com/google/gerrit/sshd/commands/ShowCaches.java
+++ b/java/com/google/gerrit/sshd/commands/ShowCaches.java
@@ -47,7 +47,6 @@
 import java.util.Collection;
 import java.util.Date;
 import java.util.Map;
-import java.util.Map.Entry;
 import org.apache.sshd.common.io.IoAcceptor;
 import org.apache.sshd.common.io.IoSession;
 import org.apache.sshd.common.io.mina.MinaSession;
@@ -267,7 +266,7 @@
         stdout.print(String.format(" %14s", s.name()));
       }
       stdout.print('\n');
-      for (Entry<String, Map<Thread.State, Integer>> e : threadSummary.counts.entrySet()) {
+      for (Map.Entry<String, Map<Thread.State, Integer>> e : threadSummary.counts.entrySet()) {
         stdout.print(String.format("  %-22s", e.getKey()));
         for (Thread.State s : Thread.State.values()) {
           stdout.print(String.format(" %14d", nullToZero(e.getValue().get(s))));
diff --git a/java/com/google/gerrit/sshd/commands/UploadArchive.java b/java/com/google/gerrit/sshd/commands/UploadArchive.java
index ac914a5..24f82a7 100644
--- a/java/com/google/gerrit/sshd/commands/UploadArchive.java
+++ b/java/com/google/gerrit/sshd/commands/UploadArchive.java
@@ -238,7 +238,7 @@
                   options.level9)
               .indexOf(true);
       if (value >= 0) {
-        return ImmutableMap.<String, Object>of("level", Integer.valueOf(value));
+        return ImmutableMap.of("level", Integer.valueOf(value));
       }
     }
     return Collections.emptyMap();
diff --git a/java/com/google/gerrit/testing/BUILD b/java/com/google/gerrit/testing/BUILD
index dfcd34f..b7b1e5f 100644
--- a/java/com/google/gerrit/testing/BUILD
+++ b/java/com/google/gerrit/testing/BUILD
@@ -16,6 +16,7 @@
     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/gpg",
         "//java/com/google/gerrit/index",
@@ -36,7 +37,6 @@
         "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/server/util/time",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib:h2",
         "//lib:junit",
         "//lib/auto:auto-value",
diff --git a/java/com/google/gerrit/testing/DisabledReviewDb.java b/java/com/google/gerrit/testing/DisabledReviewDb.java
deleted file mode 100644
index 2bf95b0..0000000
--- a/java/com/google/gerrit/testing/DisabledReviewDb.java
+++ /dev/null
@@ -1,111 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.testing;
-
-import com.google.gerrit.reviewdb.server.ChangeAccess;
-import com.google.gerrit.reviewdb.server.ChangeMessageAccess;
-import com.google.gerrit.reviewdb.server.PatchLineCommentAccess;
-import com.google.gerrit.reviewdb.server.PatchSetAccess;
-import com.google.gerrit.reviewdb.server.PatchSetApprovalAccess;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.reviewdb.server.SchemaVersionAccess;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.StatementExecutor;
-
-/** ReviewDb that is disabled for testing. */
-public class DisabledReviewDb implements ReviewDb {
-  public static class Disabled extends RuntimeException {
-    private static final long serialVersionUID = 1L;
-
-    private Disabled() {
-      super("ReviewDb is disabled for this test");
-    }
-  }
-
-  @Override
-  public void close() {
-    // Do nothing.
-  }
-
-  @Override
-  public void commit() {
-    throw new Disabled();
-  }
-
-  @Override
-  public void rollback() {
-    throw new Disabled();
-  }
-
-  @Override
-  public void updateSchema(StatementExecutor e) {
-    throw new Disabled();
-  }
-
-  @Override
-  public void pruneSchema(StatementExecutor e) {
-    throw new Disabled();
-  }
-
-  @Override
-  public Access<?, ?>[] allRelations() {
-    throw new Disabled();
-  }
-
-  @Override
-  public SchemaVersionAccess schemaVersion() {
-    throw new Disabled();
-  }
-
-  @Override
-  public ChangeAccess changes() {
-    throw new Disabled();
-  }
-
-  @Override
-  public PatchSetApprovalAccess patchSetApprovals() {
-    throw new Disabled();
-  }
-
-  @Override
-  public ChangeMessageAccess changeMessages() {
-    throw new Disabled();
-  }
-
-  @Override
-  public PatchSetAccess patchSets() {
-    throw new Disabled();
-  }
-
-  @Override
-  public PatchLineCommentAccess patchComments() {
-    throw new Disabled();
-  }
-
-  @Override
-  public int nextAccountId() {
-    throw new Disabled();
-  }
-
-  @Override
-  public int nextAccountGroupId() {
-    throw new Disabled();
-  }
-
-  @Override
-  public int nextChangeId() {
-    throw new Disabled();
-  }
-}
diff --git a/java/com/google/gerrit/testing/FakeEmailSender.java b/java/com/google/gerrit/testing/FakeEmailSender.java
index dbb83c0..a60995b 100644
--- a/java/com/google/gerrit/testing/FakeEmailSender.java
+++ b/java/com/google/gerrit/testing/FakeEmailSender.java
@@ -21,7 +21,7 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.mail.EmailHeader;
 import com.google.gerrit.mail.MailHeader;
@@ -87,7 +87,7 @@
   @Inject
   FakeEmailSender(WorkQueue workQueue) {
     this.workQueue = workQueue;
-    messages = Collections.synchronizedList(new ArrayList<Message>());
+    messages = Collections.synchronizedList(new ArrayList<>());
     messagesRead = 0;
   }
 
diff --git a/java/com/google/gerrit/testing/FakeGroupAuditService.java b/java/com/google/gerrit/testing/FakeGroupAuditService.java
deleted file mode 100644
index f2e85cd..0000000
--- a/java/com/google/gerrit/testing/FakeGroupAuditService.java
+++ /dev/null
@@ -1,112 +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.testing;
-
-import com.google.common.collect.ImmutableSet;
-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.AuditEvent;
-import com.google.gerrit.server.audit.AuditListener;
-import com.google.gerrit.server.audit.group.GroupAuditListener;
-import com.google.gerrit.server.audit.group.GroupMemberAuditEvent;
-import com.google.gerrit.server.audit.group.GroupSubgroupAuditEvent;
-import com.google.gerrit.server.group.GroupAuditService;
-import com.google.gerrit.server.plugincontext.PluginSetContext;
-import com.google.inject.AbstractModule;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import java.sql.Timestamp;
-import java.util.ArrayList;
-import java.util.List;
-
-@Singleton
-public class FakeGroupAuditService implements GroupAuditService {
-
-  protected final PluginSetContext<GroupAuditListener> groupAuditListeners;
-  protected final PluginSetContext<AuditListener> auditListeners;
-
-  public static class Module extends AbstractModule {
-    @Override
-    public void configure() {
-      DynamicSet.setOf(binder(), GroupAuditListener.class);
-      DynamicSet.setOf(binder(), AuditListener.class);
-      bind(GroupAuditService.class).to(FakeGroupAuditService.class);
-    }
-  }
-
-  @Inject
-  public FakeGroupAuditService(
-      PluginSetContext<GroupAuditListener> groupAuditListeners,
-      PluginSetContext<AuditListener> auditListeners) {
-    this.groupAuditListeners = groupAuditListeners;
-    this.auditListeners = auditListeners;
-  }
-
-  public List<AuditEvent> auditEvents = new ArrayList<>();
-
-  public void clearEvents() {
-    auditEvents.clear();
-  }
-
-  @Override
-  public void dispatch(AuditEvent action) {
-    auditEvents.add(action);
-  }
-
-  @Override
-  public void dispatchAddMembers(
-      Account.Id actor,
-      AccountGroup.UUID updatedGroup,
-      ImmutableSet<Account.Id> addedMembers,
-      Timestamp addedOn) {
-    GroupMemberAuditEvent event =
-        GroupMemberAuditEvent.create(actor, updatedGroup, addedMembers, addedOn);
-    groupAuditListeners.runEach(l -> l.onAddMembers(event));
-  }
-
-  @Override
-  public void dispatchDeleteMembers(
-      Account.Id actor,
-      AccountGroup.UUID updatedGroup,
-      ImmutableSet<Account.Id> deletedMembers,
-      Timestamp deletedOn) {
-    GroupMemberAuditEvent event =
-        GroupMemberAuditEvent.create(actor, updatedGroup, deletedMembers, deletedOn);
-    groupAuditListeners.runEach(l -> l.onDeleteMembers(event));
-  }
-
-  @Override
-  public void dispatchAddSubgroups(
-      Account.Id actor,
-      AccountGroup.UUID updatedGroup,
-      ImmutableSet<AccountGroup.UUID> addedSubgroups,
-      Timestamp addedOn) {
-    GroupSubgroupAuditEvent event =
-        GroupSubgroupAuditEvent.create(actor, updatedGroup, addedSubgroups, addedOn);
-    groupAuditListeners.runEach(l -> l.onAddSubgroups(event));
-  }
-
-  @Override
-  public void dispatchDeleteSubgroups(
-      Account.Id actor,
-      AccountGroup.UUID updatedGroup,
-      ImmutableSet<AccountGroup.UUID> deletedSubgroups,
-      Timestamp deletedOn) {
-    GroupSubgroupAuditEvent event =
-        GroupSubgroupAuditEvent.create(actor, updatedGroup, deletedSubgroups, deletedOn);
-    groupAuditListeners.runEach(l -> l.onDeleteSubgroups(event));
-  }
-}
diff --git a/java/com/google/gerrit/testing/GerritBaseTests.java b/java/com/google/gerrit/testing/GerritBaseTests.java
index 01fb85d..d6a2261 100644
--- a/java/com/google/gerrit/testing/GerritBaseTests.java
+++ b/java/com/google/gerrit/testing/GerritBaseTests.java
@@ -15,8 +15,6 @@
 package com.google.gerrit.testing;
 
 import com.google.common.base.CharMatcher;
-import com.google.gwtorm.client.KeyUtil;
-import com.google.gwtorm.server.StandardKeyEncoder;
 import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.rules.ExpectedException;
@@ -24,10 +22,6 @@
 
 @Ignore
 public abstract class GerritBaseTests {
-  static {
-    KeyUtil.setEncoderImpl(new StandardKeyEncoder());
-  }
-
   @Rule public ExpectedException exception = ExpectedException.none();
   @Rule public final TestName testName = new TestName();
 
diff --git a/java/com/google/gerrit/testing/GerritServerTests.java b/java/com/google/gerrit/testing/GerritServerTests.java
index 69806e1..9a922d6 100644
--- a/java/com/google/gerrit/testing/GerritServerTests.java
+++ b/java/com/google/gerrit/testing/GerritServerTests.java
@@ -14,11 +14,9 @@
 
 package com.google.gerrit.testing;
 
-import com.google.gerrit.server.notedb.MutableNotesMigration;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Rule;
 import org.junit.rules.TestRule;
-import org.junit.runner.Description;
 import org.junit.runner.RunWith;
 import org.junit.runners.model.Statement;
 
@@ -28,32 +26,13 @@
 
   @ConfigSuite.Name private String configName;
 
-  protected MutableNotesMigration notesMigration;
-
   @Rule
   public TestRule testRunner =
-      new TestRule() {
-        @Override
-        public Statement apply(Statement base, Description description) {
-          return new Statement() {
+      (base, description) ->
+          new Statement() {
             @Override
             public void evaluate() throws Throwable {
-              beforeTest();
-              try {
-                base.evaluate();
-              } finally {
-                afterTest();
-              }
+              base.evaluate();
             }
           };
-        }
-      };
-
-  public void beforeTest() throws Exception {
-    notesMigration = NoteDbMode.newNotesMigrationFromEnv();
-  }
-
-  public void afterTest() {
-    NoteDbMode.resetFromEnv(notesMigration);
-  }
 }
diff --git a/java/com/google/gerrit/testing/InMemoryDatabase.java b/java/com/google/gerrit/testing/InMemoryDatabase.java
deleted file mode 100644
index 66a5290..0000000
--- a/java/com/google/gerrit/testing/InMemoryDatabase.java
+++ /dev/null
@@ -1,194 +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.testing;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.gerrit.lifecycle.LifecycleManager;
-import com.google.gerrit.pgm.init.index.elasticsearch.ElasticIndexModuleOnInit;
-import com.google.gerrit.pgm.init.index.lucene.LuceneIndexModuleOnInit;
-import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.index.IndexModule;
-import com.google.gerrit.server.schema.SchemaCreator;
-import com.google.gerrit.server.schema.SchemaVersion;
-import com.google.gwtorm.jdbc.Database;
-import com.google.gwtorm.jdbc.SimpleDataSource;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.AbstractModule;
-import com.google.inject.Guice;
-import com.google.inject.Inject;
-import com.google.inject.Injector;
-import java.io.IOException;
-import java.sql.Connection;
-import java.sql.SQLException;
-import java.util.Properties;
-import javax.sql.DataSource;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-
-/**
- * An in-memory test instance of {@link ReviewDb} database.
- *
- * <p>Test classes should create one instance of this class for each unique test database they want
- * to use. When the tests needing this instance are complete, ensure that {@link
- * #drop(InMemoryDatabase)} is called to free the resources so the JVM running the unit tests
- * doesn't run out of heap space.
- */
-public class InMemoryDatabase implements SchemaFactory<ReviewDb> {
-  public static InMemoryDatabase newDatabase(LifecycleManager lifecycle) {
-    Injector injector = Guice.createInjector(new InMemoryModule());
-    lifecycle.add(injector);
-    return injector.getInstance(InMemoryDatabase.class);
-  }
-
-  /** Drop the database from memory; does nothing if the instance was null. */
-  public static void drop(InMemoryDatabase db) {
-    if (db != null) {
-      db.dbInstance.drop();
-    }
-  }
-
-  private final SchemaCreator schemaCreator;
-  private final Instance dbInstance;
-
-  private boolean created;
-
-  @Inject
-  InMemoryDatabase(Injector injector) throws OrmException {
-    Injector childInjector =
-        injector.createChildInjector(
-            new AbstractModule() {
-              @Override
-              protected void configure() {
-                switch (IndexModule.getIndexType(injector)) {
-                  case LUCENE:
-                    install(new LuceneIndexModuleOnInit());
-                    break;
-                  case ELASTICSEARCH:
-                    install(new ElasticIndexModuleOnInit());
-                    break;
-                  default:
-                    throw new IllegalStateException("unsupported index.type");
-                }
-              }
-            });
-    this.schemaCreator = childInjector.getInstance(SchemaCreator.class);
-    Instance dbInstanceFromInjector = childInjector.getInstance(Instance.class);
-    if (dbInstanceFromInjector != null) {
-      this.dbInstance = dbInstanceFromInjector;
-      this.created = true;
-    } else {
-      this.dbInstance = new Instance();
-    }
-  }
-
-  InMemoryDatabase(SchemaCreator schemaCreator) throws OrmException {
-    this.schemaCreator = schemaCreator;
-    this.dbInstance = new Instance();
-  }
-
-  public Instance getDbInstance() {
-    return dbInstance;
-  }
-
-  public Database<ReviewDb> getDatabase() {
-    return dbInstance.database;
-  }
-
-  @Override
-  public ReviewDb open() throws OrmException {
-    return getDatabase().open();
-  }
-
-  /** Ensure the database schema has been created and initialized. */
-  public InMemoryDatabase create() throws OrmException {
-    if (!created) {
-      created = true;
-      try (ReviewDb c = open()) {
-        schemaCreator.create(c);
-      } catch (IOException | ConfigInvalidException e) {
-        throw new OrmException("Cannot create in-memory database", e);
-      }
-    }
-    return this;
-  }
-
-  public CurrentSchemaVersion getSchemaVersion() throws OrmException {
-    try (ReviewDb c = open()) {
-      return c.schemaVersion().get(new CurrentSchemaVersion.Key());
-    }
-  }
-
-  public void assertSchemaVersion() throws OrmException {
-    assertThat(getSchemaVersion().versionNbr).isEqualTo(SchemaVersion.getBinaryVersion());
-  }
-
-  public static class Instance {
-    private static int dbCnt;
-
-    private Connection openHandle;
-    private Database<ReviewDb> database;
-    private boolean keepOpen;
-
-    private static synchronized DataSource newDataSource() throws SQLException {
-      final Properties p = new Properties();
-      p.setProperty("driver", org.h2.Driver.class.getName());
-      p.setProperty("url", "jdbc:h2:mem:Test_" + (++dbCnt));
-      return new SimpleDataSource(p);
-    }
-
-    private Instance() throws OrmException {
-      try {
-        DataSource dataSource = newDataSource();
-
-        // Open one connection. This will peg the database into memory
-        // until someone calls drop on us, allowing subsequent connections
-        // opened against the same URL to go to the same set of tables.
-        //
-        openHandle = dataSource.getConnection();
-
-        // Build the access layer around the connection factory.
-        //
-        database = new Database<>(dataSource, ReviewDb.class);
-
-      } catch (SQLException e) {
-        throw new OrmException(e);
-      }
-    }
-
-    public void setKeepOpen(boolean keepOpen) {
-      this.keepOpen = keepOpen;
-    }
-
-    /** Drop this database from memory so it no longer exists. */
-    public void drop() {
-      if (keepOpen) {
-        return;
-      }
-
-      if (openHandle != null) {
-        try {
-          openHandle.close();
-        } catch (SQLException e) {
-          System.err.println("WARNING: Cannot close database connection");
-          e.printStackTrace(System.err);
-        }
-        openHandle = null;
-        database = null;
-      }
-    }
-  }
-}
diff --git a/java/com/google/gerrit/testing/InMemoryH2Type.java b/java/com/google/gerrit/testing/InMemoryH2Type.java
deleted file mode 100644
index ae3bf36..0000000
--- a/java/com/google/gerrit/testing/InMemoryH2Type.java
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.testing;
-
-import com.google.gerrit.server.schema.BaseDataSourceType;
-
-public class InMemoryH2Type extends BaseDataSourceType {
-
-  protected InMemoryH2Type() {
-    super(null);
-  }
-
-  @Override
-  public String getUrl() {
-    // not used
-    throw new UnsupportedOperationException();
-  }
-}
diff --git a/java/com/google/gerrit/testing/InMemoryModule.java b/java/com/google/gerrit/testing/InMemoryModule.java
index 18dfea0..c8cea6f 100644
--- a/java/com/google/gerrit/testing/InMemoryModule.java
+++ b/java/com/google/gerrit/testing/InMemoryModule.java
@@ -28,7 +28,6 @@
 import com.google.gerrit.index.project.ProjectSchemaDefinitions;
 import com.google.gerrit.metrics.DisabledMetricMaker;
 import com.google.gerrit.metrics.MetricMaker;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.FanOutExecutor;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.GerritPersonIdentProvider;
@@ -70,38 +69,29 @@
 import com.google.gerrit.server.index.change.AllChangesIndexer;
 import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
 import com.google.gerrit.server.index.group.AllGroupsIndexer;
+import com.google.gerrit.server.index.group.GroupIndexCollection;
 import com.google.gerrit.server.index.group.GroupSchemaDefinitions;
 import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
-import com.google.gerrit.server.notedb.ChangeBundleReader;
-import com.google.gerrit.server.notedb.GwtormChangeBundleReader;
-import com.google.gerrit.server.notedb.MutableNotesMigration;
-import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.patch.DiffExecutor;
 import com.google.gerrit.server.permissions.DefaultPermissionBackendModule;
 import com.google.gerrit.server.plugins.ServerInformationImpl;
 import com.google.gerrit.server.project.DefaultProjectNameLockManager;
 import com.google.gerrit.server.restapi.RestApiModule;
-import com.google.gerrit.server.schema.DataSourceType;
-import com.google.gerrit.server.schema.InMemoryAccountPatchReviewStore;
-import com.google.gerrit.server.schema.NotesMigrationSchemaFactory;
-import com.google.gerrit.server.schema.ReviewDbFactory;
+import com.google.gerrit.server.schema.JdbcAccountPatchReviewStore;
 import com.google.gerrit.server.schema.SchemaCreator;
+import com.google.gerrit.server.schema.SchemaCreatorImpl;
 import com.google.gerrit.server.securestore.DefaultSecureStore;
 import com.google.gerrit.server.securestore.SecureStore;
 import com.google.gerrit.server.ssh.NoSshKeyCache;
 import com.google.gerrit.server.submit.LocalMergeSuperSetComputation;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.AbstractModule;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
-import com.google.inject.Key;
 import com.google.inject.Module;
 import com.google.inject.Provider;
 import com.google.inject.Provides;
 import com.google.inject.ProvisionException;
 import com.google.inject.Singleton;
-import com.google.inject.TypeLiteral;
 import com.google.inject.servlet.RequestScoped;
 import com.google.inject.util.Providers;
 import java.lang.reflect.InvocationTargetException;
@@ -122,6 +112,8 @@
   }
 
   public static void setDefaults(Config cfg) {
+    cfg.setString(
+        "accountPatchReviewDb", null, "url", JdbcAccountPatchReviewStore.TEST_IN_MEMORY_URL);
     cfg.setEnum("auth", null, "type", AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT);
     cfg.setString("gerrit", null, "allProjects", "Test-Projects");
     cfg.setString("gerrit", null, "basePath", "git");
@@ -137,15 +129,13 @@
   }
 
   private final Config cfg;
-  private final MutableNotesMigration notesMigration;
 
   public InMemoryModule() {
-    this(newDefaultConfig(), NoteDbMode.newNotesMigrationFromEnv());
+    this(newDefaultConfig());
   }
 
-  public InMemoryModule(Config cfg, MutableNotesMigration notesMigration) {
+  public InMemoryModule(Config cfg) {
     this.cfg = cfg;
-    this.notesMigration = notesMigration;
   }
 
   public void inject(Object instance) {
@@ -188,33 +178,17 @@
     // 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(cfg, false, false, false));
-    bind(PersonIdent.class)
-        .annotatedWith(GerritPersonIdent.class)
-        .toProvider(GerritPersonIdentProvider.class);
-    bind(String.class)
-        .annotatedWith(AnonymousCowardName.class)
-        .toProvider(AnonymousCowardNameProvider.class);
+    bind(GerritOptions.class).toInstance(new GerritOptions(false, false, false));
 
-    bind(AllProjectsName.class).toProvider(AllProjectsNameProvider.class);
-    bind(AllUsersName.class).toProvider(AllUsersNameProvider.class);
     bind(GitRepositoryManager.class).to(InMemoryRepositoryManager.class);
     bind(InMemoryRepositoryManager.class).in(SINGLETON);
     bind(TrackingFooters.class).toProvider(TrackingFootersProvider.class).in(SINGLETON);
-    bind(MutableNotesMigration.class).toInstance(notesMigration);
-    bind(NotesMigration.class).to(MutableNotesMigration.class);
     bind(ListeningExecutorService.class)
         .annotatedWith(ChangeUpdateExecutor.class)
         .toInstance(MoreExecutors.newDirectExecutorService());
-    bind(DataSourceType.class).to(InMemoryH2Type.class);
-    bind(ChangeBundleReader.class).to(GwtormChangeBundleReader.class);
     bind(SecureStore.class).to(DefaultSecureStore.class);
 
-    TypeLiteral<SchemaFactory<ReviewDb>> schemaFactory =
-        new TypeLiteral<SchemaFactory<ReviewDb>>() {};
-    bind(schemaFactory).to(NotesMigrationSchemaFactory.class);
-    bind(Key.get(schemaFactory, ReviewDbFactory.class)).to(InMemoryDatabase.class);
-
+    install(new InMemorySchemaModule());
     install(NoSshKeyCache.module());
     install(new GerritInstanceNameModule());
     install(
@@ -243,7 +217,6 @@
     install(new FakeEmailSender.Module());
     install(new SignedTokenEmailTokenVerifier.Module());
     install(new GpgModule(cfg));
-    install(new InMemoryAccountPatchReviewStore.Module());
     install(new LocalMergeSuperSetComputation.Module());
 
     bind(AllAccountsIndexer.class).toProvider(Providers.of(null));
@@ -274,6 +247,41 @@
     install(new DefaultProjectNameLockManager.Module());
   }
 
+  /** Copy of SchemaModule with a slightly different server ID provider. */
+  // TODO(dborowitz): Better code sharing.
+  private class InMemorySchemaModule extends FactoryModule {
+    @Override
+    public void configure() {
+      bind(PersonIdent.class)
+          .annotatedWith(GerritPersonIdent.class)
+          .toProvider(GerritPersonIdentProvider.class);
+
+      bind(AllProjectsName.class).toProvider(AllProjectsNameProvider.class).in(SINGLETON);
+
+      bind(AllUsersName.class).toProvider(AllUsersNameProvider.class).in(SINGLETON);
+
+      bind(String.class)
+          .annotatedWith(AnonymousCowardName.class)
+          .toProvider(AnonymousCowardNameProvider.class);
+
+      bind(GroupIndexCollection.class);
+      bind(SchemaCreator.class).to(SchemaCreatorImpl.class);
+    }
+
+    @Provides
+    @Singleton
+    @GerritServerId
+    public String createServerId() {
+      String serverId =
+          cfg.getString(GerritServerIdProvider.SECTION, null, GerritServerIdProvider.KEY);
+      if (!Strings.isNullOrEmpty(serverId)) {
+        return serverId;
+      }
+
+      return "gerrit";
+    }
+  }
+
   @Provides
   @Singleton
   @SendEmailExecutor
@@ -288,25 +296,6 @@
     return queues.createQueue(2, "FanOut");
   }
 
-  @Provides
-  @Singleton
-  @GerritServerId
-  public String createServerId() {
-    String serverId =
-        cfg.getString(GerritServerIdProvider.SECTION, null, GerritServerIdProvider.KEY);
-    if (!Strings.isNullOrEmpty(serverId)) {
-      return serverId;
-    }
-
-    return "gerrit";
-  }
-
-  @Provides
-  @Singleton
-  InMemoryDatabase getInMemoryDatabase(SchemaCreator schemaCreator) throws OrmException {
-    return new InMemoryDatabase(schemaCreator);
-  }
-
   private Module luceneIndexModule() {
     return indexModule("com.google.gerrit.lucene.LuceneIndexModule");
   }
diff --git a/java/com/google/gerrit/testing/InMemoryTestEnvironment.java b/java/com/google/gerrit/testing/InMemoryTestEnvironment.java
index cebd139..05672aa 100644
--- a/java/com/google/gerrit/testing/InMemoryTestEnvironment.java
+++ b/java/com/google/gerrit/testing/InMemoryTestEnvironment.java
@@ -16,20 +16,15 @@
 
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.schema.SchemaCreator;
-import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Guice;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.Provider;
-import com.google.inject.util.Providers;
 import org.eclipse.jgit.lib.Config;
 import org.junit.rules.MethodRule;
 import org.junit.runners.model.FrameworkMethod;
@@ -39,7 +34,7 @@
  * An in-memory test environment for integration tests.
  *
  * <p>This test environment emulates the internals of a Gerrit server without starting a Gerrit
- * site. ReviewDb as well as NoteDb are represented by in-memory representations.
+ * site. Git repositories, including NoteDb, are stored in memory.
  *
  * <p>Each test is executed with a fresh and clean test environment. Hence, modifications applied in
  * one test don't carry over to subsequent ones.
@@ -49,13 +44,9 @@
 
   @Inject private AccountManager accountManager;
   @Inject private IdentifiedUser.GenericFactory userFactory;
-  @Inject private SchemaFactory<ReviewDb> schemaFactory;
   @Inject private SchemaCreator schemaCreator;
   @Inject private ThreadLocalRequestContext requestContext;
-  // Only for use in setting up/tearing down injector.
-  @Inject private InMemoryDatabase inMemoryDatabase;
 
-  private ReviewDb db;
   private LifecycleManager lifecycle;
 
   /** Create a test environment using an empty base config. */
@@ -92,40 +83,26 @@
 
   public void setApiUser(Account.Id id) {
     IdentifiedUser user = userFactory.create(id);
-    requestContext.setContext(
-        new RequestContext() {
-          @Override
-          public CurrentUser getUser() {
-            return user;
-          }
-
-          @Override
-          public Provider<ReviewDb> getReviewDbProvider() {
-            return Providers.of(db);
-          }
-        });
+    requestContext.setContext(() -> user);
   }
 
   private void setUp(Object target) throws Exception {
     Config cfg = configProvider.get();
     InMemoryModule.setDefaults(cfg);
 
-    Injector injector =
-        Guice.createInjector(new InMemoryModule(cfg, NoteDbMode.newNotesMigrationFromEnv()));
+    Injector injector = Guice.createInjector(new InMemoryModule(cfg));
     injector.injectMembers(this);
     lifecycle = new LifecycleManager();
     lifecycle.add(injector);
     lifecycle.start();
 
-    try (ReviewDb underlyingDb = inMemoryDatabase.getDatabase().open()) {
-      schemaCreator.create(underlyingDb);
-    }
-    db = schemaFactory.open();
+    schemaCreator.create();
 
     // The first user is added to the "Administrators" group. See AccountManager#create().
     setApiUser(accountManager.authenticate(AuthRequest.forUser("admin")).getAccountId());
 
-    // Inject target members after setting API user, so it can @Inject a ReviewDb if it wants.
+    // Inject target members after setting API user, so it can @Inject request-scoped objects if it
+    // wants.
     injector.injectMembers(target);
   }
 
@@ -136,9 +113,5 @@
     if (requestContext != null) {
       requestContext.setContext(null);
     }
-    if (db != null) {
-      db.close();
-    }
-    InMemoryDatabase.drop(inMemoryDatabase);
   }
 }
diff --git a/java/com/google/gerrit/testing/NoteDbChecker.java b/java/com/google/gerrit/testing/NoteDbChecker.java
deleted file mode 100644
index 1dc8ee2..0000000
--- a/java/com/google/gerrit/testing/NoteDbChecker.java
+++ /dev/null
@@ -1,225 +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.testing;
-
-import static com.google.common.truth.Truth.assertThat;
-import static java.util.Comparator.comparing;
-import static java.util.stream.Collectors.toList;
-
-import com.google.common.base.Joiner;
-import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.ListMultimap;
-import com.google.common.flogger.FluentLogger;
-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.server.ReviewDb;
-import com.google.gerrit.reviewdb.server.ReviewDbUtil;
-import com.google.gerrit.server.CommentsUtil;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.notedb.ChangeBundle;
-import com.google.gerrit.server.notedb.ChangeBundleReader;
-import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.notedb.MutableNotesMigration;
-import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder;
-import com.google.gwtorm.client.IntKey;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.OrmRuntimeException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.stream.Stream;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Repository;
-import org.junit.runner.Description;
-
-@Singleton
-public class NoteDbChecker {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
-  private final Provider<ReviewDb> dbProvider;
-  private final GitRepositoryManager repoManager;
-  private final MutableNotesMigration notesMigration;
-  private final ChangeBundleReader bundleReader;
-  private final ChangeNotes.Factory notesFactory;
-  private final ChangeRebuilder changeRebuilder;
-  private final CommentsUtil commentsUtil;
-
-  @Inject
-  NoteDbChecker(
-      Provider<ReviewDb> dbProvider,
-      GitRepositoryManager repoManager,
-      MutableNotesMigration notesMigration,
-      ChangeBundleReader bundleReader,
-      ChangeNotes.Factory notesFactory,
-      ChangeRebuilder changeRebuilder,
-      CommentsUtil commentsUtil) {
-    this.dbProvider = dbProvider;
-    this.repoManager = repoManager;
-    this.bundleReader = bundleReader;
-    this.notesMigration = notesMigration;
-    this.notesFactory = notesFactory;
-    this.changeRebuilder = changeRebuilder;
-    this.commentsUtil = commentsUtil;
-  }
-
-  public void rebuildAndCheckAllChanges() throws Exception {
-    rebuildAndCheckChanges(
-        getUnwrappedDb().changes().all().toList().stream().map(Change::getId),
-        ImmutableListMultimap.of());
-  }
-
-  public void rebuildAndCheckChanges(Change.Id... changeIds) throws Exception {
-    rebuildAndCheckChanges(Arrays.stream(changeIds), ImmutableListMultimap.of());
-  }
-
-  private void rebuildAndCheckChanges(
-      Stream<Change.Id> changeIds, ListMultimap<Change.Id, String> expectedDiffs) throws Exception {
-    ReviewDb db = getUnwrappedDb();
-
-    List<ChangeBundle> allExpected = readExpected(changeIds);
-
-    boolean oldWrite = notesMigration.rawWriteChangesSetting();
-    boolean oldRead = notesMigration.readChanges();
-    try {
-      notesMigration.setWriteChanges(true);
-      notesMigration.setReadChanges(true);
-      List<String> msgs = new ArrayList<>();
-      for (ChangeBundle expected : allExpected) {
-        Change c = expected.getChange();
-        try {
-          changeRebuilder.rebuild(db, c.getId());
-        } catch (RepositoryNotFoundException e) {
-          msgs.add("Repository not found for change, cannot convert: " + c);
-        }
-      }
-
-      checkActual(allExpected, expectedDiffs, msgs);
-    } finally {
-      notesMigration.setReadChanges(oldRead);
-      notesMigration.setWriteChanges(oldWrite);
-    }
-  }
-
-  public void checkChanges(Change.Id... changeIds) throws Exception {
-    checkActual(
-        readExpected(Arrays.stream(changeIds)), ImmutableListMultimap.of(), new ArrayList<>());
-  }
-
-  public void rebuildAndCheckChange(Change.Id changeId, String... expectedDiff) throws Exception {
-    ImmutableListMultimap.Builder<Change.Id, String> b = ImmutableListMultimap.builder();
-    b.putAll(changeId, Arrays.asList(expectedDiff));
-    rebuildAndCheckChanges(Stream.of(changeId), b.build());
-  }
-
-  public void assertNoChangeRef(Project.NameKey project, Change.Id changeId) throws Exception {
-    try (Repository repo = repoManager.openRepository(project)) {
-      assertThat(repo.exactRef(RefNames.changeMetaRef(changeId))).isNull();
-    }
-  }
-
-  public void assertNoReviewDbChanges(Description desc) throws Exception {
-    ReviewDb db = getUnwrappedDb();
-    assertThat(db.changes().all().toList()).named("Changes in " + desc.getTestClass()).isEmpty();
-    assertThat(db.changeMessages().all().toList())
-        .named("ChangeMessages in " + desc.getTestClass())
-        .isEmpty();
-    assertThat(db.patchSets().all().toList())
-        .named("PatchSets in " + desc.getTestClass())
-        .isEmpty();
-    assertThat(db.patchSetApprovals().all().toList())
-        .named("PatchSetApprovals in " + desc.getTestClass())
-        .isEmpty();
-    assertThat(db.patchComments().all().toList())
-        .named("PatchLineComments in " + desc.getTestClass())
-        .isEmpty();
-  }
-
-  private List<ChangeBundle> readExpected(Stream<Change.Id> changeIds) throws Exception {
-    boolean old = notesMigration.readChanges();
-    try {
-      notesMigration.setReadChanges(false);
-      return changeIds
-          .sorted(comparing(IntKey::get))
-          .map(this::readBundleUnchecked)
-          .collect(toList());
-    } finally {
-      notesMigration.setReadChanges(old);
-    }
-  }
-
-  private ChangeBundle readBundleUnchecked(Change.Id id) {
-    try {
-      return bundleReader.fromReviewDb(getUnwrappedDb(), id);
-    } catch (OrmException e) {
-      throw new OrmRuntimeException(e);
-    }
-  }
-
-  private void checkActual(
-      List<ChangeBundle> allExpected,
-      ListMultimap<Change.Id, String> expectedDiffs,
-      List<String> msgs)
-      throws Exception {
-    ReviewDb db = getUnwrappedDb();
-    boolean oldRead = notesMigration.readChanges();
-    boolean oldWrite = notesMigration.rawWriteChangesSetting();
-    try {
-      notesMigration.setWriteChanges(true);
-      notesMigration.setReadChanges(true);
-      for (ChangeBundle expected : allExpected) {
-        Change c = expected.getChange();
-        ChangeBundle actual;
-        try {
-          actual =
-              ChangeBundle.fromNotes(
-                  commentsUtil, notesFactory.create(db, c.getProject(), c.getId()));
-        } catch (Throwable t) {
-          String msg = "Error converting change: " + c;
-          msgs.add(msg);
-          logger.atSevere().withCause(t).log(msg);
-          continue;
-        }
-        List<String> diff = expected.differencesFrom(actual);
-        List<String> expectedDiff = expectedDiffs.get(c.getId());
-        if (!diff.equals(expectedDiff)) {
-          msgs.add("Differences between ReviewDb and NoteDb for " + c + ":");
-          msgs.addAll(diff);
-          if (!expectedDiff.isEmpty()) {
-            msgs.add("Expected differences:");
-            msgs.addAll(expectedDiff);
-          }
-          msgs.add("");
-        } else {
-          System.err.println("NoteDb conversion of change " + c.getId() + " successful");
-        }
-      }
-    } finally {
-      notesMigration.setReadChanges(oldRead);
-      notesMigration.setWriteChanges(oldWrite);
-    }
-    if (!msgs.isEmpty()) {
-      throw new AssertionError(Joiner.on('\n').join(msgs));
-    }
-  }
-
-  private ReviewDb getUnwrappedDb() {
-    ReviewDb db = dbProvider.get();
-    return ReviewDbUtil.unwrapDb(db);
-  }
-}
diff --git a/java/com/google/gerrit/testing/NoteDbMode.java b/java/com/google/gerrit/testing/NoteDbMode.java
deleted file mode 100644
index d4a7c7e..0000000
--- a/java/com/google/gerrit/testing/NoteDbMode.java
+++ /dev/null
@@ -1,87 +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.testing;
-
-import static com.google.common.base.Preconditions.checkArgument;
-
-import com.google.common.base.Enums;
-import com.google.common.base.Strings;
-import com.google.gerrit.server.notedb.MutableNotesMigration;
-import com.google.gerrit.server.notedb.NotesMigrationState;
-
-public enum NoteDbMode {
-  /** NoteDb is disabled. */
-  OFF(NotesMigrationState.REVIEW_DB),
-
-  /** Writing data to NoteDb is enabled. */
-  WRITE(NotesMigrationState.WRITE),
-
-  /** Reading and writing all data to NoteDb is enabled. */
-  READ_WRITE(NotesMigrationState.READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY),
-
-  /** Changes are created with their primary storage as NoteDb. */
-  PRIMARY(NotesMigrationState.READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY),
-
-  /** All change tables are entirely disabled, and code/meta ref updates are fused. */
-  ON(NotesMigrationState.NOTE_DB),
-
-  /**
-   * Run tests with NoteDb disabled, then convert ReviewDb to NoteDb and check that the results
-   * match.
-   */
-  CHECK(NotesMigrationState.REVIEW_DB);
-
-  private static final String ENV_VAR = "GERRIT_NOTEDB";
-  private static final String SYS_PROP = "gerrit.notedb";
-
-  public static NoteDbMode get() {
-    String value = System.getenv(ENV_VAR);
-    if (Strings.isNullOrEmpty(value)) {
-      value = System.getProperty(SYS_PROP);
-    }
-    if (Strings.isNullOrEmpty(value)) {
-      return OFF;
-    }
-    value = value.toUpperCase().replace("-", "_");
-    NoteDbMode mode = Enums.getIfPresent(NoteDbMode.class, value).orNull();
-    if (!Strings.isNullOrEmpty(System.getenv(ENV_VAR))) {
-      checkArgument(
-          mode != null, "Invalid value for env variable %s: %s", ENV_VAR, System.getenv(ENV_VAR));
-    } else {
-      checkArgument(
-          mode != null,
-          "Invalid value for system property %s: %s",
-          SYS_PROP,
-          System.getProperty(SYS_PROP));
-    }
-    return mode;
-  }
-
-  public static MutableNotesMigration newNotesMigrationFromEnv() {
-    MutableNotesMigration m = MutableNotesMigration.newDisabled();
-    resetFromEnv(m);
-    return m;
-  }
-
-  public static void resetFromEnv(MutableNotesMigration migration) {
-    migration.setFrom(get().state);
-  }
-
-  private final NotesMigrationState state;
-
-  private NoteDbMode(NotesMigrationState state) {
-    this.state = state;
-  }
-}
diff --git a/java/com/google/gerrit/testing/TempFileUtil.java b/java/com/google/gerrit/testing/TempFileUtil.java
deleted file mode 100644
index c42bd74..0000000
--- a/java/com/google/gerrit/testing/TempFileUtil.java
+++ /dev/null
@@ -1,66 +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.testing;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
-public class TempFileUtil {
-  private static List<File> allDirsCreated = new ArrayList<>();
-
-  public static synchronized File createTempDirectory() throws IOException {
-    File tmp = File.createTempFile("gerrit_test_", "").getCanonicalFile();
-    if (!tmp.delete() || !tmp.mkdir()) {
-      throw new IOException("Cannot create " + tmp.getPath());
-    }
-    allDirsCreated.add(tmp);
-    return tmp;
-  }
-
-  public static synchronized void cleanup() throws IOException {
-    for (File dir : allDirsCreated) {
-      recursivelyDelete(dir);
-    }
-    allDirsCreated.clear();
-  }
-
-  public static void recursivelyDelete(File dir) throws IOException {
-    if (!dir.getPath().equals(dir.getCanonicalPath())) {
-      // Directory symlink reaching outside of temporary space.
-      return;
-    }
-    File[] contents = dir.listFiles();
-    if (contents != null) {
-      for (File d : contents) {
-        if (d.isDirectory()) {
-          recursivelyDelete(d);
-        } else {
-          deleteNowOrOnExit(d);
-        }
-      }
-    }
-    deleteNowOrOnExit(dir);
-  }
-
-  private static void deleteNowOrOnExit(File dir) {
-    if (!dir.delete()) {
-      dir.deleteOnExit();
-    }
-  }
-
-  private TempFileUtil() {}
-}
diff --git a/java/com/google/gerrit/testing/TestChanges.java b/java/com/google/gerrit/testing/TestChanges.java
index 8e752fa..d0a939d 100644
--- a/java/com/google/gerrit/testing/TestChanges.java
+++ b/java/com/google/gerrit/testing/TestChanges.java
@@ -30,7 +30,6 @@
 import com.google.gerrit.server.notedb.AbstractChangeNotes;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Injector;
 import java.util.TimeZone;
@@ -77,8 +76,8 @@
     return ps;
   }
 
-  public static ChangeUpdate newUpdate(Injector injector, Change c, CurrentUser user)
-      throws Exception {
+  public static ChangeUpdate newUpdate(
+      Injector injector, Change c, CurrentUser user, boolean shouldExist) throws Exception {
     injector =
         injector.createChildInjector(
             new FactoryModule() {
@@ -91,23 +90,24 @@
         injector
             .getInstance(ChangeUpdate.Factory.class)
             .create(
-                new ChangeNotes(injector.getInstance(AbstractChangeNotes.Args.class), c).load(),
+                new ChangeNotes(
+                        injector.getInstance(AbstractChangeNotes.Args.class), c, shouldExist, null)
+                    .load(),
                 user,
                 TimeUtil.nowTs(),
-                Ordering.<String>natural());
+                Ordering.natural());
 
     ChangeNotes notes = update.getNotes();
     boolean hasPatchSets = notes.getPatchSets() != null && !notes.getPatchSets().isEmpty();
-    NotesMigration migration = injector.getInstance(NotesMigration.class);
-    if (hasPatchSets || !migration.readChanges()) {
+    if (hasPatchSets) {
       return update;
     }
 
     // Change doesn't exist yet. NoteDb requires that there be a commit for the
     // first patch set, so create one.
     GitRepositoryManager repoManager = injector.getInstance(GitRepositoryManager.class);
-    try (Repository repo = repoManager.openRepository(c.getProject())) {
-      TestRepository<Repository> tr = new TestRepository<>(repo);
+    try (Repository repo = repoManager.openRepository(c.getProject());
+        TestRepository<Repository> tr = new TestRepository<>(repo)) {
       PersonIdent ident =
           user.asIdentifiedUser().newCommitterIdent(update.getWhen(), TimeZone.getDefault());
       TestRepository<Repository>.CommitBuilder cb =
diff --git a/java/com/google/gerrit/testing/TestTimeUtil.java b/java/com/google/gerrit/testing/TestTimeUtil.java
index 9228123..2020e5d 100644
--- a/java/com/google/gerrit/testing/TestTimeUtil.java
+++ b/java/com/google/gerrit/testing/TestTimeUtil.java
@@ -118,6 +118,15 @@
     clockMs.addAndGet(clockStepUnit.toMillis(clockStep));
   }
 
+  /**
+   * Returns the current timestamp.
+   *
+   * @return current timestamp
+   */
+  public static synchronized Timestamp getCurrentTimestamp() {
+    return new Timestamp(clockMs.get());
+  }
+
   /** Reset the clock to use the actual system clock. */
   public static synchronized void useSystemTime() {
     clockMs = null;
diff --git a/java/com/google/gerrit/testing/TestUpdateUI.java b/java/com/google/gerrit/testing/TestUpdateUI.java
index f36fc7e..76671fb 100644
--- a/java/com/google/gerrit/testing/TestUpdateUI.java
+++ b/java/com/google/gerrit/testing/TestUpdateUI.java
@@ -15,9 +15,6 @@
 package com.google.gerrit.testing;
 
 import com.google.gerrit.server.schema.UpdateUI;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.StatementExecutor;
-import java.util.List;
 import java.util.Set;
 
 public class TestUpdateUI implements UpdateUI {
@@ -41,11 +38,4 @@
   public boolean isBatch() {
     return true;
   }
-
-  @Override
-  public void pruneSchema(StatementExecutor e, List<String> pruneList) throws OrmException {
-    for (String sql : pruneList) {
-      e.execute(sql);
-    }
-  }
 }
diff --git a/java/com/google/gerrit/truth/BUILD b/java/com/google/gerrit/truth/BUILD
index b832543..f21e3c9 100644
--- a/java/com/google/gerrit/truth/BUILD
+++ b/java/com/google/gerrit/truth/BUILD
@@ -6,6 +6,7 @@
     srcs = glob(["**/*.java"]),
     visibility = ["//visibility:public"],
     deps = [
+        "//java/com/google/gerrit/common:annotations",
         "//lib:guava",
         "//lib/truth",
     ],
diff --git a/java/com/google/gerrit/truth/CacheStatsSubject.java b/java/com/google/gerrit/truth/CacheStatsSubject.java
new file mode 100644
index 0000000..f1a9393
--- /dev/null
+++ b/java/com/google/gerrit/truth/CacheStatsSubject.java
@@ -0,0 +1,62 @@
+// 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.cache.CacheStats;
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.Subject;
+import com.google.gerrit.common.UsedAt;
+import com.google.gerrit.common.UsedAt.Project;
+
+@UsedAt(Project.PLUGINS_ALL)
+public class CacheStatsSubject extends Subject<CacheStatsSubject, CacheStats> {
+  public static CacheStatsSubject assertThat(CacheStats stats) {
+    return assertAbout(CacheStatsSubject::new).that(stats);
+  }
+
+  public static CacheStats cloneStats(CacheStats other) {
+    return new CacheStats(
+        other.hitCount(),
+        other.missCount(),
+        other.loadSuccessCount(),
+        other.loadExceptionCount(),
+        other.totalLoadTime(),
+        other.evictionCount());
+  }
+
+  private CacheStats start = new CacheStats(0, 0, 0, 0, 0, 0);
+
+  private CacheStatsSubject(FailureMetadata failureMetadata, CacheStats stats) {
+    super(failureMetadata, stats);
+  }
+
+  public CacheStatsSubject since(CacheStats start) {
+    this.start = requireNonNull(start);
+    return this;
+  }
+
+  public void hasHitCount(int expectedHitCount) {
+    isNotNull();
+    check("hitCount()").that(actual().minus(start).hitCount()).isEqualTo(expectedHitCount);
+  }
+
+  public void hasMissCount(int expectedMissCount) {
+    isNotNull();
+    check("missCount()").that(actual().minus(start).missCount()).isEqualTo(expectedMissCount);
+  }
+}
diff --git a/java/com/google/gerrit/truth/ListSubject.java b/java/com/google/gerrit/truth/ListSubject.java
index bd9df30..9a839dd 100644
--- a/java/com/google/gerrit/truth/ListSubject.java
+++ b/java/com/google/gerrit/truth/ListSubject.java
@@ -18,28 +18,34 @@
 import static com.google.common.truth.Fact.fact;
 import static com.google.common.truth.Truth.assertAbout;
 
+import com.google.common.collect.Iterables;
+import com.google.common.truth.CustomSubjectBuilder;
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.IterableSubject;
+import com.google.common.truth.StandardSubjectBuilder;
 import com.google.common.truth.Subject;
 import java.util.List;
-import java.util.function.Function;
+import java.util.function.BiFunction;
 
 public class ListSubject<S extends Subject<S, E>, E> extends IterableSubject {
 
-  private final Function<E, S> elementAssertThatFunction;
+  private final BiFunction<StandardSubjectBuilder, E, S> elementSubjectCreator;
 
-  @SuppressWarnings("unchecked")
   public static <S extends Subject<S, E>, E> ListSubject<S, E> assertThat(
-      List<E> list, Function<E, S> elementAssertThatFunction) {
-    // The ListSubjectFactory always returns ListSubjects. -> Casting is appropriate.
-    return (ListSubject<S, E>)
-        assertAbout(new ListSubjectFactory<>(elementAssertThatFunction)).that(list);
+      List<E> list, Subject.Factory<S, E> subjectFactory) {
+    return assertAbout(elements()).thatCustom(list, subjectFactory);
+  }
+
+  public static CustomSubjectBuilder.Factory<ListSubjectBuilder> elements() {
+    return ListSubjectBuilder::new;
   }
 
   private ListSubject(
-      FailureMetadata failureMetadata, List<E> list, Function<E, S> elementAssertThatFunction) {
+      FailureMetadata failureMetadata,
+      List<E> list,
+      BiFunction<StandardSubjectBuilder, E, S> elementSubjectCreator) {
     super(failureMetadata, list);
-    this.elementAssertThatFunction = elementAssertThatFunction;
+    this.elementSubjectCreator = elementSubjectCreator;
   }
 
   public S element(int index) {
@@ -49,20 +55,21 @@
     if (index >= list.size()) {
       failWithoutActual(fact("expected to have element at index", index));
     }
-    return elementAssertThatFunction.apply(list.get(index));
+    return elementSubjectCreator.apply(check("element(%s)", index), list.get(index));
   }
 
   public S onlyElement() {
     isNotNull();
     hasSize(1);
-    return element(0);
+    List<E> list = getActualList();
+    return elementSubjectCreator.apply(check("onlyElement()"), Iterables.getOnlyElement(list));
   }
 
   public S lastElement() {
     isNotNull();
     isNotEmpty();
     List<E> list = getActualList();
-    return element(list.size() - 1);
+    return elementSubjectCreator.apply(check("lastElement()"), Iterables.getLast(list));
   }
 
   @SuppressWarnings("unchecked")
@@ -78,20 +85,20 @@
     return (ListSubject<S, E>) super.named(s, objects);
   }
 
-  private static class ListSubjectFactory<S extends Subject<S, T>, T>
-      implements Subject.Factory<IterableSubject, Iterable<?>> {
+  public static class ListSubjectBuilder extends CustomSubjectBuilder {
 
-    private Function<T, S> elementAssertThatFunction;
-
-    ListSubjectFactory(Function<T, S> elementAssertThatFunction) {
-      this.elementAssertThatFunction = elementAssertThatFunction;
+    ListSubjectBuilder(FailureMetadata failureMetadata) {
+      super(failureMetadata);
     }
 
-    @SuppressWarnings("unchecked")
-    @Override
-    public ListSubject<S, T> createSubject(FailureMetadata failureMetadata, Iterable<?> objects) {
-      // The constructor of ListSubject only accepts lists. -> Casting is appropriate.
-      return new ListSubject<>(failureMetadata, (List<T>) objects, elementAssertThatFunction);
+    public <S extends Subject<S, E>, E> ListSubject<S, E> thatCustom(
+        List<E> list, Subject.Factory<S, 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) {
+      return new ListSubject<>(metadata(), list, elementSubjectCreator);
     }
   }
 }
diff --git a/java/com/google/gerrit/truth/OptionalSubject.java b/java/com/google/gerrit/truth/OptionalSubject.java
index d91f07b..b5fc5d0 100644
--- a/java/com/google/gerrit/truth/OptionalSubject.java
+++ b/java/com/google/gerrit/truth/OptionalSubject.java
@@ -17,41 +17,52 @@
 import static com.google.common.truth.Fact.fact;
 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 com.google.common.truth.Truth;
 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>> {
 
-  private final Function<? super T, ? extends S> valueAssertThatFunction;
+  private final BiFunction<StandardSubjectBuilder, ? super T, ? extends S> valueSubjectCreator;
 
-  public static <S extends Subject<S, ? super T>, T> OptionalSubject<S, T> assertThat(
+  // 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) {
-    OptionalSubjectFactory<S, T> optionalSubjectFactory =
-        new OptionalSubjectFactory<>(elementAssertThatFunction);
-    return assertAbout(optionalSubjectFactory).that(optional);
+    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) {
+    return assertAbout(optionals()).thatCustom(optional, valueSubjectFactory);
   }
 
   public static OptionalSubject<DefaultSubject, ?> assertThat(Optional<?> optional) {
-    // Unfortunately, we need to cast to DefaultSubject as Truth.assertThat()
+    // 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.
-    Function<Object, DefaultSubject> valueAssertThatFunction =
-        value -> (DefaultSubject) Truth.assertThat(value);
-    return assertThat(optional, valueAssertThatFunction);
+    return assertAbout(optionals())
+        .that(optional, (builder, value) -> (DefaultSubject) builder.that(value));
+  }
+
+  public static CustomSubjectBuilder.Factory<OptionalSubjectBuilder> optionals() {
+    return OptionalSubjectBuilder::new;
   }
 
   private OptionalSubject(
       FailureMetadata failureMetadata,
       Optional<T> optional,
-      Function<? super T, ? extends S> valueAssertThatFunction) {
+      BiFunction<StandardSubjectBuilder, ? super T, ? extends S> valueSubjectCreator) {
     super(failureMetadata, optional);
-    this.valueAssertThatFunction = valueAssertThatFunction;
+    this.valueSubjectCreator = valueSubjectCreator;
   }
 
   public void isPresent() {
@@ -78,22 +89,28 @@
     isNotNull();
     isPresent();
     Optional<T> optional = actual();
-    return valueAssertThatFunction.apply(optional.get());
+    return valueSubjectCreator.apply(check("value()"), optional.get());
   }
 
-  private static class OptionalSubjectFactory<S extends Subject<S, ? super T>, T>
-      implements Subject.Factory<OptionalSubject<S, T>, Optional<T>> {
+  public static class OptionalSubjectBuilder extends CustomSubjectBuilder {
 
-    private Function<? super T, ? extends S> valueAssertThatFunction;
-
-    OptionalSubjectFactory(Function<? super T, ? extends S> valueAssertThatFunction) {
-      this.valueAssertThatFunction = valueAssertThatFunction;
+    OptionalSubjectBuilder(FailureMetadata failureMetadata) {
+      super(failureMetadata);
     }
 
-    @Override
-    public OptionalSubject<S, T> createSubject(
-        FailureMetadata failureMetadata, Optional<T> optional) {
-      return new OptionalSubject<>(failureMetadata, optional, valueAssertThatFunction);
+    public <S extends Subject<S, T>, T> OptionalSubject<S, T> thatCustom(
+        Optional<T> optional, Subject.Factory<S, 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 <S extends Subject<S, ? super T>, 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/BUILD b/java/com/google/gerrit/util/cli/BUILD
index 754cb76..e4f2c21 100644
--- a/java/com/google/gerrit/util/cli/BUILD
+++ b/java/com/google/gerrit/util/cli/BUILD
@@ -9,6 +9,7 @@
         "//java/com/google/gerrit/common:server",
         "//lib:args4j",
         "//lib:guava",
+        "//lib/auto:auto-value-annotations",
         "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
diff --git a/java/com/google/gerrit/util/cli/CmdLineParser.java b/java/com/google/gerrit/util/cli/CmdLineParser.java
index 5b7ea3f..1c16133 100644
--- a/java/com/google/gerrit/util/cli/CmdLineParser.java
+++ b/java/com/google/gerrit/util/cli/CmdLineParser.java
@@ -34,7 +34,9 @@
 
 package com.google.gerrit.util.cli;
 
+import static com.google.common.base.Preconditions.checkArgument;
 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.ListMultimap;
@@ -45,13 +47,14 @@
 import com.google.inject.assistedinject.Assisted;
 import java.io.StringWriter;
 import java.io.Writer;
-import java.lang.annotation.Annotation;
-import java.lang.reflect.AnnotatedElement;
 import java.lang.reflect.Field;
 import java.lang.reflect.Method;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.ResourceBundle;
@@ -65,7 +68,6 @@
 import org.kohsuke.args4j.ParserProperties;
 import org.kohsuke.args4j.spi.BooleanOptionHandler;
 import org.kohsuke.args4j.spi.EnumOptionHandler;
-import org.kohsuke.args4j.spi.FieldSetter;
 import org.kohsuke.args4j.spi.MethodSetter;
 import org.kohsuke.args4j.spi.OptionHandler;
 import org.kohsuke.args4j.spi.Setter;
@@ -85,6 +87,89 @@
     CmdLineParser create(Object bean);
   }
 
+  /**
+   * This may be used by an option handler during parsing to "call" additional parameters simulating
+   * as if they had been passed from the command line originally.
+   *
+   * <p>To call additional parameters from within an option handler, instantiate this class with the
+   * parameters and then call callParameters() with the additional parameters to be parsed.
+   * OptionHandlers may optionally pass this class to other methods which may then both
+   * parse/consume more parameters and call additional parameters.
+   */
+  public static class Parameters implements org.kohsuke.args4j.spi.Parameters {
+    protected final String[] args;
+    protected MyParser parser;
+    protected int consumed = 0;
+
+    public Parameters(org.kohsuke.args4j.spi.Parameters args, MyParser parser)
+        throws CmdLineException {
+      this.args = new String[args.size()];
+      for (int i = 0; i < args.size(); i++) {
+        this.args[i] = args.getParameter(i);
+      }
+      this.parser = parser;
+    }
+
+    public Parameters(String[] args, MyParser parser) {
+      this.args = args;
+      this.parser = parser;
+    }
+
+    @Override
+    public String getParameter(int idx) throws CmdLineException {
+      return args[idx];
+    }
+
+    /**
+     * get and consume (consider parsed) a parameter
+     *
+     * @return the consumed parameter
+     */
+    public String consumeParameter() throws CmdLineException {
+      return getParameter(consumed++);
+    }
+
+    @Override
+    public int size() {
+      return args.length;
+    }
+
+    /**
+     * Add 'count' to the value of parsed parameters. May be called more than once.
+     *
+     * @param count How many parameters were just parsed.
+     */
+    public void consume(int count) {
+      consumed += count;
+    }
+
+    /**
+     * Reports handlers how many parameters were parsed
+     *
+     * @return the count of parsed parameters
+     */
+    public int getConsumed() {
+      return consumed;
+    }
+
+    /**
+     * Use during parsing to call additional parameters simulating as if they had been passed from
+     * the command line originally.
+     *
+     * @param args A variable amount of parameters to call immediately
+     *     <p>The parameters will be parsed immediately, before the remaining parameter will be
+     *     parsed.
+     *     <p>Note: Since this is done outside of the arg4j parsing loop, it will not match exactly
+     *     what would happen if they were actually passed from the command line, but it will be
+     *     pretty close. If this were moved to args4j, the interface could be the same and it could
+     *     match exactly the behavior as if passed from the command line originally.
+     */
+    public void callParameters(String... args) throws CmdLineException {
+      Parameters impl = new Parameters(Arrays.copyOfRange(args, 1, args.length), parser);
+      parser.findOptionByName(args[0]).parseArguments(impl);
+    }
+  }
+
   private final OptionHandlers handlers;
   private final MyParser parser;
 
@@ -200,7 +285,7 @@
   }
 
   public boolean wasHelpRequestedByOption() {
-    return parser.help.value;
+    return parser.help;
   }
 
   public void parseArgument(String... args) throws CmdLineException {
@@ -270,6 +355,10 @@
     parser.parseWithPrefix(prefix, bean);
   }
 
+  public void drainOptionQueue() {
+    parser.addOptionsWithMetRequirements();
+  }
+
   private String makeOption(String name) {
     if (!name.startsWith("-")) {
       if (name.length() == 1) {
@@ -330,92 +419,82 @@
     throw new CmdLineException(parser, localizable("invalid boolean \"%s=%s\""), name, value);
   }
 
-  private static class PrefixedOption implements Option {
-    private final String prefix;
-    private final Option o;
-
-    PrefixedOption(String prefix, Option o) {
-      this.prefix = prefix;
-      this.o = o;
-    }
-
-    @Override
-    public String name() {
-      return getPrefixedName(prefix, o.name());
-    }
-
-    @Override
-    public String[] aliases() {
-      String[] prefixedAliases = new String[o.aliases().length];
-      for (int i = 0; i < prefixedAliases.length; i++) {
-        prefixedAliases[i] = getPrefixedName(prefix, o.aliases()[i]);
-      }
-      return prefixedAliases;
-    }
-
-    @Override
-    public String usage() {
-      return o.usage();
-    }
-
-    @Override
-    public String metaVar() {
-      return o.metaVar();
-    }
-
-    @Override
-    public boolean required() {
-      return o.required();
-    }
-
-    @Override
-    public boolean hidden() {
-      return o.hidden();
-    }
-
-    @SuppressWarnings("rawtypes")
-    @Override
-    public Class<? extends OptionHandler> handler() {
-      return o.handler();
-    }
-
-    @Override
-    public String[] depends() {
-      return o.depends();
-    }
-
-    @Override
-    public String[] forbids() {
-      return null;
-    }
-
-    @Override
-    public boolean help() {
-      return false;
-    }
-
-    @Override
-    public Class<? extends Annotation> annotationType() {
-      return o.annotationType();
-    }
-
-    private static String getPrefixedName(String prefix, String name) {
-      return prefix + name;
-    }
+  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);
+    return OptionUtil.newOption(
+        prefix + o.name(),
+        aliases,
+        o.usage(),
+        o.metaVar(),
+        o.required(),
+        false,
+        o.hidden(),
+        o.handler(),
+        o.depends(),
+        new String[0]);
   }
 
-  private class MyParser extends org.kohsuke.args4j.CmdLineParser {
+  public class MyParser extends org.kohsuke.args4j.CmdLineParser {
+    boolean help;
+
     @SuppressWarnings("rawtypes")
     private List<OptionHandler> optionsList;
 
-    private HelpOption help;
+    private Map<String, QueuedOption> queuedOptionsByName = new LinkedHashMap<>();
+
+    private class QueuedOption {
+      public final Option option;
+
+      @SuppressWarnings("rawtypes")
+      public final Setter setter;
+
+      public final String[] requiredOptions;
+
+      private QueuedOption(
+          Option option,
+          @SuppressWarnings("rawtypes") Setter setter,
+          RequiresOptions requiresOptions) {
+        this.option = option;
+        this.setter = setter;
+        this.requiredOptions = requiresOptions != null ? requiresOptions.value() : new String[0];
+      }
+    }
 
     MyParser(Object bean) {
       super(bean, ParserProperties.defaults().withAtSyntax(false));
       parseAdditionalOptions(bean, new HashSet<>());
+      addOptionsWithMetRequirements();
       ensureOptionsInitialized();
     }
 
+    public int addOptionsWithMetRequirements() {
+      int count = 0;
+      for (Iterator<Map.Entry<String, QueuedOption>> it = queuedOptionsByName.entrySet().iterator();
+          it.hasNext(); ) {
+        QueuedOption queuedOption = it.next().getValue();
+        if (hasAllRequiredOptions(queuedOption)) {
+          addOption(queuedOption.setter, queuedOption.option);
+          it.remove();
+          count++;
+        }
+      }
+      if (count > 0) {
+        count += addOptionsWithMetRequirements();
+      }
+      return count;
+    }
+
+    private boolean hasAllRequiredOptions(QueuedOption queuedOption) {
+      for (String name : queuedOption.requiredOptions) {
+        if (findOptionByName(name) == null) {
+          return false;
+        }
+      }
+      return true;
+    }
+
     // NOTE: Argument annotations on bean are ignored.
     public void parseWithPrefix(String prefix, Object bean) {
       parseWithPrefix(prefix, bean, new HashSet<>());
@@ -430,13 +509,19 @@
         for (Method m : c.getDeclaredMethods()) {
           Option o = m.getAnnotation(Option.class);
           if (o != null) {
-            addOption(new MethodSetter(this, bean, m), new PrefixedOption(prefix, o));
+            queueOption(
+                newPrefixedOption(prefix, o),
+                new MethodSetter(this, bean, m),
+                m.getAnnotation(RequiresOptions.class));
           }
         }
         for (Field f : c.getDeclaredFields()) {
           Option o = f.getAnnotation(Option.class);
           if (o != null) {
-            addOption(Setters.create(f, bean), new PrefixedOption(prefix, o));
+            queueOption(
+                newPrefixedOption(prefix, o),
+                Setters.create(f, bean),
+                f.getAnnotation(RequiresOptions.class));
           }
           if (f.isAnnotationPresent(Options.class)) {
             try {
@@ -480,6 +565,41 @@
       return add(super.createOptionHandler(option, setter));
     }
 
+    /**
+     * Finds a registered {@code OptionHandler} by its name or its alias.
+     *
+     * @param name name
+     * @return the {@code OptionHandler} or {@code null}
+     *     <p>Note: this is cut & pasted from the parent class in arg4j, it was private and it
+     *     needed to be exposed.
+     */
+    @SuppressWarnings("rawtypes")
+    public OptionHandler findOptionByName(String name) {
+      for (OptionHandler h : optionsList) {
+        NamedOptionDef option = (NamedOptionDef) h.option;
+        if (name.equals(option.name())) {
+          return h;
+        }
+        for (String alias : option.aliases()) {
+          if (name.equals(alias)) {
+            return h;
+          }
+        }
+      }
+      return null;
+    }
+
+    private void queueOption(
+        Option option,
+        @SuppressWarnings("rawtypes") Setter setter,
+        RequiresOptions requiresOptions) {
+      if (queuedOptionsByName.put(option.name(), new QueuedOption(option, setter, requiresOptions))
+          != null) {
+        throw new IllegalAnnotationError(
+            "Option name " + option.name() + " is used more than once");
+      }
+    }
+
     @SuppressWarnings("rawtypes")
     private OptionHandler add(OptionHandler handler) {
       ensureOptionsInitialized();
@@ -489,12 +609,33 @@
 
     private void ensureOptionsInitialized() {
       if (optionsList == null) {
-        help = new HelpOption();
         optionsList = new ArrayList<>();
-        addOption(help, help);
+        addOption(newHelpSetter(), newHelpOption());
       }
     }
 
+    private Setter<?> newHelpSetter() {
+      try {
+        return Setters.create(getClass().getDeclaredField("help"), this);
+      } catch (NoSuchFieldException e) {
+        throw new IllegalStateException(e);
+      }
+    }
+
+    private Option newHelpOption() {
+      return OptionUtil.newOption(
+          "--help",
+          new String[] {"-h"},
+          "display this help text",
+          "",
+          false,
+          false,
+          false,
+          BooleanOptionHandler.class,
+          new String[0],
+          new String[0]);
+    }
+
     private boolean isHandlerSpecified(OptionDef option) {
       return option.handler() != OptionHandler.class;
     }
@@ -508,90 +649,6 @@
     }
   }
 
-  private static class HelpOption implements Option, Setter<Boolean> {
-    private boolean value;
-
-    @Override
-    public String name() {
-      return "--help";
-    }
-
-    @Override
-    public String[] aliases() {
-      return new String[] {"-h"};
-    }
-
-    @Override
-    public String[] depends() {
-      return new String[] {};
-    }
-
-    @Override
-    public boolean hidden() {
-      return false;
-    }
-
-    @Override
-    public String usage() {
-      return "display this help text";
-    }
-
-    @Override
-    public void addValue(Boolean val) {
-      value = val;
-    }
-
-    @Override
-    public Class<? extends OptionHandler<Boolean>> handler() {
-      return BooleanOptionHandler.class;
-    }
-
-    @Override
-    public String metaVar() {
-      return "";
-    }
-
-    @Override
-    public boolean required() {
-      return false;
-    }
-
-    @Override
-    public Class<? extends Annotation> annotationType() {
-      return Option.class;
-    }
-
-    @Override
-    public FieldSetter asFieldSetter() {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public AnnotatedElement asAnnotatedElement() {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public Class<Boolean> getType() {
-      return Boolean.class;
-    }
-
-    @Override
-    public boolean isMultiValued() {
-      return false;
-    }
-
-    @Override
-    public String[] forbids() {
-      return null;
-    }
-
-    @Override
-    public boolean help() {
-      return false;
-    }
-  }
-
   public CmdLineException reject(String message) {
     return new CmdLineException(parser, localizable(message));
   }
diff --git a/java/com/google/gerrit/util/cli/OptionHandlers.java b/java/com/google/gerrit/util/cli/OptionHandlers.java
index 84a0809..9547410 100644
--- a/java/com/google/gerrit/util/cli/OptionHandlers.java
+++ b/java/com/google/gerrit/util/cli/OptionHandlers.java
@@ -24,7 +24,7 @@
 import com.google.inject.Singleton;
 import com.google.inject.TypeLiteral;
 import java.lang.reflect.ParameterizedType;
-import java.util.Map.Entry;
+import java.util.Map;
 
 @Singleton
 public class OptionHandlers {
@@ -53,7 +53,7 @@
   private static ImmutableMap<Class<?>, Provider<OptionHandlerFactory<?>>> build(Injector i) {
     ImmutableMap.Builder<Class<?>, Provider<OptionHandlerFactory<?>>> map = ImmutableMap.builder();
     for (; i != null; i = i.getParent()) {
-      for (Entry<Key<?>, Binding<?>> e : i.getBindings().entrySet()) {
+      for (Map.Entry<Key<?>, Binding<?>> e : i.getBindings().entrySet()) {
         TypeLiteral<?> type = e.getKey().getTypeLiteral();
         if (type.getRawType() == OptionHandlerFactory.class
             && e.getKey().getAnnotation() == null
diff --git a/java/com/google/gerrit/util/cli/OptionUtil.java b/java/com/google/gerrit/util/cli/OptionUtil.java
new file mode 100644
index 0000000..1125a0d
--- /dev/null
+++ b/java/com/google/gerrit/util/cli/OptionUtil.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.util.cli;
+
+import com.google.auto.value.AutoAnnotation;
+import org.kohsuke.args4j.Option;
+import org.kohsuke.args4j.spi.OptionHandler;
+
+/** Utilities to support creating new {@link Option} instances. */
+public class OptionUtil {
+  @AutoAnnotation
+  @SuppressWarnings("rawtypes")
+  public static Option newOption(
+      String name,
+      String[] aliases,
+      String usage,
+      String metaVar,
+      boolean required,
+      boolean help,
+      boolean hidden,
+      Class<? extends OptionHandler> handler,
+      String[] depends,
+      String[] forbids) {
+    return new AutoAnnotation_OptionUtil_newOption(
+        name, aliases, usage, metaVar, required, help, hidden, handler, depends, forbids);
+  }
+}
diff --git a/java/com/google/gerrit/util/cli/RequiresOptions.java b/java/com/google/gerrit/util/cli/RequiresOptions.java
new file mode 100644
index 0000000..de6ba44
--- /dev/null
+++ b/java/com/google/gerrit/util/cli/RequiresOptions.java
@@ -0,0 +1,45 @@
+// 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.util.cli;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a field/setter annotated with {@literal @}Option as having a dependency on multiple other
+ * command line option.
+ *
+ * <p>If any of the required command line options are not present, the {@literal @}Option will be
+ * ignored.
+ *
+ * <p>For example:
+ *
+ * <pre>
+ *   {@literal @}RequiresOptions({"--help", "--usage"})
+ *   {@literal @}Option(name = "--help-as-json",
+ *           usage = "display help text in json format")
+ *   public boolean displayHelpAsJson;
+ * </pre>
+ */
+@Retention(RUNTIME)
+@Target({FIELD, METHOD, PARAMETER})
+public @interface RequiresOptions {
+  String[] value();
+}
diff --git a/java/com/google/gerrit/util/http/RequestUtil.java b/java/com/google/gerrit/util/http/RequestUtil.java
index 2a359ca..92d9967 100644
--- a/java/com/google/gerrit/util/http/RequestUtil.java
+++ b/java/com/google/gerrit/util/http/RequestUtil.java
@@ -56,5 +56,43 @@
     return pathInfo;
   }
 
+  /**
+   * Trims leading '/' and 'a/'. Removes the context path, but keeps the servlet path. Removes all
+   * IDs from the rest of the URI.
+   *
+   * <p>The returned string is a good fit for cases where one wants the full context of the request
+   * without any identifiable data. For example: Logging or quota checks.
+   *
+   * <p>Examples:
+   *
+   * <ul>
+   *   <li>/a/accounts/self/detail => /accounts/detail
+   *   <li>/changes/123/revisions/current/detail => /changes/revisions/detail
+   *   <li>/changes/ => /changes
+   * </ul>
+   */
+  public static String getRestPathWithoutIds(HttpServletRequest req) {
+    String encodedPathInfo = req.getRequestURI().substring(req.getContextPath().length());
+    if (encodedPathInfo.startsWith("/")) {
+      encodedPathInfo = encodedPathInfo.substring(1);
+    }
+    if (encodedPathInfo.startsWith("a/")) {
+      encodedPathInfo = encodedPathInfo.substring(2);
+    }
+
+    String[] parts = encodedPathInfo.split("/");
+    StringBuilder result = new StringBuilder(parts.length);
+    for (int i = 0; i < parts.length; i = i + 2) {
+      result.append("/");
+      result.append(parts[i]);
+    }
+    return result.toString();
+  }
+
+  public static boolean acceptsGzipEncoding(HttpServletRequest request) {
+    String accepts = request.getHeader("Accept-Encoding");
+    return accepts != null && accepts.indexOf("gzip") != -1;
+  }
+
   private RequestUtil() {}
 }
diff --git a/java/com/google/gwtexpui/clippy/BUILD b/java/com/google/gwtexpui/clippy/BUILD
deleted file mode 100644
index 80b6767..0000000
--- a/java/com/google/gwtexpui/clippy/BUILD
+++ /dev/null
@@ -1,23 +0,0 @@
-load("//tools/bzl:gwt.bzl", "gwt_module")
-
-gwt_module(
-    name = "clippy",
-    srcs = glob(["client/*.java"]),
-    data = [
-        "//lib:LICENSE-clippy",
-        "//lib:LICENSE-silk_icons",
-    ],
-    gwt_xml = "Clippy.gwt.xml",
-    resources = [
-        "client/CopyableLabelText.properties",
-        "client/clippy.css",
-        "client/clippy.swf",
-        "client/page_white_copy.png",
-    ],
-    visibility = ["//visibility:public"],
-    deps = [
-        "//java/com/google/gwtexpui/safehtml",
-        "//java/com/google/gwtexpui/user:agent",
-        "//lib/gwt:user-neverlink",
-    ],
-)
diff --git a/java/com/google/gwtexpui/clippy/Clippy.gwt.xml b/java/com/google/gwtexpui/clippy/Clippy.gwt.xml
deleted file mode 100644
index 0e9b072..0000000
--- a/java/com/google/gwtexpui/clippy/Clippy.gwt.xml
+++ /dev/null
@@ -1,20 +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.
--->
-<module>
-  <inherits name='com.google.gwt.resources.Resources'/>
-  <inherits name="com.google.gwtexpui.safehtml.SafeHtml"/>
-  <inherits name="com.google.gwtexpui.user.User"/>
-</module>
diff --git a/java/com/google/gwtexpui/clippy/client/ClippyCss.java b/java/com/google/gwtexpui/clippy/client/ClippyCss.java
deleted file mode 100644
index 0d340ff..0000000
--- a/java/com/google/gwtexpui/clippy/client/ClippyCss.java
+++ /dev/null
@@ -1,25 +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.gwtexpui.clippy.client;
-
-import com.google.gwt.resources.client.CssResource;
-
-public interface ClippyCss extends CssResource {
-  String label();
-
-  String copier();
-
-  String swf();
-}
diff --git a/java/com/google/gwtexpui/clippy/client/ClippyResources.java b/java/com/google/gwtexpui/clippy/client/ClippyResources.java
deleted file mode 100644
index a97b392..0000000
--- a/java/com/google/gwtexpui/clippy/client/ClippyResources.java
+++ /dev/null
@@ -1,35 +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.gwtexpui.clippy.client;
-
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.resources.client.ClientBundle;
-import com.google.gwt.resources.client.DataResource;
-import com.google.gwt.resources.client.DataResource.DoNotEmbed;
-import com.google.gwt.resources.client.ImageResource;
-
-public interface ClippyResources extends ClientBundle {
-  ClippyResources I = GWT.create(ClippyResources.class);
-
-  @Source("clippy.css")
-  ClippyCss css();
-
-  @Source("clippy.swf")
-  @DoNotEmbed
-  DataResource swf();
-
-  @Source("page_white_copy.png")
-  ImageResource clipboard();
-}
diff --git a/java/com/google/gwtexpui/clippy/client/CopyableLabel.java b/java/com/google/gwtexpui/clippy/client/CopyableLabel.java
deleted file mode 100644
index 7b70c6a..0000000
--- a/java/com/google/gwtexpui/clippy/client/CopyableLabel.java
+++ /dev/null
@@ -1,315 +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.gwtexpui.clippy.client;
-
-import com.google.gwt.core.client.Scheduler;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.Style.Display;
-import com.google.gwt.event.dom.client.BlurEvent;
-import com.google.gwt.event.dom.client.BlurHandler;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwt.event.dom.client.KeyUpEvent;
-import com.google.gwt.event.dom.client.KeyUpHandler;
-import com.google.gwt.event.dom.client.MouseOutEvent;
-import com.google.gwt.event.dom.client.MouseOutHandler;
-import com.google.gwt.http.client.URL;
-import com.google.gwt.user.client.Command;
-import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.HasText;
-import com.google.gwt.user.client.ui.InlineLabel;
-import com.google.gwt.user.client.ui.Label;
-import com.google.gwt.user.client.ui.TextBox;
-import com.google.gwtexpui.safehtml.client.SafeHtml;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-import com.google.gwtexpui.user.client.Tooltip;
-import com.google.gwtexpui.user.client.UserAgent;
-
-/**
- * Label which permits the user to easily copy the complete content.
- *
- * <p>If the Flash plugin is available a "movie" is embedded that provides one-click copying of the
- * content onto the system clipboard. The label (if visible) can also be clicked, switching from a
- * label to an input box, allowing the user to copy the text with a keyboard shortcut.
- */
-public class CopyableLabel extends Composite implements HasText {
-  private static final int SWF_WIDTH = 110;
-  private static final int SWF_HEIGHT = 14;
-  private static boolean flashEnabled = true;
-
-  static {
-    ClippyResources.I.css().ensureInjected();
-  }
-
-  public static boolean isFlashEnabled() {
-    return flashEnabled;
-  }
-
-  public static void setFlashEnabled(boolean on) {
-    flashEnabled = on;
-  }
-
-  private static String swfUrl() {
-    return ClippyResources.I.swf().getSafeUri().asString();
-  }
-
-  private final FlowPanel content;
-  private String text;
-  private int visibleLen;
-  private Label textLabel;
-  private TextBox textBox;
-  private Button copier;
-  private Element swf;
-
-  public CopyableLabel() {
-    this("");
-  }
-
-  /**
-   * Create a new label
-   *
-   * @param str initial content
-   */
-  public CopyableLabel(String str) {
-    this(str, true);
-  }
-
-  /**
-   * Create a new label
-   *
-   * @param str initial content
-   * @param showLabel if true, the content is shown, if false it is hidden from view and only the
-   *     copy icon is displayed.
-   */
-  public CopyableLabel(String str, boolean showLabel) {
-    content = new FlowPanel();
-    initWidget(content);
-
-    text = str;
-    visibleLen = text.length();
-
-    if (showLabel) {
-      textLabel = new InlineLabel(getText());
-      textLabel.setStyleName(ClippyResources.I.css().label());
-      textLabel.addClickHandler(
-          new ClickHandler() {
-            @Override
-            public void onClick(ClickEvent event) {
-              showTextBox();
-            }
-          });
-      content.add(textLabel);
-    }
-
-    if (UserAgent.hasJavaScriptClipboard()) {
-      copier =
-          new Button(
-              new SafeHtmlBuilder()
-                  .openElement("img")
-                  .setAttribute("src", ClippyResources.I.clipboard().getSafeUri().asString())
-                  .setWidth(14)
-                  .setHeight(14)
-                  .closeSelf());
-      copier.setStyleName(ClippyResources.I.css().copier());
-      Tooltip.addStyle(copier);
-      Tooltip.setLabel(copier, CopyableLabelText.I.tooltip());
-      copier.addClickHandler(
-          new ClickHandler() {
-            @Override
-            public void onClick(ClickEvent event) {
-              copy();
-            }
-          });
-      copier.addMouseOutHandler(
-          new MouseOutHandler() {
-            @Override
-            public void onMouseOut(MouseOutEvent event) {
-              Tooltip.setLabel(copier, CopyableLabelText.I.tooltip());
-            }
-          });
-
-      FlowPanel p = new FlowPanel();
-      p.getElement().getStyle().setDisplay(Display.INLINE_BLOCK);
-      p.add(copier);
-      content.add(p);
-    } else {
-      embedMovie();
-    }
-  }
-
-  /**
-   * Change the text which is displayed in the clickable label.
-   *
-   * @param text the new preview text, should be shorter than the original text which would be
-   *     copied to the clipboard.
-   */
-  public void setPreviewText(String text) {
-    if (textLabel != null) {
-      textLabel.setText(text);
-    }
-  }
-
-  private void embedMovie() {
-    if (copier == null && flashEnabled && !text.isEmpty() && UserAgent.Flash.isInstalled()) {
-      final String flashVars = "text=" + URL.encodeQueryString(getText());
-      final SafeHtmlBuilder h = new SafeHtmlBuilder();
-
-      h.openElement("div");
-      h.setStyleName(ClippyResources.I.css().swf());
-
-      h.openElement("object");
-      h.setWidth(SWF_WIDTH);
-      h.setHeight(SWF_HEIGHT);
-      h.setAttribute("classid", "clsid:d27cdb6e-ae6d-11cf-96b8-444553540000");
-      h.paramElement("movie", swfUrl());
-      h.paramElement("FlashVars", flashVars);
-
-      h.openElement("embed");
-      h.setWidth(SWF_WIDTH);
-      h.setHeight(SWF_HEIGHT);
-      h.setAttribute("wmode", "transparent");
-      h.setAttribute("type", "application/x-shockwave-flash");
-      h.setAttribute("src", swfUrl());
-      h.setAttribute("FlashVars", flashVars);
-      h.closeSelf();
-
-      h.closeElement("object");
-      h.closeElement("div");
-
-      if (swf != null) {
-        getElement().removeChild(swf);
-      }
-      DOM.appendChild(getElement(), swf = SafeHtml.parse(h));
-    }
-  }
-
-  @Override
-  public String getText() {
-    return text;
-  }
-
-  @Override
-  public void setText(String newText) {
-    text = newText;
-    visibleLen = newText.length();
-
-    if (textLabel != null) {
-      textLabel.setText(getText());
-    }
-    if (textBox != null) {
-      textBox.setText(getText());
-      textBox.selectAll();
-    }
-    embedMovie();
-  }
-
-  private void showTextBox() {
-    if (textBox == null) {
-      textBox = new TextBox();
-      textBox.setText(getText());
-      textBox.setVisibleLength(visibleLen);
-      textBox.setReadOnly(true);
-      textBox.addKeyPressHandler(
-          new KeyPressHandler() {
-            @Override
-            public void onKeyPress(KeyPressEvent event) {
-              if (event.isControlKeyDown() || event.isMetaKeyDown()) {
-                switch (event.getCharCode()) {
-                  case 'c':
-                  case 'x':
-                    textBox.addKeyUpHandler(
-                        new KeyUpHandler() {
-                          @Override
-                          public void onKeyUp(KeyUpEvent event) {
-                            Scheduler.get()
-                                .scheduleDeferred(
-                                    new Command() {
-                                      @Override
-                                      public void execute() {
-                                        hideTextBox();
-                                      }
-                                    });
-                          }
-                        });
-                    break;
-                }
-              }
-            }
-          });
-      textBox.addBlurHandler(
-          new BlurHandler() {
-            @Override
-            public void onBlur(BlurEvent event) {
-              hideTextBox();
-            }
-          });
-      content.insert(textBox, 1);
-    }
-
-    textLabel.setVisible(false);
-    textBox.setVisible(true);
-    Scheduler.get()
-        .scheduleDeferred(
-            new Command() {
-              @Override
-              public void execute() {
-                textBox.selectAll();
-                textBox.setFocus(true);
-              }
-            });
-  }
-
-  private void hideTextBox() {
-    if (textBox != null) {
-      textBox.removeFromParent();
-      textBox = null;
-    }
-    textLabel.setVisible(true);
-  }
-
-  private void copy() {
-    TextBox t = new TextBox();
-    try {
-      t.setText(getText());
-      content.add(t);
-      t.setFocus(true);
-      t.selectAll();
-
-      boolean ok = execCommand("copy");
-      Tooltip.setLabel(copier, ok ? CopyableLabelText.I.copied() : CopyableLabelText.I.failed());
-      if (!ok) {
-        // Disable JavaScript clipboard and try flash movie in another instance.
-        UserAgent.disableJavaScriptClipboard();
-      }
-    } finally {
-      t.removeFromParent();
-    }
-  }
-
-  private static boolean execCommand(String command) {
-    try {
-      return nativeExec(command);
-    } catch (Exception e) {
-      return false;
-    }
-  }
-
-  private static native boolean nativeExec(String c) /*-{ return !! $doc.execCommand(c) }-*/;
-}
diff --git a/java/com/google/gwtexpui/clippy/client/CopyableLabelText.java b/java/com/google/gwtexpui/clippy/client/CopyableLabelText.java
deleted file mode 100644
index ff36541..0000000
--- a/java/com/google/gwtexpui/clippy/client/CopyableLabelText.java
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gwtexpui.clippy.client;
-
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.i18n.client.Constants;
-
-interface CopyableLabelText extends Constants {
-  CopyableLabelText I = GWT.create(CopyableLabelText.class);
-
-  String tooltip();
-
-  String copied();
-
-  String failed();
-}
diff --git a/java/com/google/gwtexpui/clippy/client/CopyableLabelText.properties b/java/com/google/gwtexpui/clippy/client/CopyableLabelText.properties
deleted file mode 100644
index cf93bfa..0000000
--- a/java/com/google/gwtexpui/clippy/client/CopyableLabelText.properties
+++ /dev/null
@@ -1,3 +0,0 @@
-tooltip = Copy to clipboard
-copied = Copied
-failed = Failed !
diff --git a/java/com/google/gwtexpui/clippy/client/clippy.css b/java/com/google/gwtexpui/clippy/client/clippy.css
deleted file mode 100644
index b25e006..0000000
--- a/java/com/google/gwtexpui/clippy/client/clippy.css
+++ /dev/null
@@ -1,39 +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.
- */
-
-.label {
-  vertical-align: top;
-}
-.swf, .copier {
-  margin-left: 5px;
-  height: 14px;
-  width: 14px;
-}
-.swf {
-  display: inline-block !important;
-  overflow: hidden;
-}
-.copier {
-  display: inline-block;
-  font-size: 12px;
-  vertical-align: top;
-  padding: 0;
-  border: 0;
-  background-color: inherit;
-  cursor: pointer;
-}
-.copier:focus {
-  outline: none;
-}
diff --git a/java/com/google/gwtexpui/clippy/client/clippy.swf b/java/com/google/gwtexpui/clippy/client/clippy.swf
deleted file mode 100644
index e46886c..0000000
--- a/java/com/google/gwtexpui/clippy/client/clippy.swf
+++ /dev/null
Binary files differ
diff --git a/java/com/google/gwtexpui/clippy/client/page_white_copy.png b/java/com/google/gwtexpui/clippy/client/page_white_copy.png
deleted file mode 100644
index a9f31a2..0000000
--- a/java/com/google/gwtexpui/clippy/client/page_white_copy.png
+++ /dev/null
Binary files differ
diff --git a/java/com/google/gwtexpui/css/BUILD b/java/com/google/gwtexpui/css/BUILD
deleted file mode 100644
index 34029ad..0000000
--- a/java/com/google/gwtexpui/css/BUILD
+++ /dev/null
@@ -1,9 +0,0 @@
-load("@rules_java//java:defs.bzl", "java_library")
-
-java_library(
-    name = "css",
-    srcs = glob(["rebind/*.java"]),
-    resources = ["CSS.gwt.xml"],
-    visibility = ["//visibility:public"],
-    deps = ["//lib/gwt:dev"],
-)
diff --git a/java/com/google/gwtexpui/css/CSS.gwt.xml b/java/com/google/gwtexpui/css/CSS.gwt.xml
deleted file mode 100644
index b385987..0000000
--- a/java/com/google/gwtexpui/css/CSS.gwt.xml
+++ /dev/null
@@ -1,19 +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.
--->
-<module>
-  <define-linker name='cachecss' class='com.google.gwtexpui.css.rebind.CssLinker'/>
-  <add-linker name='cachecss'/>
-</module>
diff --git a/java/com/google/gwtexpui/css/rebind/CssLinker.java b/java/com/google/gwtexpui/css/rebind/CssLinker.java
deleted file mode 100644
index 6ef5d7b..0000000
--- a/java/com/google/gwtexpui/css/rebind/CssLinker.java
+++ /dev/null
@@ -1,121 +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.gwtexpui.css.rebind;
-
-import com.google.gwt.core.ext.LinkerContext;
-import com.google.gwt.core.ext.TreeLogger;
-import com.google.gwt.core.ext.UnableToCompleteException;
-import com.google.gwt.core.ext.linker.AbstractLinker;
-import com.google.gwt.core.ext.linker.Artifact;
-import com.google.gwt.core.ext.linker.ArtifactSet;
-import com.google.gwt.core.ext.linker.LinkerOrder;
-import com.google.gwt.core.ext.linker.PublicResource;
-import com.google.gwt.core.ext.linker.impl.StandardLinkerContext;
-import com.google.gwt.core.ext.linker.impl.StandardStylesheetReference;
-import com.google.gwt.dev.util.Util;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.HashMap;
-
-@LinkerOrder(LinkerOrder.Order.PRE)
-public class CssLinker extends AbstractLinker {
-  @Override
-  public String getDescription() {
-    return "CssLinker";
-  }
-
-  @Override
-  public ArtifactSet link(final TreeLogger logger, LinkerContext context, ArtifactSet artifacts)
-      throws UnableToCompleteException {
-    final ArtifactSet returnTo = new ArtifactSet();
-    int index = 0;
-
-    final HashMap<String, PublicResource> css = new HashMap<>();
-
-    for (StandardStylesheetReference ssr :
-        artifacts.<StandardStylesheetReference>find(StandardStylesheetReference.class)) {
-      css.put(ssr.getSrc(), null);
-    }
-    for (PublicResource pr : artifacts.<PublicResource>find(PublicResource.class)) {
-      if (css.containsKey(pr.getPartialPath())) {
-        css.put(pr.getPartialPath(), new CssPubRsrc(name(logger, pr), pr));
-      }
-    }
-
-    for (Artifact<?> a : artifacts) {
-      if (a instanceof PublicResource) {
-        final PublicResource r = (PublicResource) a;
-        if (css.containsKey(r.getPartialPath())) {
-          a = css.get(r.getPartialPath());
-        }
-      } else if (a instanceof StandardStylesheetReference) {
-        final StandardStylesheetReference r = (StandardStylesheetReference) a;
-        final PublicResource p = css.get(r.getSrc());
-        a = new StandardStylesheetReference(p.getPartialPath(), index);
-      }
-
-      returnTo.add(a);
-      index++;
-    }
-    return returnTo;
-  }
-
-  private String name(TreeLogger logger, PublicResource r) throws UnableToCompleteException {
-    byte[] out;
-    try (ByteArrayOutputStream tmp = new ByteArrayOutputStream();
-        InputStream in = r.getContents(logger)) {
-      final byte[] buf = new byte[2048];
-      int n;
-      while ((n = in.read(buf)) >= 0) {
-        tmp.write(buf, 0, n);
-      }
-      out = tmp.toByteArray();
-    } catch (IOException e) {
-      final UnableToCompleteException ute = new UnableToCompleteException();
-      ute.initCause(e);
-      throw ute;
-    }
-
-    String base = r.getPartialPath();
-    final int s = base.lastIndexOf('/');
-    if (0 < s) {
-      base = base.substring(0, s + 1);
-    } else {
-      base = "";
-    }
-    return base + Util.computeStrongName(out) + ".cache.css";
-  }
-
-  private static class CssPubRsrc extends PublicResource {
-    private static final long serialVersionUID = 1L;
-    private final PublicResource src;
-
-    CssPubRsrc(String partialPath, PublicResource r) {
-      super(StandardLinkerContext.class, partialPath);
-      src = r;
-    }
-
-    @Override
-    public InputStream getContents(TreeLogger logger) throws UnableToCompleteException {
-      return src.getContents(logger);
-    }
-
-    @Override
-    public long getLastModified() {
-      return src.getLastModified();
-    }
-  }
-}
diff --git a/java/com/google/gwtexpui/globalkey/BUILD b/java/com/google/gwtexpui/globalkey/BUILD
deleted file mode 100644
index c637194..0000000
--- a/java/com/google/gwtexpui/globalkey/BUILD
+++ /dev/null
@@ -1,17 +0,0 @@
-load("//tools/bzl:gwt.bzl", "gwt_module")
-
-gwt_module(
-    name = "globalkey",
-    srcs = glob(["client/*.java"]),
-    gwt_xml = "GlobalKey.gwt.xml",
-    resources = [
-        "client/KeyConstants.properties",
-        "client/key.css",
-    ],
-    visibility = ["//visibility:public"],
-    deps = [
-        "//java/com/google/gwtexpui/safehtml",
-        "//java/com/google/gwtexpui/user:agent",
-        "//lib/gwt:user",
-    ],
-)
diff --git a/java/com/google/gwtexpui/globalkey/GlobalKey.gwt.xml b/java/com/google/gwtexpui/globalkey/GlobalKey.gwt.xml
deleted file mode 100644
index 771050f..0000000
--- a/java/com/google/gwtexpui/globalkey/GlobalKey.gwt.xml
+++ /dev/null
@@ -1,20 +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.
--->
-<module>
-  <inherits name='com.google.gwt.resources.Resources'/>
-  <inherits name='com.google.gwtexpui.user.User'/>
-  <inherits name='com.google.gwtexpui.safehtml.SafeHtml'/>
-</module>
diff --git a/java/com/google/gwtexpui/globalkey/client/CompoundKeyCommand.java b/java/com/google/gwtexpui/globalkey/client/CompoundKeyCommand.java
deleted file mode 100644
index 5a4f6aa..0000000
--- a/java/com/google/gwtexpui/globalkey/client/CompoundKeyCommand.java
+++ /dev/null
@@ -1,40 +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.gwtexpui.globalkey.client;
-
-import com.google.gwt.event.dom.client.KeyPressEvent;
-
-public final class CompoundKeyCommand extends KeyCommand {
-  final KeyCommandSet set;
-
-  public CompoundKeyCommand(int mask, char key, String help, KeyCommandSet s) {
-    super(mask, key, help);
-    set = s;
-  }
-
-  public CompoundKeyCommand(int mask, int key, String help, KeyCommandSet s) {
-    super(mask, key, help);
-    set = s;
-  }
-
-  public KeyCommandSet getSet() {
-    return set;
-  }
-
-  @Override
-  public void onKeyPress(KeyPressEvent event) {
-    GlobalKey.temporaryWithTimeout(set);
-  }
-}
diff --git a/java/com/google/gwtexpui/globalkey/client/DocWidget.java b/java/com/google/gwtexpui/globalkey/client/DocWidget.java
deleted file mode 100644
index 320010e..0000000
--- a/java/com/google/gwtexpui/globalkey/client/DocWidget.java
+++ /dev/null
@@ -1,59 +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.gwtexpui.globalkey.client;
-
-import com.google.gwt.dom.client.Document;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.Node;
-import com.google.gwt.event.dom.client.HasKeyPressHandlers;
-import com.google.gwt.event.dom.client.HasMouseMoveHandlers;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwt.event.dom.client.MouseMoveEvent;
-import com.google.gwt.event.dom.client.MouseMoveHandler;
-import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.user.client.ui.RootPanel;
-import com.google.gwt.user.client.ui.Widget;
-
-public class DocWidget extends Widget implements HasKeyPressHandlers, HasMouseMoveHandlers {
-  private static DocWidget me;
-
-  public static DocWidget get() {
-    if (me == null) {
-      me = new DocWidget();
-    }
-    return me;
-  }
-
-  private DocWidget() {
-    setElement((Element) docnode());
-    onAttach();
-    RootPanel.detachOnWindowClose(this);
-  }
-
-  @Override
-  public HandlerRegistration addKeyPressHandler(KeyPressHandler handler) {
-    return addDomHandler(handler, KeyPressEvent.getType());
-  }
-
-  @Override
-  public HandlerRegistration addMouseMoveHandler(MouseMoveHandler handler) {
-    return addDomHandler(handler, MouseMoveEvent.getType());
-  }
-
-  private static Node docnode() {
-    return Document.get();
-  }
-}
diff --git a/java/com/google/gwtexpui/globalkey/client/GlobalKey.java b/java/com/google/gwtexpui/globalkey/client/GlobalKey.java
deleted file mode 100644
index cbaca61..0000000
--- a/java/com/google/gwtexpui/globalkey/client/GlobalKey.java
+++ /dev/null
@@ -1,191 +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.gwtexpui.globalkey.client;
-
-import com.google.gwt.event.dom.client.DomEvent;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyDownEvent;
-import com.google.gwt.event.dom.client.KeyDownHandler;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwt.event.logical.shared.CloseEvent;
-import com.google.gwt.event.logical.shared.CloseHandler;
-import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.user.client.Timer;
-import com.google.gwt.user.client.ui.PopupPanel;
-import com.google.gwt.user.client.ui.Widget;
-
-public class GlobalKey {
-  public static final KeyPressHandler STOP_PROPAGATION = DomEvent::stopPropagation;
-
-  private static State global;
-  static State active;
-  private static CloseHandler<PopupPanel> restoreGlobal;
-  private static Timer restoreTimer;
-
-  static {
-    KeyResources.I.css().ensureInjected();
-  }
-
-  private static void initEvents() {
-    if (active == null) {
-      DocWidget.get()
-          .addKeyPressHandler(
-              new KeyPressHandler() {
-                @Override
-                public void onKeyPress(KeyPressEvent event) {
-                  final KeyCommandSet s = active.live;
-                  if (s != active.all) {
-                    active.live = active.all;
-                    restoreTimer.cancel();
-                  }
-                  s.onKeyPress(event);
-                }
-              });
-
-      restoreTimer =
-          new Timer() {
-            @Override
-            public void run() {
-              active.live = active.all;
-            }
-          };
-
-      global = new State(null);
-      active = global;
-    }
-  }
-
-  private static void initDialog() {
-    if (restoreGlobal == null) {
-      restoreGlobal =
-          new CloseHandler<PopupPanel>() {
-            @Override
-            public void onClose(CloseEvent<PopupPanel> event) {
-              active = global;
-            }
-          };
-    }
-  }
-
-  static void temporaryWithTimeout(KeyCommandSet s) {
-    active.live = s;
-    restoreTimer.schedule(250);
-  }
-
-  public static void dialog(PopupPanel panel) {
-    initEvents();
-    initDialog();
-    assert panel.isShowing();
-    assert active == global;
-    active = new State(panel);
-    active.add(new HidePopupPanelCommand(0, KeyCodes.KEY_ESCAPE, panel));
-    panel.addCloseHandler(restoreGlobal);
-    panel.addDomHandler(
-        new KeyDownHandler() {
-          @Override
-          public void onKeyDown(KeyDownEvent event) {
-            if (event.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) {
-              panel.hide();
-            }
-          }
-        },
-        KeyDownEvent.getType());
-  }
-
-  public static HandlerRegistration addApplication(Widget widget, KeyCommand appKey) {
-    initEvents();
-    final State state = stateFor(widget);
-    state.add(appKey);
-    return new HandlerRegistration() {
-      @Override
-      public void removeHandler() {
-        state.remove(appKey);
-      }
-    };
-  }
-
-  public static HandlerRegistration add(Widget widget, KeyCommandSet cmdSet) {
-    initEvents();
-    final State state = stateFor(widget);
-    state.add(cmdSet);
-    return new HandlerRegistration() {
-      @Override
-      public void removeHandler() {
-        state.remove(cmdSet);
-      }
-    };
-  }
-
-  private static State stateFor(Widget w) {
-    while (w != null) {
-      if (w == active.root) {
-        return active;
-      }
-      w = w.getParent();
-    }
-    return global;
-  }
-
-  public static void filter(KeyCommandFilter filter) {
-    active.filter(filter);
-    if (active != global) {
-      global.filter(filter);
-    }
-  }
-
-  private GlobalKey() {}
-
-  static class State {
-    final Widget root;
-    final KeyCommandSet app;
-    final KeyCommandSet all;
-    KeyCommandSet live;
-
-    State(Widget r) {
-      root = r;
-
-      app = new KeyCommandSet(KeyConstants.I.applicationSection());
-      app.add(ShowHelpCommand.INSTANCE);
-
-      all = new KeyCommandSet();
-      all.add(app);
-
-      live = all;
-    }
-
-    void add(KeyCommand k) {
-      app.add(k);
-      all.add(k);
-    }
-
-    void remove(KeyCommand k) {
-      app.remove(k);
-      all.remove(k);
-    }
-
-    void add(KeyCommandSet s) {
-      all.add(s);
-    }
-
-    void remove(KeyCommandSet s) {
-      all.remove(s);
-    }
-
-    void filter(KeyCommandFilter f) {
-      all.filter(f);
-    }
-  }
-}
diff --git a/java/com/google/gwtexpui/globalkey/client/HidePopupPanelCommand.java b/java/com/google/gwtexpui/globalkey/client/HidePopupPanelCommand.java
deleted file mode 100644
index 8222f8b..0000000
--- a/java/com/google/gwtexpui/globalkey/client/HidePopupPanelCommand.java
+++ /dev/null
@@ -1,33 +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.gwtexpui.globalkey.client;
-
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.user.client.ui.PopupPanel;
-
-/** Hides the given popup panel when invoked. */
-public class HidePopupPanelCommand extends KeyCommand {
-  private final PopupPanel panel;
-
-  public HidePopupPanelCommand(int mask, int key, PopupPanel panel) {
-    super(mask, key, KeyConstants.I.closeCurrentDialog());
-    this.panel = panel;
-  }
-
-  @Override
-  public void onKeyPress(KeyPressEvent event) {
-    panel.hide();
-  }
-}
diff --git a/java/com/google/gwtexpui/globalkey/client/KeyCommand.java b/java/com/google/gwtexpui/globalkey/client/KeyCommand.java
deleted file mode 100644
index f1c92e0..0000000
--- a/java/com/google/gwtexpui/globalkey/client/KeyCommand.java
+++ /dev/null
@@ -1,104 +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.gwtexpui.globalkey.client;
-
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwtexpui.safehtml.client.SafeHtml;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-
-public abstract class KeyCommand implements KeyPressHandler {
-  public static final int M_CTRL = 1 << 16;
-  public static final int M_ALT = 2 << 16;
-  public static final int M_META = 4 << 16;
-  public static final int M_SHIFT = 8 << 16;
-
-  public static boolean same(KeyCommand a, KeyCommand b) {
-    return a.getClass() == b.getClass() && a.helpText.equals(b.helpText) && a.sibling == b.sibling;
-  }
-
-  final int keyMask;
-  private final String helpText;
-  KeyCommand sibling;
-
-  public KeyCommand(int mask, int key, String help) {
-    this(mask, (char) key, help);
-  }
-
-  public KeyCommand(int mask, char key, String help) {
-    assert help != null;
-    keyMask = mask | key;
-    helpText = help;
-  }
-
-  public String getHelpText() {
-    return helpText;
-  }
-
-  SafeHtml describeKeyStroke() {
-    final SafeHtmlBuilder b = new SafeHtmlBuilder();
-
-    if ((keyMask & M_CTRL) == M_CTRL) {
-      modifier(b, KeyConstants.I.keyCtrl());
-    }
-    if ((keyMask & M_ALT) == M_ALT) {
-      modifier(b, KeyConstants.I.keyAlt());
-    }
-    if ((keyMask & M_META) == M_META) {
-      modifier(b, KeyConstants.I.keyMeta());
-    }
-    if ((keyMask & M_SHIFT) == M_SHIFT) {
-      modifier(b, KeyConstants.I.keyShift());
-    }
-
-    final char c = (char) (keyMask & 0xffff);
-    switch (c) {
-      case KeyCodes.KEY_ENTER:
-        namedKey(b, KeyConstants.I.keyEnter());
-        break;
-      case KeyCodes.KEY_ESCAPE:
-        namedKey(b, KeyConstants.I.keyEsc());
-        break;
-      case KeyCodes.KEY_LEFT:
-        namedKey(b, KeyConstants.I.keyLeft());
-        break;
-      case KeyCodes.KEY_RIGHT:
-        namedKey(b, KeyConstants.I.keyRight());
-        break;
-      default:
-        b.openSpan();
-        b.setStyleName(KeyResources.I.css().helpKey());
-        b.append(String.valueOf(c));
-        b.closeSpan();
-        break;
-    }
-
-    return b;
-  }
-
-  private void modifier(SafeHtmlBuilder b, String name) {
-    namedKey(b, name);
-    b.append(" + ");
-  }
-
-  private void namedKey(SafeHtmlBuilder b, String name) {
-    b.append('<');
-    b.openSpan();
-    b.setStyleName(KeyResources.I.css().helpKey());
-    b.append(name);
-    b.closeSpan();
-    b.append(">");
-  }
-}
diff --git a/java/com/google/gwtexpui/globalkey/client/KeyCommandFilter.java b/java/com/google/gwtexpui/globalkey/client/KeyCommandFilter.java
deleted file mode 100644
index 4b67260..0000000
--- a/java/com/google/gwtexpui/globalkey/client/KeyCommandFilter.java
+++ /dev/null
@@ -1,19 +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.gwtexpui.globalkey.client;
-
-public interface KeyCommandFilter {
-  boolean include(KeyCommand key);
-}
diff --git a/java/com/google/gwtexpui/globalkey/client/KeyCommandSet.java b/java/com/google/gwtexpui/globalkey/client/KeyCommandSet.java
deleted file mode 100644
index 90aa419..0000000
--- a/java/com/google/gwtexpui/globalkey/client/KeyCommandSet.java
+++ /dev/null
@@ -1,145 +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.gwtexpui.globalkey.client;
-
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-
-public class KeyCommandSet implements KeyPressHandler {
-  private final Map<Integer, KeyCommand> map;
-  private List<KeyCommandSet> sets;
-  private String name;
-
-  public KeyCommandSet() {
-    this("");
-  }
-
-  public KeyCommandSet(String setName) {
-    map = new HashMap<>();
-    name = setName;
-  }
-
-  public String getName() {
-    return name;
-  }
-
-  public void setName(String setName) {
-    assert setName != null;
-    name = setName;
-  }
-
-  public boolean isEmpty() {
-    return map.isEmpty();
-  }
-
-  public void add(KeyCommand a, KeyCommand b) {
-    add(a);
-    add(b);
-    pair(a, b);
-  }
-
-  public void pair(KeyCommand a, KeyCommand b) {
-    a.sibling = b;
-    b.sibling = a;
-  }
-
-  public void add(KeyCommand k) {
-    assert !map.containsKey(k.keyMask)
-        : "Key " + k.describeKeyStroke().asString() + " already registered";
-    if (!map.containsKey(k.keyMask)) {
-      map.put(k.keyMask, k);
-    }
-  }
-
-  public void remove(KeyCommand k) {
-    assert map.get(k.keyMask) == k;
-    map.remove(k.keyMask);
-  }
-
-  public void add(KeyCommandSet set) {
-    if (sets == null) {
-      sets = new ArrayList<>();
-    }
-    assert !sets.contains(set);
-    sets.add(set);
-    for (KeyCommand k : set.map.values()) {
-      add(k);
-    }
-  }
-
-  public void remove(KeyCommandSet set) {
-    assert sets != null;
-    assert sets.contains(set);
-    sets.remove(set);
-    for (KeyCommand k : set.map.values()) {
-      remove(k);
-    }
-  }
-
-  public void filter(KeyCommandFilter filter) {
-    if (sets != null) {
-      for (KeyCommandSet s : sets) {
-        s.filter(filter);
-      }
-    }
-    for (Iterator<KeyCommand> i = map.values().iterator(); i.hasNext(); ) {
-      final KeyCommand kc = i.next();
-      if (!filter.include(kc)) {
-        i.remove();
-      } else if (kc instanceof CompoundKeyCommand) {
-        ((CompoundKeyCommand) kc).set.filter(filter);
-      }
-    }
-  }
-
-  public Collection<KeyCommand> getKeys() {
-    return map.values();
-  }
-
-  public Collection<KeyCommandSet> getSets() {
-    return sets != null ? sets : Collections.<KeyCommandSet>emptyList();
-  }
-
-  @Override
-  public void onKeyPress(KeyPressEvent event) {
-    final KeyCommand k = map.get(toMask(event));
-    if (k != null) {
-      event.preventDefault();
-      event.stopPropagation();
-      k.onKeyPress(event);
-    }
-  }
-
-  static int toMask(KeyPressEvent event) {
-    int mask = event.getUnicodeCharCode();
-    if (mask == 0) {
-      mask = event.getNativeEvent().getKeyCode();
-    }
-    if (event.isControlKeyDown()) {
-      mask |= KeyCommand.M_CTRL;
-    }
-    if (event.isMetaKeyDown()) {
-      mask |= KeyCommand.M_META;
-    }
-    return mask;
-  }
-}
diff --git a/java/com/google/gwtexpui/globalkey/client/KeyConstants.java b/java/com/google/gwtexpui/globalkey/client/KeyConstants.java
deleted file mode 100644
index 209b170..0000000
--- a/java/com/google/gwtexpui/globalkey/client/KeyConstants.java
+++ /dev/null
@@ -1,52 +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.gwtexpui.globalkey.client;
-
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.i18n.client.Constants;
-
-public interface KeyConstants extends Constants {
-  KeyConstants I = GWT.create(KeyConstants.class);
-
-  String applicationSection();
-
-  String showHelp();
-
-  String closeCurrentDialog();
-
-  String keyboardShortcuts();
-
-  String closeButton();
-
-  String orOtherKey();
-
-  String thenOtherKey();
-
-  String keyCtrl();
-
-  String keyAlt();
-
-  String keyMeta();
-
-  String keyShift();
-
-  String keyEnter();
-
-  String keyEsc();
-
-  String keyLeft();
-
-  String keyRight();
-}
diff --git a/java/com/google/gwtexpui/globalkey/client/KeyConstants.properties b/java/com/google/gwtexpui/globalkey/client/KeyConstants.properties
deleted file mode 100644
index 76a0318..0000000
--- a/java/com/google/gwtexpui/globalkey/client/KeyConstants.properties
+++ /dev/null
@@ -1,17 +0,0 @@
-applicationSection = Application
-showHelp = Open shortcut help
-closeCurrentDialog = Close current dialog
-
-keyboardShortcuts = Keyboard Shortcuts
-closeButton = Close
-orOtherKey = or
-thenOtherKey = then
-
-keyCtrl = Ctrl
-keyAlt = Alt
-keyMeta = Meta
-keyShift = Shift
-keyEnter = Enter
-keyEsc = Esc
-keyLeft = Left
-keyRight = Right
diff --git a/java/com/google/gwtexpui/globalkey/client/KeyCss.java b/java/com/google/gwtexpui/globalkey/client/KeyCss.java
deleted file mode 100644
index 658af57..0000000
--- a/java/com/google/gwtexpui/globalkey/client/KeyCss.java
+++ /dev/null
@@ -1,37 +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.gwtexpui.globalkey.client;
-
-import com.google.gwt.resources.client.CssResource;
-
-public interface KeyCss extends CssResource {
-  String helpPopup();
-
-  String helpHeader();
-
-  String helpHeaderGlue();
-
-  String helpTable();
-
-  String helpTableGlue();
-
-  String helpGroup();
-
-  String helpKeyStroke();
-
-  String helpSeparator();
-
-  String helpKey();
-}
diff --git a/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java b/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java
deleted file mode 100644
index 20d093e..0000000
--- a/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java
+++ /dev/null
@@ -1,233 +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.gwtexpui.globalkey.client;
-
-import static java.util.Comparator.comparing;
-import static java.util.stream.Collectors.toList;
-
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyDownEvent;
-import com.google.gwt.event.dom.client.KeyDownHandler;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.FocusPanel;
-import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
-import com.google.gwt.user.client.ui.HasHorizontalAlignment;
-import com.google.gwt.user.client.ui.PopupPanel;
-import com.google.gwtexpui.safehtml.client.SafeHtml;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
-public class KeyHelpPopup extends PopupPanel implements KeyPressHandler, KeyDownHandler {
-  private final FocusPanel focus;
-
-  public KeyHelpPopup() {
-    super(true /* autohide */, true /* modal */);
-    setStyleName(KeyResources.I.css().helpPopup());
-
-    final Anchor closer = new Anchor(KeyConstants.I.closeButton());
-    closer.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            hide();
-          }
-        });
-
-    final Grid header = new Grid(1, 3);
-    header.setStyleName(KeyResources.I.css().helpHeader());
-    header.setText(0, 0, KeyConstants.I.keyboardShortcuts());
-    header.setWidget(0, 2, closer);
-
-    final CellFormatter fmt = header.getCellFormatter();
-    fmt.addStyleName(0, 1, KeyResources.I.css().helpHeaderGlue());
-    fmt.setHorizontalAlignment(0, 2, HasHorizontalAlignment.ALIGN_RIGHT);
-
-    final Grid lists = new Grid(0, 7);
-    lists.setStyleName(KeyResources.I.css().helpTable());
-    populate(lists);
-    lists.getCellFormatter().addStyleName(0, 3, KeyResources.I.css().helpTableGlue());
-
-    final FlowPanel body = new FlowPanel();
-    body.add(header);
-    body.getElement().appendChild(DOM.createElement("hr"));
-    body.add(lists);
-
-    focus = new FocusPanel(body);
-    focus.getElement().getStyle().setProperty("outline", "0px");
-    focus.getElement().setAttribute("hideFocus", "true");
-    focus.addKeyPressHandler(this);
-    focus.addKeyDownHandler(this);
-    add(focus);
-  }
-
-  @Override
-  public void setVisible(boolean show) {
-    super.setVisible(show);
-    if (show) {
-      focus.setFocus(true);
-    }
-  }
-
-  @Override
-  public void onKeyPress(KeyPressEvent event) {
-    if (KeyCommandSet.toMask(event) == ShowHelpCommand.INSTANCE.keyMask) {
-      // Block the '?' key from triggering us to show right after
-      // we just hide ourselves.
-      //
-      event.stopPropagation();
-      event.preventDefault();
-    }
-    hide();
-  }
-
-  @Override
-  public void onKeyDown(KeyDownEvent event) {
-    if (event.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) {
-      hide();
-    }
-  }
-
-  private void populate(Grid lists) {
-    int[] end = new int[5];
-    int column = 0;
-    for (KeyCommandSet set : combinedSetsByName()) {
-      int row = end[column];
-      row = formatGroup(lists, row, column, set);
-      end[column] = row;
-      if (column == 0) {
-        column = 4;
-      } else {
-        column = 0;
-      }
-    }
-  }
-
-  /**
-   * @return an ordered collection of KeyCommandSet, combining sets which share the same name, so
-   *     that each set name appears at most once.
-   */
-  private static Collection<KeyCommandSet> combinedSetsByName() {
-    LinkedHashMap<String, KeyCommandSet> byName = new LinkedHashMap<>();
-    for (KeyCommandSet set : GlobalKey.active.all.getSets()) {
-      KeyCommandSet v = byName.get(set.getName());
-      if (v == null) {
-        v = new KeyCommandSet(set.getName());
-        byName.put(v.getName(), v);
-      }
-      v.add(set);
-    }
-    return byName.values();
-  }
-
-  private int formatGroup(Grid lists, int row, int col, KeyCommandSet set) {
-    if (set.isEmpty()) {
-      return row;
-    }
-
-    if (lists.getRowCount() < row + 1) {
-      lists.resizeRows(row + 1);
-    }
-    lists.setText(row, col + 2, set.getName());
-    lists.getCellFormatter().addStyleName(row, col + 2, KeyResources.I.css().helpGroup());
-    row++;
-
-    return formatKeys(lists, row, col, set, null);
-  }
-
-  private int formatKeys(final Grid lists, int row, int col, KeyCommandSet set, SafeHtml prefix) {
-    final CellFormatter fmt = lists.getCellFormatter();
-    final List<KeyCommand> keys = sort(set);
-    if (lists.getRowCount() < row + keys.size()) {
-      lists.resizeRows(row + keys.size());
-    }
-
-    Map<KeyCommand, Integer> rows = new HashMap<>();
-    FORMAT_KEYS:
-    for (int i = 0; i < keys.size(); i++) {
-      final KeyCommand k = keys.get(i);
-      if (rows.containsKey(k)) {
-        continue;
-      }
-
-      if (k instanceof CompoundKeyCommand) {
-        final SafeHtmlBuilder b = new SafeHtmlBuilder();
-        b.append(k.describeKeyStroke());
-        row = formatKeys(lists, row, col, ((CompoundKeyCommand) k).getSet(), b);
-        continue;
-      }
-
-      for (int prior = 0; prior < i; prior++) {
-        if (KeyCommand.same(keys.get(prior), k)) {
-          final int r = rows.get(keys.get(prior));
-          final SafeHtmlBuilder b = new SafeHtmlBuilder();
-          b.append(SafeHtml.get(lists, r, col + 0));
-          b.append(" ");
-          b.append(KeyConstants.I.orOtherKey());
-          b.append(" ");
-          if (prefix != null) {
-            b.append(prefix);
-            b.append(" ");
-            b.append(KeyConstants.I.thenOtherKey());
-            b.append(" ");
-          }
-          b.append(k.describeKeyStroke());
-          SafeHtml.set(lists, r, col + 0, b);
-          rows.put(k, r);
-          continue FORMAT_KEYS;
-        }
-      }
-
-      SafeHtmlBuilder b = new SafeHtmlBuilder();
-      String t = k.getHelpText();
-      if (prefix != null) {
-        b.append(prefix);
-        b.append(" ");
-        b.append(KeyConstants.I.thenOtherKey());
-        b.append(" ");
-      }
-      b.append(k.describeKeyStroke());
-      if (k.sibling != null) {
-        b.append(" / ").append(k.sibling.describeKeyStroke());
-        t += " / " + k.sibling.getHelpText();
-        rows.put(k.sibling, row);
-      }
-      SafeHtml.set(lists, row, col + 0, b);
-      lists.setText(row, col + 1, ":");
-      lists.setText(row, col + 2, t);
-      rows.put(k, row);
-
-      fmt.addStyleName(row, col + 0, KeyResources.I.css().helpKeyStroke());
-      fmt.addStyleName(row, col + 1, KeyResources.I.css().helpSeparator());
-      row++;
-    }
-
-    return row;
-  }
-
-  private List<KeyCommand> sort(KeyCommandSet set) {
-    return set.getKeys().stream().sorted(comparing(KeyCommand::getHelpText)).collect(toList());
-  }
-}
diff --git a/java/com/google/gwtexpui/globalkey/client/KeyResources.java b/java/com/google/gwtexpui/globalkey/client/KeyResources.java
deleted file mode 100644
index 562e12d..0000000
--- a/java/com/google/gwtexpui/globalkey/client/KeyResources.java
+++ /dev/null
@@ -1,25 +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.gwtexpui.globalkey.client;
-
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.resources.client.ClientBundle;
-
-public interface KeyResources extends ClientBundle {
-  KeyResources I = GWT.create(KeyResources.class);
-
-  @Source("key.css")
-  KeyCss css();
-}
diff --git a/java/com/google/gwtexpui/globalkey/client/NpTextArea.java b/java/com/google/gwtexpui/globalkey/client/NpTextArea.java
deleted file mode 100644
index fd0da74..0000000
--- a/java/com/google/gwtexpui/globalkey/client/NpTextArea.java
+++ /dev/null
@@ -1,27 +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.gwtexpui.globalkey.client;
-
-import com.google.gwt.user.client.ui.TextArea;
-
-public class NpTextArea extends TextArea {
-  public NpTextArea() {
-    addKeyPressHandler(GlobalKey.STOP_PROPAGATION);
-  }
-
-  public void setSpellCheck(boolean spell) {
-    getElement().setPropertyBoolean("spellcheck", spell);
-  }
-}
diff --git a/java/com/google/gwtexpui/globalkey/client/NpTextBox.java b/java/com/google/gwtexpui/globalkey/client/NpTextBox.java
deleted file mode 100644
index 1392675..0000000
--- a/java/com/google/gwtexpui/globalkey/client/NpTextBox.java
+++ /dev/null
@@ -1,29 +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.gwtexpui.globalkey.client;
-
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.user.client.ui.TextBox;
-
-public class NpTextBox extends TextBox {
-  public NpTextBox() {
-    addKeyPressHandler(GlobalKey.STOP_PROPAGATION);
-  }
-
-  public NpTextBox(Element element) {
-    super(element);
-    addKeyPressHandler(GlobalKey.STOP_PROPAGATION);
-  }
-}
diff --git a/java/com/google/gwtexpui/globalkey/client/ShowHelpCommand.java b/java/com/google/gwtexpui/globalkey/client/ShowHelpCommand.java
deleted file mode 100644
index 08217f4..0000000
--- a/java/com/google/gwtexpui/globalkey/client/ShowHelpCommand.java
+++ /dev/null
@@ -1,72 +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.gwtexpui.globalkey.client;
-
-import com.google.gwt.event.dom.client.FocusEvent;
-import com.google.gwt.event.dom.client.FocusHandler;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.logical.shared.CloseEvent;
-import com.google.gwt.event.logical.shared.CloseHandler;
-import com.google.gwt.event.shared.EventBus;
-import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.event.shared.SimpleEventBus;
-import com.google.gwt.user.client.Window;
-import com.google.gwt.user.client.ui.PopupPanel;
-import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;
-
-public class ShowHelpCommand extends KeyCommand {
-  public static final ShowHelpCommand INSTANCE = new ShowHelpCommand();
-  private static final EventBus BUS = new SimpleEventBus();
-  private static KeyHelpPopup current;
-
-  public static HandlerRegistration addFocusHandler(FocusHandler fh) {
-    return BUS.addHandler(FocusEvent.getType(), fh);
-  }
-
-  public ShowHelpCommand() {
-    super(0, '?', KeyConstants.I.showHelp());
-  }
-
-  @Override
-  public void onKeyPress(KeyPressEvent event) {
-    if (current != null) {
-      // Already open? Close the dialog.
-      //
-      current.hide();
-      return;
-    }
-
-    final KeyHelpPopup help = new KeyHelpPopup();
-    help.addCloseHandler(
-        new CloseHandler<PopupPanel>() {
-          @Override
-          public void onClose(CloseEvent<PopupPanel> event) {
-            current = null;
-            BUS.fireEvent(new FocusEvent() {});
-          }
-        });
-    current = help;
-    help.setPopupPositionAndShow(
-        new PositionCallback() {
-          @Override
-          public void setPosition(int pWidth, int pHeight) {
-            final int left = (Window.getClientWidth() - pWidth) >> 1;
-            final int wLeft = Window.getScrollLeft();
-            final int wTop = Window.getScrollTop();
-            help.setPopupPosition(wLeft + left, wTop + 50);
-          }
-        });
-  }
-}
diff --git a/java/com/google/gwtexpui/globalkey/client/key.css b/java/com/google/gwtexpui/globalkey/client/key.css
deleted file mode 100644
index 755e686..0000000
--- a/java/com/google/gwtexpui/globalkey/client/key.css
+++ /dev/null
@@ -1,99 +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.
- */
-
-@external .popupContent;
-
-.helpPopup {
-  background: #000000 none repeat scroll 0 50%;
-  color: #ffffff;
-  font-family: arial,sans-serif;
-  font-weight: bold;
-  overflow: hidden;
-  text-align: left;
-  text-shadow: 1px 1px 7px #000000;
-  width: 92%;
-  z-index: 1002;
-  opacity: 0.85;
-  border-radius: 10px;
- }
-
-@if user.agent safari {
-  .helpPopup {
-    \-webkit-border-radius: 10px;
-  }
-}
-@if user.agent gecko1_8 {
-  .helpPopup {
-    \-moz-border-radius: 10px;
-  }
-}
-
-.helpPopup .popupContent {
-  margin: 10px;
-}
-
-.helpPopup hr {
-  width: 100%;
-}
-
-.helpHeader {
-  width: 100%;
-}
-
-.helpHeader td {
-  white-space: nowrap;
-  color: #ffffff;
-}
-
-.helpHeader a,
-.helpHeader a:visited,
-.helpHeader a:hover {
-  color: #dddd00;
-}
-
-.helpHeaderGlue {
-  width: 100%;
-}
-
-.helpTable {
-  width: 90%;
-}
-.helpTable td {
-  vertical-align: top;
-  white-space: nowrap;
-}
-
-.helpTableGlue {
-  width: 25px;
-}
-
-.helpGroup {
-  color: #dddd00;
-  text-align: left;
-}
-
-.helpKeyStroke {
-  text-align: right;
-}
-
-.helpSeparator {
-  width: 0.5em;
-  text-align: center;
-  font-weight: bold;
-}
-
-.helpKey {
-  color: #dddd00;
-}
diff --git a/java/com/google/gwtexpui/progress/BUILD b/java/com/google/gwtexpui/progress/BUILD
deleted file mode 100644
index 74caa57..0000000
--- a/java/com/google/gwtexpui/progress/BUILD
+++ /dev/null
@@ -1,10 +0,0 @@
-load("//tools/bzl:gwt.bzl", "gwt_module")
-
-gwt_module(
-    name = "progress",
-    srcs = glob(["client/*.java"]),
-    gwt_xml = "Progress.gwt.xml",
-    resources = ["client/progress.css"],
-    visibility = ["//visibility:public"],
-    deps = ["//lib/gwt:user"],
-)
diff --git a/java/com/google/gwtexpui/progress/Progress.gwt.xml b/java/com/google/gwtexpui/progress/Progress.gwt.xml
deleted file mode 100644
index 0df8928..0000000
--- a/java/com/google/gwtexpui/progress/Progress.gwt.xml
+++ /dev/null
@@ -1,19 +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.
--->
-<module>
-  <inherits name='com.google.gwt.resources.Resources'/>
-  <inherits name="com.google.gwt.user.User"/>
-</module>
diff --git a/java/com/google/gwtexpui/progress/client/ProgressBar.java b/java/com/google/gwtexpui/progress/client/ProgressBar.java
deleted file mode 100644
index f133e4d..0000000
--- a/java/com/google/gwtexpui/progress/client/ProgressBar.java
+++ /dev/null
@@ -1,77 +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.gwtexpui.progress.client;
-
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.Label;
-
-/**
- * A simple progress bar with a text label.
- *
- * <p>The bar is 200 pixels wide and 20 pixels high. To keep the implementation simple and
- * lightweight this dimensions are fixed and shouldn't be modified by style overrides in client code
- * or CSS.
- */
-public class ProgressBar extends Composite {
-  static {
-    ProgressResources.I.css().ensureInjected();
-  }
-
-  private final String callerText;
-  private final Label bar;
-  private final Label msg;
-  private int value;
-
-  /** Create a bar with no message text. */
-  public ProgressBar() {
-    this("");
-  }
-
-  /** Create a bar displaying the specified message. */
-  public ProgressBar(String text) {
-    if (text == null || text.length() == 0) {
-      callerText = "";
-    } else {
-      callerText = text + " ";
-    }
-
-    final FlowPanel body = new FlowPanel();
-    body.setStyleName(ProgressResources.I.css().container());
-
-    msg = new Label(callerText);
-    msg.setStyleName(ProgressResources.I.css().text());
-    body.add(msg);
-
-    bar = new Label("");
-    bar.setStyleName(ProgressResources.I.css().bar());
-    body.add(bar);
-
-    initWidget(body);
-  }
-
-  /** @return the current value of the progress meter. */
-  public int getValue() {
-    return value;
-  }
-
-  /** Update the bar's percent completion. */
-  public void setValue(int pComplete) {
-    assert 0 <= pComplete && pComplete <= 100;
-    value = pComplete;
-    bar.setWidth(2 * pComplete + "px");
-    msg.setText(callerText + pComplete + "%");
-  }
-}
diff --git a/java/com/google/gwtexpui/progress/client/ProgressCss.java b/java/com/google/gwtexpui/progress/client/ProgressCss.java
deleted file mode 100644
index ec27490..0000000
--- a/java/com/google/gwtexpui/progress/client/ProgressCss.java
+++ /dev/null
@@ -1,25 +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.gwtexpui.progress.client;
-
-import com.google.gwt.resources.client.CssResource;
-
-public interface ProgressCss extends CssResource {
-  String container();
-
-  String text();
-
-  String bar();
-}
diff --git a/java/com/google/gwtexpui/progress/client/ProgressResources.java b/java/com/google/gwtexpui/progress/client/ProgressResources.java
deleted file mode 100644
index 6bcf2c4..0000000
--- a/java/com/google/gwtexpui/progress/client/ProgressResources.java
+++ /dev/null
@@ -1,25 +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.gwtexpui.progress.client;
-
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.resources.client.ClientBundle;
-
-public interface ProgressResources extends ClientBundle {
-  ProgressResources I = GWT.create(ProgressResources.class);
-
-  @Source("progress.css")
-  ProgressCss css();
-}
diff --git a/java/com/google/gwtexpui/progress/client/progress.css b/java/com/google/gwtexpui/progress/client/progress.css
deleted file mode 100644
index 683396e..0000000
--- a/java/com/google/gwtexpui/progress/client/progress.css
+++ /dev/null
@@ -1,43 +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.
- */
-
-.container {
-  position: relative;
-  border: 1px solid #6B90DA;
-  height: 20px;
-  width: 200px;
-}
-
-.text {
-  position: absolute;
-  bottom: 0;
-  left: 0;
-  z-index: 2;
-  width: 200px;
-  padding-bottom: 3px;
-  text-align: center;
-  font-weight: bold;
-  font-style: italic;
-  font-size: smaller;
-}
-
-.bar {
-  background: #F0F7F9;
-  border-right: 1px solid #D0D7D9;
-  position: absolute;
-  top: 0;
-  left: 0;
-  height: 20px;
-}
diff --git a/java/com/google/gwtexpui/safehtml/BUILD b/java/com/google/gwtexpui/safehtml/BUILD
deleted file mode 100644
index af85c33..0000000
--- a/java/com/google/gwtexpui/safehtml/BUILD
+++ /dev/null
@@ -1,10 +0,0 @@
-load("//tools/bzl:gwt.bzl", "gwt_module")
-
-gwt_module(
-    name = "safehtml",
-    srcs = glob(["client/*.java"]),
-    gwt_xml = "SafeHtml.gwt.xml",
-    resources = ["client/safehtml.css"],
-    visibility = ["//visibility:public"],
-    deps = ["//lib/gwt:user"],
-)
diff --git a/java/com/google/gwtexpui/safehtml/SafeHtml.gwt.xml b/java/com/google/gwtexpui/safehtml/SafeHtml.gwt.xml
deleted file mode 100644
index 0df8928..0000000
--- a/java/com/google/gwtexpui/safehtml/SafeHtml.gwt.xml
+++ /dev/null
@@ -1,19 +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.
--->
-<module>
-  <inherits name='com.google.gwt.resources.Resources'/>
-  <inherits name="com.google.gwt.user.User"/>
-</module>
diff --git a/java/com/google/gwtexpui/safehtml/client/AttMap.java b/java/com/google/gwtexpui/safehtml/client/AttMap.java
deleted file mode 100644
index c93a78b..0000000
--- a/java/com/google/gwtexpui/safehtml/client/AttMap.java
+++ /dev/null
@@ -1,141 +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.gwtexpui.safehtml.client;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-
-/** Lightweight map of names/values for element attribute construction. */
-class AttMap {
-  private static final Tag ANY = new AnyTag();
-  private static final HashMap<String, Tag> TAGS;
-
-  static {
-    final Tag src = new SrcTag();
-    TAGS = new HashMap<>();
-    TAGS.put("a", new AnchorTag());
-    TAGS.put("form", new FormTag());
-    TAGS.put("img", src);
-    TAGS.put("script", src);
-    TAGS.put("frame", src);
-  }
-
-  private final ArrayList<String> names = new ArrayList<>();
-  private final ArrayList<String> values = new ArrayList<>();
-
-  private Tag tag = ANY;
-  private int live;
-
-  void reset(String tagName) {
-    tag = TAGS.get(tagName.toLowerCase());
-    if (tag == null) {
-      tag = ANY;
-    }
-    live = 0;
-  }
-
-  void onto(Buffer raw, SafeHtmlBuilder esc) {
-    for (int i = 0; i < live; i++) {
-      final String v = values.get(i);
-      if (v.length() > 0) {
-        raw.append(" ");
-        raw.append(names.get(i));
-        raw.append("=\"");
-        esc.append(v);
-        raw.append("\"");
-      }
-    }
-  }
-
-  String get(String name) {
-    name = name.toLowerCase();
-
-    for (int i = 0; i < live; i++) {
-      if (name.equals(names.get(i))) {
-        return values.get(i);
-      }
-    }
-    return "";
-  }
-
-  void set(String name, String value) {
-    name = name.toLowerCase();
-    tag.assertSafe(name, value);
-
-    for (int i = 0; i < live; i++) {
-      if (name.equals(names.get(i))) {
-        values.set(i, value);
-        return;
-      }
-    }
-
-    final int i = live++;
-    if (names.size() < live) {
-      names.add(name);
-      values.add(value);
-    } else {
-      names.set(i, name);
-      values.set(i, value);
-    }
-  }
-
-  private static void assertNotJavascriptUrl(String value) {
-    if (value.startsWith("#")) {
-      // common in GWT, and safe, so bypass further checks
-
-    } else if (value.trim().toLowerCase().startsWith("javascript:")) {
-      // possibly unsafe, we could have random user code here
-      // we can't tell if its safe or not so we refuse to accept
-      //
-      throw new RuntimeException("javascript unsafe in href: " + value);
-    }
-  }
-
-  private interface Tag {
-    void assertSafe(String name, String value);
-  }
-
-  private static class AnyTag implements Tag {
-    @Override
-    public void assertSafe(String name, String value) {}
-  }
-
-  private static class AnchorTag implements Tag {
-    @Override
-    public void assertSafe(String name, String value) {
-      if ("href".equals(name)) {
-        assertNotJavascriptUrl(value);
-      }
-    }
-  }
-
-  private static class FormTag implements Tag {
-    @Override
-    public void assertSafe(String name, String value) {
-      if ("action".equals(name)) {
-        assertNotJavascriptUrl(value);
-      }
-    }
-  }
-
-  private static class SrcTag implements Tag {
-    @Override
-    public void assertSafe(String name, String value) {
-      if ("src".equals(name)) {
-        assertNotJavascriptUrl(value);
-      }
-    }
-  }
-}
diff --git a/java/com/google/gwtexpui/safehtml/client/Buffer.java b/java/com/google/gwtexpui/safehtml/client/Buffer.java
deleted file mode 100644
index 12389b4..0000000
--- a/java/com/google/gwtexpui/safehtml/client/Buffer.java
+++ /dev/null
@@ -1,34 +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.gwtexpui.safehtml.client;
-
-interface Buffer {
-  void append(boolean v);
-
-  void append(char v);
-
-  void append(int v);
-
-  void append(long v);
-
-  void append(float v);
-
-  void append(double v);
-
-  void append(String v);
-
-  @Override
-  String toString();
-}
diff --git a/java/com/google/gwtexpui/safehtml/client/BufferDirect.java b/java/com/google/gwtexpui/safehtml/client/BufferDirect.java
deleted file mode 100644
index c6e1d30..0000000
--- a/java/com/google/gwtexpui/safehtml/client/BufferDirect.java
+++ /dev/null
@@ -1,63 +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.gwtexpui.safehtml.client;
-
-final class BufferDirect implements Buffer {
-  private final StringBuilder strbuf = new StringBuilder();
-
-  boolean isEmpty() {
-    return strbuf.length() == 0;
-  }
-
-  @Override
-  public void append(boolean v) {
-    strbuf.append(v);
-  }
-
-  @Override
-  public void append(char v) {
-    strbuf.append(v);
-  }
-
-  @Override
-  public void append(int v) {
-    strbuf.append(v);
-  }
-
-  @Override
-  public void append(long v) {
-    strbuf.append(v);
-  }
-
-  @Override
-  public void append(float v) {
-    strbuf.append(v);
-  }
-
-  @Override
-  public void append(double v) {
-    strbuf.append(v);
-  }
-
-  @Override
-  public void append(String v) {
-    strbuf.append(v);
-  }
-
-  @Override
-  public String toString() {
-    return strbuf.toString();
-  }
-}
diff --git a/java/com/google/gwtexpui/safehtml/client/BufferSealElement.java b/java/com/google/gwtexpui/safehtml/client/BufferSealElement.java
deleted file mode 100644
index bdd9801..0000000
--- a/java/com/google/gwtexpui/safehtml/client/BufferSealElement.java
+++ /dev/null
@@ -1,63 +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.gwtexpui.safehtml.client;
-
-final class BufferSealElement implements Buffer {
-  private final SafeHtmlBuilder shb;
-
-  BufferSealElement(SafeHtmlBuilder safeHtmlBuilder) {
-    shb = safeHtmlBuilder;
-  }
-
-  @Override
-  public void append(boolean v) {
-    shb.sealElement().append(v);
-  }
-
-  @Override
-  public void append(char v) {
-    shb.sealElement().append(v);
-  }
-
-  @Override
-  public void append(double v) {
-    shb.sealElement().append(v);
-  }
-
-  @Override
-  public void append(float v) {
-    shb.sealElement().append(v);
-  }
-
-  @Override
-  public void append(int v) {
-    shb.sealElement().append(v);
-  }
-
-  @Override
-  public void append(long v) {
-    shb.sealElement().append(v);
-  }
-
-  @Override
-  public void append(String v) {
-    shb.sealElement().append(v);
-  }
-
-  @Override
-  public String toString() {
-    return shb.sealElement().toString();
-  }
-}
diff --git a/java/com/google/gwtexpui/safehtml/client/FindReplace.java b/java/com/google/gwtexpui/safehtml/client/FindReplace.java
deleted file mode 100644
index 4fb3246..0000000
--- a/java/com/google/gwtexpui/safehtml/client/FindReplace.java
+++ /dev/null
@@ -1,36 +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.gwtexpui.safehtml.client;
-
-import com.google.gwt.regexp.shared.RegExp;
-
-/** A Find/Replace pair used against the {@link SafeHtml} block of text. */
-public interface FindReplace {
-  /** @return regular expression to match substrings with; should be treated as immutable. */
-  RegExp pattern();
-
-  /**
-   * Find and replace a single instance of this pattern in an input.
-   *
-   * <p><b>WARNING:</b> No XSS sanitization is done on the return value of this method, e.g. this
-   * value may be passed directly to {@link SafeHtml#replaceAll(String, String)}. Implementations
-   * must sanitize output appropriately.
-   *
-   * @param input input string.
-   * @return result of regular expression replacement.
-   * @throws IllegalArgumentException if the input could not be safely sanitized.
-   */
-  String replace(String input);
-}
diff --git a/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java b/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java
deleted file mode 100644
index 758521f..0000000
--- a/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java
+++ /dev/null
@@ -1,151 +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.gwtexpui.safehtml.client;
-
-import static java.util.Comparator.comparing;
-import static java.util.stream.Collectors.toList;
-
-import com.google.gwt.user.client.ui.SuggestOracle;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-/**
- * A suggestion oracle that tries to highlight the matched text.
- *
- * <p>Suggestions supplied by the implementation of {@link #onRequestSuggestions(Request, Callback)}
- * are modified to wrap all occurrences of the {@link
- * com.google.gwt.user.client.ui.SuggestOracle.Request#getQuery()} substring in HTML {@code
- * &lt;strong&gt;} tags, so they can be emphasized to the user.
- */
-public abstract class HighlightSuggestOracle extends SuggestOracle {
-  private static String escape(String ds) {
-    return new SafeHtmlBuilder().append(ds).asString();
-  }
-
-  @Override
-  public final boolean isDisplayStringHTML() {
-    return true;
-  }
-
-  @Override
-  public final void requestSuggestions(Request request, Callback cb) {
-    onRequestSuggestions(
-        request,
-        new Callback() {
-          @Override
-          public void onSuggestionsReady(Request request, Response response) {
-            final String qpat = getQueryPattern(request.getQuery());
-            final boolean html = isHTML();
-            final ArrayList<Suggestion> r = new ArrayList<>();
-            for (Suggestion s : response.getSuggestions()) {
-              r.add(new BoldSuggestion(qpat, s, html));
-            }
-            cb.onSuggestionsReady(request, new Response(r));
-          }
-        });
-  }
-
-  protected String getQueryPattern(String query) {
-    return query;
-  }
-
-  /**
-   * @return true if {@link
-   *     com.google.gwt.user.client.ui.SuggestOracle.Suggestion#getDisplayString()} returns HTML;
-   *     false if the text must be escaped before evaluating in an HTML like context.
-   */
-  protected boolean isHTML() {
-    return false;
-  }
-
-  /** Compute the suggestions and return them for display. */
-  protected abstract void onRequestSuggestions(Request request, Callback done);
-
-  private static class BoldSuggestion implements Suggestion {
-    private final Suggestion suggestion;
-    private final String displayString;
-
-    BoldSuggestion(String qstr, Suggestion s, boolean html) {
-      suggestion = s;
-
-      String ds = s.getDisplayString();
-      if (!html) {
-        ds = escape(ds);
-      }
-
-      if (qstr != null && !qstr.isEmpty()) {
-        StringBuilder pattern = new StringBuilder();
-        for (String qterm : splitQuery(qstr)) {
-          qterm = escape(qterm);
-          // We now surround qstr by <strong>. But the chosen approach is not too
-          // smooth, if qstr is small (e.g.: "t") and this small qstr may occur in
-          // escapes (e.g.: "Tim &lt;email@example.org&gt;"). Those escapes will
-          // get <strong>-ed as well (e.g.: "&lt;" -> "&<strong>l</strong>t;"). But
-          // as repairing those mangled escapes is easier than not mangling them in
-          // the first place, we repair them afterwards.
-          if (pattern.length() > 0) {
-            pattern.append("|");
-          }
-          pattern.append(qterm);
-        }
-
-        ds = sgi(ds, "(" + pattern.toString() + ")", "<strong>$1</strong>");
-
-        // Repairing <strong>-ed escapes.
-        ds = sgi(ds, "(&[a-z]*)<strong>([a-z]*)</strong>([a-z]*;)", "$1$2$3");
-      }
-
-      displayString = ds;
-    }
-
-    /**
-     * Split the query by whitespace and filter out query terms which are substrings of other query
-     * terms.
-     */
-    private static List<String> splitQuery(String query) {
-      List<String> queryTerms =
-          Arrays.stream(query.split("\\s+")).sorted(comparing(String::length)).collect(toList());
-
-      List<String> result = new ArrayList<>();
-      for (String s : queryTerms) {
-        boolean add = true;
-        for (String queryTerm : result) {
-          if (queryTerm.toLowerCase().contains(s.toLowerCase())) {
-            add = false;
-            break;
-          }
-        }
-        if (add) {
-          result.add(s);
-        }
-      }
-      return result;
-    }
-
-    private static native String sgi(String inString, String pat, String newHtml)
-        /*-{ return inString.replace(RegExp(pat, 'gi'), newHtml); }-*/ ;
-
-    @Override
-    public String getDisplayString() {
-      return displayString;
-    }
-
-    @Override
-    public String getReplacementString() {
-      return suggestion.getReplacementString();
-    }
-  }
-}
diff --git a/java/com/google/gwtexpui/safehtml/client/LinkFindReplace.java b/java/com/google/gwtexpui/safehtml/client/LinkFindReplace.java
deleted file mode 100644
index cf0e51d..0000000
--- a/java/com/google/gwtexpui/safehtml/client/LinkFindReplace.java
+++ /dev/null
@@ -1,82 +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.gwtexpui.safehtml.client;
-
-import com.google.gwt.regexp.shared.RegExp;
-
-/**
- * A Find/Replace pair whose replacement string is a link.
- *
- * <p>It is safe to pass arbitrary user-provided links to this class. Links are sanitized as
- * follows:
- *
- * <ul>
- *   <li>Only http(s) and mailto links are supported; any other scheme results in an {@link
- *       IllegalArgumentException} from {@link #replace(String)}.
- *   <li>Special characters in the link after regex replacement are escaped with {@link
- *       SafeHtmlBuilder}.
- * </ul>
- */
-public class LinkFindReplace implements FindReplace {
-  public static boolean hasValidScheme(String link) {
-    int colon = link.indexOf(':');
-    if (colon < 0) {
-      return true;
-    }
-    String scheme = link.substring(0, colon);
-    return "http".equalsIgnoreCase(scheme)
-        || "https".equalsIgnoreCase(scheme)
-        || "mailto".equalsIgnoreCase(scheme);
-  }
-
-  private RegExp pat;
-  private String link;
-
-  protected LinkFindReplace() {}
-
-  /**
-   * @param find regular expression pattern to match substrings with.
-   * @param link replacement link href. Capture groups within {@code find} can be referenced with
-   *     {@code $<i>n</i>}.
-   */
-  public LinkFindReplace(String find, String link) {
-    this.pat = RegExp.compile(find);
-    this.link = link;
-  }
-
-  @Override
-  public RegExp pattern() {
-    return pat;
-  }
-
-  @Override
-  public String replace(String input) {
-    String href = pat.replace(input, link);
-    if (!hasValidScheme(href)) {
-      throw new IllegalArgumentException("Invalid scheme (" + toString() + "): " + href);
-    }
-    return new SafeHtmlBuilder()
-        .openAnchor()
-        .setAttribute("href", href)
-        .append(SafeHtml.asis(input))
-        .closeAnchor()
-        .asString();
-  }
-
-  @Override
-  public String toString() {
-    return "find = " + pat.getSource() + ", link = " + link;
-  }
-}
diff --git a/java/com/google/gwtexpui/safehtml/client/RawFindReplace.java b/java/com/google/gwtexpui/safehtml/client/RawFindReplace.java
deleted file mode 100644
index dc39af6..0000000
--- a/java/com/google/gwtexpui/safehtml/client/RawFindReplace.java
+++ /dev/null
@@ -1,54 +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.gwtexpui.safehtml.client;
-
-import com.google.gwt.regexp.shared.RegExp;
-
-/**
- * A Find/Replace pair whose replacement string is arbitrary HTML.
- *
- * <p><b>WARNING:</b> This class is not safe used with user-provided patterns.
- */
-public class RawFindReplace implements FindReplace {
-  private RegExp pat;
-  private String replace;
-
-  protected RawFindReplace() {}
-
-  /**
-   * @param find regular expression pattern to match substrings with.
-   * @param replace replacement expression. Capture groups within {@code find} can be referenced
-   *     with {@code $<i>n</i>}.
-   */
-  public RawFindReplace(String find, String replace) {
-    this.pat = RegExp.compile(find);
-    this.replace = replace;
-  }
-
-  @Override
-  public RegExp pattern() {
-    return pat;
-  }
-
-  @Override
-  public String replace(String input) {
-    return pat.replace(input, replace);
-  }
-
-  @Override
-  public String toString() {
-    return "find = " + pat.getSource() + ", replace = " + replace;
-  }
-}
diff --git a/java/com/google/gwtexpui/safehtml/client/SafeHtml.java b/java/com/google/gwtexpui/safehtml/client/SafeHtml.java
deleted file mode 100644
index 5b3b9b6..0000000
--- a/java/com/google/gwtexpui/safehtml/client/SafeHtml.java
+++ /dev/null
@@ -1,335 +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.gwtexpui.safehtml.client;
-
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.regexp.shared.MatchResult;
-import com.google.gwt.regexp.shared.RegExp;
-import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.ui.HTML;
-import com.google.gwt.user.client.ui.HTMLTable;
-import com.google.gwt.user.client.ui.HasHTML;
-import com.google.gwt.user.client.ui.InlineHTML;
-import com.google.gwt.user.client.ui.Widget;
-import java.util.Iterator;
-import java.util.List;
-
-/** Immutable string safely placed as HTML without further escaping. */
-public abstract class SafeHtml implements com.google.gwt.safehtml.shared.SafeHtml {
-  private static final long serialVersionUID = 1L;
-
-  public static final SafeHtmlResources RESOURCES;
-
-  static {
-    if (GWT.isClient()) {
-      RESOURCES = GWT.create(SafeHtmlResources.class);
-      RESOURCES.css().ensureInjected();
-
-    } else {
-      RESOURCES =
-          new SafeHtmlResources() {
-            @Override
-            public SafeHtmlCss css() {
-              return new SafeHtmlCss() {
-                @Override
-                public String wikiList() {
-                  return "wikiList";
-                }
-
-                @Override
-                public String wikiPreFormat() {
-                  return "wikiPreFormat";
-                }
-
-                @Override
-                public String wikiQuote() {
-                  return "wikiQuote";
-                }
-
-                @Override
-                public boolean ensureInjected() {
-                  return false;
-                }
-
-                @Override
-                public String getName() {
-                  return null;
-                }
-
-                @Override
-                public String getText() {
-                  return null;
-                }
-              };
-            }
-          };
-    }
-  }
-
-  /** @return the existing HTML property of a widget. */
-  public static SafeHtml get(HasHTML t) {
-    return new SafeHtmlString(t.getHTML());
-  }
-
-  /** @return the existing HTML text, wrapped in a safe buffer. */
-  public static SafeHtml asis(String htmlText) {
-    return new SafeHtmlString(htmlText);
-  }
-
-  /** Set the HTML property of a widget. */
-  public static <T extends HasHTML> T set(T e, SafeHtml str) {
-    e.setHTML(str.asString());
-    return e;
-  }
-
-  /** @return the existing inner HTML of any element. */
-  public static SafeHtml get(Element e) {
-    return new SafeHtmlString(e.getInnerHTML());
-  }
-
-  /** Set the inner HTML of any element. */
-  public static Element setInnerHTML(Element e, SafeHtml str) {
-    e.setInnerHTML(str.asString());
-    return e;
-  }
-
-  /** @return the existing inner HTML of a table cell. */
-  public static SafeHtml get(HTMLTable t, int row, int col) {
-    return new SafeHtmlString(t.getHTML(row, col));
-  }
-
-  /** Set the inner HTML of a table cell. */
-  public static <T extends HTMLTable> T set(final T t, int row, int col, SafeHtml str) {
-    t.setHTML(row, col, str.asString());
-    return t;
-  }
-
-  /** Parse an HTML block and return the first (typically root) element. */
-  public static Element parse(SafeHtml html) {
-    Element e = DOM.createDiv();
-    setInnerHTML(e, html);
-    return DOM.getFirstChild(e);
-  }
-
-  /** Convert bare http:// and https:// URLs into &lt;a href&gt; tags. */
-  public SafeHtml linkify() {
-    final String part = "(?:[a-zA-Z0-9$_+!*'%;:@=?#/~-]|&(?!lt;|gt;)|[.,](?!(?:\\s|$)))";
-    return replaceAll(
-        "(https?://" + part + "{2,}(?:[(]" + part + "*[)])*" + part + "*)",
-        "<a href=\"$1\" target=\"_blank\" rel=\"nofollow\">$1</a>");
-  }
-
-  /**
-   * Apply {@link #linkify()}, and "\n\n" to &lt;p&gt;.
-   *
-   * <p>Lines that start with whitespace are assumed to be preformatted, and are formatted by the
-   * {@link SafeHtmlCss#wikiPreFormat()} CSS class.
-   */
-  public SafeHtml wikify() {
-    final SafeHtmlBuilder r = new SafeHtmlBuilder();
-    for (String p : linkify().asString().split("\n\n")) {
-      if (isQuote(p)) {
-        wikifyQuote(r, p);
-
-      } else if (isPreFormat(p)) {
-        r.openElement("p");
-        for (String line : p.split("\n")) {
-          r.openSpan();
-          r.setStyleName(RESOURCES.css().wikiPreFormat());
-          r.append(asis(line));
-          r.closeSpan();
-          r.br();
-        }
-        r.closeElement("p");
-
-      } else if (isList(p)) {
-        wikifyList(r, p);
-
-      } else {
-        r.openElement("p");
-        r.append(asis(p));
-        r.closeElement("p");
-      }
-    }
-    return r.toSafeHtml();
-  }
-
-  private void wikifyList(SafeHtmlBuilder r, String p) {
-    boolean in_ul = false;
-    boolean in_p = false;
-    for (String line : p.split("\n")) {
-      if (line.startsWith("-") || line.startsWith("*")) {
-        if (!in_ul) {
-          if (in_p) {
-            in_p = false;
-            r.closeElement("p");
-          }
-
-          in_ul = true;
-          r.openElement("ul");
-          r.setStyleName(RESOURCES.css().wikiList());
-        }
-        line = line.substring(1).trim();
-
-      } else if (!in_ul) {
-        if (!in_p) {
-          in_p = true;
-          r.openElement("p");
-        } else {
-          r.append(' ');
-        }
-        r.append(asis(line));
-        continue;
-      }
-
-      r.openElement("li");
-      r.append(asis(line));
-      r.closeElement("li");
-    }
-
-    if (in_ul) {
-      r.closeElement("ul");
-    } else if (in_p) {
-      r.closeElement("p");
-    }
-  }
-
-  private void wikifyQuote(SafeHtmlBuilder r, String p) {
-    r.openElement("blockquote");
-    r.setStyleName(RESOURCES.css().wikiQuote());
-    if (p.startsWith("&gt; ")) {
-      p = p.substring(5);
-    } else if (p.startsWith(" &gt; ")) {
-      p = p.substring(6);
-    }
-    p = p.replaceAll("\\n ?&gt; ", "\n");
-    for (String e : p.split("\n\n")) {
-      if (isQuote(e)) {
-        SafeHtmlBuilder b = new SafeHtmlBuilder();
-        wikifyQuote(b, e);
-        r.append(b);
-      } else {
-        r.append(asis(e));
-      }
-    }
-    r.closeElement("blockquote");
-  }
-
-  private static boolean isQuote(String p) {
-    return p.startsWith("&gt; ") || p.startsWith(" &gt; ");
-  }
-
-  private static boolean isPreFormat(String p) {
-    return p.contains("\n ") || p.contains("\n\t") || p.startsWith(" ") || p.startsWith("\t");
-  }
-
-  private static boolean isList(String p) {
-    return p.contains("\n- ") || p.contains("\n* ") || p.startsWith("- ") || p.startsWith("* ");
-  }
-
-  /**
-   * Replace first occurrence of {@code regex} with {@code repl} .
-   *
-   * <p><b>WARNING:</b> This replacement is being performed against an otherwise safe HTML string.
-   * The caller must ensure that the replacement does not introduce cross-site scripting attack
-   * entry points.
-   *
-   * @param regex regular expression pattern to match the substring with.
-   * @param repl replacement expression. Capture groups within {@code regex} can be referenced with
-   *     {@code $<i>n</i>}.
-   * @return a new string, after the replacement has been made.
-   */
-  public SafeHtml replaceFirst(String regex, String repl) {
-    return new SafeHtmlString(asString().replaceFirst(regex, repl));
-  }
-
-  /**
-   * Replace each occurrence of {@code regex} with {@code repl} .
-   *
-   * <p><b>WARNING:</b> This replacement is being performed against an otherwise safe HTML string.
-   * The caller must ensure that the replacement does not introduce cross-site scripting attack
-   * entry points.
-   *
-   * @param regex regular expression pattern to match substrings with.
-   * @param repl replacement expression. Capture groups within {@code regex} can be referenced with
-   *     {@code $<i>n</i>}.
-   * @return a new string, after the replacements have been made.
-   */
-  public SafeHtml replaceAll(String regex, String repl) {
-    return new SafeHtmlString(asString().replaceAll(regex, repl));
-  }
-
-  /**
-   * Replace all find/replace pairs in the list in a single pass.
-   *
-   * @param findReplaceList find/replace pairs to use.
-   * @return a new string, after the replacements have been made.
-   */
-  public <T> SafeHtml replaceAll(List<? extends FindReplace> findReplaceList) {
-    if (findReplaceList == null || findReplaceList.isEmpty()) {
-      return this;
-    }
-
-    StringBuilder pat = new StringBuilder();
-    Iterator<? extends FindReplace> it = findReplaceList.iterator();
-    while (it.hasNext()) {
-      FindReplace fr = it.next();
-      pat.append(fr.pattern().getSource());
-      if (it.hasNext()) {
-        pat.append('|');
-      }
-    }
-
-    StringBuilder result = new StringBuilder();
-    RegExp re = RegExp.compile(pat.toString(), "g");
-    String orig = asString();
-    int index = 0;
-    MatchResult mat;
-    while ((mat = re.exec(orig)) != null) {
-      String g = mat.getGroup(0);
-      // Re-run each candidate to find which one matched.
-      for (FindReplace fr : findReplaceList) {
-        if (fr.pattern().test(g)) {
-          try {
-            String repl = fr.replace(g);
-            result.append(orig.substring(index, mat.getIndex()));
-            result.append(repl);
-          } catch (IllegalArgumentException e) {
-            continue;
-          }
-          index = mat.getIndex() + g.length();
-          break;
-        }
-      }
-    }
-    result.append(orig.substring(index, orig.length()));
-    return asis(result.toString());
-  }
-
-  /** @return a GWT block display widget displaying this HTML. */
-  public Widget toBlockWidget() {
-    return new HTML(asString());
-  }
-
-  /** @return a GWT inline display widget displaying this HTML. */
-  public Widget toInlineWidget() {
-    return new InlineHTML(asString());
-  }
-
-  /** @return a clean HTML string safe for inclusion in any context. */
-  @Override
-  public abstract String asString();
-}
diff --git a/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java b/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java
deleted file mode 100644
index cde0f2a..0000000
--- a/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java
+++ /dev/null
@@ -1,425 +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.gwtexpui.safehtml.client;
-
-import com.google.gwt.core.client.GWT;
-
-/** Safely constructs a {@link SafeHtml}, escaping user provided content. */
-public class SafeHtmlBuilder extends SafeHtml {
-  private static final long serialVersionUID = 1L;
-
-  private static final Impl impl;
-
-  static {
-    if (GWT.isClient()) {
-      impl = new ClientImpl();
-    } else {
-      impl = new ServerImpl();
-    }
-  }
-
-  private final BufferDirect dBuf;
-  private Buffer cb;
-
-  private BufferSealElement sBuf;
-  private AttMap att;
-
-  public SafeHtmlBuilder() {
-    cb = dBuf = new BufferDirect();
-  }
-
-  /** @return true if this builder has not had an append occur yet. */
-  public boolean isEmpty() {
-    return dBuf.isEmpty();
-  }
-
-  /** @return true if this builder has content appended into it. */
-  public boolean hasContent() {
-    return !isEmpty();
-  }
-
-  public SafeHtmlBuilder append(boolean in) {
-    cb.append(in);
-    return this;
-  }
-
-  public SafeHtmlBuilder append(char in) {
-    switch (in) {
-      case '&':
-        cb.append("&amp;");
-        break;
-
-      case '>':
-        cb.append("&gt;");
-        break;
-
-      case '<':
-        cb.append("&lt;");
-        break;
-
-      case '"':
-        cb.append("&quot;");
-        break;
-
-      case '\'':
-        cb.append("&#39;");
-        break;
-
-      default:
-        cb.append(in);
-        break;
-    }
-    return this;
-  }
-
-  public SafeHtmlBuilder append(int in) {
-    cb.append(in);
-    return this;
-  }
-
-  public SafeHtmlBuilder append(long in) {
-    cb.append(in);
-    return this;
-  }
-
-  public SafeHtmlBuilder append(float in) {
-    cb.append(in);
-    return this;
-  }
-
-  public SafeHtmlBuilder append(double in) {
-    cb.append(in);
-    return this;
-  }
-
-  /** Append already safe HTML as-is, avoiding double escaping. */
-  public SafeHtmlBuilder append(com.google.gwt.safehtml.shared.SafeHtml in) {
-    if (in != null) {
-      cb.append(in.asString());
-    }
-    return this;
-  }
-
-  /** Append already safe HTML as-is, avoiding double escaping. */
-  public SafeHtmlBuilder append(SafeHtml in) {
-    if (in != null) {
-      cb.append(in.asString());
-    }
-    return this;
-  }
-
-  /** Append the string, escaping unsafe characters. */
-  public SafeHtmlBuilder append(String in) {
-    if (in != null) {
-      impl.escapeStr(this, in);
-    }
-    return this;
-  }
-
-  /** Append the string, escaping unsafe characters. */
-  public SafeHtmlBuilder append(StringBuilder in) {
-    if (in != null) {
-      append(in.toString());
-    }
-    return this;
-  }
-
-  /** Append the string, escaping unsafe characters. */
-  public SafeHtmlBuilder append(StringBuffer in) {
-    if (in != null) {
-      append(in.toString());
-    }
-    return this;
-  }
-
-  /** Append the result of toString(), escaping unsafe characters. */
-  public SafeHtmlBuilder append(Object in) {
-    if (in != null) {
-      append(in.toString());
-    }
-    return this;
-  }
-
-  /** Append the string, escaping unsafe characters. */
-  public SafeHtmlBuilder append(CharSequence in) {
-    if (in != null) {
-      escapeCS(this, in);
-    }
-    return this;
-  }
-
-  /**
-   * Open an element, appending "{@code <tagName>}" to the buffer.
-   *
-   * <p>After the element is open the attributes may be manipulated until the next {@code append},
-   * {@code openElement}, {@code closeSelf} or {@code closeElement} call.
-   *
-   * @param tagName name of the HTML element to open.
-   */
-  public SafeHtmlBuilder openElement(String tagName) {
-    assert isElementName(tagName);
-    cb.append("<");
-    cb.append(tagName);
-    if (sBuf == null) {
-      att = new AttMap();
-      sBuf = new BufferSealElement(this);
-    }
-    att.reset(tagName);
-    cb = sBuf;
-    return this;
-  }
-
-  /**
-   * Get an attribute of the last opened element.
-   *
-   * @param name name of the attribute to read.
-   * @return the attribute value, as a string. The empty string if the attribute has not been
-   *     assigned a value. The returned string is the raw (unescaped) value.
-   */
-  public String getAttribute(String name) {
-    assert isAttributeName(name);
-    assert cb == sBuf;
-    return att.get(name);
-  }
-
-  /**
-   * Set an attribute of the last opened element.
-   *
-   * @param name name of the attribute to set.
-   * @param value value to assign; any existing value is replaced. The value is escaped (if
-   *     necessary) during the assignment.
-   */
-  public SafeHtmlBuilder setAttribute(String name, String value) {
-    assert isAttributeName(name);
-    assert cb == sBuf;
-    att.set(name, value != null ? value : "");
-    return this;
-  }
-
-  /**
-   * Set an attribute of the last opened element.
-   *
-   * @param name name of the attribute to set.
-   * @param value value to assign, any existing value is replaced.
-   */
-  public SafeHtmlBuilder setAttribute(String name, int value) {
-    return setAttribute(name, String.valueOf(value));
-  }
-
-  /**
-   * Append a new value into a whitespace delimited attribute.
-   *
-   * <p>If the attribute is not yet assigned, this method sets the attribute. If the attribute is
-   * already assigned, the new value is appended onto the end, after appending a single space to
-   * delimit the values.
-   *
-   * @param name name of the attribute to append onto.
-   * @param value additional value to append.
-   */
-  public SafeHtmlBuilder appendAttribute(String name, String value) {
-    if (value != null && value.length() > 0) {
-      final String e = getAttribute(name);
-      return setAttribute(name, e.length() > 0 ? e + " " + value : value);
-    }
-    return this;
-  }
-
-  /** Set the height attribute of the current element. */
-  public SafeHtmlBuilder setHeight(int height) {
-    return setAttribute("height", height);
-  }
-
-  /** Set the width attribute of the current element. */
-  public SafeHtmlBuilder setWidth(int width) {
-    return setAttribute("width", width);
-  }
-
-  /** Set the CSS class name for this element. */
-  public SafeHtmlBuilder setStyleName(String style) {
-    assert isCssName(style);
-    return setAttribute("class", style);
-  }
-
-  /**
-   * Add an additional CSS class name to this element.
-   *
-   * <p>If no CSS class name has been specified yet, this method initializes it to the single name.
-   */
-  public SafeHtmlBuilder addStyleName(String style) {
-    assert isCssName(style);
-    return appendAttribute("class", style);
-  }
-
-  private void sealElement0() {
-    assert cb == sBuf;
-    cb = dBuf;
-    att.onto(cb, this);
-  }
-
-  Buffer sealElement() {
-    sealElement0();
-    cb.append(">");
-    return cb;
-  }
-
-  /** Close the current element with a self closing suffix ("/ &gt;"). */
-  public SafeHtmlBuilder closeSelf() {
-    sealElement0();
-    cb.append(" />");
-    return this;
-  }
-
-  /** Append a closing tag for the named element. */
-  public SafeHtmlBuilder closeElement(String name) {
-    assert isElementName(name);
-    cb.append("</");
-    cb.append(name);
-    cb.append(">");
-    return this;
-  }
-
-  /** Append "&amp;nbsp;" - a non-breaking space, useful in empty table cells. */
-  public SafeHtmlBuilder nbsp() {
-    cb.append("&nbsp;");
-    return this;
-  }
-
-  /** Append "&lt;br /&gt;" - a line break with no attributes */
-  public SafeHtmlBuilder br() {
-    cb.append("<br />");
-    return this;
-  }
-
-  /** Append "&lt;tr&gt;"; attributes may be set if needed */
-  public SafeHtmlBuilder openTr() {
-    return openElement("tr");
-  }
-
-  /** Append "&lt;/tr&gt;" */
-  public SafeHtmlBuilder closeTr() {
-    return closeElement("tr");
-  }
-
-  /** Append "&lt;td&gt;"; attributes may be set if needed */
-  public SafeHtmlBuilder openTd() {
-    return openElement("td");
-  }
-
-  /** Append "&lt;/td&gt;" */
-  public SafeHtmlBuilder closeTd() {
-    return closeElement("td");
-  }
-
-  /** Append "&lt;th&gt;"; attributes may be set if needed */
-  public SafeHtmlBuilder openTh() {
-    return openElement("th");
-  }
-
-  /** Append "&lt;/th&gt;" */
-  public SafeHtmlBuilder closeTh() {
-    return closeElement("th");
-  }
-
-  /** Append "&lt;div&gt;"; attributes may be set if needed */
-  public SafeHtmlBuilder openDiv() {
-    return openElement("div");
-  }
-
-  /** Append "&lt;/div&gt;" */
-  public SafeHtmlBuilder closeDiv() {
-    return closeElement("div");
-  }
-
-  /** Append "&lt;span&gt;"; attributes may be set if needed */
-  public SafeHtmlBuilder openSpan() {
-    return openElement("span");
-  }
-
-  /** Append "&lt;/span&gt;" */
-  public SafeHtmlBuilder closeSpan() {
-    return closeElement("span");
-  }
-
-  /** Append "&lt;a&gt;"; attributes may be set if needed */
-  public SafeHtmlBuilder openAnchor() {
-    return openElement("a");
-  }
-
-  /** Append "&lt;/a&gt;" */
-  public SafeHtmlBuilder closeAnchor() {
-    return closeElement("a");
-  }
-
-  /** Append "&lt;param name=... value=... /&gt;". */
-  public SafeHtmlBuilder paramElement(String name, String value) {
-    openElement("param");
-    setAttribute("name", name);
-    setAttribute("value", value);
-    return closeSelf();
-  }
-
-  /** @return an immutable {@link SafeHtml} representation of the buffer. */
-  public SafeHtml toSafeHtml() {
-    return new SafeHtmlString(asString());
-  }
-
-  @Override
-  public String asString() {
-    return cb.toString();
-  }
-
-  private static void escapeCS(SafeHtmlBuilder b, CharSequence in) {
-    for (int i = 0; i < in.length(); i++) {
-      b.append(in.charAt(i));
-    }
-  }
-
-  private static boolean isElementName(String name) {
-    return name.matches("^[a-zA-Z][a-zA-Z0-9_-]*$");
-  }
-
-  private static boolean isAttributeName(String name) {
-    return isElementName(name);
-  }
-
-  private static boolean isCssName(String name) {
-    return isElementName(name);
-  }
-
-  private abstract static class Impl {
-    abstract void escapeStr(SafeHtmlBuilder b, String in);
-  }
-
-  private static class ServerImpl extends Impl {
-    @Override
-    void escapeStr(SafeHtmlBuilder b, String in) {
-      SafeHtmlBuilder.escapeCS(b, in);
-    }
-  }
-
-  private static class ClientImpl extends Impl {
-    @Override
-    void escapeStr(SafeHtmlBuilder b, String in) {
-      b.cb.append(escape(in));
-    }
-
-    private static native String escape(String src) /*-{ return src.replace(/&/g,'&amp;')
-                   .replace(/>/g,'&gt;')
-                   .replace(/</g,'&lt;')
-                   .replace(/"/g,'&quot;')
-                   .replace(/'/g,'&#39;');
-     }-*/;
-  }
-}
diff --git a/java/com/google/gwtexpui/safehtml/client/SafeHtmlCss.java b/java/com/google/gwtexpui/safehtml/client/SafeHtmlCss.java
deleted file mode 100644
index f4b1c77..0000000
--- a/java/com/google/gwtexpui/safehtml/client/SafeHtmlCss.java
+++ /dev/null
@@ -1,25 +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.gwtexpui.safehtml.client;
-
-import com.google.gwt.resources.client.CssResource;
-
-public interface SafeHtmlCss extends CssResource {
-  String wikiList();
-
-  String wikiPreFormat();
-
-  String wikiQuote();
-}
diff --git a/java/com/google/gwtexpui/safehtml/client/SafeHtmlResources.java b/java/com/google/gwtexpui/safehtml/client/SafeHtmlResources.java
deleted file mode 100644
index e3f5724..0000000
--- a/java/com/google/gwtexpui/safehtml/client/SafeHtmlResources.java
+++ /dev/null
@@ -1,22 +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.gwtexpui.safehtml.client;
-
-import com.google.gwt.resources.client.ClientBundle;
-
-public interface SafeHtmlResources extends ClientBundle {
-  @Source("safehtml.css")
-  SafeHtmlCss css();
-}
diff --git a/java/com/google/gwtexpui/safehtml/client/SafeHtmlString.java b/java/com/google/gwtexpui/safehtml/client/SafeHtmlString.java
deleted file mode 100644
index 5335170..0000000
--- a/java/com/google/gwtexpui/safehtml/client/SafeHtmlString.java
+++ /dev/null
@@ -1,30 +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.gwtexpui.safehtml.client;
-
-class SafeHtmlString extends SafeHtml {
-  private static final long serialVersionUID = 1L;
-
-  private final String html;
-
-  SafeHtmlString(String h) {
-    html = h;
-  }
-
-  @Override
-  public String asString() {
-    return html;
-  }
-}
diff --git a/java/com/google/gwtexpui/safehtml/client/safehtml.css b/java/com/google/gwtexpui/safehtml/client/safehtml.css
deleted file mode 100644
index 163c548..0000000
--- a/java/com/google/gwtexpui/safehtml/client/safehtml.css
+++ /dev/null
@@ -1,29 +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.
- */
-
-.wikiList {
-}
-
-.wikiPreFormat {
-  white-space: pre;
-  font-family: 'Lucida Console', 'Lucida Sans Typewriter', Monaco, monospace;
-  font-size: small;
-}
-
-.wikiQuote {
-  margin-left: 0;
-  border-left: 1px solid #888;
-  padding-left: 5px;
-}
diff --git a/java/com/google/gwtexpui/user/BUILD b/java/com/google/gwtexpui/user/BUILD
deleted file mode 100644
index 813f433..0000000
--- a/java/com/google/gwtexpui/user/BUILD
+++ /dev/null
@@ -1,10 +0,0 @@
-load("//tools/bzl:gwt.bzl", "gwt_module")
-
-gwt_module(
-    name = "agent",
-    srcs = glob(["client/*.java"]),
-    gwt_xml = "User.gwt.xml",
-    resources = ["client/tooltip.css"],
-    visibility = ["//visibility:public"],
-    deps = ["//lib/gwt:user"],
-)
diff --git a/java/com/google/gwtexpui/user/User.gwt.xml b/java/com/google/gwtexpui/user/User.gwt.xml
deleted file mode 100644
index f4f8d51..0000000
--- a/java/com/google/gwtexpui/user/User.gwt.xml
+++ /dev/null
@@ -1,18 +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.
--->
-<module>
-  <inherits name="com.google.gwt.user.User"/>
-</module>
diff --git a/java/com/google/gwtexpui/user/client/AutoCenterDialogBox.java b/java/com/google/gwtexpui/user/client/AutoCenterDialogBox.java
deleted file mode 100644
index fdaf861..0000000
--- a/java/com/google/gwtexpui/user/client/AutoCenterDialogBox.java
+++ /dev/null
@@ -1,79 +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.gwtexpui.user.client;
-
-import com.google.gwt.event.logical.shared.ResizeEvent;
-import com.google.gwt.event.logical.shared.ResizeHandler;
-import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.user.client.Window;
-import com.google.gwt.user.client.ui.DialogBox;
-
-/** A DialogBox that automatically re-centers itself if the window changes */
-public class AutoCenterDialogBox extends DialogBox {
-  private HandlerRegistration recenter;
-
-  public AutoCenterDialogBox() {
-    this(false);
-  }
-
-  public AutoCenterDialogBox(boolean autoHide) {
-    this(autoHide, true);
-  }
-
-  public AutoCenterDialogBox(boolean autoHide, boolean modal) {
-    super(autoHide, modal);
-  }
-
-  @Override
-  public void show() {
-    if (recenter == null) {
-      recenter =
-          Window.addResizeHandler(
-              new ResizeHandler() {
-                @Override
-                public void onResize(ResizeEvent event) {
-                  final int w = event.getWidth();
-                  final int h = event.getHeight();
-                  AutoCenterDialogBox.this.onResize(w, h);
-                }
-              });
-    }
-    super.show();
-  }
-
-  @Override
-  protected void onUnload() {
-    if (recenter != null) {
-      recenter.removeHandler();
-      recenter = null;
-    }
-    super.onUnload();
-  }
-
-  /**
-   * Invoked when the outer browser window resizes.
-   *
-   * <p>Subclasses may override (but should ensure they still call super.onResize) to implement
-   * custom logic when a window resize occurs.
-   *
-   * @param width new browser window width
-   * @param height new browser window height
-   */
-  protected void onResize(int width, int height) {
-    if (isAttached()) {
-      center();
-    }
-  }
-}
diff --git a/java/com/google/gwtexpui/user/client/Tooltip.java b/java/com/google/gwtexpui/user/client/Tooltip.java
deleted file mode 100644
index b5ce046..0000000
--- a/java/com/google/gwtexpui/user/client/Tooltip.java
+++ /dev/null
@@ -1,79 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gwtexpui.user.client;
-
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.resources.client.ClientBundle;
-import com.google.gwt.resources.client.CssResource;
-import com.google.gwt.user.client.ui.UIObject;
-
-/** Displays custom tooltip message below an element. */
-public class Tooltip {
-  interface Resources extends ClientBundle {
-    Resources I = GWT.create(Resources.class);
-
-    @Source("tooltip.css")
-    Css css();
-  }
-
-  interface Css extends CssResource {
-    String tooltip();
-  }
-
-  static {
-    Resources.I.css().ensureInjected();
-  }
-
-  /**
-   * Add required supporting style to enable custom tooltip rendering.
-   *
-   * @param o widget whose element should display a tooltip on hover.
-   */
-  public static void addStyle(UIObject o) {
-    addStyle(o.getElement());
-  }
-
-  /**
-   * Add required supporting style to enable custom tooltip rendering.
-   *
-   * @param e element that should display a tooltip on hover.
-   */
-  public static void addStyle(Element e) {
-    e.addClassName(Resources.I.css().tooltip());
-  }
-
-  /**
-   * Set the text displayed on hover.
-   *
-   * @param o widget whose hover text is being set.
-   * @param text message to display on hover.
-   */
-  public static void setLabel(UIObject o, String text) {
-    setLabel(o.getElement(), text);
-  }
-
-  /**
-   * Set the text displayed on hover.
-   *
-   * @param e element whose hover text is being set.
-   * @param text message to display on hover.
-   */
-  public static void setLabel(Element e, String text) {
-    e.setAttribute("aria-label", text != null ? text : "");
-  }
-
-  private Tooltip() {}
-}
diff --git a/java/com/google/gwtexpui/user/client/UserAgent.java b/java/com/google/gwtexpui/user/client/UserAgent.java
deleted file mode 100644
index 1660a62..0000000
--- a/java/com/google/gwtexpui/user/client/UserAgent.java
+++ /dev/null
@@ -1,168 +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.gwtexpui.user.client;
-
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.user.client.Window;
-
-/**
- * User agent feature tests we don't create permutations for.
- *
- * <p>Some features aren't worth creating full permutations in GWT for, as each new boolean
- * permutation (only two settings) doubles the compile time required. If the setting only affects a
- * couple of lines of JavaScript code, the slightly larger cache files for user agents that lack the
- * functionality requested is trivial compared to the time developers lose building their
- * application.
- */
-public class UserAgent {
-  private static boolean jsClip = guessJavaScriptClipboard();
-
-  public static boolean hasJavaScriptClipboard() {
-    return jsClip;
-  }
-
-  public static void disableJavaScriptClipboard() {
-    jsClip = false;
-  }
-
-  private static native boolean nativeHasCopy()
-      /*-{ return $doc['queryCommandSupported'] && $doc.queryCommandSupported('copy') }-*/ ;
-
-  private static boolean guessJavaScriptClipboard() {
-    String ua = Window.Navigator.getUserAgent();
-    int chrome = major(ua, "Chrome/");
-    if (chrome > 0) {
-      return 42 <= chrome;
-    }
-
-    int ff = major(ua, "Firefox/");
-    if (ff > 0) {
-      return 41 <= ff;
-    }
-
-    int opera = major(ua, "OPR/");
-    if (opera > 0) {
-      return 29 <= opera;
-    }
-
-    int msie = major(ua, "MSIE ");
-    if (msie > 0) {
-      return 9 <= msie;
-    }
-
-    if (nativeHasCopy()) {
-      // Firefox 39.0 lies and says it supports copy, then fails.
-      // So we try this after the browser specific test above.
-      return true;
-    }
-
-    // Safari is not planning to support document.execCommand('copy').
-    // Assume the browser does not have the feature.
-    return false;
-  }
-
-  private static int major(String ua, String product) {
-    int entry = ua.indexOf(product);
-    if (entry >= 0) {
-      String s = ua.substring(entry + product.length());
-      String p = s.split("[ /;,.)]", 2)[0];
-      try {
-        return Integer.parseInt(p);
-      } catch (NumberFormatException nan) {
-        // Ignored
-      }
-    }
-    return -1;
-  }
-
-  public static class Flash {
-    private static boolean checked;
-    private static boolean installed;
-
-    /**
-     * Does the browser have ShockwaveFlash plugin installed?
-     *
-     * <p>This method may still return true if the user has disabled Flash or set the plugin to
-     * "click to run".
-     */
-    public static boolean isInstalled() {
-      if (!checked) {
-        installed = hasFlash();
-        checked = true;
-      }
-      return installed;
-    }
-
-    private static native boolean hasFlash() /*-{
-      if (navigator.plugins && navigator.plugins.length) {
-        if (navigator.plugins['Shockwave Flash'])     return true;
-        if (navigator.plugins['Shockwave Flash 2.0']) return true;
-
-      } else if (navigator.mimeTypes && navigator.mimeTypes.length) {
-        var mimeType = navigator.mimeTypes['application/x-shockwave-flash'];
-        if (mimeType && mimeType.enabledPlugin) return true;
-
-      } else {
-        try { new ActiveXObject('ShockwaveFlash.ShockwaveFlash.7'); return true; } catch (e) {}
-        try { new ActiveXObject('ShockwaveFlash.ShockwaveFlash.6'); return true; } catch (e) {}
-        try { new ActiveXObject('ShockwaveFlash.ShockwaveFlash');   return true; } catch (e) {}
-      }
-      return false;
-    }-*/;
-  }
-
-  /**
-   * Test for and disallow running this application in an &lt;iframe&gt;.
-   *
-   * <p>If the application is running within an iframe this method requests a browser generated
-   * redirect to pop the application out of the iframe into the top level window, and then aborts
-   * execution by throwing an exception. This is call should be placed early within the module's
-   * onLoad() method, before any real UI can be initialized that an attacking site could try to snip
-   * out and present in a confusing context.
-   *
-   * <p>If the break out works, execution will restart automatically in a proper top level window,
-   * where the script has full control over the display. If the break out fails, execution will
-   * abort and stop immediately, preventing UI widgets from being created, leaving the user with an
-   * empty frame.
-   */
-  public static void assertNotInIFrame() {
-    if (GWT.isScript() && amInsideIFrame()) {
-      bustOutOfIFrame(Window.Location.getHref());
-      throw new RuntimeException();
-    }
-  }
-
-  private static native boolean amInsideIFrame() /*-{ return top.location != $wnd.location; }-*/;
-
-  private static native void bustOutOfIFrame(String newloc) /*-{ top.location.href = newloc }-*/;
-
-  /**
-   * Test if Gerrit is running on a mobile browser. This check could be incomplete, but should cover
-   * most cases. Regexes shamelessly borrowed from CodeMirror.
-   */
-  public static native boolean isMobile() /*-{
-    var ua = $wnd.navigator.userAgent;
-    var ios = /AppleWebKit/.test(ua) && /Mobile\/\w+/.test(ua);
-    return ios
-        || /Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(ua);
-  }-*/;
-
-  /** Check if the height of the browser view is greater than its width. */
-  public static boolean isPortrait() {
-    return Window.getClientHeight() > Window.getClientWidth();
-  }
-
-  private UserAgent() {}
-}
diff --git a/java/com/google/gwtexpui/user/client/View.java b/java/com/google/gwtexpui/user/client/View.java
deleted file mode 100644
index b15d2fd..0000000
--- a/java/com/google/gwtexpui/user/client/View.java
+++ /dev/null
@@ -1,55 +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.gwtexpui.user.client;
-
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.Widget;
-
-/**
- * Widget to display within a {@link ViewSite}.
- *
- * <p>Implementations must override {@code protected void onLoad()} and arrange for {@link
- * #display()} to be invoked once the DOM within the view is consistent for presentation to the
- * user. Typically this means that the subclass can start RPCs within {@code onLoad()} and then
- * invoke {@code display()} from within the AsyncCallback's {@code onSuccess(Object)} method.
- */
-public abstract class View extends Composite {
-  ViewSite<? extends View> site;
-
-  @Override
-  protected void onUnload() {
-    site = null;
-    super.onUnload();
-  }
-
-  /** true if this is the current view of its parent view site */
-  public final boolean isCurrentView() {
-    Widget p = getParent();
-    while (p != null) {
-      if (p instanceof ViewSite<?>) {
-        return ((ViewSite<?>) p).getView() == this;
-      }
-      p = p.getParent();
-    }
-    return false;
-  }
-
-  /** Replace the current view in the parent ViewSite with this view. */
-  public final void display() {
-    if (site != null) {
-      site.swap(this);
-    }
-  }
-}
diff --git a/java/com/google/gwtexpui/user/client/ViewSite.java b/java/com/google/gwtexpui/user/client/ViewSite.java
deleted file mode 100644
index 4614546..0000000
--- a/java/com/google/gwtexpui/user/client/ViewSite.java
+++ /dev/null
@@ -1,84 +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.gwtexpui.user.client;
-
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.SimplePanel;
-
-/**
- * Hosts a single {@link View}.
- *
- * <p>View instances are attached inside of an invisible DOM node, permitting their {@code onLoad()}
- * method to be invoked and to update the DOM prior to the elements being made visible in the UI.
- *
- * <p>Complaint View instances must invoke {@link View#display()} once the DOM is ready for
- * presentation.
- */
-public class ViewSite<V extends View> extends Composite {
-  private final FlowPanel main;
-  private SimplePanel current;
-  private SimplePanel next;
-
-  public ViewSite() {
-    main = new FlowPanel();
-    initWidget(main);
-  }
-
-  /** Get the current view; null if there is no view being displayed. */
-  @SuppressWarnings("unchecked")
-  public V getView() {
-    return current != null ? (V) current.getWidget() : null;
-  }
-
-  /**
-   * Set the next view to display.
-   *
-   * <p>The view will be attached to the DOM tree within a hidden container, permitting its {@code
-   * onLoad()} method to execute and update the DOM without the user seeing the result.
-   *
-   * @param view the next view to display.
-   */
-  public void setView(V view) {
-    if (next != null) {
-      main.remove(next);
-    }
-    view.site = this;
-    next = new SimplePanel();
-    next.setVisible(false);
-    main.add(next);
-    next.add(view);
-  }
-
-  /**
-   * Invoked after the view becomes the current view and has been made visible.
-   *
-   * @param view the view being displayed.
-   */
-  protected void onShowView(V view) {}
-
-  @SuppressWarnings("unchecked")
-  final void swap(View v) {
-    if (next != null && next.getWidget() == v) {
-      if (current != null) {
-        main.remove(current);
-      }
-      current = next;
-      next = null;
-      current.setVisible(true);
-      onShowView((V) v);
-    }
-  }
-}
diff --git a/java/com/google/gwtexpui/user/client/tooltip.css b/java/com/google/gwtexpui/user/client/tooltip.css
deleted file mode 100644
index 1aeb015..0000000
--- a/java/com/google/gwtexpui/user/client/tooltip.css
+++ /dev/null
@@ -1,54 +0,0 @@
-/* Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-.tooltip {
-  position: relative;
-}
-
-.tooltip:hover:before {
-  position: absolute;
-  z-index: 51;
-  border: solid;
-  border-color: #333 transparent;
-  border-width: 0 4px 4px 4px;
-  pointer-events: none;
-  content: "";
-
-  top: auto;
-  right: 50%;
-  bottom: -5px;
-  margin-right: -5px;
-}
-
-.tooltip:hover:after {
-  position: absolute;
-  z-index: 50;
-  font: normal normal 11px/1.5 Helvetica, arial, sans-serif;
-  text-align: center;
-  white-space: pre;
-  pointer-events: none;
-  background: rgba(0,0,0,.7);
-  color: #fff;
-  border-radius: 3px;
-  padding: 5px;
-  content: attr(aria-label);
-
-  top: 100%;
-  right: 50%;
-  margin-top: 5px;
-  -webkit-transform: translateX(50%);
-  -ms-transform: translateX(50%);
-  transform: translateX(50%)
-}
diff --git a/java/com/google/gwtorm/BUILD b/java/com/google/gwtorm/BUILD
new file mode 100644
index 0000000..baf7a8cb
--- /dev/null
+++ b/java/com/google/gwtorm/BUILD
@@ -0,0 +1,7 @@
+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
new file mode 100644
index 0000000..1c66d18
--- /dev/null
+++ b/java/com/google/gwtorm/client/CompoundKey.java
@@ -0,0 +1,108 @@
+// 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
new file mode 100644
index 0000000..08c90e0
--- /dev/null
+++ b/java/com/google/gwtorm/client/IntKey.java
@@ -0,0 +1,80 @@
+// 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
new file mode 100644
index 0000000..69a2248
--- /dev/null
+++ b/java/com/google/gwtorm/client/Key.java
@@ -0,0 +1,45 @@
+// 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
new file mode 100644
index 0000000..e236d37
--- /dev/null
+++ b/java/com/google/gwtorm/client/KeyUtil.java
@@ -0,0 +1,91 @@
+// 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/StandardKeyEncoder.java b/java/com/google/gwtorm/client/StandardKeyEncoder.java
new file mode 100644
index 0000000..1ceb0dc
--- /dev/null
+++ b/java/com/google/gwtorm/client/StandardKeyEncoder.java
@@ -0,0 +1,111 @@
+// 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 com.google.gwtorm.client.KeyUtil.Encoder;
+import java.io.UnsupportedEncodingException;
+import java.util.Arrays;
+
+public class StandardKeyEncoder extends Encoder {
+  private static final char[] hexc = {
+    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
+  };
+  private static final char safe[];
+  private static final byte hexb[];
+
+  static {
+    safe = new char[256];
+    safe['-'] = '-';
+    safe['_'] = '_';
+    safe['.'] = '.';
+    safe['!'] = '!';
+    safe['~'] = '~';
+    safe['*'] = '*';
+    safe['\''] = '\'';
+    safe['('] = '(';
+    safe[')'] = ')';
+    safe['/'] = '/';
+    safe[' '] = '+';
+    for (char c = '0'; c <= '9'; c++) safe[c] = c;
+    for (char c = 'A'; c <= 'Z'; c++) safe[c] = c;
+    for (char c = 'a'; c <= 'z'; c++) safe[c] = c;
+
+    hexb = new byte['f' + 1];
+    Arrays.fill(hexb, (byte) -1);
+    for (char i = '0'; i <= '9'; i++) hexb[i] = (byte) (i - '0');
+    for (char i = 'A'; i <= 'F'; i++) hexb[i] = (byte) ((i - 'A') + 10);
+    for (char i = 'a'; i <= 'f'; i++) hexb[i] = (byte) ((i - 'a') + 10);
+  }
+
+  @Override
+  public String encode(final String e) {
+    final byte[] b;
+    try {
+      b = e.getBytes("UTF-8");
+    } catch (UnsupportedEncodingException e1) {
+      throw new RuntimeException("No UTF-8 support", e1);
+    }
+
+    final StringBuilder r = new StringBuilder(b.length);
+    for (int i = 0; i < b.length; i++) {
+      final int c = b[i] & 0xff;
+      final char s = safe[c];
+      if (s == 0) {
+        r.append('%');
+        r.append(hexc[c >> 4]);
+        r.append(hexc[c & 15]);
+      } else {
+        r.append(s);
+      }
+    }
+    return r.toString();
+  }
+
+  @Override
+  public String decode(final String e) {
+    if (e.indexOf('%') < 0) {
+      return e.replace('+', ' ');
+    }
+
+    final byte[] b = new byte[e.length()];
+    int bPtr = 0;
+    try {
+      for (int i = 0; i < e.length(); ) {
+        final char c = e.charAt(i);
+        if (c == '%' && i + 2 < e.length()) {
+          final int v = (hexb[e.charAt(i + 1)] << 4) | hexb[e.charAt(i + 2)];
+          if (v < 0) {
+            throw new IllegalArgumentException(e.substring(i, i + 3));
+          }
+          b[bPtr++] = (byte) v;
+          i += 3;
+        } else if (c == '+') {
+          b[bPtr++] = ' ';
+          i++;
+        } else {
+          b[bPtr++] = (byte) c;
+          i++;
+        }
+      }
+    } catch (ArrayIndexOutOfBoundsException err) {
+      throw new IllegalArgumentException("Bad encoding" + e, err);
+    }
+    try {
+      return new String(b, 0, bPtr, "UTF-8");
+    } catch (UnsupportedEncodingException e1) {
+      throw new RuntimeException("No UTF-8 support", e1);
+    }
+  }
+}
diff --git a/java/com/google/gwtorm/client/StringKey.java b/java/com/google/gwtorm/client/StringKey.java
new file mode 100644
index 0000000..e56661f
--- /dev/null
+++ b/java/com/google/gwtorm/client/StringKey.java
@@ -0,0 +1,86 @@
+// 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/BUILD b/java/gerrit/BUILD
index 84f65db..d7e2306 100644
--- a/java/gerrit/BUILD
+++ b/java/gerrit/BUILD
@@ -6,10 +6,10 @@
     visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
-        "//lib:gwtorm",
         "//lib/flogger:api",
         "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/prolog:runtime",
diff --git a/java/gerrit/PRED__load_commit_labels_1.java b/java/gerrit/PRED__load_commit_labels_1.java
index 1d0ba8a..693c89e 100644
--- a/java/gerrit/PRED__load_commit_labels_1.java
+++ b/java/gerrit/PRED__load_commit_labels_1.java
@@ -7,8 +7,6 @@
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.rules.StoredValues;
-import com.google.gwtorm.server.OrmException;
-import com.googlecode.prolog_cafe.exceptions.JavaException;
 import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
 import com.googlecode.prolog_cafe.lang.ListTerm;
@@ -36,27 +34,22 @@
     Term a1 = arg1.dereference();
 
     Term listHead = Prolog.Nil;
-    try {
-      ChangeData cd = StoredValues.CHANGE_DATA.get(engine);
-      LabelTypes types = cd.getLabelTypes();
+    ChangeData cd = StoredValues.CHANGE_DATA.get(engine);
+    LabelTypes types = cd.getLabelTypes();
 
-      for (PatchSetApproval a : cd.currentApprovals()) {
-        LabelType t = types.byLabel(a.getLabelId());
-        if (t == null) {
-          continue;
-        }
-
-        StructureTerm labelTerm =
-            new StructureTerm(
-                sym_label, SymbolTerm.intern(t.getName()), new IntegerTerm(a.getValue()));
-
-        StructureTerm userTerm =
-            new StructureTerm(sym_user, new IntegerTerm(a.getAccountId().get()));
-
-        listHead = new ListTerm(new StructureTerm(sym_commit_label, labelTerm, userTerm), listHead);
+    for (PatchSetApproval a : cd.currentApprovals()) {
+      LabelType t = types.byLabel(a.getLabelId());
+      if (t == null) {
+        continue;
       }
-    } catch (OrmException err) {
-      throw new JavaException(this, 1, err);
+
+      StructureTerm labelTerm =
+          new StructureTerm(
+              sym_label, SymbolTerm.intern(t.getName()), new IntegerTerm(a.getValue()));
+
+      StructureTerm userTerm = new StructureTerm(sym_user, new IntegerTerm(a.getAccountId().get()));
+
+      listHead = new ListTerm(new StructureTerm(sym_commit_label, labelTerm, userTerm), listHead);
     }
 
     if (!a1.unify(listHead, engine.trail)) {
diff --git a/java/gerrit/PRED_commit_edits_2.java b/java/gerrit/PRED_commit_edits_2.java
index f46a6487..6ca5338 100644
--- a/java/gerrit/PRED_commit_edits_2.java
+++ b/java/gerrit/PRED_commit_edits_2.java
@@ -97,11 +97,7 @@
 
         if (fileRegex.matcher(newName).find()
             || (oldName != null && fileRegex.matcher(oldName).find())) {
-          // This cast still seems to be needed on JDK 8 as workaround for:
-          // https://bugs.openjdk.java.net/browse/JDK-8039214
-          @SuppressWarnings("cast")
-          List<Edit> edits = (List<Edit>) entry.getEdits();
-
+          List<Edit> edits = entry.getEdits();
           if (edits.isEmpty()) {
             continue;
           }
diff --git a/java/gerrit/PRED_get_legacy_label_types_1.java b/java/gerrit/PRED_get_legacy_label_types_1.java
index ef79e05..2f0c1ea 100644
--- a/java/gerrit/PRED_get_legacy_label_types_1.java
+++ b/java/gerrit/PRED_get_legacy_label_types_1.java
@@ -17,8 +17,6 @@
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelValue;
 import com.google.gerrit.server.rules.StoredValues;
-import com.google.gwtorm.server.OrmException;
-import com.googlecode.prolog_cafe.exceptions.JavaException;
 import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
 import com.googlecode.prolog_cafe.lang.ListTerm;
@@ -53,12 +51,7 @@
   public Operation exec(Prolog engine) throws PrologException {
     engine.setB0();
     Term a1 = arg1.dereference();
-    List<LabelType> list;
-    try {
-      list = StoredValues.CHANGE_DATA.get(engine).getLabelTypes().getLabelTypes();
-    } catch (OrmException err) {
-      throw new JavaException(this, 1, err);
-    }
+    List<LabelType> list = StoredValues.CHANGE_DATA.get(engine).getLabelTypes().getLabelTypes();
     Term head = Prolog.Nil;
     for (int idx = list.size() - 1; 0 <= idx; idx--) {
       head = new ListTerm(export(list.get(idx)), head);
diff --git a/java/gerrit/PRED_pure_revert_1.java b/java/gerrit/PRED_pure_revert_1.java
index 95a0729..6300a668 100644
--- a/java/gerrit/PRED_pure_revert_1.java
+++ b/java/gerrit/PRED_pure_revert_1.java
@@ -15,8 +15,6 @@
 package gerrit;
 
 import com.google.gerrit.server.rules.StoredValues;
-import com.google.gwtorm.server.OrmException;
-import com.googlecode.prolog_cafe.exceptions.JavaException;
 import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
 import com.googlecode.prolog_cafe.lang.Operation;
@@ -36,12 +34,7 @@
     engine.setB0();
     Term a1 = arg1.dereference();
 
-    Boolean isPureRevert;
-    try {
-      isPureRevert = StoredValues.CHANGE_DATA.get(engine).isPureRevert();
-    } catch (OrmException e) {
-      throw new JavaException(this, 1, e);
-    }
+    Boolean isPureRevert = StoredValues.CHANGE_DATA.get(engine).isPureRevert();
     if (!a1.unify(new IntegerTerm(Boolean.TRUE.equals(isPureRevert) ? 1 : 0), engine.trail)) {
       return engine.fail();
     }
diff --git a/java/gerrit/PRED_unresolved_comments_count_1.java b/java/gerrit/PRED_unresolved_comments_count_1.java
index 5ed1525..d4abcc54 100644
--- a/java/gerrit/PRED_unresolved_comments_count_1.java
+++ b/java/gerrit/PRED_unresolved_comments_count_1.java
@@ -15,8 +15,6 @@
 package gerrit;
 
 import com.google.gerrit.server.rules.StoredValues;
-import com.google.gwtorm.server.OrmException;
-import com.googlecode.prolog_cafe.exceptions.JavaException;
 import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
 import com.googlecode.prolog_cafe.lang.Operation;
@@ -35,13 +33,9 @@
     engine.setB0();
     Term a1 = arg1.dereference();
 
-    try {
-      Integer count = StoredValues.CHANGE_DATA.get(engine).unresolvedCommentCount();
-      if (!a1.unify(new IntegerTerm(count != null ? count : 0), engine.trail)) {
-        return engine.fail();
-      }
-    } catch (OrmException err) {
-      throw new JavaException(this, 1, err);
+    Integer count = StoredValues.CHANGE_DATA.get(engine).unresolvedCommentCount();
+    if (!a1.unify(new IntegerTerm(count != null ? count : 0), engine.trail)) {
+      return engine.fail();
     }
     return cont;
   }
diff --git a/java/org/eclipse/jgit/BUILD b/java/org/eclipse/jgit/BUILD
deleted file mode 100644
index f90debf1..0000000
--- a/java/org/eclipse/jgit/BUILD
+++ /dev/null
@@ -1,49 +0,0 @@
-load("@rules_java//java:defs.bzl", "java_library")
-load("//tools/bzl:genrule2.bzl", "genrule2")
-load("//tools/bzl:gwt.bzl", "gwt_module")
-
-gwt_module(
-    name = "client",
-    srcs = [
-        "diff/Edit_JsonSerializer.java",
-        "diff/ReplaceEdit.java",
-    ],
-    gwt_xml = "JGit.gwt.xml",
-    visibility = ["//visibility:public"],
-    deps = [
-        ":Edit",
-        "//lib:gwtjsonrpc",
-        "//lib/gwt:user",
-    ],
-)
-
-gwt_module(
-    name = "Edit",
-    srcs = [":jgit_edit_src"],
-    visibility = ["//visibility:public"],
-)
-
-genrule2(
-    name = "jgit_edit_src",
-    outs = ["edit.srcjar"],
-    cmd = " && ".join([
-        "unzip -qd $$TMP $(location //lib/jgit/org.eclipse.jgit:jgit-source) " +
-        "org/eclipse/jgit/diff/Edit.java",
-        "cd $$TMP",
-        "zip -Dq $$ROOT/$@ org/eclipse/jgit/diff/Edit.java",
-    ]),
-    tools = ["//lib/jgit/org.eclipse.jgit:jgit-source"],
-)
-
-java_library(
-    name = "server",
-    srcs = [
-        "diff/EditDeserializer.java",
-        "diff/ReplaceEdit.java",
-    ],
-    visibility = ["//visibility:public"],
-    deps = [
-        "//lib:gson",
-        "//lib/jgit/org.eclipse.jgit:jgit",
-    ],
-)
diff --git a/java/org/eclipse/jgit/JGit.gwt.xml b/java/org/eclipse/jgit/JGit.gwt.xml
deleted file mode 100644
index 5aeb936..0000000
--- a/java/org/eclipse/jgit/JGit.gwt.xml
+++ /dev/null
@@ -1,22 +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.
--->
-<module>
-  <source path='diff' includes='
-      Edit.java
-      Edit_JsonSerializer.java
-      ReplaceEdit.java
-    '/>
-</module>
diff --git a/java/org/eclipse/jgit/diff/EditDeserializer.java b/java/org/eclipse/jgit/diff/EditDeserializer.java
deleted file mode 100644
index 9435979..0000000
--- a/java/org/eclipse/jgit/diff/EditDeserializer.java
+++ /dev/null
@@ -1,95 +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 org.eclipse.jgit.diff;
-
-import com.google.gson.JsonArray;
-import com.google.gson.JsonDeserializationContext;
-import com.google.gson.JsonDeserializer;
-import com.google.gson.JsonElement;
-import com.google.gson.JsonNull;
-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;
-import java.util.ArrayList;
-import java.util.List;
-
-public class EditDeserializer implements JsonDeserializer<Edit>, JsonSerializer<Edit> {
-  @Override
-  public Edit deserialize(final JsonElement json, Type typeOfT, JsonDeserializationContext context)
-      throws JsonParseException {
-    if (json.isJsonNull()) {
-      return null;
-    }
-    if (!json.isJsonArray()) {
-      throw new JsonParseException("Expected array for Edit type");
-    }
-
-    final JsonArray o = (JsonArray) json;
-    final int cnt = o.size();
-    if (cnt < 4 || cnt % 4 != 0) {
-      throw new JsonParseException("Expected array of 4 for Edit type");
-    }
-
-    if (4 == cnt) {
-      return new Edit(get(o, 0), get(o, 1), get(o, 2), get(o, 3));
-    }
-
-    List<Edit> l = new ArrayList<>((cnt / 4) - 1);
-    for (int i = 4; i < cnt; ) {
-      int as = get(o, i++);
-      int ae = get(o, i++);
-      int bs = get(o, i++);
-      int be = get(o, i++);
-      l.add(new Edit(as, ae, bs, be));
-    }
-    return new ReplaceEdit(get(o, 0), get(o, 1), get(o, 2), get(o, 3), l);
-  }
-
-  private static int get(JsonArray a, int idx) throws JsonParseException {
-    final JsonElement v = a.get(idx);
-    if (!v.isJsonPrimitive()) {
-      throw new JsonParseException("Expected array of 4 for Edit type");
-    }
-    final JsonPrimitive p = (JsonPrimitive) v;
-    if (!p.isNumber()) {
-      throw new JsonParseException("Expected array of 4 for Edit type");
-    }
-    return p.getAsInt();
-  }
-
-  @Override
-  public JsonElement serialize(final Edit src, Type typeOfSrc, JsonSerializationContext context) {
-    if (src == null) {
-      return JsonNull.INSTANCE;
-    }
-    final JsonArray a = new JsonArray();
-    add(a, src);
-    if (src instanceof ReplaceEdit) {
-      for (Edit e : ((ReplaceEdit) src).getInternalEdits()) {
-        add(a, e);
-      }
-    }
-    return a;
-  }
-
-  private void add(JsonArray a, Edit src) {
-    a.add(new JsonPrimitive(src.getBeginA()));
-    a.add(new JsonPrimitive(src.getEndA()));
-    a.add(new JsonPrimitive(src.getBeginB()));
-    a.add(new JsonPrimitive(src.getEndB()));
-  }
-}
diff --git a/java/org/eclipse/jgit/diff/Edit_JsonSerializer.java b/java/org/eclipse/jgit/diff/Edit_JsonSerializer.java
deleted file mode 100644
index 184cb36..0000000
--- a/java/org/eclipse/jgit/diff/Edit_JsonSerializer.java
+++ /dev/null
@@ -1,74 +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 org.eclipse.jgit.diff;
-
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwtjsonrpc.client.impl.JsonSerializer;
-import java.util.ArrayList;
-import java.util.List;
-
-public class Edit_JsonSerializer extends JsonSerializer<Edit> {
-  public static final Edit_JsonSerializer INSTANCE = new Edit_JsonSerializer();
-
-  @Override
-  public Edit fromJson(Object jso) {
-    if (jso == null) {
-      return null;
-    }
-
-    final JavaScriptObject o = (JavaScriptObject) jso;
-    final int cnt = length(o);
-    if (4 == cnt) {
-      return new Edit(get(o, 0), get(o, 1), get(o, 2), get(o, 3));
-    }
-
-    List<Edit> l = new ArrayList<>((cnt / 4) - 1);
-    for (int i = 4; i < cnt; ) {
-      int as = get(o, i++);
-      int ae = get(o, i++);
-      int bs = get(o, i++);
-      int be = get(o, i++);
-      l.add(new Edit(as, ae, bs, be));
-    }
-    return new ReplaceEdit(get(o, 0), get(o, 1), get(o, 2), get(o, 3), l);
-  }
-
-  @Override
-  public void printJson(StringBuilder sb, Edit o) {
-    sb.append('[');
-    append(sb, o);
-    if (o instanceof ReplaceEdit) {
-      for (Edit e : ((ReplaceEdit) o).getInternalEdits()) {
-        sb.append(',');
-        append(sb, e);
-      }
-    }
-    sb.append(']');
-  }
-
-  private void append(StringBuilder sb, Edit o) {
-    sb.append(o.getBeginA());
-    sb.append(',');
-    sb.append(o.getEndA());
-    sb.append(',');
-    sb.append(o.getBeginB());
-    sb.append(',');
-    sb.append(o.getEndB());
-  }
-
-  private static native int length(JavaScriptObject jso) /*-{ return jso.length; }-*/;
-
-  private static native int get(JavaScriptObject jso, int idx) /*-{ return jso[idx]; }-*/;
-}
diff --git a/java/org/eclipse/jgit/diff/ReplaceEdit.java b/java/org/eclipse/jgit/diff/ReplaceEdit.java
deleted file mode 100644
index 46681c6..0000000
--- a/java/org/eclipse/jgit/diff/ReplaceEdit.java
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT 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 org.eclipse.jgit.diff;
-
-import java.util.List;
-
-public class ReplaceEdit extends Edit {
-  private List<Edit> internalEdit;
-
-  public ReplaceEdit(int as, int ae, int bs, int be, List<Edit> internal) {
-    super(as, ae, bs, be);
-    internalEdit = internal;
-  }
-
-  public ReplaceEdit(Edit orig, List<Edit> internal) {
-    super(orig.getBeginA(), orig.getEndA(), orig.getBeginB(), orig.getEndB());
-    internalEdit = internal;
-  }
-
-  public List<Edit> getInternalEdits() {
-    return internalEdit;
-  }
-}
diff --git a/javatests/com/google/gerrit/acceptance/BUILD b/javatests/com/google/gerrit/acceptance/BUILD
index 32804ef..54b3626 100644
--- a/javatests/com/google/gerrit/acceptance/BUILD
+++ b/javatests/com/google/gerrit/acceptance/BUILD
@@ -6,6 +6,7 @@
     deps = [
         "//java/com/google/gerrit/acceptance:lib",
         "//java/com/google/gerrit/server/util/time",
+        "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
         "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/truth",
diff --git a/javatests/com/google/gerrit/acceptance/MergeableFileBasedConfigTest.java b/javatests/com/google/gerrit/acceptance/MergeableFileBasedConfigTest.java
index 49c23e3..69fdc7e 100644
--- a/javatests/com/google/gerrit/acceptance/MergeableFileBasedConfigTest.java
+++ b/javatests/com/google/gerrit/acceptance/MergeableFileBasedConfigTest.java
@@ -18,13 +18,14 @@
 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 {
+public class MergeableFileBasedConfigTest extends GerritBaseTests {
   @Test
   public void mergeNull() throws Exception {
     MergeableFileBasedConfig cfg = newConfig();
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index be30ab7..6247739 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -48,6 +48,7 @@
 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;
@@ -58,12 +59,16 @@
 import com.google.gerrit.acceptance.UseSsh;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
 import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
+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.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule.Action;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.accounts.AccountInput;
 import com.google.gerrit.extensions.api.accounts.DeleteDraftCommentsInput;
 import com.google.gerrit.extensions.api.accounts.DeletedDraftCommentInfo;
@@ -94,16 +99,17 @@
 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.git.LockFailureException;
 import com.google.gerrit.gpg.Fingerprint;
 import com.google.gerrit.gpg.PublicKeyStore;
 import com.google.gerrit.gpg.testing.TestKey;
 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.Sequences;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.AccountProperties;
 import com.google.gerrit.server.account.AccountState;
@@ -116,11 +122,10 @@
 import com.google.gerrit.server.account.externalids.ExternalIdNotes;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.LockFailureException;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.index.account.AccountIndexer;
 import com.google.gerrit.server.index.account.StalenessChecker;
-import com.google.gerrit.server.notedb.rebuild.ChangeRebuilderImpl;
+import com.google.gerrit.server.notedb.Sequences;
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.project.RefPattern;
 import com.google.gerrit.server.query.account.InternalAccountQuery;
@@ -132,7 +137,6 @@
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.FakeEmailSender.Message;
 import com.google.gerrit.testing.TestTimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.name.Named;
@@ -192,36 +196,25 @@
     return cfg;
   }
 
-  @Inject private Provider<PublicKeyStore> publicKeyStoreProvider;
-
   @Inject private @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider;
-
-  @Inject private ExternalIds externalIds;
-
+  @Inject private AccountIndexer accountIndexer;
   @Inject private DynamicSet<AccountIndexedListener> accountIndexedListeners;
-
   @Inject private DynamicSet<GitReferenceUpdatedListener> refUpdateListeners;
-
-  @Inject private Sequences seq;
-
+  @Inject private ExternalIdNotes.Factory extIdNotesFactory;
+  @Inject private ExternalIds externalIds;
+  @Inject private GitReferenceUpdated gitReferenceUpdated;
+  @Inject private ProjectOperations projectOperations;
   @Inject private Provider<InternalAccountQuery> accountQueryProvider;
+  @Inject private Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory;
+  @Inject private Provider<PublicKeyStore> publicKeyStoreProvider;
+  @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private RetryHelper.Metrics retryMetrics;
+  @Inject private Sequences seq;
+  @Inject private StalenessChecker stalenessChecker;
+  @Inject private VersionedAuthorizedKeys.Accessor authorizedKeys;
 
   @Inject protected Emails emails;
 
-  @Inject private StalenessChecker stalenessChecker;
-
-  @Inject private AccountIndexer accountIndexer;
-
-  @Inject private GitReferenceUpdated gitReferenceUpdated;
-
-  @Inject private RetryHelper.Metrics retryMetrics;
-
-  @Inject private Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory;
-
-  @Inject private ExternalIdNotes.Factory extIdNotesFactory;
-
-  @Inject private VersionedAuthorizedKeys.Accessor authorizedKeys;
-
   @Inject
   @Named("accounts")
   private LoadingCache<Account.Id, Optional<AccountState>> accountsCache;
@@ -231,6 +224,8 @@
   @Inject
   private DynamicSet<AccountActivationValidationListener> accountActivationValidationListeners;
 
+  @Inject protected GroupOperations groupOperations;
+
   private AccountIndexedCounter accountIndexedCounter;
   private RegistrationHandle accountIndexEventCounterHandle;
   private RefUpdateCounter refUpdateCounter;
@@ -335,19 +330,19 @@
   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();
+    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.getId(), name, null);
-    return foo.getId();
+    assertUserBranch(foo.id(), name, null);
+    return foo.id();
   }
 
   @Test
   public void createAnonymousCowardByAccountCreator() throws Exception {
     TestAccount anonymousCoward = accountCreator.create();
     accountIndexedCounter.assertReindexOf(anonymousCoward);
-    assertUserBranchWithoutAccountConfig(anonymousCoward.getId());
+    assertUserBranchWithoutAccountConfig(anonymousCoward.id());
   }
 
   @Test
@@ -374,10 +369,10 @@
   @Test
   public void createAccountUsernameAlreadyTaken() throws Exception {
     AccountInput input = new AccountInput();
-    input.username = admin.username;
+    input.username = admin.username();
 
     exception.expect(ResourceConflictException.class);
-    exception.expectMessage("username '" + admin.username + "' already exists");
+    exception.expectMessage("username '" + admin.username() + "' already exists");
     gApi.accounts().create(input);
   }
 
@@ -385,10 +380,10 @@
   public void createAccountEmailAlreadyTaken() throws Exception {
     AccountInput input = new AccountInput();
     input.username = "foo";
-    input.email = admin.email;
+    input.email = admin.email();
 
     exception.expect(UnprocessableEntityException.class);
-    exception.expectMessage("email '" + admin.email + "' already exists");
+    exception.expectMessage("email '" + admin.email() + "' already exists");
     gApi.accounts().create(input);
   }
 
@@ -460,18 +455,18 @@
   @Test
   public void updateAccountWithoutAccountConfigNoteDb() throws Exception {
     TestAccount anonymousCoward = accountCreator.create();
-    assertUserBranchWithoutAccountConfig(anonymousCoward.getId());
+    assertUserBranchWithoutAccountConfig(anonymousCoward.id());
 
     String status = "OOO";
     Optional<AccountState> accountState =
         accountsUpdateProvider
             .get()
-            .update("Set status", anonymousCoward.getId(), u -> u.setStatus(status));
+            .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);
-    assertUserBranch(anonymousCoward.getId(), null, status);
+    assertUserBranch(anonymousCoward.id(), null, status);
   }
 
   private void assertUserBranchWithoutAccountConfig(Account.Id accountId) throws Exception {
@@ -488,7 +483,7 @@
       RevCommit c = rw.parseCommit(ref.getObjectId());
       long timestampDiffMs =
           Math.abs(c.getCommitTime() * 1000L - getAccount(accountId).getRegisteredOn().getTime());
-      assertThat(timestampDiffMs).isAtMost(ChangeRebuilderImpl.MAX_WINDOW_MS);
+      assertThat(timestampDiffMs).isAtMost(SECONDS.toMillis(1));
 
       // Check the 'account.config' file.
       try (TreeWalk tw = TreeWalk.forPath(or, AccountProperties.ACCOUNT_CONFIG, c.getTree())) {
@@ -538,17 +533,50 @@
 
   @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);
-    assertThat(gApi.accounts().id("user").getActive()).isFalse();
     accountIndexedCounter.assertReindexOf(user);
 
-    gApi.accounts().id("user").setActive(true);
+    // Inactive users may only be resolved by ID.
+    try {
+      gApi.accounts().id("user");
+      assert_().fail("expected ResourceNotFoundException");
+    } catch (ResourceNotFoundException e) {
+      assertThat(e)
+          .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();
+
+    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);
+
+    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);
+
+    gApi.changes().query("owner:foo").get();
+  }
+
+  @Test
   public void validateAccountActivation() throws Exception {
     Account.Id activatableAccountId =
         accountOperations.newAccount().inactive().preferredEmail("foo@activatable.com").create();
@@ -642,16 +670,17 @@
 
   @Test
   public void deactivateNotActive() throws Exception {
+    int id = gApi.accounts().id("user").get()._accountId;
     assertThat(gApi.accounts().id("user").getActive()).isTrue();
     gApi.accounts().id("user").setActive(false);
-    assertThat(gApi.accounts().id("user").getActive()).isFalse();
+    assertThat(gApi.accounts().id(id).getActive()).isFalse();
     try {
-      gApi.accounts().id("user").setActive(false);
+      gApi.accounts().id(id).setActive(false);
       fail("Expected exception");
     } catch (ResourceConflictException e) {
       assertThat(e.getMessage()).isEqualTo("account not active");
     }
-    gApi.accounts().id("user").setActive(true);
+    gApi.accounts().id(id).setActive(true);
   }
 
   @Test
@@ -666,7 +695,7 @@
     assertThat(change.stars).contains(DEFAULT_LABEL);
     refUpdateCounter.assertRefUpdateFor(
         RefUpdateCounter.projectRef(
-            allUsers, RefNames.refsStarredChanges(new Change.Id(change._number), admin.id)));
+            allUsers, RefNames.refsStarredChanges(new Change.Id(change._number), admin.id())));
 
     gApi.accounts().self().unstarChange(triplet);
     change = info(triplet);
@@ -674,7 +703,7 @@
     assertThat(change.stars).isNull();
     refUpdateCounter.assertRefUpdateFor(
         RefUpdateCounter.projectRef(
-            allUsers, RefNames.refsStarredChanges(new Change.Id(change._number), admin.id)));
+            allUsers, RefNames.refsStarredChanges(new Change.Id(change._number), admin.id())));
 
     accountIndexedCounter.assertNoReindex();
   }
@@ -705,7 +734,7 @@
     assertThat(starredChange.stars).containsExactly("blue", "red", DEFAULT_LABEL).inOrder();
     refUpdateCounter.assertRefUpdateFor(
         RefUpdateCounter.projectRef(
-            allUsers, RefNames.refsStarredChanges(new Change.Id(change._number), admin.id)));
+            allUsers, RefNames.refsStarredChanges(new Change.Id(change._number), admin.id())));
 
     gApi.accounts()
         .self()
@@ -724,14 +753,14 @@
     assertThat(starredChange.stars).containsExactly("red", "yellow").inOrder();
     refUpdateCounter.assertRefUpdateFor(
         RefUpdateCounter.projectRef(
-            allUsers, RefNames.refsStarredChanges(new Change.Id(change._number), admin.id)));
+            allUsers, RefNames.refsStarredChanges(new Change.Id(change._number), admin.id())));
 
     accountIndexedCounter.assertNoReindex();
 
-    setApiUser(user);
+    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);
+    gApi.accounts().id(Integer.toString((admin.id().get()))).getStars(triplet);
   }
 
   @Test
@@ -784,22 +813,22 @@
     PushOneCommit.Result r = createChange();
 
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     gApi.changes().id(r.getChangeId()).addReviewer(in);
 
     in = new AddReviewerInput();
-    in.reviewer = user2.email;
+    in.reviewer = user2.email();
     gApi.changes().id(r.getChangeId()).addReviewer(in);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     gApi.accounts().self().setStars(r.getChangeId(), new StarsInput(ImmutableSet.of(IGNORE_LABEL)));
 
     sender.clear();
-    setApiUser(admin);
+    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.emailAddress);
+    assertThat(messages.get(0).rcpt()).containsExactly(user2.getEmailAddress());
     accountIndexedCounter.assertNoReindex();
   }
 
@@ -807,20 +836,20 @@
   public void addReviewerToIgnoredChange() throws Exception {
     PushOneCommit.Result r = createChange();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     gApi.accounts().self().setStars(r.getChangeId(), new StarsInput(ImmutableSet.of(IGNORE_LABEL)));
 
     sender.clear();
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
 
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    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.emailAddress);
-    assertMailReplyTo(message, admin.email);
+    assertThat(message.rcpt()).containsExactly(user.getEmailAddress());
+    assertMailReplyTo(message, admin.email());
     accountIndexedCounter.assertNoReindex();
   }
 
@@ -847,20 +876,20 @@
     TestAccount foo = accountCreator.create(username, email, name);
     String secondaryEmail = "secondary@example.com";
     EmailInput input = newEmailInput(secondaryEmail);
-    gApi.accounts().id(foo.id.get()).addEmail(input);
+    gApi.accounts().id(foo.id().get()).addEmail(input);
 
     String status = "OOO";
-    gApi.accounts().id(foo.id.get()).setStatus(status);
+    gApi.accounts().id(foo.id().get()).setStatus(status);
 
-    setApiUser(foo);
-    AccountDetailInfo detail = gApi.accounts().id(foo.id.get()).detail();
-    assertThat(detail._accountId).isEqualTo(foo.id.get());
+    requestScopeOperations.setApiUser(foo.id());
+    AccountDetailInfo detail = gApi.accounts().id(foo.id().get()).detail();
+    assertThat(detail._accountId).isEqualTo(foo.id().get());
     assertThat(detail.name).isEqualTo(name);
     assertThat(detail.username).isEqualTo(username);
     assertThat(detail.email).isEqualTo(email);
     assertThat(detail.secondaryEmails).containsExactly(secondaryEmail);
     assertThat(detail.status).isEqualTo(status);
-    assertThat(detail.registeredOn).isEqualTo(getAccount(foo.getId()).getRegisteredOn());
+    assertThat(detail.registeredOn).isEqualTo(getAccount(foo.id()).getRegisteredOn());
     assertThat(detail.inactive).isNull();
     assertThat(detail._moreAccounts).isNull();
   }
@@ -872,10 +901,10 @@
     TestAccount foo = accountCreator.create(name("foo"), email, "Foo");
     String secondaryEmail = "secondary@example.com";
     EmailInput input = newEmailInput(secondaryEmail);
-    gApi.accounts().id(foo.id.get()).addEmail(input);
+    gApi.accounts().id(foo.id().get()).addEmail(input);
 
-    setApiUser(user);
-    AccountDetailInfo detail = gApi.accounts().id(foo.id.get()).detail();
+    requestScopeOperations.setApiUser(user.id());
+    AccountDetailInfo detail = gApi.accounts().id(foo.id().get()).detail();
     assertThat(detail.secondaryEmails).isNull();
   }
 
@@ -885,9 +914,9 @@
     TestAccount foo = accountCreator.create(name("foo"), email, "Foo");
     String secondaryEmail = "secondary@example.com";
     EmailInput input = newEmailInput(secondaryEmail);
-    gApi.accounts().id(foo.id.get()).addEmail(input);
+    gApi.accounts().id(foo.id().get()).addEmail(input);
 
-    AccountDetailInfo detail = gApi.accounts().id(foo.id.get()).detail();
+    AccountDetailInfo detail = gApi.accounts().id(foo.id().get()).detail();
     assertThat(detail.secondaryEmails).containsExactly(secondaryEmail);
   }
 
@@ -896,15 +925,15 @@
     String email = "preferred@example.com";
     TestAccount foo = accountCreator.create(name("foo"), email, "Foo");
 
-    setApiUser(foo);
+    requestScopeOperations.setApiUser(foo.id());
     assertThat(getEmails()).containsExactly(email);
 
-    setApiUser(admin);
+    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().hashCode()).addEmail(input);
 
-    setApiUser(foo);
+    requestScopeOperations.setApiUser(foo.id());
     assertThat(getEmails()).containsExactly(email, secondaryEmail);
   }
 
@@ -913,10 +942,10 @@
     String email = "preferred2@example.com";
     TestAccount foo = accountCreator.create(name("foo"), email, "Foo");
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("modify account not permitted");
-    gApi.accounts().id(foo.id.get()).getEmails();
+    gApi.accounts().id(foo.id().get()).getEmails();
   }
 
   @Test
@@ -925,10 +954,10 @@
     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().hashCode()).addEmail(input);
 
     assertThat(
-            gApi.accounts().id(foo.id.get()).getEmails().stream()
+            gApi.accounts().id(foo.id().get()).getEmails().stream()
                 .map(e -> e.email)
                 .collect(toSet()))
         .containsExactly(email, secondaryEmail);
@@ -945,8 +974,8 @@
       accountIndexedCounter.assertReindexOf(admin);
     }
 
-    resetCurrentApiUser();
-    assertThat(getEmails()).containsAllIn(emails);
+    requestScopeOperations.resetCurrentApiUser();
+    assertThat(getEmails()).containsAtLeastElementsIn(emails);
   }
 
   @Test
@@ -980,9 +1009,9 @@
   public void cannotAddNonConfirmedEmailWithoutModifyAccountPermission() throws Exception {
     TestAccount account = accountCreator.create(name("user"));
     EmailInput input = newEmailInput("test@test.com");
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
-    gApi.accounts().id(account.username).addEmail(input);
+    gApi.accounts().id(account.username()).addEmail(input);
   }
 
   @Test
@@ -992,7 +1021,7 @@
     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);
+    gApi.accounts().id(user.username()).addEmail(input);
   }
 
   @Test
@@ -1015,7 +1044,7 @@
       value = "HsOc6l+2lhS9G7sE/RsnS7Z6GJjdRDX14co=")
   public void addEmailToBeConfirmedToOwnAccount() throws Exception {
     TestAccount user = accountCreator.create();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
 
     String email = "self@example.com";
     EmailInput input = newEmailInput(email, false);
@@ -1026,11 +1055,11 @@
   public void cannotAddEmailToBeConfirmedToOtherAccountWithoutModifyAccountPermission()
       throws Exception {
     TestAccount user = accountCreator.create();
-    setApiUser(user);
+    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));
+    gApi.accounts().id(admin.id().get()).addEmail(newEmailInput("foo@example.com", false));
   }
 
   @Test
@@ -1040,7 +1069,7 @@
   public void addEmailToBeConfirmedToOtherAccount() throws Exception {
     TestAccount user = accountCreator.create();
     String email = "me@example.com";
-    gApi.accounts().id(user.id.get()).addEmail(newEmailInput(email, false));
+    gApi.accounts().id(user.id().get()).addEmail(newEmailInput(email, false));
   }
 
   @Test
@@ -1066,14 +1095,14 @@
     EmailInput input = newEmailInput(email);
     gApi.accounts().self().addEmail(input);
 
-    resetCurrentApiUser();
+    requestScopeOperations.resetCurrentApiUser();
     assertThat(getEmails()).contains(email);
 
     accountIndexedCounter.clear();
     gApi.accounts().self().deleteEmail(input.email);
     accountIndexedCounter.assertReindexOf(admin);
 
-    resetCurrentApiUser();
+    requestScopeOperations.resetCurrentApiUser();
     assertThat(getEmails()).doesNotContain(email);
   }
 
@@ -1098,7 +1127,7 @@
     gApi.accounts().self().deleteEmail(input.email);
     accountIndexedCounter.assertReindexOf(admin);
 
-    resetCurrentApiUser();
+    requestScopeOperations.resetCurrentApiUser();
     assertThat(getEmails()).containsExactly(previous);
     assertThat(gApi.accounts().self().get().email).isNull();
   }
@@ -1111,7 +1140,7 @@
     input.noConfirmation = true;
     gApi.accounts().self().addEmail(input);
 
-    resetCurrentApiUser();
+    requestScopeOperations.resetCurrentApiUser();
     Set<String> allEmails = getEmails();
     assertThat(allEmails).hasSize(2);
 
@@ -1119,7 +1148,7 @@
       gApi.accounts().self().deleteEmail(email);
     }
 
-    resetCurrentApiUser();
+    requestScopeOperations.resetCurrentApiUser();
     assertThat(getEmails()).isEmpty();
     assertThat(gApi.accounts().self().get().email).isNull();
   }
@@ -1133,24 +1162,25 @@
         .get()
         .update(
             "Add External IDs",
-            admin.id,
+            admin.id(),
             u ->
                 u.addExternalId(
-                        ExternalId.createWithEmail(ExternalId.Key.parse(extId1), admin.id, email))
+                        ExternalId.createWithEmail(ExternalId.Key.parse(extId1), admin.id(), email))
                     .addExternalId(
-                        ExternalId.createWithEmail(ExternalId.Key.parse(extId2), admin.id, email)));
+                        ExternalId.createWithEmail(
+                            ExternalId.Key.parse(extId2), admin.id(), email)));
     accountIndexedCounter.assertReindexOf(admin);
     assertThat(
             gApi.accounts().self().getExternalIds().stream().map(e -> e.identity).collect(toSet()))
-        .containsAllOf(extId1, extId2);
+        .containsAtLeast(extId1, extId2);
 
-    resetCurrentApiUser();
+    requestScopeOperations.resetCurrentApiUser();
     assertThat(getEmails()).contains(email);
 
     gApi.accounts().self().deleteEmail(email);
     accountIndexedCounter.assertReindexOf(admin);
 
-    resetCurrentApiUser();
+    requestScopeOperations.resetCurrentApiUser();
     assertThat(getEmails()).doesNotContain(email);
     assertThat(
             gApi.accounts().self().getExternalIds().stream().map(e -> e.identity).collect(toSet()))
@@ -1163,30 +1193,30 @@
     EmailInput input = new EmailInput();
     input.email = email;
     input.noConfirmation = true;
-    gApi.accounts().id(user.id.get()).addEmail(input);
+    gApi.accounts().id(user.id().get()).addEmail(input);
     accountIndexedCounter.assertReindexOf(user);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     assertThat(getEmails()).contains(email);
 
     // admin can delete email of user
-    setApiUser(admin);
-    gApi.accounts().id(user.id.get()).deleteEmail(email);
+    requestScopeOperations.setApiUser(admin.id());
+    gApi.accounts().id(user.id().get()).deleteEmail(email);
     accountIndexedCounter.assertReindexOf(user);
 
-    setApiUser(user);
+    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);
+    gApi.accounts().id(admin.id().get()).deleteEmail(admin.email());
   }
 
   @Test
   public void lookUpByEmail() throws Exception {
     // exact match with scheme "mailto:"
-    assertEmail(emails.getAccountFor(admin.email), admin);
+    assertEmail(emails.getAccountFor(admin.email()), admin);
 
     // exact match with other scheme
     String email = "foo.bar@example.com";
@@ -1194,26 +1224,28 @@
         .get()
         .update(
             "Add Email",
-            admin.id,
+            admin.id(),
             u ->
                 u.addExternalId(
-                    ExternalId.createWithEmail(ExternalId.Key.parse("foo:bar"), admin.id, email)));
+                    ExternalId.createWithEmail(
+                        ExternalId.Key.parse("foo:bar"), admin.id(), email)));
     assertEmail(emails.getAccountFor(email), admin);
 
     // wrong case doesn't match
-    assertThat(emails.getAccountFor(admin.email.toUpperCase(Locale.US))).isEmpty();
+    assertThat(emails.getAccountFor(admin.email().toUpperCase(Locale.US))).isEmpty();
 
     // prefix doesn't match
-    assertThat(emails.getAccountFor(admin.email.substring(0, admin.email.indexOf('@')))).isEmpty();
+    assertThat(emails.getAccountFor(admin.email().substring(0, admin.email().indexOf('@'))))
+        .isEmpty();
 
     // non-existing doesn't match
     assertThat(emails.getAccountFor("non-existing@example.com")).isEmpty();
 
     // lookup several accounts by email at once
     ImmutableSetMultimap<String, Account.Id> byEmails =
-        emails.getAccountsFor(admin.email, user.email);
-    assertEmail(byEmails.get(admin.email), admin);
-    assertEmail(byEmails.get(user.email), user);
+        emails.getAccountsFor(admin.email(), user.email());
+    assertEmail(byEmails.get(admin.email()), admin);
+    assertEmail(byEmails.get(user.email()), user);
   }
 
   @Test
@@ -1224,12 +1256,12 @@
     TestAccount foo = accountCreator.create(name("foo"));
     accountsUpdateProvider
         .get()
-        .update("Set Preferred Email", foo.id, u -> u.setPreferredEmail(prefEmail));
+        .update("Set Preferred Email", foo.id(), u -> u.setPreferredEmail(prefEmail));
 
     // verify that the account is still found when using the preferred email to lookup the account
     ImmutableSet<Account.Id> accountsByPrefEmail = emails.getAccountFor(prefEmail);
     assertThat(accountsByPrefEmail).hasSize(1);
-    assertThat(Iterables.getOnlyElement(accountsByPrefEmail)).isEqualTo(foo.id);
+    assertThat(Iterables.getOnlyElement(accountsByPrefEmail)).isEqualTo(foo.id());
 
     // look up by email prefix doesn't find the account
     accountsByPrefEmail = emails.getAccountFor(prefix);
@@ -1265,31 +1297,31 @@
 
   @Test
   public void adminCanSetNameOfOtherUser() throws Exception {
-    gApi.accounts().id(user.username).setName("User McUserface");
-    assertThat(gApi.accounts().id(user.username).get().name).isEqualTo("User McUserface");
+    gApi.accounts().id(user.username()).setName("User McUserface");
+    assertThat(gApi.accounts().id(user.username()).get().name).isEqualTo("User McUserface");
   }
 
   @Test
   public void userCannotSetNameOfOtherUser() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
-    gApi.accounts().id(admin.username).setName("Admin McAdminface");
+    gApi.accounts().id(admin.username()).setName("Admin McAdminface");
   }
 
   @Test
   @Sandboxed
   public void userCanSetNameOfOtherUserWithModifyAccountPermission() throws Exception {
     allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.MODIFY_ACCOUNT);
-    gApi.accounts().id(admin.username).setName("Admin McAdminface");
-    assertThat(gApi.accounts().id(admin.username).get().name).isEqualTo("Admin McAdminface");
+    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 {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, user);
-    String userRefName = RefNames.refsUsers(user.id);
+    String userRefName = RefNames.refsUsers(user.id());
 
     // remove default READ permissions
     try (ProjectConfigUpdate u = updateProject(allUsers)) {
@@ -1332,7 +1364,7 @@
     accountIndexedCounter.assertNoReindex();
 
     // fetching user branch of another user fails
-    String otherUserRefName = RefNames.refsUsers(admin.id);
+    String otherUserRefName = RefNames.refsUsers(admin.id());
     exception.expect(TransportException.class);
     exception.expectMessage("Remote does not have " + otherUserRefName + " available for fetch.");
     fetch(allUsersRepo, otherUserRefName + ":otherUserRef");
@@ -1341,24 +1373,24 @@
   @Test
   public void pushToUserBranch() throws Exception {
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    fetch(allUsersRepo, RefNames.refsUsers(admin.id) + ":userRef");
+    fetch(allUsersRepo, RefNames.refsUsers(admin.id()) + ":userRef");
     allUsersRepo.reset("userRef");
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), allUsersRepo);
-    push.to(RefNames.refsUsers(admin.id)).assertOkStatus();
+    PushOneCommit push = pushFactory.create(admin.newIdent(), allUsersRepo);
+    push.to(RefNames.refsUsers(admin.id())).assertOkStatus();
     accountIndexedCounter.assertReindexOf(admin);
 
-    push = pushFactory.create(db, admin.getIdent(), allUsersRepo);
+    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);
+    String userRefName = RefNames.refsUsers(admin.id());
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     fetch(allUsersRepo, userRefName + ":userRef");
     allUsersRepo.reset("userRef");
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), allUsersRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), allUsersRepo);
     PushOneCommit.Result r = push.to(MagicBranch.NEW_CHANGE + userRefName);
     r.assertOkStatus();
     accountIndexedCounter.assertNoReindex();
@@ -1367,7 +1399,7 @@
     gApi.changes().id(r.getChangeId()).current().submit();
     accountIndexedCounter.assertReindexOf(admin);
 
-    push = pushFactory.create(db, admin.getIdent(), allUsersRepo);
+    push = pushFactory.create(admin.newIdent(), allUsersRepo);
     r = push.to(MagicBranch.NEW_CHANGE + RefNames.REFS_USERS_SELF);
     r.assertOkStatus();
     accountIndexedCounter.assertNoReindex();
@@ -1379,7 +1411,7 @@
 
   @Test
   public void pushAccountConfigToUserBranchForReviewAndSubmit() throws Exception {
-    String userRef = RefNames.refsUsers(admin.id);
+    String userRef = RefNames.refsUsers(admin.id());
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     fetch(allUsersRepo, userRef + ":userRef");
     allUsersRepo.reset("userRef");
@@ -1390,8 +1422,7 @@
     PushOneCommit.Result r =
         pushFactory
             .create(
-                db,
-                admin.getIdent(),
+                admin.newIdent(),
                 allUsersRepo,
                 "Update account config",
                 AccountProperties.ACCOUNT_CONFIG,
@@ -1406,8 +1437,8 @@
     accountIndexedCounter.assertReindexOf(admin);
 
     AccountInfo info = gApi.accounts().self().get();
-    assertThat(info.email).isEqualTo(admin.email);
-    assertThat(info.name).isEqualTo(admin.fullName);
+    assertThat(info.email).isEqualTo(admin.email());
+    assertThat(info.name).isEqualTo(admin.fullName());
     assertThat(info.status).isEqualTo("out-of-office");
   }
 
@@ -1415,7 +1446,7 @@
   public void pushAccountConfigWithPrefEmailThatDoesNotExistAsExtIdToUserBranchForReviewAndSubmit()
       throws Exception {
     TestAccount foo = accountCreator.create(name("foo"), name("foo") + "@example.com", "Foo");
-    String userRef = RefNames.refsUsers(foo.id);
+    String userRef = RefNames.refsUsers(foo.id());
     accountIndexedCounter.clear();
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, foo);
@@ -1429,8 +1460,7 @@
     PushOneCommit.Result r =
         pushFactory
             .create(
-                db,
-                foo.getIdent(),
+                foo.newIdent(),
                 allUsersRepo,
                 "Update account config",
                 AccountProperties.ACCOUNT_CONFIG,
@@ -1440,7 +1470,7 @@
     accountIndexedCounter.assertNoReindex();
     assertThat(r.getChange().change().getDest().get()).isEqualTo(userRef);
 
-    setApiUser(foo);
+    requestScopeOperations.setApiUser(foo.id());
     gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
     gApi.changes().id(r.getChangeId()).current().submit();
 
@@ -1448,13 +1478,13 @@
 
     AccountInfo info = gApi.accounts().self().get();
     assertThat(info.email).isEqualTo(email);
-    assertThat(info.name).isEqualTo(foo.fullName);
+    assertThat(info.name).isEqualTo(foo.fullName());
   }
 
   @Test
   public void pushAccountConfigToUserBranchForReviewIsRejectedOnSubmitIfConfigIsInvalid()
       throws Exception {
-    String userRef = RefNames.refsUsers(admin.id);
+    String userRef = RefNames.refsUsers(admin.id());
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     fetch(allUsersRepo, userRef + ":userRef");
     allUsersRepo.reset("userRef");
@@ -1462,8 +1492,7 @@
     PushOneCommit.Result r =
         pushFactory
             .create(
-                db,
-                admin.getIdent(),
+                admin.newIdent(),
                 allUsersRepo,
                 "Update account config",
                 AccountProperties.ACCOUNT_CONFIG,
@@ -1481,7 +1510,7 @@
                 + " Invalid config file %s in commit %s",
             r.getCommit().name(),
             AccountProperties.ACCOUNT_CONFIG,
-            admin.id,
+            admin.id(),
             AccountProperties.ACCOUNT_CONFIG,
             r.getCommit().name()));
     gApi.changes().id(r.getChangeId()).current().submit();
@@ -1490,7 +1519,7 @@
   @Test
   public void pushAccountConfigToUserBranchForReviewIsRejectedOnSubmitIfPreferredEmailIsInvalid()
       throws Exception {
-    String userRef = RefNames.refsUsers(admin.id);
+    String userRef = RefNames.refsUsers(admin.id());
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     fetch(allUsersRepo, userRef + ":userRef");
     allUsersRepo.reset("userRef");
@@ -1502,8 +1531,7 @@
     PushOneCommit.Result r =
         pushFactory
             .create(
-                db,
-                admin.getIdent(),
+                admin.newIdent(),
                 allUsersRepo,
                 "Update account config",
                 AccountProperties.ACCOUNT_CONFIG,
@@ -1518,14 +1546,14 @@
     exception.expectMessage(
         String.format(
             "invalid account configuration: invalid preferred email '%s' for account '%s'",
-            noEmail, admin.id));
+            noEmail, admin.id()));
     gApi.changes().id(r.getChangeId()).current().submit();
   }
 
   @Test
   public void pushAccountConfigToUserBranchForReviewIsRejectedOnSubmitIfOwnAccountIsDeactivated()
       throws Exception {
-    String userRef = RefNames.refsUsers(admin.id);
+    String userRef = RefNames.refsUsers(admin.id());
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     fetch(allUsersRepo, userRef + ":userRef");
     allUsersRepo.reset("userRef");
@@ -1536,8 +1564,7 @@
     PushOneCommit.Result r =
         pushFactory
             .create(
-                db,
-                admin.getIdent(),
+                admin.newIdent(),
                 allUsersRepo,
                 "Update account config",
                 AccountProperties.ACCOUNT_CONFIG,
@@ -1558,8 +1585,8 @@
     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);
+    assertThat(gApi.accounts().id(foo.id().get()).getActive()).isTrue();
+    String userRef = RefNames.refsUsers(foo.id());
     accountIndexedCounter.clear();
 
     grant(allUsers, userRef, Permission.PUSH, false, adminGroupUuid());
@@ -1576,8 +1603,7 @@
     PushOneCommit.Result r =
         pushFactory
             .create(
-                db,
-                admin.getIdent(),
+                admin.newIdent(),
                 allUsersRepo,
                 "Update account config",
                 AccountProperties.ACCOUNT_CONFIG,
@@ -1591,13 +1617,13 @@
     gApi.changes().id(r.getChangeId()).current().submit();
     accountIndexedCounter.assertReindexOf(foo);
 
-    assertThat(gApi.accounts().id(foo.id.get()).getActive()).isFalse();
+    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");
+    fetch(allUsersRepo, RefNames.refsUsers(admin.id()) + ":userRef");
     allUsersRepo.reset("userRef");
 
     Config wc = new Config();
@@ -1608,8 +1634,7 @@
         ProjectWatches.NotifyValue.create(null, EnumSet.of(NotifyType.ALL_COMMENTS)).toString());
     PushOneCommit push =
         pushFactory.create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             allUsersRepo,
             "Add project watch",
             ProjectWatches.WATCH_CONFIG,
@@ -1622,8 +1647,7 @@
         ProjectWatches.PROJECT, project.get(), ProjectWatches.KEY_NOTIFY, invalidNotifyValue);
     push =
         pushFactory.create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             allUsersRepo,
             "Add invalid project watch",
             ProjectWatches.WATCH_CONFIG,
@@ -1633,17 +1657,17 @@
     r.assertMessage(
         String.format(
             "%s: Invalid project watch of account %d for project %s: %s",
-            ProjectWatches.WATCH_CONFIG, admin.getId().get(), project.get(), invalidNotifyValue));
+            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");
-    setApiUser(oooUser);
+    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");
+    fetch(allUsersRepo, RefNames.refsUsers(oooUser.id()) + ":userRef");
     allUsersRepo.reset("userRef");
 
     Config ac = getAccountConfig(allUsersRepo);
@@ -1652,34 +1676,32 @@
     accountIndexedCounter.clear();
     pushFactory
         .create(
-            db,
-            oooUser.getIdent(),
+            oooUser.newIdent(),
             allUsersRepo,
             "Update account config",
             AccountProperties.ACCOUNT_CONFIG,
             ac.toText())
-        .to(RefNames.refsUsers(oooUser.id))
+        .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.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");
+    fetch(allUsersRepo, RefNames.refsUsers(admin.id()) + ":userRef");
     allUsersRepo.reset("userRef");
 
     PushOneCommit.Result r =
         pushFactory
             .create(
-                db,
-                admin.getIdent(),
+                admin.newIdent(),
                 allUsersRepo,
                 "Update account config",
                 AccountProperties.ACCOUNT_CONFIG,
@@ -1692,7 +1714,7 @@
                 + " Invalid config file %s in commit %s",
             r.getCommit().name(),
             AccountProperties.ACCOUNT_CONFIG,
-            admin.id,
+            admin.id(),
             AccountProperties.ACCOUNT_CONFIG,
             r.getCommit().name()));
     accountIndexedCounter.assertNoReindex();
@@ -1701,7 +1723,7 @@
   @Test
   public void pushAccountConfigToUserBranchIsRejectedIfPreferredEmailIsInvalid() throws Exception {
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    fetch(allUsersRepo, RefNames.refsUsers(admin.id) + ":userRef");
+    fetch(allUsersRepo, RefNames.refsUsers(admin.id()) + ":userRef");
     allUsersRepo.reset("userRef");
 
     String noEmail = "no.email";
@@ -1711,8 +1733,7 @@
     PushOneCommit.Result r =
         pushFactory
             .create(
-                db,
-                admin.getIdent(),
+                admin.newIdent(),
                 allUsersRepo,
                 "Update account config",
                 AccountProperties.ACCOUNT_CONFIG,
@@ -1720,19 +1741,19 @@
             .to(RefNames.REFS_USERS_SELF);
     r.assertErrorStatus("invalid account configuration");
     r.assertMessage(
-        String.format("invalid preferred email '%s' for account '%s'", noEmail, admin.id));
+        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 userRef = RefNames.refsUsers(foo.id());
 
     String noEmail = "no.email";
     accountsUpdateProvider
         .get()
-        .update("Set Preferred Email", foo.id, u -> u.setPreferredEmail(noEmail));
+        .update("Set Preferred Email", foo.id(), u -> u.setPreferredEmail(noEmail));
     accountIndexedCounter.clear();
 
     grant(allUsers, userRef, Permission.PUSH, false, REGISTERED_USERS);
@@ -1746,8 +1767,7 @@
 
     pushFactory
         .create(
-            db,
-            foo.getIdent(),
+            foo.newIdent(),
             allUsersRepo,
             "Update account config",
             AccountProperties.ACCOUNT_CONFIG,
@@ -1756,16 +1776,16 @@
         .assertOkStatus();
     accountIndexedCounter.assertReindexOf(foo);
 
-    AccountInfo info = gApi.accounts().id(foo.id.get()).get();
+    AccountInfo info = gApi.accounts().id(foo.id().get()).get();
     assertThat(info.email).isEqualTo(noEmail);
-    assertThat(info.name).isEqualTo(foo.fullName);
+    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);
+    String userRef = RefNames.refsUsers(foo.id());
     accountIndexedCounter.clear();
 
     grant(allUsers, userRef, Permission.PUSH, false, adminGroupUuid());
@@ -1780,8 +1800,7 @@
 
     pushFactory
         .create(
-            db,
-            foo.getIdent(),
+            foo.newIdent(),
             allUsersRepo,
             "Update account config",
             AccountProperties.ACCOUNT_CONFIG,
@@ -1790,15 +1809,15 @@
         .assertOkStatus();
     accountIndexedCounter.assertReindexOf(foo);
 
-    AccountInfo info = gApi.accounts().id(foo.id.get()).get();
+    AccountInfo info = gApi.accounts().id(foo.id().get()).get();
     assertThat(info.email).isEqualTo(email);
-    assertThat(info.name).isEqualTo(foo.fullName);
+    assertThat(info.name).isEqualTo(foo.fullName());
   }
 
   @Test
   public void pushAccountConfigToUserBranchIsRejectedIfOwnAccountIsDeactivated() throws Exception {
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    fetch(allUsersRepo, RefNames.refsUsers(admin.id) + ":userRef");
+    fetch(allUsersRepo, RefNames.refsUsers(admin.id()) + ":userRef");
     allUsersRepo.reset("userRef");
 
     Config ac = getAccountConfig(allUsersRepo);
@@ -1807,8 +1826,7 @@
     PushOneCommit.Result r =
         pushFactory
             .create(
-                db,
-                admin.getIdent(),
+                admin.newIdent(),
                 allUsersRepo,
                 "Update account config",
                 AccountProperties.ACCOUNT_CONFIG,
@@ -1824,8 +1842,8 @@
     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);
+    assertThat(gApi.accounts().id(foo.id().get()).getActive()).isTrue();
+    String userRef = RefNames.refsUsers(foo.id());
     accountIndexedCounter.clear();
 
     grant(allUsers, userRef, Permission.PUSH, false, adminGroupUuid());
@@ -1839,8 +1857,7 @@
 
     pushFactory
         .create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             allUsersRepo,
             "Update account config",
             AccountProperties.ACCOUNT_CONFIG,
@@ -1849,7 +1866,7 @@
         .assertOkStatus();
     accountIndexedCounter.assertReindexOf(foo);
 
-    assertThat(gApi.accounts().id(foo.id.get()).getActive()).isFalse();
+    assertThat(gApi.accounts().id(foo.id().get()).getActive()).isFalse();
   }
 
   @Test
@@ -1859,7 +1876,7 @@
 
     String userRef = RefNames.refsUsers(new Account.Id(seq.nextAccountId()));
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    PushOneCommit.Result r = pushFactory.create(db, admin.getIdent(), allUsersRepo).to(userRef);
+    PushOneCommit.Result r = pushFactory.create(admin.newIdent(), allUsersRepo).to(userRef);
     r.assertErrorStatus();
     assertThat(r.getMessage()).contains("Not allowed to create user branch.");
 
@@ -1876,7 +1893,7 @@
 
     String userRef = RefNames.refsUsers(new Account.Id(seq.nextAccountId()));
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    pushFactory.create(db, admin.getIdent(), allUsersRepo).to(userRef).assertOkStatus();
+    pushFactory.create(admin.newIdent(), allUsersRepo).to(userRef).assertOkStatus();
 
     try (Repository repo = repoManager.openRepository(allUsers)) {
       assertThat(repo.exactRef(userRef)).isNotNull();
@@ -1892,7 +1909,7 @@
 
     String userRef = RefNames.REFS_USERS + "foo";
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    PushOneCommit.Result r = pushFactory.create(db, admin.getIdent(), allUsersRepo).to(userRef);
+    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/.");
 
@@ -1912,7 +1929,7 @@
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     pushFactory
-        .create(db, admin.getIdent(), allUsersRepo)
+        .create(admin.newIdent(), allUsersRepo)
         .to(RefNames.REFS_USERS_DEFAULT)
         .assertOkStatus();
 
@@ -1931,7 +1948,7 @@
         REGISTERED_USERS);
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    String userRef = RefNames.refsUsers(admin.id);
+    String userRef = RefNames.refsUsers(admin.id());
     PushResult r = deleteRef(allUsersRepo, userRef);
     RemoteRefUpdate refUpdate = r.getRemoteUpdate(userRef);
     assertThat(refUpdate.getStatus()).isEqualTo(RemoteRefUpdate.Status.REJECTED_OTHER_REASON);
@@ -1953,7 +1970,7 @@
         REGISTERED_USERS);
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    String userRef = RefNames.refsUsers(admin.id);
+    String userRef = RefNames.refsUsers(admin.id());
     PushResult r = deleteRef(allUsersRepo, userRef);
     RemoteRefUpdate refUpdate = r.getRemoteUpdate(userRef);
     assertThat(refUpdate.getStatus()).isEqualTo(RemoteRefUpdate.Status.OK);
@@ -1962,8 +1979,8 @@
       assertThat(repo.exactRef(userRef)).isNull();
     }
 
-    assertThat(accountCache.get(admin.id)).isEmpty();
-    assertThat(accountQueryProvider.get().byDefault(admin.id.toString())).isEmpty();
+    assertThat(accountCache.get(admin.id())).isEmpty();
+    assertThat(accountQueryProvider.get().byDefault(admin.id().toString())).isEmpty();
   }
 
   @Test
@@ -1978,7 +1995,7 @@
     assertThat(sender.getMessages()).hasSize(1);
     assertThat(sender.getMessages().get(0).body()).contains("new GPG keys have been added");
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(ResourceNotFoundException.class);
     exception.expectMessage(id);
     gApi.accounts().self().gpgKey(id).get();
@@ -1990,7 +2007,7 @@
     addExternalIdEmail(user, "test1@example.com");
 
     sender.clear();
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     exception.expect(ResourceNotFoundException.class);
     addGpgKey(user, key.getPublicKeyArmored());
   }
@@ -2027,13 +2044,13 @@
         .get()
         .update(
             "Add External ID",
-            user.getId(),
-            u -> u.addExternalId(ExternalId.create("foo", "myId", user.getId())));
+            user.id(),
+            u -> u.addExternalId(ExternalId.create("foo", "myId", user.id())));
     accountIndexedCounter.assertReindexOf(user);
 
     TestKey key = validKeyWithSecondUserId();
     addGpgKey(key.getPublicKeyArmored());
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
 
     exception.expect(ResourceConflictException.class);
     exception.expectMessage("GPG key already associated with another account");
@@ -2048,7 +2065,7 @@
       addExternalIdEmail(admin, PushCertificateIdent.parse(key.getFirstUserId()).getEmailAddress());
       toAdd.add(key.getPublicKeyArmored());
     }
-    gApi.accounts().self().putGpgKeys(toAdd, ImmutableList.<String>of());
+    gApi.accounts().self().putGpgKeys(toAdd, ImmutableList.of());
     assertKeys(keys);
     accountIndexedCounter.assertReindexOf(admin);
   }
@@ -2129,13 +2146,13 @@
     assertSequenceNumbers(info);
     SshKeyInfo key = info.get(0);
     KeyPair keyPair = sshKeys.getKeyPair(admin);
-    String initial = TestSshKeys.publicKey(keyPair, admin.email);
+    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);
+    String newKey = TestSshKeys.publicKey(TestSshKeys.genSshKey(), admin.email());
     gApi.accounts().self().addSshKey(newKey);
     info = gApi.accounts().self().listSshKeys();
     assertThat(info).hasSize(2);
@@ -2157,7 +2174,7 @@
 
     // Add another new key
     sender.clear();
-    String newKey2 = TestSshKeys.publicKey(TestSshKeys.genSshKey(), admin.email);
+    String newKey2 = TestSshKeys.publicKey(TestSshKeys.genSshKey(), admin.email());
     gApi.accounts().self().addSshKey(newKey2);
     info = gApi.accounts().self().listSshKeys();
     assertThat(info).hasSize(3);
@@ -2180,7 +2197,7 @@
 
     // Mark first key as invalid
     assertThat(info.get(0).valid).isTrue();
-    authorizedKeys.markKeyInvalid(admin.id, 1);
+    authorizedKeys.markKeyInvalid(admin.id(), 1);
     info = gApi.accounts().self().listSshKeys();
     assertThat(info).hasSize(2);
     assertThat(info.get(0).seq).isEqualTo(1);
@@ -2198,77 +2215,77 @@
     assertSequenceNumbers(info);
     SshKeyInfo key = info.get(0);
     KeyPair keyPair = sshKeys.getKeyPair(admin);
-    String initial = TestSshKeys.publicKey(keyPair, admin.email);
+    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();
+    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.emailAddress);
+    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();
+    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.emailAddress);
+    assertThat(message.rcpt()).containsExactly(user.getEmailAddress());
     assertThat(message.body()).contains("SSH keys have been deleted");
   }
 
   @Test
   @UseSsh
   public void userCannotAddSshKeyToOtherAccount() throws Exception {
-    String newKey = TestSshKeys.publicKey(TestSshKeys.genSshKey(), admin.email);
-    setApiUser(user);
+    String newKey = TestSshKeys.publicKey(TestSshKeys.genSshKey(), admin.email());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
-    gApi.accounts().id(admin.username).addSshKey(newKey);
+    gApi.accounts().id(admin.username()).addSshKey(newKey);
   }
 
   @Test
   @UseSsh
   public void userCannotDeleteSshKeyOfOtherAccount() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(ResourceNotFoundException.class);
-    gApi.accounts().id(admin.username).deleteSshKey(0);
+    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
-    setApiUser(admin);
-    gApi.accounts().id(user.username).index();
+    requestScopeOperations.setApiUser(admin.id());
+    gApi.accounts().id(user.username()).index();
     accountIndexedCounter.assertReindexOf(user);
 
     // user can reindex own account
-    setApiUser(user);
+    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();
+    gApi.accounts().id(admin.username()).index();
   }
 
   @Test
   public void checkConsistency() throws Exception {
     allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
-    resetCurrentApiUser();
+    requestScopeOperations.resetCurrentApiUser();
 
     // Create an account with a preferred email.
     String username = name("foo");
@@ -2288,13 +2305,13 @@
         .get()
         .update(
             "Delete External ID",
-            account.getId(),
-            u -> u.deleteExternalId(ExternalId.createEmail(account.getId(), email)));
+            account.id(),
+            u -> u.deleteExternalId(ExternalId.createEmail(account.id(), email)));
     expectedProblems.add(
         new ConsistencyProblemInfo(
             ConsistencyProblemInfo.Status.ERROR,
             "Account '"
-                + account.getId().get()
+                + account.id().get()
                 + "' has no external ID for its preferred email '"
                 + email
                 + "'"));
@@ -2310,11 +2327,11 @@
     assertThat(accountQueryProvider.get().byDefault(name)).isEmpty();
 
     TestAccount foo1 = accountCreator.create(name + "-1");
-    assertThat(gApi.accounts().id(foo1.username).getActive()).isTrue();
+    assertThat(gApi.accounts().id(foo1.username()).getActive()).isTrue();
 
     TestAccount foo2 = accountCreator.create(name + "-2");
-    gApi.accounts().id(foo2.username).setActive(false);
-    assertThat(gApi.accounts().id(foo2.username).getActive()).isFalse();
+    gApi.accounts().id(foo2.username()).setActive(false);
+    assertThat(gApi.accounts().id(foo2.id().get()).getActive()).isFalse();
 
     assertThat(accountQueryProvider.get().byDefault(name)).hasSize(2);
   }
@@ -2322,8 +2339,8 @@
   @Test
   public void checkMetaId() throws Exception {
     // metaId is set when account is loaded
-    assertThat(accounts.get(admin.getId()).get().getAccount().getMetaId())
-        .isEqualTo(getMetaId(admin.getId()));
+    assertThat(accounts.get(admin.id()).get().getAccount().getMetaId())
+        .isEqualTo(getMetaId(admin.id()));
 
     // metaId is set when account is created
     AccountsUpdate au = accountsUpdateProvider.get();
@@ -2362,7 +2379,7 @@
 
   @Test
   public void allGroupsForAnAdminAccountCanBeRetrieved() throws Exception {
-    List<GroupInfo> groups = gApi.accounts().id(admin.username).getGroups();
+    List<GroupInfo> groups = gApi.accounts().id(admin.username()).getGroups();
     assertThat(groups)
         .comparingElementsUsing(getGroupToNameCorrespondence())
         .containsExactly("Anonymous Users", "Registered Users", "Administrators");
@@ -2404,7 +2421,9 @@
   public void allGroupsForAUserAccountCanBeRetrieved() throws Exception {
     String username = name("user1");
     accountOperations.newAccount().username(username).create();
-    String group = createGroup("group");
+    AccountGroup.UUID groupID = groupOperations.newGroup().name("group").create();
+    String group = groupOperations.group(groupID).get().name();
+
     gApi.groups().id(group).addMembers(username);
 
     List<GroupInfo> allGroups = gApi.accounts().id(username).getGroups();
@@ -2448,17 +2467,12 @@
         new AccountsUpdate(
             repoManager,
             gitReferenceUpdated,
-            null,
+            Optional.empty(),
             allUsers,
             externalIds,
             metaDataUpdateInternalFactory,
             new RetryHelper(
-                cfg,
-                retryMetrics,
-                null,
-                null,
-                null,
-                r -> r.withBlockStrategy(noSleepBlockStrategy)),
+                cfg, retryMetrics, null, r -> r.withBlockStrategy(noSleepBlockStrategy)),
             extIdNotesFactory,
             ident,
             ident,
@@ -2467,20 +2481,20 @@
                 try {
                   accountsUpdateProvider
                       .get()
-                      .update("Set Status", admin.id, u -> u.setStatus(status));
-                } catch (IOException | ConfigInvalidException | OrmException e) {
+                      .update("Set Status", admin.id(), u -> u.setStatus(status));
+                } catch (IOException | ConfigInvalidException | StorageException e) {
                   // Ignore, the successful update of the account is asserted later
                 }
               }
             },
             Runnables.doNothing());
     assertThat(doneBgUpdate.get()).isFalse();
-    AccountInfo accountInfo = gApi.accounts().id(admin.id.get()).get();
+    AccountInfo accountInfo = gApi.accounts().id(admin.id().get()).get();
     assertThat(accountInfo.status).isNull();
     assertThat(accountInfo.name).isNotEqualTo(fullName);
 
     Optional<AccountState> updatedAccountState =
-        update.update("Set Full Name", admin.id, u -> u.setFullName(fullName));
+        update.update("Set Full Name", admin.id(), u -> u.setFullName(fullName));
     assertThat(doneBgUpdate.get()).isTrue();
 
     assertThat(updatedAccountState).isPresent();
@@ -2488,7 +2502,7 @@
     assertThat(updatedAccount.getStatus()).isEqualTo(status);
     assertThat(updatedAccount.getFullName()).isEqualTo(fullName);
 
-    accountInfo = gApi.accounts().id(admin.id.get()).get();
+    accountInfo = gApi.accounts().id(admin.id().get()).get();
     assertThat(accountInfo.status).isEqualTo(status);
     assertThat(accountInfo.name).isEqualTo(fullName);
   }
@@ -2503,7 +2517,7 @@
         new AccountsUpdate(
             repoManager,
             gitReferenceUpdated,
-            null,
+            Optional.empty(),
             allUsers,
             externalIds,
             metaDataUpdateInternalFactory,
@@ -2511,8 +2525,6 @@
                 cfg,
                 retryMetrics,
                 null,
-                null,
-                null,
                 r ->
                     r.withStopStrategy(StopStrategies.stopAfterAttempt(status.size()))
                         .withBlockStrategy(noSleepBlockStrategy)),
@@ -2525,38 +2537,38 @@
                     .get()
                     .update(
                         "Set Status",
-                        admin.id,
+                        admin.id(),
                         u -> u.setStatus(status.get(bgCounter.getAndAdd(1))));
-              } catch (IOException | ConfigInvalidException | OrmException e) {
+              } catch (IOException | ConfigInvalidException | StorageException e) {
                 // Ignore, the expected exception is asserted later
               }
             },
             Runnables.doNothing());
     assertThat(bgCounter.get()).isEqualTo(0);
-    AccountInfo accountInfo = gApi.accounts().id(admin.id.get()).get();
+    AccountInfo accountInfo = gApi.accounts().id(admin.id().get()).get();
     assertThat(accountInfo.status).isNull();
     assertThat(accountInfo.name).isNotEqualTo(fullName);
 
     try {
-      update.update("Set Full Name", admin.id, u -> u.setFullName(fullName));
+      update.update("Set Full Name", admin.id(), u -> u.setFullName(fullName));
       fail("expected LockFailureException");
     } catch (LockFailureException e) {
       // Ignore, expected
     }
     assertThat(bgCounter.get()).isEqualTo(status.size());
 
-    Account updatedAccount = accounts.get(admin.id).get().getAccount();
+    Account updatedAccount = accounts.get(admin.id()).get().getAccount();
     assertThat(updatedAccount.getStatus()).isEqualTo(Iterables.getLast(status));
-    assertThat(updatedAccount.getFullName()).isEqualTo(admin.fullName);
+    assertThat(updatedAccount.getFullName()).isEqualTo(admin.fullName());
 
-    accountInfo = gApi.accounts().id(admin.id.get()).get();
+    accountInfo = gApi.accounts().id(admin.id().get()).get();
     assertThat(accountInfo.status).isEqualTo(Iterables.getLast(status));
-    assertThat(accountInfo.name).isEqualTo(admin.fullName);
+    assertThat(accountInfo.name).isEqualTo(admin.fullName());
   }
 
   @Test
   public void atomicReadMofifyWrite() throws Exception {
-    gApi.accounts().id(admin.id.get()).setStatus("A-1");
+    gApi.accounts().id(admin.id().get()).setStatus("A-1");
 
     AtomicInteger bgCounterA1 = new AtomicInteger(0);
     AtomicInteger bgCounterA2 = new AtomicInteger(0);
@@ -2565,17 +2577,12 @@
         new AccountsUpdate(
             repoManager,
             gitReferenceUpdated,
-            null,
+            Optional.empty(),
             allUsers,
             externalIds,
             metaDataUpdateInternalFactory,
             new RetryHelper(
-                cfg,
-                retryMetrics,
-                null,
-                null,
-                null,
-                r -> r.withBlockStrategy(noSleepBlockStrategy)),
+                cfg, retryMetrics, null, r -> r.withBlockStrategy(noSleepBlockStrategy)),
             extIdNotesFactory,
             ident,
             ident,
@@ -2584,19 +2591,19 @@
               try {
                 accountsUpdateProvider
                     .get()
-                    .update("Set Status", admin.id, u -> u.setStatus("A-2"));
-              } catch (IOException | ConfigInvalidException | OrmException e) {
+                    .update("Set Status", admin.id(), u -> u.setStatus("A-2"));
+              } catch (IOException | ConfigInvalidException | StorageException e) {
                 // Ignore, the expected exception is asserted later
               }
             });
     assertThat(bgCounterA1.get()).isEqualTo(0);
     assertThat(bgCounterA2.get()).isEqualTo(0);
-    assertThat(gApi.accounts().id(admin.id.get()).get().status).isEqualTo("A-1");
+    assertThat(gApi.accounts().id(admin.id().get()).get().status).isEqualTo("A-1");
 
     Optional<AccountState> updatedAccountState =
         update.update(
             "Set Status",
-            admin.id,
+            admin.id(),
             (a, u) -> {
               if ("A-1".equals(a.getAccount().getStatus())) {
                 bgCounterA1.getAndIncrement();
@@ -2614,8 +2621,8 @@
 
     assertThat(updatedAccountState).isPresent();
     assertThat(updatedAccountState.get().getAccount().getStatus()).isEqualTo("B-2");
-    assertThat(accounts.get(admin.id).get().getAccount().getStatus()).isEqualTo("B-2");
-    assertThat(gApi.accounts().id(admin.id.get()).get().status).isEqualTo("B-2");
+    assertThat(accounts.get(admin.id()).get().getAccount().getStatus()).isEqualTo("B-2");
+    assertThat(gApi.accounts().id(admin.id().get()).get().status).isEqualTo("B-2");
   }
 
   @Test
@@ -2636,17 +2643,12 @@
         new AccountsUpdate(
             repoManager,
             gitReferenceUpdated,
-            null,
+            Optional.empty(),
             allUsers,
             externalIds,
             metaDataUpdateInternalFactory,
             new RetryHelper(
-                cfg,
-                retryMetrics,
-                null,
-                null,
-                null,
-                r -> r.withBlockStrategy(noSleepBlockStrategy)),
+                cfg, retryMetrics, null, r -> r.withBlockStrategy(noSleepBlockStrategy)),
             extIdNotesFactory,
             ident,
             ident,
@@ -2659,7 +2661,7 @@
                         "Update External ID",
                         accountId,
                         u -> u.replaceExternalId(extIdA1, extIdA2));
-              } catch (IOException | ConfigInvalidException | OrmException e) {
+              } catch (IOException | ConfigInvalidException | StorageException e) {
                 // Ignore, the expected exception is asserted later
               }
             });
@@ -2786,7 +2788,7 @@
   public void deleteAllDraftComments() throws Exception {
     try {
       TestTimeUtil.resetWithClockStep(1, SECONDS);
-      Project.NameKey project2 = createProject("project2");
+      Project.NameKey project2 = projectOperations.newProject().create();
       PushOneCommit.Result r1 = createChange();
 
       TestRepository<?> tr2 = cloneProject(project2);
@@ -2800,7 +2802,7 @@
               null);
 
       // Create 2 drafts each on both changes for user.
-      setApiUser(user);
+      requestScopeOperations.setApiUser(user.id());
       createDraft(r1, PushOneCommit.FILE_NAME, "draft 1a");
       createDraft(r1, PushOneCommit.FILE_NAME, "draft 1b");
       createDraft(r2, PushOneCommit.FILE_NAME, "draft 2a");
@@ -2809,12 +2811,12 @@
       assertThat(gApi.changes().id(r2.getChangeId()).current().draftsAsList()).hasSize(2);
 
       // Create 1 draft on first change for admin.
-      setApiUser(admin);
+      requestScopeOperations.setApiUser(admin.id());
       createDraft(r1, PushOneCommit.FILE_NAME, "admin draft");
       assertThat(gApi.changes().id(r1.getChangeId()).current().draftsAsList()).hasSize(1);
 
       // Delete user's draft comments; leave admin's alone.
-      setApiUser(user);
+      requestScopeOperations.setApiUser(user.id());
       List<DeletedDraftCommentInfo> result =
           gApi.accounts().self().deleteDraftComments(new DeleteDraftCommentsInput());
 
@@ -2830,7 +2832,7 @@
       assertThat(gApi.changes().id(r1.getChangeId()).current().draftsAsList()).isEmpty();
       assertThat(gApi.changes().id(r2.getChangeId()).current().draftsAsList()).isEmpty();
 
-      setApiUser(admin);
+      requestScopeOperations.setApiUser(admin.id());
       assertThat(gApi.changes().id(r1.getChangeId()).current().draftsAsList()).hasSize(1);
     } finally {
       cleanUpDrafts();
@@ -2867,11 +2869,11 @@
   public void deleteOtherUsersDraftCommentsDisallowed() throws Exception {
     try {
       PushOneCommit.Result r = createChange();
-      setApiUser(user);
+      requestScopeOperations.setApiUser(user.id());
       createDraft(r, PushOneCommit.FILE_NAME, "draft");
-      setApiUser(admin);
+      requestScopeOperations.setApiUser(admin.id());
       try {
-        gApi.accounts().id(user.id.get()).deleteDraftComments(new DeleteDraftCommentsInput());
+        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");
@@ -2888,7 +2890,7 @@
       PushOneCommit.Result r1 = createChange();
       PushOneCommit.Result r2 = createChange("refs/for/secret");
 
-      setApiUser(user);
+      requestScopeOperations.setApiUser(user.id());
       createDraft(r1, PushOneCommit.FILE_NAME, "draft a");
       createDraft(r2, PushOneCommit.FILE_NAME, "draft b");
       assertThat(gApi.changes().id(r1.getChangeId()).current().draftsAsList()).hasSize(1);
@@ -2921,9 +2923,9 @@
 
   @Test
   public void adminCanGenerateNewHttpPasswordForUser() throws Exception {
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     sender.clear();
-    String newPassword = gApi.accounts().id(user.username).generateHttpPassword();
+    String newPassword = gApi.accounts().id(user.username()).generateHttpPassword();
     assertThat(newPassword).isNotNull();
     assertThat(sender.getMessages()).hasSize(1);
     assertThat(sender.getMessages().get(0).body()).contains("HTTP password was added or updated");
@@ -2931,28 +2933,28 @@
 
   @Test
   public void userCannotGenerateNewHttpPasswordForOtherUser() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
-    gApi.accounts().id(admin.username).generateHttpPassword();
+    gApi.accounts().id(admin.username()).generateHttpPassword();
   }
 
   @Test
   public void userCannotExplicitlySetHttpPassword() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     gApi.accounts().self().setHttpPassword("my-new-password");
   }
 
   @Test
   public void userCannotExplicitlySetHttpPasswordForOtherUser() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
-    gApi.accounts().id(admin.username).setHttpPassword("my-new-password");
+    gApi.accounts().id(admin.username()).setHttpPassword("my-new-password");
   }
 
   @Test
   public void userCanRemoveHttpPassword() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     sender.clear();
     assertThat(gApi.accounts().self().setHttpPassword(null)).isNull();
     assertThat(sender.getMessages()).hasSize(1);
@@ -2961,17 +2963,17 @@
 
   @Test
   public void userCannotRemoveHttpPasswordForOtherUser() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
-    gApi.accounts().id(admin.username).setHttpPassword(null);
+    gApi.accounts().id(admin.username()).setHttpPassword(null);
   }
 
   @Test
   public void adminCanExplicitlySetHttpPasswordForUser() throws Exception {
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     String httpPassword = "new-password-for-user";
     sender.clear();
-    assertThat(gApi.accounts().id(user.username).setHttpPassword(httpPassword))
+    assertThat(gApi.accounts().id(user.username()).setHttpPassword(httpPassword))
         .isEqualTo(httpPassword);
     assertThat(sender.getMessages()).hasSize(1);
     assertThat(sender.getMessages().get(0).body()).contains("HTTP password was added or updated");
@@ -2979,17 +2981,17 @@
 
   @Test
   public void adminCanRemoveHttpPasswordForUser() throws Exception {
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     sender.clear();
-    assertThat(gApi.accounts().id(user.username).setHttpPassword(null)).isNull();
+    assertThat(gApi.accounts().id(user.username()).setHttpPassword(null)).isNull();
     assertThat(sender.getMessages()).hasSize(1);
     assertThat(sender.getMessages().get(0).body()).contains("HTTP password was deleted");
   }
 
   @Test
   public void cannotGenerateHttpPasswordWhenUsernameIsNotSet() throws Exception {
-    setApiUser(admin);
-    int userId = accountCreator.create().id.get();
+    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");
@@ -3006,7 +3008,7 @@
 
   private void cleanUpDrafts() throws Exception {
     for (TestAccount testAccount : accountCreator.getAll()) {
-      setApiUser(testAccount);
+      requestScopeOperations.setApiUser(testAccount.id());
       for (ChangeInfo changeInfo : gApi.changes().query("has:draft").get()) {
         for (CommentInfo c :
             gApi.changes().id(changeInfo.id).drafts().values().stream()
@@ -3019,18 +3021,15 @@
   }
 
   private static Correspondence<GroupInfo, String> getGroupToNameCorrespondence() {
-    return new Correspondence<GroupInfo, String>() {
-      @Override
-      public boolean compare(GroupInfo actualGroup, String expectedName) {
-        String groupName = actualGroup == null ? null : actualGroup.name;
-        return Objects.equals(groupName, expectedName);
-      }
-
-      @Override
-      public String toString() {
-        return "has name";
-      }
-    };
+    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);
+          }
+        },
+        "has name");
   }
 
   private void assertSequenceNumbers(List<SshKeyInfo> sshKeys) {
@@ -3129,12 +3128,12 @@
         .get()
         .update(
             "Add Email",
-            account.getId(),
+            account.id(),
             u ->
                 u.addExternalId(
-                    ExternalId.createWithEmail(name("test"), email, account.getId(), email)));
+                    ExternalId.createWithEmail(name("test"), email, account.id(), email)));
     accountIndexedCounter.assertReindexOf(account);
-    setApiUser(account);
+    requestScopeOperations.setApiUser(account.id());
   }
 
   private Map<String, GpgKeyInfo> addGpgKey(String armored) throws Exception {
@@ -3144,14 +3143,14 @@
   private Map<String, GpgKeyInfo> addGpgKey(TestAccount account, String armored) throws Exception {
     Map<String, GpgKeyInfo> gpgKeys =
         gApi.accounts()
-            .id(account.username)
+            .id(account.username())
             .putGpgKeys(ImmutableList.of(armored), ImmutableList.<String>of());
-    accountIndexedCounter.assertReindexOf(gApi.accounts().id(account.username).get());
+    accountIndexedCounter.assertReindexOf(gApi.accounts().id(account.username()).get());
     return gpgKeys;
   }
 
   private Map<String, GpgKeyInfo> addGpgKeyNoReindex(String armored) throws Exception {
-    return gApi.accounts().self().putGpgKeys(ImmutableList.of(armored), ImmutableList.<String>of());
+    return gApi.accounts().self().putGpgKeys(ImmutableList.of(armored), ImmutableList.of());
   }
 
   private void assertUser(AccountInfo info, TestAccount account) throws Exception {
@@ -3160,9 +3159,9 @@
 
   private void assertUser(AccountInfo info, TestAccount account, @Nullable String expectedStatus)
       throws Exception {
-    assertThat(info.name).isEqualTo(account.fullName);
-    assertThat(info.email).isEqualTo(account.email);
-    assertThat(info.username).isEqualTo(account.username);
+    assertThat(info.name).isEqualTo(account.fullName());
+    assertThat(info.email).isEqualTo(account.email());
+    assertThat(info.username).isEqualTo(account.username());
     assertThat(info.status).isEqualTo(expectedStatus);
   }
 
@@ -3172,7 +3171,7 @@
 
   private void assertEmail(Set<Account.Id> accounts, TestAccount expectedAccount) {
     assertThat(accounts).hasSize(1);
-    assertThat(Iterables.getOnlyElement(accounts)).isEqualTo(expectedAccount.getId());
+    assertThat(Iterables.getOnlyElement(accounts)).isEqualTo(expectedAccount.id());
   }
 
   private Config getAccountConfig(TestRepository<?> allUsersRepo) throws Exception {
@@ -3181,7 +3180,7 @@
         TreeWalk.forPath(
             allUsersRepo.getRepository(),
             AccountProperties.ACCOUNT_CONFIG,
-            getHead(allUsersRepo.getRepository()).getTree())) {
+            getHead(allUsersRepo.getRepository(), "HEAD").getTree())) {
       assertThat(tw).isNotNull();
       ac.fromText(
           new String(
@@ -3221,7 +3220,7 @@
     }
 
     void assertReindexOf(TestAccount testAccount, int expectedCount) {
-      assertThat(getCount(testAccount.id)).isEqualTo(expectedCount);
+      assertThat(getCount(testAccount.id())).isEqualTo(expectedCount);
       assertThat(countsByAccount).hasSize(1);
       clear();
     }
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
index b5bab32..a277b88 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
@@ -35,6 +34,7 @@
 import com.google.gerrit.server.account.externalids.ExternalIdNotes;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.notedb.Sequences;
 import com.google.inject.Inject;
 import java.util.Optional;
 import java.util.Set;
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
index 7a4a901..a4a5745 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
@@ -16,13 +16,23 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
+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.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.extensions.api.changes.CherryPickInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
+import com.google.gerrit.extensions.api.groups.GroupApi;
 import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.client.InheritableBoolean;
@@ -34,8 +44,14 @@
 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;
@@ -46,6 +62,47 @@
 public class AgreementsIT extends AbstractDaemonTest {
   private ContributorAgreement caAutoVerify;
   private ContributorAgreement caNoAutoVerify;
+  @Inject private GroupOperations groupOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
+
+  protected void setUseContributorAgreements(InheritableBoolean value) throws Exception {
+    try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
+      ProjectConfig config = projectConfigFactory.read(md);
+      config.getProject().setBooleanConfig(BooleanProjectConfig.USE_CONTRIBUTOR_AGREEMENTS, value);
+      config.commit(md);
+      projectCache.evict(config.getProject());
+    }
+  }
+
+  protected ContributorAgreement configureContributorAgreement(boolean autoVerify)
+      throws Exception {
+    ContributorAgreement ca;
+    String name = autoVerify ? "cla-test-group" : "cla-test-no-auto-verify-group";
+    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));
+    GroupReference groupRef = new GroupReference(caGroup.getGroupUUID(), caGroup.getName());
+    PermissionRule rule = new PermissionRule(groupRef);
+    rule.setAction(PermissionRule.Action.ALLOW);
+    if (autoVerify) {
+      ca = new ContributorAgreement("cla-test");
+      ca.setAutoVerify(groupRef);
+      ca.setAccepted(ImmutableList.of(rule));
+    } else {
+      ca = new ContributorAgreement("cla-test-no-auto-verify");
+    }
+    ca.setDescription("description");
+    ca.setAgreementUrl("agreement-url");
+    ca.setAccepted(ImmutableList.of(rule));
+    ca.setExcludeProjectsRegexes(ImmutableList.of("ExcludedProject"));
+
+    try (ProjectConfigUpdate u = updateProject(allProjects)) {
+      u.getConfig().replace(ca);
+      u.save();
+      return ca;
+    }
+  }
 
   @ConfigSuite.Config
   public static Config enableAgreementsConfig() {
@@ -68,7 +125,7 @@
   public void setUp() throws Exception {
     caAutoVerify = configureContributorAgreement(true);
     caNoAutoVerify = configureContributorAgreement(false);
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
   }
 
   @Test
@@ -113,7 +170,7 @@
     gApi.accounts().self().signAgreement(caAutoVerify.getName());
 
     // Explicitly reset the user to force a new request context
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
 
     // Verify that the agreement was signed
     result = gApi.accounts().self().listAgreements();
@@ -138,7 +195,7 @@
 
   @Test
   public void signAgreementAnonymous() throws Exception {
-    setApiUserAnonymous();
+    requestScopeOperations.setApiUserAnonymous();
     exception.expect(AuthException.class);
     exception.expectMessage("Authentication required");
     gApi.accounts().self().signAgreement(caAutoVerify.getName());
@@ -169,12 +226,12 @@
     ChangeInfo change = gApi.changes().create(newChangeInput()).get();
 
     // Approve and submit it
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     gApi.changes().id(change.changeId).current().review(ReviewInput.approve());
     gApi.changes().id(change.changeId).current().submit(new SubmitInput());
 
     // Revert is not allowed when CLA is required but not signed
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     setUseContributorAgreements(InheritableBoolean.TRUE);
     exception.expect(AuthException.class);
     exception.expectMessage("Contributor Agreement");
@@ -182,11 +239,33 @@
   }
 
   @Test
+  public void revertExcludedProjectChangeWithoutCLA() throws Exception {
+    // Contributor agreements configured with excludeProjects = ExcludedProject
+    // in AbstractDaemonTest.configureContributorAgreement(...)
+    assume().that(isContributorAgreementsEnabled()).isTrue();
+
+    // Create a change succeeds when agreement is not required
+    setUseContributorAgreements(InheritableBoolean.FALSE);
+    // Project name includes test method name which contains ExcludedProject
+    ChangeInfo change = gApi.changes().create(newChangeInput()).get();
+
+    // Approve and submit it
+    requestScopeOperations.setApiUser(admin.id());
+    gApi.changes().id(change.changeId).current().review(ReviewInput.approve());
+    gApi.changes().id(change.changeId).current().submit(new SubmitInput());
+
+    // Revert in excluded project is allowed even when CLA is required but not signed
+    requestScopeOperations.setApiUser(user.id());
+    setUseContributorAgreements(InheritableBoolean.TRUE);
+    gApi.changes().id(change.changeId).revert();
+  }
+
+  @Test
   public void cherrypickChangeWithoutCLA() throws Exception {
     assume().that(isContributorAgreementsEnabled()).isTrue();
 
     // Create a new branch
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     BranchInfo dest =
         gApi.projects()
             .name(project.get())
@@ -203,7 +282,7 @@
     gApi.changes().id(change.changeId).current().submit(new SubmitInput());
 
     // Cherry-pick is not allowed when CLA is required but not signed
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     setUseContributorAgreements(InheritableBoolean.TRUE);
     CherryPickInput in = new CherryPickInput();
     in.destination = dest.ref;
@@ -234,12 +313,23 @@
     gApi.accounts().self().signAgreement(caAutoVerify.getName());
 
     // Explicitly reset the user to force a new request context
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
 
     // Create a change succeeds after signing the agreement
     gApi.changes().create(newChangeInput());
   }
 
+  @Test
+  public void createExcludedProjectChangeIgnoresCLA() throws Exception {
+    // Contributor agreements configured with excludeProjects = ExcludedProject
+    // in AbstractDaemonTest.configureContributorAgreement(...)
+    assume().that(isContributorAgreementsEnabled()).isTrue();
+
+    // Create a change in excluded project is allowed even when CLA is required but not signed.
+    setUseContributorAgreements(InheritableBoolean.TRUE);
+    gApi.changes().create(newChangeInput());
+  }
+
   private void assertAgreement(AgreementInfo info, ContributorAgreement ca) {
     assertThat(info.name).isEqualTo(ca.getName());
     assertThat(info.description).isEqualTo(ca.getDescription());
@@ -258,4 +348,33 @@
     in.project = project.get();
     return in;
   }
+
+  @Test
+  @GerritConfig(name = "auth.contributorAgreements", value = "true")
+  public void anonymousAccessServerInfoEvenWithCLAs() throws Exception {
+    requestScopeOperations.setApiUserAnonymous();
+    gApi.config().server().getInfo();
+  }
+
+  @Test
+  public void publishEditRestWithoutCLA() throws Exception {
+    String filename = "foo";
+    PushOneCommit push =
+        pushFactory.create(admin.newIdent(), testRepo, "subject1", filename, "contentold");
+    PushOneCommit.Result result = push.to("refs/for/master");
+    result.assertOkStatus();
+    String changeId = result.getChangeId();
+
+    gApi.changes().id(changeId).edit().create();
+    gApi.changes()
+        .id(changeId)
+        .edit()
+        .modifyFile(filename, RawInputUtil.create("newcontent".getBytes(UTF_8)));
+
+    String url = "/changes/" + changeId + "/edit:publish";
+    setUseContributorAgreements(InheritableBoolean.TRUE);
+    userRestSession.post(url).assertForbidden();
+    setUseContributorAgreements(InheritableBoolean.FALSE);
+    userRestSession.post(url).assertNoContent();
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/BUILD b/javatests/com/google/gerrit/acceptance/api/accounts/BUILD
index 31f3f91..2167d27 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/BUILD
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/BUILD
@@ -9,6 +9,7 @@
         "no_windows",
     ],
     deps = [
+        "//java/com/google/gerrit/git",
         "//java/com/google/gerrit/mail",
         "//java/com/google/gerrit/server/util/time",
     ],
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java
index ba340eb..a442ddd 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
-import com.google.gerrit.extensions.client.Theme;
 import org.junit.Test;
 
 @NoHttpd
@@ -29,7 +28,7 @@
   @Test
   public void getDiffPreferences() throws Exception {
     DiffPreferencesInfo d = DiffPreferencesInfo.defaults();
-    DiffPreferencesInfo o = gApi.accounts().id(admin.getId().toString()).getDiffPreferences();
+    DiffPreferencesInfo o = gApi.accounts().id(admin.id().toString()).getDiffPreferences();
     assertPrefs(o, d);
   }
 
@@ -43,7 +42,6 @@
     i.fontSize *= -1;
     i.lineLength *= -1;
     i.cursorBlinkRate = 500;
-    i.theme = Theme.MIDNIGHT;
     i.ignoreWhitespace = Whitespace.IGNORE_ALL;
     i.expandAllComments ^= true;
     i.intralineDifference ^= true;
@@ -64,13 +62,13 @@
     i.matchBrackets ^= true;
     i.lineWrapping ^= true;
 
-    DiffPreferencesInfo o = gApi.accounts().id(admin.getId().toString()).setDiffPreferences(i);
+    DiffPreferencesInfo o = gApi.accounts().id(admin.id().toString()).setDiffPreferences(i);
     assertPrefs(o, i);
 
     // Partially fill input record
     i = new DiffPreferencesInfo();
     i.tabSize = 42;
-    DiffPreferencesInfo a = gApi.accounts().id(admin.getId().toString()).setDiffPreferences(i);
+    DiffPreferencesInfo a = gApi.accounts().id(admin.id().toString()).setDiffPreferences(i);
     assertPrefs(a, o, "tabSize");
     assertThat(a.tabSize).isEqualTo(42);
   }
@@ -87,7 +85,7 @@
     update.fontSize = newFontSize;
     gApi.config().server().setDefaultDiffPreferences(update);
 
-    DiffPreferencesInfo o = gApi.accounts().id(admin.getId().toString()).getDiffPreferences();
+    DiffPreferencesInfo o = gApi.accounts().id(admin.id().toString()).getDiffPreferences();
 
     // assert configured defaults
     assertThat(o.lineLength).isEqualTo(newLineLength);
@@ -106,29 +104,29 @@
     update.lineLength = configuredDefaultLineLength;
     gApi.config().server().setDefaultDiffPreferences(update);
 
-    DiffPreferencesInfo o = gApi.accounts().id(admin.getId().toString()).getDiffPreferences();
+    DiffPreferencesInfo o = gApi.accounts().id(admin.id().toString()).getDiffPreferences();
     assertThat(o.lineLength).isEqualTo(configuredDefaultLineLength);
     assertPrefs(o, d, "lineLength");
 
     int newLineLength = configuredDefaultLineLength + 10;
     DiffPreferencesInfo i = new DiffPreferencesInfo();
     i.lineLength = newLineLength;
-    DiffPreferencesInfo a = gApi.accounts().id(admin.getId().toString()).setDiffPreferences(i);
+    DiffPreferencesInfo a = gApi.accounts().id(admin.id().toString()).setDiffPreferences(i);
     assertThat(a.lineLength).isEqualTo(newLineLength);
     assertPrefs(a, d, "lineLength");
 
-    a = gApi.accounts().id(admin.getId().toString()).getDiffPreferences();
+    a = gApi.accounts().id(admin.id().toString()).getDiffPreferences();
     assertThat(a.lineLength).isEqualTo(newLineLength);
     assertPrefs(a, d, "lineLength");
 
     // overwrite the configured default with original hard-coded default
     i = new DiffPreferencesInfo();
     i.lineLength = d.lineLength;
-    a = gApi.accounts().id(admin.getId().toString()).setDiffPreferences(i);
+    a = gApi.accounts().id(admin.id().toString()).setDiffPreferences(i);
     assertThat(a.lineLength).isEqualTo(d.lineLength);
     assertPrefs(a, d, "lineLength");
 
-    a = gApi.accounts().id(admin.getId().toString()).getDiffPreferences();
+    a = gApi.accounts().id(admin.id().toString()).getDiffPreferences();
     assertThat(a.lineLength).isEqualTo(d.lineLength);
     assertPrefs(a, d, "lineLength");
   }
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/EditPreferencesIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/EditPreferencesIT.java
index c1d9bcb..f142c08 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/EditPreferencesIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/EditPreferencesIT.java
@@ -19,15 +19,13 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.extensions.client.EditPreferencesInfo;
-import com.google.gerrit.extensions.client.KeyMapType;
-import com.google.gerrit.extensions.client.Theme;
 import org.junit.Test;
 
 @NoHttpd
 public class EditPreferencesIT extends AbstractDaemonTest {
   @Test
   public void getSetEditPreferences() throws Exception {
-    EditPreferencesInfo out = gApi.accounts().id(admin.getId().toString()).getEditPreferences();
+    EditPreferencesInfo out = gApi.accounts().id(admin.id().toString()).getEditPreferences();
 
     assertThat(out.lineLength).isEqualTo(100);
     assertThat(out.indentUnit).isEqualTo(2);
@@ -43,8 +41,6 @@
     assertThat(out.indentWithTabs).isNull();
     assertThat(out.autoCloseBrackets).isNull();
     assertThat(out.showBase).isNull();
-    assertThat(out.theme).isEqualTo(Theme.DEFAULT);
-    assertThat(out.keyMapType).isEqualTo(KeyMapType.DEFAULT);
 
     // change some default values
     out.lineLength = 80;
@@ -61,10 +57,8 @@
     out.indentWithTabs = true;
     out.autoCloseBrackets = true;
     out.showBase = true;
-    out.theme = Theme.TWILIGHT;
-    out.keyMapType = KeyMapType.EMACS;
 
-    EditPreferencesInfo info = gApi.accounts().id(admin.getId().toString()).setEditPreferences(out);
+    EditPreferencesInfo info = gApi.accounts().id(admin.id().toString()).setEditPreferences(out);
 
     assertEditPreferences(info, out);
 
@@ -72,7 +66,7 @@
     EditPreferencesInfo in = new EditPreferencesInfo();
     in.tabSize = 42;
 
-    info = gApi.accounts().id(admin.getId().toString()).setEditPreferences(in);
+    info = gApi.accounts().id(admin.id().toString()).setEditPreferences(in);
 
     out.tabSize = in.tabSize;
     assertEditPreferences(info, out);
@@ -94,7 +88,5 @@
     assertThat(out.indentWithTabs).isEqualTo(in.indentWithTabs);
     assertThat(out.autoCloseBrackets).isEqualTo(in.autoCloseBrackets);
     assertThat(out.showBase).isEqualTo(in.showBase);
-    assertThat(out.theme).isEqualTo(in.theme);
-    assertThat(out.keyMapType).isEqualTo(in.keyMapType);
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
index 24040a4..c905d3f 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
@@ -24,7 +24,6 @@
 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.DownloadCommand;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailFormat;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.ReviewCategoryStrategy;
@@ -38,7 +37,6 @@
 import com.google.inject.Inject;
 import com.google.inject.util.Providers;
 import java.util.ArrayList;
-import java.util.HashMap;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -56,7 +54,7 @@
 
   @Test
   public void getAndSetPreferences() throws Exception {
-    GeneralPreferencesInfo o = gApi.accounts().id(user42.id.toString()).getPreferences();
+    GeneralPreferencesInfo o = gApi.accounts().id(user42.id().toString()).getPreferences();
     assertPrefs(o, GeneralPreferencesInfo.defaults(), "my", "changeTable");
     assertThat(o.my)
         .containsExactly(
@@ -74,7 +72,6 @@
     i.changesPerPage *= -1;
     i.showSiteHeader ^= true;
     i.useFlashClipboard ^= true;
-    i.downloadCommand = DownloadCommand.REPO_DOWNLOAD;
     i.dateFormat = DateFormat.US;
     i.timeFormat = TimeFormat.HHMM_24;
     i.emailStrategy = EmailStrategy.DISABLED;
@@ -93,10 +90,8 @@
     i.my.add(new MenuItem("name", "url"));
     i.changeTable = new ArrayList<>();
     i.changeTable.add("Status");
-    i.urlAliases = new HashMap<>();
-    i.urlAliases.put("foo", "bar");
 
-    o = gApi.accounts().id(user42.getId().toString()).setPreferences(i);
+    o = gApi.accounts().id(user42.id().toString()).setPreferences(i);
     assertPrefs(o, i, "my");
     assertThat(o.my).containsExactlyElementsIn(i.my);
     assertThat(o.changeTable).containsExactlyElementsIn(i.changeTable);
@@ -110,7 +105,7 @@
     update.changesPerPage = newChangesPerPage;
     gApi.config().server().setDefaultPreferences(update);
 
-    GeneralPreferencesInfo o = gApi.accounts().id(user42.getId().toString()).getPreferences();
+    GeneralPreferencesInfo o = gApi.accounts().id(user42.id().toString()).getPreferences();
 
     // assert configured defaults
     assertThat(o.changesPerPage).isEqualTo(newChangesPerPage);
@@ -127,29 +122,29 @@
     update.changesPerPage = configuredChangesPerPage;
     gApi.config().server().setDefaultPreferences(update);
 
-    GeneralPreferencesInfo o = gApi.accounts().id(admin.getId().toString()).getPreferences();
+    GeneralPreferencesInfo o = gApi.accounts().id(admin.id().toString()).getPreferences();
     assertThat(o.changesPerPage).isEqualTo(configuredChangesPerPage);
     assertPrefs(o, d, "my", "changeTable", "changesPerPage");
 
     int newChangesPerPage = configuredChangesPerPage * 2;
     GeneralPreferencesInfo i = new GeneralPreferencesInfo();
     i.changesPerPage = newChangesPerPage;
-    GeneralPreferencesInfo a = gApi.accounts().id(admin.getId().toString()).setPreferences(i);
+    GeneralPreferencesInfo a = gApi.accounts().id(admin.id().toString()).setPreferences(i);
     assertThat(a.changesPerPage).isEqualTo(newChangesPerPage);
     assertPrefs(a, d, "my", "changeTable", "changesPerPage");
 
-    a = gApi.accounts().id(admin.getId().toString()).getPreferences();
+    a = gApi.accounts().id(admin.id().toString()).getPreferences();
     assertThat(a.changesPerPage).isEqualTo(newChangesPerPage);
     assertPrefs(a, d, "my", "changeTable", "changesPerPage");
 
     // overwrite the configured default with original hard-coded default
     i = new GeneralPreferencesInfo();
     i.changesPerPage = d.changesPerPage;
-    a = gApi.accounts().id(admin.getId().toString()).setPreferences(i);
+    a = gApi.accounts().id(admin.id().toString()).setPreferences(i);
     assertThat(a.changesPerPage).isEqualTo(d.changesPerPage);
     assertPrefs(a, d, "my", "changeTable", "changesPerPage");
 
-    a = gApi.accounts().id(admin.getId().toString()).getPreferences();
+    a = gApi.accounts().id(admin.id().toString()).getPreferences();
     assertThat(a.changesPerPage).isEqualTo(d.changesPerPage);
     assertPrefs(a, d, "my", "changeTable", "changesPerPage");
   }
@@ -162,7 +157,7 @@
 
     exception.expect(BadRequestException.class);
     exception.expectMessage("name for menu item is required");
-    gApi.accounts().id(user42.getId().toString()).setPreferences(i);
+    gApi.accounts().id(user42.id().toString()).setPreferences(i);
   }
 
   @Test
@@ -173,7 +168,7 @@
 
     exception.expect(BadRequestException.class);
     exception.expectMessage("URL for menu item is required");
-    gApi.accounts().id(user42.getId().toString()).setPreferences(i);
+    gApi.accounts().id(user42.id().toString()).setPreferences(i);
   }
 
   @Test
@@ -182,7 +177,7 @@
     i.my = new ArrayList<>();
     i.my.add(new MenuItem(" name\t", " url\t", " _blank\t", " id\t"));
 
-    GeneralPreferencesInfo o = gApi.accounts().id(user42.getId().toString()).setPreferences(i);
+    GeneralPreferencesInfo o = gApi.accounts().id(user42.id().toString()).setPreferences(i);
     assertThat(o.my).containsExactly(new MenuItem("name", "url", "_blank", "id"));
   }
 
@@ -193,7 +188,7 @@
 
     exception.expect(BadRequestException.class);
     exception.expectMessage("Unsupported download scheme: " + i.downloadScheme);
-    gApi.accounts().id(user42.getId().toString()).setPreferences(i);
+    gApi.accounts().id(user42.id().toString()).setPreferences(i);
   }
 
   @Test
@@ -206,10 +201,10 @@
       GeneralPreferencesInfo i = GeneralPreferencesInfo.defaults();
       i.downloadScheme = schemeName;
 
-      GeneralPreferencesInfo o = gApi.accounts().id(user42.getId().toString()).setPreferences(i);
+      GeneralPreferencesInfo o = gApi.accounts().id(user42.id().toString()).setPreferences(i);
       assertThat(o.downloadScheme).isEqualTo(schemeName);
 
-      o = gApi.accounts().id(user42.getId().toString()).getPreferences();
+      o = gApi.accounts().id(user42.id().toString()).getPreferences();
       assertThat(o.downloadScheme).isEqualTo(schemeName);
     } finally {
       registrationHandle.remove();
@@ -222,7 +217,7 @@
     // becomes unsupported.
     setDownloadScheme();
 
-    GeneralPreferencesInfo o = gApi.accounts().id(user42.getId().toString()).getPreferences();
+    GeneralPreferencesInfo o = gApi.accounts().id(user42.id().toString()).getPreferences();
     assertThat(o.downloadScheme).isNull();
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java b/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java
index b626ab3..925c66a 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
 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.client.ChangeStatus;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -45,6 +46,7 @@
 
 public class AbandonIT extends AbstractDaemonTest {
   @Inject private AbandonUtil abandonUtil;
+  @Inject private RequestScopeOperations requestScopeOperations;
   @Inject private ChangeCleanupConfig cleanupConfig;
 
   @Test
@@ -154,7 +156,7 @@
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
     assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW);
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("abandon not permitted");
     gApi.changes().id(changeId).abandon();
@@ -166,7 +168,7 @@
     String changeId = r.getChangeId();
     assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW);
     grant(project, "refs/heads/master", Permission.ABANDON, false, REGISTERED_USERS);
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(changeId).abandon();
     assertThat(info(changeId).status).isEqualTo(ChangeStatus.ABANDONED);
     gApi.changes().id(changeId).restore();
@@ -197,7 +199,7 @@
     String changeId = r.getChangeId();
     assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW);
     gApi.changes().id(changeId).abandon();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     assertThat(info(changeId).status).isEqualTo(ChangeStatus.ABANDONED);
     exception.expect(AuthException.class);
     exception.expectMessage("restore not permitted");
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index e6dfa1d..fe97688 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -16,7 +16,6 @@
 
 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.assertPushOk;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
 import static com.google.gerrit.acceptance.PushOneCommit.FILE_CONTENT;
@@ -40,7 +39,6 @@
 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.reviewdb.server.ReviewDbUtil.unwrapDb;
 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;
@@ -59,8 +57,8 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
+import com.google.common.truth.ThrowableSubject;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
 import com.google.gerrit.acceptance.ChangeIndexedCounter;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.GitUtil;
@@ -68,10 +66,15 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestProjectInput;
 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.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.extensions.annotations.Exports;
 import com.google.gerrit.extensions.api.accounts.DeleteDraftCommentsInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerResult;
@@ -83,10 +86,12 @@
 import com.google.gerrit.extensions.api.changes.NotifyInfo;
 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;
+import com.google.gerrit.extensions.api.changes.ReviewerInfo;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
 import com.google.gerrit.extensions.api.changes.StarsInput;
 import com.google.gerrit.extensions.api.groups.GroupApi;
@@ -127,14 +132,14 @@
 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.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.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.ChangeMessagesUtil;
@@ -142,8 +147,12 @@
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.git.ChangeMessageModifier;
 import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
+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.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder.ChangeOperatorFactory;
 import com.google.gerrit.server.restapi.change.PostReview;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
@@ -151,17 +160,18 @@
 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 java.io.IOException;
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import java.util.function.Function;
@@ -182,11 +192,14 @@
 public class ChangeIT extends AbstractDaemonTest {
   private String systemTimeZone;
 
-  @Inject private DynamicSet<ChangeMessageModifier> changeMessageModifiers;
-
-  @Inject private DynamicSet<ChangeIndexedListener> changeIndexedListeners;
-
   @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;
 
   private ChangeIndexedCounter changeIndexedCounter;
   private RegistrationHandle changeIndexedCounterHandle;
@@ -231,7 +244,7 @@
     assertThat(c.created).isEqualTo(c.updated);
     assertThat(c._number).isEqualTo(r.getChange().getId().get());
 
-    assertThat(c.owner._accountId).isEqualTo(admin.getId().get());
+    assertThat(c.owner._accountId).isEqualTo(admin.id().get());
     assertThat(c.owner.name).isNull();
     assertThat(c.owner.email).isNull();
     assertThat(c.owner.username).isNull();
@@ -251,154 +264,11 @@
   }
 
   @Test
-  public void setPrivateByOwner() throws Exception {
-    TestRepository<InMemoryRepository> userRepo = cloneProject(project, user);
-    PushOneCommit.Result result =
-        pushFactory.create(db, user.getIdent(), userRepo).to("refs/for/master");
-
-    setApiUser(user);
-    String changeId = result.getChangeId();
-    assertThat(gApi.changes().id(changeId).get().isPrivate).isNull();
-
-    gApi.changes().id(changeId).setPrivate(true, null);
-    ChangeInfo info = gApi.changes().id(changeId).get();
-    assertThat(info.isPrivate).isTrue();
-    assertThat(Iterables.getLast(info.messages).message).isEqualTo("Set private");
-    assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_PRIVATE);
-
-    gApi.changes().id(changeId).setPrivate(false, null);
-    info = gApi.changes().id(changeId).get();
-    assertThat(info.isPrivate).isNull();
-    assertThat(Iterables.getLast(info.messages).message).isEqualTo("Unset private");
-    assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_UNSET_PRIVATE);
-
-    String msg = "This is a security fix that must not be public.";
-    gApi.changes().id(changeId).setPrivate(true, msg);
-    info = gApi.changes().id(changeId).get();
-    assertThat(info.isPrivate).isTrue();
-    assertThat(Iterables.getLast(info.messages).message).isEqualTo("Set private\n\n" + msg);
-    assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_PRIVATE);
-
-    msg = "After this security fix has been released we can make it public now.";
-    gApi.changes().id(changeId).setPrivate(false, msg);
-    info = gApi.changes().id(changeId).get();
-    assertThat(info.isPrivate).isNull();
-    assertThat(Iterables.getLast(info.messages).message).isEqualTo("Unset private\n\n" + msg);
-    assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_UNSET_PRIVATE);
-  }
-
-  @Test
-  public void administratorCanSetUserChangePrivate() throws Exception {
-    String changeId = createNewChange();
-    assertThat(gApi.changes().id(changeId).get().isPrivate).isNull();
-
-    gApi.changes().id(changeId).setPrivate(true, null);
-    setApiUser(user);
-    ChangeInfo info = gApi.changes().id(changeId).get();
-    assertThat(info.isPrivate).isTrue();
-  }
-
-  @Test
-  public void cannotSetOtherUsersChangePrivate() throws Exception {
-    PushOneCommit.Result result = createChange();
-    setApiUser(user);
-    exception.expect(AuthException.class);
-    exception.expectMessage("not allowed to mark private");
-    gApi.changes().id(result.getChangeId()).setPrivate(true, null);
-  }
-
-  @Test
-  public void accessPrivate() throws Exception {
-    TestRepository<InMemoryRepository> userRepo = cloneProject(project, user);
-    PushOneCommit.Result result =
-        pushFactory.create(db, user.getIdent(), userRepo).to("refs/for/master");
-
-    setApiUser(user);
-    gApi.changes().id(result.getChangeId()).setPrivate(true, null);
-    // Owner can always access its private changes.
-    assertThat(gApi.changes().id(result.getChangeId()).get().isPrivate).isTrue();
-
-    // Add admin as a reviewer.
-    gApi.changes().id(result.getChangeId()).addReviewer(admin.getId().toString());
-
-    // This change should be visible for admin as a reviewer.
-    setApiUser(admin);
-    assertThat(gApi.changes().id(result.getChangeId()).get().isPrivate).isTrue();
-
-    // Remove admin from reviewers.
-    gApi.changes().id(result.getChangeId()).reviewer(admin.getId().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());
-  }
-
-  @Test
-  public void privateChangeOfOtherUserCanBeAccessedWithPermission() throws Exception {
-    PushOneCommit.Result result = createChange();
-    gApi.changes().id(result.getChangeId()).setPrivate(true, null);
-
-    allow("refs/*", Permission.VIEW_PRIVATE_CHANGES, REGISTERED_USERS);
-    setApiUser(user);
-    assertThat(gApi.changes().id(result.getChangeId()).get().isPrivate).isTrue();
-  }
-
-  @Test
-  public void administratorCanUnmarkPrivateAfterMerging() throws Exception {
-    PushOneCommit.Result result = createChange();
-    String changeId = result.getChangeId();
-    gApi.changes().id(changeId).setPrivate(true, null);
-    assertThat(gApi.changes().id(changeId).get().isPrivate).isTrue();
-    merge(result);
-    gApi.changes().id(changeId).setPrivate(false, null);
-    assertThat(gApi.changes().id(changeId).get().isPrivate).isNull();
-  }
-
-  @Test
-  public void administratorCanMarkPrivateAfterMerging() throws Exception {
-    PushOneCommit.Result result = createChange();
-    String changeId = result.getChangeId();
-    assertThat(gApi.changes().id(changeId).get().isPrivate).isNull();
-    merge(result);
-    gApi.changes().id(changeId).setPrivate(true, null);
-    assertThat(gApi.changes().id(changeId).get().isPrivate).isTrue();
-  }
-
-  @Test
-  public void ownerCannotMarkPrivateAfterMerging() throws Exception {
-    TestRepository<InMemoryRepository> userRepo = cloneProject(project, user);
-    PushOneCommit.Result result =
-        pushFactory.create(db, user.getIdent(), userRepo).to("refs/for/master");
-
-    String changeId = result.getChangeId();
-    assertThat(gApi.changes().id(changeId).get().isPrivate).isNull();
-
-    merge(result);
-
-    setApiUser(user);
-    exception.expect(AuthException.class);
-    exception.expectMessage("not allowed to mark private");
-    gApi.changes().id(changeId).setPrivate(true, null);
-  }
-
-  @Test
-  public void ownerCanUnmarkPrivateAfterMerging() throws Exception {
-    TestRepository<InMemoryRepository> userRepo = cloneProject(project, user);
-    PushOneCommit.Result result =
-        pushFactory.create(db, user.getIdent(), userRepo).to("refs/for/master");
-
-    String changeId = result.getChangeId();
-    assertThat(gApi.changes().id(changeId).get().isPrivate).isNull();
-    gApi.changes().id(changeId).addReviewer(admin.getId().toString());
-    gApi.changes().id(changeId).setPrivate(true, null);
-    assertThat(gApi.changes().id(changeId).get().isPrivate).isTrue();
-
-    merge(result);
-
-    setApiUser(user);
-    gApi.changes().id(changeId).setPrivate(false, null);
-    assertThat(gApi.changes().id(changeId).get().isPrivate).isNull();
+  @GerritConfig(name = "change.api.excludeMergeableInChangeInfo", value = "true")
+  public void excludeMergeableInChangeInfo() throws Exception {
+    PushOneCommit.Result r = createChange();
+    ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
+    assertThat(c.mergeable).isNull();
   }
 
   @Test
@@ -406,32 +276,32 @@
     PushOneCommit.Result rwip = createChange();
     String changeId = rwip.getChangeId();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
-    exception.expectMessage("not allowed to toggle work in progress");
+    exception.expectMessage("toggle work in progress state not permitted");
     gApi.changes().id(changeId).setWorkInProgress();
   }
 
   @Test
   public void setWorkInProgressAllowedAsAdmin() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     String changeId =
         gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get().id;
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     gApi.changes().id(changeId).setWorkInProgress();
     assertThat(gApi.changes().id(changeId).get().workInProgress).isTrue();
   }
 
   @Test
   public void setWorkInProgressAllowedAsProjectOwner() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     String changeId =
         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);
-    setApiUser(user2);
+    requestScopeOperations.setApiUser(user2.id());
     gApi.changes().id(changeId).setWorkInProgress();
     assertThat(gApi.changes().id(changeId).get().workInProgress).isTrue();
   }
@@ -452,34 +322,34 @@
     String changeId = rready.getChangeId();
     gApi.changes().id(changeId).setWorkInProgress();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
-    exception.expectMessage("not allowed to toggle work in progress");
+    exception.expectMessage("toggle work in progress state not permitted");
     gApi.changes().id(changeId).setReadyForReview();
   }
 
   @Test
   public void setReadyForReviewAllowedAsAdmin() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     String changeId =
         gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get().id;
     gApi.changes().id(changeId).setWorkInProgress();
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     gApi.changes().id(changeId).setReadyForReview();
     assertThat(gApi.changes().id(changeId).get().workInProgress).isNull();
   }
 
   @Test
   public void setReadyForReviewAllowedAsProjectOwner() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     String changeId =
         gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get().id;
     gApi.changes().id(changeId).setWorkInProgress();
 
     com.google.gerrit.acceptance.TestAccount user2 = accountCreator.user2();
     grant(project, "refs/*", Permission.OWNER, false, REGISTERED_USERS);
-    setApiUser(user2);
+    requestScopeOperations.setApiUser(user2.id());
     gApi.changes().id(changeId).setReadyForReview();
     assertThat(gApi.changes().id(changeId).get().workInProgress).isNull();
   }
@@ -498,8 +368,6 @@
 
   @Test
   public void pendingReviewersInNoteDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     ConfigInput conf = new ConfigInput();
     conf.enableReviewerByEmail = InheritableBoolean.TRUE;
     gApi.projects().name(project.get()).config(conf);
@@ -554,7 +422,7 @@
         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");
+            admin.email(), 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();
@@ -566,7 +434,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(admin.email(), email2, "byemail2@example.com");
     assertThat(toEmails.apply(info.pendingReviewers.get(CC)))
         .containsExactly(email4, "byemail4@example.com");
     assertThat(toEmails.apply(info.pendingReviewers.get(REMOVED)))
@@ -577,7 +445,7 @@
     gApi.changes().id(changeId).revision("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(admin.email(), email1, email2, "byemail2@example.com");
     assertThat(toEmails.apply(info.pendingReviewers.get(CC)))
         .containsExactly(email4, "byemail4@example.com");
     assertThat(toEmails.apply(info.pendingReviewers.get(REMOVED)))
@@ -588,7 +456,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(admin.email(), email1, email2, "byemail2@example.com");
     assertThat(toEmails.apply(info.reviewers.get(CC)))
         .containsExactly(email4, "byemail4@example.com");
     assertThat(info.reviewers.get(REMOVED)).isNull();
@@ -633,6 +501,37 @@
   }
 
   @Test
+  public void toggleWorkInProgressStateByNonOwnerWithPermission() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String refactor = "Needs some refactoring";
+    String ptal = "PTAL";
+
+    grant(
+        project,
+        "refs/heads/master",
+        Permission.TOGGLE_WORK_IN_PROGRESS_STATE,
+        false,
+        REGISTERED_USERS);
+
+    requestScopeOperations.setApiUser(user.id());
+    gApi.changes().id(changeId).setWorkInProgress(refactor);
+
+    ChangeInfo info = gApi.changes().id(changeId).get();
+
+    assertThat(info.workInProgress).isTrue();
+    assertThat(Iterables.getLast(info.messages).message).contains(refactor);
+    assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_WIP);
+
+    gApi.changes().id(changeId).setReadyForReview(ptal);
+
+    info = gApi.changes().id(changeId).get();
+    assertThat(info.workInProgress).isNull();
+    assertThat(Iterables.getLast(info.messages).message).contains(ptal);
+    assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_READY);
+  }
+
+  @Test
   public void reviewAndStartReview() throws Exception {
     PushOneCommit.Result r = createWorkInProgressChange();
     r.assertOkStatus();
@@ -665,14 +564,17 @@
     assertThat(r.getChange().change().isWorkInProgress()).isFalse();
 
     ReviewInput in =
-        ReviewInput.approve().reviewer(user.email).label("Code-Review", 1).setWorkInProgress(true);
+        ReviewInput.approve()
+            .reviewer(user.email())
+            .label("Code-Review", 1)
+            .setWorkInProgress(true);
     gApi.changes().id(r.getChangeId()).revision("current").review(in);
 
     ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
     assertThat(info.workInProgress).isTrue();
     assertThat(info.reviewers.get(REVIEWER).stream().map(ai -> ai._accountId).collect(toList()))
-        .containsExactly(admin.id.get(), user.id.get());
-    assertThat(info.labels.get("Code-Review").recommended._accountId).isEqualTo(admin.id.get());
+        .containsExactly(admin.id().get(), user.id().get());
+    assertThat(info.labels.get("Code-Review").recommended._accountId).isEqualTo(admin.id().get());
   }
 
   @Test
@@ -688,12 +590,12 @@
   @Test
   @TestProjectInput(cloneAs = "user")
   public void reviewWithWorkInProgressChangeOwner() throws Exception {
-    PushOneCommit push = pushFactory.create(db, user.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(user.newIdent(), testRepo);
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
-    assertThat(r.getChange().change().getOwner()).isEqualTo(user.id);
+    assertThat(r.getChange().change().getOwner()).isEqualTo(user.id());
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     ReviewInput in = ReviewInput.noScore().setWorkInProgress(true);
     gApi.changes().id(r.getChangeId()).current().review(in);
     ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
@@ -703,12 +605,12 @@
   @Test
   @TestProjectInput(cloneAs = "user")
   public void reviewWithWithWorkInProgressAdmin() throws Exception {
-    PushOneCommit push = pushFactory.create(db, user.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(user.newIdent(), testRepo);
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
-    assertThat(r.getChange().change().getOwner()).isEqualTo(user.id);
+    assertThat(r.getChange().change().getOwner()).isEqualTo(user.id());
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     ReviewInput in = ReviewInput.noScore().setWorkInProgress(true);
     gApi.changes().id(r.getChangeId()).current().review(in);
     ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
@@ -719,19 +621,35 @@
   public void reviewWithWorkInProgressByNonOwnerReturnsError() throws Exception {
     PushOneCommit.Result r = createChange();
     ReviewInput in = ReviewInput.noScore().setWorkInProgress(true);
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
-    exception.expectMessage("not allowed to toggle work in progress");
+    exception.expectMessage("toggle work in progress state not permitted");
     gApi.changes().id(r.getChangeId()).current().review(in);
   }
 
   @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);
+    requestScopeOperations.setApiUser(user.id());
+    gApi.changes().id(r.getChangeId()).current().review(in);
+    ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
+    assertThat(info.workInProgress).isTrue();
+  }
+
+  @Test
   public void reviewWithReadyByNonOwnerReturnsError() throws Exception {
     PushOneCommit.Result r = createChange();
     ReviewInput in = ReviewInput.noScore().setReady(true);
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
-    exception.expectMessage("not allowed to toggle work in progress");
+    exception.expectMessage("toggle work in progress state not permitted");
     gApi.changes().id(r.getChangeId()).current().review(in);
   }
 
@@ -747,8 +665,7 @@
 
     PushOneCommit push2 =
         pushFactory.create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             PushOneCommit.FILE_NAME,
@@ -789,7 +706,7 @@
   @Test
   public void revertNotifications() throws Exception {
     PushOneCommit.Result r = createChange();
-    gApi.changes().id(r.getChangeId()).addReviewer(user.email);
+    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();
 
@@ -805,7 +722,7 @@
   @Test
   public void suppressRevertNotifications() throws Exception {
     PushOneCommit.Result r = createChange();
-    gApi.changes().id(r.getChangeId()).addReviewer(user.email);
+    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();
 
@@ -822,33 +739,28 @@
     PushOneCommit.Result r = createChange();
 
     ReviewInput in = ReviewInput.approve();
-    in.reviewer(user.email);
-    in.reviewer(accountCreator.user2().email, ReviewerState.CC, true);
+    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);
+    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
-    setApiUser(accountCreator.admin2());
+    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());
-    if (notesMigration.readChanges()) {
-      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());
-    } else {
-      assertThat(reviewers)
-          .containsExactly(user.id.get(), admin.id.get(), accountCreator.user2().id.get());
-    }
+    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
@@ -903,8 +815,8 @@
     // ...and the committer and description should be correct
     ChangeInfo info = gApi.changes().id(changeId).get(CURRENT_REVISION, CURRENT_COMMIT);
     GitPerson committer = info.revisions.get(info.currentRevision).commit.committer;
-    assertThat(committer.name).isEqualTo(admin.fullName);
-    assertThat(committer.email).isEqualTo(admin.email);
+    assertThat(committer.name).isEqualTo(admin.fullName());
+    assertThat(committer.email).isEqualTo(admin.email());
     String description = info.revisions.get(info.currentRevision).description;
     assertThat(description).isEqualTo("Rebase");
 
@@ -914,17 +826,6 @@
     assertThat(cr.all).hasSize(1);
     assertThat(cr.all.get(0).value).isEqualTo(1);
 
-    if (notesMigration.changePrimaryStorage() == PrimaryStorage.REVIEW_DB) {
-      // Ensure record was actually copied under ReviewDb
-      List<PatchSetApproval> psas =
-          unwrapDb(db)
-              .patchSetApprovals()
-              .byPatchSet(new PatchSet.Id(new Change.Id(c2._number), 2))
-              .toList();
-      assertThat(psas).hasSize(1);
-      assertThat(psas.get(0).getValue()).isEqualTo((short) 1);
-    }
-
     // Rebasing the second change again should fail
     exception.expect(ResourceConflictException.class);
     exception.expectMessage("Change is already up to date");
@@ -932,6 +833,16 @@
   }
 
   @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);
+  }
+
+  @Test
   public void rebaseOnChangeNumber() throws Exception {
     String branchTip = testRepo.getRepository().exactRef("HEAD").getObjectId().name();
     PushOneCommit.Result r1 = createChange();
@@ -942,23 +853,78 @@
     RevisionInfo ri2 = ci2.revisions.get(ci2.currentRevision);
     assertThat(ri2.commit.parents.get(0).commit).isEqualTo(branchTip);
 
+    Change.Id id1 = r1.getChange().getId();
     RebaseInput in = new RebaseInput();
-    in.base = Integer.toString(r1.getChange().getId().get());
+    in.base = id1.toString();
     gApi.changes().id(r2.getChangeId()).rebase(in);
 
+    Change.Id id2 = r2.getChange().getId();
     ci2 = get(r2.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT);
     ri2 = ci2.revisions.get(ci2.currentRevision);
     assertThat(ri2.commit.parents.get(0).commit).isEqualTo(r1.getCommit().name());
+
+    List<RelatedChangeAndCommitInfo> related =
+        gApi.changes().id(id2.get()).revision(ri2._number).related().changes;
+    assertThat(related).hasSize(2);
+    assertThat(related.get(0)._changeNumber).isEqualTo(id2.get());
+    assertThat(related.get(0)._revisionNumber).isEqualTo(2);
+    assertThat(related.get(1)._changeNumber).isEqualTo(id1.get());
+    assertThat(related.get(1)._revisionNumber).isEqualTo(1);
   }
 
   @Test
-  public void rebaseOnNonExistingChange() throws Exception {
-    String changeId = createChange().getChangeId();
+  public void rebaseOnClosedChange() throws Exception {
+    String branchTip = testRepo.getRepository().exactRef("HEAD").getObjectId().name();
+    PushOneCommit.Result r1 = createChange();
+    testRepo.reset("HEAD~1");
+    PushOneCommit.Result r2 = createChange();
+
+    ChangeInfo ci2 = get(r2.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT);
+    RevisionInfo ri2 = ci2.revisions.get(ci2.currentRevision);
+    assertThat(ri2.commit.parents.get(0).commit).isEqualTo(branchTip);
+
+    // Submit first change.
+    Change.Id id1 = r1.getChange().getId();
+    gApi.changes().id(id1.get()).current().review(ReviewInput.approve());
+    gApi.changes().id(id1.get()).current().submit();
+
+    // Rebase second change on first change.
     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);
+    in.base = id1.toString();
+    gApi.changes().id(r2.getChangeId()).rebase(in);
+
+    Change.Id id2 = r2.getChange().getId();
+    ci2 = get(r2.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT);
+    ri2 = ci2.revisions.get(ci2.currentRevision);
+    assertThat(ri2.commit.parents.get(0).commit).isEqualTo(r1.getCommit().name());
+
+    assertThat(gApi.changes().id(id2.get()).revision(ri2._number).related().changes).isEmpty();
+  }
+
+  @Test
+  public void rebaseFromRelationChainToClosedChange() throws Exception {
+    PushOneCommit.Result r1 = createChange();
+    testRepo.reset("HEAD~1");
+
+    createChange();
+    PushOneCommit.Result r3 = createChange();
+
+    // Submit first change.
+    Change.Id id1 = r1.getChange().getId();
+    gApi.changes().id(id1.get()).current().review(ReviewInput.approve());
+    gApi.changes().id(id1.get()).current().submit();
+
+    // Rebase third change on first change.
+    RebaseInput in = new RebaseInput();
+    in.base = id1.toString();
+    gApi.changes().id(r3.getChangeId()).rebase(in);
+
+    Change.Id id3 = r3.getChange().getId();
+    ChangeInfo ci3 = get(r3.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT);
+    RevisionInfo ri3 = ci3.revisions.get(ci3.currentRevision);
+    assertThat(ri3.commit.parents.get(0).commit).isEqualTo(r1.getCommit().name());
+
+    assertThat(gApi.changes().id(id3.get()).revision(ri3._number).related().changes).isEmpty();
   }
 
   @Test
@@ -975,7 +941,7 @@
 
     // Rebase the second
     String changeId = r2.getChangeId();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("rebase not permitted");
     gApi.changes().id(changeId).rebase();
@@ -997,7 +963,7 @@
 
     // Rebase the second
     String changeId = r2.getChangeId();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(changeId).rebase();
   }
 
@@ -1018,7 +984,7 @@
 
     // Rebase the second
     String changeId = r2.getChangeId();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("rebase not permitted");
     gApi.changes().id(changeId).rebase();
@@ -1054,10 +1020,10 @@
   @TestProjectInput(cloneAs = "user")
   public void deleteNewChangeAsNormalUser() throws Exception {
     PushOneCommit.Result changeResult =
-        pushFactory.create(db, user.getIdent(), testRepo).to("refs/for/master");
+        pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
     String changeId = changeResult.getChangeId();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("delete not permitted");
     gApi.changes().id(changeId).delete();
@@ -1116,7 +1082,7 @@
       com.google.gerrit.acceptance.TestAccount deleteAs)
       throws Exception {
     try {
-      setApiUser(owner);
+      requestScopeOperations.setApiUser(owner.id());
       ChangeInput in = new ChangeInput();
       in.project = projectName.get();
       in.branch = "refs/heads/master";
@@ -1126,16 +1092,16 @@
       int id = changeInfo._number;
       String commit = changeInfo.currentRevision;
 
-      assertThat(gApi.changes().id(changeId).info().owner._accountId).isEqualTo(owner.id.get());
+      assertThat(gApi.changes().id(changeId).info().owner._accountId).isEqualTo(owner.id().get());
 
-      setApiUser(deleteAs);
+      requestScopeOperations.setApiUser(deleteAs.id());
       gApi.changes().id(changeId).delete();
 
       assertThat(query(changeId)).isEmpty();
 
       String ref = new Change.Id(id).toRefPrefix() + "1";
       eventRecorder.assertRefUpdatedEvents(projectName.get(), ref, null, commit, commit, null);
-      eventRecorder.assertChangeDeletedEvents(changeId, deleteAs.email);
+      eventRecorder.assertChangeDeletedEvents(changeId, deleteAs.email());
     } finally {
       removePermission(project, "refs/*", Permission.DELETE_OWN_CHANGES);
       removePermission(project, "refs/*", Permission.DELETE_CHANGES);
@@ -1155,7 +1121,7 @@
       PushOneCommit.Result changeResult = createChange();
       String changeId = changeResult.getChangeId();
 
-      setApiUser(user);
+      requestScopeOperations.setApiUser(user.id());
       exception.expect(AuthException.class);
       exception.expectMessage("delete not permitted");
       gApi.changes().id(changeId).delete();
@@ -1179,10 +1145,10 @@
   @TestProjectInput(cloneAs = "user")
   public void deleteAbandonedChangeAsNormalUser() throws Exception {
     PushOneCommit.Result changeResult =
-        pushFactory.create(db, user.getIdent(), testRepo).to("refs/for/master");
+        pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
     String changeId = changeResult.getChangeId();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(changeId).abandon();
 
     exception.expect(AuthException.class);
@@ -1194,7 +1160,7 @@
   @TestProjectInput(cloneAs = "user")
   public void deleteAbandonedChangeOfAnotherUserAsAdmin() throws Exception {
     PushOneCommit.Result changeResult =
-        pushFactory.create(db, user.getIdent(), testRepo).to("refs/for/master");
+        pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
     String changeId = changeResult.getChangeId();
 
     gApi.changes().id(changeId).abandon();
@@ -1223,12 +1189,12 @@
 
     try {
       PushOneCommit.Result changeResult =
-          pushFactory.create(db, user.getIdent(), testRepo).to("refs/for/master");
+          pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
       String changeId = changeResult.getChangeId();
 
       merge(changeResult);
 
-      setApiUser(user);
+      requestScopeOperations.setApiUser(user.id());
       exception.expect(MethodNotAllowedException.class);
       exception.expectMessage("delete not permitted");
       gApi.changes().id(changeId).delete();
@@ -1253,6 +1219,51 @@
   }
 
   @Test
+  public void deleteChangeUpdatesIndex() throws Exception {
+    PushOneCommit.Result changeResult = createChange();
+    String changeId = changeResult.getChangeId();
+    Change.Id id = changeResult.getChange().getId();
+
+    ChangeIndex idx = changeIndexCollection.getSearchIndex();
+
+    Optional<ChangeData> result =
+        idx.get(id, IndexedChangeQuery.createOptions(indexConfig, 0, 1, ImmutableSet.of()));
+
+    assertThat(result.isPresent()).isTrue();
+    gApi.changes().id(changeId).delete();
+    result = idx.get(id, IndexedChangeQuery.createOptions(indexConfig, 0, 1, ImmutableSet.of()));
+    assertThat(result.isPresent()).isFalse();
+  }
+
+  @Test
+  public void deleteChangeRemovesDraftComment() throws Exception {
+    PushOneCommit.Result r = createChange();
+
+    requestScopeOperations.setApiUser(user.id());
+
+    DraftInput dri = new DraftInput();
+    dri.message = "hello";
+    dri.path = "a.txt";
+    dri.line = 1;
+
+    gApi.changes().id(r.getChangeId()).current().createDraft(dri);
+    Change.Id num = r.getChange().getId();
+
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      assertThat(repo.getRefDatabase().getRefsByPrefix(RefNames.refsDraftComments(num, user.id())))
+          .isNotEmpty();
+    }
+
+    requestScopeOperations.setApiUser(admin.id());
+
+    gApi.changes().id(r.getChangeId()).delete();
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      assertThat(repo.getRefDatabase().getRefsByPrefix(RefNames.refsDraftComments(num, user.id())))
+          .isEmpty();
+    }
+  }
+
+  @Test
   public void rebaseUpToDateChange() throws Exception {
     PushOneCommit.Result r = createChange();
     exception.expect(ResourceConflictException.class);
@@ -1268,8 +1279,7 @@
 
     PushOneCommit push =
         pushFactory.create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             PushOneCommit.FILE_NAME,
@@ -1425,37 +1435,37 @@
   @Test
   public void pushCommitOfOtherUser() throws Exception {
     // admin pushes commit of user
-    PushOneCommit push = pushFactory.create(db, user.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(user.newIdent(), testRepo);
     PushOneCommit.Result result = push.to("refs/for/master");
     result.assertOkStatus();
 
     ChangeInfo change = gApi.changes().id(result.getChangeId()).get();
-    assertThat(change.owner._accountId).isEqualTo(admin.id.get());
+    assertThat(change.owner._accountId).isEqualTo(admin.id().get());
     CommitInfo commit = change.revisions.get(change.currentRevision).commit;
-    assertThat(commit.author.email).isEqualTo(user.email);
-    assertThat(commit.committer.email).isEqualTo(user.email);
+    assertThat(commit.author.email).isEqualTo(user.email());
+    assertThat(commit.committer.email).isEqualTo(user.email());
 
     // check that the author/committer was added as reviewer
     Collection<AccountInfo> reviewers = change.reviewers.get(REVIEWER);
     assertThat(reviewers).isNotNull();
     assertThat(reviewers).hasSize(1);
-    assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.getId().get());
+    assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.id().get());
     assertThat(change.reviewers.get(CC)).isNull();
 
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     Message m = messages.get(0);
     assertThat(m.from().getName()).isEqualTo("Administrator (Code Review)");
-    assertThat(m.rcpt()).containsExactly(user.emailAddress);
+    assertThat(m.rcpt()).containsExactly(user.getEmailAddress());
     assertThat(m.body()).contains("I'd like you to do a code review");
     assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
-    assertMailReplyTo(m, admin.email);
+    assertMailReplyTo(m, admin.email());
   }
 
   @Test
   public void pushCommitOfOtherUserThatCannotSeeChange() throws Exception {
     // create hidden project that is only visible to administrators
-    Project.NameKey p = createProject("p");
+    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/*");
@@ -1464,18 +1474,18 @@
 
     // admin pushes commit of user
     TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
-    PushOneCommit push = pushFactory.create(db, user.getIdent(), repo);
+    PushOneCommit push = pushFactory.create(user.newIdent(), repo);
     PushOneCommit.Result result = push.to("refs/for/master");
     result.assertOkStatus();
 
     ChangeInfo change = gApi.changes().id(result.getChangeId()).get();
-    assertThat(change.owner._accountId).isEqualTo(admin.id.get());
+    assertThat(change.owner._accountId).isEqualTo(admin.id().get());
     CommitInfo commit = change.revisions.get(change.currentRevision).commit;
-    assertThat(commit.author.email).isEqualTo(user.email);
-    assertThat(commit.committer.email).isEqualTo(user.email);
+    assertThat(commit.author.email).isEqualTo(user.email());
+    assertThat(commit.committer.email).isEqualTo(user.email());
 
     // check the user cannot see the change
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     try {
       gApi.changes().id(result.getChangeId()).get();
       fail("Expected ResourceNotFoundException");
@@ -1495,14 +1505,13 @@
     // admin pushes commit that references 'user' in a footer
     PushOneCommit push =
         pushFactory.create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT
                 + "\n\n"
                 + FooterConstants.REVIEWED_BY.getName()
                 + ": "
-                + user.getIdent().toExternalString(),
+                + user.newIdent().toExternalString(),
             PushOneCommit.FILE_NAME,
             PushOneCommit.FILE_CONTENT);
     PushOneCommit.Result result = push.to("refs/for/master");
@@ -1513,23 +1522,23 @@
     Collection<AccountInfo> reviewers = change.reviewers.get(REVIEWER);
     assertThat(reviewers).isNotNull();
     assertThat(reviewers).hasSize(1);
-    assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.getId().get());
+    assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.id().get());
     assertThat(change.reviewers.get(CC)).isNull();
 
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     Message m = messages.get(0);
-    assertThat(m.rcpt()).containsExactly(user.emailAddress);
-    assertThat(m.body()).contains("Hello " + user.fullName + ",\n");
+    assertThat(m.rcpt()).containsExactly(user.getEmailAddress());
+    assertThat(m.body()).contains("Hello " + user.fullName() + ",\n");
     assertThat(m.body()).contains("I'd like you to do a code review.");
     assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
-    assertMailReplyTo(m, admin.email);
+    assertMailReplyTo(m, admin.email());
   }
 
   @Test
   public void pushCommitWithFooterOfOtherUserThatCannotSeeChange() throws Exception {
     // create hidden project that is only visible to administrators
-    Project.NameKey p = createProject("p");
+    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/*");
@@ -1540,21 +1549,20 @@
     TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
     PushOneCommit push =
         pushFactory.create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             repo,
             PushOneCommit.SUBJECT
                 + "\n\n"
                 + FooterConstants.REVIEWED_BY.getName()
                 + ": "
-                + user.getIdent().toExternalString(),
+                + user.newIdent().toExternalString(),
             PushOneCommit.FILE_NAME,
             PushOneCommit.FILE_CONTENT);
     PushOneCommit.Result result = push.to("refs/for/master");
     result.assertOkStatus();
 
     // check that 'user' cannot see the change
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     try {
       gApi.changes().id(result.getChangeId()).get();
       fail("Expected ResourceNotFoundException");
@@ -1563,7 +1571,7 @@
     }
 
     // check that 'user' was NOT added as cc ('user' can't see the change)
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     ChangeInfo change = gApi.changes().id(result.getChangeId()).get();
     assertThat(change.reviewers.get(REVIEWER)).isNull();
     assertThat(change.reviewers.get(CC)).isNull();
@@ -1573,7 +1581,7 @@
   @Test
   public void addReviewerThatCannotSeeChange() throws Exception {
     // create hidden project that is only visible to administrators
-    Project.NameKey p = createProject("p");
+    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/*");
@@ -1582,12 +1590,12 @@
 
     // create change
     TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), repo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), repo);
     PushOneCommit.Result result = push.to("refs/for/master");
     result.assertOkStatus();
 
     // check the user cannot see the change
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     try {
       gApi.changes().id(result.getChangeId()).get();
       fail("Expected ResourceNotFoundException");
@@ -1596,12 +1604,12 @@
     }
 
     // try to add user as reviewer
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     AddReviewerResult r = gApi.changes().id(result.getChangeId()).addReviewer(in);
 
-    assertThat(r.input).isEqualTo(user.email);
+    assertThat(r.input).isEqualTo(user.email());
     assertThat(r.error).contains("does not have permission to see this change");
     assertThat(r.reviewers).isNull();
   }
@@ -1611,21 +1619,49 @@
     PushOneCommit.Result result = createChange();
 
     String username = name("new-user");
-    gApi.accounts().create(username).setActive(false);
+    Account.Id id = accountOperations.newAccount().username(username).inactive().create();
 
     AddReviewerInput in = new AddReviewerInput();
     in.reviewer = username;
     AddReviewerResult r = gApi.changes().id(result.getChangeId()).addReviewer(in);
 
-    assertThat(r.input).isEqualTo(username);
-    assertThat(r.error).contains("identifies an inactive account");
+    assertThat(r.input).isEqualTo(in.reviewer);
+    assertThat(r.error)
+        .isEqualTo(
+            "Account '"
+                + username
+                + "' only matches inactive accounts. To use an inactive account, retry with one of"
+                + " the following exact account IDs:\n"
+                + id
+                + ": Name of user not set ("
+                + id
+                + ")\n"
+                + username
+                + " does not identify a registered user or group");
     assertThat(r.reviewers).isNull();
   }
 
   @Test
-  public void addReviewerThatIsInactiveEmailFallback() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
+  public void addReviewerThatIsInactiveById() throws Exception {
+    PushOneCommit.Result result = createChange();
 
+    String username = name("new-user");
+    Account.Id id = accountOperations.newAccount().username(username).inactive().create();
+
+    AddReviewerInput in = new AddReviewerInput();
+    in.reviewer = Integer.toString(id.get());
+    AddReviewerResult r = gApi.changes().id(result.getChangeId()).addReviewer(in);
+
+    assertThat(r.input).isEqualTo(in.reviewer);
+    assertThat(r.error).isNull();
+    assertThat(r.reviewers).hasSize(1);
+    ReviewerInfo reviewer = r.reviewers.get(0);
+    assertThat(reviewer._accountId).isEqualTo(id.get());
+    assertThat(reviewer.username).isEqualTo(username);
+  }
+
+  @Test
+  public void addReviewerThatIsInactiveEmailFallback() throws Exception {
     ConfigInput conf = new ConfigInput();
     conf.enableReviewerByEmail = InheritableBoolean.TRUE;
     gApi.projects().name(project.get()).config(conf);
@@ -1633,7 +1669,7 @@
     PushOneCommit.Result result = createChange();
 
     String username = "user@domain.com";
-    gApi.accounts().create(username).setActive(false);
+    accountOperations.newAccount().username(username).inactive().create();
 
     AddReviewerInput in = new AddReviewerInput();
     in.reviewer = username;
@@ -1656,17 +1692,17 @@
     Timestamp oldTs = rsrc.getChange().getLastUpdatedOn();
 
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     gApi.changes().id(r.getChangeId()).addReviewer(in);
 
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     Message m = messages.get(0);
-    assertThat(m.rcpt()).containsExactly(user.emailAddress);
-    assertThat(m.body()).contains("Hello " + user.fullName + ",\n");
+    assertThat(m.rcpt()).containsExactly(user.getEmailAddress());
+    assertThat(m.body()).contains("Hello " + user.fullName() + ",\n");
     assertThat(m.body()).contains("I'd like you to do a code review.");
     assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
-    assertMailReplyTo(m, admin.email);
+    assertMailReplyTo(m, admin.email());
     ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
 
     // When NoteDb is enabled adding a reviewer records that user as reviewer
@@ -1676,7 +1712,7 @@
     Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER);
     assertThat(reviewers).isNotNull();
     assertThat(reviewers).hasSize(1);
-    assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.getId().get());
+    assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.id().get());
 
     // Ensure ETag and lastUpdatedOn are updated.
     rsrc = parseResource(r);
@@ -1685,7 +1721,7 @@
 
     // Change status of reviewer and ensure ETag is updated.
     oldETag = rsrc.getETag();
-    accountOperations.account(user.id).forUpdate().status("new status").update();
+    accountOperations.account(user.id()).forUpdate().status("new status").update();
     rsrc = parseResource(r);
     assertThat(rsrc.getETag()).isNotEqualTo(oldETag);
   }
@@ -1694,7 +1730,7 @@
   public void listReviewers() throws Exception {
     PushOneCommit.Result r = createChange();
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     gApi.changes().id(r.getChangeId()).addReviewer(in);
     assertThat(gApi.changes().id(r.getChangeId()).reviewers()).hasSize(1);
 
@@ -1710,13 +1746,13 @@
     in.state = ReviewerState.CC;
     gApi.changes().id(r.getChangeId()).addReviewer(in);
     assertThat(gApi.changes().id(r.getChangeId()).reviewers().stream().map(a -> a.username))
-        .containsExactly(user.username, username1);
+        .containsExactly(user.username(), username1);
   }
 
   @Test
   public void notificationsForAddedWorkInProgressReviewers() throws Exception {
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     ReviewInput batchIn = new ReviewInput();
     batchIn.reviewers = ImmutableList.of(in);
 
@@ -1765,10 +1801,10 @@
             .preferredEmail(email)
             .fullname(fullname)
             .create();
-    String testGroup = createGroupWithRealName("ab");
+    String testGroup = groupOperations.newGroup().name("ab").create().get();
     GroupApi groupApi = gApi.groups().id(testGroup);
     groupApi.description("test group");
-    groupApi.addMembers(user.fullName);
+    groupApi.addMembers(user.fullName());
 
     AddReviewerInput in = new AddReviewerInput();
     in.reviewer = "abc";
@@ -1826,7 +1862,7 @@
             .fullname(myGroupUserFullname)
             .create();
 
-    String testGroup = createGroupWithRealName("kobe");
+    String testGroup = groupOperations.newGroup().name("kobe").create().get();
     GroupApi groupApi = gApi.groups().id(testGroup);
     groupApi.description("test group");
     groupApi.addMembers(myGroupUserFullname);
@@ -1864,26 +1900,6 @@
   }
 
   @Test
-  public void addReviewerWithNoteDbWhenDummyApprovalInReviewDbExists() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-    assume().that(notesMigration.changePrimaryStorage()).isEqualTo(PrimaryStorage.REVIEW_DB);
-
-    PushOneCommit.Result r = createChange();
-
-    // insert dummy approval in ReviewDb
-    PatchSetApproval psa =
-        new PatchSetApproval(
-            new PatchSetApproval.Key(r.getPatchSetId(), user.id, new LabelId("Code-Review")),
-            (short) 0,
-            TimeUtil.nowTs());
-    db.patchSetApprovals().insert(Collections.singleton(psa));
-
-    AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
-    gApi.changes().id(r.getChangeId()).addReviewer(in);
-  }
-
-  @Test
   public void addSelfAsReviewer() throws Exception {
     TestTimeUtil.resetWithClockStep(1, SECONDS);
     PushOneCommit.Result r = createChange();
@@ -1892,8 +1908,8 @@
     Timestamp oldTs = rsrc.getChange().getLastUpdatedOn();
 
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
-    setApiUser(user);
+    in.reviewer = user.email();
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(r.getChangeId()).addReviewer(in);
 
     // There should be no email notification when adding self
@@ -1907,7 +1923,7 @@
     Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER);
     assertThat(reviewers).isNotNull();
     assertThat(reviewers).hasSize(1);
-    assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.getId().get());
+    assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.id().get());
 
     // Ensure ETag and lastUpdatedOn are updated.
     rsrc = parseResource(r);
@@ -1923,15 +1939,15 @@
   @Test
   public void implicitlyCcOnNonVotingReviewForUserWithoutUserNamePgStyle() throws Exception {
     com.google.gerrit.acceptance.TestAccount accountWithoutUsername = accountCreator.create();
-    assertThat(accountWithoutUsername.username).isNull();
+    assertThat(accountWithoutUsername.username()).isNull();
     testImplicitlyCcOnNonVotingReviewPgStyle(accountWithoutUsername);
   }
 
   private void testImplicitlyCcOnNonVotingReviewPgStyle(
       com.google.gerrit.acceptance.TestAccount testAccount) throws Exception {
     PushOneCommit.Result r = createChange();
-    setApiUser(testAccount);
-    assertThat(getReviewerState(r.getChangeId(), testAccount.id)).isEmpty();
+    requestScopeOperations.setApiUser(testAccount.id());
+    assertThat(getReviewerState(r.getChangeId(), testAccount.id())).isEmpty();
 
     // Exact request format made by PG UI at ddc6b7160fe416fed9e7e3180489d44c82fd64f8.
     ReviewInput in = new ReviewInput();
@@ -1941,45 +1957,13 @@
     in.reviewers = ImmutableList.of();
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(in);
 
-    // If we're not reading from NoteDb, then the CCed user will be returned in the REVIEWER state.
-    assertThat(getReviewerState(r.getChangeId(), testAccount.id))
-        .hasValue(notesMigration.readChanges() ? CC : REVIEWER);
-  }
-
-  @Test
-  public void implicitlyCcOnNonVotingReviewGwtStyle() throws Exception {
-    testImplicitlyCcOnNonVotingReviewGwtStyle(user);
-  }
-
-  @Test
-  public void implicitlyCcOnNonVotingReviewForUserWithoutUserNameGwtStyle() throws Exception {
-    com.google.gerrit.acceptance.TestAccount accountWithoutUsername = accountCreator.create();
-    assertThat(accountWithoutUsername.username).isNull();
-    testImplicitlyCcOnNonVotingReviewGwtStyle(accountWithoutUsername);
-  }
-
-  private void testImplicitlyCcOnNonVotingReviewGwtStyle(
-      com.google.gerrit.acceptance.TestAccount testAccount) throws Exception {
-    PushOneCommit.Result r = createChange();
-    setApiUser(testAccount);
-    assertThat(getReviewerState(r.getChangeId(), testAccount.id)).isEmpty();
-
-    // Exact request format made by GWT UI at ddc6b7160fe416fed9e7e3180489d44c82fd64f8.
-    ReviewInput in = new ReviewInput();
-    in.labels = ImmutableMap.of("Code-Review", (short) 0);
-    in.drafts = DraftHandling.PUBLISH_ALL_REVISIONS;
-    in.message = "comment";
-    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(in);
-
-    // If we're not reading from NoteDb, then the CCed user will be returned in the REVIEWER state.
-    assertThat(getReviewerState(r.getChangeId(), testAccount.id))
-        .hasValue(notesMigration.readChanges() ? CC : REVIEWER);
+    assertThat(getReviewerState(r.getChangeId(), testAccount.id())).hasValue(CC);
   }
 
   @Test
   public void implicitlyAddReviewerOnVotingReview() throws Exception {
     PushOneCommit.Result r = createChange();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes()
         .id(r.getChangeId())
         .revision(r.getCommit().name())
@@ -1987,24 +1971,23 @@
 
     ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
     assertThat(c.reviewers.get(REVIEWER).stream().map(ai -> ai._accountId).collect(toList()))
-        .containsExactly(user.id.get());
+        .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.
-    setApiUser(admin);
-    gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).remove();
+    requestScopeOperations.setApiUser(admin.id());
+    gApi.changes().id(r.getChangeId()).reviewer(user.id().toString()).remove();
     c = gApi.changes().id(r.getChangeId()).get();
     assertThat(c.reviewers.values()).isEmpty();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes()
         .id(r.getChangeId())
         .revision(r.getCommit().name())
         .review(new ReviewInput().message("hi"));
     c = gApi.changes().id(r.getChangeId()).get();
-    ReviewerState state = notesMigration.readChanges() ? CC : REVIEWER;
-    assertThat(c.reviewers.get(state).stream().map(ai -> ai._accountId).collect(toList()))
-        .containsExactly(user.id.get());
+    assertThat(c.reviewers.get(CC).stream().map(ai -> ai._accountId).collect(toList()))
+        .containsExactly(user.id().get());
   }
 
   @Test
@@ -2016,19 +1999,19 @@
     ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
     Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER);
     assertThat(reviewers).hasSize(1);
-    assertThat(reviewers.iterator().next()._accountId).isEqualTo(admin.getId().get());
+    assertThat(reviewers.iterator().next()._accountId).isEqualTo(admin.id().get());
     assertThat(c.reviewers).doesNotContainKey(CC);
 
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     gApi.changes().id(r.getChangeId()).addReviewer(in);
 
     c = gApi.changes().id(r.getChangeId()).get();
     reviewers = c.reviewers.get(REVIEWER);
     assertThat(reviewers).hasSize(2);
     Iterator<AccountInfo> reviewerIt = reviewers.iterator();
-    assertThat(reviewerIt.next()._accountId).isEqualTo(admin.getId().get());
-    assertThat(reviewerIt.next()._accountId).isEqualTo(user.getId().get());
+    assertThat(reviewerIt.next()._accountId).isEqualTo(admin.id().get());
+    assertThat(reviewerIt.next()._accountId).isEqualTo(user.id().get());
     assertThat(c.reviewers).doesNotContainKey(CC);
   }
 
@@ -2038,7 +2021,7 @@
     ChangeResource rsrc = parseResource(r);
     String oldETag = rsrc.getETag();
 
-    gApi.accounts().id(admin.id.get()).setStatus("new status");
+    accountOperations.account(admin.id()).forUpdate().status("new status").update();
     rsrc = parseResource(r);
     assertThat(rsrc.getETag()).isNotEqualTo(oldETag);
   }
@@ -2048,7 +2031,7 @@
     String changeId = createChange().getChangeId();
 
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     gApi.changes().id(changeId).addReviewer(in);
     sender.clear();
 
@@ -2063,7 +2046,7 @@
 
     assertThat(sender.getMessages()).hasSize(1);
     Message m = sender.getMessages().get(0);
-    assertThat(m.rcpt()).containsExactly(user.emailAddress);
+    assertThat(m.rcpt()).containsExactly(user.getEmailAddress());
   }
 
   @Test
@@ -2094,15 +2077,15 @@
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
 
     Map<String, Short> m =
-        gApi.changes().id(r.getChangeId()).reviewer(admin.getId().toString()).votes();
+        gApi.changes().id(r.getChangeId()).reviewer(admin.id().toString()).votes();
 
     assertThat(m).hasSize(1);
     assertThat(m).containsEntry("Code-Review", Short.valueOf((short) 2));
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.dislike());
 
-    m = gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).votes();
+    m = gApi.changes().id(r.getChangeId()).reviewer(user.id().toString()).votes();
 
     assertThat(m).hasSize(1);
     assertThat(m).containsEntry("Code-Review", Short.valueOf((short) -1));
@@ -2128,35 +2111,32 @@
 
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
-    gApi.changes().id(changeId).addReviewer(user.getId().toString());
+    gApi.changes().id(changeId).addReviewer(user.id().toString());
 
-    // ReviewerState will vary between ReviewDb and NoteDb; we just care that it
-    // shows up somewhere.
-    Iterable<AccountInfo> reviewers =
-        Iterables.concat(gApi.changes().id(changeId).get().reviewers.values());
-    assertThat(reviewers).hasSize(1);
-    assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.getId().get());
+    ChangeInfo c = gApi.changes().id(changeId).get(ListChangesOption.DETAILED_LABELS);
+    assertThat(getReviewers(c.reviewers.get(CC))).isEmpty();
+    assertThat(getReviewers(c.reviewers.get(REVIEWER))).containsExactly(user.id());
 
     sender.clear();
-    gApi.changes().id(changeId).reviewer(user.getId().toString()).remove();
+    gApi.changes().id(changeId).reviewer(user.id().toString()).remove();
     assertThat(gApi.changes().id(changeId).get().reviewers).isEmpty();
 
     assertThat(sender.getMessages()).hasSize(1);
     Message message = sender.getMessages().get(0);
-    assertThat(message.body()).contains("Removed reviewer " + user.fullName + ".");
+    assertThat(message.body()).contains("Removed reviewer " + user.fullName() + ".");
     assertThat(message.body()).doesNotContain("with the following votes");
 
     // Make sure the reviewer can still be added again.
-    gApi.changes().id(changeId).addReviewer(user.getId().toString());
-    reviewers = Iterables.concat(gApi.changes().id(changeId).get().reviewers.values());
-    assertThat(reviewers).hasSize(1);
-    assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.getId().get());
+    gApi.changes().id(changeId).addReviewer(user.id().toString());
+    c = gApi.changes().id(changeId).get();
+    assertThat(getReviewers(c.reviewers.get(CC))).isEmpty();
+    assertThat(getReviewers(c.reviewers.get(REVIEWER))).containsExactly(user.id());
 
     // Remove again, and then try to remove once more to verify 404 is
     // returned.
-    gApi.changes().id(changeId).reviewer(user.getId().toString()).remove();
+    gApi.changes().id(changeId).reviewer(user.id().toString()).remove();
     exception.expect(ResourceNotFoundException.class);
-    gApi.changes().id(changeId).reviewer(user.getId().toString()).remove();
+    gApi.changes().id(changeId).reviewer(user.id().toString()).remove();
   }
 
   @Test
@@ -2174,30 +2154,30 @@
     String changeId = r.getChangeId();
     gApi.changes().id(changeId).revision(r.getCommit().name()).review(ReviewInput.approve());
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(changeId).revision(r.getCommit().name()).review(ReviewInput.recommend());
 
     Collection<AccountInfo> reviewers = gApi.changes().id(changeId).get().reviewers.get(REVIEWER);
 
     assertThat(reviewers).hasSize(2);
     Iterator<AccountInfo> reviewerIt = reviewers.iterator();
-    assertThat(reviewerIt.next()._accountId).isEqualTo(admin.getId().get());
-    assertThat(reviewerIt.next()._accountId).isEqualTo(user.getId().get());
+    assertThat(reviewerIt.next()._accountId).isEqualTo(admin.id().get());
+    assertThat(reviewerIt.next()._accountId).isEqualTo(user.id().get());
 
     sender.clear();
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     DeleteReviewerInput input = new DeleteReviewerInput();
     if (!notify) {
       input.notify = NotifyHandling.NONE;
     }
-    gApi.changes().id(changeId).reviewer(user.getId().toString()).remove(input);
+    gApi.changes().id(changeId).reviewer(user.id().toString()).remove(input);
 
     if (notify) {
       assertThat(sender.getMessages()).hasSize(1);
       Message message = sender.getMessages().get(0);
       assertThat(message.body())
-          .contains("Removed reviewer " + user.fullName + " with the following votes");
-      assertThat(message.body()).contains("* Code-Review+1 by " + user.fullName);
+          .contains("Removed reviewer " + user.fullName() + " with the following votes");
+      assertThat(message.body()).contains("* Code-Review+1 by " + user.fullName());
     } else {
       assertThat(sender.getMessages()).isEmpty();
     }
@@ -2205,9 +2185,9 @@
     reviewers = gApi.changes().id(changeId).get().reviewers.get(REVIEWER);
     assertThat(reviewers).hasSize(1);
     reviewerIt = reviewers.iterator();
-    assertThat(reviewerIt.next()._accountId).isEqualTo(admin.getId().get());
+    assertThat(reviewerIt.next()._accountId).isEqualTo(admin.id().get());
 
-    eventRecorder.assertReviewerDeletedEvents(changeId, user.email);
+    eventRecorder.assertReviewerDeletedEvents(changeId, user.email());
   }
 
   @Test
@@ -2216,10 +2196,10 @@
     String changeId = r.getChangeId();
     gApi.changes().id(changeId).revision(r.getCommit().name()).review(ReviewInput.approve());
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("remove reviewer not permitted");
-    gApi.changes().id(r.getChangeId()).reviewer(admin.getId().toString()).remove();
+    gApi.changes().id(r.getChangeId()).reviewer(admin.id().toString()).remove();
   }
 
   @Test
@@ -2227,14 +2207,14 @@
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     recommend(changeId);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     approve(changeId);
     gApi.changes().id(changeId).revision(r.getCommit().name()).submit();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("remove reviewer not permitted");
     gApi.changes().id(r.getChangeId()).reviewer("self").remove();
@@ -2245,15 +2225,15 @@
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     recommend(changeId);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     gApi.changes().id(changeId).abandon();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(r.getChangeId()).reviewer("self").remove();
-    eventRecorder.assertReviewerDeletedEvents(changeId, user.email);
+    eventRecorder.assertReviewerDeletedEvents(changeId, user.email());
   }
 
   @Test
@@ -2261,17 +2241,17 @@
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     recommend(changeId);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     approve(changeId);
     gApi.changes().id(changeId).abandon();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("remove reviewer not permitted");
-    gApi.changes().id(r.getChangeId()).reviewer(admin.getId().toString()).remove();
+    gApi.changes().id(r.getChangeId()).reviewer(admin.id().toString()).remove();
   }
 
   @Test
@@ -2279,23 +2259,23 @@
     PushOneCommit.Result r = createChange();
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     recommend(r.getChangeId());
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     sender.clear();
-    gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).deleteVote("Code-Review");
+    gApi.changes().id(r.getChangeId()).reviewer(user.id().toString()).deleteVote("Code-Review");
 
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     Message msg = messages.get(0);
-    assertThat(msg.rcpt()).containsExactly(user.emailAddress);
-    assertThat(msg.body()).contains(admin.fullName + " has removed a vote from this change.");
+    assertThat(msg.rcpt()).containsExactly(user.getEmailAddress());
+    assertThat(msg.body()).contains(admin.fullName() + " has removed a vote from this change.");
     assertThat(msg.body())
-        .contains("Removed Code-Review+1 by " + user.fullName + " <" + user.email + ">\n");
+        .contains("Removed Code-Review+1 by " + user.fullName() + " <" + user.email() + ">\n");
 
     Map<String, Short> m =
-        gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).votes();
+        gApi.changes().id(r.getChangeId()).reviewer(user.id().toString()).votes();
 
     // Dummy 0 approval on the change to block vote copying to this patch set.
     assertThat(m).containsExactly("Code-Review", Short.valueOf((short) 0));
@@ -2303,10 +2283,10 @@
     ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
 
     ChangeMessageInfo message = Iterables.getLast(c.messages);
-    assertThat(message.author._accountId).isEqualTo(admin.getId().get());
+    assertThat(message.author._accountId).isEqualTo(admin.id().get());
     assertThat(message.message).isEqualTo("Removed Code-Review+1 by User <user@example.com>\n");
     assertThat(getReviewers(c.reviewers.get(REVIEWER)))
-        .containsExactlyElementsIn(ImmutableSet.of(admin.getId(), user.getId()));
+        .containsExactlyElementsIn(ImmutableSet.of(admin.id(), user.id()));
   }
 
   @Test
@@ -2314,15 +2294,15 @@
     PushOneCommit.Result r = createChange();
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     recommend(r.getChangeId());
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     sender.clear();
     DeleteVoteInput in = new DeleteVoteInput();
     in.label = "Code-Review";
     in.notify = NotifyHandling.NONE;
-    gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).deleteVote(in);
+    gApi.changes().id(r.getChangeId()).reviewer(user.id().toString()).deleteVote(in);
     assertThat(sender.getMessages()).isEmpty();
   }
 
@@ -2343,33 +2323,33 @@
         .preferredEmail(email)
         .fullname("User2")
         .create();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     recommend(r.getChangeId());
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     sender.clear();
     in.notifyDetails = new HashMap<>();
     in.notifyDetails.put(RecipientType.TO, new NotifyInfo(ImmutableList.of(email)));
-    gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).deleteVote(in);
+    gApi.changes().id(r.getChangeId()).reviewer(user.id().toString()).deleteVote(in);
     assertNotifyTo(email, "User2");
 
     // notify unrelated account as CC
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     recommend(r.getChangeId());
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     sender.clear();
     in.notifyDetails = new HashMap<>();
     in.notifyDetails.put(RecipientType.CC, new NotifyInfo(ImmutableList.of(email)));
-    gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).deleteVote(in);
+    gApi.changes().id(r.getChangeId()).reviewer(user.id().toString()).deleteVote(in);
     assertNotifyCc(email, "User2");
 
     // notify unrelated account as BCC
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     recommend(r.getChangeId());
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     sender.clear();
     in.notifyDetails = new HashMap<>();
     in.notifyDetails.put(RecipientType.BCC, new NotifyInfo(ImmutableList.of(email)));
-    gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).deleteVote(in);
+    gApi.changes().id(r.getChangeId()).reviewer(user.id().toString()).deleteVote(in);
     assertNotifyBcc(email, "User2");
   }
 
@@ -2378,10 +2358,10 @@
     PushOneCommit.Result r = createChange();
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("delete vote not permitted");
-    gApi.changes().id(r.getChangeId()).reviewer(admin.getId().toString()).deleteVote("Code-Review");
+    gApi.changes().id(r.getChangeId()).reviewer(admin.id().toString()).deleteVote("Code-Review");
   }
 
   @Test
@@ -2409,31 +2389,31 @@
     // Reviewers should only be "admin"
     ChangeInfo c = gApi.changes().id(changeId).get();
     assertThat(getReviewers(c.reviewers.get(REVIEWER)))
-        .containsExactlyElementsIn(ImmutableSet.of(admin.getId()));
+        .containsExactlyElementsIn(ImmutableSet.of(admin.id()));
     assertThat(c.reviewers.get(CC)).isNull();
 
     // Add the user as reviewer
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     gApi.changes().id(changeId).addReviewer(in);
     c = gApi.changes().id(changeId).get();
     assertThat(getReviewers(c.reviewers.get(REVIEWER)))
-        .containsExactlyElementsIn(ImmutableSet.of(admin.getId(), user.getId()));
+        .containsExactlyElementsIn(ImmutableSet.of(admin.id(), user.id()));
 
     // Approve the change as user, then remove the approval
     // (only to confirm that the user does have Code-Review+2 permission)
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(changeId).revision(commit).review(ReviewInput.approve());
     gApi.changes().id(changeId).revision(commit).review(ReviewInput.noScore());
 
     // Submit the change
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     gApi.changes().id(changeId).revision(commit).submit();
 
     // User should still be on the change
     c = gApi.changes().id(changeId).get();
     assertThat(getReviewers(c.reviewers.get(REVIEWER)))
-        .containsExactlyElementsIn(ImmutableSet.of(admin.getId(), user.getId()));
+        .containsExactlyElementsIn(ImmutableSet.of(admin.id(), user.id()));
   }
 
   @Test
@@ -2444,7 +2424,7 @@
     in.project = project.get();
     ChangeInfo info = gApi.changes().create(in).get();
     assertThat(info.project).isEqualTo(in.project);
-    assertThat(info.branch).isEqualTo(in.branch);
+    assertThat(RefNames.fullName(info.branch)).isEqualTo(RefNames.fullName(in.branch));
     assertThat(info.subject).isEqualTo(in.subject);
     assertThat(Iterables.getOnlyElement(info.messages).message).isEqualTo("Uploaded patch set 1.");
   }
@@ -2491,6 +2471,19 @@
   }
 
   @Test
+  public void queryChangesNoLimit() throws Exception {
+    allowGlobalCapabilities(
+        SystemGroupBackend.REGISTERED_USERS, 0, 2, GlobalCapability.QUERY_LIMIT);
+    for (int i = 0; i < 3; i++) {
+      createChange();
+    }
+    List<ChangeInfo> resultsWithDefaultLimit = gApi.changes().query().get();
+    List<ChangeInfo> resultsWithNoLimit = gApi.changes().query().withNoLimit().get();
+    assertThat(resultsWithDefaultLimit).hasSize(2);
+    assertThat(resultsWithNoLimit.size()).isAtLeast(3);
+  }
+
+  @Test
   public void queryChangesStart() throws Exception {
     PushOneCommit.Result r1 = createChange();
     createChange();
@@ -2533,7 +2526,7 @@
     RevisionInfo rev = Iterables.getOnlyElement(result.revisions.values());
     assertThat(rev._number).isEqualTo(r.getPatchSetId().get());
     assertThat(rev.created).isNotNull();
-    assertThat(rev.uploader._accountId).isEqualTo(admin.getId().get());
+    assertThat(rev.uploader._accountId).isEqualTo(admin.id().get());
     assertThat(rev.ref).isEqualTo(r.getPatchSetId().toRefName());
     assertThat(rev.actions).isNotEmpty();
   }
@@ -2544,18 +2537,59 @@
     assertThat(
             Iterables.getOnlyElement(query("project:{" + project.get() + "} owner:self")).changeId)
         .isEqualTo(r.getChangeId());
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     assertThat(query("owner:self project:{" + project.get() + "}")).isEmpty();
   }
 
+  private static class OperatorModule extends AbstractModule {
+    @Override
+    public void configure() {
+      bind(ChangeOperatorFactory.class)
+          .annotatedWith(Exports.named("mytopic"))
+          .toInstance((cqb, value) -> new MyTopicPredicate(value));
+    }
+
+    private static class MyTopicPredicate extends PostFilterPredicate<ChangeData> {
+      MyTopicPredicate(String value) {
+        super("mytopic", value);
+      }
+
+      @Override
+      public boolean match(ChangeData cd) {
+        return Objects.equals(cd.change().getTopic(), value);
+      }
+
+      @Override
+      public int getCost() {
+        return 2;
+      }
+    }
+  }
+
+  @Test
+  public void queryChangesPluginOperator() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String query = "mytopic_myplugin:foo";
+    String expectedMessage = "Unsupported operator mytopic_myplugin:foo";
+    assertThatQueryException(query).hasMessageThat().isEqualTo(expectedMessage);
+
+    try (AutoCloseable ignored = installPlugin("myplugin", OperatorModule.class)) {
+      assertThat(query(query)).isEmpty();
+      gApi.changes().id(r.getChangeId()).topic("foo");
+      assertThat(query(query).stream().map(i -> i.changeId)).containsExactly(r.getChangeId());
+    }
+
+    assertThatQueryException(query).hasMessageThat().isEqualTo(expectedMessage);
+  }
+
   @Test
   public void checkReviewedFlagBeforeAndAfterReview() throws Exception {
     PushOneCommit.Result r = createChange();
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     gApi.changes().id(r.getChangeId()).addReviewer(in);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     assertThat(get(r.getChangeId(), REVIEWED).reviewed).isNull();
 
     revision(r).review(ReviewInput.recommend());
@@ -2576,7 +2610,7 @@
   public void editTopicWithoutPermissionNotAllowed() throws Exception {
     PushOneCommit.Result r = createChange();
     assertThat(gApi.changes().id(r.getChangeId()).topic()).isEqualTo("");
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("edit topic name not permitted");
     gApi.changes().id(r.getChangeId()).topic("mytopic");
@@ -2587,7 +2621,7 @@
     PushOneCommit.Result r = createChange();
     assertThat(gApi.changes().id(r.getChangeId()).topic()).isEqualTo("");
     grant(project, "refs/heads/master", Permission.EDIT_TOPIC_NAME, false, REGISTERED_USERS);
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(r.getChangeId()).topic("mytopic");
     assertThat(gApi.changes().id(r.getChangeId()).topic()).isEqualTo("mytopic");
   }
@@ -2631,7 +2665,7 @@
   public void submitNotAllowedWithoutPermission() throws Exception {
     PushOneCommit.Result r = createChange();
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("submit not permitted");
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
@@ -2642,7 +2676,7 @@
     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);
-    setApiUser(user);
+    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);
   }
@@ -2678,8 +2712,7 @@
     r1.assertOkStatus();
     PushOneCommit.Result r2 =
         pushFactory
-            .create(
-                db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "new content", r1.getChangeId())
+            .create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "new content", r1.getChangeId())
             .to("refs/for/master");
     r2.assertOkStatus();
 
@@ -2726,16 +2759,9 @@
     RegistrationHandle handle =
         changeMessageModifiers.add(
             "gerrit",
-            new ChangeMessageModifier() {
-              @Override
-              public String onSubmit(
-                  String newCommitMessage,
-                  RevCommit original,
-                  RevCommit mergeTip,
-                  Branch.NameKey destination) {
-                assertThat(original.getName()).isNotEqualTo(mergeTip.getName());
-                return newCommitMessage + "Custom: " + destination.get();
-              }
+            (newCommitMessage, original, mergeTip, destination) -> {
+              assertThat(original.getName()).isNotEqualTo(mergeTip.getName());
+              return newCommitMessage + "Custom: " + destination.get();
             });
     ChangeInfo actual;
     try {
@@ -2766,7 +2792,7 @@
 
   @Test
   public void defaultSearchDoesNotTouchDatabase() throws Exception {
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     PushOneCommit.Result r1 = createChange();
     gApi.changes()
         .id(r1.getChangeId())
@@ -2776,9 +2802,8 @@
 
     createChange();
 
-    setApiUser(user);
-    AcceptanceTestRequestScope.Context ctx = disableDb();
-    try {
+    requestScopeOperations.setApiUser(user.id());
+    try (AutoCloseable ignored = disableNoteDb()) {
       assertThat(
               gApi.changes()
                   .query()
@@ -2789,8 +2814,6 @@
                   .withOption(REVIEWED)
                   .get())
           .hasSize(2);
-    } finally {
-      enableDb(ctx);
     }
   }
 
@@ -2798,12 +2821,12 @@
   public void votable() throws Exception {
     PushOneCommit.Result r = createChange();
     String triplet = project.get() + "~master~" + r.getChangeId();
-    gApi.changes().id(triplet).addReviewer(user.username);
+    gApi.changes().id(triplet).addReviewer(user.username());
     ChangeInfo c = gApi.changes().id(triplet).get(DETAILED_LABELS);
     LabelInfo codeReview = c.labels.get("Code-Review");
     assertThat(codeReview.all).hasSize(1);
     ApprovalInfo approval = codeReview.all.get(0);
-    assertThat(approval._accountId).isEqualTo(user.id.get());
+    assertThat(approval._accountId).isEqualTo(user.id().get());
     assertThat(approval.value).isEqualTo(0);
 
     try (ProjectConfigUpdate u = updateProject(project)) {
@@ -2815,7 +2838,7 @@
     codeReview = c.labels.get("Code-Review");
     assertThat(codeReview.all).hasSize(1);
     approval = codeReview.all.get(0);
-    assertThat(approval._accountId).isEqualTo(user.id.get());
+    assertThat(approval._accountId).isEqualTo(user.id().get());
     assertThat(approval.value).isNull();
   }
 
@@ -2843,7 +2866,7 @@
 
   @Test
   public void anonymousRestApi() throws Exception {
-    setApiUserAnonymous();
+    requestScopeOperations.setApiUserAnonymous();
     PushOneCommit.Result r = createChange();
 
     ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
@@ -2862,12 +2885,9 @@
 
   @Test
   public void noteDbCommitsOnPatchSetCreation() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     PushOneCommit.Result r = createChange();
     pushFactory
-        .create(
-            db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "4711", r.getChangeId())
+        .create(admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "4711", r.getChangeId())
         .to("refs/for/master")
         .assertOkStatus();
     ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
@@ -2878,7 +2898,7 @@
 
       assertThat(commitPatchSetCreation.getShortMessage()).isEqualTo("Create patch set 2");
       PersonIdent expectedAuthor =
-          changeNoteUtil.newIdent(getAccount(admin.id), c.updated, serverIdent.get());
+          changeNoteUtil.newIdent(getAccount(admin.id()), c.updated, serverIdent.get());
       assertThat(commitPatchSetCreation.getAuthorIdent()).isEqualTo(expectedAuthor);
       assertThat(commitPatchSetCreation.getCommitterIdent())
           .isEqualTo(new PersonIdent(serverIdent.get(), c.updated));
@@ -2886,7 +2906,8 @@
 
       RevCommit commitChangeCreation = rw.parseCommit(commitPatchSetCreation.getParent(0));
       assertThat(commitChangeCreation.getShortMessage()).isEqualTo("Create change");
-      expectedAuthor = changeNoteUtil.newIdent(getAccount(admin.id), c.created, serverIdent.get());
+      expectedAuthor =
+          changeNoteUtil.newIdent(getAccount(admin.id()), c.created, serverIdent.get());
       assertThat(commitChangeCreation.getAuthorIdent()).isEqualTo(expectedAuthor);
       assertThat(commitChangeCreation.getCommitterIdent())
           .isEqualTo(new PersonIdent(serverIdent.get(), c.created));
@@ -2903,7 +2924,7 @@
     in.newBranch = true;
     ChangeInfo info = gApi.changes().create(in).get();
     assertThat(info.project).isEqualTo(in.project);
-    assertThat(info.branch).isEqualTo(in.branch);
+    assertThat(RefNames.fullName(info.branch)).isEqualTo(RefNames.fullName(in.branch));
     assertThat(info.subject).isEqualTo(in.subject);
     assertThat(Iterables.getOnlyElement(info.messages).message).isEqualTo("Uploaded patch set 1.");
   }
@@ -2923,7 +2944,7 @@
   @Test
   public void createNewPatchSetWithoutPermission() throws Exception {
     // Create new project with clean permissions
-    Project.NameKey p = createProject("addPatchSet1");
+    Project.NameKey p = projectOperations.newProject().create();
 
     // Clone separate repositories of the same project as admin and as user
     TestRepository<InMemoryRepository> adminTestRepo = cloneProject(p, admin);
@@ -2933,7 +2954,7 @@
     block(p, "refs/for/*", Permission.ADD_PATCH_SET, REGISTERED_USERS);
 
     // Create change as admin
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), adminTestRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), adminTestRepo);
     PushOneCommit.Result r1 = push.to("refs/for/master");
     r1.assertOkStatus();
 
@@ -2953,7 +2974,7 @@
     TestRepository<?> userTestRepo = cloneProject(project, user);
 
     // Create change as admin
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), adminTestRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), adminTestRepo);
     PushOneCommit.Result r1 = push.to("refs/for/master");
     r1.assertOkStatus();
 
@@ -2969,7 +2990,7 @@
   @Test
   public void createNewPatchSetAsOwnerWithoutPermission() throws Exception {
     // Create new project with clean permissions
-    Project.NameKey p = createProject("addPatchSet2");
+    Project.NameKey p = projectOperations.newProject().create();
     // Clone separate repositories of the same project as admin and as user
     TestRepository<?> adminTestRepo = cloneProject(project, admin);
 
@@ -2977,7 +2998,7 @@
     block(p, "refs/for/*", Permission.ADD_PATCH_SET, REGISTERED_USERS);
 
     // Create change as admin
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), adminTestRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), adminTestRepo);
     PushOneCommit.Result r1 = push.to("refs/for/master");
     r1.assertOkStatus();
 
@@ -3009,7 +3030,7 @@
     createBranch("dev");
     PushOneCommit.Result changeA =
         pushFactory
-            .create(db, user.getIdent(), testRepo, "change A", "A.txt", "A content")
+            .create(user.newIdent(), testRepo, "change A", "A.txt", "A content")
             .to("refs/heads/dev");
     changeA.assertOkStatus();
     MergeInput mergeInput = new MergeInput();
@@ -3045,7 +3066,7 @@
     createBranch("dev");
     PushOneCommit.Result changeA =
         pushFactory
-            .create(db, user.getIdent(), testRepo, "change A", "A.txt", "A content")
+            .create(user.newIdent(), testRepo, "change A", "A.txt", "A content")
             .to("refs/heads/dev");
     changeA.assertOkStatus();
     MergeInput mergeInput = new MergeInput();
@@ -3081,7 +3102,7 @@
     gApi.changes().id(baseChange).setPrivate(true, "set private");
 
     // Create the destination change on 'master' branch.
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     testRepo.reset(initialHead);
     String changeId = createChange().getChangeId();
 
@@ -3219,8 +3240,7 @@
     testRepo.reset("config");
     PushOneCommit push2 =
         pushFactory.create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             "Ignore Verified",
             "rules.pl",
@@ -3266,8 +3286,7 @@
     testRepo.reset("config");
     PushOneCommit push2 =
         pushFactory.create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             "Configure Non-Author-Code-Review",
             "rules.pl",
@@ -3304,10 +3323,10 @@
 
     PushOneCommit.Result r = createChange();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
 
     ChangeInfo change = gApi.changes().id(r.getChangeId()).get();
@@ -3321,7 +3340,7 @@
   public void checkLabelsForAutoClosedChange() throws Exception {
     PushOneCommit.Result r = createChange();
 
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result result = push.to("refs/heads/master");
     result.assertOkStatus();
 
@@ -3340,13 +3359,13 @@
     PushOneCommit.Result r = createChange();
     String triplet = project.get() + "~master~" + r.getChangeId();
 
-    gApi.changes().id(triplet).addReviewer(user.username);
+    gApi.changes().id(triplet).addReviewer(user.username());
 
     ChangeInfo c = gApi.changes().id(triplet).get(DETAILED_LABELS);
     LabelInfo codeReview = c.labels.get("Code-Review");
     assertThat(codeReview.all).hasSize(1);
     ApprovalInfo approval = codeReview.all.get(0);
-    assertThat(approval._accountId).isEqualTo(user.id.get());
+    assertThat(approval._accountId).isEqualTo(user.id().get());
     assertThat(approval.permittedVotingRange).isNotNull();
     // default values
     assertThat(approval.permittedVotingRange.min).isEqualTo(-1);
@@ -3367,7 +3386,7 @@
     codeReview = c.labels.get("Code-Review");
     assertThat(codeReview.all).hasSize(1);
     approval = codeReview.all.get(0);
-    assertThat(approval._accountId).isEqualTo(user.id.get());
+    assertThat(approval._accountId).isEqualTo(user.id().get());
     assertThat(approval.permittedVotingRange).isNotNull();
     assertThat(approval.permittedVotingRange.min).isEqualTo(minPermittedValue);
     assertThat(approval.permittedVotingRange.max).isEqualTo(maxPermittedValue);
@@ -3383,13 +3402,13 @@
     PushOneCommit.Result r = createChange();
     String triplet = project.get() + "~master~" + r.getChangeId();
 
-    gApi.changes().id(triplet).addReviewer(user.username);
+    gApi.changes().id(triplet).addReviewer(user.username());
 
     ChangeInfo c = gApi.changes().id(triplet).get(DETAILED_LABELS);
     LabelInfo codeReview = c.labels.get("Code-Review");
     assertThat(codeReview.all).hasSize(1);
     ApprovalInfo approval = codeReview.all.get(0);
-    assertThat(approval._accountId).isEqualTo(user.id.get());
+    assertThat(approval._accountId).isEqualTo(user.id().get());
     assertThat(approval.permittedVotingRange).isNull();
   }
 
@@ -3401,7 +3420,8 @@
     ReviewInput input = ReviewInput.approve().label("Code-Style", 1);
     gApi.changes().id(changeId).current().review(input);
 
-    Map<String, Short> votes = gApi.changes().id(changeId).current().reviewer(admin.email).votes();
+    Map<String, Short> votes =
+        gApi.changes().id(changeId).current().reviewer(admin.email()).votes();
     assertThat(votes.keySet()).containsExactly("Code-Review");
     assertThat(votes.values()).containsExactly((short) 2);
   }
@@ -3414,13 +3434,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();
-    if (!notesMigration.readChanges()) {
-      assertThat(votes.keySet()).containsExactly("Code-Review");
-      assertThat(votes.values()).containsExactly((short) 0);
-    } else {
-      assertThat(votes).isEmpty();
-    }
+    Map<String, Short> votes =
+        gApi.changes().id(changeId).current().reviewer(admin.email()).votes();
+    assertThat(votes).isEmpty();
   }
 
   @Test
@@ -3460,10 +3476,10 @@
 
     String oldHead = getRemoteHead().name();
     PushOneCommit.Result result1 =
-        pushFactory.create(db, user.getIdent(), testRepo).to("refs/for/master");
+        pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
     testRepo.reset(oldHead);
     PushOneCommit.Result result2 =
-        pushFactory.create(db, user.getIdent(), testRepo).to("refs/for/master");
+        pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
 
     addComment(result1, "comment 1", true, false, null);
     addComment(result2, "comment 2", true, true, null);
@@ -3481,8 +3497,7 @@
     addPureRevertSubmitRule();
 
     // Create a change that is not a revert of another change
-    PushOneCommit.Result r1 =
-        pushFactory.create(db, user.getIdent(), testRepo).to("refs/for/master");
+    PushOneCommit.Result r1 = pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
     approve(r1.getChangeId());
 
     exception.expect(ResourceConflictException.class);
@@ -3493,8 +3508,7 @@
 
   @Test
   public void pureRevertFactBlocksSubmissionOfNonPureReverts() throws Exception {
-    PushOneCommit.Result r1 =
-        pushFactory.create(db, user.getIdent(), testRepo).to("refs/for/master");
+    PushOneCommit.Result r1 = pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
     merge(r1);
 
     addPureRevertSubmitRule();
@@ -3513,8 +3527,7 @@
   @Test
   public void pureRevertFactAllowsSubmissionOfPureReverts() throws Exception {
     // Create a change that we can later revert
-    PushOneCommit.Result r1 =
-        pushFactory.create(db, user.getIdent(), testRepo).to("refs/for/master");
+    PushOneCommit.Result r1 = pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
     merge(r1);
 
     addPureRevertSubmitRule();
@@ -3535,9 +3548,9 @@
         .isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n");
 
     for (com.google.gerrit.acceptance.TestAccount acc : ImmutableList.of(admin, user)) {
-      setApiUser(acc);
+      requestScopeOperations.setApiUser(acc.id());
       String newMessage =
-          "modified commit by " + acc.username + "\n\nChange-Id: " + r.getChangeId() + "\n";
+          "modified commit by " + acc.username() + "\n\nChange-Id: " + r.getChangeId() + "\n";
       gApi.changes().id(r.getChangeId()).setMessage(newMessage);
       RevisionApi rApi = gApi.changes().id(r.getChangeId()).current();
       assertThat(rApi.files().keySet()).containsExactly("/COMMIT_MSG", "a.txt");
@@ -3554,7 +3567,7 @@
 
     // Move the change to WIP and edit the commit message again, to observe a
     // different tag. Must switch to change owner to move into WIP.
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     gApi.changes().id(r.getChangeId()).setWorkInProgress();
     String newMessage = "modified commit in WIP change\n\nChange-Id: " + r.getChangeId() + "\n";
     gApi.changes().id(r.getChangeId()).setMessage(newMessage);
@@ -3592,6 +3605,18 @@
   }
 
   @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");
+  }
+
+  @Test
   public void changeCommitMessageWithWrongChangeIdFails() throws Exception {
     PushOneCommit.Result otherChange = createChange();
     PushOneCommit.Result r = createChange();
@@ -3607,12 +3632,12 @@
   @Test
   public void changeCommitMessageWithoutPermissionFails() throws Exception {
     // Create new project with clean permissions
-    Project.NameKey p = createProject("addPatchSetEdit");
+    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);
     // Create change as user
-    PushOneCommit push = pushFactory.create(db, user.getIdent(), userTestRepo);
+    PushOneCommit push = pushFactory.create(user.newIdent(), userTestRepo);
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
     // Try to change the commit message
@@ -3642,7 +3667,7 @@
     String subject = "A happy change " + smile;
     PushOneCommit.Result r =
         pushFactory
-            .create(db, admin.getIdent(), testRepo, subject, FILE_NAME, FILE_CONTENT)
+            .create(admin.newIdent(), testRepo, subject, FILE_NAME, FILE_CONTENT)
             .to("refs/for/master");
     r.assertOkStatus();
     String id = r.getChangeId();
@@ -3764,7 +3789,7 @@
   @Test
   public void pureRevertThrowsExceptionWhenChangeIsNotARevertAndNoIdProvided() throws Exception {
     exception.expect(BadRequestException.class);
-    exception.expectMessage("no ID was provided and change isn't a revert");
+    exception.expectMessage("revertOf not set");
     gApi.changes().id(createChange().getChangeId()).pureRevert();
   }
 
@@ -3800,7 +3825,7 @@
       u.save();
     }
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
 
@@ -3814,13 +3839,13 @@
     input.label(label, 1);
     gApi.changes().id(changeId).current().review(input);
 
-    assertThat(gApi.changes().id(changeId).current().reviewer(user.email).votes().keySet())
+    assertThat(gApi.changes().id(changeId).current().reviewer(user.email()).votes().keySet())
         .containsExactly(codeReviewLabel, label);
-    assertThat(gApi.changes().id(changeId).current().reviewer(user.email).votes().values())
+    assertThat(gApi.changes().id(changeId).current().reviewer(user.email()).votes().values())
         .containsExactly((short) 2, (short) 1);
     assertThat(gApi.changes().id(changeId).get().submittable).isTrue();
 
-    setApiUser(admin);
+    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/*");
@@ -3832,16 +3857,16 @@
     }
 
     // Verify user's new permitted range.
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     change = gApi.changes().id(changeId).get();
     assertPermitted(change, label);
     assertPermitted(change, codeReviewLabel, -1, 0, 1);
 
-    assertThat(gApi.changes().id(changeId).current().reviewer(user.email).votes().values())
+    assertThat(gApi.changes().id(changeId).current().reviewer(user.email()).votes().values())
         .containsExactly((short) 2, (short) 1);
     assertThat(gApi.changes().id(changeId).get().submittable).isTrue();
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     gApi.changes().id(changeId).current().submit();
   }
 
@@ -3873,7 +3898,7 @@
   private String createNewChange() throws Exception {
     TestRepository<InMemoryRepository> userRepo = cloneProject(project, user);
     PushOneCommit.Result result =
-        pushFactory.create(db, user.getIdent(), userRepo).to("refs/for/master");
+        pushFactory.create(user.newIdent(), userRepo).to("refs/for/master");
     String changeId = result.getChangeId();
     return changeId;
   }
@@ -3910,6 +3935,9 @@
   }
 
   private static Iterable<Account.Id> getReviewers(Collection<AccountInfo> r) {
+    if (r == null) {
+      return ImmutableList.of();
+    }
     return Iterables.transform(r, a -> new Account.Id(a._accountId));
   }
 
@@ -3931,7 +3959,7 @@
 
   private void setChangeStatus(Change.Id id, Change.Status newStatus) throws Exception {
     try (BatchUpdate batchUpdate =
-        batchUpdateFactory.create(db, project, atrScope.get().getUser(), TimeUtil.nowTs())) {
+        batchUpdateFactory.create(project, atrScope.get().getUser(), TimeUtil.nowTs())) {
       batchUpdate.addOp(id, new ChangeStatusUpdateOp(newStatus));
       batchUpdate.execute();
     }
@@ -3976,13 +4004,13 @@
   }
 
   private void modifySubmitRules(String newContent) throws Exception {
-    try (Repository repo = repoManager.openRepository(project)) {
-      TestRepository<?> testRepo = new TestRepository<>((InMemoryRepository) repo);
+    try (Repository repo = repoManager.openRepository(project);
+        TestRepository<Repository> testRepo = new TestRepository<>(repo)) {
       testRepo
           .branch(RefNames.REFS_CONFIG)
           .commit()
-          .author(admin.getIdent())
-          .committer(admin.getIdent())
+          .author(admin.newIdent())
+          .committer(admin.newIdent())
           .add("rules.pl", newContent)
           .message("Modify rules.pl")
           .create();
@@ -3996,8 +4024,7 @@
   public void trackingIds() throws Exception {
     PushOneCommit push =
         pushFactory.create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT + "\n\n" + "Bug:JRA001",
             PushOneCommit.FILE_NAME,
@@ -4046,20 +4073,20 @@
     PushOneCommit.Result r = createChange();
 
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     gApi.changes().id(r.getChangeId()).addReviewer(in);
 
     in = new AddReviewerInput();
     in.reviewer = email;
     gApi.changes().id(r.getChangeId()).addReviewer(in);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(r.getChangeId()).ignore(true);
     assertThat(gApi.changes().id(r.getChangeId()).ignored()).isTrue();
 
     // New patch set notification is not sent to users ignoring the change
     sender.clear();
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     amendChange(r.getChangeId());
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
@@ -4080,7 +4107,7 @@
     assertThat(messages).hasSize(1);
     assertThat(messages.get(0).rcpt()).containsExactly(address);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(r.getChangeId()).ignore(false);
     assertThat(gApi.changes().id(r.getChangeId()).ignored()).isFalse();
   }
@@ -4098,7 +4125,7 @@
   public void cannotIgnoreStarredChange() throws Exception {
     String changeId = createChange().getChangeId();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     gApi.accounts().self().starChange(changeId);
     assertThat(gApi.changes().id(changeId).get().starred).isTrue();
 
@@ -4116,7 +4143,7 @@
   public void cannotStarIgnoredChange() throws Exception {
     String changeId = createChange().getChangeId();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(changeId).ignore(true);
     assertThat(gApi.changes().id(changeId).ignored()).isTrue();
 
@@ -4137,31 +4164,31 @@
     PushOneCommit.Result r = createChange();
 
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     gApi.changes().id(r.getChangeId()).addReviewer(in);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     assertThat(gApi.changes().id(r.getChangeId()).get().reviewed).isNull();
     gApi.changes().id(r.getChangeId()).markAsReviewed(true);
     assertThat(gApi.changes().id(r.getChangeId()).get().reviewed).isTrue();
 
-    setApiUser(user2);
+    requestScopeOperations.setApiUser(user2.id());
     sender.clear();
     amendChange(r.getChangeId());
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     assertThat(gApi.changes().id(r.getChangeId()).get().reviewed).isNull();
 
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
-    assertThat(messages.get(0).rcpt()).containsExactly(user.emailAddress);
+    assertThat(messages.get(0).rcpt()).containsExactly(user.getEmailAddress());
   }
 
   @Test
   public void cannotSetUnreviewedLabelForPatchSetThatAlreadyHasReviewedLabel() throws Exception {
     String changeId = createChange().getChangeId();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(changeId).markAsReviewed(true);
     assertThat(gApi.changes().id(changeId).get().reviewed).isTrue();
 
@@ -4186,7 +4213,7 @@
   public void cannotSetReviewedLabelForPatchSetThatAlreadyHasUnreviewedLabel() throws Exception {
     String changeId = createChange().getChangeId();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(changeId).markAsReviewed(false);
     assertThat(gApi.changes().id(changeId).get().reviewed).isNull();
 
@@ -4211,7 +4238,7 @@
   public void setReviewedAndUnreviewedLabelsForDifferentPatchSets() throws Exception {
     String changeId = createChange().getChangeId();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(changeId).markAsReviewed(true);
     assertThat(gApi.changes().id(changeId).get().reviewed).isTrue();
 
@@ -4240,11 +4267,25 @@
 
   @Test
   public void changeDetailsDoesNotRequireIndex() throws Exception {
+    // This set of options must be kept in sync with gr-rest-api-interface.js
+    Set<ListChangesOption> options =
+        ImmutableSet.of(
+            ListChangesOption.ALL_COMMITS,
+            ListChangesOption.ALL_REVISIONS,
+            ListChangesOption.CHANGE_ACTIONS,
+            ListChangesOption.CURRENT_ACTIONS,
+            ListChangesOption.DETAILED_LABELS,
+            ListChangesOption.DOWNLOAD_COMMANDS,
+            ListChangesOption.MESSAGES,
+            ListChangesOption.SUBMITTABLE,
+            ListChangesOption.WEB_LINKS,
+            ListChangesOption.SKIP_MERGEABLE);
+
     PushOneCommit.Result change = createChange();
     int number = gApi.changes().id(change.getChangeId()).get()._number;
 
-    try (AutoCloseable ctx = disableChangeIndex()) {
-      assertThat(gApi.changes().id(project.get(), number).get(ImmutableSet.of()).changeId)
+    try (AutoCloseable ignored = disableChangeIndex()) {
+      assertThat(gApi.changes().id(project.get(), number).get(options).changeId)
           .isEqualTo(change.getChangeId());
     }
   }
@@ -4256,4 +4297,13 @@
   private BranchApi createBranch(String branch) throws Exception {
     return createBranch(new Branch.NameKey(project, branch));
   }
+
+  private ThrowableSubject assertThatQueryException(String query) throws Exception {
+    try {
+      query(query);
+    } catch (BadRequestException e) {
+      return assertThat(e);
+    }
+    throw new AssertionError("expected BadRequestException");
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIdIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIdIT.java
index fe7da66..7899ecd 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIdIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIdIT.java
@@ -19,18 +19,21 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 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.DeprecatedIdentifierException;
 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;
 
 @NoHttpd
 public class ChangeIdIT extends AbstractDaemonTest {
   private ChangeInfo changeInfo;
+  @Inject private ProjectOperations projectOperations;
 
   @Before
   public void setup() throws Exception {
@@ -45,7 +48,7 @@
 
   @Test
   public void projectChangeNumberReturnsChangeWhenProjectContainsSlashes() throws Exception {
-    Project.NameKey p = createProject("foo/bar");
+    Project.NameKey p = projectOperations.newProject().create();
     ChangeInfo ci = gApi.changes().create(new ChangeInput(p.get(), "master", "msg")).get();
     ChangeApi cApi = gApi.changes().id(p.get(), ci._number);
     assertThat(cApi.get().changeId).isEqualTo(ci.changeId);
diff --git a/javatests/com/google/gerrit/acceptance/api/change/DisablePrivateChangesIT.java b/javatests/com/google/gerrit/acceptance/api/change/DisablePrivateChangesIT.java
index ffb8b34..ae88afd 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/DisablePrivateChangesIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/DisablePrivateChangesIT.java
@@ -54,7 +54,7 @@
   @GerritConfig(name = "change.disablePrivateChanges", value = "true")
   public void pushPrivatesWithDisablePrivateChangesTrue() throws Exception {
     PushOneCommit.Result result =
-        pushFactory.create(db, admin.getIdent(), testRepo).to("refs/for/master%private");
+        pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master%private");
     result.assertErrorStatus();
   }
 
@@ -64,11 +64,11 @@
   public void pushDraftsWithDisablePrivateChangesTrue() throws Exception {
     RevCommit initialHead = getRemoteHead();
     PushOneCommit.Result result =
-        pushFactory.create(db, admin.getIdent(), testRepo).to("refs/for/master%draft");
+        pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master%draft");
     result.assertErrorStatus();
 
     testRepo.reset(initialHead);
-    result = pushFactory.create(db, admin.getIdent(), testRepo).to("refs/drafts/master");
+    result = pushFactory.create(admin.newIdent(), testRepo).to("refs/drafts/master");
     result.assertErrorStatus();
   }
 
@@ -76,7 +76,7 @@
   @GerritConfig(name = "change.disablePrivateChanges", value = "true")
   public void pushWithDisablePrivateChangesTrue() throws Exception {
     PushOneCommit.Result result =
-        pushFactory.create(db, admin.getIdent(), testRepo).to("refs/for/master");
+        pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master");
     result.assertOkStatus();
     assertThat(result.getChange().change().isPrivate()).isFalse();
   }
@@ -85,7 +85,7 @@
   @GerritConfig(name = "change.allowDrafts", value = "true")
   public void pushPrivatesWithDisablePrivateChangesFalse() throws Exception {
     PushOneCommit.Result result =
-        pushFactory.create(db, admin.getIdent(), testRepo).to("refs/for/master%private");
+        pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master%private");
     assertThat(result.getChange().change().isPrivate()).isTrue();
   }
 
@@ -94,11 +94,11 @@
   public void pushDraftsWithDisablePrivateChangesFalse() throws Exception {
     RevCommit initialHead = getRemoteHead();
     PushOneCommit.Result result =
-        pushFactory.create(db, admin.getIdent(), testRepo).to("refs/for/master%draft");
+        pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master%draft");
     assertThat(result.getChange().change().isPrivate()).isTrue();
 
     testRepo.reset(initialHead);
-    result = pushFactory.create(db, admin.getIdent(), testRepo).to("refs/drafts/master");
+    result = pushFactory.create(admin.newIdent(), testRepo).to("refs/drafts/master");
     assertThat(result.getChange().change().isPrivate()).isTrue();
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/api/change/MergeListIT.java b/javatests/com/google/gerrit/acceptance/api/change/MergeListIT.java
index 1acd71c..a08d417 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/MergeListIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/MergeListIT.java
@@ -55,8 +55,7 @@
     PushOneCommit.Result gp1 =
         pushFactory
             .create(
-                db,
-                admin.getIdent(),
+                admin.newIdent(),
                 testRepo,
                 "grand parent 1",
                 ImmutableMap.of("foo", "foo-1.1", "bar", "bar-1.1"))
@@ -66,8 +65,7 @@
     PushOneCommit.Result p1 =
         pushFactory
             .create(
-                db,
-                admin.getIdent(),
+                admin.newIdent(),
                 testRepo,
                 "parent 1",
                 ImmutableMap.of("foo", "foo-1.2", "bar", "bar-1.2"))
@@ -80,8 +78,7 @@
     PushOneCommit.Result gp2 =
         pushFactory
             .create(
-                db,
-                admin.getIdent(),
+                admin.newIdent(),
                 testRepo,
                 "grand parent 2",
                 ImmutableMap.of("foo", "foo-2.1", "bar", "bar-2.1"))
@@ -91,8 +88,7 @@
     PushOneCommit.Result p2 =
         pushFactory
             .create(
-                db,
-                admin.getIdent(),
+                admin.newIdent(),
                 testRepo,
                 "parent 2",
                 ImmutableMap.of("foo", "foo-2.2", "bar", "bar-2.2"))
@@ -101,11 +97,7 @@
 
     PushOneCommit m =
         pushFactory.create(
-            db,
-            admin.getIdent(),
-            testRepo,
-            "merge",
-            ImmutableMap.of("foo", "foo-1", "bar", "bar-2"));
+            admin.newIdent(), testRepo, "merge", ImmutableMap.of("foo", "foo-1", "bar", "bar-2"));
     m.setParents(ImmutableList.of(p1.getCommit(), p2.getCommit()));
     PushOneCommit.Result result = m.to("refs/for/master");
     result.assertOkStatus();
diff --git a/javatests/com/google/gerrit/acceptance/api/change/PluginFieldsIT.java b/javatests/com/google/gerrit/acceptance/api/change/PluginFieldsIT.java
new file mode 100644
index 0000000..d5089ff
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/change/PluginFieldsIT.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.acceptance.api.change;
+
+import com.google.gerrit.acceptance.AbstractPluginFieldsTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.server.change.ChangeAttributeFactory;
+import com.google.inject.AbstractModule;
+import org.junit.Test;
+
+@NoHttpd
+public class PluginFieldsIT extends AbstractPluginFieldsTest {
+  // No tests for /detail via the extension API, since the extension API doesn't have that method.
+
+  @Test
+  public void queryChangeWithNullAttribute() throws Exception {
+    getChangeWithNullAttribute(
+        id -> pluginInfoFromSingletonList(gApi.changes().query(id.toString()).get()));
+  }
+
+  @Test
+  public void getChangeWithNullAttribute() throws Exception {
+    getChangeWithNullAttribute(
+        id -> pluginInfoFromChangeInfo(gApi.changes().id(id.toString()).get()));
+  }
+
+  @Test
+  public void queryChangeWithSimpleAttribute() throws Exception {
+    getChangeWithSimpleAttribute(
+        id -> pluginInfoFromSingletonList(gApi.changes().query(id.toString()).get()));
+  }
+
+  @Test
+  public void getChangeWithSimpleAttribute() throws Exception {
+    getChangeWithSimpleAttribute(
+        id -> pluginInfoFromChangeInfo(gApi.changes().id(id.toString()).get()));
+  }
+
+  @Test
+  public void queryChangeWithOption() throws Exception {
+    getChangeWithOption(
+        id -> pluginInfoFromSingletonList(gApi.changes().query(id.toString()).get()),
+        (id, opts) ->
+            pluginInfoFromSingletonList(
+                gApi.changes().query(id.toString()).withPluginOptions(opts).get()));
+  }
+
+  @Test
+  public void getChangeWithOption() throws Exception {
+    getChangeWithOption(
+        id -> pluginInfoFromChangeInfo(gApi.changes().id(id.get()).get()),
+        (id, opts) -> pluginInfoFromChangeInfo(gApi.changes().id(id.get()).get(opts)));
+  }
+
+  static class SimpleAttributeWithExplicitExportModule extends AbstractModule {
+    @Override
+    public void configure() {
+      bind(ChangeAttributeFactory.class)
+          .annotatedWith(Exports.named("simple"))
+          .toInstance((cd, bp, p) -> new MyInfo("change " + cd.getId()));
+    }
+  }
+
+  @Test
+  public void getChangeWithSimpleAttributeWithExplicitExport() throws Exception {
+    // For backwards compatibility with old plugins, allow modules to bind into the
+    // DynamicSet<ChangeAttributeFactory> as if it were a DynamicMap. We only need one variant of
+    // this test to prove that the mapping works.
+    getChangeWithSimpleAttribute(
+        id -> pluginInfoFromChangeInfo(gApi.changes().id(id.toString()).get()),
+        SimpleAttributeWithExplicitExportModule.class);
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/PrivateChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/PrivateChangeIT.java
new file mode 100644
index 0000000..bfcb1a8
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/change/PrivateChangeIT.java
@@ -0,0 +1,261 @@
+// 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.server.group.SystemGroupBackend.REGISTERED_USERS;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+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.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;
+import com.google.gerrit.server.update.BatchUpdateOp;
+import com.google.gerrit.server.update.ChangeContext;
+import com.google.gerrit.server.util.time.TimeUtil;
+import com.google.inject.Inject;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.junit.Test;
+
+public class PrivateChangeIT extends AbstractDaemonTest {
+
+  @Inject private RequestScopeOperations requestScopeOperations;
+
+  @Test
+  public void setPrivateByOwner() throws Exception {
+    TestRepository<InMemoryRepository> userRepo = cloneProject(project, user);
+    PushOneCommit.Result result =
+        pushFactory.create(user.newIdent(), userRepo).to("refs/for/master");
+
+    requestScopeOperations.setApiUser(user.id());
+    String changeId = result.getChangeId();
+    assertThat(gApi.changes().id(changeId).get().isPrivate).isNull();
+
+    gApi.changes().id(changeId).setPrivate(true, null);
+    ChangeInfo info = gApi.changes().id(changeId).get();
+    assertThat(info.isPrivate).isTrue();
+    assertThat(Iterables.getLast(info.messages).message).isEqualTo("Set private");
+    assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_PRIVATE);
+
+    gApi.changes().id(changeId).setPrivate(false, null);
+    info = gApi.changes().id(changeId).get();
+    assertThat(info.isPrivate).isNull();
+    assertThat(Iterables.getLast(info.messages).message).isEqualTo("Unset private");
+    assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_UNSET_PRIVATE);
+
+    String msg = "This is a security fix that must not be public.";
+    gApi.changes().id(changeId).setPrivate(true, msg);
+    info = gApi.changes().id(changeId).get();
+    assertThat(info.isPrivate).isTrue();
+    assertThat(Iterables.getLast(info.messages).message).isEqualTo("Set private\n\n" + msg);
+    assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_PRIVATE);
+
+    msg = "After this security fix has been released we can make it public now.";
+    gApi.changes().id(changeId).setPrivate(false, msg);
+    info = gApi.changes().id(changeId).get();
+    assertThat(info.isPrivate).isNull();
+    assertThat(Iterables.getLast(info.messages).message).isEqualTo("Unset private\n\n" + msg);
+    assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_UNSET_PRIVATE);
+  }
+
+  @Test
+  public void cannotSetMergedChangePrivate() throws Exception {
+    PushOneCommit.Result result = createChange();
+    approve(result.getChangeId());
+    merge(result);
+
+    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);
+  }
+
+  @Test
+  public void cannotSetAbandonedChangePrivate() throws Exception {
+    PushOneCommit.Result result = createChange();
+    String changeId = result.getChangeId();
+
+    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);
+  }
+
+  @Test
+  public void administratorCanSetUserChangePrivate() throws Exception {
+    TestRepository<InMemoryRepository> userRepo = cloneProject(project, user);
+    PushOneCommit.Result result =
+        pushFactory.create(user.newIdent(), userRepo).to("refs/for/master");
+
+    String changeId = result.getChangeId();
+    assertThat(gApi.changes().id(changeId).get().isPrivate).isNull();
+
+    gApi.changes().id(changeId).setPrivate(true, null);
+    requestScopeOperations.setApiUser(user.id());
+    ChangeInfo info = gApi.changes().id(changeId).get();
+    assertThat(info.isPrivate).isTrue();
+  }
+
+  @Test
+  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);
+  }
+
+  @Test
+  public void accessPrivate() 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, null);
+    // Owner can always access its private changes.
+    assertThat(gApi.changes().id(result.getChangeId()).get().isPrivate).isTrue();
+
+    // Add admin as a reviewer.
+    gApi.changes().id(result.getChangeId()).addReviewer(admin.id().toString());
+
+    // This change should be visible for admin as a reviewer.
+    requestScopeOperations.setApiUser(admin.id());
+    assertThat(gApi.changes().id(result.getChangeId()).get().isPrivate).isTrue();
+
+    // Remove admin from reviewers.
+    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());
+  }
+
+  @Test
+  public void privateChangeOfOtherUserCanBeAccessedWithPermission() throws Exception {
+    PushOneCommit.Result result = createChange();
+    gApi.changes().id(result.getChangeId()).setPrivate(true, null);
+
+    allow("refs/*", Permission.VIEW_PRIVATE_CHANGES, REGISTERED_USERS);
+    requestScopeOperations.setApiUser(user.id());
+    assertThat(gApi.changes().id(result.getChangeId()).get().isPrivate).isTrue();
+  }
+
+  @Test
+  public void administratorCanUnmarkPrivateAfterMerging() throws Exception {
+    PushOneCommit.Result result = createChange();
+    String changeId = result.getChangeId();
+    merge(result);
+    markMergedChangePrivate(new Change.Id(gApi.changes().id(changeId).get()._number));
+
+    gApi.changes().id(changeId).setPrivate(false, null);
+    assertThat(gApi.changes().id(changeId).get().isPrivate).isNull();
+  }
+
+  @Test
+  public void ownerCannotMarkPrivateAfterMerging() throws Exception {
+    TestRepository<InMemoryRepository> userRepo = cloneProject(project, user);
+    PushOneCommit.Result result =
+        pushFactory.create(user.newIdent(), userRepo).to("refs/for/master");
+
+    String changeId = result.getChangeId();
+    assertThat(gApi.changes().id(changeId).get().isPrivate).isNull();
+
+    merge(result);
+
+    requestScopeOperations.setApiUser(user.id());
+    exception.expect(AuthException.class);
+    exception.expectMessage("not allowed to mark private");
+    gApi.changes().id(changeId).setPrivate(true, null);
+  }
+
+  @Test
+  public void mergingPrivateChangePublishesIt() throws Exception {
+    PushOneCommit.Result result = createChange();
+    gApi.changes().id(result.getChangeId()).setPrivate(true);
+    assertThat(gApi.changes().id(result.getChangeId()).get().isPrivate).isTrue();
+
+    approve(result.getChangeId());
+    merge(result);
+
+    assertThat(gApi.changes().id(result.getChangeId()).get().status).isEqualTo(ChangeStatus.MERGED);
+    assertThat(gApi.changes().id(result.getChangeId()).get().isPrivate).isNull();
+  }
+
+  @Test
+  public void ownerCanUnmarkPrivateAfterMerging() throws Exception {
+    TestRepository<InMemoryRepository> userRepo = cloneProject(project, user);
+    PushOneCommit.Result result =
+        pushFactory.create(user.newIdent(), userRepo).to("refs/for/master");
+
+    String changeId = result.getChangeId();
+    gApi.changes().id(changeId).addReviewer(admin.id().toString());
+    merge(result);
+    markMergedChangePrivate(new Change.Id(gApi.changes().id(changeId).get()._number));
+
+    requestScopeOperations.setApiUser(user.id());
+    gApi.changes().id(changeId).setPrivate(false, null);
+    assertThat(gApi.changes().id(changeId).get().isPrivate).isNull();
+  }
+
+  @Test
+  public void mergingPrivateChangeThroughGitPublishesIt() throws Exception {
+    PushOneCommit.Result r = createChange();
+    gApi.changes().id(r.getChangeId()).setPrivate(true);
+
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+    PushOneCommit.Result result = push.to("refs/heads/master");
+    result.assertOkStatus();
+
+    assertThat(gApi.changes().id(r.getChangeId()).get().isPrivate).isNull();
+  }
+
+  private void markMergedChangePrivate(Change.Id changeId) throws Exception {
+    try (BatchUpdate u =
+        batchUpdateFactory.create(
+            project, identifiedUserFactory.create(admin.id()), TimeUtil.nowTs())) {
+      u.addOp(
+              changeId,
+              new BatchUpdateOp() {
+                @Override
+                public boolean updateChange(ChangeContext ctx) {
+                  ctx.getChange().setPrivate(true);
+                  ChangeUpdate update = ctx.getUpdate(ctx.getChange().currentPatchSetId());
+                  ctx.getChange().setPrivate(true);
+                  ctx.getChange().setLastUpdatedOn(ctx.getWhen());
+                  update.setPrivate(true);
+                  return true;
+                }
+              })
+          .execute();
+    }
+    assertThat(gApi.changes().id(changeId.get()).get().isPrivate).isTrue();
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java b/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
index d632113..d53b305 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
@@ -34,6 +34,7 @@
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
+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.extensions.api.changes.CherryPickInput;
@@ -46,6 +47,7 @@
 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.inject.Inject;
 import java.util.EnumSet;
 import java.util.List;
 import java.util.Set;
@@ -58,6 +60,7 @@
 
 @NoHttpd
 public class StickyApprovalsIT extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   @Before
   public void setup() throws Exception {
@@ -436,8 +439,8 @@
         testRepo.amendRef("HEAD").insertChangeId(changeId.substring(1));
     commitBuilder
         .message("New subject " + System.nanoTime())
-        .author(admin.getIdent())
-        .committer(new PersonIdent(admin.getIdent(), testRepo.getDate()));
+        .author(admin.newIdent())
+        .committer(new PersonIdent(admin.newIdent(), testRepo.getDate()));
     commitBuilder.create();
     GitUtil.pushHead(testRepo, "refs/for/master", false);
     assertThat(getChangeKind(changeId)).isEqualTo(NO_CODE_CHANGE);
@@ -451,8 +454,8 @@
         testRepo.amendRef("HEAD").insertChangeId(changeId.substring(1));
     commitBuilder
         .message(commitMessage)
-        .author(admin.getIdent())
-        .committer(new PersonIdent(admin.getIdent(), testRepo.getDate()));
+        .author(admin.newIdent())
+        .committer(new PersonIdent(admin.newIdent(), testRepo.getDate()));
     commitBuilder.create();
     GitUtil.pushHead(testRepo, "refs/for/master", false);
     assertThat(getChangeKind(changeId)).isEqualTo(NO_CHANGE);
@@ -461,8 +464,7 @@
   private void rework(String changeId) throws Exception {
     PushOneCommit push =
         pushFactory.create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             PushOneCommit.FILE_NAME,
@@ -473,12 +475,11 @@
   }
 
   private void trivialRebase(String changeId) throws Exception {
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     testRepo.reset(getRemoteHead());
     PushOneCommit push =
         pushFactory.create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             "Other Change",
             "a" + System.nanoTime() + ".txt",
@@ -504,7 +505,7 @@
 
     testRepo.reset(parent1.getCommit());
 
-    PushOneCommit merge = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit merge = pushFactory.create(admin.newIdent(), testRepo);
     merge.setParents(ImmutableList.of(parent1.getCommit(), parent2.getCommit()));
     PushOneCommit.Result result = merge.to("refs/for/master");
     result.assertOkStatus();
@@ -521,7 +522,7 @@
     testRepo.reset(parent1);
     PushOneCommit.Result newParent1 = createChange("new parent 1", "p1-1.txt", "content 1-1");
 
-    PushOneCommit merge = pushFactory.create(db, admin.getIdent(), testRepo, changeId);
+    PushOneCommit merge = pushFactory.create(admin.newIdent(), testRepo, changeId);
     merge.setParents(ImmutableList.of(newParent1.getCommit(), commitParent2));
     PushOneCommit.Result result = merge.to("refs/for/master");
     result.assertOkStatus();
@@ -539,7 +540,7 @@
     testRepo.reset(parent2);
     PushOneCommit.Result newParent2 = createChange("new parent 2", "p2-2.txt", "content 2-2");
 
-    PushOneCommit merge = pushFactory.create(db, admin.getIdent(), testRepo, changeId);
+    PushOneCommit merge = pushFactory.create(admin.newIdent(), testRepo, changeId);
     merge.setParents(ImmutableList.of(commitParent1, newParent2.getCommit()));
     PushOneCommit.Result result = merge.to("refs/for/master");
     result.assertOkStatus();
@@ -563,8 +564,7 @@
     PushOneCommit.Result r =
         pushFactory
             .create(
-                db,
-                admin.getIdent(),
+                admin.newIdent(),
                 testRepo,
                 PushOneCommit.SUBJECT,
                 "other.txt",
@@ -591,21 +591,21 @@
   }
 
   private void vote(TestAccount user, String changeId, String label, int vote) throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(changeId).current().review(new ReviewInput().label(label, vote));
   }
 
   private void vote(TestAccount user, String changeId, int codeReviewVote, int verifiedVote)
       throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     ReviewInput in =
         new ReviewInput().label("Code-Review", codeReviewVote).label("Verified", verifiedVote);
     gApi.changes().id(changeId).current().review(in);
   }
 
   private void deleteVote(TestAccount user, String changeId, String label) throws Exception {
-    setApiUser(user);
-    gApi.changes().id(changeId).reviewer(user.getId().toString()).deleteVote(label);
+    requestScopeOperations.setApiUser(user.id());
+    gApi.changes().id(changeId).reviewer(user.id().toString()).deleteVote(label);
   }
 
   private void assertVotes(ChangeInfo c, TestAccount user, int codeReviewVote, int verifiedVote) {
@@ -623,7 +623,7 @@
     Integer vote = 0;
     if (c.labels.get(label) != null && c.labels.get(label).all != null) {
       for (ApprovalInfo approval : c.labels.get(label).all) {
-        if (approval._accountId == user.id.get()) {
+        if (approval._accountId == user.id().get()) {
           vote = approval.value;
           break;
         }
diff --git a/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java b/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java
index 507205d..6c0e9ab 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java
@@ -137,8 +137,7 @@
   private PushOneCommit.Result createChange(String dest, String subject) throws Exception {
     PushOneCommit push =
         pushFactory.create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             subject,
             "file" + fileCounter.incrementAndGet(),
diff --git a/javatests/com/google/gerrit/acceptance/api/config/TopMenusIT.java b/javatests/com/google/gerrit/acceptance/api/config/TopMenusIT.java
new file mode 100644
index 0000000..b6d2712
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/config/TopMenusIT.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.acceptance.api.config;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.webui.TopMenu;
+import com.google.gerrit.extensions.webui.TopMenu.MenuEntry;
+import com.google.inject.AbstractModule;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Test;
+
+@TestPlugin(
+    name = "test-topmenus",
+    sysModule = "com.google.gerrit.acceptance.api.config.TopMenusIT$Module")
+public class TopMenusIT extends LightweightPluginDaemonTest {
+
+  static final TopMenu.MenuEntry TEST_MENU_ENTRY =
+      new TopMenu.MenuEntry("MyMenu", Collections.emptyList());
+
+  public static class Module extends AbstractModule {
+
+    @Override
+    protected void configure() {
+      DynamicSet.bind(binder(), TopMenu.class).to(TopMenuTest.class);
+    }
+  }
+
+  public static class TopMenuTest implements TopMenu {
+
+    @Override
+    public List<MenuEntry> getEntries() {
+      return Arrays.asList(TEST_MENU_ENTRY);
+    }
+  }
+
+  @Test
+  public void topMenuShouldReturnOneEntry() throws RestApiException {
+    List<MenuEntry> topMenuItems = gApi.config().server().topMenus();
+    assertThat(topMenuItems).containsExactly(TEST_MENU_ENTRY);
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/group/BUILD b/javatests/com/google/gerrit/acceptance/api/group/BUILD
index f65db0d..1ba1138 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/BUILD
+++ b/javatests/com/google/gerrit/acceptance/api/group/BUILD
@@ -21,7 +21,6 @@
     deps = [
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/server",
-        "//lib:gwtorm",
         "//lib/truth",
     ],
 )
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupIndexerIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupIndexerIT.java
index a664869..d7d311b 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupIndexerIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupIndexerIT.java
@@ -15,11 +15,12 @@
 package com.google.gerrit.acceptance.api.group;
 
 import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.server.group.testing.InternalGroupSubject.internalGroups;
 import static com.google.gerrit.truth.ListSubject.assertThat;
 import static com.google.gerrit.truth.OptionalSubject.assertThat;
 
 import com.google.common.collect.ImmutableSet;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+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;
@@ -35,7 +36,6 @@
 import com.google.gerrit.testing.InMemoryTestEnvironment;
 import com.google.gerrit.truth.ListSubject;
 import com.google.gerrit.truth.OptionalSubject;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -157,17 +157,17 @@
 
   private void updateGroupWithoutCacheOrIndex(
       AccountGroup.UUID groupUuid, InternalGroupUpdate groupUpdate)
-      throws OrmException, NoSuchGroupException, IOException, ConfigInvalidException {
+      throws NoSuchGroupException, IOException, ConfigInvalidException {
     groupsUpdate.updateGroupInNoteDb(groupUuid, groupUpdate);
   }
 
   private static OptionalSubject<InternalGroupSubject, InternalGroup> assertThatGroup(
       Optional<InternalGroup> updatedGroup) {
-    return assertThat(updatedGroup, InternalGroupSubject::assertThat);
+    return assertThat(updatedGroup, internalGroups());
   }
 
   private static ListSubject<InternalGroupSubject, InternalGroup> assertThatGroups(
       List<InternalGroup> parentGroups) {
-    return assertThat(parentGroups, InternalGroupSubject::assertThat);
+    return assertThat(parentGroups, internalGroups());
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsConsistencyIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsConsistencyIT.java
index 87a566e..e269e68 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsConsistencyIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsConsistencyIT.java
@@ -20,6 +20,7 @@
 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.common.data.GlobalCapability;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
@@ -30,6 +31,7 @@
 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;
+import com.google.inject.Inject;
 import java.util.List;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.RefRename;
@@ -47,6 +49,8 @@
 @Sandboxed
 @NoHttpd
 public class GroupsConsistencyIT extends AbstractDaemonTest {
+
+  @Inject protected GroupOperations groupOperations;
   private GroupInfo gAdmin;
   private GroupInfo g1;
   private GroupInfo g2;
@@ -57,11 +61,11 @@
   public void basicSetup() throws Exception {
     allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
 
-    String name1 = createGroup("g1");
-    String name2 = createGroup("g2");
+    String name1 = groupOperations.newGroup().name("g1").create().get();
+    String name2 = groupOperations.newGroup().name("g2").create().get();
 
-    gApi.groups().id(name1).addMembers(user.fullName);
-    gApi.groups().id(name2).addMembers(admin.fullName);
+    gApi.groups().id(name1).addMembers(user.fullName());
+    gApi.groups().id(name2).addMembers(admin.fullName());
     gApi.groups().id(name1).addGroups(name2);
 
     this.g1 = gApi.groups().id(name1).detail();
@@ -218,7 +222,7 @@
   @Test
   public void cyclicSubgroup() throws Exception {
     updateGroupFile(RefNames.refsGroups(new AccountGroup.UUID(g1.id)), "subgroups", g1.id + "\n");
-    assertWarning("cyclic");
+    assertWarning("cycle");
   }
 
   private void assertError(String msg) 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 e2d390e..47ac7a9 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -28,8 +28,10 @@
 import static java.util.stream.Collectors.toList;
 
 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.GerritConfig;
@@ -40,6 +42,8 @@
 import com.google.gerrit.acceptance.Sandboxed;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
+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;
@@ -68,7 +72,6 @@
 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.Sequences;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.GroupIncludeCache;
 import com.google.gerrit.server.group.InternalGroup;
@@ -81,6 +84,9 @@
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
 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;
@@ -89,14 +95,13 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.Target;
 import java.sql.Timestamp;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.Collections;
 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;
 import org.eclipse.jgit.lib.CommitBuilder;
@@ -117,16 +122,18 @@
 
 @NoHttpd
 public class GroupsIT extends AbstractDaemonTest {
-  @Inject private Groups groups;
   @Inject @ServerInitiated private GroupsUpdate groupsUpdate;
+  @Inject private AccountOperations accountOperations;
+  @Inject private DynamicSet<GroupIndexedListener> groupIndexedListeners;
   @Inject private GroupIncludeCache groupIncludeCache;
-  @Inject private StalenessChecker stalenessChecker;
   @Inject private GroupIndexer groupIndexer;
+  @Inject private GroupOperations groupOperations;
+  @Inject private Groups groups;
   @Inject private GroupsConsistencyChecker consistencyChecker;
   @Inject private PeriodicGroupIndexer slaveGroupIndexer;
-  @Inject private DynamicSet<GroupIndexedListener> groupIndexedListeners;
+  @Inject private RequestScopeOperations requestScopeOperations;
   @Inject private Sequences seq;
-  @Inject private AccountOperations accountOperations;
+  @Inject private StalenessChecker stalenessChecker;
 
   @Before
   public void setTimeForTesting() {
@@ -173,12 +180,14 @@
 
   @Test
   public void addRemoveMember() throws Exception {
-    String g = createGroup("users");
-    gApi.groups().id(g).addMembers("user");
-    assertMembers(g, user);
+    AccountGroup.UUID group = groupOperations.newGroup().create();
 
-    gApi.groups().id(g).removeMembers("user");
-    assertNoMembers(g);
+    gApi.groups().id(group.get()).addMembers("user");
+    assertMembers(group.get(), user);
+
+    gApi.groups().id(group.get()).removeMembers("user");
+    ImmutableSet<Account.Id> members = groupOperations.group(group).get().members();
+    assertThat(members).isEmpty();
   }
 
   @Test
@@ -188,16 +197,15 @@
 
     // Fill the cache for the observed account.
     groupIncludeCache.getGroupsWithMember(accountId);
-    String groupName = createGroup("users");
-    AccountGroup.UUID groupUuid = new AccountGroup.UUID(gApi.groups().id(groupName).get().id);
+    AccountGroup.UUID groupUuid = groupOperations.newGroup().create();
 
-    gApi.groups().id(groupName).addMembers(username);
+    gApi.groups().id(groupUuid.get()).addMembers(username);
 
     Collection<AccountGroup.UUID> groupsWithMemberAfterAddition =
         groupIncludeCache.getGroupsWithMember(accountId);
     assertThat(groupsWithMemberAfterAddition).contains(groupUuid);
 
-    gApi.groups().id(groupName).removeMembers(username);
+    gApi.groups().id(groupUuid.get()).removeMembers(username);
 
     Collection<AccountGroup.UUID> groupsWithMemberAfterRemoval =
         groupIncludeCache.getGroupsWithMember(accountId);
@@ -229,16 +237,16 @@
 
   @Test
   public void addMultipleMembers() throws Exception {
-    String g = createGroup("users");
+    AccountGroup.UUID group = groupOperations.newGroup().create();
 
     String u1 = name("u1");
     accountOperations.newAccount().username(u1).create();
     String u2 = name("u2");
     accountOperations.newAccount().username(u2).create();
 
-    gApi.groups().id(g).addMembers(u1, u2);
+    gApi.groups().id(group.get()).addMembers(u1, u2);
 
-    List<AccountInfo> members = gApi.groups().id(g).members();
+    List<AccountInfo> members = gApi.groups().id(group.get()).members();
     assertThat(members)
         .comparingElementsUsing(getAccountToUsernameCorrespondence())
         .containsExactly(u1, u2);
@@ -246,13 +254,13 @@
 
   @Test
   public void membersWithAtSignInUsernameCanBeAdded() throws Exception {
-    String g = createGroup("users");
+    AccountGroup.UUID group = groupOperations.newGroup().create();
     String usernameWithAt = name("u1@something");
     accountOperations.newAccount().username(usernameWithAt).create();
 
-    gApi.groups().id(g).addMembers(usernameWithAt);
+    gApi.groups().id(group.get()).addMembers(usernameWithAt);
 
-    List<AccountInfo> members = gApi.groups().id(g).members();
+    List<AccountInfo> members = gApi.groups().id(group.get()).members();
     assertThat(members)
         .comparingElementsUsing(getAccountToUsernameCorrespondence())
         .containsExactly(usernameWithAt);
@@ -260,7 +268,7 @@
 
   @Test
   public void membersWithAtSignInUsernameAreNotConfusedWithSimilarUsernames() throws Exception {
-    String g = createGroup("users");
+    AccountGroup.UUID group = groupOperations.newGroup().create();
     String usernameWithAt = name("u1@something");
     accountOperations.newAccount().username(usernameWithAt).create();
     String usernameWithoutAt = name("u1something");
@@ -271,10 +279,10 @@
     accountOperations.newAccount().username(usernameOnlySuffix).create();
 
     gApi.groups()
-        .id(g)
+        .id(group.get())
         .addMembers(usernameWithAt, usernameWithoutAt, usernameOnlyPrefix, usernameOnlySuffix);
 
-    List<AccountInfo> members = gApi.groups().id(g).members();
+    List<AccountInfo> members = gApi.groups().id(group.get()).members();
     assertThat(members)
         .comparingElementsUsing(getAccountToUsernameCorrespondence())
         .containsExactly(usernameWithAt, usernameWithoutAt, usernameOnlyPrefix, usernameOnlySuffix);
@@ -282,52 +290,54 @@
 
   @Test
   public void includeRemoveGroup() throws Exception {
-    String p = createGroup("parent");
-    String g = createGroup("newGroup");
-    gApi.groups().id(p).addGroups(g);
-    assertIncludes(p, g);
+    AccountGroup.UUID parent = groupOperations.newGroup().create();
+    AccountGroup.UUID group = groupOperations.newGroup().create();
+    gApi.groups().id(parent.get()).addGroups(group.get());
+    assertThat(groupOperations.group(parent).get().subgroups()).containsExactly(group);
 
-    gApi.groups().id(p).removeGroups(g);
-    assertNoIncludes(p);
+    gApi.groups().id(parent.get()).removeGroups(group.get());
+    assertThat(groupOperations.group(parent).get().subgroups()).isEmpty();
   }
 
   @Test
   public void includeExternalGroup() throws Exception {
-    String g = createGroup("group");
+    AccountGroup.UUID group = groupOperations.newGroup().create();
     String subgroupUuid = SystemGroupBackend.REGISTERED_USERS.get();
-    gApi.groups().id(g).addGroups(subgroupUuid);
+    gApi.groups().id(group.get()).addGroups(subgroupUuid);
 
-    List<GroupInfo> subgroups = gApi.groups().id(g).includedGroups();
+    List<GroupInfo> subgroups = gApi.groups().id(group.get()).includedGroups();
     assertThat(subgroups).hasSize(1);
     assertThat(subgroups.get(0).id).isEqualTo(subgroupUuid.replace(":", "%3A"));
     assertThat(subgroups.get(0).name).isEqualTo("Registered Users");
     assertThat(subgroups.get(0).groupId).isNull();
 
-    List<? extends GroupAuditEventInfo> auditEvents = gApi.groups().id(g).auditLog();
+    List<? extends GroupAuditEventInfo> auditEvents = gApi.groups().id(group.get()).auditLog();
     assertThat(auditEvents).hasSize(1);
-    assertSubgroupAuditEvent(auditEvents.get(0), Type.ADD_GROUP, admin.id, "Registered Users");
+    assertSubgroupAuditEvent(auditEvents.get(0), Type.ADD_GROUP, admin.id(), "Registered Users");
   }
 
   @Test
   public void includeExistingGroup_OK() throws Exception {
-    String p = createGroup("parent");
-    String g = createGroup("newGroup");
-    gApi.groups().id(p).addGroups(g);
-    assertIncludes(p, g);
-    gApi.groups().id(p).addGroups(g);
-    assertIncludes(p, g);
+    AccountGroup.UUID parent = groupOperations.newGroup().create();
+    AccountGroup.UUID group = groupOperations.newGroup().create();
+    groupOperations.group(parent).forUpdate().addSubgroup(group);
+
+    gApi.groups().id(parent.get()).addGroups(group.get());
+
+    ImmutableSet<AccountGroup.UUID> subgroups = groupOperations.group(parent).get().subgroups();
+    assertThat(subgroups).containsExactly(group);
   }
 
   @Test
   public void addMultipleIncludes() throws Exception {
-    String p = createGroup("parent");
-    String g1 = createGroup("newGroup1");
-    String g2 = createGroup("newGroup2");
-    List<String> groups = new ArrayList<>();
-    groups.add(g1);
-    groups.add(g2);
-    gApi.groups().id(p).addGroups(g1, g2);
-    assertIncludes(p, g1, g2);
+    AccountGroup.UUID parent = groupOperations.newGroup().create();
+    AccountGroup.UUID group1 = groupOperations.newGroup().create();
+    AccountGroup.UUID group2 = groupOperations.newGroup().create();
+
+    gApi.groups().id(parent.get()).addGroups(group1.get(), group2.get());
+
+    ImmutableSet<AccountGroup.UUID> subgroups = groupOperations.group(parent).get().subgroups();
+    assertThat(subgroups).containsExactly(group1, group2);
   }
 
   @Test
@@ -403,7 +413,7 @@
 
   @Test
   public void createGroupWithoutCapability_Forbidden() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     gApi.groups().create(name("newGroup"));
   }
@@ -628,33 +638,39 @@
 
   @Test
   public void listEmptyGroupIncludes() throws Exception {
-    String gx = createGroup("gx");
-    assertThat(gApi.groups().id(gx).includedGroups()).isEmpty();
+    AccountGroup.UUID gx = groupOperations.newGroup().create();
+    assertThat(gApi.groups().id(gx.get()).includedGroups()).isEmpty();
   }
 
   @Test
   public void includeNonExistingGroup() throws Exception {
-    String gx = createGroup("gx");
+    AccountGroup.UUID gx = groupOperations.newGroup().create();
     exception.expect(UnprocessableEntityException.class);
-    gApi.groups().id(gx).addGroups("non-existing");
+    gApi.groups().id(gx.get()).addGroups("non-existing");
   }
 
   @Test
   public void listNonEmptyGroupIncludes() throws Exception {
-    String gx = createGroup("gx");
-    String gy = createGroup("gy");
-    String gz = createGroup("gz");
-    gApi.groups().id(gx).addGroups(gy);
-    gApi.groups().id(gx).addGroups(gz);
-    assertIncludes(gApi.groups().id(gx).includedGroups(), gy, gz);
+    AccountGroup.UUID gz = groupOperations.newGroup().create();
+    AccountGroup.UUID gy = groupOperations.newGroup().create();
+    AccountGroup.UUID gx = groupOperations.newGroup().subgroups(gy, gz).create();
+
+    List<GroupInfo> includes = gApi.groups().id(gx.get()).includedGroups();
+
+    String gyName = groupOperations.group(gy).get().name();
+    String gzName = groupOperations.group(gz).get().name();
+    assertIncludes(includes, gyName, gzName);
   }
 
   @Test
   public void listOneIncludeMember() throws Exception {
-    String gx = createGroup("gx");
-    String gy = createGroup("gy");
-    gApi.groups().id(gx).addGroups(gy);
-    assertIncludes(gApi.groups().id(gx).includedGroups(), gy);
+    AccountGroup.UUID gy = groupOperations.newGroup().create();
+    AccountGroup.UUID gx = groupOperations.newGroup().subgroups(gy).create();
+
+    List<GroupInfo> includes = gApi.groups().id(gx.get()).includedGroups();
+
+    String gyName = groupOperations.group(gy).get().name();
+    assertIncludes(includes, gyName);
   }
 
   @Test
@@ -665,109 +681,109 @@
 
   @Test
   public void listEmptyGroupMembers() throws Exception {
-    String group = createGroup("empty");
-    assertThat(gApi.groups().id(group).members()).isEmpty();
+    AccountGroup.UUID group = groupOperations.newGroup().create();
+    assertThat(gApi.groups().id(group.get()).members()).isEmpty();
   }
 
   @Test
   public void listNonEmptyGroupMembers() throws Exception {
-    String group = createGroup("group");
+    AccountGroup.UUID group = groupOperations.newGroup().create();
     String user1 = name("user1");
     accountOperations.newAccount().username(user1).create();
     String user2 = name("user2");
     accountOperations.newAccount().username(user2).create();
-    gApi.groups().id(group).addMembers(user1, user2);
+    gApi.groups().id(group.get()).addMembers(user1, user2);
 
-    assertMembers(gApi.groups().id(group).members(), user1, user2);
+    assertMembers(gApi.groups().id(group.get()).members(), user1, user2);
   }
 
   @Test
   public void listOneGroupMember() throws Exception {
-    String group = createGroup("group");
+    AccountGroup.UUID group = groupOperations.newGroup().create();
     String user = name("user1");
     accountOperations.newAccount().username(user).create();
-    gApi.groups().id(group).addMembers(user);
+    gApi.groups().id(group.get()).addMembers(user);
 
-    assertMembers(gApi.groups().id(group).members(), user);
+    assertMembers(gApi.groups().id(group.get()).members(), user);
   }
 
   @Test
   public void listGroupMembersRecursively() throws Exception {
-    String gx = createGroup("gx");
+    AccountGroup.UUID gx = groupOperations.newGroup().create();
     String ux = name("ux");
     accountOperations.newAccount().username(ux).create();
-    gApi.groups().id(gx).addMembers(ux);
+    gApi.groups().id(gx.get()).addMembers(ux);
 
-    String gy = createGroup("gy");
+    AccountGroup.UUID gy = groupOperations.newGroup().create();
     String uy = name("uy");
     accountOperations.newAccount().username(uy).create();
-    gApi.groups().id(gy).addMembers(uy);
+    gApi.groups().id(gy.get()).addMembers(uy);
 
-    String gz = createGroup("gz");
+    AccountGroup.UUID gz = groupOperations.newGroup().create();
     String uz = name("uz");
     accountOperations.newAccount().username(uz).create();
-    gApi.groups().id(gz).addMembers(uz);
+    gApi.groups().id(gz.get()).addMembers(uz);
 
-    gApi.groups().id(gx).addGroups(gy);
-    gApi.groups().id(gy).addGroups(gz);
-    assertMembers(gApi.groups().id(gx).members(), ux);
-    assertMembers(gApi.groups().id(gx).members(true), ux, uy, uz);
+    gApi.groups().id(gx.get()).addGroups(gy.get());
+    gApi.groups().id(gy.get()).addGroups(gz.get());
+    assertMembers(gApi.groups().id(gx.get()).members(), ux);
+    assertMembers(gApi.groups().id(gx.get()).members(true), ux, uy, uz);
   }
 
   @Test
   public void usersSeeTheirDirectMembershipWhenListingMembersRecursively() throws Exception {
-    String group = createGroup("group");
-    gApi.groups().id(group).addMembers(user.username);
+    AccountGroup.UUID group = groupOperations.newGroup().create();
+    gApi.groups().id(group.get()).addMembers(user.username());
 
-    setApiUser(user);
-    assertMembers(gApi.groups().id(group).members(true), user.fullName);
+    requestScopeOperations.setApiUser(user.id());
+    assertMembers(gApi.groups().id(group.get()).members(true), user.fullName());
   }
 
   @Test
   public void usersDoNotSeeTheirIndirectMembershipWhenListingMembersRecursively() throws Exception {
-    String group1 = createGroup("group1");
-    String group2 = createGroup("group2");
-    gApi.groups().id(group1).addGroups(group2);
-    gApi.groups().id(group2).addMembers(user.username);
+    AccountGroup.UUID group1 = groupOperations.newGroup().ownerGroupUuid(adminGroupUuid()).create();
+    AccountGroup.UUID group2 = groupOperations.newGroup().ownerGroupUuid(adminGroupUuid()).create();
+    gApi.groups().id(group1.get()).addGroups(group2.get());
+    gApi.groups().id(group2.get()).addMembers(user.username());
 
-    setApiUser(user);
-    List<AccountInfo> listedMembers = gApi.groups().id(group1).members(true);
+    requestScopeOperations.setApiUser(user.id());
+    List<AccountInfo> listedMembers = gApi.groups().id(group1.get()).members(true);
 
     assertMembers(listedMembers);
   }
 
   @Test
   public void adminsSeeTheirIndirectMembershipWhenListingMembersRecursively() throws Exception {
-    String ownerGroup = createGroup("ownerGroup", null);
-    String group1 = createGroup("group1", ownerGroup);
-    String group2 = createGroup("group2", ownerGroup);
-    gApi.groups().id(group1).addGroups(group2);
-    gApi.groups().id(group2).addMembers(admin.username);
+    AccountGroup.UUID ownerGroup = groupOperations.newGroup().create();
+    AccountGroup.UUID group1 = groupOperations.newGroup().ownerGroupUuid(ownerGroup).create();
+    AccountGroup.UUID group2 = groupOperations.newGroup().ownerGroupUuid(ownerGroup).create();
+    gApi.groups().id(group1.get()).addGroups(group2.get());
+    gApi.groups().id(group2.get()).addMembers(admin.username());
 
-    List<AccountInfo> listedMembers = gApi.groups().id(group1).members(true);
+    List<AccountInfo> listedMembers = gApi.groups().id(group1.get()).members(true);
 
-    assertMembers(listedMembers, admin.fullName);
+    assertMembers(listedMembers, admin.fullName());
   }
 
   @Test
   public void ownersSeeTheirIndirectMembershipWhenListingMembersRecursively() throws Exception {
-    String ownerGroup = createGroup("ownerGroup", null);
-    String group1 = createGroup("group1", ownerGroup);
-    String group2 = createGroup("group2", ownerGroup);
-    gApi.groups().id(group1).addGroups(group2);
-    gApi.groups().id(ownerGroup).addMembers(user.username);
-    gApi.groups().id(group2).addMembers(user.username);
+    AccountGroup.UUID ownerGroup = groupOperations.newGroup().create();
+    AccountGroup.UUID group1 = groupOperations.newGroup().ownerGroupUuid(ownerGroup).create();
+    AccountGroup.UUID group2 = groupOperations.newGroup().ownerGroupUuid(ownerGroup).create();
+    gApi.groups().id(group1.get()).addGroups(group2.get());
+    gApi.groups().id(ownerGroup.get()).addMembers(user.username());
+    gApi.groups().id(group2.get()).addMembers(user.username());
 
-    setApiUser(user);
-    List<AccountInfo> listedMembers = gApi.groups().id(group1).members(true);
+    requestScopeOperations.setApiUser(user.id());
+    List<AccountInfo> listedMembers = gApi.groups().id(group1.get()).members(true);
 
-    assertMembers(listedMembers, user.fullName);
+    assertMembers(listedMembers, user.fullName());
   }
 
   @Test
   public void defaultGroupsCreated() throws Exception {
     Iterable<String> names = gApi.groups().list().getAsMap().keySet();
-    assertThat(names).containsAllOf("Administrators", "Non-Interactive Users").inOrder();
+    assertThat(names).containsAtLeast("Administrators", "Non-Interactive Users").inOrder();
   }
 
   @Test
@@ -782,18 +798,21 @@
 
   @Test
   public void getGroupsByOwner() throws Exception {
-    String parent = createGroup("test-parent");
-    List<String> children =
-        Arrays.asList(createGroup("test-child1", parent), createGroup("test-child2", parent));
+    AccountGroup.UUID parent = groupOperations.newGroup().ownerGroupUuid(adminGroupUuid()).create();
+    List<AccountGroup.UUID> children =
+        Arrays.asList(
+            groupOperations.newGroup().ownerGroupUuid(parent).create(),
+            groupOperations.newGroup().ownerGroupUuid(parent).create());
 
     // By UUID
-    List<GroupInfo> owned = gApi.groups().list().withOwnedBy(groupUuid(parent).get()).get();
-    assertThat(owned.stream().map(g -> g.name).collect(toList()))
+    List<GroupInfo> owned = gApi.groups().list().withOwnedBy(parent.get()).get();
+    assertThat(owned.stream().map(g -> new AccountGroup.UUID(g.id)).collect(toList()))
         .containsExactlyElementsIn(children);
 
     // By name
-    owned = gApi.groups().list().withOwnedBy(parent).get();
-    assertThat(owned.stream().map(g -> g.name).collect(toList()))
+    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()))
         .containsExactlyElementsIn(children);
 
     // By group that does not own any others
@@ -816,13 +835,13 @@
     in.ownerId = adminGroupUuid().get();
     gApi.groups().create(in);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     assertThat(gApi.groups().list().getAsMap()).doesNotContainKey(newGroupName);
 
-    setApiUser(admin);
-    gApi.groups().id(newGroupName).addMembers(user.username);
+    requestScopeOperations.setApiUser(admin.id());
+    gApi.groups().id(newGroupName).addMembers(user.username());
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     assertThat(gApi.groups().list().getAsMap()).containsKey(newGroupName);
   }
 
@@ -895,41 +914,41 @@
     GroupApi g = gApi.groups().create(name("group"));
     List<? extends GroupAuditEventInfo> auditEvents = g.auditLog();
     assertThat(auditEvents).hasSize(1);
-    assertMemberAuditEvent(auditEvents.get(0), Type.ADD_USER, admin.id, admin.id);
+    assertMemberAuditEvent(auditEvents.get(0), Type.ADD_USER, admin.id(), admin.id());
 
-    g.addMembers(user.username);
+    g.addMembers(user.username());
     auditEvents = g.auditLog();
     assertThat(auditEvents).hasSize(2);
-    assertMemberAuditEvent(auditEvents.get(0), Type.ADD_USER, admin.id, user.id);
+    assertMemberAuditEvent(auditEvents.get(0), Type.ADD_USER, admin.id(), user.id());
 
-    g.removeMembers(user.username);
+    g.removeMembers(user.username());
     auditEvents = g.auditLog();
     assertThat(auditEvents).hasSize(3);
-    assertMemberAuditEvent(auditEvents.get(0), Type.REMOVE_USER, admin.id, user.id);
+    assertMemberAuditEvent(auditEvents.get(0), Type.REMOVE_USER, admin.id(), user.id());
 
     String otherGroup = name("otherGroup");
     gApi.groups().create(otherGroup);
     g.addGroups(otherGroup);
     auditEvents = g.auditLog();
     assertThat(auditEvents).hasSize(4);
-    assertSubgroupAuditEvent(auditEvents.get(0), Type.ADD_GROUP, admin.id, otherGroup);
+    assertSubgroupAuditEvent(auditEvents.get(0), Type.ADD_GROUP, admin.id(), otherGroup);
 
     g.removeGroups(otherGroup);
     auditEvents = g.auditLog();
     assertThat(auditEvents).hasSize(5);
-    assertSubgroupAuditEvent(auditEvents.get(0), Type.REMOVE_GROUP, admin.id, otherGroup);
+    assertSubgroupAuditEvent(auditEvents.get(0), Type.REMOVE_GROUP, admin.id(), otherGroup);
 
     // Add a removed member back again.
-    g.addMembers(user.username);
+    g.addMembers(user.username());
     auditEvents = g.auditLog();
     assertThat(auditEvents).hasSize(6);
-    assertMemberAuditEvent(auditEvents.get(0), Type.ADD_USER, admin.id, user.id);
+    assertMemberAuditEvent(auditEvents.get(0), Type.ADD_USER, admin.id(), user.id());
 
     // Add a removed group back again.
     g.addGroups(otherGroup);
     auditEvents = g.auditLog();
     assertThat(auditEvents).hasSize(7);
-    assertSubgroupAuditEvent(auditEvents.get(0), Type.ADD_GROUP, admin.id, otherGroup);
+    assertSubgroupAuditEvent(auditEvents.get(0), Type.ADD_GROUP, admin.id(), otherGroup);
 
     Timestamp lastDate = null;
     for (GroupAuditEventInfo auditEvent : auditEvents) {
@@ -941,8 +960,8 @@
   }
 
   /**
-   * @Sandboxed is used by this test because it deletes a group reference which introduces an
-   * inconsistency for the group storage. Once group deletion is supported, this test should be
+   * {@code @Sandboxed} is used by this test because it deletes a group reference which introduces
+   * an inconsistency for the group storage. Once group deletion is supported, this test should be
    * updated to use the API instead.
    */
   @Test
@@ -961,7 +980,7 @@
     List<? extends GroupAuditEventInfo> auditEvents = gApi.groups().id(parentGroup.id).auditLog();
     assertThat(auditEvents).hasSize(2);
     // Verify the unavailable subgroup's name is null.
-    assertSubgroupAuditEvent(auditEvents.get(0), Type.ADD_GROUP, admin.id, null);
+    assertSubgroupAuditEvent(auditEvents.get(0), Type.ADD_GROUP, admin.id(), null);
   }
 
   private void deleteGroupRef(String groupId) throws Exception {
@@ -990,21 +1009,20 @@
     TestAccount groupOwner = accountCreator.user2();
     GroupInput in = new GroupInput();
     in.name = name("group");
-    in.members =
-        Collections.singleton(groupOwner).stream().map(u -> u.id.toString()).collect(toList());
+    in.members = Stream.of(groupOwner).map(u -> u.id().toString()).collect(toList());
     in.visibleToAll = true;
     GroupInfo group = gApi.groups().create(in).get();
 
     // admin can reindex any group
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     gApi.groups().id(group.id).index();
 
     // group owner can reindex own group (group is owned by itself)
-    setApiUser(groupOwner);
+    requestScopeOperations.setApiUser(groupOwner.id());
     gApi.groups().id(group.id).index();
 
     // user cannot reindex any group
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("not allowed to index group");
     gApi.groups().id(group.id).index();
@@ -1034,7 +1052,7 @@
 
   @Test
   public void pushToGroupsBranchForNonAllUsersRepo() throws Exception {
-    assertCreateGroupBranch(project, null);
+    assertCreateGroupBranch(project);
     String groupRef =
         RefNames.refsGroups(new AccountGroup.UUID(gApi.groups().create(name("foo")).get().id));
     createBranch(project, groupRef);
@@ -1043,7 +1061,7 @@
 
   @Test
   public void pushToDeletedGroupsBranchForNonAllUsersRepo() throws Exception {
-    assertCreateGroupBranch(project, null);
+    assertCreateGroupBranch(project);
     String groupRef =
         RefNames.refsDeletedGroups(
             new AccountGroup.UUID(gApi.groups().create(name("foo")).get().id));
@@ -1059,11 +1077,15 @@
 
   private void assertPushToGroupBranch(
       Project.NameKey project, String groupRefName, String expectedErrorOnUpdate) throws Exception {
-    grant(project, RefNames.REFS_GROUPS + "*", Permission.CREATE, false, REGISTERED_USERS);
-    grant(project, RefNames.REFS_GROUPS + "*", Permission.PUSH, false, REGISTERED_USERS);
-    grant(project, RefNames.REFS_DELETED_GROUPS + "*", Permission.CREATE, false, REGISTERED_USERS);
-    grant(project, RefNames.REFS_DELETED_GROUPS + "*", Permission.PUSH, false, REGISTERED_USERS);
-    grant(project, RefNames.REFS_GROUPNAMES, Permission.PUSH, false, REGISTERED_USERS);
+    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();
+    }
 
     TestRepository<InMemoryRepository> repo = cloneProject(project);
 
@@ -1072,7 +1094,7 @@
     repo.reset("groupRef");
     PushOneCommit.Result r =
         pushFactory
-            .create(db, admin.getIdent(), repo, "Update group", "arbitraryFile.txt", "some content")
+            .create(admin.newIdent(), repo, "Update group", "arbitraryFile.txt", "some content")
             .to(groupRefName);
     if (expectedErrorOnUpdate != null) {
       r.assertErrorStatus(expectedErrorOnUpdate);
@@ -1081,21 +1103,20 @@
     }
   }
 
-  private void assertCreateGroupBranch(Project.NameKey project, String expectedErrorOnCreate)
-      throws Exception {
-    grant(project, RefNames.REFS_GROUPS + "*", Permission.CREATE, false, REGISTERED_USERS);
-    grant(project, RefNames.REFS_GROUPS + "*", Permission.PUSH, false, REGISTERED_USERS);
+  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();
+    }
     TestRepository<InMemoryRepository> repo = cloneProject(project);
     PushOneCommit.Result r =
         pushFactory
-            .create(db, admin.getIdent(), repo, "Update group", "arbitraryFile.txt", "some content")
+            .create(admin.newIdent(), repo, "Update group", "arbitraryFile.txt", "some content")
             .setParents(ImmutableList.of())
             .to(RefNames.REFS_GROUPS + name("bar"));
-    if (expectedErrorOnCreate != null) {
-      r.assertErrorStatus(expectedErrorOnCreate);
-    } else {
-      r.assertOkStatus();
-    }
+    r.assertOkStatus();
   }
 
   @Test
@@ -1130,7 +1151,7 @@
 
     PushOneCommit.Result r =
         pushFactory
-            .create(db, admin.getIdent(), repo, "Subject", "project.config", config)
+            .create(admin.newIdent(), repo, "Subject", "project.config", config)
             .to(RefNames.REFS_CONFIG);
     r.assertErrorStatus("invalid project configuration");
     r.assertMessage("All-Users must inherit from All-Projects");
@@ -1180,7 +1201,7 @@
     grant(allUsers, refPattern, Permission.PUSH);
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    PushOneCommit.Result r = pushFactory.create(db, admin.getIdent(), allUsersRepo).to(groupRef);
+    PushOneCommit.Result r = pushFactory.create(admin.newIdent(), allUsersRepo).to(groupRef);
     r.assertErrorStatus();
     assertThat(r.getMessage()).contains("Not allowed to create group branch.");
 
@@ -1279,12 +1300,12 @@
   public void groupsOfUserCanBeListedInSlaveMode() throws Exception {
     GroupInput groupInput = new GroupInput();
     groupInput.name = name("contributors");
-    groupInput.members = ImmutableList.of(user.username);
+    groupInput.members = ImmutableList.of(user.username());
     gApi.groups().create(groupInput).get();
     restartAsSlave();
 
-    setApiUser(user);
-    List<GroupInfo> groups = gApi.groups().list().withUser(user.username).get();
+    requestScopeOperations.setApiUser(user.id());
+    List<GroupInfo> groups = gApi.groups().list().withUser(user.username()).get();
     ImmutableList<String> groupNames =
         groups.stream().map(group -> group.name).collect(toImmutableList());
     assertThat(groupNames).contains(groupInput.name);
@@ -1373,18 +1394,15 @@
   }
 
   private static Correspondence<AccountInfo, String> getAccountToUsernameCorrespondence() {
-    return new Correspondence<AccountInfo, String>() {
-      @Override
-      public boolean compare(AccountInfo actualAccount, String expectedName) {
-        String username = actualAccount == null ? null : actualAccount.username;
-        return Objects.equals(username, expectedName);
-      }
-
-      @Override
-      public String toString() {
-        return "has username";
-      }
-    };
+    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);
+          }
+        },
+        "has username");
   }
 
   private void assertStaleGroupAndReindex(AccountGroup.UUID groupUuid) throws IOException {
@@ -1409,8 +1427,7 @@
 
     PushOneCommit.Result r =
         pushFactory
-            .create(
-                db, admin.getIdent(), repo, "Update group config", "group.config", "some content")
+            .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);
@@ -1484,7 +1501,7 @@
   private void assertMembers(String group, TestAccount... expectedMembers) throws Exception {
     assertMembers(
         gApi.groups().id(group).members(),
-        TestAccount.names(expectedMembers).stream().toArray(String[]::new));
+        TestAccount.names(expectedMembers).toArray(new String[0]));
     assertAccountInfos(Arrays.asList(expectedMembers), gApi.groups().id(group).members());
   }
 
@@ -1494,22 +1511,10 @@
         .inOrder();
   }
 
-  private void assertNoMembers(String group) throws Exception {
-    assertThat(gApi.groups().id(group).members()).isEmpty();
-  }
-
-  private void assertIncludes(String group, String... expectedNames) throws Exception {
-    assertIncludes(gApi.groups().id(group).includedGroups(), expectedNames);
-  }
-
-  private static void assertIncludes(Iterable<GroupInfo> includes, String... expectedNames) {
-    assertThat(Iterables.transform(includes, i -> i.name))
-        .containsExactlyElementsIn(Arrays.asList(expectedNames))
-        .inOrder();
-  }
-
-  private void assertNoIncludes(String group) throws Exception {
-    assertThat(gApi.groups().id(group).includedGroups()).isEmpty();
+  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();
   }
 
   private void assertBadRequest(ListRequest req) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsUpdateIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsUpdateIT.java
index 7056312..5e143c0 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsUpdateIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsUpdateIT.java
@@ -18,7 +18,8 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+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;
@@ -27,7 +28,6 @@
 import com.google.gerrit.server.group.db.InternalGroupCreation;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
 import com.google.gerrit.testing.InMemoryTestEnvironment;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -92,7 +92,7 @@
   }
 
   private void createGroup(InternalGroupCreation groupCreation, InternalGroupUpdate groupUpdate)
-      throws OrmException, IOException, ConfigInvalidException {
+      throws IOException, ConfigInvalidException {
     groupsUpdateProvider.get().createGroup(groupCreation, groupUpdate);
   }
 
@@ -138,7 +138,7 @@
       InternalGroupUpdate groupUpdate = InternalGroupUpdate.builder().build();
       try {
         groupsUpdateProvider.get().createGroup(groupCreation, groupUpdate);
-      } catch (OrmException | IOException | ConfigInvalidException e) {
+      } catch (StorageException | IOException | ConfigInvalidException e) {
         throw new IllegalStateException(e);
       }
     }
diff --git a/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java b/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java
index 27f737f..1d3eb17 100644
--- a/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.RawInputUtil;
 import com.google.gerrit.extensions.api.plugins.InstallPluginInput;
 import com.google.gerrit.extensions.api.plugins.PluginApi;
@@ -33,6 +34,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.inject.Inject;
 import java.util.List;
 import org.junit.Test;
 
@@ -49,6 +51,8 @@
       ImmutableList.of(
           "plugin-a.js", "plugin-b.html", "plugin-c.js", "plugin-d.html", "plugin_e.js");
 
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @Test
   @GerritConfig(name = "plugins.allowRemoteAdmin", value = "true")
   public void pluginManagement() throws Exception {
@@ -112,7 +116,7 @@
     deprecatedInput();
 
     // Non-admin cannot disable
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     try {
       gApi.plugins().name("plugin-a").disable();
       fail("Expected AuthException");
diff --git a/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java b/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
index e4194a3..8cdd2f66 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.acceptance.RestResponse;
 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.common.data.Permission;
 import com.google.gerrit.extensions.api.config.AccessCheckInfo;
 import com.google.gerrit.extensions.api.config.AccessCheckInput;
@@ -31,6 +32,8 @@
 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;
@@ -40,7 +43,7 @@
 import org.junit.Test;
 
 public class CheckAccessIT extends AbstractDaemonTest {
-
+  @Inject private ProjectOperations projectOperations;
   @Inject private GroupOperations groupOperations;
 
   private Project.NameKey normalProject;
@@ -50,35 +53,38 @@
 
   @Before
   public void setUp() throws Exception {
-    normalProject = createProject("normal");
-    secretProject = createProject("secret");
-    secretRefProject = createProject("secretRef");
+    normalProject = projectOperations.newProject().create();
+    secretProject = projectOperations.newProject().create();
+    secretRefProject = projectOperations.newProject().create();
     AccountGroup.UUID privilegedGroupUuid =
         groupOperations.newGroup().name(name("privilegedGroup")).create();
 
     privilegedUser = accountCreator.create("privilegedUser", "snowden@nsa.gov", "Ed Snowden");
-    groupOperations.group(privilegedGroupUuid).forUpdate().addMember(privilegedUser.id).update();
+    groupOperations.group(privilegedGroupUuid).forUpdate().addMember(privilegedUser.id()).update();
 
-    grant(secretProject, "refs/*", Permission.READ, false, privilegedGroupUuid);
-    block(secretProject, "refs/*", Permission.READ, SystemGroupBackend.REGISTERED_USERS);
+    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();
+    }
 
-    deny(secretRefProject, "refs/*", Permission.READ, SystemGroupBackend.ANONYMOUS_USERS);
-    grant(secretRefProject, "refs/heads/secret/*", Permission.READ, false, privilegedGroupUuid);
-    block(
-        secretRefProject,
-        "refs/heads/secret/*",
-        Permission.READ,
-        SystemGroupBackend.REGISTERED_USERS);
-    grant(
-        secretRefProject,
-        "refs/heads/*",
-        Permission.READ,
-        false,
-        SystemGroupBackend.REGISTERED_USERS);
+    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();
+    }
 
     // Ref permission
-    grant(normalProject, "refs/*", Permission.VIEW_PRIVATE_CHANGES, false, privilegedGroupUuid);
-    grant(normalProject, "refs/*", Permission.FORGE_SERVER, false, privilegedGroupUuid);
+    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();
+    }
   }
 
   @Test
@@ -91,7 +97,7 @@
   @Test
   public void nonexistentPermission() throws Exception {
     AccessCheckInput in = new AccessCheckInput();
-    in.account = user.email;
+    in.account = user.email();
     in.permission = "notapermission";
     in.ref = "refs/heads/master";
 
@@ -103,7 +109,7 @@
   @Test
   public void permissionLacksRef() throws Exception {
     AccessCheckInput in = new AccessCheckInput();
-    in.account = user.email;
+    in.account = user.email();
     in.permission = "forge_author";
 
     exception.expect(BadRequestException.class);
@@ -114,7 +120,7 @@
   @Test
   public void changePermission() throws Exception {
     AccessCheckInput in = new AccessCheckInput();
-    in.account = user.email;
+    in.account = user.email();
     in.permission = "rebase";
     in.ref = "refs/heads/master";
 
@@ -131,7 +137,7 @@
     in.ref = "refs/heads/master";
 
     exception.expect(UnprocessableEntityException.class);
-    exception.expectMessage("cannot find account doesnotexist@invalid.com");
+    exception.expectMessage("Account 'doesnotexist@invalid.com' not found");
     gApi.projects().name(normalProject.get()).checkAccess(in);
   }
 
@@ -181,7 +187,7 @@
                 + normalProject.get()
                 + "/check.access"
                 + "?ref=refs/heads/master&perm=viewPrivateChanges&account="
-                + user.email);
+                + user.email());
     rep.assertOK();
     assertThat(rep.getEntityContent()).contains("403");
   }
@@ -191,28 +197,28 @@
     List<TestCase> inputs =
         ImmutableList.of(
             TestCase.projectRefPerm(
-                user.email,
+                user.email(),
                 normalProject.get(),
                 "refs/heads/master",
                 Permission.VIEW_PRIVATE_CHANGES,
                 403),
-            TestCase.project(user.email, normalProject.get(), 200),
-            TestCase.project(user.email, secretProject.get(), 403),
+            TestCase.project(user.email(), normalProject.get(), 200),
+            TestCase.project(user.email(), secretProject.get(), 403),
             TestCase.projectRef(
-                user.email, secretRefProject.get(), "refs/heads/secret/master", 403),
+                user.email(), secretRefProject.get(), "refs/heads/secret/master", 403),
             TestCase.projectRef(
-                privilegedUser.email, secretRefProject.get(), "refs/heads/secret/master", 200),
-            TestCase.projectRef(privilegedUser.email, normalProject.get(), null, 200),
-            TestCase.projectRef(privilegedUser.email, secretProject.get(), null, 200),
-            TestCase.projectRef(privilegedUser.email, secretProject.get(), null, 200),
+                privilegedUser.email(), secretRefProject.get(), "refs/heads/secret/master", 200),
+            TestCase.projectRef(privilegedUser.email(), normalProject.get(), null, 200),
+            TestCase.projectRef(privilegedUser.email(), secretProject.get(), null, 200),
+            TestCase.projectRef(privilegedUser.email(), secretProject.get(), null, 200),
             TestCase.projectRefPerm(
-                privilegedUser.email,
+                privilegedUser.email(),
                 normalProject.get(),
                 "refs/heads/master",
                 Permission.VIEW_PRIVATE_CHANGES,
                 200),
             TestCase.projectRefPerm(
-                privilegedUser.email,
+                privilegedUser.email(),
                 normalProject.get(),
                 "refs/heads/master",
                 Permission.FORGE_SERVER,
@@ -260,7 +266,7 @@
       assertThat(u.delete()).isEqualTo(Result.FORCED);
     }
     AccessCheckInput input = new AccessCheckInput();
-    input.account = privilegedUser.email;
+    input.account = privilegedUser.email();
 
     AccessCheckInfo info = gApi.projects().name(normalProject.get()).checkAccess(input);
     assertThat(info.status).isEqualTo(200);
diff --git a/javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java b/javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java
index 3b2df79..388ea30 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java
@@ -280,8 +280,8 @@
             .branch("HEAD")
             .commit()
             .message("A change")
-            .author(admin.getIdent())
-            .committer(new PersonIdent(admin.getIdent(), testRepo.getDate()))
+            .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 39d630a..a341b3c 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java
@@ -145,7 +145,7 @@
   }
 
   private static void assertPerson(GitPerson actual, TestAccount expected) {
-    assertThat(actual.email).isEqualTo(expected.email);
-    assertThat(actual.name).isEqualTo(expected.fullName);
+    assertThat(actual.email).isEqualTo(expected.email());
+    assertThat(actual.name).isEqualTo(expected.fullName());
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/project/DashboardIT.java b/javatests/com/google/gerrit/acceptance/api/project/DashboardIT.java
index e51a069..f597392 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/DashboardIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/DashboardIT.java
@@ -192,9 +192,9 @@
         throw e;
       }
     }
-    try (Repository r = repoManager.openRepository(project)) {
-      TestRepository<Repository>.CommitBuilder cb =
-          new TestRepository<>(r).branch(canonicalRef).commit();
+    try (Repository r = repoManager.openRepository(project);
+        TestRepository<Repository> tr = new TestRepository<>(r)) {
+      TestRepository<Repository>.CommitBuilder cb = tr.branch(canonicalRef).commit();
       StringBuilder content = new StringBuilder("[dashboard]\n");
       if (info.title != null) {
         content.append("title = ").append(info.title).append("\n");
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
index 9fcbaa7..473b04b 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
@@ -31,15 +31,22 @@
 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.Nullable;
 import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.api.projects.CommentLinkInfo;
 import com.google.gerrit.extensions.api.projects.ConfigInfo;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
+import com.google.gerrit.extensions.api.projects.ConfigValue;
 import com.google.gerrit.extensions.api.projects.DescriptionInput;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 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.ProjectIndexedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
@@ -49,9 +56,15 @@
 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.index.IndexExecutor;
+import com.google.gerrit.server.project.CommentLinkInfoImpl;
+import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
+import com.google.inject.Module;
+import java.util.HashMap;
+import java.util.Map;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.PushResult;
 import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
@@ -61,7 +74,16 @@
 
 @NoHttpd
 public class ProjectIT extends AbstractDaemonTest {
+  private static final String BUGZILLA = "bugzilla";
+  private static final String BUGZILLA_LINK = "http://bugzilla.example.com/?id=$2";
+  private static final String BUGZILLA_MATCH = "(bug\\\\s+#?)(\\\\d+)";
+  private static final String JIRA = "jira";
+  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;
 
   @Inject
   @IndexExecutor(BATCH)
@@ -70,6 +92,18 @@
   private ProjectIndexedCounter projectIndexedCounter;
   private RegistrationHandle projectIndexedCounterHandle;
 
+  @Override
+  public Module createModule() {
+    return new AbstractModule() {
+      @Override
+      protected void configure() {
+        bind(ProjectConfigEntry.class)
+            .annotatedWith(Exports.named("test-plugin-key"))
+            .toInstance(new ProjectConfigEntry("Test Plugin Config Item", true));
+      }
+    };
+  }
+
   @Before
   public void addProjectIndexedCounter() {
     projectIndexedCounter = new ProjectIndexedCounter();
@@ -146,6 +180,17 @@
   }
 
   @Test
+  public void createProjectWithPluginConfigs() throws Exception {
+    String name = name("foo");
+    ProjectInput input = new ProjectInput();
+    input.name = name;
+    input.description = "foo description";
+    input.pluginConfigValues = newPluginConfigValues();
+    ProjectInfo info = gApi.projects().create(input).get();
+    assertThat(info.description).isEqualTo(input.description);
+  }
+
+  @Test
   public void createProjectWithMismatchedInput() throws Exception {
     ProjectInput in = new ProjectInput();
     in.name = name("foo");
@@ -206,14 +251,14 @@
 
   @Test
   public void createAndDeleteBranch() throws Exception {
-    assertThat(getRemoteHead(project.get(), "foo")).isNull();
+    assertThat(hasHead(project, "foo")).isFalse();
 
     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(getRemoteHead(project.get(), "foo")).isNull();
+    assertThat(hasHead(project, "foo")).isFalse();
     projectIndexedCounter.assertNoReindex();
   }
 
@@ -222,7 +267,7 @@
     grant(project, "refs/*", Permission.PUSH, true);
     projectIndexedCounter.clear();
 
-    assertThat(getRemoteHead(project.get(), "foo")).isNull();
+    assertThat(hasHead(project, "foo")).isFalse();
 
     PushOneCommit.Result r = pushTo("refs/heads/foo");
     r.assertOkStatus();
@@ -231,7 +276,7 @@
 
     PushResult r2 = GitUtil.pushOne(testRepo, null, "refs/heads/foo", false, true, null);
     assertThat(r2.getRemoteUpdate("refs/heads/foo").getStatus()).isEqualTo(Status.OK);
-    assertThat(getRemoteHead(project.get(), "foo")).isNull();
+    assertThat(hasHead(project, "foo")).isFalse();
     projectIndexedCounter.assertNoReindex();
   }
 
@@ -337,7 +382,7 @@
   @Test
   public void nonOwnerCannotSetConfig() throws Exception {
     ConfigInput input = createTestConfigInput();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("write refs/meta/config not permitted");
     gApi.projects().name(project.get()).config(input);
@@ -372,7 +417,7 @@
   @Test
   public void setHeadNotAllowed() throws Exception {
     gApi.projects().name(project.get()).branch("test").create(new BranchInput());
-    setApiUser(user);
+    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");
@@ -418,7 +463,7 @@
 
   @Test
   public void reindexProject() throws Exception {
-    createProject("child", project);
+    projectOperations.newProject().parent(project).create();
     projectIndexedCounter.clear();
 
     gApi.projects().name(allProjects.get()).index(false);
@@ -427,8 +472,8 @@
 
   @Test
   public void reindexProjectWithChildren() throws Exception {
-    Project.NameKey middle = createProject("middle", project);
-    Project.NameKey leave = createProject("leave", middle);
+    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);
@@ -462,7 +507,7 @@
   @Test
   @GerritConfig(name = "receive.inheritProjectMaxObjectSizeLimit", value = "true")
   public void maxObjectSizeIsInheritedFromParentProject() throws Exception {
-    Project.NameKey child = createProject(name("child"), project);
+    Project.NameKey child = projectOperations.newProject().parent(project).create();
 
     ConfigInfo info = setMaxObjectSize("100k");
     assertThat(info.maxObjectSizeLimit.value).isEqualTo("102400");
@@ -478,7 +523,7 @@
 
   @Test
   public void maxObjectSizeIsNotInheritedFromParentProject() throws Exception {
-    Project.NameKey child = createProject(name("child"), project);
+    Project.NameKey child = projectOperations.newProject().parent(project).create();
 
     ConfigInfo info = setMaxObjectSize("100k");
     assertThat(info.maxObjectSizeLimit.value).isEqualTo("102400");
@@ -493,7 +538,7 @@
 
   @Test
   public void maxObjectSizeOverridesParentProjectWhenNotSetOnParent() throws Exception {
-    Project.NameKey child = createProject(name("child"), project);
+    Project.NameKey child = projectOperations.newProject().parent(project).create();
 
     ConfigInfo info = setMaxObjectSize("0");
     assertThat(info.maxObjectSizeLimit.value).isNull();
@@ -508,7 +553,7 @@
 
   @Test
   public void maxObjectSizeOverridesParentProjectWhenLower() throws Exception {
-    Project.NameKey child = createProject(name("child"), project);
+    Project.NameKey child = projectOperations.newProject().parent(project).create();
 
     ConfigInfo info = setMaxObjectSize("200k");
     assertThat(info.maxObjectSizeLimit.value).isEqualTo("204800");
@@ -524,7 +569,7 @@
   @Test
   @GerritConfig(name = "receive.inheritProjectMaxObjectSizeLimit", value = "true")
   public void maxObjectSizeDoesNotOverrideParentProjectWhenHigher() throws Exception {
-    Project.NameKey child = createProject(name("child"), project);
+    Project.NameKey child = projectOperations.newProject().parent(project).create();
 
     ConfigInfo info = setMaxObjectSize("100k");
     assertThat(info.maxObjectSizeLimit.value).isEqualTo("102400");
@@ -541,7 +586,7 @@
   @Test
   @GerritConfig(name = "receive.maxObjectSizeLimit", value = "200k")
   public void maxObjectSizeIsInheritedFromGlobalConfig() throws Exception {
-    Project.NameKey child = createProject(name("child"), project);
+    Project.NameKey child = projectOperations.newProject().parent(project).create();
 
     ConfigInfo info = getConfig();
     assertThat(info.maxObjectSizeLimit.value).isEqualTo("204800");
@@ -566,7 +611,7 @@
   @Test
   @GerritConfig(name = "receive.maxObjectSizeLimit", value = "300k")
   public void inheritedMaxObjectSizeOverridesGlobalConfigWhenLower() throws Exception {
-    Project.NameKey child = createProject(name("child"), project);
+    Project.NameKey child = projectOperations.newProject().parent(project).create();
 
     ConfigInfo info = setMaxObjectSize("200k");
     assertThat(info.maxObjectSizeLimit.value).isEqualTo("204800");
@@ -583,7 +628,7 @@
   @GerritConfig(name = "receive.maxObjectSizeLimit", value = "200k")
   @GerritConfig(name = "receive.inheritProjectMaxObjectSizeLimit", value = "true")
   public void maxObjectSizeDoesNotOverrideGlobalConfigWhenHigher() throws Exception {
-    Project.NameKey child = createProject(name("child"), project);
+    Project.NameKey child = projectOperations.newProject().parent(project).create();
 
     ConfigInfo info = setMaxObjectSize("300k");
     assertThat(info.maxObjectSizeLimit.value).isEqualTo("204800");
@@ -603,6 +648,31 @@
     setMaxObjectSize("100 foo");
   }
 
+  @Test
+  public void noCommentlinksByDefault() throws Exception {
+    assertThat(getConfig().commentlinks).isEmpty();
+  }
+
+  @Test
+  @GerritConfig(name = "commentlink.bugzilla.match", value = BUGZILLA_MATCH)
+  @GerritConfig(name = "commentlink.bugzilla.link", value = BUGZILLA_LINK)
+  @GerritConfig(name = "commentlink.jira.match", value = JIRA_MATCH)
+  @GerritConfig(name = "commentlink.jira.link", value = JIRA_LINK)
+  public void projectConfigUsesCommentlinksFromGlobalConfig() throws Exception {
+    Map<String, CommentLinkInfo> expected = new HashMap<>();
+    expected.put(BUGZILLA, commentLinkInfo(BUGZILLA, BUGZILLA_MATCH, BUGZILLA_LINK));
+    expected.put(JIRA, commentLinkInfo(JIRA, JIRA_MATCH, JIRA_LINK));
+    assertCommentLinks(getConfig(), expected);
+  }
+
+  private CommentLinkInfo commentLinkInfo(String name, String match, String link) {
+    return new CommentLinkInfoImpl(name, match, link, null /*html*/, null /*enabled*/);
+  }
+
+  private void assertCommentLinks(ConfigInfo actual, Map<String, CommentLinkInfo> expected) {
+    assertThat(actual.commentlinks).containsExactlyEntriesIn(expected);
+  }
+
   private ConfigInfo setConfig(Project.NameKey name, ConfigInput input) throws Exception {
     return gApi.projects().name(name.get()).config(input);
   }
@@ -642,6 +712,16 @@
     return setConfig(name, input);
   }
 
+  private static Map<String, Map<String, ConfigValue>> newPluginConfigValues() {
+    Map<String, Map<String, ConfigValue>> pluginConfigValues = new HashMap<>();
+    Map<String, ConfigValue> configValues = new HashMap<>();
+    ConfigValue value = new ConfigValue();
+    value.value = "true";
+    configValues.put("test-plugin-key", value);
+    pluginConfigValues.put("gerrit", configValues);
+    return pluginConfigValues;
+  }
+
   private static class ProjectIndexedCounter implements ProjectIndexedListener {
     private final AtomicLongMap<String> countsByProject = AtomicLongMap.create();
 
@@ -677,4 +757,13 @@
       clear();
     }
   }
+
+  @Nullable
+  protected RevCommit getRemoteHead(String project, String branch) throws Exception {
+    return getRemoteHead(new Project.NameKey(project), branch);
+  }
+
+  boolean hasHead(Project.NameKey k, String b) {
+    return projectOperations.project(k).hasHead(b);
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java
index 6fde012..6b511f6 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java
@@ -19,6 +19,7 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.RefState;
@@ -45,6 +46,7 @@
   @Inject private ProjectIndexCollection indexes;
   @Inject private IndexConfig indexConfig;
   @Inject private StalenessChecker stalenessChecker;
+  @Inject private ProjectOperations projectOperations;
 
   private static final ImmutableSet<String> FIELDS =
       ImmutableSet.of(ProjectField.NAME.getName(), ProjectField.REF_STATE.getName());
@@ -96,8 +98,8 @@
 
   @Test
   public void stalenessChecker_hierarchyChange_isStale() throws Exception {
-    Project.NameKey p1 = createProject("p1", allProjects);
-    Project.NameKey p2 = createProject("p2", allProjects);
+    Project.NameKey p1 = projectOperations.newProject().create();
+    Project.NameKey p2 = projectOperations.newProject().create();
     try (ProjectConfigUpdate u = updateProject(project)) {
       u.getConfig().getProject().setParentName(p1);
       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 3295f1a..3c1428d 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/SetParentIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/SetParentIT.java
@@ -19,6 +19,8 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
 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.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -27,15 +29,19 @@
 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;
 import org.junit.Test;
 
 @NoHttpd
 public class SetParentIT extends AbstractDaemonTest {
 
+  @Inject private ProjectOperations projectOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @Test
   public void setParentNotAllowed() throws Exception {
-    String parent = createProject("parent", null, true).get();
-    setApiUser(user);
+    String parent = projectOperations.newProject().create().get();
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     gApi.projects().name(project.get()).parent(parent);
   }
@@ -43,8 +49,8 @@
   @Test
   @GerritConfig(name = "receive.allowProjectOwnersToChangeParent", value = "true")
   public void setParentNotAllowedForNonOwners() throws Exception {
-    String parent = createProject("parent", null, true).get();
-    setApiUser(user);
+    String parent = projectOperations.newProject().create().get();
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     gApi.projects().name(project.get()).parent(parent);
   }
@@ -52,7 +58,7 @@
   @Test
   @GerritConfig(name = "receive.allowProjectOwnersToChangeParent", value = "true")
   public void setParentAllowedByAdminWhenAllowProjectOwnersEnabled() throws Exception {
-    String parent = createProject("parent", null, true).get();
+    String parent = projectOperations.newProject().create().get();
 
     gApi.projects().name(project.get()).parent(parent);
     assertThat(gApi.projects().name(project.get()).parent()).isEqualTo(parent);
@@ -67,8 +73,8 @@
   @Test
   @GerritConfig(name = "receive.allowProjectOwnersToChangeParent", value = "true")
   public void setParentAllowedForOwners() throws Exception {
-    String parent = createProject("parent", null, true).get();
-    setApiUser(user);
+    String parent = projectOperations.newProject().create().get();
+    requestScopeOperations.setApiUser(user.id());
     grant(project, "refs/*", Permission.OWNER, false, SystemGroupBackend.REGISTERED_USERS);
     gApi.projects().name(project.get()).parent(parent);
     assertThat(gApi.projects().name(project.get()).parent()).isEqualTo(parent);
@@ -76,7 +82,7 @@
 
   @Test
   public void setParent() throws Exception {
-    String parent = createProject("parent", null, true).get();
+    String parent = projectOperations.newProject().create().get();
 
     gApi.projects().name(project.get()).parent(parent);
     assertThat(gApi.projects().name(project.get()).parent()).isEqualTo(parent);
@@ -104,7 +110,7 @@
 
   @Test
   public void setParentToOwnChildNotAllowed() throws Exception {
-    String child = createProject("child", project, true).get();
+    String child = projectOperations.newProject().parent(project).create().get();
     exception.expect(ResourceConflictException.class);
     exception.expectMessage("cycle exists between");
     gApi.projects().name(project.get()).parent(child);
@@ -112,8 +118,8 @@
 
   @Test
   public void setParentToGrandchildNotAllowed() throws Exception {
-    Project.NameKey child = createProject("child", project, true);
-    String grandchild = createProject("grandchild", child, true).get();
+    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);
@@ -137,7 +143,7 @@
   public void setParentForAllUsersMustBeAllProjects() throws Exception {
     gApi.projects().name(allUsers.get()).parent(allProjects.get());
 
-    String parent = createProject("parent", null, true).get();
+    String parent = projectOperations.newProject().create().get();
 
     exception.expect(BadRequestException.class);
     exception.expectMessage("All-Users must inherit from All-Projects");
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/GetBlameIT.java b/javatests/com/google/gerrit/acceptance/api/revision/GetBlameIT.java
new file mode 100644
index 0000000..8dfebad
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/revision/GetBlameIT.java
@@ -0,0 +1,128 @@
+// 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.revision;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.extensions.common.BlameInfo;
+import com.google.gerrit.extensions.common.RangeInfo;
+import java.util.List;
+import org.junit.Test;
+
+public class GetBlameIT extends AbstractDaemonTest {
+  @Test
+  public void forNonExistingFile() throws Exception {
+    PushOneCommit.Result r = createChange("Test Change", "foo.txt", "FOO");
+    List<BlameInfo> blameInfos =
+        gApi.changes().id(r.getChangeId()).current().file("non-existing.txt").blameRequest().get();
+
+    // File doesn't exist in commit.
+    assertThat(blameInfos).isEmpty();
+  }
+
+  @Test
+  public void forNonExistingFileFromBase() throws Exception {
+    PushOneCommit.Result r = createChange("Test Change", "foo.txt", "FOO");
+    List<BlameInfo> blameInfos =
+        gApi.changes()
+            .id(r.getChangeId())
+            .current()
+            .file("non-existing.txt")
+            .blameRequest()
+            .forBase(true)
+            .get();
+
+    // File doesn't exist in base commit.
+    assertThat(blameInfos).isEmpty();
+  }
+
+  @Test
+  public void forNewlyAddedFile() throws Exception {
+    PushOneCommit.Result r = createChange("Test Change", "foo.txt", "FOO");
+    List<BlameInfo> blameInfos =
+        gApi.changes().id(r.getChangeId()).current().file("foo.txt").blameRequest().get();
+
+    assertThat(blameInfos).hasSize(1);
+    BlameInfo blameInfo = blameInfos.get(0);
+    assertThat(blameInfo.author).isEqualTo(admin.fullName());
+    assertThat(blameInfo.id).isEqualTo(r.getCommit().getId().name());
+    assertThat(blameInfo.commitMsg).isEqualTo(r.getCommit().getFullMessage());
+    assertThat(blameInfo.time).isEqualTo(r.getCommit().getCommitTime());
+
+    assertThat(blameInfo.ranges).hasSize(1);
+    RangeInfo rangeInfo = blameInfo.ranges.get(0);
+    assertThat(rangeInfo.start).isEqualTo(1);
+    assertThat(rangeInfo.end).isEqualTo(1);
+  }
+
+  @Test
+  public void forNewlyAddedFileFromBase() throws Exception {
+    String changeId = createChange("Test Change", "foo.txt", "FOO").getChangeId();
+    List<BlameInfo> blameInfos =
+        gApi.changes().id(changeId).current().file("foo.txt").blameRequest().forBase(true).get();
+
+    // File doesn't exist in base commit.
+    assertThat(blameInfos).isEmpty();
+  }
+
+  @Test
+  public void forRecreatedFile() throws Exception {
+    // Create change that adds 'foo.txt'.
+    createChange("Change 1", "foo.txt", "FOO");
+
+    // Create change that deletes 'foo.txt'.
+    pushFactory
+        .create(admin.newIdent(), testRepo, "Change 2", "foo.txt", "FOO")
+        .rm("refs/for/master");
+
+    // Create change that recreates 'foo.txt'.
+    PushOneCommit.Result r = createChange("Change 3", "foo.txt", "FOO");
+    List<BlameInfo> blameInfos =
+        gApi.changes().id(r.getChangeId()).current().file("foo.txt").blameRequest().get();
+
+    assertThat(blameInfos).hasSize(1);
+    BlameInfo blameInfo = blameInfos.get(0);
+    assertThat(blameInfo.author).isEqualTo(admin.fullName());
+    assertThat(blameInfo.id).isEqualTo(r.getCommit().getId().name());
+    assertThat(blameInfo.commitMsg).isEqualTo(r.getCommit().getFullMessage());
+    assertThat(blameInfo.time).isEqualTo(r.getCommit().getCommitTime());
+
+    assertThat(blameInfo.ranges).hasSize(1);
+    RangeInfo rangeInfo = blameInfo.ranges.get(0);
+    assertThat(rangeInfo.start).isEqualTo(1);
+    assertThat(rangeInfo.end).isEqualTo(1);
+  }
+
+  @Test
+  public void forRecreatedFileFromBase() throws Exception {
+    // Create change that adds 'foo.txt'.
+    createChange("Change 1", "foo.txt", "FOO");
+
+    // Create change that deletes 'foo.txt'.
+    pushFactory
+        .create(admin.newIdent(), testRepo, "Change 2", "foo.txt", "FOO")
+        .rm("refs/for/master");
+
+    // Create change that recreates 'foo.txt'.
+    String changeId3 = createChange("Change 3", "foo.txt", "FOO").getChangeId();
+    List<BlameInfo> blameInfos =
+        gApi.changes().id(changeId3).current().file("foo.txt").blameRequest().forBase(true).get();
+
+    // File doesn't exist in base commit.
+    assertThat(blameInfos).isEmpty();
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
index 057f837..c27d637 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
@@ -19,6 +19,7 @@
 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 java.util.stream.Collectors.joining;
 import static java.util.stream.Collectors.toMap;
 
@@ -40,6 +41,7 @@
 import com.google.gerrit.extensions.common.DiffInfo;
 import com.google.gerrit.extensions.common.FileInfo;
 import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.testing.ConfigSuite;
 import java.awt.image.BufferedImage;
 import java.io.ByteArrayOutputStream;
@@ -400,6 +402,24 @@
   }
 
   @Test
+  public void diffBetweenPatchSetsOfMergeCommitCanBeRetrievedForCommitMessageAndMergeList()
+      throws Exception {
+    PushOneCommit.Result result = createMergeCommitChange("refs/for/master", "my_file.txt");
+    String changeId = result.getChangeId();
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(changeId, "my_file.txt", content -> content.concat("Line I\nLine II\n"));
+
+    // Call both of them in succession to ensure that they don't share the same cache keys.
+    DiffInfo commitMessageDiffInfo =
+        getDiffRequest(changeId, CURRENT, COMMIT_MSG).withBase(previousPatchSetId).get();
+    DiffInfo mergeListDiffInfo =
+        getDiffRequest(changeId, CURRENT, MERGE_LIST).withBase(previousPatchSetId).get();
+
+    assertThat(commitMessageDiffInfo).content().hasSize(3);
+    assertThat(mergeListDiffInfo).content().hasSize(1);
+  }
+
+  @Test
   public void diffOfUnmodifiedFileMarksAllLinesAsCommon() throws Exception {
     String filePath = "a_new_file.txt";
     String fileContent = "Line 1\nLine 2\nLine 3\n";
@@ -415,7 +435,7 @@
         .content()
         .onlyElement()
         .commonLines()
-        .containsAllOf("Line 1", "Line 2", "Line 3")
+        .containsExactly("Line 1", "Line 2", "Line 3", "")
         .inOrder();
     assertThat(diffInfo).content().onlyElement().linesOfA().isNull();
     assertThat(diffInfo).content().onlyElement().linesOfB().isNull();
@@ -577,6 +597,109 @@
   }
 
   @Test
+  public void addedNewlineAtEndOfFileIsMarkedInDiffWhenOtherwiseOnlyEditsDueToRebaseExist()
+      throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    String newBaseFileContent = FILE_CONTENT.replace("Line 70\n", "Line seventy\n");
+    ObjectId commit2 = addCommit(commit1, FILE_NAME, newBaseFileContent);
+    rebaseChangeOn(changeId, commit2);
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("\n"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME)
+            .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_NONE)
+            .withBase(previousPatchSetId)
+            .get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(1).isNotNull(); // Line 70 modification
+    assertThat(diffInfo).content().element(2).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(3).linesOfA().containsExactly("Line 101");
+    assertThat(diffInfo).content().element(3).linesOfB().containsExactly("Line 101", "");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(101);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(102);
+  }
+
+  @Test
+  // TODO: Fix this issue. This test documents the current behavior and ensures that we at least
+  //  don't run into an internal server error.
+  public void addedNewlineAtEndOfFileIsNotIdentifiedAsDueToRebaseEvenThoughItShould()
+      throws Exception {
+    String baseFileContent = FILE_CONTENT.concat("Line 101");
+    ObjectId commit2 = addCommit(commit1, FILE_NAME, baseFileContent);
+    rebaseChangeOn(changeId, commit2);
+    // Add a comment so that file contents are not 'skipped'. To be able to add a comment, touch
+    // (= modify) the file in the change.
+    addModifiedPatchSet(
+        changeId, FILE_NAME, fileContent -> fileContent.replace("Line 2\n", "Line two\n"));
+    CommentInput comment = createCommentInput(3, 0, 4, 0, "Comment to not skip file content.");
+    addCommentTo(changeId, CURRENT, FILE_NAME, comment);
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    String newBaseFileContent = baseFileContent.concat("\n");
+    ObjectId commit3 = addCommit(commit2, FILE_NAME, newBaseFileContent);
+    rebaseChangeOn(changeId, commit3);
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME)
+            .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_ALL)
+            .withBase(previousPatchSetId)
+            .get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(1).linesOfA().isNull();
+    assertThat(diffInfo).content().element(1).linesOfB().containsExactly("");
+    // This should actually be isDueToRebase().
+    assertThat(diffInfo).content().element(1).isNotDueToRebase();
+  }
+
+  @Test
+  public void
+      addedNewlineAtEndOfFileIsMarkedWhenEditDueToRebaseIncreasedLineCountAndWhitespaceConsidered()
+          throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    String newBaseFileContent = FILE_CONTENT.replace("Line 70\n", "Line 70\nLine 70.5\n");
+    ObjectId commit2 = addCommit(commit1, FILE_NAME, newBaseFileContent);
+    rebaseChangeOn(changeId, commit2);
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("\n"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME)
+            .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_NONE)
+            .withBase(previousPatchSetId)
+            .get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(1).isNotNull(); // Line 70.5 insertion
+    assertThat(diffInfo).content().element(2).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(3).linesOfA().containsExactly("Line 101");
+    assertThat(diffInfo).content().element(3).linesOfB().containsExactly("Line 101", "");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(101);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(103);
+  }
+
+  @Test
+  // TODO: Fix this issue. This test documents the current behavior and ensures that we at least
+  //  don't run into an internal server error.
+  public void
+      addedNewlineAtEndOfFileIsNotMarkedWhenEditDueToRebaseIncreasedLineCountAndWhitespaceIgnoredEvenThoughItShould()
+          throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    String newBaseFileContent = FILE_CONTENT.replace("Line 70\n", "Line 70\nLine 70.5\n");
+    ObjectId commit2 = addCommit(commit1, FILE_NAME, newBaseFileContent);
+    rebaseChangeOn(changeId, commit2);
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("\n"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME)
+            .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_ALL)
+            .withBase(previousPatchSetId)
+            .get();
+    assertThat(diffInfo).content().element(0).numberOfSkippedLines().isGreaterThan(0);
+  }
+
+  @Test
   public void addedLastLineWithoutNewlineBeforeAndAfterwardsIsMarkedInDiff() throws Exception {
     addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
     String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
@@ -2363,7 +2486,7 @@
         .content()
         .element(0)
         .commonLines()
-        .containsAllOf("Line 1", "Line two", "Line 3", "Line 4", "Line 5")
+        .containsAtLeast("Line 1", "Line two", "Line 3", "Line 4", "Line 5")
         .inOrder();
   }
 
@@ -2373,9 +2496,7 @@
     addModifiedPatchSet(changeId, FILE_NAME, content -> content.replace("Line 2\n", "Line two\n"));
     String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
     CommentInput comment = createCommentInput(2, 0, 3, 0, "Should be 'Line 2'.");
-    ReviewInput reviewInput = new ReviewInput();
-    reviewInput.comments = ImmutableMap.of(FILE_NAME, ImmutableList.of(comment));
-    gApi.changes().id(changeId).revision(previousPatchSetId).review(reviewInput);
+    addCommentTo(changeId, previousPatchSetId, FILE_NAME, comment);
     addModifiedPatchSet(
         changeId, FILE_NAME2, content -> content.replace("2nd line\n", "Second line\n"));
 
@@ -2389,7 +2510,113 @@
         .content()
         .element(0)
         .commonLines()
-        .containsAllOf("Line 1", "Line two", "Line 3", "Line 4", "Line 5")
+        .containsAtLeast("Line 1", "Line two", "Line 3", "Line 4", "Line 5")
+        .inOrder();
+  }
+
+  @Test
+  public void
+      diffOfFileWithOnlyRebaseHunksAndWithCommentAndConsideringWhitespaceReturnsFileContents()
+          throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, content -> content.replace("Line 2\n", "Line two\n"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    String newBaseFileContent = FILE_CONTENT.replace("Line 70\n", "Line seventy\n");
+    ObjectId commit2 = addCommit(commit1, FILE_NAME, newBaseFileContent);
+    rebaseChangeOn(changeId, commit2);
+    CommentInput comment = createCommentInput(2, 0, 3, 0, "Should be 'Line 2'.");
+    addCommentTo(changeId, previousPatchSetId, FILE_NAME, comment);
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME)
+            .withBase(previousPatchSetId)
+            .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_NONE)
+            .get();
+    // We don't list the full file contents here as that is not the focus of this test.
+    assertThat(diffInfo)
+        .content()
+        .element(0)
+        .commonLines()
+        .containsAtLeast("Line 1", "Line two", "Line 3", "Line 4", "Line 5")
+        .inOrder();
+  }
+
+  @Test
+  public void diffOfFileWithOnlyRebaseHunksAndWithCommentAndIgnoringWhitespaceReturnsFileContents()
+      throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, content -> content.replace("Line 2\n", "Line two\n"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    String newBaseFileContent = FILE_CONTENT.replace("Line 70\n", "Line seventy\n");
+    ObjectId commit2 = addCommit(commit1, FILE_NAME, newBaseFileContent);
+    rebaseChangeOn(changeId, commit2);
+    CommentInput comment = createCommentInput(2, 0, 3, 0, "Should be 'Line 2'.");
+    addCommentTo(changeId, previousPatchSetId, FILE_NAME, comment);
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME)
+            .withBase(previousPatchSetId)
+            .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_ALL)
+            .get();
+    // We don't list the full file contents here as that is not the focus of this test.
+    assertThat(diffInfo)
+        .content()
+        .element(0)
+        .commonLines()
+        .containsAtLeast("Line 1", "Line two", "Line 3", "Line 4", "Line 5")
+        .inOrder();
+  }
+
+  @Test
+  public void
+      diffOfFileWithMultilineRebaseHunkAddingNewlineAtEndOfFileAndWithCommentReturnsFileContents()
+          throws Exception {
+    String baseFileContent = FILE_CONTENT.concat("Line 101");
+    ObjectId commit2 = addCommit(commit1, FILE_NAME, baseFileContent);
+    rebaseChangeOn(changeId, commit2);
+    addModifiedPatchSet(changeId, FILE_NAME, content -> content.replace("Line 2\n", "Line two\n"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    String newBaseFileContent = baseFileContent.concat("\nLine 102\nLine 103\n");
+    ObjectId commit3 = addCommit(commit2, FILE_NAME, newBaseFileContent);
+    rebaseChangeOn(changeId, commit3);
+    CommentInput comment = createCommentInput(2, 0, 3, 0, "Should be 'Line 2'.");
+    addCommentTo(changeId, previousPatchSetId, FILE_NAME, comment);
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME)
+            .withBase(previousPatchSetId)
+            .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_NONE)
+            .get();
+    // We don't list the full file contents here as that is not the focus of this test.
+    assertThat(diffInfo)
+        .content()
+        .element(0)
+        .commonLines()
+        .containsAtLeast("Line 1", "Line two", "Line 3", "Line 4", "Line 5")
+        .inOrder();
+  }
+
+  @Test
+  public void
+      diffOfFileWithMultilineRebaseHunkRemovingNewlineAtEndOfFileAndWithCommentReturnsFileContents()
+          throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, content -> content.replace("Line 2\n", "Line two\n"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    String newBaseFileContent = FILE_CONTENT.concat("Line 101\nLine 103\nLine 104");
+    ObjectId commit2 = addCommit(commit1, FILE_NAME, newBaseFileContent);
+    rebaseChangeOn(changeId, commit2);
+    CommentInput comment = createCommentInput(2, 0, 3, 0, "Should be 'Line 2'.");
+    addCommentTo(changeId, previousPatchSetId, FILE_NAME, comment);
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME)
+            .withBase(previousPatchSetId)
+            .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_NONE)
+            .get();
+    // We don't list the full file contents here as that is not the focus of this test.
+    assertThat(diffInfo)
+        .content()
+        .element(0)
+        .commonLines()
+        .containsAtLeast("Line 1", "Line two", "Line 3", "Line 4", "Line 5")
         .inOrder();
   }
 
@@ -2405,6 +2632,44 @@
     assertThat(diffInfo).content().isEmpty();
   }
 
+  // This behavior is likely a bug. A fix might not be easy as it might break syntax highlighting.
+  // TODO: Fix this issue or remove the broken parameter (at least in the documentation).
+  @Test
+  public void contextParameterIsIgnored() throws Exception {
+    addModifiedPatchSet(
+        changeId, FILE_NAME, content -> content.replace("Line 20\n", "Line twenty\n"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME)
+            .withBase(initialPatchSetId)
+            .withContext(5)
+            .get();
+    assertThat(diffInfo).content().element(0).commonLines().hasSize(19);
+    assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 20");
+    assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line twenty");
+    assertThat(diffInfo).content().element(2).commonLines().hasSize(81);
+  }
+
+  // This behavior is likely a bug. A fix might not be easy as it might break syntax highlighting.
+  // TODO: Fix this issue or remove the broken parameter (at least in the documentation).
+  @Test
+  public void contextParameterIsIgnoredForUnmodifiedFileWithComment() throws Exception {
+    addModifiedPatchSet(
+        changeId, FILE_NAME, content -> content.replace("Line 20\n", "Line twenty\n"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    CommentInput comment = createCommentInput(20, 0, 21, 0, "Should be 'Line 20'.");
+    addCommentTo(changeId, previousPatchSetId, FILE_NAME, comment);
+    addModifiedPatchSet(
+        changeId, FILE_NAME2, content -> content.replace("2nd line\n", "Second line\n"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME)
+            .withBase(previousPatchSetId)
+            .withContext(5)
+            .get();
+    assertThat(diffInfo).content().element(0).commonLines().hasSize(101);
+  }
+
   @Test
   public void requestingDiffForOldFileNameOfRenamedFileYieldsReasonableResult() throws Exception {
     addModifiedPatchSet(changeId, FILE_NAME, content -> content.replace("Line 2\n", "Line two\n"));
@@ -2435,9 +2700,7 @@
     addModifiedPatchSet(changeId, FILE_NAME, content -> content.replace("Line 2\n", "Line two\n"));
     String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
     CommentInput comment = createCommentInput(2, 0, 3, 0, "Should be 'Line 2'.");
-    ReviewInput reviewInput = new ReviewInput();
-    reviewInput.comments = ImmutableMap.of(FILE_NAME, ImmutableList.of(comment));
-    gApi.changes().id(changeId).revision(previousPatchSetId).review(reviewInput);
+    addCommentTo(changeId, previousPatchSetId, FILE_NAME, comment);
     String newFilePath = "a_new_file.txt";
     gApi.changes().id(changeId).edit().renameFile(FILE_NAME, newFilePath);
     gApi.changes().id(changeId).edit().publish();
@@ -2466,6 +2729,14 @@
     return comment;
   }
 
+  private void addCommentTo(
+      String changeId, String previousPatchSetId, String fileName, CommentInput comment)
+      throws RestApiException {
+    ReviewInput reviewInput = new ReviewInput();
+    reviewInput.comments = ImmutableMap.of(fileName, ImmutableList.of(comment));
+    gApi.changes().id(changeId).revision(previousPatchSetId).review(reviewInput);
+  }
+
   private void assertDiffForNewFile(
       PushOneCommit.Result pushResult, String path, String expectedContentSideB) throws Exception {
     DiffInfo diff =
@@ -2521,7 +2792,7 @@
       throws Exception {
     testRepo.reset(parentCommit);
     PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), testRepo, "Adjust files of repo", files);
+        pushFactory.create(admin.newIdent(), testRepo, "Adjust files of repo", files);
     PushOneCommit.Result result = push.to("refs/for/master");
     return result.getCommit();
   }
@@ -2545,7 +2816,7 @@
         Arrays.stream(removedFilePaths)
             .collect(toMap(Function.identity(), path -> "Irrelevant content"));
     PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), testRepo, "Remove files from repo", files);
+        pushFactory.create(admin.newIdent(), testRepo, "Remove files from repo", files);
     PushOneCommit.Result result = push.rm("refs/for/master");
     return result.getCommit();
   }
@@ -2564,7 +2835,7 @@
 
   private Result createEmptyChange() throws Exception {
     PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), testRepo, "Test change", ImmutableMap.of());
+        pushFactory.create(admin.newIdent(), testRepo, "Test change", ImmutableMap.of());
     return push.to("refs/for/master");
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 8ac8537..2375607 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -42,6 +42,7 @@
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
@@ -86,6 +87,7 @@
 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;
@@ -114,9 +116,10 @@
 
 public class RevisionIT extends AbstractDaemonTest {
 
-  @Inject private GetRevisionActions getRevisionActions;
-  @Inject private DynamicSet<PatchSetWebLink> patchSetLinks;
   @Inject private DynamicSet<ChangeIndexedListener> changeIndexedListeners;
+  @Inject private DynamicSet<PatchSetWebLink> patchSetLinks;
+  @Inject private GetRevisionActions getRevisionActions;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
   public void reviewTriplet() throws Exception {
@@ -217,24 +220,24 @@
     PushOneCommit.Result r = createChange();
     String changeId = project.get() + "~master~" + r.getChangeId();
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     revision(r).review(ReviewInput.approve());
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     revision(r).review(ReviewInput.recommend());
 
-    setApiUser(admin);
-    gApi.changes().id(changeId).reviewer(user.username).deleteVote("Code-Review");
+    requestScopeOperations.setApiUser(admin.id());
+    gApi.changes().id(changeId).reviewer(user.username()).deleteVote("Code-Review");
     Optional<ApprovalInfo> crUser =
         get(changeId, DETAILED_LABELS).labels.get("Code-Review").all.stream()
-            .filter(a -> a._accountId == user.id.get())
+            .filter(a -> a._accountId == user.id().get())
             .findFirst();
     assertThat(crUser).isPresent();
     assertThat(crUser.get().value).isEqualTo(0);
 
     revision(r).submit();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     ReviewInput in = new ReviewInput();
     in.label("Code-Review", 1);
     in.message = "Still LGTM";
@@ -242,7 +245,7 @@
 
     ApprovalInfo cr =
         gApi.changes().id(changeId).get(DETAILED_LABELS).labels.get("Code-Review").all.stream()
-            .filter(a -> a._accountId == user.getId().get())
+            .filter(a -> a._accountId == user.id().get())
             .findFirst()
             .get();
     assertThat(cr.postSubmit).isTrue();
@@ -294,7 +297,7 @@
   @Test
   public void voteNotAllowedWithoutPermission() throws Exception {
     PushOneCommit.Result r = createChange();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("is restricted");
     gApi.changes().id(r.getChange().getId().get()).current().review(ReviewInput.approve());
@@ -411,7 +414,7 @@
     String subject = "Test change\n\nChange-Id: Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
     PushOneCommit push =
         pushFactory.create(
-            db, admin.getIdent(), testRepo, subject, "another_file.txt", "another content");
+            admin.newIdent(), testRepo, subject, "another_file.txt", "another content");
     PushOneCommit.Result r2 = push.to("refs/for/master");
 
     // Change 2's parent should be change 1
@@ -468,8 +471,7 @@
 
     PushOneCommit push =
         pushFactory.create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             PushOneCommit.FILE_NAME,
@@ -495,8 +497,7 @@
     String destContent = "some content";
     PushOneCommit push =
         pushFactory.create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             ImmutableMap.of(PushOneCommit.FILE_NAME, destContent, "foo.txt", "foo"));
@@ -507,8 +508,7 @@
     String changeContent = "another content";
     push =
         pushFactory.create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             ImmutableMap.of(PushOneCommit.FILE_NAME, changeContent, "bar.txt", "bar"));
@@ -586,7 +586,7 @@
   public void cherryPickToExistingChange() throws Exception {
     PushOneCommit.Result r1 =
         pushFactory
-            .create(db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "a")
+            .create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "a")
             .to("refs/for/master");
     String t1 = project.get() + "~master~" + r1.getChangeId();
 
@@ -596,7 +596,7 @@
 
     PushOneCommit.Result r2 =
         pushFactory
-            .create(db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "b", r1.getChangeId())
+            .create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "b", r1.getChangeId())
             .to("refs/for/foo");
     String t2 = project.get() + "~foo~" + r2.getChangeId();
     gApi.changes().id(t2).abandon();
@@ -730,7 +730,7 @@
 
     // 'user' cherry-picks the change to a new branch, the source change's author/committer('admin')
     // will be added as a reviewer of the newly created change.
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     CherryPickInput input = new CherryPickInput();
     input.message = "it goes to a new branch";
 
@@ -753,7 +753,7 @@
     input.destination = "branch-3";
     input.notify = NotifyHandling.NONE;
     input.notifyDetails =
-        ImmutableMap.of(RecipientType.TO, new NotifyInfo(ImmutableList.of(userToNotify.email)));
+        ImmutableMap.of(RecipientType.TO, new NotifyInfo(ImmutableList.of(userToNotify.email())));
     sender.clear();
     gApi.changes().id(changeId).current().cherryPick(input);
     assertNotifyTo(userToNotify);
@@ -766,13 +766,13 @@
     // Change is created by 'admin'.
     PushOneCommit.Result r = createChange();
     // Change is approved by 'admin2'. Change is CC'd to 'user'.
-    setApiUser(accountCreator.admin2());
+    requestScopeOperations.setApiUser(accountCreator.admin2().id());
     ReviewInput in = ReviewInput.approve();
-    in.reviewer(user.email, ReviewerState.CC, true);
+    in.reviewer(user.email(), ReviewerState.CC, true);
     gApi.changes().id(r.getChangeId()).current().review(in);
 
     // Change is cherrypicked by 'user2'.
-    setApiUser(accountCreator.user2());
+    requestScopeOperations.setApiUser(accountCreator.user2().id());
     CherryPickInput cin = new CherryPickInput();
     cin.message = "this need to go to stable";
     cin.destination = "stable";
@@ -786,16 +786,11 @@
     assertThat(result).containsKey(ReviewerState.REVIEWER);
     List<Integer> reviewers =
         result.get(ReviewerState.REVIEWER).stream().map(a -> a._accountId).collect(toList());
-    if (notesMigration.readChanges()) {
-      assertThat(result).containsKey(ReviewerState.CC);
-      List<Integer> ccs =
-          result.get(ReviewerState.CC).stream().map(a -> a._accountId).collect(toList());
-      assertThat(ccs).containsExactly(user.id.get());
-      assertThat(reviewers).containsExactly(admin.id.get(), accountCreator.admin2().id.get());
-    } else {
-      assertThat(reviewers)
-          .containsExactly(user.id.get(), admin.id.get(), accountCreator.admin2().id.get());
-    }
+    assertThat(result).containsKey(ReviewerState.CC);
+    List<Integer> ccs =
+        result.get(ReviewerState.CC).stream().map(a -> a._accountId).collect(toList());
+    assertThat(ccs).containsExactly(user.id().get());
+    assertThat(reviewers).containsExactly(admin.id().get(), accountCreator.admin2().id().get());
   }
 
   @Test
@@ -856,7 +851,7 @@
     input.base = dstChange.getCommit().name();
     input.message = srcChange.getCommit().getFullMessage();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(UnprocessableEntityException.class);
     exception.expectMessage(
         String.format("Commit %s does not exist on branch refs/heads/foo", input.base));
@@ -877,8 +872,8 @@
     exception.expect(ResourceConflictException.class);
     exception.expectMessage(
         String.format(
-            "Change %s with commit %s is %s",
-            change2.getChange().getId().get(), input.base, ChangeStatus.ABANDONED));
+            "Change %s with commit %s is abandoned",
+            change2.getChange().getId().get(), input.base));
     gApi.changes().id(change1.getChangeId()).current().cherryPick(input);
   }
 
@@ -915,12 +910,48 @@
   }
 
   @Test
+  public void cherryPickToNonExistingBranch() throws Exception {
+    PushOneCommit.Result result = createChange();
+
+    CherryPickInput input = new CherryPickInput();
+    input.message = "foo bar";
+    input.destination = "non-existing";
+    // TODO(ekempin): This should rather result in an UnprocessableEntityException.
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class,
+            () -> gApi.changes().id(result.getChangeId()).current().cherryPick(input));
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo(
+            String.format("Branch %s does not exist.", RefNames.REFS_HEADS + input.destination));
+  }
+
+  @Test
+  public void cherryPickToNonExistingBaseCommit() throws Exception {
+    createBranch(new Branch.NameKey(project, "foo"));
+    PushOneCommit.Result result = createChange();
+
+    CherryPickInput input = new CherryPickInput();
+    input.message = "foo bar";
+    input.destination = "foo";
+    input.base = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+    UnprocessableEntityException thrown =
+        assertThrows(
+            UnprocessableEntityException.class,
+            () -> gApi.changes().id(result.getChangeId()).current().cherryPick(input));
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo(String.format("Base %s doesn't exist", input.base));
+  }
+
+  @Test
   public void canRebase() throws Exception {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result r1 = push.to("refs/for/master");
     merge(r1);
 
-    push = pushFactory.create(db, admin.getIdent(), testRepo);
+    push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result r2 = push.to("refs/for/master");
     boolean canRebase =
         gApi.changes().id(r2.getChangeId()).revision(r2.getCommit().name()).canRebase();
@@ -928,7 +959,7 @@
     merge(r2);
 
     testRepo.reset(r1.getCommit());
-    push = pushFactory.create(db, admin.getIdent(), testRepo);
+    push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result r3 = push.to("refs/for/master");
 
     canRebase = gApi.changes().id(r3.getChangeId()).revision(r3.getCommit().name()).canRebase();
@@ -937,7 +968,7 @@
 
   @Test
   public void setUnsetReviewedFlag() throws Exception {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result r = push.to("refs/for/master");
 
     gApi.changes().id(r.getChangeId()).current().setReviewed(PushOneCommit.FILE_NAME, true);
@@ -952,7 +983,7 @@
 
   @Test
   public void setUnsetReviewedFlagByFileApi() throws Exception {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result r = push.to("refs/for/master");
 
     gApi.changes().id(r.getChangeId()).current().file(PushOneCommit.FILE_NAME).setReviewed(true);
@@ -971,8 +1002,7 @@
 
     PushOneCommit push1 =
         pushFactory.create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             PushOneCommit.FILE_NAME,
@@ -987,8 +1017,7 @@
 
     PushOneCommit push2 =
         pushFactory.create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             PushOneCommit.FILE_NAME,
@@ -1004,8 +1033,8 @@
 
     // Make the same change in a separate commit and update server HEAD behind Gerrit's back, which
     // will not reindex any open changes.
-    try (Repository repo = repoManager.openRepository(project)) {
-      TestRepository<?> tr = new TestRepository<>(repo);
+    try (Repository repo = repoManager.openRepository(project);
+        TestRepository<Repository> tr = new TestRepository<>(repo)) {
       String ref = "refs/heads/master";
       assertThat(repo.exactRef(ref).getObjectId()).isEqualTo(r1.getCommit());
       tr.update(ref, tr.getRevWalk().parseCommit(initial));
@@ -1123,7 +1152,7 @@
 
   @Test
   public void listFilesOnDifferentBases() throws Exception {
-    RevCommit initialCommit = getHead(repo());
+    RevCommit initialCommit = getHead(repo(), "HEAD");
 
     PushOneCommit.Result result1 = createChange();
     String changeId = result1.getChangeId();
@@ -1166,7 +1195,7 @@
   public void queryRevisionFiles() throws Exception {
     Map<String, String> files = ImmutableMap.of("file1.txt", "content 1", "file2.txt", "content 2");
     PushOneCommit.Result result =
-        pushFactory.create(db, admin.getIdent(), testRepo, SUBJECT, files).to("refs/for/master");
+        pushFactory.create(admin.newIdent(), testRepo, SUBJECT, files).to("refs/for/master");
     result.assertOkStatus();
     String changeId = result.getChangeId();
 
@@ -1198,7 +1227,7 @@
   public void setDescriptionNotAllowedWithoutPermission() throws Exception {
     PushOneCommit.Result r = createChange();
     assertDescription(r, "");
-    setApiUser(user);
+    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");
@@ -1209,7 +1238,7 @@
     PushOneCommit.Result r = createChange();
     assertDescription(r, "");
     grant(project, "refs/heads/master", Permission.OWNER, false, REGISTERED_USERS);
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).description("test");
     assertDescription(r, "test");
   }
@@ -1248,14 +1277,7 @@
   public void commit() throws Exception {
     WebLinkInfo expectedWebLinkInfo = new WebLinkInfo("foo", "imageUrl", "url");
     RegistrationHandle handle =
-        patchSetLinks.add(
-            "gerrit",
-            new PatchSetWebLink() {
-              @Override
-              public WebLinkInfo getPatchSetWebLink(String projectName, String commit) {
-                return expectedWebLinkInfo;
-              }
-            });
+        patchSetLinks.add("gerrit", (projectName, commit) -> expectedWebLinkInfo);
 
     try {
       PushOneCommit.Result r = createChange();
@@ -1340,7 +1362,7 @@
                 .get()
                 .author
                 .email)
-        .isEqualTo(admin.email);
+        .isEqualTo(admin.email());
 
     draftApi.delete();
     assertThat(gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).drafts())
@@ -1366,7 +1388,7 @@
     assertThat(out).hasSize(1);
     CommentInfo comment = Iterables.getOnlyElement(out.get(FILE_NAME));
     assertThat(comment.message).isEqualTo(in.message);
-    assertThat(comment.author.email).isEqualTo(admin.email);
+    assertThat(comment.author.email).isEqualTo(admin.email());
     assertThat(comment.path).isNull();
 
     List<CommentInfo> list =
@@ -1479,17 +1501,17 @@
     amendChange(r.getChangeId());
 
     // code-review
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     recommend(r.getChangeId());
 
     // check if it's blocked to delete a vote on a non-current patch set.
-    setApiUser(admin);
+    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.getId().toString())
+        .reviewer(user.id().toString())
         .deleteVote("Code-Review");
   }
 
@@ -1502,27 +1524,27 @@
     amendChange(r.getChangeId());
 
     // code-review
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     recommend(r.getChangeId());
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     gApi.changes()
         .id(r.getChangeId())
         .current()
-        .reviewer(user.getId().toString())
+        .reviewer(user.id().toString())
         .deleteVote("Code-Review");
 
     Map<String, Short> m =
-        gApi.changes().id(r.getChangeId()).current().reviewer(user.getId().toString()).votes();
+        gApi.changes().id(r.getChangeId()).current().reviewer(user.id().toString()).votes();
 
     assertThat(m).containsExactly("Code-Review", Short.valueOf((short) 0));
 
     ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
     ChangeMessageInfo message = Iterables.getLast(c.messages);
-    assertThat(message.author._accountId).isEqualTo(admin.getId().get());
+    assertThat(message.author._accountId).isEqualTo(admin.id().get());
     assertThat(message.message).isEqualTo("Removed Code-Review+1 by User <user@example.com>\n");
     assertThat(getReviewers(c.reviewers.get(ReviewerState.REVIEWER)))
-        .containsExactlyElementsIn(ImmutableSet.of(admin.getId(), user.getId()));
+        .containsExactlyElementsIn(ImmutableSet.of(admin.id(), user.id()));
   }
 
   @Test
@@ -1537,22 +1559,22 @@
     List<ApprovalInfo> approvals = votes.get("Code-Review");
     assertThat(approvals).hasSize(1);
     ApprovalInfo approval = approvals.get(0);
-    assertThat(approval._accountId).isEqualTo(admin.id.get());
-    assertThat(approval.email).isEqualTo(admin.email);
-    assertThat(approval.username).isEqualTo(admin.username);
+    assertThat(approval._accountId).isEqualTo(admin.id().get());
+    assertThat(approval.email).isEqualTo(admin.email());
+    assertThat(approval.username).isEqualTo(admin.username());
 
     // Also vote on it with another user
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(changeId).current().review(ReviewInput.dislike());
 
     // Patch set 1 has 2 votes on Code-Review
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     votes = gApi.changes().id(changeId).current().votes();
     assertThat(votes.keySet()).containsExactly("Code-Review");
     approvals = votes.get("Code-Review");
     assertThat(approvals).hasSize(2);
     assertThat(approvals.stream().map(a -> a._accountId))
-        .containsExactlyElementsIn(ImmutableList.of(admin.id.get(), user.id.get()));
+        .containsExactlyElementsIn(ImmutableList.of(admin.id().get(), user.id().get()));
 
     // Create a new patch set which does not have any votes
     amendChange(changeId);
@@ -1580,7 +1602,7 @@
       throws Exception {
     PushOneCommit push =
         pushFactory.create(
-            db, admin.getIdent(), testRepo, "test commit", "a.txt", content, r.getChangeId());
+            admin.newIdent(), testRepo, "test commit", "a.txt", content, r.getChangeId());
     return push.to("refs/for/master");
   }
 
@@ -1597,7 +1619,7 @@
 
   private PushOneCommit.Result createCherryPickableMerge(
       String parent1FileName, String parent2FileName) throws Exception {
-    RevCommit initialCommit = getHead(repo());
+    RevCommit initialCommit = getHead(repo(), "HEAD");
 
     String branchAName = "branchA";
     createBranch(new Branch.NameKey(project, branchAName));
@@ -1606,19 +1628,18 @@
 
     PushOneCommit.Result changeAResult =
         pushFactory
-            .create(db, admin.getIdent(), testRepo, "change a", parent1FileName, "Content of a")
+            .create(admin.newIdent(), testRepo, "change a", parent1FileName, "Content of a")
             .to("refs/for/" + branchAName);
 
     testRepo.reset(initialCommit);
     PushOneCommit.Result changeBResult =
         pushFactory
-            .create(db, admin.getIdent(), testRepo, "change b", parent2FileName, "Content of b")
+            .create(admin.newIdent(), testRepo, "change b", parent2FileName, "Content of b")
             .to("refs/for/" + branchBName);
 
     PushOneCommit pushableMergeCommit =
         pushFactory.create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             "merge",
             ImmutableMap.of(parent1FileName, "Content of a", parent2FileName, "Content of b"));
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
index dfd25e2..ba228f6 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.acceptance.api.revision;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.TruthJUnit.assume;
 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;
@@ -25,7 +24,6 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -38,7 +36,6 @@
 import com.google.gerrit.extensions.common.RobotCommentInfo;
 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.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -70,8 +67,7 @@
   public void setUp() throws Exception {
     PushOneCommit push =
         pushFactory.create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             "Provide files which can be used for fixes",
             ImmutableMap.of(FILE_NAME, FILE_CONTENT, FILE_NAME2, FILE_CONTENT2));
@@ -85,8 +81,6 @@
 
   @Test
   public void retrievingRobotCommentsBeforeAddingAnyDoesNotRaiseAnException() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     Map<String, List<RobotCommentInfo>> robotComments =
         gApi.changes().id(changeId).current().robotComments();
 
@@ -96,8 +90,6 @@
 
   @Test
   public void addedRobotCommentsCanBeRetrieved() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     RobotCommentInput in = createRobotCommentInput();
     addRobotComment(changeId, in);
 
@@ -110,12 +102,10 @@
 
   @Test
   public void addedRobotCommentsCanBeRetrievedByChange() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     RobotCommentInput in = createRobotCommentInput();
     addRobotComment(changeId, in);
 
-    pushFactory.create(db, admin.getIdent(), testRepo, changeId).to("refs/for/master");
+    pushFactory.create(admin.newIdent(), testRepo, changeId).to("refs/for/master");
 
     RobotCommentInput in2 = createRobotCommentInput();
     addRobotComment(changeId, in2);
@@ -133,8 +123,6 @@
 
   @Test
   public void robotCommentsCanBeRetrievedAsList() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     RobotCommentInput robotCommentInput = createRobotCommentInput();
     addRobotComment(changeId, robotCommentInput);
 
@@ -148,8 +136,6 @@
 
   @Test
   public void specificRobotCommentCanBeRetrieved() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     RobotCommentInput robotCommentInput = createRobotCommentInput();
     addRobotComment(changeId, robotCommentInput);
 
@@ -163,8 +149,6 @@
 
   @Test
   public void robotCommentWithoutOptionalFieldsCanBeAdded() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     RobotCommentInput in = createRobotCommentInputWithMandatoryFields();
     addRobotComment(changeId, in);
 
@@ -176,8 +160,6 @@
 
   @Test
   public void hugeRobotCommentIsRejected() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     int defaultSizeLimit = 1024 * 1024;
     int sizeOfRest = 451;
     fixReplacementInfo.replacement = getStringFor(defaultSizeLimit - sizeOfRest + 1);
@@ -189,8 +171,6 @@
 
   @Test
   public void reasonablyLargeRobotCommentIsAccepted() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     int defaultSizeLimit = 1024 * 1024;
     int sizeOfRest = 451;
     fixReplacementInfo.replacement = getStringFor(defaultSizeLimit - sizeOfRest);
@@ -204,8 +184,6 @@
   @Test
   @GerritConfig(name = "change.robotCommentSizeLimit", value = "10k")
   public void maximumAllowedSizeOfRobotCommentCanBeAdjusted() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     int sizeLimit = 10 * 1024;
     fixReplacementInfo.replacement = getStringFor(sizeLimit);
 
@@ -217,8 +195,6 @@
   @Test
   @GerritConfig(name = "change.robotCommentSizeLimit", value = "0")
   public void zeroForMaximumAllowedSizeOfRobotCommentRemovesRestriction() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     int defaultSizeLimit = 1024 * 1024;
     fixReplacementInfo.replacement = getStringFor(defaultSizeLimit);
 
@@ -232,8 +208,6 @@
   @GerritConfig(name = "change.robotCommentSizeLimit", value = "-1")
   public void negativeValueForMaximumAllowedSizeOfRobotCommentRemovesRestriction()
       throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     int defaultSizeLimit = 1024 * 1024;
     fixReplacementInfo.replacement = getStringFor(defaultSizeLimit);
 
@@ -245,8 +219,6 @@
 
   @Test
   public void addedFixSuggestionCanBeRetrieved() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     addRobotComment(changeId, withFixRobotCommentInput);
     List<RobotCommentInfo> robotCommentInfos = getRobotComments();
 
@@ -255,8 +227,6 @@
 
   @Test
   public void fixIdIsGeneratedForFixSuggestion() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     addRobotComment(changeId, withFixRobotCommentInput);
     List<RobotCommentInfo> robotCommentInfos = getRobotComments();
 
@@ -270,8 +240,6 @@
 
   @Test
   public void descriptionOfFixSuggestionIsAcceptedAsIs() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     addRobotComment(changeId, withFixRobotCommentInput);
     List<RobotCommentInfo> robotCommentInfos = getRobotComments();
 
@@ -284,8 +252,6 @@
 
   @Test
   public void descriptionOfFixSuggestionIsMandatory() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     fixSuggestionInfo.description = null;
 
     exception.expect(BadRequestException.class);
@@ -298,8 +264,6 @@
 
   @Test
   public void addedFixReplacementCanBeRetrieved() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     addRobotComment(changeId, withFixRobotCommentInput);
     List<RobotCommentInfo> robotCommentInfos = getRobotComments();
 
@@ -312,8 +276,6 @@
 
   @Test
   public void fixReplacementsAreMandatory() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     fixSuggestionInfo.replacements = Collections.emptyList();
 
     exception.expect(BadRequestException.class);
@@ -327,8 +289,6 @@
 
   @Test
   public void pathOfFixReplacementIsAcceptedAsIs() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     addRobotComment(changeId, withFixRobotCommentInput);
 
     List<RobotCommentInfo> robotCommentInfos = getRobotComments();
@@ -343,8 +303,6 @@
 
   @Test
   public void pathOfFixReplacementIsMandatory() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     fixReplacementInfo.path = null;
 
     exception.expect(BadRequestException.class);
@@ -357,8 +315,6 @@
 
   @Test
   public void rangeOfFixReplacementIsAcceptedAsIs() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     addRobotComment(changeId, withFixRobotCommentInput);
 
     List<RobotCommentInfo> robotCommentInfos = getRobotComments();
@@ -373,8 +329,6 @@
 
   @Test
   public void rangeOfFixReplacementIsMandatory() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     fixReplacementInfo.range = null;
 
     exception.expect(BadRequestException.class);
@@ -387,8 +341,6 @@
 
   @Test
   public void rangeOfFixReplacementNeedsToBeValid() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     fixReplacementInfo.range = createRange(13, 9, 5, 10);
     exception.expect(BadRequestException.class);
     exception.expectMessage("Range (13:9 - 5:10)");
@@ -398,8 +350,6 @@
   @Test
   public void rangesOfFixReplacementsOfSameFixSuggestionForSameFileMayNotOverlap()
       throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
     fixReplacementInfo1.path = FILE_NAME;
     fixReplacementInfo1.range = createRange(2, 0, 3, 1);
@@ -422,8 +372,6 @@
   @Test
   public void rangesOfFixReplacementsOfSameFixSuggestionForDifferentFileMayOverlap()
       throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
     fixReplacementInfo1.path = FILE_NAME;
     fixReplacementInfo1.range = createRange(2, 0, 3, 1);
@@ -447,8 +395,6 @@
   @Test
   public void rangesOfFixReplacementsOfDifferentFixSuggestionsForSameFileMayOverlap()
       throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
     fixReplacementInfo1.path = FILE_NAME;
     fixReplacementInfo1.range = createRange(2, 0, 3, 1);
@@ -472,8 +418,6 @@
 
   @Test
   public void fixReplacementsDoNotNeedToBeOrderedAccordingToRange() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
     fixReplacementInfo1.path = FILE_NAME;
     fixReplacementInfo1.range = createRange(2, 0, 3, 0);
@@ -501,8 +445,6 @@
 
   @Test
   public void replacementStringOfFixReplacementIsAcceptedAsIs() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     addRobotComment(changeId, withFixRobotCommentInput);
 
     List<RobotCommentInfo> robotCommentInfos = getRobotComments();
@@ -517,8 +459,6 @@
 
   @Test
   public void replacementStringOfFixReplacementIsMandatory() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     fixReplacementInfo.replacement = null;
 
     exception.expect(BadRequestException.class);
@@ -532,8 +472,6 @@
 
   @Test
   public void fixWithinALineCanBeApplied() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     fixReplacementInfo.path = FILE_NAME;
     fixReplacementInfo.replacement = "Modified content";
     fixReplacementInfo.range = createRange(3, 1, 3, 3);
@@ -557,8 +495,6 @@
 
   @Test
   public void fixSpanningMultipleLinesCanBeApplied() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     fixReplacementInfo.path = FILE_NAME;
     fixReplacementInfo.replacement = "Modified content\n5";
     fixReplacementInfo.range = createRange(3, 2, 5, 3);
@@ -581,8 +517,6 @@
 
   @Test
   public void fixWithTwoCloseReplacementsOnSameFileCanBeApplied() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
     fixReplacementInfo1.path = FILE_NAME;
     fixReplacementInfo1.range = createRange(2, 0, 3, 0);
@@ -615,8 +549,6 @@
 
   @Test
   public void twoFixesOnSameFileCanBeApplied() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
     fixReplacementInfo1.path = FILE_NAME;
     fixReplacementInfo1.range = createRange(2, 0, 3, 0);
@@ -650,8 +582,6 @@
 
   @Test
   public void twoConflictingFixesOnSameFileCannotBeApplied() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
     fixReplacementInfo1.path = FILE_NAME;
     fixReplacementInfo1.range = createRange(2, 0, 3, 1);
@@ -679,8 +609,6 @@
 
   @Test
   public void twoFixesOfSameRobotCommentCanBeApplied() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
     fixReplacementInfo1.path = FILE_NAME;
     fixReplacementInfo1.range = createRange(2, 0, 3, 0);
@@ -714,8 +642,6 @@
 
   @Test
   public void fixReferringToDifferentFileThanRobotCommentCanBeApplied() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     fixReplacementInfo.path = FILE_NAME2;
     fixReplacementInfo.range = createRange(2, 0, 3, 0);
     fixReplacementInfo.replacement = "Modified content\n";
@@ -736,8 +662,6 @@
 
   @Test
   public void fixInvolvingTwoFilesCanBeApplied() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
     fixReplacementInfo1.path = FILE_NAME;
     fixReplacementInfo1.range = createRange(2, 0, 3, 0);
@@ -775,8 +699,6 @@
 
   @Test
   public void fixReferringToNonExistentFileCannotBeApplied() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     fixReplacementInfo.path = "a_non_existent_file.txt";
     fixReplacementInfo.range = createRange(1, 0, 2, 0);
     fixReplacementInfo.replacement = "Modified content\n";
@@ -792,8 +714,6 @@
 
   @Test
   public void fixOnPreviousPatchSetWithoutChangeEditCannotBeApplied() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     fixReplacementInfo.path = FILE_NAME;
     fixReplacementInfo.replacement = "Modified content";
     fixReplacementInfo.range = createRange(3, 1, 3, 3);
@@ -815,8 +735,6 @@
 
   @Test
   public void fixOnPreviousPatchSetWithExistingChangeEditCanBeApplied() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     // Create an empty change edit.
     gApi.changes().id(changeId).edit().create();
 
@@ -849,8 +767,6 @@
   @Test
   public void fixOnCurrentPatchSetWithChangeEditOnPreviousPatchSetCannotBeApplied()
       throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     // Create an empty change edit.
     gApi.changes().id(changeId).edit().create();
 
@@ -874,8 +790,6 @@
 
   @Test
   public void fixDoesNotModifyCommitMessageOfChangeEdit() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     String changeEditCommitMessage = "This is the commit message of the change edit.\n";
     gApi.changes().id(changeId).edit().modifyCommitMessage(changeEditCommitMessage);
 
@@ -897,8 +811,6 @@
 
   @Test
   public void applyingFixTwiceIsIdempotent() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     fixReplacementInfo.path = FILE_NAME;
     fixReplacementInfo.replacement = "Modified content";
     fixReplacementInfo.range = createRange(3, 1, 3, 3);
@@ -922,8 +834,6 @@
 
   @Test
   public void nonExistentFixCannotBeApplied() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     fixReplacementInfo.path = FILE_NAME;
     fixReplacementInfo.replacement = "Modified content";
     fixReplacementInfo.range = createRange(3, 1, 3, 3);
@@ -941,8 +851,6 @@
 
   @Test
   public void applyingFixReturnsEditInfoForCreatedChangeEdit() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     fixReplacementInfo.path = FILE_NAME;
     fixReplacementInfo.replacement = "Modified content";
     fixReplacementInfo.range = createRange(3, 1, 3, 3);
@@ -964,8 +872,6 @@
 
   @Test
   public void applyingFixOnTopOfChangeEditReturnsEditInfoForUpdatedChangeEdit() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     gApi.changes().id(changeId).edit().create();
 
     fixReplacementInfo.path = FILE_NAME;
@@ -989,7 +895,6 @@
 
   @Test
   public void createdChangeEditIsBasedOnCurrentPatchSet() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
     String currentRevision = gApi.changes().id(changeId).get().currentRevision;
 
     fixReplacementInfo.path = FILE_NAME;
@@ -1008,43 +913,22 @@
   }
 
   @Test
-  public void robotCommentsNotSupportedWithoutNoteDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isFalse();
-
-    RobotCommentInput in = createRobotCommentInput();
-    ReviewInput reviewInput = new ReviewInput();
-    Map<String, List<RobotCommentInput>> robotComments = new HashMap<>();
-    robotComments.put(in.path, ImmutableList.of(in));
-    reviewInput.robotComments = robotComments;
-    reviewInput.message = "comment test";
-
-    exception.expect(MethodNotAllowedException.class);
-    exception.expectMessage("robot comments not supported");
-    gApi.changes().id(changeId).current().review(reviewInput);
-  }
-
-  @Test
-  public void queryChangesWithUnresolvedCommentCount() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
+  public void queryChangesWithCommentCounts() throws Exception {
     PushOneCommit.Result r1 = createChange();
     PushOneCommit.Result r2 =
         pushFactory
-            .create(
-                db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "new content", r1.getChangeId())
+            .create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "new content", r1.getChangeId())
             .to("refs/for/master");
 
     addRobotComment(r2.getChangeId(), createRobotCommentInputWithMandatoryFields());
 
-    AcceptanceTestRequestScope.Context ctx = disableDb();
-    try {
+    try (AutoCloseable ignored = disableNoteDb()) {
       ChangeInfo result = Iterables.getOnlyElement(query(r2.getChangeId()));
       // currently, we create all robot comments as 'resolved' by default.
       // if we allow users to resolve a robot comment, then this test should
       // be modified.
       assertThat(result.unresolvedCommentCount).isEqualTo(0);
-    } finally {
-      enableDb(ctx);
+      assertThat(result.totalCommentCount).isEqualTo(1);
     }
   }
 
@@ -1122,7 +1006,7 @@
     assertThat(c.line).isEqualTo(expected.line);
     assertThat(c.message).isEqualTo(expected.message);
 
-    assertThat(c.author.email).isEqualTo(admin.email);
+    assertThat(c.author.email).isEqualTo(admin.email());
 
     if (expectPath) {
       assertThat(c.path).isEqualTo(expected.path);
diff --git a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
index 7573a09..1317ead 100644
--- a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
+++ b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
@@ -24,6 +24,7 @@
 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;
@@ -35,6 +36,8 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
 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.RawInputUtil;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.Permission;
@@ -43,7 +46,6 @@
 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.InheritableBoolean;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -51,11 +53,11 @@
 import com.google.gerrit.extensions.common.EditInfo;
 import com.google.gerrit.extensions.common.FileInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
+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.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.project.testing.Util;
 import com.google.gerrit.server.restapi.change.ChangeEdits.EditMessage;
@@ -64,7 +66,6 @@
 import com.google.gerrit.testing.TestTimeUtil;
 import com.google.gson.reflect.TypeToken;
 import com.google.gson.stream.JsonReader;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.sql.Timestamp;
@@ -78,7 +79,6 @@
 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.AfterClass;
 import org.junit.Before;
 import org.junit.BeforeClass;
@@ -94,7 +94,8 @@
   private static final String CONTENT_NEW2_STR = "quxÄÜÖßµ";
   private static final byte[] CONTENT_NEW2 = CONTENT_NEW2_STR.getBytes(UTF_8);
 
-  @Inject private SchemaFactory<ReviewDb> reviewDbProvider;
+  @Inject private ProjectOperations projectOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   private String changeId;
   private String changeId2;
@@ -112,17 +113,11 @@
 
   @Before
   public void setUp() throws Exception {
-    db = reviewDbProvider.open();
-    changeId = newChange(admin.getIdent());
+    changeId = newChange(admin.newIdent());
     ps = getCurrentPatchSet(changeId);
     assertThat(ps).isNotNull();
-    amendChange(admin.getIdent(), changeId);
-    changeId2 = newChange2(admin.getIdent());
-  }
-
-  @After
-  public void cleanup() {
-    db.close();
+    amendChange(admin.newIdent(), changeId);
+    changeId2 = newChange2(admin.newIdent());
   }
 
   @Test
@@ -146,7 +141,7 @@
   @Test
   public void deleteEditOfOlderPatchSet() throws Exception {
     createArbitraryEditFor(changeId2);
-    amendChange(admin.getIdent(), changeId2);
+    amendChange(admin.newIdent(), changeId2);
 
     gApi.changes().id(changeId2).edit().delete();
     assertThat(getEdit(changeId2)).isAbsent();
@@ -206,7 +201,7 @@
   @Test
   public void publishEditNotifyRest() throws Exception {
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     gApi.changes().id(changeId).addReviewer(in);
 
     createArbitraryEditFor(changeId);
@@ -221,7 +216,7 @@
   @Test
   public void publishEditWithDefaultNotify() throws Exception {
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     gApi.changes().id(changeId).addReviewer(in);
 
     createArbitraryEditFor(changeId);
@@ -239,20 +234,11 @@
   }
 
   @Test
-  public void publishEditRestWithoutCLA() throws Exception {
-    createArbitraryEditFor(changeId);
-    setUseContributorAgreements(InheritableBoolean.TRUE);
-    adminRestSession.post(urlPublish(changeId)).assertForbidden();
-    setUseContributorAgreements(InheritableBoolean.FALSE);
-    adminRestSession.post(urlPublish(changeId)).assertNoContent();
-  }
-
-  @Test
   public void rebaseEdit() throws Exception {
     PatchSet previousPatchSet = getCurrentPatchSet(changeId2);
     createEmptyEditFor(changeId2);
     gApi.changes().id(changeId2).edit().modifyFile(FILE_NAME, RawInputUtil.create(CONTENT_NEW));
-    amendChange(admin.getIdent(), changeId2);
+    amendChange(admin.newIdent(), changeId2);
     PatchSet currentPatchSet = getCurrentPatchSet(changeId2);
 
     Optional<EditInfo> originalEdit = getEdit(changeId2);
@@ -271,7 +257,7 @@
     PatchSet previousPatchSet = getCurrentPatchSet(changeId2);
     createEmptyEditFor(changeId2);
     gApi.changes().id(changeId2).edit().modifyFile(FILE_NAME, RawInputUtil.create(CONTENT_NEW));
-    amendChange(admin.getIdent(), changeId2);
+    amendChange(admin.newIdent(), changeId2);
     PatchSet currentPatchSet = getCurrentPatchSet(changeId2);
 
     Optional<EditInfo> originalEdit = getEdit(changeId2);
@@ -294,8 +280,7 @@
     assertThat(edit).value().baseRevision().isEqualTo(currentPatchSet.getRevision().get());
     PushOneCommit push =
         pushFactory.create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             FILE_NAME,
@@ -319,7 +304,7 @@
   public void updateRootCommitMessage() throws Exception {
     // Re-clone empty repo; TestRepository doesn't let us reset to unborn head.
     testRepo = cloneProject(project);
-    changeId = newChange(admin.getIdent());
+    changeId = newChange(admin.newIdent());
 
     createEmptyEditFor(changeId);
     Optional<EditInfo> edit = getEdit(changeId);
@@ -453,6 +438,16 @@
   }
 
   @Test
+  public void renameExistingFileToInvalidPath() throws Exception {
+    createEmptyEditFor(changeId);
+    BadRequestException badRequest =
+        assertThrows(
+            BadRequestException.class,
+            () -> gApi.changes().id(changeId).edit().renameFile(FILE_NAME, "invalid/path/"));
+    assertThat(badRequest.getMessage()).isEqualTo("Invalid path: invalid/path/");
+  }
+
+  @Test
   public void createEditByDeletingExistingFileRest() throws Exception {
     adminRestSession.delete(urlEditFile(changeId, FILE_NAME)).assertNoContent();
     assertThat(getFileContentOfEdit(changeId, FILE_NAME)).isAbsent();
@@ -645,11 +640,11 @@
     gApi.changes().id(changeId2).edit().publish(publishInput);
     assertThat(queryEdits()).isEmpty();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     createEmptyEditFor(changeId);
     assertThat(queryEdits()).hasSize(1);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     assertThat(queryEdits()).isEmpty();
   }
 
@@ -690,7 +685,7 @@
   @Test
   public void createEditWithoutPushPatchSetPermission() throws Exception {
     // Create new project with clean permissions
-    Project.NameKey p = createProject("addPatchSetEdit");
+    Project.NameKey p = projectOperations.newProject().create();
     // Clone repository as user
     TestRepository<InMemoryRepository> userTestRepo = cloneProject(p, user);
 
@@ -698,7 +693,7 @@
     block(p, "refs/for/*", Permission.ADD_PATCH_SET, REGISTERED_USERS);
 
     // Create change as user
-    PushOneCommit push = pushFactory.create(db, user.getIdent(), userTestRepo);
+    PushOneCommit push = pushFactory.create(user.newIdent(), userTestRepo);
     PushOneCommit.Result r1 = push.to("refs/for/master");
     r1.assertOkStatus();
 
@@ -753,14 +748,13 @@
   private String newChange(PersonIdent ident) throws Exception {
     PushOneCommit push =
         pushFactory.create(
-            db, ident, testRepo, PushOneCommit.SUBJECT, FILE_NAME, new String(CONTENT_OLD, UTF_8));
+            ident, testRepo, PushOneCommit.SUBJECT, FILE_NAME, new String(CONTENT_OLD, UTF_8));
     return push.to("refs/for/master").getChangeId();
   }
 
   private String amendChange(PersonIdent ident, String changeId) throws Exception {
     PushOneCommit push =
         pushFactory.create(
-            db,
             ident,
             testRepo,
             PushOneCommit.SUBJECT,
@@ -773,7 +767,7 @@
   private String newChange2(PersonIdent ident) throws Exception {
     PushOneCommit push =
         pushFactory.create(
-            db, ident, testRepo, PushOneCommit.SUBJECT, FILE_NAME, new String(CONTENT_OLD, UTF_8));
+            ident, testRepo, PushOneCommit.SUBJECT, FILE_NAME, new String(CONTENT_OLD, UTF_8));
     return push.rm("refs/for/master").getChangeId();
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractGitOverHttpServlet.java b/javatests/com/google/gerrit/acceptance/git/AbstractGitOverHttpServlet.java
index 2f54f88..fe1c264 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractGitOverHttpServlet.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractGitOverHttpServlet.java
@@ -16,8 +16,15 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.gerrit.server.AuditEvent;
-import java.util.Collections;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.FakeGroupAuditService;
+import com.google.gerrit.acceptance.Sandboxed;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.audit.HttpAuditEvent;
+import com.google.inject.Inject;
+import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.transport.CredentialsProvider;
 import org.eclipse.jgit.transport.RefSpec;
 import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
@@ -25,17 +32,20 @@
 import org.junit.Test;
 
 public class AbstractGitOverHttpServlet extends AbstractPushForReview {
+  @Inject protected FakeGroupAuditService auditService;
 
   @Before
   public void beforeEach() throws Exception {
     CredentialsProvider.setDefault(
-        new UsernamePasswordCredentialsProvider(admin.username, admin.httpPassword));
+        new UsernamePasswordCredentialsProvider(admin.username(), admin.httpPassword()));
     selectProtocol(AbstractPushForReview.Protocol.HTTP);
-    auditService.clearEvents();
+    // Don't clear audit events here, since we can't guarantee all test setup has run yet.
   }
 
   @Test
+  @Sandboxed
   public void receivePackAuditEventLog() throws Exception {
+    auditService.drainHttpAuditEvents();
     testRepo
         .git()
         .push()
@@ -43,26 +53,47 @@
         .setRefSpecs(new RefSpec("HEAD:refs/for/master"))
         .call();
 
-    // Git smart protocol makes two requests:
-    // https://github.com/git/git/blob/master/Documentation/technical/http-protocol.txt
-    assertThat(auditService.auditEvents.size()).isEqualTo(2);
+    ImmutableList<HttpAuditEvent> auditEvents = auditService.drainHttpAuditEvents();
+    assertThat(auditEvents).hasSize(2);
 
-    AuditEvent e = auditService.auditEvents.get(1);
-    assertThat(e.who.getAccountId()).isEqualTo(admin.id);
-    assertThat(e.what).endsWith("/git-receive-pack");
-    assertThat(e.params).isEmpty();
+    HttpAuditEvent lsRemote = auditEvents.get(0);
+    assertThat(lsRemote.who.getAccountId()).isEqualTo(admin.id());
+    assertThat(lsRemote.what).endsWith("/info/refs?service=git-receive-pack");
+    assertThat(lsRemote.params).containsExactly("service", "git-receive-pack");
+    assertThat(lsRemote.httpStatus).isEqualTo(HttpServletResponse.SC_OK);
+
+    HttpAuditEvent receivePack = auditEvents.get(1);
+    assertThat(receivePack.who.getAccountId()).isEqualTo(admin.id());
+    assertThat(receivePack.what).endsWith("/git-receive-pack");
+    assertThat(receivePack.params).isEmpty();
+    assertThat(receivePack.httpStatus).isEqualTo(HttpServletResponse.SC_OK);
   }
 
   @Test
+  @Sandboxed
   public void uploadPackAuditEventLog() throws Exception {
+    auditService.drainHttpAuditEvents();
+    // testRepo is already a clone. Make a server-side change so we have something to fetch.
+    try (Repository repo = repoManager.openRepository(project);
+        TestRepository<?> testRepo = new TestRepository<>(repo)) {
+      testRepo.branch("master").commit().create();
+    }
     testRepo.git().fetch().call();
 
-    assertThat(auditService.auditEvents.size()).isEqualTo(1);
+    ImmutableList<HttpAuditEvent> auditEvents = auditService.drainHttpAuditEvents();
+    assertThat(auditEvents).hasSize(2);
 
-    AuditEvent e = auditService.auditEvents.get(0);
-    assertThat(e.who.toString()).isEqualTo("ANONYMOUS");
-    assertThat(e.params.get("service"))
-        .containsExactlyElementsIn(Collections.singletonList("git-upload-pack"));
-    assertThat(e.what).endsWith("service=git-upload-pack");
+    HttpAuditEvent lsRemote = auditEvents.get(0);
+    // Repo URL doesn't include /a, so fetching doesn't cause authentication.
+    assertThat(lsRemote.who).isInstanceOf(AnonymousUser.class);
+    assertThat(lsRemote.what).endsWith("/info/refs?service=git-upload-pack");
+    assertThat(lsRemote.params).containsExactly("service", "git-upload-pack");
+    assertThat(lsRemote.httpStatus).isEqualTo(HttpServletResponse.SC_OK);
+
+    HttpAuditEvent uploadPack = auditEvents.get(1);
+    assertThat(lsRemote.who).isInstanceOf(AnonymousUser.class);
+    assertThat(uploadPack.what).endsWith("/git-upload-pack");
+    assertThat(uploadPack.params).isEmpty();
+    assertThat(uploadPack.httpStatus).isEqualTo(HttpServletResponse.SC_OK);
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 398ed84..216677d 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -18,7 +18,6 @@
 import static com.google.common.collect.ImmutableList.toImmutableList;
 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.assertPushOk;
 import static com.google.gerrit.acceptance.GitUtil.assertPushRejected;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
@@ -51,8 +50,10 @@
 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.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.Permission;
@@ -132,6 +133,7 @@
 import org.junit.BeforeClass;
 import org.junit.Test;
 
+@SkipProjectClone
 public abstract class AbstractPushForReview extends AbstractDaemonTest {
   protected enum Protocol {
     // TODO(dborowitz): TEST.
@@ -139,6 +141,9 @@
     HTTP
   }
 
+  @Inject private RequestScopeOperations requestScopeOperations;
+
+  private static String NEW_CHANGE_INDICATOR = " [NEW]";
   private LabelType patchSetLock;
 
   @Inject private DynamicSet<CommitValidationListener> commitValidators;
@@ -173,10 +178,10 @@
 
   @After
   public void resetPublishCommentOnPushOption() throws Exception {
-    setApiUser(admin);
-    GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id.get()).getPreferences();
+    requestScopeOperations.setApiUser(admin.id());
+    GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id().get()).getPreferences();
     prefs.publishCommentsOnPush = false;
-    gApi.accounts().id(admin.id.get()).setPreferences(prefs);
+    gApi.accounts().id(admin.id().get()).setPreferences(prefs);
   }
 
   protected void selectProtocol(Protocol p) throws Exception {
@@ -230,6 +235,60 @@
 
   @Test
   @TestProjectInput(createEmptyCommit = false)
+  public void pushInitialCommitSeriesForMasterBranch() throws Exception {
+    testPushInitialCommitSeriesForMasterBranch();
+  }
+
+  @Test
+  @TestProjectInput(createEmptyCommit = false)
+  public void pushInitialCommitSeriesForMasterBranchWithCreateNewChangeForAllNotInTarget()
+      throws Exception {
+    enableCreateNewChangeForAllNotInTarget();
+    testPushInitialCommitSeriesForMasterBranch();
+  }
+
+  private void testPushInitialCommitSeriesForMasterBranch() throws Exception {
+    RevCommit c = testRepo.commit().message("Initial commit").insertChangeId().create();
+    String id = GitUtil.getChangeId(testRepo, c).get();
+    testRepo.reset(c);
+
+    RevCommit c2 = testRepo.commit().parent(c).message("Second commit").insertChangeId().create();
+    String id2 = GitUtil.getChangeId(testRepo, c2).get();
+    testRepo.reset(c2);
+
+    String r = "refs/for/master";
+    PushResult pr = pushHead(testRepo, r, false);
+    assertPushOk(pr, r);
+
+    ChangeInfo change = gApi.changes().id(id).info();
+    assertThat(change.branch).isEqualTo("master");
+    assertThat(change.status).isEqualTo(ChangeStatus.NEW);
+
+    ChangeInfo change2 = gApi.changes().id(id2).info();
+    assertThat(change2.branch).isEqualTo("master");
+    assertThat(change2.status).isEqualTo(ChangeStatus.NEW);
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      assertThat(repo.resolve("master")).isNull();
+    }
+
+    gApi.changes().id(change.id).current().review(ReviewInput.approve());
+    gApi.changes().id(change.id).current().submit();
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      assertThat(repo.resolve("master")).isEqualTo(c);
+    }
+
+    gApi.changes().id(change2.id).current().review(ReviewInput.approve());
+    gApi.changes().id(change2.id).current().submit();
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      assertThat(repo.resolve("master")).isEqualTo(c2);
+    }
+  }
+
+  @Test
+  @TestProjectInput(createEmptyCommit = false)
   public void validateConnected() throws Exception {
     RevCommit c = testRepo.commit().message("Initial commit").insertChangeId().create();
     testRepo.reset(c);
@@ -270,8 +329,8 @@
         testRepo
             .commit()
             .message("Initial commit")
-            .author(admin.getIdent())
-            .committer(admin.getIdent())
+            .author(admin.newIdent())
+            .committer(admin.newIdent())
             .insertChangeId()
             .create();
     String id = GitUtil.getChangeId(testRepo, c).get();
@@ -303,8 +362,8 @@
         testRepo
             .commit()
             .message("Initial commit")
-            .author(admin.getIdent())
-            .committer(admin.getIdent())
+            .author(admin.newIdent())
+            .committer(admin.newIdent())
             .insertChangeId()
             .create();
     testRepo.reset(c);
@@ -327,7 +386,7 @@
     r1.assertOkStatus();
     r1.assertChange(Change.Status.NEW, null);
     r1.assertMessage(
-        "New changes:\n  " + url + id1 + " " + r1.getCommit().getShortMessage() + "\n");
+        url + id1 + " " + r1.getCommit().getShortMessage() + NEW_CHANGE_INDICATOR + "\n");
 
     testRepo.reset(initialHead);
     String newMsg = r1.getCommit().getShortMessage() + " v2";
@@ -339,7 +398,7 @@
         .create();
     PushOneCommit.Result r2 =
         pushFactory
-            .create(db, admin.getIdent(), testRepo, "another commit", "b.txt", "bbb")
+            .create(admin.newIdent(), testRepo, "another commit", "b.txt", "bbb")
             .to("refs/for/master");
     Change.Id id2 = r2.getChange().getId();
     r2.assertOkStatus();
@@ -347,18 +406,17 @@
     r2.assertMessage(
         "success\n"
             + "\n"
-            + "New changes:\n"
-            + "  "
-            + url
-            + id2
-            + " another commit\n"
-            + "\n"
-            + "Updated changes:\n"
             + "  "
             + url
             + id1
             + " "
             + newMsg
+            + "\n"
+            + "  "
+            + url
+            + id2
+            + " another commit"
+            + NEW_CHANGE_INDICATOR
             + "\n");
   }
 
@@ -390,8 +448,8 @@
         .branch("HEAD")
         .commit()
         .message("A change")
-        .author(admin.getIdent())
-        .committer(new PersonIdent(admin.getIdent(), testRepo.getDate()))
+        .author(admin.newIdent())
+        .committer(new PersonIdent(admin.newIdent(), testRepo.getDate()))
         .create();
     PushResult result = pushHead(testRepo, "refs/for/master");
     assertThat(result.getMessages()).contains("warning: pushing without Change-Id is deprecated");
@@ -444,7 +502,7 @@
     List<String> pushOptions = new ArrayList<>();
     pushOptions.add(topicOption);
 
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     push.setPushOptions(pushOptions);
     PushOneCommit.Result r = push.to("refs/for/master");
 
@@ -477,11 +535,11 @@
     pwi.filter = "*";
     pwi.notifyNewChanges = true;
     projectsToWatch.add(pwi);
-    setApiUser(user3);
+    requestScopeOperations.setApiUser(user3.id());
     gApi.accounts().self().setWatchedProjects(projectsToWatch);
 
     TestAccount user2 = accountCreator.user2();
-    String pushSpec = "refs/for/master%reviewer=" + user.email + ",cc=" + user2.email;
+    String pushSpec = "refs/for/master%reviewer=" + user.email() + ",cc=" + user2.email();
 
     sender.clear();
     PushOneCommit.Result r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE);
@@ -499,48 +557,44 @@
     r.assertOkStatus();
     assertThat(sender.getMessages()).hasSize(1);
     Message m = sender.getMessages().get(0);
-    if (notesMigration.readChanges()) {
-      assertThat(m.rcpt()).containsExactly(user.emailAddress);
-    } else {
-      // CCs are considered reviewers in the storage layer and so get notified.
-      assertThat(m.rcpt()).containsExactly(user.emailAddress, user2.emailAddress);
-    }
+    assertThat(m.rcpt()).containsExactly(user.getEmailAddress());
 
     sender.clear();
     r = pushTo(pushSpec + ",notify=" + NotifyHandling.ALL);
     r.assertOkStatus();
     assertThat(sender.getMessages()).hasSize(1);
     m = sender.getMessages().get(0);
-    assertThat(m.rcpt()).containsExactly(user.emailAddress, user2.emailAddress, user3.emailAddress);
+    assertThat(m.rcpt())
+        .containsExactly(user.getEmailAddress(), user2.getEmailAddress(), user3.getEmailAddress());
 
     sender.clear();
-    r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-to=" + user3.email);
+    r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-to=" + user3.email());
     r.assertOkStatus();
     assertNotifyTo(user3);
 
     sender.clear();
-    r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-cc=" + user3.email);
+    r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-cc=" + user3.email());
     r.assertOkStatus();
     assertNotifyCc(user3);
 
     sender.clear();
-    r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-bcc=" + user3.email);
+    r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-bcc=" + user3.email());
     r.assertOkStatus();
     assertNotifyBcc(user3);
 
     // request that sender gets notified as TO, CC and BCC, email should be sent
     // even if the sender is the only recipient
     sender.clear();
-    pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-to=" + admin.email);
+    pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-to=" + admin.email());
     assertNotifyTo(admin);
 
     sender.clear();
-    r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-cc=" + admin.email);
+    r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-cc=" + admin.email());
     r.assertOkStatus();
     assertNotifyCc(admin);
 
     sender.clear();
-    r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-bcc=" + admin.email);
+    r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-bcc=" + admin.email());
     r.assertOkStatus();
     assertNotifyBcc(admin);
   }
@@ -549,7 +603,7 @@
   public void pushForMasterWithCc() throws Exception {
     // cc one user
     String topic = "my/topic";
-    PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%cc=" + user.email);
+    PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%cc=" + user.email());
     r.assertOkStatus();
     r.assertChange(Change.Status.NEW, topic, ImmutableList.of(), ImmutableList.of(user));
 
@@ -559,11 +613,11 @@
             "refs/for/master/"
                 + topic
                 + "%cc="
-                + admin.email
+                + admin.email()
                 + ",cc="
-                + user.email
+                + user.email()
                 + ",cc="
-                + accountCreator.user2().email);
+                + accountCreator.user2().email());
     r.assertOkStatus();
     // Check that admin isn't CC'd as they own the change
     r.assertChange(
@@ -579,11 +633,11 @@
             "refs/for/master/"
                 + topic
                 + "%cc="
-                + admin.email
+                + admin.email()
                 + ",cc="
                 + nonExistingEmail
                 + ",cc="
-                + user.email);
+                + user.email());
     r.assertErrorStatus(nonExistingEmail + " does not identify a registered user or group");
   }
 
@@ -595,22 +649,18 @@
 
     PushOneCommit.Result r =
         pushTo("refs/for/master%cc=non.existing.1@example.com,cc=non.existing.2@example.com");
-    if (notesMigration.readChanges()) {
-      r.assertOkStatus();
+    r.assertOkStatus();
 
-      ChangeInfo ci = get(r.getChangeId(), DETAILED_LABELS);
-      ImmutableList<AccountInfo> ccs =
-          firstNonNull(ci.reviewers.get(ReviewerState.CC), ImmutableList.<AccountInfo>of()).stream()
-              .sorted(comparing((AccountInfo a) -> a.email))
-              .collect(toImmutableList());
-      assertThat(ccs).hasSize(2);
-      assertThat(ccs.get(0).email).isEqualTo("non.existing.1@example.com");
-      assertThat(ccs.get(0)._accountId).isNull();
-      assertThat(ccs.get(1).email).isEqualTo("non.existing.2@example.com");
-      assertThat(ccs.get(1)._accountId).isNull();
-    } else {
-      r.assertErrorStatus("non.existing.1@example.com does not identify a registered user");
-    }
+    ChangeInfo ci = get(r.getChangeId(), DETAILED_LABELS);
+    ImmutableList<AccountInfo> ccs =
+        firstNonNull(ci.reviewers.get(ReviewerState.CC), ImmutableList.<AccountInfo>of()).stream()
+            .sorted(comparing((AccountInfo a) -> a.email))
+            .collect(toImmutableList());
+    assertThat(ccs).hasSize(2);
+    assertThat(ccs.get(0).email).isEqualTo("non.existing.1@example.com");
+    assertThat(ccs.get(0)._accountId).isNull();
+    assertThat(ccs.get(1).email).isEqualTo("non.existing.2@example.com");
+    assertThat(ccs.get(1)._accountId).isNull();
   }
 
   @Test
@@ -619,7 +669,7 @@
     String group = name("group");
     GroupInput gin = new GroupInput();
     gin.name = group;
-    gin.members = ImmutableList.of(user.username, user2.username);
+    gin.members = ImmutableList.of(user.username(), user2.username());
     gin.visibleToAll = true; // TODO(dborowitz): Shouldn't be necessary; see ReviewerAdder.
     gApi.groups().create(gin);
 
@@ -632,7 +682,7 @@
   public void pushForMasterWithReviewer() throws Exception {
     // add one reviewer
     String topic = "my/topic";
-    PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%r=" + user.email);
+    PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%r=" + user.email());
     r.assertOkStatus();
     r.assertChange(Change.Status.NEW, topic, user);
 
@@ -644,11 +694,11 @@
             "refs/for/master/"
                 + topic
                 + "%r="
-                + admin.email
+                + admin.email()
                 + ",r="
-                + user.email
+                + user.email()
                 + ",r="
-                + user2.email);
+                + user2.email());
     r.assertOkStatus();
     // admin is the owner of the change and should not appear as reviewer
     r.assertChange(Change.Status.NEW, topic, user, user2);
@@ -660,11 +710,11 @@
             "refs/for/master/"
                 + topic
                 + "%r="
-                + admin.email
+                + admin.email()
                 + ",r="
                 + nonExistingEmail
                 + ",r="
-                + user.email);
+                + user.email());
     r.assertErrorStatus(nonExistingEmail + " does not identify a registered user or group");
   }
 
@@ -676,23 +726,19 @@
 
     PushOneCommit.Result r =
         pushTo("refs/for/master%r=non.existing.1@example.com,r=non.existing.2@example.com");
-    if (notesMigration.readChanges()) {
-      r.assertOkStatus();
+    r.assertOkStatus();
 
-      ChangeInfo ci = get(r.getChangeId(), DETAILED_LABELS);
-      ImmutableList<AccountInfo> reviewers =
-          firstNonNull(ci.reviewers.get(ReviewerState.REVIEWER), ImmutableList.<AccountInfo>of())
-              .stream()
-              .sorted(comparing((AccountInfo a) -> a.email))
-              .collect(toImmutableList());
-      assertThat(reviewers).hasSize(2);
-      assertThat(reviewers.get(0).email).isEqualTo("non.existing.1@example.com");
-      assertThat(reviewers.get(0)._accountId).isNull();
-      assertThat(reviewers.get(1).email).isEqualTo("non.existing.2@example.com");
-      assertThat(reviewers.get(1)._accountId).isNull();
-    } else {
-      r.assertErrorStatus("non.existing.1@example.com does not identify a registered user");
-    }
+    ChangeInfo ci = get(r.getChangeId(), DETAILED_LABELS);
+    ImmutableList<AccountInfo> reviewers =
+        firstNonNull(ci.reviewers.get(ReviewerState.REVIEWER), ImmutableList.<AccountInfo>of())
+            .stream()
+            .sorted(comparing((AccountInfo a) -> a.email))
+            .collect(toImmutableList());
+    assertThat(reviewers).hasSize(2);
+    assertThat(reviewers.get(0).email).isEqualTo("non.existing.1@example.com");
+    assertThat(reviewers.get(0)._accountId).isNull();
+    assertThat(reviewers.get(1).email).isEqualTo("non.existing.2@example.com");
+    assertThat(reviewers.get(1)._accountId).isNull();
   }
 
   @Test
@@ -701,7 +747,7 @@
     String group = name("group");
     GroupInput gin = new GroupInput();
     gin.name = group;
-    gin.members = ImmutableList.of(user.username, user2.username);
+    gin.members = ImmutableList.of(user.username(), user2.username());
     gin.visibleToAll = true; // TODO(dborowitz): Shouldn't be necessary; see ReviewerAdder.
     gApi.groups().create(gin);
 
@@ -807,9 +853,9 @@
   public void pushWorkInProgressChangeWhenNotOwner() throws Exception {
     TestRepository<?> userRepo = cloneProject(project, user);
     PushOneCommit.Result r =
-        pushFactory.create(db, user.getIdent(), userRepo).to("refs/for/master%wip");
+        pushFactory.create(user.newIdent(), userRepo).to("refs/for/master%wip");
     r.assertOkStatus();
-    assertThat(r.getChange().change().getOwner()).isEqualTo(user.id);
+    assertThat(r.getChange().change().getOwner()).isEqualTo(user.id());
     assertThat(r.getChange().change().isWorkInProgress()).isTrue();
 
     // Admin user trying to move from WIP to ready should succeed.
@@ -824,7 +870,7 @@
     assertThat(r.getChange().change().isWorkInProgress()).isTrue();
 
     // Push as change owner to move change from WIP to ready.
-    r = pushFactory.create(db, user.getIdent(), userRepo).to("refs/for/master%ready");
+    r = pushFactory.create(user.newIdent(), userRepo).to("refs/for/master%ready");
     r.assertOkStatus();
     assertThat(r.getChange().change().isWorkInProgress()).isFalse();
 
@@ -869,8 +915,7 @@
     assertThat(edit).isPresent();
     EditInfo editInfo = edit.get();
     r.assertMessage(
-        "Updated Changes:\n  "
-            + canonicalWebUrl.get()
+        canonicalWebUrl.get()
             + "c/"
             + project.get()
             + "/+/"
@@ -906,8 +951,7 @@
     enableCreateNewChangeForAllNotInTarget();
 
     PushOneCommit push =
-        pushFactory.create(
-            db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
+        pushFactory.create(admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
     // %2C is comma; the value below tests that percent decoding happens after splitting.
     // All three ways of representing space ("%20", "+", and "_" are also exercised.
     PushOneCommit.Result r = push.to("refs/for/master/%m=my_test%20+_message%2Cm=");
@@ -915,8 +959,7 @@
 
     push =
         pushFactory.create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             "b.txt",
@@ -993,8 +1036,7 @@
 
     PushOneCommit push =
         pushFactory.create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             "b.txt",
@@ -1015,8 +1057,7 @@
 
     push =
         pushFactory.create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             "c.txt",
@@ -1034,8 +1075,7 @@
 
     PushOneCommit push =
         pushFactory.create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             "b.txt",
@@ -1063,8 +1103,8 @@
     // Create a commit with different forged author and committer.
     RevCommit c =
         commitBuilder()
-            .author(user.getIdent())
-            .committer(user2.getIdent())
+            .author(user.newIdent())
+            .committer(user2.newIdent())
             .add(PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT)
             .message(PushOneCommit.SUBJECT)
             .create();
@@ -1072,13 +1112,13 @@
     pushHead(testRepo, "refs/for/master");
 
     String changeId = GitUtil.getChangeId(testRepo, c).get();
-    assertThat(getOwnerEmail(changeId)).isEqualTo(admin.email);
+    assertThat(getOwnerEmail(changeId)).isEqualTo(admin.email());
     assertThat(getReviewerEmails(changeId, ReviewerState.REVIEWER))
-        .containsExactly(user.email, user2.email);
+        .containsExactly(user.email(), user2.email());
 
     assertThat(sender.getMessages()).hasSize(1);
     assertThat(sender.getMessages().get(0).rcpt())
-        .containsExactly(user.emailAddress, user2.emailAddress);
+        .containsExactly(user.getEmailAddress(), user2.getEmailAddress());
   }
 
   @Test
@@ -1087,23 +1127,23 @@
     // First patch set has author and committer matching change owner.
     PushOneCommit.Result r = pushTo("refs/for/master");
 
-    assertThat(getOwnerEmail(r.getChangeId())).isEqualTo(admin.email);
+    assertThat(getOwnerEmail(r.getChangeId())).isEqualTo(admin.email());
     assertThat(getReviewerEmails(r.getChangeId(), ReviewerState.REVIEWER)).isEmpty();
 
     amendBuilder()
-        .author(user.getIdent())
-        .committer(user2.getIdent())
+        .author(user.newIdent())
+        .committer(user2.newIdent())
         .add(PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT + "2")
         .create();
     pushHead(testRepo, "refs/for/master");
 
-    assertThat(getOwnerEmail(r.getChangeId())).isEqualTo(admin.email);
+    assertThat(getOwnerEmail(r.getChangeId())).isEqualTo(admin.email());
     assertThat(getReviewerEmails(r.getChangeId(), ReviewerState.REVIEWER))
-        .containsExactly(user.email, user2.email);
+        .containsExactly(user.email(), user2.email());
 
     assertThat(sender.getMessages()).hasSize(1);
     assertThat(sender.getMessages().get(0).rcpt())
-        .containsExactly(user.emailAddress, user2.emailAddress);
+        .containsExactly(user.getEmailAddress(), user2.getEmailAddress());
   }
 
   /**
@@ -1121,8 +1161,8 @@
     // Create a commit with "User" as author and committer
     RevCommit c =
         commitBuilder()
-            .author(user.getIdent())
-            .committer(user.getIdent())
+            .author(user.newIdent())
+            .committer(user.newIdent())
             .add(PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT)
             .message(PushOneCommit.SUBJECT)
             .create();
@@ -1141,11 +1181,11 @@
         get(GitUtil.getChangeId(testRepo, c).get(), DETAILED_LABELS, MESSAGES, DETAILED_ACCOUNTS);
     LabelInfo cr = ci.labels.get("Code-Review");
     assertThat(cr.all).hasSize(2);
-    int indexAdmin = admin.fullName.equals(cr.all.get(0).name) ? 0 : 1;
+    int indexAdmin = admin.fullName().equals(cr.all.get(0).name) ? 0 : 1;
     int indexUser = indexAdmin == 0 ? 1 : 0;
-    assertThat(cr.all.get(indexAdmin).name).isEqualTo(admin.fullName);
+    assertThat(cr.all.get(indexAdmin).name).isEqualTo(admin.fullName());
     assertThat(cr.all.get(indexAdmin).value.intValue()).isEqualTo(1);
-    assertThat(cr.all.get(indexUser).name).isEqualTo(user.fullName);
+    assertThat(cr.all.get(indexUser).name).isEqualTo(user.fullName());
     assertThat(cr.all.get(indexUser).value.intValue()).isEqualTo(0);
     assertThat(Iterables.getLast(ci.messages).message)
         .isEqualTo("Uploaded patch set 1: Code-Review+1.");
@@ -1167,8 +1207,8 @@
 
     RevCommit c =
         commitBuilder()
-            .author(admin.getIdent())
-            .committer(admin.getIdent())
+            .author(admin.newIdent())
+            .committer(admin.newIdent())
             .add(PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT)
             .message(PushOneCommit.SUBJECT)
             .create();
@@ -1209,8 +1249,7 @@
     r.assertOkStatus();
     PushOneCommit push =
         pushFactory.create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             "b.txt",
@@ -1225,8 +1264,7 @@
     r.assertOkStatus();
     PushOneCommit push =
         pushFactory.create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             "b.txt",
@@ -1258,9 +1296,6 @@
 
   @Test
   public void pushForMasterWithHashtags() throws Exception {
-    // Hashtags only work when reading from NoteDB is enabled
-    assume().that(notesMigration.readChanges()).isTrue();
-
     // specify a single hashtag as option
     String hashtag1 = "tag1";
     Set<String> expected = ImmutableSet.of(hashtag1);
@@ -1275,8 +1310,7 @@
     String hashtag2 = "tag2";
     PushOneCommit push =
         pushFactory.create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             "b.txt",
@@ -1291,9 +1325,6 @@
 
   @Test
   public void pushForMasterWithMultipleHashtags() throws Exception {
-    // Hashtags only work when reading from NoteDB is enabled
-    assume().that(notesMigration.readChanges()).isTrue();
-
     // specify multiple hashtags as options
     String hashtag1 = "tag1";
     String hashtag2 = "tag2";
@@ -1311,8 +1342,7 @@
     String hashtag4 = "tag4";
     PushOneCommit push =
         pushFactory.create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             "b.txt",
@@ -1326,18 +1356,10 @@
   }
 
   @Test
-  public void pushForMasterWithHashtagsNoteDbDisabled() throws Exception {
-    // Push with hashtags should fail when reading from NoteDb is disabled.
-    assume().that(notesMigration.readChanges()).isFalse();
-    PushOneCommit.Result r = pushTo("refs/for/master%hashtag=tag1");
-    r.assertErrorStatus("cannot add hashtags; noteDb is disabled");
-  }
-
-  @Test
   public void pushCommitUsingSignedOffBy() throws Exception {
     PushOneCommit push =
         pushFactory.create(
-            db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
+            admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
 
@@ -1346,11 +1368,10 @@
 
     push =
         pushFactory.create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT
-                + String.format("\n\nSigned-off-by: %s <%s>", admin.fullName, admin.email),
+                + String.format("\n\nSigned-off-by: %s <%s>", admin.fullName(), admin.email()),
             "b.txt",
             "anotherContent");
     r = push.to("refs/for/master");
@@ -1358,7 +1379,7 @@
 
     push =
         pushFactory.create(
-            db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
+            admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
     r = push.to("refs/for/master");
     r.assertErrorStatus("not Signed-off-by author/committer/uploader in message footer");
   }
@@ -1368,14 +1389,13 @@
     enableCreateNewChangeForAllNotInTarget();
 
     PushOneCommit push =
-        pushFactory.create(
-            db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
+        pushFactory.create(admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
 
     push =
         pushFactory.create(
-            db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
+            admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
     r = push.to("refs/for/master");
     r.assertOkStatus();
 
@@ -1393,8 +1413,7 @@
 
     // create a change as admin
     PushOneCommit push =
-        pushFactory.create(
-            db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
+        pushFactory.create(admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
     RevCommit commitChange1 = r.getCommit();
@@ -1405,7 +1424,7 @@
     userRepo.reset("change");
     push =
         pushFactory.create(
-            db, user.getIdent(), userRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
+            user.newIdent(), userRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
     r = push.to("refs/for/master");
     r.assertOkStatus();
 
@@ -1423,14 +1442,13 @@
 
     PushOneCommit push =
         pushFactory.create(
-            db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
+            admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
 
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
 
     PushResult pr =
         GitUtil.pushHead(testRepo, "refs/for/foo%base=" + rBase.getCommit().name(), false, false);
-    assertThat(pr.getMessages()).containsMatch("changes: .*new: 1.*done");
 
     // BatchUpdate implementations differ in how they hook into progress monitors. We mostly just
     // care that there is a new change.
@@ -1440,24 +1458,16 @@
 
   @Test
   public void pushSameCommitTwice() throws Exception {
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      u.getConfig()
-          .getProject()
-          .setBooleanConfig(
-              BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET,
-              InheritableBoolean.TRUE);
-      u.save();
-    }
+    enableCreateNewChangeForAllNotInTarget();
 
     PushOneCommit push =
-        pushFactory.create(
-            db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
+        pushFactory.create(admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
 
     push =
         pushFactory.create(
-            db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
+            admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
     r = push.to("refs/for/master");
     r.assertOkStatus();
 
@@ -1469,24 +1479,16 @@
 
   @Test
   public void pushSameCommitTwiceWhenIndexFailed() throws Exception {
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      u.getConfig()
-          .getProject()
-          .setBooleanConfig(
-              BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET,
-              InheritableBoolean.TRUE);
-      u.save();
-    }
+    enableCreateNewChangeForAllNotInTarget();
 
     PushOneCommit push =
-        pushFactory.create(
-            db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
+        pushFactory.create(admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
 
     push =
         pushFactory.create(
-            db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
+            admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
     r = push.to("refs/for/master");
     r.assertOkStatus();
 
@@ -1572,6 +1574,32 @@
   }
 
   @Test
+  public void pushWithChangeIdAboveFooter() throws Exception {
+    testPushWithChangeIdAboveFooter();
+  }
+
+  @Test
+  public void pushWithChangeIdAboveFooterWithCreateNewChangeForAllNotInTarget() throws Exception {
+    enableCreateNewChangeForAllNotInTarget();
+    testPushWithChangeIdAboveFooter();
+  }
+
+  private void testPushWithChangeIdAboveFooter() throws Exception {
+    RevCommit c =
+        createCommit(
+            testRepo,
+            PushOneCommit.SUBJECT
+                + "\n\n"
+                + "Change-Id: Ied70ea827f5bf968f1f6aaee6594e07c846d217a\n\n"
+                + "More text, uh oh.\n");
+    assertThat(GitUtil.getChangeId(testRepo, c)).isEmpty();
+    pushForReviewRejected(testRepo, "Change-Id must be in message footer");
+
+    setRequireChangeId(InheritableBoolean.FALSE);
+    pushForReviewRejected(testRepo, "Change-Id must be in message footer");
+  }
+
+  @Test
   public void errorMessageFormat() throws Exception {
     RevCommit c = createCommit(testRepo, "Message without Change-Id");
     assertThat(GitUtil.getChangeId(testRepo, c)).isEmpty();
@@ -1594,8 +1622,7 @@
     r.assertOkStatus();
     PushOneCommit push =
         pushFactory.create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT
                 + "\n\n"
@@ -1690,8 +1717,7 @@
   @Test
   public void pushCommitWithSameChangeIdAsPredecessorChange() throws Exception {
     PushOneCommit push =
-        pushFactory.create(
-            db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
+        pushFactory.create(admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
     RevCommit commitChange1 = r.getCommit();
@@ -1770,8 +1796,8 @@
     Change.Id id2 = r2.getChange().getId();
 
     // Merge change 1 behind Gerrit's back.
-    try (Repository repo = repoManager.openRepository(project)) {
-      TestRepository<?> tr = new TestRepository<>(repo);
+    try (Repository repo = repoManager.openRepository(project);
+        TestRepository<?> tr = new TestRepository<>(repo)) {
       tr.branch("refs/heads/master").update(r1.getCommit());
     }
 
@@ -1800,7 +1826,7 @@
 
     // Added a new patch set and auto-closed the change.
     cd = byChangeId(id);
-    assertThat(cd.change().getStatus()).isEqualTo(Change.Status.MERGED);
+    assertThat(cd.change().isMerged()).isTrue();
     assertThat(getPatchSetRevisions(cd))
         .containsExactlyEntriesIn(
             ImmutableMap.of(1, ps1Rev, 2, testRepo.getRepository().resolve("HEAD").name()));
@@ -1818,7 +1844,7 @@
 
     // Change not updated.
     cd = byChangeId(id);
-    assertThat(cd.change().getStatus()).isEqualTo(Change.Status.NEW);
+    assertThat(cd.change().isNew()).isTrue();
     assertThat(getPatchSetRevisions(cd)).containsExactlyEntriesIn(ImmutableMap.of(1, ps1Rev));
   }
 
@@ -1826,7 +1852,7 @@
   public void forcePushAbandonedChange() throws Exception {
     grant(project, "refs/*", Permission.PUSH, true);
     PushOneCommit push1 =
-        pushFactory.create(db, admin.getIdent(), testRepo, "change1", "a.txt", "content");
+        pushFactory.create(admin.newIdent(), testRepo, "change1", "a.txt", "content");
     PushOneCommit.Result r = push1.to("refs/for/master");
     r.assertOkStatus();
 
@@ -1850,12 +1876,12 @@
     Change c = r.getChange().change();
 
     RevCommit ps2Commit;
-    try (Repository repo = repoManager.openRepository(project)) {
+    try (Repository repo = repoManager.openRepository(project);
+        TestRepository<?> tr = new TestRepository<>(repo)) {
       // Create a new patch set of the change directly in Gerrit's repository,
       // without pushing it. In reality it's more likely that the client would
       // create and push this behind Gerrit's back (e.g. an admin accidentally
       // using direct ssh access to the repo), but that's harder to do in tests.
-      TestRepository<?> tr = new TestRepository<>(repo);
       ps2Commit =
           tr.branch("refs/heads/master")
               .commit()
@@ -1868,7 +1894,7 @@
     testRepo.reset(ps2Commit);
 
     ChangeData cd = byCommit(ps1Commit);
-    assertThat(cd.change().getStatus()).isEqualTo(Change.Status.NEW);
+    assertThat(cd.change().isNew()).isTrue();
     assertThat(getPatchSetRevisions(cd))
         .containsExactlyEntriesIn(ImmutableMap.of(1, ps1Commit.name()));
     return c.getId();
@@ -1876,12 +1902,12 @@
 
   @Test
   public void pushWithEmailInFooter() throws Exception {
-    pushWithReviewerInFooter(user.emailAddress.toString(), user);
+    pushWithReviewerInFooter(user.getEmailAddress().toString(), user);
   }
 
   @Test
   public void pushWithNameInFooter() throws Exception {
-    pushWithReviewerInFooter(user.fullName, user);
+    pushWithReviewerInFooter(user.fullName(), user);
   }
 
   @Test
@@ -1907,8 +1933,7 @@
     r.assertOkStatus();
     PushOneCommit push =
         pushFactory.create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             "b.txt",
@@ -1972,8 +1997,8 @@
     gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
     gApi.changes().id(r.getChangeId()).current().submit();
 
-    try (Repository repo = repoManager.openRepository(project)) {
-      TestRepository<?> tr = new TestRepository<>(repo);
+    try (Repository repo = repoManager.openRepository(project);
+        TestRepository<Repository> tr = new TestRepository<>(repo)) {
       tr.branch("refs/heads/branch").commit().message("Initial commit on branch").create();
     }
 
@@ -2031,8 +2056,8 @@
     // expecting the change to be auto-closed, but the change metadata update
     // fails.
     ObjectId c2;
-    try (Repository repo = repoManager.openRepository(project)) {
-      TestRepository<?> tr = new TestRepository<>(repo);
+    try (Repository repo = repoManager.openRepository(project);
+        TestRepository<Repository> tr = new TestRepository<>(repo)) {
       RevCommit commit2 =
           tr.amend(c1).message("New subject").insertChangeId(r.getChangeId().substring(1)).create();
       c2 = commit2.copy();
@@ -2065,7 +2090,7 @@
 
     assertThat(getPublishedComments(r.getChangeId())).isEmpty();
 
-    gApi.changes().id(r.getChangeId()).addReviewer(user.email);
+    gApi.changes().id(r.getChangeId()).addReviewer(user.email());
     sender.clear();
     amendChange(r.getChangeId(), "refs/for/master%publish-comments");
 
@@ -2179,9 +2204,9 @@
 
     assertThat(getPublishedComments(r.getChangeId())).isEmpty();
 
-    GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id.get()).getPreferences();
+    GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id().get()).getPreferences();
     prefs.publishCommentsOnPush = true;
-    gApi.accounts().id(admin.id.get()).setPreferences(prefs);
+    gApi.accounts().id(admin.id().get()).setPreferences(prefs);
 
     r = amendChange(r.getChangeId());
     assertThat(getPublishedComments(r.getChangeId()).stream().map(c -> c.message))
@@ -2193,9 +2218,9 @@
     PushOneCommit.Result r = createChange();
     addDraft(r.getChangeId(), r.getCommit().name(), newDraft(FILE_NAME, 1, "comment1"));
 
-    GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id.get()).getPreferences();
+    GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id().get()).getPreferences();
     prefs.publishCommentsOnPush = true;
-    gApi.accounts().id(admin.id.get()).setPreferences(prefs);
+    gApi.accounts().id(admin.id().get()).setPreferences(prefs);
 
     r = amendChange(r.getChangeId(), "refs/for/master%no-publish-comments");
 
@@ -2204,7 +2229,6 @@
 
   @Test
   public void noEditAndUpdateAllUsersInSameChangeStack() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
     List<RevCommit> commits = createChanges(2, "refs/for/master");
     String id2 = byCommit(commits.get(1)).change().getKey().get();
     addDraft(id2, commits.get(1).name(), newDraft(FILE_NAME, 1, "comment2"));
@@ -2351,7 +2375,7 @@
     try {
       // Validation listener is called on normal push
       PushOneCommit push =
-          pushFactory.create(db, admin.getIdent(), testRepo, "change1", "a.txt", "content");
+          pushFactory.create(admin.newIdent(), testRepo, "change1", "a.txt", "content");
       PushOneCommit.Result r = push.to(master);
       r.assertOkStatus();
       assertThat(validator.count()).isEqualTo(1);
@@ -2359,7 +2383,7 @@
       // Push is rejected and validation listener is not called when not allowed
       // to use skip option
       PushOneCommit push2 =
-          pushFactory.create(db, admin.getIdent(), testRepo, "change2", "b.txt", "content");
+          pushFactory.create(admin.newIdent(), testRepo, "change2", "b.txt", "content");
       push2.setPushOptions(ImmutableList.of(PUSH_OPTION_SKIP_VALIDATION));
       r = push2.to(master);
       r.assertErrorStatus("not permitted: skip validation");
@@ -2368,7 +2392,7 @@
       // Validation listener is not called when skip option is used
       grantSkipValidation(project, master, SystemGroupBackend.REGISTERED_USERS);
       PushOneCommit push3 =
-          pushFactory.create(db, admin.getIdent(), testRepo, "change2", "b.txt", "content");
+          pushFactory.create(admin.newIdent(), testRepo, "change2", "b.txt", "content");
       push3.setPushOptions(ImmutableList.of(PUSH_OPTION_SKIP_VALIDATION));
       r = push3.to(master);
       r.assertOkStatus();
@@ -2379,7 +2403,7 @@
       TestValidator validator2 = new TestValidator(true);
       handle2 = commitValidators.add("test-validator-2", validator2);
       PushOneCommit push4 =
-          pushFactory.create(db, admin.getIdent(), testRepo, "change2", "b.txt", "content");
+          pushFactory.create(admin.newIdent(), testRepo, "change2", "b.txt", "content");
       push4.setPushOptions(ImmutableList.of(PUSH_OPTION_SKIP_VALIDATION));
       r = push4.to(master);
       r.assertOkStatus();
@@ -2396,16 +2420,6 @@
   }
 
   @Test
-  public void pushToPublishMagicBranchIsAllowed() throws Exception {
-    // Push to "refs/publish/*" will be a synonym of "refs/for/*".
-    createChange("refs/publish/master");
-    PushOneCommit.Result result = pushTo("refs/publish/master");
-    result.assertOkStatus();
-    assertThat(result.getMessage())
-        .endsWith("Pushing to refs/publish/* is deprecated, use refs/for/* instead.\n");
-  }
-
-  @Test
   public void pushNoteDbRef() throws Exception {
     String ref = "refs/changes/34/1234/meta";
     RevCommit c = testRepo.commit().message("Junk NoteDb commit").create();
@@ -2452,6 +2466,181 @@
     assertPushOk(pr, "refs/heads/permitted");
   }
 
+  @Test
+  public void pushCommitsWithSameTreeNoChanges() throws Exception {
+    RevCommit c =
+        testRepo
+            .commit()
+            .message("Foo")
+            .parent(getHead(testRepo.getRepository(), "HEAD"))
+            .insertChangeId()
+            .create();
+    testRepo.reset(c);
+
+    String r = "refs/for/master";
+    PushResult pr = pushHead(testRepo, r, false);
+    assertPushOk(pr, r);
+
+    RevCommit amended = testRepo.amend(c).create();
+    testRepo.reset(amended);
+
+    pr = pushHead(testRepo, r, false);
+    assertPushOk(pr, r);
+    assertThat(pr.getMessages())
+        .contains(
+            "warning: no changes between prior commit "
+                + c.abbreviate(7).name()
+                + " and new commit "
+                + amended.abbreviate(7).name());
+  }
+
+  @Test
+  public void pushCommitsWithSameTreeNoFilesChangedMessageUpdated() throws Exception {
+    RevCommit c =
+        testRepo
+            .commit()
+            .message("Foo")
+            .parent(getHead(testRepo.getRepository(), "HEAD"))
+            .insertChangeId()
+            .create();
+    String id = GitUtil.getChangeId(testRepo, c).get();
+    testRepo.reset(c);
+
+    String r = "refs/for/master";
+    PushResult pr = pushHead(testRepo, r, false);
+    assertPushOk(pr, r);
+
+    RevCommit amended =
+        testRepo.amend(c).message("Foo Bar").insertChangeId(id.substring(1)).create();
+    testRepo.reset(amended);
+
+    pr = pushHead(testRepo, r, false);
+    assertPushOk(pr, r);
+    assertThat(pr.getMessages())
+        .contains(
+            "warning: " + amended.abbreviate(7).name() + ": no files changed, message updated");
+  }
+
+  @Test
+  public void pushCommitsWithSameTreeNoFilesChangedAuthorChanged() throws Exception {
+    RevCommit c =
+        testRepo
+            .commit()
+            .message("Foo")
+            .parent(getHead(testRepo.getRepository(), "HEAD"))
+            .insertChangeId()
+            .create();
+    testRepo.reset(c);
+
+    String r = "refs/for/master";
+    PushResult pr = pushHead(testRepo, r, false);
+    assertPushOk(pr, r);
+
+    RevCommit amended = testRepo.amend(c).author(user.newIdent()).create();
+    testRepo.reset(amended);
+
+    pr = pushHead(testRepo, r, false);
+    assertPushOk(pr, r);
+    assertThat(pr.getMessages())
+        .contains(
+            "warning: " + amended.abbreviate(7).name() + ": no files changed, author changed");
+  }
+
+  @Test
+  public void pushCommitsWithSameTreeNoFilesChangedWasRebased() throws Exception {
+    RevCommit head = getHead(testRepo.getRepository(), "HEAD");
+    RevCommit c = testRepo.commit().message("Foo").parent(head).insertChangeId().create();
+    testRepo.reset(c);
+
+    String r = "refs/for/master";
+    PushResult pr = pushHead(testRepo, r, false);
+    assertPushOk(pr, r);
+
+    testRepo.reset(head);
+    RevCommit newBase = testRepo.commit().message("Base").parent(head).insertChangeId().create();
+    testRepo.reset(newBase);
+
+    pr = pushHead(testRepo, r, false);
+    assertPushOk(pr, r);
+
+    testRepo.reset(c);
+    RevCommit amended = testRepo.amend(c).parent(newBase).create();
+    testRepo.reset(amended);
+
+    pr = pushHead(testRepo, r, false);
+    assertPushOk(pr, r);
+    assertThat(pr.getMessages())
+        .contains("warning: " + amended.abbreviate(7).name() + ": no files changed, was rebased");
+  }
+
+  @Test
+  public void sequentialCommitMessages() throws Exception {
+    String url = canonicalWebUrl.get() + "c/" + project.get() + "/+/";
+    ObjectId initialHead = testRepo.getRepository().resolve("HEAD");
+
+    PushOneCommit.Result r1 = pushTo("refs/for/master");
+    Change.Id id1 = r1.getChange().getId();
+    r1.assertOkStatus();
+    r1.assertChange(Change.Status.NEW, null);
+    r1.assertMessage(
+        url + id1 + " " + r1.getCommit().getShortMessage() + NEW_CHANGE_INDICATOR + "\n");
+
+    PushOneCommit.Result r2 = pushTo("refs/for/master");
+    Change.Id id2 = r2.getChange().getId();
+    r2.assertOkStatus();
+    r2.assertChange(Change.Status.NEW, null);
+    r2.assertMessage(
+        url + id2 + " " + r2.getCommit().getShortMessage() + NEW_CHANGE_INDICATOR + "\n");
+
+    testRepo.reset(initialHead);
+
+    // rearrange the commit so that change no. 2 is the parent of change no. 1
+    String r1Message = "Position 2";
+    String r2Message = "Position 1";
+    testRepo
+        .branch("HEAD")
+        .commit()
+        .message(r2Message)
+        .insertChangeId(r2.getChangeId().substring(1))
+        .create();
+    testRepo
+        .branch("HEAD")
+        .commit()
+        .message(r1Message)
+        .insertChangeId(r1.getChangeId().substring(1))
+        .create();
+
+    PushOneCommit.Result r3 =
+        pushFactory
+            .create(admin.newIdent(), testRepo, "another commit", "b.txt", "bbb")
+            .to("refs/for/master");
+    Change.Id id3 = r3.getChange().getId();
+    r3.assertOkStatus();
+    r3.assertChange(Change.Status.NEW, null);
+    // should display commit r2, r1, r3 in that order.
+    r3.assertMessage(
+        "success\n"
+            + "\n"
+            + "  "
+            + url
+            + id2
+            + " "
+            + r2Message
+            + "\n"
+            + "  "
+            + url
+            + id1
+            + " "
+            + r1Message
+            + "\n"
+            + "  "
+            + url
+            + id3
+            + " another commit"
+            + NEW_CHANGE_INDICATOR
+            + "\n");
+  }
+
   private DraftInput newDraft(String path, int line, String message) {
     DraftInput d = new DraftInput();
     d.path = path;
@@ -2482,7 +2671,7 @@
     assertThat(ci.reviewers).isNotNull();
     assertThat(ci.reviewers.keySet()).containsExactly(ReviewerState.REVIEWER);
     assertThat(ci.reviewers.get(ReviewerState.REVIEWER).iterator().next().email)
-        .isEqualTo(reviewer.email);
+        .isEqualTo(reviewer.email());
   }
 
   private void pushWithReviewerInFooter(String nameEmail, TestAccount expectedReviewer)
@@ -2496,9 +2685,9 @@
       ChangeData cd = byCommit(c);
       String name = "reviewers for " + (i + 1);
       if (expectedReviewer != null) {
-        assertThat(cd.reviewers().all()).named(name).containsExactly(expectedReviewer.getId());
+        assertThat(cd.reviewers().all()).named(name).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.getId().toString()).remove();
+        gApi.changes().id(cd.getId().get()).reviewer(expectedReviewer.id().toString()).remove();
       }
       assertThat(byCommit(c).reviewers().all()).named(name).isEmpty();
     }
@@ -2509,7 +2698,7 @@
       ChangeData cd = byCommit(c);
       String name = "reviewers for " + (i + 1);
       if (expectedReviewer != null) {
-        assertThat(cd.reviewers().all()).named(name).containsExactly(expectedReviewer.getId());
+        assertThat(cd.reviewers().all()).named(name).containsExactly(expectedReviewer.id());
       } else {
         assertThat(byCommit(c).reviewers().all()).named(name).isEmpty();
       }
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java b/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
index 943b052..d1349d0 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
@@ -19,13 +19,14 @@
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.common.Nullable;
+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.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;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.StreamSupport;
@@ -53,9 +54,16 @@
 import org.eclipse.jgit.transport.RefSpec;
 import org.eclipse.jgit.transport.RemoteRefUpdate;
 import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
+import org.junit.Before;
 
 public abstract class AbstractSubmoduleSubscription extends AbstractDaemonTest {
 
+  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);
   }
@@ -95,34 +103,27 @@
     return cfg;
   }
 
-  protected TestRepository<?> createProjectWithPush(
-      String name,
-      @Nullable Project.NameKey parent,
-      boolean createEmptyCommit,
-      SubmitType submitType)
-      throws Exception {
-    Project.NameKey project = createProject(name, parent, createEmptyCommit, submitType);
+  protected void grantPush(Project.NameKey project) throws Exception {
     grant(project, "refs/heads/*", Permission.PUSH);
     grant(project, "refs/for/refs/heads/*", Permission.SUBMIT);
-    return cloneProject(project);
   }
 
-  protected TestRepository<?> createProjectWithPush(String name, @Nullable Project.NameKey parent)
-      throws Exception {
-    return createProjectWithPush(name, parent, true, getSubmitType());
-  }
-
-  protected TestRepository<?> createProjectWithPush(String name, boolean createEmptyCommit)
-      throws Exception {
-    return createProjectWithPush(name, null, createEmptyCommit, getSubmitType());
-  }
-
-  protected TestRepository<?> createProjectWithPush(String name) throws Exception {
-    return createProjectWithPush(name, null, true, getSubmitType());
+  protected Project.NameKey createProjectForPush(SubmitType submitType) throws Exception {
+    Project.NameKey project = projectOperations.newProject().submitType(submitType).create();
+    grantPush(project);
+    return project;
   }
 
   private static AtomicInteger contentCounter = new AtomicInteger(0);
 
+  @Before
+  public void setUp() throws Exception {
+    superKey = createProjectForPush(getSubmitType());
+    subKey = createProjectForPush(getSubmitType());
+    superRepo = cloneProject(superKey);
+    subRepo = cloneProject(subKey);
+  }
+
   protected ObjectId pushChangeTo(
       TestRepository<?> repo, String ref, String file, String content, String message, String topic)
       throws Exception {
@@ -181,19 +182,27 @@
     return Iterables.getLast(res).getRemoteUpdate(remoteBranch).getNewObjectId();
   }
 
-  protected void allowSubmoduleSubscription(
-      String submodule, String subBranch, String superproject, String superBranch, boolean match)
+  protected void allowMatchingSubmoduleSubscription(
+      Project.NameKey submodule, String subBranch, Project.NameKey superproject, String superBranch)
       throws Exception {
-    Project.NameKey sub = new Project.NameKey(name(submodule));
-    Project.NameKey superName = new Project.NameKey(name(superproject));
-    try (MetaDataUpdate md = metaDataUpdateFactory.create(sub)) {
+    allowSubmoduleSubscription(submodule, subBranch, superproject, superBranch, true);
+  }
+
+  protected void allowSubmoduleSubscription(
+      Project.NameKey submodule,
+      String subBranch,
+      Project.NameKey superproject,
+      String superBranch,
+      boolean match)
+      throws Exception {
+    try (MetaDataUpdate md = metaDataUpdateFactory.create(submodule)) {
       md.setMessage("Added superproject subscription");
       SubscribeSection s;
-      ProjectConfig pc = ProjectConfig.read(md);
-      if (pc.getSubscribeSections().containsKey(superName)) {
-        s = pc.getSubscribeSections().get(superName);
+      ProjectConfig pc = projectConfigFactory.read(md);
+      if (pc.getSubscribeSections().containsKey(superproject)) {
+        s = pc.getSubscribeSections().get(superproject);
       } else {
-        s = new SubscribeSection(superName);
+        s = new SubscribeSection(superproject);
       }
       String refspec;
       if (superBranch == null) {
@@ -214,14 +223,11 @@
     }
   }
 
-  protected void allowMatchingSubmoduleSubscription(
-      String submodule, String subBranch, String superproject, String superBranch)
-      throws Exception {
-    allowSubmoduleSubscription(submodule, subBranch, superproject, superBranch, true);
-  }
-
   protected void createSubmoduleSubscription(
-      TestRepository<?> repo, String branch, String subscribeToRepo, String subscribeToBranch)
+      TestRepository<?> repo,
+      String branch,
+      Project.NameKey subscribeToRepo,
+      String subscribeToBranch)
       throws Exception {
     Config config = new Config();
     prepareSubmoduleConfigEntry(config, subscribeToRepo, subscribeToBranch);
@@ -232,7 +238,7 @@
       TestRepository<?> repo,
       String branch,
       String subscribeToRepoPrefix,
-      String subscribeToRepo,
+      Project.NameKey subscribeToRepo,
       String subscribeToBranch)
       throws Exception {
     Config config = new Config();
@@ -244,19 +250,18 @@
   protected void prepareRelativeSubmoduleConfigEntry(
       Config config,
       String subscribeToRepoPrefix,
-      String subscribeToRepo,
+      Project.NameKey subscribeToRepo,
       String subscribeToBranch) {
-    subscribeToRepo = name(subscribeToRepo);
-    String url = subscribeToRepoPrefix + subscribeToRepo;
-    config.setString("submodule", subscribeToRepo, "path", subscribeToRepo);
-    config.setString("submodule", subscribeToRepo, "url", url);
+    String url = subscribeToRepoPrefix + subscribeToRepo.get();
+    config.setString("submodule", subscribeToRepo.get(), "path", subscribeToRepo.get());
+    config.setString("submodule", subscribeToRepo.get(), "url", url);
     if (subscribeToBranch != null) {
-      config.setString("submodule", subscribeToRepo, "branch", subscribeToBranch);
+      config.setString("submodule", subscribeToRepo.get(), "branch", subscribeToBranch);
     }
   }
 
   protected void prepareSubmoduleConfigEntry(
-      Config config, String subscribeToRepo, String subscribeToBranch) {
+      Config config, Project.NameKey subscribeToRepo, String subscribeToBranch) {
     // The submodule subscription module checks for gerrit.canonicalWebUrl to
     // detect if it's configured for automatic updates. It doesn't matter if
     // it serves from that URL.
@@ -264,17 +269,18 @@
   }
 
   protected void prepareSubmoduleConfigEntry(
-      Config config, String subscribeToRepo, String subscribeToRepoPath, String subscribeToBranch) {
-    subscribeToRepo = name(subscribeToRepo);
-    subscribeToRepoPath = name(subscribeToRepoPath);
+      Config config,
+      Project.NameKey subscribeToRepo,
+      Project.NameKey subscribeToRepoPath,
+      String subscribeToBranch) {
     // The submodule subscription module checks for gerrit.canonicalWebUrl to
     // detect if it's configured for automatic updates. It doesn't matter if
     // it serves from that URL.
     String url = cfg.getString("gerrit", null, "canonicalWebUrl") + "/" + subscribeToRepo;
-    config.setString("submodule", subscribeToRepoPath, "path", subscribeToRepoPath);
-    config.setString("submodule", subscribeToRepoPath, "url", url);
+    config.setString("submodule", subscribeToRepoPath.get(), "path", subscribeToRepoPath.get());
+    config.setString("submodule", subscribeToRepoPath.get(), "url", url);
     if (subscribeToBranch != null) {
-      config.setString("submodule", subscribeToRepoPath, "branch", subscribeToBranch);
+      config.setString("submodule", subscribeToRepoPath.get(), "branch", subscribeToBranch);
     }
   }
 
@@ -298,12 +304,11 @@
   protected void expectToHaveSubmoduleState(
       TestRepository<?> repo,
       String branch,
-      String submodule,
+      Project.NameKey submodule,
       TestRepository<?> subRepo,
       String subBranch)
       throws Exception {
 
-    submodule = name(submodule);
     ObjectId commitId =
         repo.git()
             .fetch()
@@ -326,16 +331,14 @@
     rw.parseBody(c.getTree());
 
     RevTree tree = c.getTree();
-    RevObject actualId = repo.get(tree, submodule);
+    RevObject actualId = repo.get(tree, submodule.get());
 
     assertThat(actualId).isEqualTo(subHead);
   }
 
   protected void expectToHaveSubmoduleState(
-      TestRepository<?> repo, String branch, String submodule, ObjectId expectedId)
+      TestRepository<?> repo, String branch, Project.NameKey submodule, ObjectId expectedId)
       throws Exception {
-
-    submodule = name(submodule);
     ObjectId commitId =
         repo.git()
             .fetch()
@@ -349,7 +352,7 @@
     rw.parseBody(c.getTree());
 
     RevTree tree = c.getTree();
-    RevObject actualId = repo.get(tree, submodule);
+    RevObject actualId = repo.get(tree, submodule.get());
 
     assertThat(actualId).isEqualTo(expectedId);
   }
@@ -408,10 +411,8 @@
     assertThat(actualId).isEqualTo(expectedId);
   }
 
-  protected boolean hasSubmodule(TestRepository<?> repo, String branch, String submodule)
+  protected boolean hasSubmodule(TestRepository<?> repo, String branch, Project.NameKey submodule)
       throws Exception {
-
-    submodule = name(submodule);
     Ref branchTip =
         repo.git().fetch().setRemote("origin").call().getAdvertisedRef("refs/heads/" + branch);
     if (branchTip == null) {
@@ -426,7 +427,7 @@
 
     RevTree tree = c.getTree();
     try {
-      repo.get(tree, submodule);
+      repo.get(tree, submodule.get());
       return true;
     } catch (AssertionError e) {
       return false;
@@ -446,7 +447,8 @@
 
     RevWalk rw = repo.getRevWalk();
     RevCommit c = rw.parseCommit(commitId);
-    assertThat(c.getFullMessage()).isEqualTo(expectedMessage);
+    String msg = c.getFullMessage();
+    assertThat(msg).isEqualTo(expectedMessage);
   }
 
   protected PersonIdent getAuthor(TestRepository<?> repo, String branch) throws Exception {
@@ -463,10 +465,10 @@
     return c.getAuthorIdent();
   }
 
-  protected void directUpdateSubmodule(String project, String refName, String path, AnyObjectId id)
+  protected void directUpdateSubmodule(
+      Project.NameKey project, String refName, Project.NameKey path, AnyObjectId id)
       throws Exception {
-    path = name(path);
-    try (Repository serverRepo = repoManager.openRepository(new Project.NameKey(name(project)));
+    try (Repository serverRepo = repoManager.openRepository(project);
         ObjectInserter ins = serverRepo.newObjectInserter();
         RevWalk rw = new RevWalk(serverRepo)) {
       Ref ref = serverRepo.exactRef(refName);
@@ -480,7 +482,7 @@
       b.finish();
       DirCacheEditor e = dc.editor();
       e.add(
-          new PathEdit(path) {
+          new PathEdit(path.get()) {
             @Override
             public void apply(DirCacheEntry ent) {
               ent.setFileMode(FileMode.GITLINK);
diff --git a/javatests/com/google/gerrit/acceptance/git/BUILD b/javatests/com/google/gerrit/acceptance/git/BUILD
index 1ecfd43..24a83e0 100644
--- a/javatests/com/google/gerrit/acceptance/git/BUILD
+++ b/javatests/com/google/gerrit/acceptance/git/BUILD
@@ -8,6 +8,7 @@
     deps = [
         ":push_for_review",
         ":submodule_util",
+        "//lib/commons:lang",
     ],
 ) for f in glob(["*IT.java"])]
 
diff --git a/javatests/com/google/gerrit/acceptance/git/ForcePushIT.java b/javatests/com/google/gerrit/acceptance/git/ForcePushIT.java
index d80faa8..eb4845e 100644
--- a/javatests/com/google/gerrit/acceptance/git/ForcePushIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/ForcePushIT.java
@@ -38,7 +38,7 @@
   public void forcePushNotAllowed() throws Exception {
     ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
     PushOneCommit push1 =
-        pushFactory.create(db, admin.getIdent(), testRepo, "change1", "a.txt", "content");
+        pushFactory.create(admin.newIdent(), testRepo, "change1", "a.txt", "content");
     PushOneCommit.Result r1 = push1.to("refs/heads/master");
     r1.assertOkStatus();
 
@@ -48,7 +48,7 @@
     assertThat(ru.forceUpdate()).isEqualTo(RefUpdate.Result.FORCED);
 
     PushOneCommit push2 =
-        pushFactory.create(db, admin.getIdent(), testRepo, "change2", "b.txt", "content");
+        pushFactory.create(admin.newIdent(), testRepo, "change2", "b.txt", "content");
     push2.setForce(true);
     PushOneCommit.Result r2 = push2.to("refs/heads/master");
     r2.assertErrorStatus("not permitted: force update");
@@ -59,7 +59,7 @@
     ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
     grant(project, "refs/*", Permission.PUSH, true);
     PushOneCommit push1 =
-        pushFactory.create(db, admin.getIdent(), testRepo, "change1", "a.txt", "content");
+        pushFactory.create(admin.newIdent(), testRepo, "change1", "a.txt", "content");
     PushOneCommit.Result r1 = push1.to("refs/heads/master");
     r1.assertOkStatus();
 
@@ -69,7 +69,7 @@
     assertThat(ru.forceUpdate()).isEqualTo(RefUpdate.Result.FORCED);
 
     PushOneCommit push2 =
-        pushFactory.create(db, admin.getIdent(), testRepo, "change2", "b.txt", "content");
+        pushFactory.create(admin.newIdent(), testRepo, "change2", "b.txt", "content");
     push2.setForce(true);
     PushOneCommit.Result r2 = push2.to("refs/heads/master");
     r2.assertOkStatus();
diff --git a/javatests/com/google/gerrit/acceptance/git/GitmodulesIT.java b/javatests/com/google/gerrit/acceptance/git/GitmodulesIT.java
index a13c8c8..ac0cbd8 100644
--- a/javatests/com/google/gerrit/acceptance/git/GitmodulesIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/GitmodulesIT.java
@@ -47,7 +47,7 @@
         .commit()
         .insertChangeId()
         .message("subject: adding new subscription")
-        .add(".gitmodules", config.toText().toString())
+        .add(".gitmodules", config.toText())
         .create();
 
     exception.expectMessage(expectedErrorMessage);
diff --git a/javatests/com/google/gerrit/acceptance/git/HttpPushForReviewIT.java b/javatests/com/google/gerrit/acceptance/git/HttpPushForReviewIT.java
index ed17c38..6b7adf1 100644
--- a/javatests/com/google/gerrit/acceptance/git/HttpPushForReviewIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/HttpPushForReviewIT.java
@@ -14,15 +14,83 @@
 
 package com.google.gerrit.acceptance.git;
 
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.FakeGroupAuditService;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.audit.HttpAuditEvent;
+import com.google.inject.Inject;
+import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.RefSpec;
 import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
 import org.junit.Before;
+import org.junit.Test;
 
 public class HttpPushForReviewIT extends AbstractPushForReview {
+  @Inject private FakeGroupAuditService auditService;
+
   @Before
   public void selectHttpUrl() throws Exception {
     CredentialsProvider.setDefault(
-        new UsernamePasswordCredentialsProvider(admin.username, admin.httpPassword));
+        new UsernamePasswordCredentialsProvider(admin.username(), admin.httpPassword()));
     selectProtocol(Protocol.HTTP);
+    // Don't clear audit events here, since we can't guarantee all test setup has run yet.
+  }
+
+  @Test
+  public void receivePackAuditEventLog() throws Exception {
+    auditService.drainHttpAuditEvents();
+    testRepo
+        .git()
+        .push()
+        .setRemote("origin")
+        .setRefSpecs(new RefSpec("HEAD:refs/for/master"))
+        .call();
+
+    ImmutableList<HttpAuditEvent> auditEvents = auditService.drainHttpAuditEvents();
+    assertThat(auditEvents).hasSize(2);
+
+    HttpAuditEvent lsRemote = auditEvents.get(0);
+    assertThat(lsRemote.who.getAccountId()).isEqualTo(admin.id());
+    assertThat(lsRemote.what).endsWith("/info/refs?service=git-receive-pack");
+    assertThat(lsRemote.params).containsExactly("service", "git-receive-pack");
+    assertThat(lsRemote.httpStatus).isEqualTo(HttpServletResponse.SC_OK);
+
+    HttpAuditEvent receivePack = auditEvents.get(1);
+    assertThat(receivePack.who.getAccountId()).isEqualTo(admin.id());
+    assertThat(receivePack.what).endsWith("/git-receive-pack");
+    assertThat(receivePack.params).isEmpty();
+    assertThat(receivePack.httpStatus).isEqualTo(HttpServletResponse.SC_OK);
+  }
+
+  @Test
+  public void uploadPackAuditEventLog() throws Exception {
+    auditService.drainHttpAuditEvents();
+    // testRepo is already a clone. Make a server-side change so we have something to fetch.
+    try (Repository repo = repoManager.openRepository(project);
+        TestRepository<Repository> tr = new TestRepository<>(repo)) {
+      tr.branch("master").commit().create();
+    }
+    testRepo.git().fetch().call();
+
+    ImmutableList<HttpAuditEvent> auditEvents = auditService.drainHttpAuditEvents();
+    assertThat(auditEvents).hasSize(2);
+
+    HttpAuditEvent lsRemote = auditEvents.get(0);
+    // Repo URL doesn't include /a, so fetching doesn't cause authentication.
+    assertThat(lsRemote.who).isInstanceOf(AnonymousUser.class);
+    assertThat(lsRemote.what).endsWith("/info/refs?service=git-upload-pack");
+    assertThat(lsRemote.params).containsExactly("service", "git-upload-pack");
+    assertThat(lsRemote.httpStatus).isEqualTo(HttpServletResponse.SC_OK);
+
+    HttpAuditEvent uploadPack = auditEvents.get(1);
+    assertThat(lsRemote.who).isInstanceOf(AnonymousUser.class);
+    assertThat(uploadPack.what).endsWith("/git-upload-pack");
+    assertThat(uploadPack.params).isEmpty();
+    assertThat(uploadPack.httpStatus).isEqualTo(HttpServletResponse.SC_OK);
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/git/ImplicitMergeCheckIT.java b/javatests/com/google/gerrit/acceptance/git/ImplicitMergeCheckIT.java
index 954ca8b..6516b32 100644
--- a/javatests/com/google/gerrit/acceptance/git/ImplicitMergeCheckIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/ImplicitMergeCheckIT.java
@@ -91,8 +91,7 @@
 
   private PushOneCommit.Result push(String ref, String subject, String fileName, String content)
       throws Exception {
-    PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), testRepo, subject, fileName, content);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo, subject, fileName, content);
     return push.to(ref);
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java b/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
index 30e179d..cc19ad2 100644
--- a/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
@@ -24,6 +24,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+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;
@@ -36,6 +37,7 @@
 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;
 import org.eclipse.jgit.api.PushCommand;
@@ -52,6 +54,8 @@
 import org.junit.Test;
 
 public class PushPermissionsIT extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @Before
   public void setUp() throws Exception {
     try (ProjectConfigUpdate u = updateProject(allProjects)) {
@@ -155,8 +159,8 @@
 
   @Test
   public void groupRefsByMessage() throws Exception {
-    try (Repository repo = repoManager.openRepository(project)) {
-      TestRepository<?> tr = new TestRepository<>(repo);
+    try (Repository repo = repoManager.openRepository(project);
+        TestRepository<Repository> tr = new TestRepository<>(repo)) {
       tr.branch("foo").commit().create();
       tr.branch("bar").commit().create();
     }
@@ -264,14 +268,14 @@
   @Test
   public void addPatchSetDenied() throws Exception {
     grant(project, "refs/for/refs/heads/*", Permission.PUSH, false, REGISTERED_USERS);
-    setApiUser(user);
+    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);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     ObjectId ps1Id = forceFetch(new PatchSet.Id(id, 1).toRefName());
     ObjectId ps2Id = testRepo.amend(ps1Id).add("file", "content").create();
     PushResult r = push(ps2Id.name() + ":refs/for/master");
diff --git a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
index 3f06dfb..5bed77c 100644
--- a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -20,16 +20,17 @@
 import static com.google.gerrit.acceptance.GitUtil.fetch;
 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;
-import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.NoHttpd;
-import com.google.gerrit.acceptance.ProjectResetter;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
+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;
@@ -44,28 +45,26 @@
 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.Sequences;
 import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.git.receive.ReceiveCommitsAdvertiseRefsHook;
 import com.google.gerrit.server.notedb.ChangeNoteUtil;
-import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
+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.NoteDbMode;
-import com.google.gerrit.testing.TestChanges;
+import com.google.gerrit.testing.ConfigSuite;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Function;
 import java.util.function.Predicate;
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -77,22 +76,36 @@
 
 @NoHttpd
 public class RefAdvertisementIT extends AbstractDaemonTest {
-  @Inject private PermissionBackend permissionBackend;
-  @Inject private ChangeNoteUtil noteUtil;
-  @Inject @AnonymousCowardName private String anonymousCowardName;
   @Inject private AllUsersName allUsersName;
+  @Inject private ChangeNoteUtil noteUtil;
+  @Inject private PermissionBackend permissionBackend;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   private AccountGroup.UUID admins;
   private AccountGroup.UUID nonInteractiveUsers;
 
-  private ChangeData c1;
-  private ChangeData c2;
-  private ChangeData c3;
-  private ChangeData c4;
-  private String r1;
-  private String r2;
-  private String r3;
-  private String r4;
+  private ChangeData cd1;
+  private String psRef1;
+  private String metaRef1;
+
+  private ChangeData cd2;
+  private String psRef2;
+  private String metaRef2;
+
+  private ChangeData cd3;
+  private String psRef3;
+  private String metaRef3;
+
+  private ChangeData cd4;
+  private String psRef4;
+  private String metaRef4;
+
+  @ConfigSuite.Config
+  public static Config enableFullRefEvaluation() {
+    Config cfg = new Config();
+    cfg.setBoolean("auth", null, "skipFullRefEvaluationIfAllRefsAreVisible", false);
+    return cfg;
+  }
 
   @Before
   public void setUp() throws Exception {
@@ -102,9 +115,9 @@
     setUpChanges();
   }
 
+  // This method is idempotent, so it is safe to call it on every test setup.
   private void setUpPermissions() throws Exception {
-    // Remove read permissions for all users besides admin. This method is idempotent, so is safe
-    // to call on every test setup.
+    // Remove read permissions for all users besides admin.
     try (ProjectConfigUpdate u = updateProject(allProjects)) {
       for (AccessSection sec : u.getConfig().getAccessSections()) {
         sec.removePermission(Permission.READ);
@@ -113,8 +126,7 @@
       u.save();
     }
 
-    // Remove all read permissions on All-Users. This method is idempotent, so is safe to call on
-    // every test setup.
+    // Remove all read permissions on All-Users.
     try (ProjectConfigUpdate u = updateProject(allUsers)) {
       for (AccessSection sec : u.getConfig().getAccessSections()) {
         sec.removePermission(Permission.READ);
@@ -123,11 +135,6 @@
     }
   }
 
-  private static String changeRefPrefix(Change.Id id) {
-    String ps = new PatchSet.Id(id, 1).toRefName();
-    return ps.substring(0, ps.length() - 1);
-  }
-
   private void setUpChanges() throws Exception {
     gApi.projects().name(project.get()).branch("branch").create(new BranchInput());
 
@@ -135,25 +142,29 @@
     // visible.
     allow("refs/for/refs/heads/*", Permission.SUBMIT, admins);
     PushOneCommit.Result mr =
-        pushFactory.create(db, admin.getIdent(), testRepo).to("refs/for/master%submit");
+        pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master%submit");
     mr.assertOkStatus();
-    c1 = mr.getChange();
-    r1 = changeRefPrefix(c1.getId());
+    cd1 = mr.getChange();
+    psRef1 = cd1.currentPatchSet().getId().toRefName();
+    metaRef1 = RefNames.changeMetaRef(cd1.getId());
     PushOneCommit.Result br =
-        pushFactory.create(db, admin.getIdent(), testRepo).to("refs/for/branch%submit");
+        pushFactory.create(admin.newIdent(), testRepo).to("refs/for/branch%submit");
     br.assertOkStatus();
-    c2 = br.getChange();
-    r2 = changeRefPrefix(c2.getId());
+    cd2 = br.getChange();
+    psRef2 = cd2.currentPatchSet().getId().toRefName();
+    metaRef2 = RefNames.changeMetaRef(cd2.getId());
 
     // Second 2 changes are unmerged.
-    mr = pushFactory.create(db, admin.getIdent(), testRepo).to("refs/for/master");
+    mr = pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master");
     mr.assertOkStatus();
-    c3 = mr.getChange();
-    r3 = changeRefPrefix(c3.getId());
-    br = pushFactory.create(db, admin.getIdent(), testRepo).to("refs/for/branch");
+    cd3 = mr.getChange();
+    psRef3 = cd3.currentPatchSet().getId().toRefName();
+    metaRef3 = RefNames.changeMetaRef(cd3.getId());
+    br = pushFactory.create(admin.newIdent(), testRepo).to("refs/for/branch");
     br.assertOkStatus();
-    c4 = br.getChange();
-    r4 = changeRefPrefix(c4.getId());
+    cd4 = br.getChange();
+    psRef4 = cd4.currentPatchSet().getId().toRefName();
+    metaRef4 = RefNames.changeMetaRef(cd4.getId());
 
     try (Repository repo = repoManager.openRepository(project)) {
       // master-tag -> master
@@ -179,17 +190,17 @@
       u.save();
     }
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     assertUploadPackRefs(
         "HEAD",
-        r1 + "1",
-        r1 + "meta",
-        r2 + "1",
-        r2 + "meta",
-        r3 + "1",
-        r3 + "meta",
-        r4 + "1",
-        r4 + "meta",
+        psRef1,
+        metaRef1,
+        psRef2,
+        metaRef2,
+        psRef3,
+        metaRef3,
+        psRef4,
+        metaRef4,
         "refs/heads/branch",
         "refs/heads/master",
         "refs/tags/branch-tag",
@@ -203,14 +214,14 @@
 
     assertUploadPackRefs(
         "HEAD",
-        r1 + "1",
-        r1 + "meta",
-        r2 + "1",
-        r2 + "meta",
-        r3 + "1",
-        r3 + "meta",
-        r4 + "1",
-        r4 + "meta",
+        psRef1,
+        metaRef1,
+        psRef2,
+        metaRef2,
+        psRef3,
+        metaRef3,
+        psRef4,
+        metaRef4,
         "refs/heads/branch",
         "refs/heads/master",
         RefNames.REFS_CONFIG,
@@ -223,15 +234,9 @@
     allow("refs/heads/master", Permission.READ, REGISTERED_USERS);
     deny("refs/heads/branch", Permission.READ, REGISTERED_USERS);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     assertUploadPackRefs(
-        "HEAD",
-        r1 + "1",
-        r1 + "meta",
-        r3 + "1",
-        r3 + "meta",
-        "refs/heads/master",
-        "refs/tags/master-tag");
+        "HEAD", psRef1, metaRef1, psRef3, metaRef3, "refs/heads/master", "refs/tags/master-tag");
   }
 
   @Test
@@ -239,12 +244,12 @@
     deny("refs/heads/master", Permission.READ, REGISTERED_USERS);
     allow("refs/heads/branch", Permission.READ, REGISTERED_USERS);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     assertUploadPackRefs(
-        r2 + "1",
-        r2 + "meta",
-        r4 + "1",
-        r4 + "meta",
+        psRef2,
+        metaRef2,
+        psRef4,
+        metaRef4,
         "refs/heads/branch",
         "refs/tags/branch-tag",
         // master branch is not visible but master-tag is reachable from branch
@@ -256,27 +261,23 @@
   public void uploadPackSubsetOfBranchesVisibleWithEdit() throws Exception {
     allow("refs/heads/master", Permission.READ, REGISTERED_USERS);
 
-    Change c = notesFactory.createChecked(db, project, c3.getId()).getChange();
-    String changeId = c.getKey().get();
-
     // Admin's edit is not visible.
-    setApiUser(admin);
-    gApi.changes().id(changeId).edit().create();
+    requestScopeOperations.setApiUser(admin.id());
+    gApi.changes().id(cd3.getId().get()).edit().create();
 
     // User's edit is visible.
-    setApiUser(user);
-    gApi.changes().id(changeId).edit().create();
+    requestScopeOperations.setApiUser(user.id());
+    gApi.changes().id(cd3.getId().get()).edit().create();
 
     assertUploadPackRefs(
         "HEAD",
-        r1 + "1",
-        r1 + "meta",
-        r3 + "1",
-        r3 + "meta",
+        psRef1,
+        metaRef1,
+        psRef3,
+        metaRef3,
         "refs/heads/master",
         "refs/tags/master-tag",
-        "refs/tags/branch-tag",
-        "refs/users/01/1000001/edit-" + c3.getId() + "/1");
+        "refs/users/01/1000001/edit-" + cd3.getId() + "/1");
   }
 
   @Test
@@ -284,33 +285,27 @@
     allow("refs/heads/master", Permission.READ, REGISTERED_USERS);
     allow("refs/*", Permission.VIEW_PRIVATE_CHANGES, REGISTERED_USERS);
 
-    Change change3 = notesFactory.createChecked(db, project, c3.getId()).getChange();
-    String changeId3 = change3.getKey().get();
-    Change change4 = notesFactory.createChecked(db, project, c4.getId()).getChange();
-    String changeId4 = change4.getKey().get();
-
     // Admin's edit on change3 is visible.
-    setApiUser(admin);
-    gApi.changes().id(changeId3).edit().create();
+    requestScopeOperations.setApiUser(admin.id());
+    gApi.changes().id(cd3.getId().get()).edit().create();
 
     // Admin's edit on change4 is not visible since user cannot see the change.
-    gApi.changes().id(changeId4).edit().create();
+    gApi.changes().id(cd4.getId().get()).edit().create();
 
     // User's edit is visible.
-    setApiUser(user);
-    gApi.changes().id(changeId3).edit().create();
+    requestScopeOperations.setApiUser(user.id());
+    gApi.changes().id(cd3.getId().get()).edit().create();
 
     assertUploadPackRefs(
         "HEAD",
-        r1 + "1",
-        r1 + "meta",
-        r3 + "1",
-        r3 + "meta",
+        psRef1,
+        metaRef1,
+        psRef3,
+        metaRef3,
         "refs/heads/master",
         "refs/tags/master-tag",
-        "refs/tags/branch-tag",
-        "refs/users/00/1000000/edit-" + c3.getId() + "/1",
-        "refs/users/01/1000001/edit-" + c3.getId() + "/1");
+        "refs/users/00/1000000/edit-" + cd3.getId() + "/1",
+        "refs/users/01/1000001/edit-" + cd3.getId() + "/1");
   }
 
   @Test
@@ -319,28 +314,27 @@
     deny("refs/heads/master", Permission.READ, REGISTERED_USERS);
     allow("refs/heads/branch", Permission.READ, REGISTERED_USERS);
 
-    String changeId = c3.change().getKey().get();
-    setApiUser(admin);
-    gApi.changes().id(changeId).edit().create();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(admin.id());
+    gApi.changes().id(cd3.getId().get()).edit().create();
+    requestScopeOperations.setApiUser(user.id());
 
     assertUploadPackRefs(
         // Change 1 is visible due to accessDatabase capability, even though
         // refs/heads/master is not.
-        r1 + "1",
-        r1 + "meta",
-        r2 + "1",
-        r2 + "meta",
-        r3 + "1",
-        r3 + "meta",
-        r4 + "1",
-        r4 + "meta",
+        psRef1,
+        metaRef1,
+        psRef2,
+        metaRef2,
+        psRef3,
+        metaRef3,
+        psRef4,
+        metaRef4,
         "refs/heads/branch",
         "refs/tags/branch-tag",
         // See comment in subsetOfBranchesVisibleNotIncludingHead.
         "refs/tags/master-tag",
         // All edits are visible due to accessDatabase capability.
-        "refs/users/00/1000000/edit-" + c3.getId() + "/1");
+        "refs/users/00/1000000/edit-" + cd3.getId() + "/1");
   }
 
   @Test
@@ -357,38 +351,55 @@
   private void uploadPackNoSearchingChangeCacheImpl() throws Exception {
     allow("refs/heads/*", Permission.READ, REGISTERED_USERS);
 
-    setApiUser(user);
-    try (Repository repo = repoManager.openRepository(project)) {
-      assertRefs(
-          repo,
-          permissionBackend.user(user(user)).project(project),
-          // Can't use stored values from the index so DB must be enabled.
-          false,
-          "HEAD",
-          r1 + "1",
-          r1 + "meta",
-          r2 + "1",
-          r2 + "meta",
-          r3 + "1",
-          r3 + "meta",
-          r4 + "1",
-          r4 + "meta",
-          "refs/heads/branch",
-          "refs/heads/master",
-          "refs/tags/branch-tag",
-          "refs/tags/master-tag");
-    }
+    requestScopeOperations.setApiUser(user.id());
+    assertRefs(
+        project,
+        user,
+        // Can't use stored values from the index so DB must be enabled.
+        false,
+        "HEAD",
+        psRef1,
+        metaRef1,
+        psRef2,
+        metaRef2,
+        psRef3,
+        metaRef3,
+        psRef4,
+        metaRef4,
+        "refs/heads/branch",
+        "refs/heads/master",
+        "refs/tags/branch-tag",
+        "refs/tags/master-tag");
   }
 
   @Test
   public void uploadPackSequencesWithAccessDatabase() throws Exception {
-    assume().that(notesMigration.readChangeSequence()).isTrue();
-    try (Repository repo = repoManager.openRepository(allProjects)) {
-      assertRefs(repo, newFilter(allProjects, user), true);
+    assertRefs(allProjects, user, true);
 
-      allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
-      assertRefs(repo, newFilter(allProjects, user), true, "refs/sequences/changes");
-    }
+    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    assertRefs(allProjects, user, true, "refs/sequences/changes");
+  }
+
+  @Test
+  public void uploadPackAllRefsAreVisibleOrphanedTag() throws Exception {
+    allow("refs/*", Permission.READ, REGISTERED_USERS);
+    // Delete the pending change on 'branch' and 'branch' itself so that the tag gets orphaned
+    gApi.changes().id(cd4.getId().id).delete();
+    gApi.projects().name(project.get()).branch("refs/heads/branch").delete();
+
+    requestScopeOperations.setApiUser(user.id());
+    assertUploadPackRefs(
+        "HEAD",
+        "refs/meta/config",
+        psRef1,
+        metaRef1,
+        psRef2,
+        metaRef2,
+        psRef3,
+        metaRef3,
+        "refs/heads/master",
+        "refs/tags/branch-tag",
+        "refs/tags/master-tag");
   }
 
   @Test
@@ -396,78 +407,70 @@
     ReceiveCommitsAdvertiseRefsHook.Result r = getReceivePackRefs();
     assertThat(r.allRefs().keySet())
         .containsExactly(
-            // meta refs are excluded even when NoteDb is enabled.
+            // meta refs are excluded
             "HEAD",
             "refs/heads/branch",
             "refs/heads/master",
             "refs/meta/config",
             "refs/tags/branch-tag",
             "refs/tags/master-tag");
-    assertThat(r.additionalHaves()).containsExactly(obj(c3, 1), obj(c4, 1));
+    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);
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
 
-    assertThat(getReceivePackRefs().additionalHaves()).containsExactly(obj(c3, 1));
+    assertThat(getReceivePackRefs().additionalHaves()).containsExactly(obj(cd3, 1));
   }
 
   @Test
   public void receivePackListsOnlyLatestPatchSet() throws Exception {
-    testRepo.reset(obj(c3, 1));
-    PushOneCommit.Result r = amendChange(c3.change().getKey().get());
+    testRepo.reset(obj(cd3, 1));
+    PushOneCommit.Result r = amendChange(cd3.change().getKey().get());
     r.assertOkStatus();
-    c3 = r.getChange();
-    assertThat(getReceivePackRefs().additionalHaves()).containsExactly(obj(c3, 2), obj(c4, 1));
+    cd3 = r.getChange();
+    assertThat(getReceivePackRefs().additionalHaves()).containsExactly(obj(cd3, 2), obj(cd4, 1));
   }
 
   @Test
   public void receivePackOmitsMissingObject() throws Exception {
     String rev = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
-    try (Repository repo = repoManager.openRepository(project)) {
-      TestRepository<?> tr = new TestRepository<>(repo);
+    try (Repository repo = repoManager.openRepository(project);
+        TestRepository<Repository> tr = new TestRepository<>(repo)) {
       String subject = "Subject for missing commit";
-      Change c = new Change(c3.change());
-      PatchSet.Id psId = new PatchSet.Id(c3.getId(), 2);
+      Change c = new Change(cd3.change());
+      PatchSet.Id psId = new PatchSet.Id(cd3.getId(), 2);
       c.setCurrentPatchSet(psId, subject, c.getOriginalSubject());
 
-      if (notesMigration.changePrimaryStorage() == PrimaryStorage.REVIEW_DB) {
-        PatchSet ps = TestChanges.newPatchSet(psId, rev, admin.getId());
-        db.patchSets().insert(Collections.singleton(ps));
-        db.changes().update(Collections.singleton(c));
-      }
-
-      if (notesMigration.commitChangeWrites()) {
-        PersonIdent committer = serverIdent.get();
-        PersonIdent author =
-            noteUtil.newIdent(getAccount(admin.getId()), committer.getWhen(), committer);
-        tr.branch(RefNames.changeMetaRef(c3.getId()))
-            .commit()
-            .author(author)
-            .committer(committer)
-            .message(
-                "Update patch set "
-                    + psId.get()
-                    + "\n"
-                    + "\n"
-                    + "Patch-set: "
-                    + psId.get()
-                    + "\n"
-                    + "Commit: "
-                    + rev
-                    + "\n"
-                    + "Subject: "
-                    + subject
-                    + "\n")
-            .create();
-      }
-      indexer.index(db, c.getProject(), c.getId());
+      PersonIdent committer = serverIdent.get();
+      PersonIdent author =
+          noteUtil.newIdent(getAccount(admin.id()), committer.getWhen(), committer);
+      tr.branch(RefNames.changeMetaRef(cd3.getId()))
+          .commit()
+          .author(author)
+          .committer(committer)
+          .message(
+              "Update patch set "
+                  + psId.get()
+                  + "\n"
+                  + "\n"
+                  + "Patch-set: "
+                  + psId.get()
+                  + "\n"
+                  + "Commit: "
+                  + rev
+                  + "\n"
+                  + "Subject: "
+                  + subject
+                  + "\n")
+          .create();
+      indexer.index(c.getProject(), c.getId());
     }
 
-    assertThat(getReceivePackRefs().additionalHaves()).containsExactly(obj(c4, 1));
+    assertThat(getReceivePackRefs().additionalHaves()).containsExactly(obj(cd4, 1));
   }
 
   @Test
@@ -484,7 +487,7 @@
     TestRepository<?> userTestRepository = cloneProject(allUsers, user);
     try (Git git = userTestRepository.git()) {
       assertThat(getUserRefs(git))
-          .containsExactly(RefNames.REFS_USERS_SELF, RefNames.refsUsers(user.id));
+          .containsExactly(RefNames.REFS_USERS_SELF, RefNames.refsUsers(user.id()));
     }
   }
 
@@ -495,57 +498,49 @@
     try (Git git = userTestRepository.git()) {
       assertThat(getUserRefs(git))
           .containsExactly(
-              RefNames.REFS_USERS_SELF, RefNames.refsUsers(user.id), RefNames.refsUsers(admin.id));
+              RefNames.REFS_USERS_SELF,
+              RefNames.refsUsers(user.id()),
+              RefNames.refsUsers(admin.id()));
     }
   }
 
   @Test
-  @GerritConfig(name = "noteDb.groups.write", value = "true")
   public void advertisedReferencesDontShowGroupBranchToOwnerWithoutRead() throws Exception {
-    try (ProjectResetter resetter = resetGroups()) {
-      createSelfOwnedGroup("Foos", user);
-      TestRepository<?> userTestRepository = cloneProject(allUsers, user);
-      try (Git git = userTestRepository.git()) {
-        assertThat(getGroupRefs(git)).isEmpty();
-      }
+    createSelfOwnedGroup("Foos", user);
+    TestRepository<?> userTestRepository = cloneProject(allUsers, user);
+    try (Git git = userTestRepository.git()) {
+      assertThat(getGroupRefs(git)).isEmpty();
     }
   }
 
   @Test
-  @GerritConfig(name = "noteDb.groups.write", value = "true")
   public void advertisedReferencesOmitGroupBranchesOfNonOwnedGroups() throws Exception {
-    try (ProjectResetter resetter = resetGroups()) {
-      allow(allUsersName, RefNames.REFS_GROUPS + "*", Permission.READ, REGISTERED_USERS);
-      AccountGroup.UUID users = createGroup("Users", admins, user);
-      AccountGroup.UUID foos = createGroup("Foos", users);
-      AccountGroup.UUID bars = createSelfOwnedGroup("Bars", user);
-      TestRepository<?> userTestRepository = cloneProject(allUsers, user);
-      try (Git git = userTestRepository.git()) {
-        assertThat(getGroupRefs(git))
-            .containsExactly(RefNames.refsGroups(foos), RefNames.refsGroups(bars));
-      }
+    allow(allUsersName, RefNames.REFS_GROUPS + "*", Permission.READ, REGISTERED_USERS);
+    AccountGroup.UUID users = createGroup("Users", admins, user);
+    AccountGroup.UUID foos = createGroup("Foos", users);
+    AccountGroup.UUID bars = createSelfOwnedGroup("Bars", user);
+    TestRepository<?> userTestRepository = cloneProject(allUsers, user);
+    try (Git git = userTestRepository.git()) {
+      assertThat(getGroupRefs(git))
+          .containsExactly(RefNames.refsGroups(foos), RefNames.refsGroups(bars));
     }
   }
 
   @Test
-  @GerritConfig(name = "noteDb.groups.write", value = "true")
   public void advertisedReferencesIncludeAllGroupBranchesWithAccessDatabase() throws Exception {
-    try (ProjectResetter resetter = resetGroups()) {
-      allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
-      AccountGroup.UUID users = createGroup("Users", admins);
-      TestRepository<?> userTestRepository = cloneProject(allUsers, user);
-      try (Git git = userTestRepository.git()) {
-        assertThat(getGroupRefs(git))
-            .containsExactly(
-                RefNames.refsGroups(admins),
-                RefNames.refsGroups(nonInteractiveUsers),
-                RefNames.refsGroups(users));
-      }
+    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    AccountGroup.UUID users = createGroup("Users", admins);
+    TestRepository<?> userTestRepository = cloneProject(allUsers, user);
+    try (Git git = userTestRepository.git()) {
+      assertThat(getGroupRefs(git))
+          .containsExactly(
+              RefNames.refsGroups(admins),
+              RefNames.refsGroups(nonInteractiveUsers),
+              RefNames.refsGroups(users));
     }
   }
 
   @Test
-  @GerritConfig(name = "noteDb.groups.write", value = "true")
   public void advertisedReferencesIncludeAllGroupBranchesForAdmins() throws Exception {
     allow(allUsersName, RefNames.REFS_GROUPS + "*", Permission.READ, REGISTERED_USERS);
     allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ADMINISTRATE_SERVER);
@@ -561,7 +556,6 @@
   }
 
   @Test
-  @GerritConfig(name = "noteDb.groups.write", value = "true")
   public void advertisedReferencesOmitNoteDbNotesBranches() throws Exception {
     allow(allUsersName, RefNames.REFS + "*", Permission.READ, REGISTERED_USERS);
     TestRepository<?> userTestRepository = cloneProject(allUsers, user);
@@ -576,24 +570,27 @@
 
     TestRepository<?> userTestRepository = cloneProject(project, user);
     try (Git git = userTestRepository.git()) {
-      String change3RefName = c3.currentPatchSet().getRefName();
+      String change3RefName = cd3.currentPatchSet().getRefName();
       assertWithMessage("Precondition violated").that(getRefs(git)).contains(change3RefName);
 
-      gApi.changes().id(c3.getId().get()).setPrivate(true, null);
+      gApi.changes().id(cd3.getId().get()).setPrivate(true, null);
       assertThat(getRefs(git)).doesNotContain(change3RefName);
     }
   }
 
   @Test
   public void advertisedReferencesIncludePrivateChangesWhenAllRefsMayBeRead() throws Exception {
+    assume()
+        .that(baseConfig.getBoolean("auth", "skipFullRefEvaluationIfAllRefsAreVisible", true))
+        .isTrue();
     allow("refs/*", Permission.READ, REGISTERED_USERS);
 
     TestRepository<?> userTestRepository = cloneProject(project, user);
     try (Git git = userTestRepository.git()) {
-      String change3RefName = c3.currentPatchSet().getRefName();
+      String change3RefName = cd3.currentPatchSet().getRefName();
       assertWithMessage("Precondition violated").that(getRefs(git)).contains(change3RefName);
 
-      gApi.changes().id(c3.getId().get()).setPrivate(true, null);
+      gApi.changes().id(cd3.getId().get()).setPrivate(true, null);
       assertThat(getRefs(git)).contains(change3RefName);
     }
   }
@@ -606,28 +603,26 @@
 
     TestRepository<?> userTestRepository = cloneProject(project, user);
     try (Git git = userTestRepository.git()) {
-      String change3RefName = c3.currentPatchSet().getRefName();
+      String change3RefName = cd3.currentPatchSet().getRefName();
       assertWithMessage("Precondition violated").that(getRefs(git)).contains(change3RefName);
 
-      gApi.changes().id(c3.getId().get()).setPrivate(true, null);
+      gApi.changes().id(cd3.getId().get()).setPrivate(true, null);
       assertThat(getRefs(git)).doesNotContain(change3RefName);
     }
   }
 
   @Test
   public void advertisedReferencesOmitDraftCommentRefsOfOtherUsers() throws Exception {
-    assume().that(notesMigration.commitChangeWrites()).isTrue();
-
     allow(project, "refs/*", Permission.READ, REGISTERED_USERS);
     allow(allUsersName, "refs/*", Permission.READ, REGISTERED_USERS);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     DraftInput draftInput = new DraftInput();
     draftInput.line = 1;
     draftInput.message = "nit: trailing whitespace";
     draftInput.path = Patch.COMMIT_MSG;
-    gApi.changes().id(c3.getId().get()).current().createDraft(draftInput);
-    String draftCommentRef = RefNames.refsDraftComments(c3.getId(), user.id);
+    gApi.changes().id(cd3.getId().get()).current().createDraft(draftInput);
+    String draftCommentRef = RefNames.refsDraftComments(cd3.getId(), user.id());
 
     // user can see the draft comment ref of the own draft comment
     assertThat(lsRemote(allUsersName, user)).contains(draftCommentRef);
@@ -638,14 +633,12 @@
 
   @Test
   public void advertisedReferencesOmitStarredChangesRefsOfOtherUsers() throws Exception {
-    assume().that(notesMigration.commitChangeWrites()).isTrue();
-
     allow(project, "refs/*", Permission.READ, REGISTERED_USERS);
     allow(allUsersName, "refs/*", Permission.READ, REGISTERED_USERS);
 
-    setApiUser(user);
-    gApi.accounts().self().starChange(c3.getId().toString());
-    String starredChangesRef = RefNames.refsStarredChanges(c3.getId(), user.id);
+    requestScopeOperations.setApiUser(user.id());
+    gApi.accounts().self().starChange(cd3.getId().toString());
+    String starredChangesRef = RefNames.refsStarredChanges(cd3.getId(), user.id());
 
     // user can see the starred changes ref of the own star
     assertThat(lsRemote(allUsersName, user)).contains(starredChangesRef);
@@ -655,7 +648,6 @@
   }
 
   @Test
-  @GerritConfig(name = "noteDb.groups.write", value = "true")
   public void hideMetadata() throws Exception {
     allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
     // create change
@@ -664,15 +656,15 @@
     allUsersRepo.reset("userRef");
     PushOneCommit.Result mr =
         pushFactory
-            .create(db, admin.getIdent(), allUsersRepo)
+            .create(admin.newIdent(), allUsersRepo)
             .to("refs/for/" + RefNames.REFS_USERS_SELF);
     mr.assertOkStatus();
 
     List<String> expectedNonMetaRefs =
         ImmutableList.of(
             RefNames.REFS_USERS_SELF,
-            RefNames.refsUsers(admin.id),
-            RefNames.refsUsers(user.id),
+            RefNames.refsUsers(admin.id()),
+            RefNames.refsUsers(user.id()),
             RefNames.REFS_EXTERNAL_IDS,
             RefNames.REFS_GROUPNAMES,
             RefNames.refsGroups(admins),
@@ -684,15 +676,13 @@
 
     List<String> expectedMetaRefs =
         new ArrayList<>(ImmutableList.of(mr.getPatchSetId().toRefName()));
-    if (NoteDbMode.get() != NoteDbMode.OFF) {
-      expectedMetaRefs.add(changeRefPrefix(mr.getChange().getId()) + "meta");
-    }
+    expectedMetaRefs.add(RefNames.changeMetaRef(mr.getChange().getId()));
 
     List<String> expectedAllRefs = new ArrayList<>(expectedNonMetaRefs);
     expectedAllRefs.addAll(expectedMetaRefs);
 
     try (Repository repo = repoManager.openRepository(allUsers)) {
-      Map<String, Ref> all = repo.getAllRefs();
+      Map<String, Ref> all = getAllRefs(repo);
 
       PermissionBackend.ForProject forProject = newFilter(allUsers, admin);
       assertThat(forProject.filter(all, repo, RefFilterOptions.defaults()).keySet())
@@ -705,6 +695,22 @@
     }
   }
 
+  @Test
+  public void fetchSingleChangeWithoutIndexAccess() throws Exception {
+    PushOneCommit.Result change = createChange();
+    String patchSetRef = change.getPatchSetId().toRefName();
+    try (AutoCloseable ignored = disableChangeIndex();
+        Repository repo = repoManager.openRepository(project)) {
+      Map<String, Ref> singleRef = ImmutableMap.of(patchSetRef, repo.exactRef(patchSetRef));
+      Map<String, Ref> filteredRefs =
+          permissionBackend
+              .user(user(admin))
+              .project(project)
+              .filter(singleRef, repo, RefFilterOptions.defaults());
+      assertThat(filteredRefs).isEqualTo(singleRef);
+    }
+  }
+
   private List<String> lsRemote(Project.NameKey p, TestAccount a) throws Exception {
     TestRepository<?> testRepository = cloneProject(p, a);
     try (Git git = testRepository.git()) {
@@ -729,43 +735,27 @@
   }
 
   /**
-   * Assert that refs seen by a non-admin user match expected.
+   * Assert that refs seen by a non-admin user match the expected refs.
    *
-   * @param expectedWithMeta expected refs, in order. If NoteDb is disabled by the configuration,
-   *     any NoteDb refs (i.e. ending in "/meta") are removed from the expected list before
-   *     comparing to the actual results.
+   * @param expectedRefs expected refs.
    * @throws Exception
    */
-  private void assertUploadPackRefs(String... expectedWithMeta) throws Exception {
-    try (Repository repo = repoManager.openRepository(project)) {
-      assertRefs(repo, permissionBackend.user(user(user)).project(project), true, expectedWithMeta);
-    }
+  private void assertUploadPackRefs(String... expectedRefs) throws Exception {
+    assertRefs(project, user, true, expectedRefs);
   }
 
   private void assertRefs(
-      Repository repo,
-      PermissionBackend.ForProject forProject,
-      boolean disableDb,
-      String... expectedWithMeta)
+      Project.NameKey project, TestAccount user, boolean disableDb, String... expectedRefs)
       throws Exception {
-    List<String> expected = new ArrayList<>(expectedWithMeta.length);
-    for (String r : expectedWithMeta) {
-      if (notesMigration.commitChangeWrites() || !r.endsWith(RefNames.META_SUFFIX)) {
-        expected.add(r);
-      }
-    }
-
-    AcceptanceTestRequestScope.Context ctx = null;
+    AutoCloseable ctx = null;
     if (disableDb) {
-      ctx = disableDb();
+      ctx = disableNoteDb();
     }
     try {
-      Map<String, Ref> all = repo.getAllRefs();
-      assertThat(forProject.filter(all, repo, RefFilterOptions.defaults()).keySet())
-          .containsExactlyElementsIn(expected);
+      assertThat(lsRemote(project, user)).containsExactlyElementsIn(expectedRefs);
     } finally {
       if (disableDb) {
-        enableDb(ctx);
+        ctx.close();
       }
     }
   }
@@ -774,7 +764,7 @@
     ReceiveCommitsAdvertiseRefsHook hook =
         new ReceiveCommitsAdvertiseRefsHook(queryProvider, project);
     try (Repository repo = repoManager.openRepository(project)) {
-      return hook.advertiseRefs(repo.getAllRefs());
+      return hook.advertiseRefs(getAllRefs(repo));
     }
   }
 
@@ -801,22 +791,12 @@
     groupInput.name = name(name);
     groupInput.ownerId = ownerGroup != null ? ownerGroup.get() : null;
     groupInput.members =
-        Arrays.stream(members).map(m -> String.valueOf(m.id.get())).collect(toList());
+        Arrays.stream(members).map(m -> String.valueOf(m.id().get())).collect(toList());
     return new AccountGroup.UUID(gApi.groups().create(groupInput).get().id);
   }
 
-  /**
-   * Create a resetter to reset the group branches in All-Users. This makes the group data between
-   * ReviewDb and NoteDb inconsistent, but in the context of this test class we only care about refs
-   * and hence this is not an issue. Once groups are no longer in ReviewDb and {@link
-   * AbstractDaemonTest#resetProjects} takes care to reset group branches we no longer need this
-   * method.
-   */
-  private ProjectResetter resetGroups() throws IOException {
-    return projectResetter
-        .builder()
-        .build(
-            new ProjectResetter.Config()
-                .reset(allUsers, RefNames.REFS_GROUPS + "*", RefNames.REFS_GROUPNAMES));
+  private static Map<String, Ref> getAllRefs(Repository repo) throws IOException {
+    return repo.getRefDatabase().getRefs().stream()
+        .collect(toMap(Ref::getName, Function.identity()));
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/git/RefOperationValidationIT.java b/javatests/com/google/gerrit/acceptance/git/RefOperationValidationIT.java
index d1d4f0c..9b823b7 100644
--- a/javatests/com/google/gerrit/acceptance/git/RefOperationValidationIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/RefOperationValidationIT.java
@@ -91,7 +91,7 @@
     try (TestRefValidator validator = new TestRefValidator(CREATE)) {
       grant(project, "refs/*", Permission.PUSH, true);
       PushOneCommit push1 =
-          pushFactory.create(db, admin.getIdent(), testRepo, "change1", "a.txt", "content");
+          pushFactory.create(admin.newIdent(), testRepo, "change1", "a.txt", "content");
       PushOneCommit.Result r1 = push1.to("refs/heads/master");
       r1.assertOkStatus();
       PushOneCommit.Result r2 = push1.to(TEST_REF);
@@ -127,7 +127,7 @@
     try (TestRefValidator validator = new TestRefValidator(UPDATE)) {
       grant(project, "refs/*", Permission.PUSH, true);
       PushOneCommit push1 =
-          pushFactory.create(db, admin.getIdent(), testRepo, "change1", "a.txt", "content");
+          pushFactory.create(admin.newIdent(), testRepo, "change1", "a.txt", "content");
       PushOneCommit.Result r1 = push1.to(TEST_REF);
       r1.assertErrorStatus(UPDATE.name());
     }
@@ -140,7 +140,7 @@
       ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
       grant(project, "refs/*", Permission.PUSH, true);
       PushOneCommit push1 =
-          pushFactory.create(db, admin.getIdent(), testRepo, "change1", "a.txt", "content");
+          pushFactory.create(admin.newIdent(), testRepo, "change1", "a.txt", "content");
       PushOneCommit.Result r1 = push1.to(TEST_REF);
       r1.assertOkStatus();
 
@@ -150,7 +150,7 @@
       assertThat(ru.forceUpdate()).isEqualTo(RefUpdate.Result.FORCED);
 
       PushOneCommit push2 =
-          pushFactory.create(db, admin.getIdent(), testRepo, "change2", "b.txt", "content");
+          pushFactory.create(admin.newIdent(), testRepo, "change2", "b.txt", "content");
       push2.setForce(true);
       PushOneCommit.Result r2 = push2.to(TEST_REF);
       r2.assertErrorStatus(UPDATE_NONFASTFORWARD.name());
@@ -164,13 +164,13 @@
     try (TestRefValidator validator = new TestRefValidator(UPDATE_NONFASTFORWARD)) {
       grant(project, "refs/*", Permission.PUSH, true);
       PushOneCommit push1 =
-          pushFactory.create(db, admin.getIdent(), testRepo, "change1", "a.txt", "content");
+          pushFactory.create(admin.newIdent(), testRepo, "change1", "a.txt", "content");
       PushOneCommit.Result r1 = push1.to("refs/heads/master");
       r1.assertOkStatus();
       ObjectId push1Id = r1.getCommit();
 
       PushOneCommit push2 =
-          pushFactory.create(db, admin.getIdent(), testRepo, "change2", "b.txt", "content");
+          pushFactory.create(admin.newIdent(), testRepo, "change2", "b.txt", "content");
       PushOneCommit.Result r2 = push2.to("refs/heads/master");
       r2.assertOkStatus();
       ObjectId push2Id = r2.getCommit();
@@ -180,7 +180,7 @@
       assertThat(ru.forceUpdate()).isEqualTo(RefUpdate.Result.FORCED);
 
       PushOneCommit push3 =
-          pushFactory.create(db, admin.getIdent(), testRepo, "change3", "c.txt", "content");
+          pushFactory.create(admin.newIdent(), testRepo, "change3", "c.txt", "content");
       PushOneCommit.Result r3 = push3.to(TEST_REF);
       r3.assertOkStatus();
 
@@ -189,7 +189,7 @@
       assertThat(ru.forceUpdate()).isEqualTo(RefUpdate.Result.FORCED);
 
       PushOneCommit push4 =
-          pushFactory.create(db, admin.getIdent(), testRepo, "change4", "d.txt", "content");
+          pushFactory.create(admin.newIdent(), testRepo, "change4", "d.txt", "content");
       push4.setForce(true);
       PushOneCommit.Result r4 = push4.to(TEST_REF);
       r4.assertErrorStatus(UPDATE_NONFASTFORWARD.name());
diff --git a/javatests/com/google/gerrit/acceptance/git/SubmitOnPushIT.java b/javatests/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
index 7646c45..2b10b17 100644
--- a/javatests/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
@@ -162,7 +162,7 @@
     RevCommit c = r.getCommit();
     PatchSet.Id psId = cd.currentPatchSet().getId();
     assertThat(psId.get()).isEqualTo(1);
-    assertThat(cd.change().getStatus()).isEqualTo(Change.Status.MERGED);
+    assertThat(cd.change().isMerged()).isTrue();
     assertSubmitApproval(psId);
 
     assertThat(cd.patchSets()).hasSize(1);
@@ -199,7 +199,7 @@
     assertCommit(project, master);
     ChangeData cd =
         Iterables.getOnlyElement(queryProvider.get().byKey(new Change.Key(r.getChangeId())));
-    assertThat(cd.change().getStatus()).isEqualTo(Change.Status.MERGED);
+    assertThat(cd.change().isMerged()).isTrue();
 
     RemoteRefUpdate.Status status = pushCommitTo(commit, "refs/for/other");
     assertThat(status).isEqualTo(RemoteRefUpdate.Status.OK);
@@ -209,7 +209,7 @@
 
     for (ChangeData c : queryProvider.get().byKey(new Change.Key(r.getChangeId()))) {
       if (c.change().getDest().get().equals(other)) {
-        assertThat(c.change().getStatus()).isEqualTo(Change.Status.MERGED);
+        assertThat(c.change().isMerged()).isTrue();
       }
     }
   }
@@ -233,8 +233,7 @@
 
     PushOneCommit push =
         pushFactory.create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             "b.txt",
@@ -246,7 +245,7 @@
 
     ChangeData cd = r.getChange();
     RevCommit c2 = r.getCommit();
-    assertThat(cd.change().getStatus()).isEqualTo(Change.Status.MERGED);
+    assertThat(cd.change().isMerged()).isTrue();
     PatchSet.Id psId2 = cd.change().currentPatchSetId();
     assertThat(psId2.get()).isEqualTo(2);
     assertCommit(project, "refs/heads/master");
@@ -276,9 +275,9 @@
     testRepo.reset(c1);
     assertPushOk(pushHead(testRepo, "refs/heads/master", false), "refs/heads/master");
 
-    cd = changeDataFactory.create(db, project, psId1.getParentKey());
+    cd = changeDataFactory.create(project, psId1.getParentKey());
     Change c = cd.change();
-    assertThat(c.getStatus()).isEqualTo(Change.Status.MERGED);
+    assertThat(c.isMerged()).isTrue();
     assertThat(c.currentPatchSetId()).isEqualTo(psId1);
     assertThat(cd.patchSets().stream().map(PatchSet::getId).collect(toList()))
         .containsExactly(psId1, psId2);
@@ -317,14 +316,14 @@
     assertPushOk(pushHead(testRepo, "refs/heads/master", false), "refs/heads/master");
 
     ChangeData cd2 = r2.getChange();
-    assertThat(cd2.change().getStatus()).isEqualTo(Change.Status.MERGED);
+    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());
 
     ChangeData cd1 = r1.getChange();
-    assertThat(cd1.change().getStatus()).isEqualTo(Change.Status.MERGED);
+    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());
@@ -332,15 +331,15 @@
   }
 
   private PatchSetApproval getSubmitter(PatchSet.Id patchSetId) throws Exception {
-    ChangeNotes notes = notesFactory.createChecked(db, project, patchSetId.getParentKey()).load();
-    return approvalsUtil.getSubmitter(db, notes, patchSetId);
+    ChangeNotes notes = notesFactory.createChecked(project, patchSetId.getParentKey()).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.getAccountId()).isEqualTo(admin.id());
   }
 
   private void assertCommit(Project.NameKey project, String branch) throws Exception {
@@ -348,8 +347,8 @@
         RevWalk rw = new RevWalk(r)) {
       RevCommit c = rw.parseCommit(r.exactRef(branch).getObjectId());
       assertThat(c.getShortMessage()).isEqualTo(PushOneCommit.SUBJECT);
-      assertThat(c.getAuthorIdent().getEmailAddress()).isEqualTo(admin.email);
-      assertThat(c.getCommitterIdent().getEmailAddress()).isEqualTo(admin.email);
+      assertThat(c.getAuthorIdent().getEmailAddress()).isEqualTo(admin.email());
+      assertThat(c.getCommitterIdent().getEmailAddress()).isEqualTo(admin.email());
     }
   }
 
@@ -359,7 +358,7 @@
       RevCommit c = rw.parseCommit(r.exactRef(branch).getObjectId());
       assertThat(c.getParentCount()).isEqualTo(2);
       assertThat(c.getShortMessage()).isEqualTo("Merge \"" + subject + "\"");
-      assertThat(c.getAuthorIdent().getEmailAddress()).isEqualTo(admin.email);
+      assertThat(c.getAuthorIdent().getEmailAddress()).isEqualTo(admin.email());
       assertThat(c.getCommitterIdent().getEmailAddress())
           .isEqualTo(serverIdent.get().getEmailAddress());
     }
@@ -367,8 +366,7 @@
 
   private PushOneCommit.Result push(String ref, String subject, String fileName, String content)
       throws Exception {
-    PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), testRepo, subject, fileName, content);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo, subject, fileName, content);
     return push.to(ref);
   }
 
@@ -376,7 +374,7 @@
       String ref, String subject, String fileName, String content, String changeId)
       throws Exception {
     PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), testRepo, subject, fileName, content, changeId);
+        pushFactory.create(admin.newIdent(), testRepo, subject, fileName, content, changeId);
     return push.to(ref);
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
index 03b1165..e090fae 100644
--- a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.TestTimeUtil;
+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;
@@ -47,198 +48,169 @@
   @Test
   @GerritConfig(name = "submodule.enableSuperProjectSubscriptions", value = "false")
   public void testSubscriptionWithoutGlobalServerSetting() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
 
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
     pushChangeTo(subRepo, "master");
-    assertThat(hasSubmodule(superRepo, "master", "subscribed-to-project")).isFalse();
+    assertThat(hasSubmodule(superRepo, "master", subKey)).isFalse();
   }
 
   @Test
   public void subscriptionWithoutSpecificSubscription() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
     pushChangeTo(subRepo, "master");
-    assertThat(hasSubmodule(superRepo, "master", "subscribed-to-project")).isFalse();
+    assertThat(hasSubmodule(superRepo, "master", subKey)).isFalse();
   }
 
   @Test
   public void subscriptionToEmptyRepo() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
 
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
+
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
     pushChangeTo(subRepo, "master");
     ObjectId subHEAD = pushChangeTo(subRepo, "master");
-    assertThat(hasSubmodule(superRepo, "master", "subscribed-to-project")).isTrue();
-    expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project", subHEAD);
+    assertThat(hasSubmodule(superRepo, "master", subKey)).isTrue();
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subHEAD);
   }
 
   @Test
   public void subscriptionToExistingRepo() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
+
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
     ObjectId subHEAD = pushChangeTo(subRepo, "master");
-    assertThat(hasSubmodule(superRepo, "master", "subscribed-to-project")).isTrue();
-    expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project", subHEAD);
+    assertThat(hasSubmodule(superRepo, "master", subKey)).isTrue();
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subHEAD);
   }
 
   @Test
   public void subscriptionWildcardACLForSingleBranch() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+
     // master is allowed to be subscribed to master branch only:
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", null);
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, null);
     // create 'branch':
     pushChangeTo(superRepo, "branch");
 
     pushChangeTo(subRepo, "master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
-    createSubmoduleSubscription(superRepo, "branch", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
+    createSubmoduleSubscription(superRepo, "branch", subKey, "master");
 
     ObjectId subHEAD = pushChangeTo(subRepo, "master");
 
-    expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project", subHEAD);
-    assertThat(hasSubmodule(superRepo, "branch", "subscribed-to-project")).isFalse();
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subHEAD);
+    assertThat(hasSubmodule(superRepo, "branch", subKey)).isFalse();
   }
 
   @Test
   public void subscriptionWildcardACLForMissingProject() throws Exception {
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+
     allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/*", "not-existing-super-project", "refs/heads/*");
+        subKey, "refs/heads/*", new Project.NameKey("not-existing-super-project"), "refs/heads/*");
     pushChangeTo(subRepo, "master");
   }
 
   @Test
   public void subscriptionWildcardACLForMissingBranch() throws Exception {
-    createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/*", "super-project", "refs/heads/*");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/*", superKey, "refs/heads/*");
     pushChangeTo(subRepo, "foo");
   }
 
   @Test
   public void subscriptionWildcardACLForMissingGitmodules() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/*", "super-project", "refs/heads/*");
+
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/*", superKey, "refs/heads/*");
     pushChangeTo(superRepo, "master");
     pushChangeTo(subRepo, "master");
   }
 
   @Test
   public void subscriptionWildcardACLOneOnOneMapping() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+
     // any branch is allowed to be subscribed to the same superprojects branch:
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/*", "super-project", "refs/heads/*");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/*", superKey, "refs/heads/*");
 
     // create 'branch' in both repos:
     pushChangeTo(superRepo, "branch");
     pushChangeTo(subRepo, "branch");
 
     pushChangeTo(subRepo, "master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
-    createSubmoduleSubscription(superRepo, "branch", "subscribed-to-project", "branch");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
+    createSubmoduleSubscription(superRepo, "branch", subKey, "branch");
 
     ObjectId subHEAD1 = pushChangeTo(subRepo, "master");
     ObjectId subHEAD2 = pushChangeTo(subRepo, "branch");
 
-    expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project", subHEAD1);
-    expectToHaveSubmoduleState(superRepo, "branch", "subscribed-to-project", subHEAD2);
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subHEAD1);
+    expectToHaveSubmoduleState(superRepo, "branch", subKey, subHEAD2);
 
     // Now test that cross subscriptions do not work:
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "branch");
+    createSubmoduleSubscription(superRepo, "master", subKey, "branch");
     ObjectId subHEAD3 = pushChangeTo(subRepo, "branch");
 
-    expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project", subHEAD1);
-    expectToHaveSubmoduleState(superRepo, "branch", "subscribed-to-project", subHEAD3);
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subHEAD1);
+    expectToHaveSubmoduleState(superRepo, "branch", subKey, subHEAD3);
   }
 
   @Test
   public void subscriptionWildcardACLForManyBranches() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
 
     // Any branch is allowed to be subscribed to any superproject branch:
-    allowSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/*", "super-project", null, false);
+    allowSubmoduleSubscription(subKey, "refs/heads/*", superKey, null, false);
     pushChangeTo(superRepo, "branch");
     pushChangeTo(subRepo, "another-branch");
-    createSubmoduleSubscription(superRepo, "branch", "subscribed-to-project", "another-branch");
+    createSubmoduleSubscription(superRepo, "branch", subKey, "another-branch");
     ObjectId subHEAD = pushChangeTo(subRepo, "another-branch");
-    expectToHaveSubmoduleState(superRepo, "branch", "subscribed-to-project", subHEAD);
+    expectToHaveSubmoduleState(superRepo, "branch", subKey, subHEAD);
   }
 
   @Test
   public void subscriptionWildcardACLOneToManyBranches() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
 
     // Any branch is allowed to be subscribed to any superproject branch:
-    allowSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/*", false);
+    allowSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/*", false);
     pushChangeTo(superRepo, "branch");
-    createSubmoduleSubscription(superRepo, "branch", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "branch", subKey, "master");
     ObjectId subHEAD = pushChangeTo(subRepo, "master");
-    expectToHaveSubmoduleState(superRepo, "branch", "subscribed-to-project", subHEAD);
+    expectToHaveSubmoduleState(superRepo, "branch", subKey, subHEAD);
 
-    createSubmoduleSubscription(superRepo, "branch", "subscribed-to-project", "branch");
+    createSubmoduleSubscription(superRepo, "branch", subKey, "branch");
     pushChangeTo(subRepo, "branch");
 
     // no change expected, as only master is subscribed:
-    expectToHaveSubmoduleState(superRepo, "branch", "subscribed-to-project", subHEAD);
+    expectToHaveSubmoduleState(superRepo, "branch", subKey, subHEAD);
   }
 
   @Test
   @GerritConfig(name = "submodule.verboseSuperprojectUpdate", value = "false")
   public void testSubmoduleShortCommitMessage() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
+
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
 
     // The first update doesn't include any commit messages
     ObjectId subRepoId = pushChangeTo(subRepo, "master");
-    expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project", subRepoId);
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subRepoId);
     expectToHaveCommitMessage(superRepo, "master", "Update git submodules\n\n");
 
     // Any following update also has a short message
     subRepoId = pushChangeTo(subRepo, "master");
-    expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project", subRepoId);
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subRepoId);
     expectToHaveCommitMessage(superRepo, "master", "Update git submodules\n\n");
   }
 
   @Test
   @GerritConfig(name = "submodule.verboseSuperprojectUpdate", value = "SUBJECT_ONLY")
   public void testSubmoduleSubjectCommitMessage() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
+
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
     ObjectId subHEAD = pushChangeTo(subRepo, "master");
 
     // The first update doesn't include the rev log
@@ -248,7 +220,7 @@
         "master",
         "Update git submodules\n\n"
             + "* Update "
-            + name("subscribed-to-project")
+            + subKey.get()
             + " from branch 'master'\n  to "
             + subHEAD.getName());
 
@@ -261,7 +233,7 @@
         "master",
         "Update git submodules\n\n"
             + "* Update "
-            + name("subscribed-to-project")
+            + subKey.get()
             + " from branch 'master'\n  to "
             + subHEAD.getName()
             + "\n  - "
@@ -270,13 +242,11 @@
 
   @Test
   public void submoduleCommitMessage() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
+
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
     ObjectId subHEAD = pushChangeTo(subRepo, "master");
 
     // The first update doesn't include the rev log
@@ -286,7 +256,7 @@
         "master",
         "Update git submodules\n\n"
             + "* Update "
-            + name("subscribed-to-project")
+            + subKey.get()
             + " from branch 'master'\n  to "
             + subHEAD.getName());
 
@@ -299,7 +269,7 @@
         "master",
         "Update git submodules\n\n"
             + "* Update "
-            + name("subscribed-to-project")
+            + subKey.get()
             + " from branch 'master'\n  to "
             + subHEAD.getName()
             + "\n  - "
@@ -308,171 +278,154 @@
 
   @Test
   public void subscriptionUnsubscribe() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
+
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
 
     pushChangeTo(subRepo, "master");
     ObjectId subHEADbeforeUnsubscribing = pushChangeTo(subRepo, "master");
 
     deleteAllSubscriptions(superRepo, "master");
-    expectToHaveSubmoduleState(
-        superRepo, "master", "subscribed-to-project", subHEADbeforeUnsubscribing);
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subHEADbeforeUnsubscribing);
 
     pushChangeTo(superRepo, "refs/heads/master", "commit after unsubscribe", "");
     pushChangeTo(subRepo, "refs/heads/master", "commit after unsubscribe", "");
-    expectToHaveSubmoduleState(
-        superRepo, "master", "subscribed-to-project", subHEADbeforeUnsubscribing);
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subHEADbeforeUnsubscribing);
   }
 
   @Test
   public void subscriptionUnsubscribeByDeletingGitModules() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
+
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
 
     pushChangeTo(subRepo, "master");
     ObjectId subHEADbeforeUnsubscribing = pushChangeTo(subRepo, "master");
 
     deleteGitModulesFile(superRepo, "master");
-    expectToHaveSubmoduleState(
-        superRepo, "master", "subscribed-to-project", subHEADbeforeUnsubscribing);
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subHEADbeforeUnsubscribing);
 
     pushChangeTo(superRepo, "refs/heads/master", "commit after unsubscribe", "");
     pushChangeTo(subRepo, "refs/heads/master", "commit after unsubscribe", "");
-    expectToHaveSubmoduleState(
-        superRepo, "master", "subscribed-to-project", subHEADbeforeUnsubscribing);
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subHEADbeforeUnsubscribing);
   }
 
   @Test
   public void subscriptionToDifferentBranches() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/foo", "super-project", "refs/heads/master");
 
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "foo");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/foo", superKey, "refs/heads/master");
+
+    createSubmoduleSubscription(superRepo, "master", subKey, "foo");
     ObjectId subFoo = pushChangeTo(subRepo, "foo");
     pushChangeTo(subRepo, "master");
 
-    expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project", subFoo);
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subFoo);
   }
 
   @Test
   public void branchCircularSubscription() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
-    allowMatchingSubmoduleSubscription(
-        "super-project", "refs/heads/master", "subscribed-to-project", "refs/heads/master");
+
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
+    allowMatchingSubmoduleSubscription(superKey, "refs/heads/master", subKey, "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
     pushChangeTo(superRepo, "master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
-    createSubmoduleSubscription(subRepo, "master", "super-project", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
+    createSubmoduleSubscription(subRepo, "master", superKey, "master");
 
     pushChangeTo(subRepo, "master");
     pushChangeTo(superRepo, "master");
 
-    assertThat(hasSubmodule(subRepo, "master", "super-project")).isFalse();
-    assertThat(hasSubmodule(superRepo, "master", "subscribed-to-project")).isFalse();
+    assertThat(hasSubmodule(subRepo, "master", superKey)).isFalse();
+    assertThat(hasSubmodule(superRepo, "master", subKey)).isFalse();
   }
 
   @Test
   public void projectCircularSubscription() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
 
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
-    allowMatchingSubmoduleSubscription(
-        "super-project", "refs/heads/dev", "subscribed-to-project", "refs/heads/dev");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
+    allowMatchingSubmoduleSubscription(superKey, "refs/heads/dev", subKey, "refs/heads/dev");
 
     pushChangeTo(subRepo, "master");
     pushChangeTo(superRepo, "master");
     pushChangeTo(subRepo, "dev");
     pushChangeTo(superRepo, "dev");
 
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
-    createSubmoduleSubscription(subRepo, "dev", "super-project", "dev");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
+    createSubmoduleSubscription(subRepo, "dev", superKey, "dev");
 
     ObjectId subMasterHead = pushChangeTo(subRepo, "master");
     ObjectId superDevHead = pushChangeTo(superRepo, "dev");
 
-    assertThat(hasSubmodule(superRepo, "master", "subscribed-to-project")).isTrue();
-    assertThat(hasSubmodule(subRepo, "dev", "super-project")).isTrue();
-    expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project", subMasterHead);
-    expectToHaveSubmoduleState(subRepo, "dev", "super-project", superDevHead);
+    assertThat(hasSubmodule(superRepo, "master", subKey)).isTrue();
+    assertThat(hasSubmodule(subRepo, "dev", superKey)).isTrue();
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subMasterHead);
+    expectToHaveSubmoduleState(subRepo, "dev", superKey, superDevHead);
   }
 
   @Test
   public void subscriptionFailOnMissingACL() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
 
     pushChangeTo(subRepo, "master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
     pushChangeTo(subRepo, "master");
-    assertThat(hasSubmodule(superRepo, "master", "subscribed-to-project")).isFalse();
+    assertThat(hasSubmodule(superRepo, "master", subKey)).isFalse();
   }
 
   @Test
   public void subscriptionFailOnWrongProjectACL() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
     allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "wrong-super-project", "refs/heads/master");
+        subKey,
+        "refs/heads/master",
+        new Project.NameKey("wrong-super-project"),
+        "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
     pushChangeTo(subRepo, "master");
-    assertThat(hasSubmodule(superRepo, "master", "subscribed-to-project")).isFalse();
+    assertThat(hasSubmodule(superRepo, "master", subKey)).isFalse();
   }
 
   @Test
   public void subscriptionFailOnWrongBranchACL() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
     allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/wrong-branch");
+        subKey, "refs/heads/master", superKey, "refs/heads/wrong-branch");
 
     pushChangeTo(subRepo, "master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
     pushChangeTo(subRepo, "master");
-    assertThat(hasSubmodule(superRepo, "master", "subscribed-to-project")).isFalse();
+    assertThat(hasSubmodule(superRepo, "master", subKey)).isFalse();
   }
 
   @Test
   public void subscriptionInheritACL() throws Exception {
-    createProjectWithPush("config-repo");
-    createProjectWithPush("config-repo2", new Project.NameKey(name("config-repo")));
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo =
-        createProjectWithPush("subscribed-to-project", new Project.NameKey(name("config-repo2")));
-    allowMatchingSubmoduleSubscription(
-        "config-repo", "refs/heads/*", "super-project", "refs/heads/*");
+    Project.NameKey configKey = projectOperations.newProject().submitType(getSubmitType()).create();
+    grantPush(configKey);
+    Project.NameKey config2Key =
+        projectOperations.newProject().parent(configKey).submitType(getSubmitType()).create();
+    grantPush(config2Key);
+    cloneProject(config2Key);
+
+    subKey = projectOperations.newProject().parent(config2Key).submitType(getSubmitType()).create();
+    grantPush(subKey);
+    subRepo = cloneProject(subKey);
+
+    allowMatchingSubmoduleSubscription(configKey, "refs/heads/*", superKey, "refs/heads/*");
 
     pushChangeTo(subRepo, "master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
     ObjectId subHEAD = pushChangeTo(subRepo, "master");
-    expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project", subHEAD);
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subHEAD);
   }
 
   @Test
   public void allowedButNotSubscribed() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
+
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
     subRepo
@@ -490,24 +443,22 @@
     assertThat(r.getRemoteUpdate("refs/heads/master").getStatus())
         .isEqualTo(RemoteRefUpdate.Status.OK);
 
-    assertThat(hasSubmodule(superRepo, "master", "subscribed-to-project")).isFalse();
+    assertThat(hasSubmodule(superRepo, "master", subKey)).isFalse();
   }
 
   @Test
   public void subscriptionDeepRelative() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("nested/subscribed-to-project");
+    Project.NameKey nest = createProjectForPush(getSubmitType());
+    TestRepository<?> subRepo = cloneProject(nest);
     // master is allowed to be subscribed to any superprojects branch:
-    allowMatchingSubmoduleSubscription(
-        "nested/subscribed-to-project", "refs/heads/master", "super-project", null);
+    allowMatchingSubmoduleSubscription(nest, "refs/heads/master", superKey, null);
 
     pushChangeTo(subRepo, "master");
-    createRelativeSubmoduleSubscription(
-        superRepo, "master", "../", "nested/subscribed-to-project", "master");
+    createRelativeSubmoduleSubscription(superRepo, "master", "../", nest, "master");
 
     ObjectId subHEAD = pushChangeTo(subRepo, "master");
 
-    expectToHaveSubmoduleState(superRepo, "master", "nested/subscribed-to-project", subHEAD);
+    expectToHaveSubmoduleState(superRepo, "master", nest, subHEAD);
   }
 
   @Test
@@ -519,7 +470,9 @@
 
   @Test
   @GerritConfig(name = "submodule.verboseSuperprojectUpdate", value = "SUBJECT_ONLY")
-  @GerritConfig(name = "submodule.maxCombinedCommitMessageSize", value = "220")
+  // The value 110 must tuned to the test environment, and is sensitive to the
+  // length of the uniquified repository name.
+  @GerritConfig(name = "submodule.maxCombinedCommitMessageSize", value = "110")
   public void submoduleSubjectCommitMessageSizeLimit() throws Exception {
     assume().that(isSubmitWholeTopicEnabled()).isFalse();
     testSubmoduleSubjectCommitMessageAndExpectTruncation();
@@ -530,11 +483,9 @@
     // Make sure that the commit is created at an earlier timestamp than the submit timestamp.
     TestTimeUtil.resetWithClockStep(1, SECONDS);
     try {
-      TestRepository<?> superRepo = createProjectWithPush("super-project");
-      TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
       allowMatchingSubmoduleSubscription(
-          "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
-      createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+          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);
@@ -544,8 +495,8 @@
       // 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.getName()).isEqualTo(admin.fullName());
+      assertThat(authorIdent.getEmailAddress()).isEqualTo(admin.email());
       assertThat(authorIdent.getWhen())
           .isGreaterThan(pushResult.getCommit().getAuthorIdent().getWhen());
     } finally {
@@ -561,18 +512,17 @@
     // is afterwards.
     TestTimeUtil.resetWithClockStep(1, SECONDS);
     try {
-      TestRepository<?> superRepo = createProjectWithPush("super-project");
-      TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-      TestRepository<?> subRepo2 = createProjectWithPush("subscribed-to-project-2");
 
+      Project.NameKey proj2 = createProjectForPush(getSubmitType());
+
+      TestRepository<?> subRepo2 = cloneProject(proj2);
       allowMatchingSubmoduleSubscription(
-          "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
-      allowMatchingSubmoduleSubscription(
-          "subscribed-to-project-2", "refs/heads/master", "super-project", "refs/heads/master");
+          subKey, "refs/heads/master", superKey, "refs/heads/master");
+      allowMatchingSubmoduleSubscription(proj2, "refs/heads/master", superKey, "refs/heads/master");
 
       Config config = new Config();
-      prepareSubmoduleConfigEntry(config, "subscribed-to-project", "master");
-      prepareSubmoduleConfigEntry(config, "subscribed-to-project-2", "master");
+      prepareSubmoduleConfigEntry(config, subKey, subKey, "master");
+      prepareSubmoduleConfigEntry(config, proj2, proj2, "master");
       pushSubmoduleConfig(superRepo, "master", config);
 
       String topic = "foo";
@@ -591,8 +541,8 @@
       // 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.getName()).isEqualTo(admin.fullName());
+      assertThat(authorIdent.getEmailAddress()).isEqualTo(admin.email());
       assertThat(authorIdent.getWhen())
           .isGreaterThan(pushResult1.getCommit().getAuthorIdent().getWhen());
       assertThat(authorIdent.getWhen())
@@ -610,21 +560,16 @@
     // is afterwards.
     TestTimeUtil.resetWithClockStep(1, SECONDS);
     try {
-      TestRepository<?> superRepo = createProjectWithPush("super-project");
-      TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-
-      createProjectWithPush("subscribed-to-project-2");
-      TestRepository<?> subRepo2 =
-          cloneProject(new Project.NameKey(name("subscribed-to-project-2")), user);
+      Project.NameKey proj2 = createProjectForPush(getSubmitType());
+      TestRepository<InMemoryRepository> repo2 = cloneProject(proj2, user);
 
       allowMatchingSubmoduleSubscription(
-          "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
-      allowMatchingSubmoduleSubscription(
-          "subscribed-to-project-2", "refs/heads/master", "super-project", "refs/heads/master");
+          subKey, "refs/heads/master", superKey, "refs/heads/master");
+      allowMatchingSubmoduleSubscription(proj2, "refs/heads/master", superKey, "refs/heads/master");
 
       Config config = new Config();
-      prepareSubmoduleConfigEntry(config, "subscribed-to-project", "master");
-      prepareSubmoduleConfigEntry(config, "subscribed-to-project-2", "master");
+      prepareSubmoduleConfigEntry(config, subKey, subKey, "master");
+      prepareSubmoduleConfigEntry(config, proj2, proj2, "master");
       pushSubmoduleConfig(superRepo, "master", config);
 
       String topic = "foo";
@@ -636,7 +581,7 @@
 
       // Create change as user.
       PushOneCommit push =
-          pushFactory.create(db, user.getIdent(), subRepo2, "Change 2", "b.txt", "other content");
+          pushFactory.create(user.newIdent(), repo2, "Change 2", "b.txt", "other content");
       PushOneCommit.Result pushResult2 = push.to("refs/for/master/" + name(topic));
       approve(pushResult2.getChangeId());
 
@@ -659,71 +604,65 @@
 
   @Test
   public void updateOnlyRelevantSubmodules() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo1 = createProjectWithPush("subscribed-to-project-1");
-    TestRepository<?> subRepo2 = createProjectWithPush("subscribed-to-project-2");
+    Project.NameKey subkey1 = createProjectForPush(getSubmitType());
+    Project.NameKey subkey2 = createProjectForPush(getSubmitType());
+    TestRepository<?> subRepo1 = cloneProject(subkey1);
+    TestRepository<?> subRepo2 = cloneProject(subkey2);
 
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project-1", "refs/heads/master", "super-project", "refs/heads/master");
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project-2", "refs/heads/master", "super-project", "refs/heads/master");
+    allowMatchingSubmoduleSubscription(subkey1, "refs/heads/master", superKey, "refs/heads/master");
+    allowMatchingSubmoduleSubscription(subkey2, "refs/heads/master", superKey, "refs/heads/master");
 
     Config config = new Config();
-    prepareSubmoduleConfigEntry(config, "subscribed-to-project-1", "master");
-    prepareSubmoduleConfigEntry(config, "subscribed-to-project-2", "master");
+    prepareSubmoduleConfigEntry(config, subkey1, "master");
+    prepareSubmoduleConfigEntry(config, subkey2, "master");
     pushSubmoduleConfig(superRepo, "master", config);
 
     // Push once to initialize submodules.
     ObjectId subTip2 = pushChangeTo(subRepo2, "master");
     ObjectId subTip1 = pushChangeTo(subRepo1, "master");
 
-    expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project-1", subTip1);
-    expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project-2", subTip2);
+    expectToHaveSubmoduleState(superRepo, "master", subkey1, subTip1);
+    expectToHaveSubmoduleState(superRepo, "master", subkey2, subTip2);
 
-    directUpdateRef("subscribed-to-project-2", "refs/heads/master");
+    directUpdateRef(subkey2, "refs/heads/master");
     subTip1 = pushChangeTo(subRepo1, "master");
 
-    expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project-1", subTip1);
-    expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project-2", subTip2);
+    expectToHaveSubmoduleState(superRepo, "master", subkey1, subTip1);
+    expectToHaveSubmoduleState(superRepo, "master", subkey2, subTip2);
   }
 
   @Test
   public void skipUpdatingBrokenGitlinkPointer() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
 
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
 
     // Push once to initialize submodule.
     ObjectId subTip = pushChangeTo(subRepo, "master");
-    expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project", subTip);
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subTip);
 
     // Write an invalid SHA-1 directly to the gitlink.
     ObjectId badId = ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
-    directUpdateSubmodule("super-project", "refs/heads/master", "subscribed-to-project", badId);
-    expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project", badId);
+    directUpdateSubmodule(superKey, "refs/heads/master", subKey, badId);
+    expectToHaveSubmoduleState(superRepo, "master", subKey, badId);
 
     // Push succeeds, but gitlink update is skipped.
     pushChangeTo(subRepo, "master");
-    expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project", badId);
+    expectToHaveSubmoduleState(superRepo, "master", subKey, badId);
   }
 
-  private ObjectId directUpdateRef(String project, String ref) throws Exception {
-    try (Repository serverRepo = repoManager.openRepository(new Project.NameKey(name(project)))) {
-      return new TestRepository<>(serverRepo).branch(ref).commit().create().copy();
+  private ObjectId directUpdateRef(Project.NameKey project, String ref) throws Exception {
+    try (Repository serverRepo = repoManager.openRepository(project);
+        TestRepository<Repository> tr = new TestRepository<>(serverRepo)) {
+      return tr.branch(ref).commit().create().copy();
     }
   }
 
   private void testSubmoduleSubjectCommitMessageAndExpectTruncation() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
     // The first update doesn't include the rev log, so we ignore it
     pushChangeTo(subRepo, "master");
 
@@ -736,6 +675,6 @@
         "master",
         String.format(
             "Update git submodules\n\n* Update %s from branch 'master'\n  to %s\n  - %s\n\n[...]",
-            name("subscribed-to-project"), subHEAD.getName(), subCommitMsg.getShortMessage()));
+            subKey.get(), subHEAD.getName(), subCommitMsg.getShortMessage()));
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
index eef3295..dc84d13 100644
--- a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
@@ -16,11 +16,11 @@
 
 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.GitUtil.getChangeId;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.SubmitType;
@@ -30,6 +30,7 @@
 import com.google.gerrit.testing.ConfigSuite;
 import java.util.ArrayDeque;
 import java.util.Map;
+import org.apache.commons.lang.RandomStringUtils;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
@@ -68,12 +69,8 @@
 
   @Test
   public void subscriptionUpdateOfManyChanges() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
-
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
 
     ObjectId subHEAD =
         subRepo
@@ -150,15 +147,13 @@
             .getAdvertisedRef("refs/heads/master")
             .getObjectId();
 
-    expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project", subRepoId);
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subRepoId);
 
     // 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:
-    Project.NameKey p1 = new Project.NameKey(name("super-project"));
-    Project.NameKey p2 = new Project.NameKey(name("subscribed-to-project"));
-    assertThat(preview).containsKey(new Branch.NameKey(p1, "refs/heads/master"));
-    assertThat(preview).containsKey(new Branch.NameKey(p2, "refs/heads/master"));
+    assertThat(preview).containsKey(new Branch.NameKey(superKey, "refs/heads/master"));
+    assertThat(preview).containsKey(new Branch.NameKey(subKey, "refs/heads/master"));
 
     if ((getSubmitType() == SubmitType.CHERRY_PICK)
         || (getSubmitType() == SubmitType.REBASE_ALWAYS)) {
@@ -176,12 +171,9 @@
 
   @Test
   public void subscriptionUpdateIncludingChangeInSuperproject() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
 
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
 
     ObjectId subHEAD =
         subRepo
@@ -274,71 +266,65 @@
             .getAdvertisedRef("refs/heads/master")
             .getObjectId();
 
-    expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project", subRepoId);
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subRepoId);
   }
 
   @Test
   public void updateManySubmodules() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> sub1 = createProjectWithPush("sub1");
-    TestRepository<?> sub2 = createProjectWithPush("sub2");
-    TestRepository<?> sub3 = createProjectWithPush("sub3");
+    final int NUM = 3;
+    Project.NameKey subKey[] = new Project.NameKey[NUM];
+    TestRepository<?> sub[] = new TestRepository[NUM];
+    String prefix = RandomStringUtils.randomAlphabetic(8);
+    for (int i = 0; i < subKey.length; i++) {
+      subKey[i] =
+          projectOperations
+              .newProject()
+              .name(prefix + "sub" + i)
+              .submitType(getSubmitType())
+              .create();
+      grant(subKey[i], "refs/heads/*", Permission.PUSH);
+      grant(subKey[i], "refs/for/refs/heads/*", Permission.SUBMIT);
+      sub[i] = cloneProject(subKey[i]);
+    }
 
-    allowMatchingSubmoduleSubscription(
-        "sub1", "refs/heads/master", "super-project", "refs/heads/master");
-    allowMatchingSubmoduleSubscription(
-        "sub2", "refs/heads/master", "super-project", "refs/heads/master");
-    allowMatchingSubmoduleSubscription(
-        "sub3", "refs/heads/master", "super-project", "refs/heads/master");
+    for (int i = 0; i < subKey.length; i++) {
+      allowMatchingSubmoduleSubscription(
+          subKey[i], "refs/heads/master", superKey, "refs/heads/master");
+    }
 
     Config config = new Config();
-    prepareSubmoduleConfigEntry(config, "sub1", "master");
-    prepareSubmoduleConfigEntry(config, "sub2", "master");
-    prepareSubmoduleConfigEntry(config, "sub3", "master");
+    for (int i = 0; i < subKey.length; i++) {
+      prepareSubmoduleConfigEntry(config, subKey[i], "master");
+    }
     pushSubmoduleConfig(superRepo, "master", config);
 
     ObjectId superPreviousId = pushChangeTo(superRepo, "master");
 
-    ObjectId sub1Id = pushChangeTo(sub1, "refs/for/master", "some message", "same-topic");
-    ObjectId sub2Id = pushChangeTo(sub2, "refs/for/master", "some message", "same-topic");
-    ObjectId sub3Id = pushChangeTo(sub3, "refs/for/master", "some message", "same-topic");
+    ObjectId subId[] = new ObjectId[NUM];
 
-    approve(getChangeId(sub1, sub1Id).get());
-    approve(getChangeId(sub2, sub2Id).get());
-    approve(getChangeId(sub3, sub3Id).get());
+    for (int i = 0; i < sub.length; i++) {
+      subId[i] = pushChangeTo(sub[i], "refs/for/master", "some message", "same-topic");
+      approve(getChangeId(sub[i], subId[i]).get());
+    }
 
-    gApi.changes().id(getChangeId(sub1, sub1Id).get()).current().submit();
+    gApi.changes().id(getChangeId(sub[0], subId[0]).get()).current().submit();
 
-    expectToHaveSubmoduleState(superRepo, "master", "sub1", sub1, "master");
-    expectToHaveSubmoduleState(superRepo, "master", "sub2", sub2, "master");
-    expectToHaveSubmoduleState(superRepo, "master", "sub3", sub3, "master");
+    for (int i = 0; i < sub.length; i++) {
+      expectToHaveSubmoduleState(superRepo, "master", subKey[i], sub[i], "master");
+    }
 
-    String sub1HEAD =
-        sub1.git()
-            .fetch()
-            .setRemote("origin")
-            .call()
-            .getAdvertisedRef("refs/heads/master")
-            .getObjectId()
-            .name();
-
-    String sub2HEAD =
-        sub2.git()
-            .fetch()
-            .setRemote("origin")
-            .call()
-            .getAdvertisedRef("refs/heads/master")
-            .getObjectId()
-            .name();
-
-    String sub3HEAD =
-        sub3.git()
-            .fetch()
-            .setRemote("origin")
-            .call()
-            .getAdvertisedRef("refs/heads/master")
-            .getObjectId()
-            .name();
+    String heads[] = new String[NUM];
+    for (int i = 0; i < heads.length; i++) {
+      heads[i] =
+          sub[i]
+              .git()
+              .fetch()
+              .setRemote("origin")
+              .call()
+              .getAdvertisedRef("refs/heads/master")
+              .getObjectId()
+              .name();
+    }
 
     if (getSubmitType() == SubmitType.MERGE_IF_NECESSARY) {
       expectToHaveCommitMessage(
@@ -346,17 +332,17 @@
           "master",
           "Update git submodules\n\n"
               + "* Update "
-              + name("sub1")
+              + subKey[0].get()
               + " from branch 'master'\n  to "
-              + sub1HEAD
+              + heads[0]
               + "\n\n* Update "
-              + name("sub2")
+              + subKey[1].get()
               + " from branch 'master'\n  to "
-              + sub2HEAD
+              + heads[1]
               + "\n\n* Update "
-              + name("sub3")
+              + subKey[2].get()
               + " from branch 'master'\n  to "
-              + sub3HEAD);
+              + heads[2]);
     }
 
     superRepo
@@ -374,73 +360,98 @@
 
   @Test
   public void doNotUseFastForward() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project", false);
-    TestRepository<?> sub = createProjectWithPush("sub", false);
+    // like setup, but without empty commit
+    superKey =
+        projectOperations
+            .newProject()
+            .submitType(getSubmitType())
+            .createEmptyCommit(false)
+            .create();
+    grantPush(superKey);
+    subKey =
+        projectOperations
+            .newProject()
+            .submitType(getSubmitType())
+            .createEmptyCommit(false)
+            .create();
+    grantPush(subKey);
+    superRepo = cloneProject(superKey);
+    subRepo = cloneProject(subKey);
 
-    allowMatchingSubmoduleSubscription(
-        "sub", "refs/heads/master", "super-project", "refs/heads/master");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
 
-    createSubmoduleSubscription(superRepo, "master", "sub", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
 
-    ObjectId subId = pushChangeTo(sub, "refs/for/master", "some message", "same-topic");
+    ObjectId subId = pushChangeTo(subRepo, "refs/for/master", "some message", "same-topic");
 
     ObjectId superId = pushChangeTo(superRepo, "refs/for/master", "some message", "same-topic");
 
-    String subChangeId = getChangeId(sub, subId).get();
+    String subChangeId = getChangeId(subRepo, subId).get();
     approve(subChangeId);
     approve(getChangeId(superRepo, superId).get());
 
     gApi.changes().id(subChangeId).current().submit();
 
-    expectToHaveSubmoduleState(superRepo, "master", "sub", sub, "master");
-    RevCommit superHead = getRemoteHead(name("super-project"), "master");
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subRepo, "master");
+    RevCommit superHead = getRemoteHead(superKey, "master");
     assertThat(superHead.getShortMessage()).contains("some message");
     assertThat(superHead.getId()).isNotEqualTo(superId);
   }
 
   @Test
   public void useFastForwardWhenNoSubmodule() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project", false);
-    TestRepository<?> sub = createProjectWithPush("sub", false);
+    // like setup, but without empty commit
+    superKey =
+        projectOperations
+            .newProject()
+            .submitType(getSubmitType())
+            .createEmptyCommit(false)
+            .create();
+    grantPush(superKey);
+    subKey =
+        projectOperations
+            .newProject()
+            .submitType(getSubmitType())
+            .createEmptyCommit(false)
+            .create();
+    grantPush(subKey);
+    superRepo = cloneProject(superKey);
+    subRepo = cloneProject(subKey);
 
-    ObjectId subId = pushChangeTo(sub, "refs/for/master", "some message", "same-topic");
-
+    ObjectId subId = pushChangeTo(subRepo, "refs/for/master", "some message", "same-topic");
     ObjectId superId = pushChangeTo(superRepo, "refs/for/master", "some message", "same-topic");
 
-    String subChangeId = getChangeId(sub, subId).get();
+    String subChangeId = getChangeId(subRepo, subId).get();
     approve(subChangeId);
     approve(getChangeId(superRepo, superId).get());
 
     gApi.changes().id(subChangeId).current().submit();
 
-    RevCommit superHead = getRemoteHead(name("super-project"), "master");
+    RevCommit superHead = getRemoteHead(superKey, "master");
     assertThat(superHead.getShortMessage()).isEqualTo("some message");
     assertThat(superHead.getId()).isEqualTo(superId);
   }
 
   @Test
   public void sameProjectSameBranchDifferentPaths() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> sub = createProjectWithPush("sub");
-
-    allowMatchingSubmoduleSubscription(
-        "sub", "refs/heads/master", "super-project", "refs/heads/master");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
 
     Config config = new Config();
-    prepareSubmoduleConfigEntry(config, "sub", "master");
-    prepareSubmoduleConfigEntry(config, "sub", "sub-copy", "master");
+    prepareSubmoduleConfigEntry(config, subKey, "master");
+    Project.NameKey copyKey = nameKey("sub-copy");
+    prepareSubmoduleConfigEntry(config, subKey, copyKey, "master");
     pushSubmoduleConfig(superRepo, "master", config);
 
     ObjectId superPreviousId = pushChangeTo(superRepo, "master");
 
-    ObjectId subId = pushChangeTo(sub, "refs/for/master", "some message", "");
+    ObjectId subId = pushChangeTo(subRepo, "refs/for/master", "some message", "");
 
-    approve(getChangeId(sub, subId).get());
+    approve(getChangeId(subRepo, subId).get());
 
-    gApi.changes().id(getChangeId(sub, subId).get()).current().submit();
+    gApi.changes().id(getChangeId(subRepo, subId).get()).current().submit();
 
-    expectToHaveSubmoduleState(superRepo, "master", "sub", sub, "master");
-    expectToHaveSubmoduleState(superRepo, "master", "sub-copy", sub, "master");
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subRepo, "master");
+    expectToHaveSubmoduleState(superRepo, "master", copyKey, subRepo, "master");
 
     superRepo
         .git()
@@ -457,37 +468,33 @@
 
   @Test
   public void sameProjectDifferentBranchDifferentPaths() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> sub = createProjectWithPush("sub");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/dev", superKey, "refs/heads/master");
 
-    allowMatchingSubmoduleSubscription(
-        "sub", "refs/heads/master", "super-project", "refs/heads/master");
-    allowMatchingSubmoduleSubscription(
-        "sub", "refs/heads/dev", "super-project", "refs/heads/master");
-
-    ObjectId devHead = pushChangeTo(sub, "dev");
+    ObjectId devHead = pushChangeTo(subRepo, "dev");
     Config config = new Config();
-    prepareSubmoduleConfigEntry(config, "sub", "sub-master", "master");
-    prepareSubmoduleConfigEntry(config, "sub", "sub-dev", "dev");
+    prepareSubmoduleConfigEntry(config, subKey, nameKey("sub-master"), "master");
+    prepareSubmoduleConfigEntry(config, subKey, nameKey("sub-dev"), "dev");
     pushSubmoduleConfig(superRepo, "master", config);
 
     ObjectId subMasterId =
-        pushChangeTo(sub, "refs/for/master", "some message", "b.txt", "content b", "same-topic");
+        pushChangeTo(
+            subRepo, "refs/for/master", "some message", "b.txt", "content b", "same-topic");
 
-    sub.reset(devHead);
+    subRepo.reset(devHead);
     ObjectId subDevId =
         pushChangeTo(
-            sub, "refs/for/dev", "some message in dev", "b.txt", "content b", "same-topic");
+            subRepo, "refs/for/dev", "some message in dev", "b.txt", "content b", "same-topic");
 
-    approve(getChangeId(sub, subMasterId).get());
-    approve(getChangeId(sub, subDevId).get());
+    approve(getChangeId(subRepo, subMasterId).get());
+    approve(getChangeId(subRepo, subDevId).get());
 
     ObjectId superPreviousId = pushChangeTo(superRepo, "master");
 
-    gApi.changes().id(getChangeId(sub, subMasterId).get()).current().submit();
+    gApi.changes().id(getChangeId(subRepo, subMasterId).get()).current().submit();
 
-    expectToHaveSubmoduleState(superRepo, "master", "sub-master", sub, "master");
-    expectToHaveSubmoduleState(superRepo, "master", "sub-dev", sub, "dev");
+    expectToHaveSubmoduleState(superRepo, "master", nameKey("sub-master"), subRepo, "master");
+    expectToHaveSubmoduleState(superRepo, "master", nameKey("sub-dev"), subRepo, "dev");
 
     superRepo
         .git()
@@ -504,29 +511,27 @@
 
   @Test
   public void nonSubmoduleInSameTopic() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> sub = createProjectWithPush("sub");
-    TestRepository<?> standAlone = createProjectWithPush("standalone");
+    Project.NameKey standaloneKey = createProjectForPush(getSubmitType());
+    TestRepository<?> standAlone = cloneProject(standaloneKey);
 
-    allowMatchingSubmoduleSubscription(
-        "sub", "refs/heads/master", "super-project", "refs/heads/master");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
 
-    createSubmoduleSubscription(superRepo, "master", "sub", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
 
     ObjectId superPreviousId = pushChangeTo(superRepo, "master");
 
-    ObjectId subId = pushChangeTo(sub, "refs/for/master", "some message", "same-topic");
+    ObjectId subId = pushChangeTo(subRepo, "refs/for/master", "some message", "same-topic");
     ObjectId standAloneId =
         pushChangeTo(standAlone, "refs/for/master", "some message", "same-topic");
 
-    String subChangeId = getChangeId(sub, subId).get();
+    String subChangeId = getChangeId(subRepo, subId).get();
     String standAloneChangeId = getChangeId(standAlone, standAloneId).get();
     approve(subChangeId);
     approve(standAloneChangeId);
 
     gApi.changes().id(subChangeId).current().submit();
 
-    expectToHaveSubmoduleState(superRepo, "master", "sub", sub, "master");
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subRepo, "master");
 
     ChangeStatus status = gApi.changes().id(standAloneChangeId).info().status;
     assertThat(status).isEqualTo(ChangeStatus.MERGED);
@@ -546,17 +551,18 @@
 
   @Test
   public void recursiveSubmodules() throws Exception {
-    TestRepository<?> topRepo = createProjectWithPush("top-project");
-    TestRepository<?> midRepo = createProjectWithPush("mid-project");
-    TestRepository<?> bottomRepo = createProjectWithPush("bottom-project");
+    Project.NameKey topKey = createProjectForPush(getSubmitType());
+    Project.NameKey midKey = createProjectForPush(getSubmitType());
+    Project.NameKey botKey = createProjectForPush(getSubmitType());
+    TestRepository<?> topRepo = cloneProject(topKey);
+    TestRepository<?> midRepo = cloneProject(midKey);
+    TestRepository<?> bottomRepo = cloneProject(botKey);
 
-    allowMatchingSubmoduleSubscription(
-        "mid-project", "refs/heads/master", "top-project", "refs/heads/master");
-    allowMatchingSubmoduleSubscription(
-        "bottom-project", "refs/heads/master", "mid-project", "refs/heads/master");
+    allowMatchingSubmoduleSubscription(midKey, "refs/heads/master", topKey, "refs/heads/master");
+    allowMatchingSubmoduleSubscription(botKey, "refs/heads/master", midKey, "refs/heads/master");
 
-    createSubmoduleSubscription(topRepo, "master", "mid-project", "master");
-    createSubmoduleSubscription(midRepo, "master", "bottom-project", "master");
+    createSubmoduleSubscription(topRepo, "master", midKey, "master");
+    createSubmoduleSubscription(midRepo, "master", botKey, "master");
 
     ObjectId bottomHead = pushChangeTo(bottomRepo, "refs/for/master", "some message", "same-topic");
     ObjectId topHead = pushChangeTo(topRepo, "refs/for/master", "some message", "same-topic");
@@ -569,27 +575,27 @@
 
     gApi.changes().id(id1).current().submit();
 
-    expectToHaveSubmoduleState(midRepo, "master", "bottom-project", bottomRepo, "master");
-    expectToHaveSubmoduleState(topRepo, "master", "mid-project", midRepo, "master");
+    expectToHaveSubmoduleState(midRepo, "master", botKey, bottomRepo, "master");
+    expectToHaveSubmoduleState(topRepo, "master", midKey, midRepo, "master");
   }
 
   @Test
   public void triangleSubmodules() throws Exception {
-    TestRepository<?> topRepo = createProjectWithPush("top-project");
-    TestRepository<?> midRepo = createProjectWithPush("mid-project");
-    TestRepository<?> bottomRepo = createProjectWithPush("bottom-project");
+    Project.NameKey topKey = createProjectForPush(getSubmitType());
+    Project.NameKey midKey = createProjectForPush(getSubmitType());
+    Project.NameKey botKey = createProjectForPush(getSubmitType());
+    TestRepository<?> topRepo = cloneProject(topKey);
+    TestRepository<?> midRepo = cloneProject(midKey);
+    TestRepository<?> bottomRepo = cloneProject(botKey);
 
-    allowMatchingSubmoduleSubscription(
-        "mid-project", "refs/heads/master", "top-project", "refs/heads/master");
-    allowMatchingSubmoduleSubscription(
-        "bottom-project", "refs/heads/master", "mid-project", "refs/heads/master");
-    allowMatchingSubmoduleSubscription(
-        "bottom-project", "refs/heads/master", "top-project", "refs/heads/master");
+    allowMatchingSubmoduleSubscription(midKey, "refs/heads/master", topKey, "refs/heads/master");
+    allowMatchingSubmoduleSubscription(botKey, "refs/heads/master", midKey, "refs/heads/master");
+    allowMatchingSubmoduleSubscription(botKey, "refs/heads/master", topKey, "refs/heads/master");
 
-    createSubmoduleSubscription(midRepo, "master", "bottom-project", "master");
+    createSubmoduleSubscription(midRepo, "master", botKey, "master");
     Config config = new Config();
-    prepareSubmoduleConfigEntry(config, "bottom-project", "master");
-    prepareSubmoduleConfigEntry(config, "mid-project", "master");
+    prepareSubmoduleConfigEntry(config, botKey, "master");
+    prepareSubmoduleConfigEntry(config, midKey, "master");
     pushSubmoduleConfig(topRepo, "master", config);
 
     ObjectId bottomHead = pushChangeTo(bottomRepo, "refs/for/master", "some message", "same-topic");
@@ -603,35 +609,35 @@
 
     gApi.changes().id(id1).current().submit();
 
-    expectToHaveSubmoduleState(midRepo, "master", "bottom-project", bottomRepo, "master");
-    expectToHaveSubmoduleState(topRepo, "master", "mid-project", midRepo, "master");
-    expectToHaveSubmoduleState(topRepo, "master", "bottom-project", bottomRepo, "master");
+    expectToHaveSubmoduleState(midRepo, "master", botKey, bottomRepo, "master");
+    expectToHaveSubmoduleState(topRepo, "master", midKey, midRepo, "master");
+    expectToHaveSubmoduleState(topRepo, "master", botKey, bottomRepo, "master");
   }
 
   private String prepareBranchCircularSubscription() throws Exception {
-    TestRepository<?> topRepo = createProjectWithPush("top-project");
-    TestRepository<?> midRepo = createProjectWithPush("mid-project");
-    TestRepository<?> bottomRepo = createProjectWithPush("bottom-project");
+    Project.NameKey topKey = createProjectForPush(getSubmitType());
+    Project.NameKey midKey = createProjectForPush(getSubmitType());
+    Project.NameKey botKey = createProjectForPush(getSubmitType());
+    TestRepository<?> topRepo = cloneProject(topKey);
+    TestRepository<?> midRepo = cloneProject(midKey);
+    TestRepository<?> bottomRepo = cloneProject(botKey);
 
-    createSubmoduleSubscription(midRepo, "master", "bottom-project", "master");
-    createSubmoduleSubscription(topRepo, "master", "mid-project", "master");
-    createSubmoduleSubscription(bottomRepo, "master", "top-project", "master");
+    createSubmoduleSubscription(midRepo, "master", botKey, "master");
+    createSubmoduleSubscription(topRepo, "master", midKey, "master");
+    createSubmoduleSubscription(bottomRepo, "master", topKey, "master");
 
-    allowMatchingSubmoduleSubscription(
-        "bottom-project", "refs/heads/master", "mid-project", "refs/heads/master");
-    allowMatchingSubmoduleSubscription(
-        "mid-project", "refs/heads/master", "top-project", "refs/heads/master");
-    allowMatchingSubmoduleSubscription(
-        "top-project", "refs/heads/master", "bottom-project", "refs/heads/master");
+    allowMatchingSubmoduleSubscription(botKey, "refs/heads/master", midKey, "refs/heads/master");
+    allowMatchingSubmoduleSubscription(midKey, "refs/heads/master", topKey, "refs/heads/master");
+    allowMatchingSubmoduleSubscription(topKey, "refs/heads/master", botKey, "refs/heads/master");
 
     ObjectId bottomMasterHead = pushChangeTo(bottomRepo, "refs/for/master", "some message", "");
     String changeId = getChangeId(bottomRepo, bottomMasterHead).get();
 
     approve(changeId);
     exception.expectMessage("Branch level circular subscriptions detected");
-    exception.expectMessage("top-project,refs/heads/master");
-    exception.expectMessage("mid-project,refs/heads/master");
-    exception.expectMessage("bottom-project,refs/heads/master");
+    exception.expectMessage(topKey.get() + ",refs/heads/master");
+    exception.expectMessage(midKey.get() + ",refs/heads/master");
+    exception.expectMessage(botKey.get() + ",refs/heads/master");
     return changeId;
   }
 
@@ -649,19 +655,14 @@
 
   @Test
   public void projectCircularSubscriptionWholeTopic() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
-    allowMatchingSubmoduleSubscription(
-        "super-project", "refs/heads/dev", "subscribed-to-project", "refs/heads/dev");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
+    allowMatchingSubmoduleSubscription(superKey, "refs/heads/dev", subKey, "refs/heads/dev");
 
     pushChangeTo(subRepo, "dev");
     pushChangeTo(superRepo, "dev");
 
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
-    createSubmoduleSubscription(subRepo, "dev", "super-project", "dev");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
+    createSubmoduleSubscription(subRepo, "dev", superKey, "dev");
 
     ObjectId subMasterHead =
         pushChangeTo(
@@ -672,15 +673,18 @@
     approve(getChangeId(superRepo, superDevHead).get());
 
     exception.expectMessage("Project level circular subscriptions detected");
-    exception.expectMessage("subscribed-to-project");
-    exception.expectMessage("super-project");
+    exception.expectMessage(subKey.get());
+    exception.expectMessage(superKey.get());
     gApi.changes().id(getChangeId(subRepo, subMasterHead).get()).current().submit();
   }
 
   @Test
   public void projectNoSubscriptionWholeTopic() throws Exception {
-    TestRepository<?> repoA = createProjectWithPush("project-a");
-    TestRepository<?> repoB = createProjectWithPush("project-b");
+    Project.NameKey keyA = createProjectForPush(getSubmitType());
+    Project.NameKey keyB = createProjectForPush(getSubmitType());
+
+    TestRepository<?> repoA = cloneProject(keyA);
+    TestRepository<?> repoB = cloneProject(keyB);
     // bootstrap the dev branch
     ObjectId a0 = pushChangeTo(repoA, "dev");
 
@@ -735,33 +739,33 @@
     approve(getChangeId(repoB, bDevHead).get());
 
     gApi.changes().id(getChangeId(repoA, aDevHead).get()).current().submit();
-    assertThat(getRemoteHead(name("project-a"), "refs/heads/master").getShortMessage())
+    assertThat(getRemoteHead(keyA, "refs/heads/master").getShortMessage())
         .contains("some message in a master.txt");
-    assertThat(getRemoteHead(name("project-a"), "refs/heads/dev").getShortMessage())
+    assertThat(getRemoteHead(keyA, "refs/heads/dev").getShortMessage())
         .contains("some message in a dev.txt");
-    assertThat(getRemoteHead(name("project-b"), "refs/heads/master").getShortMessage())
+    assertThat(getRemoteHead(keyB, "refs/heads/master").getShortMessage())
         .contains("some message in b master.txt");
-    assertThat(getRemoteHead(name("project-b"), "refs/heads/dev").getShortMessage())
+    assertThat(getRemoteHead(keyB, "refs/heads/dev").getShortMessage())
         .contains("some message in b dev.txt");
   }
 
   @Test
   public void twoProjectsMultipleBranchesWholeTopic() throws Exception {
-    TestRepository<?> repoA = createProjectWithPush("project-a");
-    TestRepository<?> repoB = createProjectWithPush("project-b");
+    Project.NameKey keyA = createProjectForPush(getSubmitType());
+    Project.NameKey keyB = createProjectForPush(getSubmitType());
+    TestRepository<?> repoA = cloneProject(keyA);
+    TestRepository<?> repoB = cloneProject(keyB);
     // bootstrap the dev branch
     pushChangeTo(repoA, "dev");
 
     // bootstrap the dev branch
     ObjectId b0 = pushChangeTo(repoB, "dev");
 
-    allowMatchingSubmoduleSubscription(
-        "project-b", "refs/heads/master", "project-a", "refs/heads/master");
-    allowMatchingSubmoduleSubscription(
-        "project-b", "refs/heads/dev", "project-a", "refs/heads/dev");
+    allowMatchingSubmoduleSubscription(keyB, "refs/heads/master", keyA, "refs/heads/master");
+    allowMatchingSubmoduleSubscription(keyB, "refs/heads/dev", keyA, "refs/heads/dev");
 
-    createSubmoduleSubscription(repoA, "master", "project-b", "master");
-    createSubmoduleSubscription(repoA, "dev", "project-b", "dev");
+    createSubmoduleSubscription(repoA, "master", keyB, "master");
+    createSubmoduleSubscription(repoA, "dev", keyB, "dev");
 
     // create a change for master branch in repo b
     ObjectId bHead =
@@ -788,26 +792,23 @@
     approve(getChangeId(repoB, bDevHead).get());
     gApi.changes().id(getChangeId(repoB, bHead).get()).current().submit();
 
-    expectToHaveSubmoduleState(repoA, "master", "project-b", repoB, "master");
-    expectToHaveSubmoduleState(repoA, "dev", "project-b", repoB, "dev");
+    expectToHaveSubmoduleState(repoA, "master", keyB, repoB, "master");
+    expectToHaveSubmoduleState(repoA, "dev", keyB, repoB, "dev");
   }
 
   @Test
   public void retrySubmitAfterTornTopicOnLockFailure() throws Exception {
-    assume().that(notesMigration.disableChangeReviewDb()).isTrue();
+    Project.NameKey subKey1 = createProjectForPush(getSubmitType());
+    TestRepository<?> sub1 = cloneProject(subKey1);
+    Project.NameKey subKey2 = createProjectForPush(getSubmitType());
+    TestRepository<?> sub2 = cloneProject(subKey2);
 
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> sub1 = createProjectWithPush("sub1");
-    TestRepository<?> sub2 = createProjectWithPush("sub2");
-
-    allowMatchingSubmoduleSubscription(
-        "sub1", "refs/heads/master", "super-project", "refs/heads/master");
-    allowMatchingSubmoduleSubscription(
-        "sub2", "refs/heads/master", "super-project", "refs/heads/master");
+    allowMatchingSubmoduleSubscription(subKey1, "refs/heads/master", superKey, "refs/heads/master");
+    allowMatchingSubmoduleSubscription(subKey2, "refs/heads/master", superKey, "refs/heads/master");
 
     Config config = new Config();
-    prepareSubmoduleConfigEntry(config, "sub1", "master");
-    prepareSubmoduleConfigEntry(config, "sub2", "master");
+    prepareSubmoduleConfigEntry(config, subKey1, "master");
+    prepareSubmoduleConfigEntry(config, subKey2, "master");
     pushSubmoduleConfig(superRepo, "master", config);
 
     ObjectId superPreviousId = pushChangeTo(superRepo, "master");
@@ -837,20 +838,20 @@
 
     sub1.git().fetch().call();
     RevWalk rw1 = sub1.getRevWalk();
-    RevCommit master1 = rw1.parseCommit(getRemoteHead(name("sub1"), "master"));
+    RevCommit master1 = rw1.parseCommit(getRemoteHead(subKey1, "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(name("sub2"), "master"));
+    RevCommit master2 = rw2.parseCommit(getRemoteHead(subKey2, "master"));
     RevCommit change2Ps = parseCurrentRevision(rw2, changeId2);
     assertThat(rw2.isMergedInto(change2Ps, master2)).isTrue();
 
     assertThat(input.generateLockFailures).containsExactly(false);
 
-    expectToHaveSubmoduleState(superRepo, "master", "sub1", sub1, "master");
-    expectToHaveSubmoduleState(superRepo, "master", "sub2", sub2, "master");
+    expectToHaveSubmoduleState(superRepo, "master", subKey1, sub1, "master");
+    expectToHaveSubmoduleState(superRepo, "master", subKey2, sub2, "master");
 
     assertWithMessage("submodule subscription update should have made one commit")
         .that(superRepo.getRepository().resolve("origin/master^"))
@@ -859,24 +860,23 @@
 
   @Test
   public void skipUpdatingBrokenGitlinkPointer() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> sub1 = createProjectWithPush("sub1");
-    TestRepository<?> sub2 = createProjectWithPush("sub2");
+    Project.NameKey subKey1 = createProjectForPush(getSubmitType());
+    TestRepository<?> sub1 = cloneProject(subKey1);
+    Project.NameKey subKey2 = createProjectForPush(getSubmitType());
+    TestRepository<?> sub2 = cloneProject(subKey2);
 
-    allowMatchingSubmoduleSubscription(
-        "sub1", "refs/heads/master", "super-project", "refs/heads/master");
-    allowMatchingSubmoduleSubscription(
-        "sub2", "refs/heads/master", "super-project", "refs/heads/master");
+    allowMatchingSubmoduleSubscription(subKey1, "refs/heads/master", superKey, "refs/heads/master");
+    allowMatchingSubmoduleSubscription(subKey2, "refs/heads/master", superKey, "refs/heads/master");
 
     Config config = new Config();
-    prepareSubmoduleConfigEntry(config, "sub1", "master");
-    prepareSubmoduleConfigEntry(config, "sub2", "master");
+    prepareSubmoduleConfigEntry(config, subKey1, "master");
+    prepareSubmoduleConfigEntry(config, subKey2, "master");
     pushSubmoduleConfig(superRepo, "master", config);
 
     // Write an invalid SHA-1 directly to one of the gitlinks.
     ObjectId badId = ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
-    directUpdateSubmodule("super-project", "refs/heads/master", "sub1", badId);
-    expectToHaveSubmoduleState(superRepo, "master", "sub1", badId);
+    directUpdateSubmodule(superKey, "refs/heads/master", subKey1, badId);
+    expectToHaveSubmoduleState(superRepo, "master", subKey1, badId);
 
     String topic = "same-topic";
     ObjectId sub1Id = pushChangeTo(sub1, "refs/for/master", "some message", topic);
@@ -893,7 +893,11 @@
     assertThat(info(changeId2).status).isEqualTo(ChangeStatus.MERGED);
 
     // sub1 was skipped but sub2 succeeded.
-    expectToHaveSubmoduleState(superRepo, "master", "sub1", badId);
-    expectToHaveSubmoduleState(superRepo, "master", "sub2", sub2, "master");
+    expectToHaveSubmoduleState(superRepo, "master", subKey1, badId);
+    expectToHaveSubmoduleState(superRepo, "master", subKey2, sub2, "master");
+  }
+
+  private Project.NameKey nameKey(String s) {
+    return new Project.NameKey(name(s));
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java b/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
index a5ac3a9..eeeacab 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
@@ -41,7 +41,7 @@
   }
 
   @Override
-  public void configureIndex(Injector injector) throws Exception {
+  public void configureIndex(Injector injector) {
     createAllIndexes(injector);
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/pgm/StandaloneNoteDbMigrationIT.java b/javatests/com/google/gerrit/acceptance/pgm/StandaloneNoteDbMigrationIT.java
deleted file mode 100644
index 1bb23fb..0000000
--- a/javatests/com/google/gerrit/acceptance/pgm/StandaloneNoteDbMigrationIT.java
+++ /dev/null
@@ -1,363 +0,0 @@
-// Copyright (C) 2014 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.acceptance.pgm;
-
-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 com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.gerrit.acceptance.NoHttpd;
-import com.google.gerrit.acceptance.StandaloneSiteTest;
-import com.google.gerrit.extensions.api.GerritApi;
-import com.google.gerrit.extensions.common.ChangeInput;
-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.server.ReviewDb;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.LocalDiskRepositoryManager;
-import com.google.gerrit.server.index.GerritIndexStatus;
-import com.google.gerrit.server.index.change.ChangeIndexCollection;
-import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
-import com.google.gerrit.server.notedb.NoteDbChangeState;
-import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
-import com.google.gerrit.server.notedb.NoteDbChangeState.RefState;
-import com.google.gerrit.server.notedb.NotesMigrationState;
-import com.google.gerrit.server.schema.ReviewDbFactory;
-import com.google.gerrit.testing.NoteDbMode;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Key;
-import com.google.inject.TypeLiteral;
-import java.io.File;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.stream.Stream;
-import org.eclipse.jgit.internal.storage.file.FileRepository;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.lib.StoredConfig;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.util.FS;
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Tests for NoteDb migrations where the entry point is through a program, {@code
- * migrate-to-note-db} or {@code daemon}.
- *
- * <p><strong>Note:</strong> These tests are very slow due to the repeated daemon startup. Prefer
- * adding tests to {@link com.google.gerrit.acceptance.server.notedb.OnlineNoteDbMigrationIT} if
- * possible.
- */
-@NoHttpd
-public class StandaloneNoteDbMigrationIT extends StandaloneSiteTest {
-  private StoredConfig gerritConfig;
-  private StoredConfig noteDbConfig;
-
-  private Project.NameKey project;
-  private Change.Id changeId;
-
-  @Before
-  public void setUp() throws Exception {
-    assume().that(NoteDbMode.get()).isEqualTo(NoteDbMode.OFF);
-    gerritConfig = new FileBasedConfig(sitePaths.gerrit_config.toFile(), FS.detect());
-    // Unlike in the running server, for tests, we don't stack notedb.config on gerrit.config.
-    noteDbConfig = new FileBasedConfig(sitePaths.notedb_config.toFile(), FS.detect());
-
-    // Set gc.pruneExpire=now so GC prunes all unreachable objects from All-Users, which allows us
-    // to reliably test that it behaves as expected.
-    Path cfgPath = sitePaths.site_path.resolve("git").resolve("All-Users.git").resolve("config");
-    assertWithMessage("Expected All-Users config at %s", cfgPath)
-        .that(Files.isRegularFile(cfgPath))
-        .isTrue();
-    FileBasedConfig cfg = new FileBasedConfig(cfgPath.toFile(), FS.detect());
-    cfg.setString("gc", null, "pruneExpire", "now");
-    cfg.save();
-  }
-
-  @Test
-  public void rebuildOneChangeTrialMode() throws Exception {
-    assertNoAutoMigrateConfig(gerritConfig);
-    assertNoAutoMigrateConfig(noteDbConfig);
-    assertNotesMigrationState(NotesMigrationState.REVIEW_DB);
-    setUpOneChange();
-
-    migrate("--trial");
-    assertNotesMigrationState(NotesMigrationState.READ_WRITE_NO_SEQUENCE);
-
-    try (ServerContext ctx = startServer()) {
-      GitRepositoryManager repoManager = ctx.getInjector().getInstance(GitRepositoryManager.class);
-      ObjectId metaId;
-      try (Repository repo = repoManager.openRepository(project)) {
-        Ref ref = repo.exactRef(RefNames.changeMetaRef(changeId));
-        assertThat(ref).isNotNull();
-        metaId = ref.getObjectId();
-      }
-
-      try (ReviewDb db = openUnderlyingReviewDb(ctx)) {
-        Change c = db.changes().get(changeId);
-        assertThat(c).isNotNull();
-        NoteDbChangeState state = NoteDbChangeState.parse(c);
-        assertThat(state).isNotNull();
-        assertThat(state.getPrimaryStorage()).isEqualTo(PrimaryStorage.REVIEW_DB);
-        assertThat(state.getRefState()).hasValue(RefState.create(metaId, ImmutableMap.of()));
-      }
-    }
-  }
-
-  @Test
-  public void migrateOneChange() throws Exception {
-    assertNoAutoMigrateConfig(gerritConfig);
-    assertNoAutoMigrateConfig(noteDbConfig);
-    assertNotesMigrationState(NotesMigrationState.REVIEW_DB);
-    setUpOneChange();
-
-    migrate();
-    assertNotesMigrationState(NotesMigrationState.NOTE_DB);
-
-    File allUsersDir;
-    try (ServerContext ctx = startServer()) {
-      GitRepositoryManager repoManager = ctx.getInjector().getInstance(GitRepositoryManager.class);
-      try (Repository repo = repoManager.openRepository(project)) {
-        assertThat(repo.exactRef(RefNames.changeMetaRef(changeId))).isNotNull();
-      }
-      assertThat(repoManager).isInstanceOf(LocalDiskRepositoryManager.class);
-      try (Repository repo =
-          repoManager.openRepository(ctx.getInjector().getInstance(AllUsersName.class))) {
-        allUsersDir = repo.getDirectory();
-      }
-
-      try (ReviewDb db = openUnderlyingReviewDb(ctx)) {
-        Change c = db.changes().get(changeId);
-        assertThat(c).isNotNull();
-        NoteDbChangeState state = NoteDbChangeState.parse(c);
-        assertThat(state).isNotNull();
-        assertThat(state.getPrimaryStorage()).isEqualTo(PrimaryStorage.NOTE_DB);
-        assertThat(state.getRefState()).isEmpty();
-
-        ChangeInput in = new ChangeInput(project.get(), "master", "NoteDb-only change");
-        in.newBranch = true;
-        GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
-        Change.Id id2 = new Change.Id(gApi.changes().create(in).info()._number);
-        assertThat(db.changes().get(id2)).isNull();
-      }
-    }
-    assertNoAutoMigrateConfig(gerritConfig);
-    assertAutoMigrateConfig(noteDbConfig, false);
-
-    try (FileRepository repo = new FileRepository(allUsersDir)) {
-      try (Stream<Path> paths = Files.walk(repo.getObjectsDirectory().toPath())) {
-        assertThat(paths.filter(p -> !p.toString().contains("pack") && Files.isRegularFile(p)))
-            .named("loose object files in All-Users")
-            .isEmpty();
-      }
-      assertThat(repo.getObjectDatabase().getPacks()).named("packfiles in All-Users").hasSize(1);
-    }
-  }
-
-  @Test
-  public void migrationWithReindex() throws Exception {
-    assertNotesMigrationState(NotesMigrationState.REVIEW_DB);
-    setUpOneChange();
-
-    int version = ChangeSchemaDefinitions.INSTANCE.getLatest().getVersion();
-    GerritIndexStatus status = new GerritIndexStatus(sitePaths);
-    assertThat(status.getReady(ChangeSchemaDefinitions.NAME, version)).isTrue();
-    status.setReady(ChangeSchemaDefinitions.NAME, version, false);
-    status.save();
-    assertServerStartupFails();
-
-    migrate();
-    assertNotesMigrationState(NotesMigrationState.NOTE_DB);
-
-    status = new GerritIndexStatus(sitePaths);
-    assertThat(status.getReady(ChangeSchemaDefinitions.NAME, version)).isTrue();
-  }
-
-  @Test
-  public void onlineMigrationViaDaemon() throws Exception {
-    assertNoAutoMigrateConfig(gerritConfig);
-    assertNoAutoMigrateConfig(noteDbConfig);
-
-    testOnlineMigration(u -> startServer(u.module(), "--migrate-to-note-db", "true"));
-
-    assertNoAutoMigrateConfig(gerritConfig);
-    assertAutoMigrateConfig(noteDbConfig, false);
-  }
-
-  @Test
-  public void onlineMigrationViaConfig() throws Exception {
-    assertNoAutoMigrateConfig(gerritConfig);
-    assertNoAutoMigrateConfig(noteDbConfig);
-
-    testOnlineMigration(
-        u -> {
-          gerritConfig.setBoolean("noteDb", "changes", "autoMigrate", true);
-          gerritConfig.save();
-          return startServer(u.module());
-        });
-
-    // Auto-migration is turned off in notedb.config, which takes precedence, but is still on in
-    // gerrit.config. This means Puppet can continue overwriting gerrit.config without turning
-    // auto-migration back on.
-    assertAutoMigrateConfig(gerritConfig, true);
-    assertAutoMigrateConfig(noteDbConfig, false);
-  }
-
-  @Test
-  public void onlineMigrationTrialModeViaFlag() throws Exception {
-    assertNoAutoMigrateConfig(gerritConfig);
-    assertNoTrialConfig(gerritConfig);
-
-    assertNoAutoMigrateConfig(noteDbConfig);
-    assertNoTrialConfig(noteDbConfig);
-
-    testOnlineMigration(
-        u -> startServer(u.module(), "--migrate-to-note-db", "--trial"),
-        NotesMigrationState.READ_WRITE_NO_SEQUENCE);
-
-    assertNoAutoMigrateConfig(gerritConfig);
-    assertNoTrialConfig(gerritConfig);
-
-    assertAutoMigrateConfig(noteDbConfig, true);
-    assertTrialConfig(noteDbConfig, true);
-  }
-
-  @Test
-  public void onlineMigrationTrialModeViaConfig() throws Exception {
-    assertNoAutoMigrateConfig(gerritConfig);
-    assertNoTrialConfig(gerritConfig);
-
-    assertNoAutoMigrateConfig(noteDbConfig);
-    assertNoTrialConfig(noteDbConfig);
-
-    testOnlineMigration(
-        u -> {
-          gerritConfig.setBoolean("noteDb", "changes", "autoMigrate", true);
-          gerritConfig.setBoolean("noteDb", "changes", "trial", true);
-          gerritConfig.save();
-          return startServer(u.module());
-        },
-        NotesMigrationState.READ_WRITE_NO_SEQUENCE);
-
-    assertAutoMigrateConfig(gerritConfig, true);
-    assertTrialConfig(gerritConfig, true);
-
-    assertAutoMigrateConfig(noteDbConfig, true);
-    assertTrialConfig(noteDbConfig, true);
-  }
-
-  @FunctionalInterface
-  private interface StartServerWithMigration {
-    ServerContext start(IndexUpgradeController u) throws Exception;
-  }
-
-  private void testOnlineMigration(StartServerWithMigration start) throws Exception {
-    testOnlineMigration(start, NotesMigrationState.NOTE_DB);
-  }
-
-  private void testOnlineMigration(
-      StartServerWithMigration start, NotesMigrationState expectedEndState) throws Exception {
-    assertNotesMigrationState(NotesMigrationState.REVIEW_DB);
-    int prevVersion = ChangeSchemaDefinitions.INSTANCE.getPrevious().getVersion();
-    int currVersion = ChangeSchemaDefinitions.INSTANCE.getLatest().getVersion();
-
-    // Before storing any changes, switch back to the previous version.
-    GerritIndexStatus status = new GerritIndexStatus(sitePaths);
-    status.setReady(ChangeSchemaDefinitions.NAME, currVersion, false);
-    status.setReady(ChangeSchemaDefinitions.NAME, prevVersion, true);
-    status.save();
-
-    setOnlineUpgradeConfig(false);
-    setUpOneChange();
-    setOnlineUpgradeConfig(true);
-
-    IndexUpgradeController u = new IndexUpgradeController(1);
-    try (ServerContext ctx = start.start(u)) {
-      ChangeIndexCollection indexes = ctx.getInjector().getInstance(ChangeIndexCollection.class);
-      assertThat(indexes.getSearchIndex().getSchema().getVersion()).isEqualTo(prevVersion);
-
-      // Index schema upgrades happen after NoteDb migration, so waiting for those to complete
-      // should be sufficient.
-      u.runUpgrades();
-
-      assertThat(indexes.getSearchIndex().getSchema().getVersion()).isEqualTo(currVersion);
-      assertNotesMigrationState(expectedEndState);
-    }
-  }
-
-  private void setUpOneChange() throws Exception {
-    project = new Project.NameKey("project");
-    try (ServerContext ctx = startServer()) {
-      GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
-      gApi.projects().create("project");
-
-      ChangeInput in = new ChangeInput(project.get(), "master", "Test change");
-      in.newBranch = true;
-      changeId = new Change.Id(gApi.changes().create(in).info()._number);
-    }
-  }
-
-  private void migrate(String... additionalArgs) throws Exception {
-    runGerrit(
-        ImmutableList.of(
-            "migrate-to-note-db", "-d", sitePaths.site_path.toString(), "--show-stack-trace"),
-        ImmutableList.copyOf(additionalArgs));
-  }
-
-  private void assertNotesMigrationState(NotesMigrationState expected) throws Exception {
-    noteDbConfig.load();
-    assertThat(NotesMigrationState.forConfig(noteDbConfig)).hasValue(expected);
-  }
-
-  private ReviewDb openUnderlyingReviewDb(ServerContext ctx) throws Exception {
-    return ctx.getInjector()
-        .getInstance(Key.get(new TypeLiteral<SchemaFactory<ReviewDb>>() {}, ReviewDbFactory.class))
-        .open();
-  }
-
-  private static void assertNoAutoMigrateConfig(StoredConfig cfg) throws Exception {
-    cfg.load();
-    assertThat(cfg.getString("noteDb", "changes", "autoMigrate")).isNull();
-  }
-
-  private static void assertAutoMigrateConfig(StoredConfig cfg, boolean expected) throws Exception {
-    cfg.load();
-    assertThat(cfg.getString("noteDb", "changes", "autoMigrate")).isNotNull();
-    assertThat(cfg.getBoolean("noteDb", "changes", "autoMigrate", false)).isEqualTo(expected);
-  }
-
-  private static void assertNoTrialConfig(StoredConfig cfg) throws Exception {
-    cfg.load();
-    assertThat(cfg.getString("noteDb", "changes", "trial")).isNull();
-  }
-
-  private static void assertTrialConfig(StoredConfig cfg, boolean expected) throws Exception {
-    cfg.load();
-    assertThat(cfg.getString("noteDb", "changes", "trial")).isNotNull();
-    assertThat(cfg.getBoolean("noteDb", "changes", "trial", false)).isEqualTo(expected);
-  }
-
-  private void setOnlineUpgradeConfig(boolean enable) throws Exception {
-    gerritConfig.load();
-    gerritConfig.setBoolean("index", null, "onlineUpgrade", enable);
-    gerritConfig.save();
-  }
-}
diff --git a/javatests/com/google/gerrit/acceptance/rest/AbstractRestApiBindingsTest.java b/javatests/com/google/gerrit/acceptance/rest/AbstractRestApiBindingsTest.java
deleted file mode 100644
index 2bb3dca..0000000
--- a/javatests/com/google/gerrit/acceptance/rest/AbstractRestApiBindingsTest.java
+++ /dev/null
@@ -1,165 +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.acceptance.rest;
-
-import static com.google.common.base.Preconditions.checkState;
-import static com.google.common.truth.Truth.assertWithMessage;
-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;
-import static org.apache.http.HttpStatus.SC_NOT_FOUND;
-
-import com.google.auto.value.AutoValue;
-import com.google.common.collect.ImmutableList;
-import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.RestResponse;
-import java.util.List;
-import java.util.Optional;
-import org.apache.commons.lang.StringUtils;
-import org.junit.Ignore;
-
-/**
- * Base class for testing the REST API bindings.
- *
- * <p>This test sends a request to each REST endpoint and verifies that an implementation is found
- * (no '404 Not Found' response) and that the request doesn't fail (no '500 Internal Server Error'
- * response). It doesn't verify that the REST endpoint works correctly. This is okay since the
- * purpose of the test is only to verify that the REST endpoint implementations are correctly bound.
- */
-@Ignore
-public abstract class AbstractRestApiBindingsTest extends AbstractDaemonTest {
-  protected void execute(List<RestCall> restCalls, String... args) throws Exception {
-    execute(restCalls, () -> {}, args);
-  }
-
-  protected void execute(List<RestCall> restCalls, BeforeRestCall beforeRestCall, String... args)
-      throws Exception {
-    for (RestCall restCall : restCalls) {
-      beforeRestCall.run();
-      execute(restCall, args);
-    }
-  }
-
-  protected void execute(RestCall restCall, String... args) throws Exception {
-    String method = restCall.httpMethod().name();
-    String uri = restCall.uri(args);
-
-    RestResponse response;
-    switch (restCall.httpMethod()) {
-      case GET:
-        response = adminRestSession.get(uri);
-        break;
-      case PUT:
-        response = adminRestSession.put(uri);
-        break;
-      case POST:
-        response = adminRestSession.post(uri);
-        break;
-      case DELETE:
-        response = adminRestSession.delete(uri);
-        break;
-      default:
-        fail("unsupported method: %s", restCall.httpMethod().name());
-        throw new IllegalStateException();
-    }
-
-    int status = response.getStatusCode();
-    String body = response.hasContent() ? response.getEntityContent() : "";
-
-    String msg = String.format("%s %s returned %d: %s", method, uri, status, body);
-    if (restCall.expectedResponseCode().isPresent()) {
-      assertWithMessage(msg).that(status).isEqualTo(restCall.expectedResponseCode().get());
-      if (restCall.expectedMessage().isPresent()) {
-        assertWithMessage(msg).that(body).contains(restCall.expectedMessage().get());
-      }
-    } else {
-      assertWithMessage(msg)
-          .that(status)
-          .isNotIn(ImmutableList.of(SC_FORBIDDEN, SC_NOT_FOUND, SC_METHOD_NOT_ALLOWED));
-      assertWithMessage(msg).that(status).isLessThan(SC_INTERNAL_SERVER_ERROR);
-    }
-  }
-
-  enum Method {
-    GET,
-    PUT,
-    POST,
-    DELETE
-  }
-
-  @AutoValue
-  abstract static class RestCall {
-    static RestCall get(String uriFormat) {
-      return builder(Method.GET, uriFormat).build();
-    }
-
-    static RestCall put(String uriFormat) {
-      return builder(Method.PUT, uriFormat).build();
-    }
-
-    static RestCall post(String uriFormat) {
-      return builder(Method.POST, uriFormat).build();
-    }
-
-    static RestCall delete(String uriFormat) {
-      return builder(Method.DELETE, uriFormat).build();
-    }
-
-    static Builder builder(Method httpMethod, String uriFormat) {
-      return new AutoValue_AbstractRestApiBindingsTest_RestCall.Builder()
-          .httpMethod(httpMethod)
-          .uriFormat(uriFormat);
-    }
-
-    abstract Method httpMethod();
-
-    abstract String uriFormat();
-
-    abstract Optional<Integer> expectedResponseCode();
-
-    abstract Optional<String> expectedMessage();
-
-    String uri(String... args) {
-      String uriFormat = uriFormat();
-      int expectedArgNum = StringUtils.countMatches(uriFormat, "%s");
-      checkState(
-          args.length == expectedArgNum,
-          "uriFormat %s needs %s arguments, got only %s: %s",
-          uriFormat,
-          expectedArgNum,
-          args.length,
-          args);
-      return String.format(uriFormat, (Object[]) args);
-    }
-
-    @AutoValue.Builder
-    abstract static class Builder {
-      abstract Builder httpMethod(Method httpMethod);
-
-      abstract Builder uriFormat(String uriFormat);
-
-      abstract Builder expectedResponseCode(int expectedResponseCode);
-
-      abstract Builder expectedMessage(String expectedMessage);
-
-      abstract RestCall build();
-    }
-  }
-
-  @FunctionalInterface
-  public interface BeforeRestCall {
-    void run() throws Exception;
-  }
-}
diff --git a/javatests/com/google/gerrit/acceptance/rest/AccountsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/AccountsRestApiBindingsIT.java
deleted file mode 100644
index b0adba7..0000000
--- a/javatests/com/google/gerrit/acceptance/rest/AccountsRestApiBindingsIT.java
+++ /dev/null
@@ -1,191 +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.acceptance.rest;
-
-import static com.google.gerrit.acceptance.rest.AbstractRestApiBindingsTest.Method.PUT;
-import static com.google.gerrit.gpg.testing.TestKeys.validKeyWithoutExpiration;
-import static org.apache.http.HttpStatus.SC_METHOD_NOT_ALLOWED;
-
-import com.google.common.collect.ImmutableList;
-import com.google.gerrit.acceptance.GerritConfig;
-import com.google.gerrit.acceptance.UseSsh;
-import com.google.gerrit.extensions.common.ChangeInput;
-import com.google.gerrit.gpg.testing.TestKey;
-import com.google.gerrit.server.ServerInitiated;
-import com.google.gerrit.server.account.AccountsUpdate;
-import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import org.junit.Test;
-
-/**
- * Tests for checking the bindings of the accounts REST API.
- *
- * <p>These tests only verify that the account REST endpoints are correctly bound, they do no test
- * the functionality of the account REST endpoints (for details see JavaDoc on {@link
- * AbstractRestApiBindingsTest}).
- */
-public class AccountsRestApiBindingsIT extends AbstractRestApiBindingsTest {
-  @Inject private @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider;
-
-  /**
-   * Account REST endpoints to be tested, each URL contains a placeholder for the account
-   * identifier.
-   */
-  private static final ImmutableList<RestCall> ACCOUNT_ENDPOINTS =
-      ImmutableList.of(
-          RestCall.get("/accounts/%s"),
-          RestCall.put("/accounts/%s"),
-          RestCall.get("/accounts/%s/detail"),
-          RestCall.get("/accounts/%s/name"),
-          RestCall.put("/accounts/%s/name"),
-          RestCall.delete("/accounts/%s/name"),
-          RestCall.get("/accounts/%s/username"),
-          RestCall.builder(PUT, "/accounts/%s/username")
-              // Changing the username is not allowed.
-              .expectedResponseCode(SC_METHOD_NOT_ALLOWED)
-              .expectedMessage("Username cannot be changed.")
-              .build(),
-          RestCall.get("/accounts/%s/active"),
-          RestCall.put("/accounts/%s/active"),
-          RestCall.delete("/accounts/%s/active"),
-          RestCall.put("/accounts/%s/password.http"),
-          RestCall.delete("/accounts/%s/password.http"),
-          RestCall.get("/accounts/%s/status"),
-          RestCall.put("/accounts/%s/status"),
-          RestCall.get("/accounts/%s/avatar"),
-          RestCall.get("/accounts/%s/avatar.change.url"),
-          RestCall.get("/accounts/%s/emails/"),
-          RestCall.put("/accounts/%s/emails/new-email@foo.com"),
-          RestCall.get("/accounts/%s/sshkeys/"),
-          RestCall.post("/accounts/%s/sshkeys/"),
-          RestCall.get("/accounts/%s/watched.projects"),
-          RestCall.post("/accounts/%s/watched.projects"),
-          RestCall.post("/accounts/%s/watched.projects:delete"),
-          RestCall.get("/accounts/%s/groups"),
-          RestCall.get("/accounts/%s/preferences"),
-          RestCall.put("/accounts/%s/preferences"),
-          RestCall.get("/accounts/%s/preferences.diff"),
-          RestCall.put("/accounts/%s/preferences.diff"),
-          RestCall.get("/accounts/%s/preferences.edit"),
-          RestCall.put("/accounts/%s/preferences.edit"),
-          RestCall.get("/accounts/%s/starred.changes"),
-          RestCall.get("/accounts/%s/stars.changes"),
-          RestCall.post("/accounts/%s/index"),
-          RestCall.get("/accounts/%s/agreements"),
-          RestCall.put("/accounts/%s/agreements"),
-          RestCall.get("/accounts/%s/external.ids"),
-          RestCall.post("/accounts/%s/external.ids:delete"),
-          RestCall.post("/accounts/%s/drafts:delete"),
-          RestCall.get("/accounts/%s/oauthtoken"),
-          RestCall.get("/accounts/%s/capabilities"),
-          RestCall.get("/accounts/%s/capabilities/viewPlugins"),
-          RestCall.get("/accounts/%s/gpgkeys"),
-          RestCall.post("/accounts/%s/gpgkeys"));
-
-  /**
-   * Email REST endpoints to be tested, each URL contains a placeholders for the account and email
-   * identifier.
-   */
-  private static final ImmutableList<RestCall> EMAIL_ENDPOINTS =
-      ImmutableList.of(
-          RestCall.get("/accounts/%s/emails/%s"),
-          RestCall.put("/accounts/%s/emails/%s"),
-          RestCall.put("/accounts/%s/emails/%s/preferred"),
-
-          // email deletion must be tested last
-          RestCall.delete("/accounts/%s/emails/%s"));
-
-  /**
-   * GPG key REST endpoints to be tested, each URL contains a placeholders for the account
-   * identifier and the GPG key identifier.
-   */
-  private static final ImmutableList<RestCall> GPG_KEY_ENDPOINTS =
-      ImmutableList.of(
-          RestCall.get("/accounts/%s/gpgkeys/%s"),
-
-          // GPG key deletion must be tested last
-          RestCall.delete("/accounts/%s/gpgkeys/%s"));
-
-  /**
-   * SSH key REST endpoints to be tested, each URL contains a placeholders for the account and SSH
-   * key identifier.
-   */
-  private static final ImmutableList<RestCall> SSH_KEY_ENDPOINTS =
-      ImmutableList.of(
-          RestCall.get("/accounts/%s/sshkeys/%s"),
-
-          // SSH key deletion must be tested last
-          RestCall.delete("/accounts/%s/sshkeys/%s"));
-
-  /**
-   * Star REST endpoints to be tested, each URL contains a placeholders for the account and change
-   * identifier.
-   */
-  private static final ImmutableList<RestCall> STAR_ENDPOINTS =
-      ImmutableList.of(
-          RestCall.put("/accounts/%s/starred.changes/%s"),
-          RestCall.delete("/accounts/%s/starred.changes/%s"),
-          RestCall.get("/accounts/%s/stars.changes/%s"),
-          RestCall.post("/accounts/%s/stars.changes/%s"));
-
-  @Test
-  public void accountEndpoints() throws Exception {
-    execute(ACCOUNT_ENDPOINTS, "self");
-  }
-
-  @Test
-  public void emailEndpoints() throws Exception {
-    execute(EMAIL_ENDPOINTS, "self", admin.email);
-  }
-
-  @Test
-  @GerritConfig(name = "receive.enableSignedPush", value = "true")
-  public void gpgKeyEndpoints() throws Exception {
-    TestKey key = validKeyWithoutExpiration();
-    String id = key.getKeyIdString();
-
-    String email = "test1@example.com"; // email that is hard-coded in the test GPG key
-    accountsUpdateProvider
-        .get()
-        .update(
-            "Add Email",
-            admin.getId(),
-            u ->
-                u.addExternalId(
-                    ExternalId.createWithEmail(name("test"), email, admin.getId(), email)));
-
-    setApiUser(admin);
-    gApi.accounts()
-        .self()
-        .putGpgKeys(ImmutableList.of(key.getPublicKeyArmored()), ImmutableList.of());
-
-    execute(GPG_KEY_ENDPOINTS, "self", id);
-  }
-
-  @Test
-  @UseSsh
-  public void sshKeyEndpoints() throws Exception {
-    String sshKeySeq = Integer.toString(gApi.accounts().self().listSshKeys().size());
-    execute(SSH_KEY_ENDPOINTS, "self", sshKeySeq);
-  }
-
-  @Test
-  public void starEndpoints() throws Exception {
-    ChangeInput ci = new ChangeInput(project.get(), "master", "Test change");
-    String changeId = gApi.changes().create(ci).get().id;
-    execute(STAR_ENDPOINTS, "self", changeId);
-  }
-}
diff --git a/javatests/com/google/gerrit/acceptance/rest/BUILD b/javatests/com/google/gerrit/acceptance/rest/BUILD
index e746eb7..84887da 100644
--- a/javatests/com/google/gerrit/acceptance/rest/BUILD
+++ b/javatests/com/google/gerrit/acceptance/rest/BUILD
@@ -1,25 +1,10 @@
-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_bindings",
+    group = "rest_bindings_collection",
     labels = ["rest"],
     deps = [
-        ":util",
         "//java/com/google/gerrit/server/logging",
     ],
 )
-
-java_library(
-    name = "util",
-    testonly = True,
-    srcs = [
-        "AbstractRestApiBindingsTest.java",
-    ],
-    visibility = ["//visibility:public"],
-    deps = [
-        "//java/com/google/gerrit/acceptance:lib",
-        "//lib/commons:lang",
-    ],
-)
diff --git a/javatests/com/google/gerrit/acceptance/rest/ChangesRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/ChangesRestApiBindingsIT.java
deleted file mode 100644
index c00c2aa..0000000
--- a/javatests/com/google/gerrit/acceptance/rest/ChangesRestApiBindingsIT.java
+++ /dev/null
@@ -1,509 +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.acceptance.rest;
-
-import static com.google.common.truth.TruthJUnit.assume;
-import static com.google.gerrit.acceptance.rest.AbstractRestApiBindingsTest.Method.GET;
-import static com.google.gerrit.extensions.common.testing.RobotCommentInfoSubject.assertThatList;
-import static java.util.stream.Collectors.toList;
-import static org.apache.http.HttpStatus.SC_NOT_FOUND;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
-import com.google.gerrit.extensions.api.changes.AddReviewerInput;
-import com.google.gerrit.extensions.api.changes.DraftInput;
-import com.google.gerrit.extensions.api.changes.ReviewInput;
-import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
-import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
-import com.google.gerrit.extensions.client.Comment;
-import com.google.gerrit.extensions.client.Side;
-import com.google.gerrit.extensions.common.CommentInfo;
-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;
-import org.junit.Test;
-
-/**
- * Tests for checking the bindings of the changes REST API.
- *
- * <p>These tests only verify that the change REST endpoints are correctly bound, they do no test
- * the functionality of the change REST endpoints (for details see JavaDoc on {@link
- * AbstractRestApiBindingsTest}).
- */
-public class ChangesRestApiBindingsIT extends AbstractRestApiBindingsTest {
-  /**
-   * Change REST endpoints to be tested, each URL contains a placeholder for the change identifier.
-   */
-  private static final ImmutableList<RestCall> CHANGE_ENDPOINTS =
-      ImmutableList.of(
-          RestCall.get("/changes/%s"),
-          RestCall.get("/changes/%s/detail"),
-          RestCall.get("/changes/%s/topic"),
-          RestCall.put("/changes/%s/topic"),
-          RestCall.delete("/changes/%s/topic"),
-          RestCall.get("/changes/%s/in"),
-          RestCall.get("/changes/%s/hashtags"),
-          RestCall.get("/changes/%s/comments"),
-          RestCall.get("/changes/%s/robotcomments"),
-          RestCall.get("/changes/%s/drafts"),
-          RestCall.get("/changes/%s/assignee"),
-          RestCall.get("/changes/%s/past_assignees"),
-          RestCall.put("/changes/%s/assignee"),
-          RestCall.delete("/changes/%s/assignee"),
-          RestCall.post("/changes/%s/private"),
-          RestCall.post("/changes/%s/private.delete"),
-          RestCall.delete("/changes/%s/private"),
-          RestCall.post("/changes/%s/wip"),
-          RestCall.post("/changes/%s/ready"),
-          RestCall.put("/changes/%s/ignore"),
-          RestCall.put("/changes/%s/unignore"),
-          RestCall.put("/changes/%s/reviewed"),
-          RestCall.put("/changes/%s/unreviewed"),
-          RestCall.get("/changes/%s/messages"),
-          RestCall.put("/changes/%s/message"),
-          RestCall.post("/changes/%s/merge"),
-          RestCall.post("/changes/%s/abandon"),
-          RestCall.post("/changes/%s/move"),
-          RestCall.post("/changes/%s/rebase"),
-          RestCall.post("/changes/%s/restore"),
-          RestCall.post("/changes/%s/revert"),
-          RestCall.get("/changes/%s/pure_revert"),
-          RestCall.post("/changes/%s/submit"),
-          RestCall.get("/changes/%s/submitted_together"),
-          RestCall.post("/changes/%s/index"),
-          RestCall.get("/changes/%s/check"),
-          RestCall.post("/changes/%s/check"),
-          RestCall.get("/changes/%s/reviewers"),
-          RestCall.post("/changes/%s/reviewers"),
-          RestCall.get("/changes/%s/suggest_reviewers"),
-          RestCall.builder(GET, "/changes/%s/revisions")
-              // GET /changes/<change-id>/revisions is not implemented
-              .expectedResponseCode(SC_NOT_FOUND)
-              .build(),
-          RestCall.get("/changes/%s/edit"),
-          RestCall.post("/changes/%s/edit"),
-          RestCall.post("/changes/%s/edit:rebase"),
-          RestCall.get("/changes/%s/edit:message"),
-          RestCall.put("/changes/%s/edit:message"),
-
-          // Publish edit and create a new edit
-          RestCall.post("/changes/%s/edit:publish"),
-          RestCall.put("/changes/%s/edit/a.txt"),
-
-          // Deletion of change edit and change must be tested last
-          RestCall.delete("/changes/%s/edit"),
-          RestCall.delete("/changes/%s"));
-
-  /**
-   * Change REST endpoints to be tested with NoteDb, each URL contains a placeholder for the change
-   * identifier.
-   */
-  private static final ImmutableList<RestCall> CHANGE_ENDPOINTS_NOTEDB =
-      ImmutableList.of(
-          RestCall.post("/changes/%s/hashtags"), RestCall.post("/changes/%s/rebuild.notedb"));
-
-  /**
-   * Reviewer REST endpoints to be tested, each URL contains placeholders for the change identifier
-   * and the reviewer identifier.
-   */
-  private static final ImmutableList<RestCall> REVIEWER_ENDPOINTS =
-      ImmutableList.of(
-          RestCall.get("/changes/%s/reviewers/%s"),
-          RestCall.get("/changes/%s/reviewers/%s/votes"),
-          RestCall.post("/changes/%s/reviewers/%s/delete"),
-          RestCall.delete("/changes/%s/reviewers/%s"));
-
-  /**
-   * Vote REST endpoints to be tested, each URL contains placeholders for the change identifier, the
-   * reviewer identifier and the label identifier.
-   */
-  private static final ImmutableList<RestCall> VOTE_ENDPOINTS =
-      ImmutableList.of(
-          RestCall.post("/changes/%s/reviewers/%s/votes/%s/delete"),
-          RestCall.delete("/changes/%s/reviewers/%s/votes/%s"));
-
-  /**
-   * Revision REST endpoints to be tested, each URL contains placeholders for the change identifier
-   * and the revision identifier.
-   */
-  private static final ImmutableList<RestCall> REVISION_ENDPOINTS =
-      ImmutableList.of(
-          RestCall.get("/changes/%s/revisions/%s/actions"),
-          RestCall.post("/changes/%s/revisions/%s/cherrypick"),
-          RestCall.get("/changes/%s/revisions/%s/commit"),
-          RestCall.get("/changes/%s/revisions/%s/mergeable"),
-          RestCall.get("/changes/%s/revisions/%s/related"),
-          RestCall.get("/changes/%s/revisions/%s/review"),
-          RestCall.post("/changes/%s/revisions/%s/review"),
-          RestCall.get("/changes/%s/revisions/%s/preview_submit"),
-          RestCall.post("/changes/%s/revisions/%s/submit"),
-          RestCall.get("/changes/%s/revisions/%s/submit_type"),
-          RestCall.post("/changes/%s/revisions/%s/test.submit_rule"),
-          RestCall.post("/changes/%s/revisions/%s/test.submit_type"),
-          RestCall.post("/changes/%s/revisions/%s/rebase"),
-          RestCall.get("/changes/%s/revisions/%s/description"),
-          RestCall.put("/changes/%s/revisions/%s/description"),
-          RestCall.get("/changes/%s/revisions/%s/patch"),
-          RestCall.get("/changes/%s/revisions/%s/archive"),
-          RestCall.get("/changes/%s/revisions/%s/mergelist"),
-          RestCall.get("/changes/%s/revisions/%s/reviewers"),
-          RestCall.get("/changes/%s/revisions/%s/drafts"),
-          RestCall.put("/changes/%s/revisions/%s/drafts"),
-          RestCall.get("/changes/%s/revisions/%s/comments"),
-          RestCall.get("/changes/%s/revisions/%s/robotcomments"),
-          RestCall.builder(GET, "/changes/%s/revisions/%s/fixes")
-              // GET /changes/<change>/revisions/<revision>/fixes is not implemented
-              .expectedResponseCode(SC_NOT_FOUND)
-              .build(),
-          RestCall.get("/changes/%s/revisions/%s/files"));
-
-  /**
-   * Revision reviewer REST endpoints to be tested, each URL contains placeholders for the change
-   * identifier, the revision identifier and the reviewer identifier.
-   */
-  private static final ImmutableList<RestCall> REVISION_REVIEWER_ENDPOINTS =
-      ImmutableList.of(
-          RestCall.get("/changes/%s/revisions/%s/reviewers/%s"),
-          RestCall.get("/changes/%s/revisions/%s/reviewers/%s/votes"),
-          RestCall.post("/changes/%s/revisions/%s/reviewers/%s/delete"),
-          RestCall.delete("/changes/%s/revisions/%s/reviewers/%s"));
-
-  /**
-   * Revision vote REST endpoints to be tested, each URL contains placeholders for the change
-   * identifier, the revision identifier, the reviewer identifier and the label identifier.
-   */
-  private static final ImmutableList<RestCall> REVISION_VOTE_ENDPOINTS =
-      ImmutableList.of(
-          RestCall.post("/changes/%s/revisions/%s/reviewers/%s/votes/%s/delete"),
-          RestCall.delete("/changes/%s/revisions/%s/reviewers/%s/votes/%s"));
-
-  /**
-   * Draft comment REST endpoints to be tested, each URL contains placeholders for the change
-   * identifier, the revision identifier and the draft comment identifier.
-   */
-  private static final ImmutableList<RestCall> DRAFT_COMMENT_ENDPOINTS =
-      ImmutableList.of(
-          RestCall.get("/changes/%s/revisions/%s/drafts/%s"),
-          RestCall.put("/changes/%s/revisions/%s/drafts/%s"),
-          RestCall.delete("/changes/%s/revisions/%s/drafts/%s"));
-
-  /**
-   * Comment REST endpoints to be tested, each URL contains placeholders for the change identifier,
-   * the revision identifier and the comment identifier.
-   */
-  private static final ImmutableList<RestCall> COMMENT_ENDPOINTS =
-      ImmutableList.of(
-          RestCall.get("/changes/%s/revisions/%s/comments/%s"),
-          RestCall.delete("/changes/%s/revisions/%s/comments/%s"),
-          RestCall.post("/changes/%s/revisions/%s/comments/%s/delete"));
-
-  /**
-   * Robot comment REST endpoints to be tested, each URL contains placeholders for the change
-   * identifier, the revision identifier and the robot comment identifier.
-   */
-  private static final ImmutableList<RestCall> ROBOT_COMMENT_ENDPOINTS =
-      ImmutableList.of(RestCall.get("/changes/%s/revisions/%s/robotcomments/%s"));
-
-  /**
-   * Fix REST endpoints to be tested, each URL contains placeholders for the change identifier, the
-   * revision identifier and the fix identifier.
-   */
-  private static final ImmutableList<RestCall> FIX_ENDPOINTS =
-      ImmutableList.of(RestCall.post("/changes/%s/revisions/%s/fixes/%s/apply"));
-
-  /**
-   * Revision file REST endpoints to be tested, each URL contains placeholders for the change
-   * identifier, the revision identifier and the file identifier.
-   */
-  private static final ImmutableList<RestCall> REVISION_FILE_ENDPOINTS =
-      ImmutableList.of(
-          RestCall.put("/changes/%s/revisions/%s/files/%s/reviewed"),
-          RestCall.delete("/changes/%s/revisions/%s/files/%s/reviewed"),
-          RestCall.get("/changes/%s/revisions/%s/files/%s/content"),
-          RestCall.get("/changes/%s/revisions/%s/files/%s/download"),
-          RestCall.get("/changes/%s/revisions/%s/files/%s/diff"),
-          RestCall.get("/changes/%s/revisions/%s/files/%s/blame"));
-
-  /**
-   * Change message REST endpoints to be tested, each URL contains placeholders for the change
-   * identifier and the change message identifier.
-   */
-  private static final ImmutableList<RestCall> CHANGE_MESSAGE_ENDPOINTS =
-      ImmutableList.of(RestCall.get("/changes/%s/messages/%s"));
-
-  /**
-   * Change edit REST endpoints that create an edit to be tested, each URL contains placeholders for
-   * the change identifier and the change edit identifier.
-   */
-  private static final ImmutableList<RestCall> CHANGE_EDIT_CREATE_ENDPOINTS =
-      ImmutableList.of(
-          // Create change edit by editing an existing file.
-          RestCall.put("/changes/%s/edit/%s"),
-
-          // Create change edit by deleting an existing file.
-          RestCall.delete("/changes/%s/edit/%s"));
-
-  /**
-   * Change edit REST endpoints to be tested, each URL contains placeholders for the change
-   * identifier and the change edit identifier.
-   */
-  private static final ImmutableList<RestCall> CHANGE_EDIT_ENDPOINTS =
-      ImmutableList.of(
-          // Calls on existing change edit.
-          RestCall.get("/changes/%s/edit/%s"),
-          RestCall.put("/changes/%s/edit/%s"),
-          RestCall.get("/changes/%s/edit/%s/meta"),
-
-          // Delete content of a file in an existing change edit.
-          RestCall.delete("/changes/%s/edit/%s"));
-
-  private static final String FILENAME = "test.txt";
-
-  @Test
-  public void changeEndpoints() throws Exception {
-    String changeId = createChange().getChangeId();
-    gApi.changes().id(changeId).edit().create();
-    execute(CHANGE_ENDPOINTS, changeId);
-  }
-
-  @Test
-  public void changeEndpointsNoteDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
-    String changeId = createChange().getChangeId();
-    execute(CHANGE_ENDPOINTS_NOTEDB, changeId);
-  }
-
-  @Test
-  public void reviewerEndpoints() throws Exception {
-    String changeId = createChange().getChangeId();
-
-    AddReviewerInput addReviewerInput = new AddReviewerInput();
-    addReviewerInput.reviewer = user.email;
-
-    execute(
-        REVIEWER_ENDPOINTS,
-        () -> gApi.changes().id(changeId).addReviewer(addReviewerInput),
-        changeId,
-        addReviewerInput.reviewer);
-  }
-
-  @Test
-  public void voteEndpoints() throws Exception {
-    String changeId = createChange().getChangeId();
-
-    execute(
-        VOTE_ENDPOINTS,
-        () -> gApi.changes().id(changeId).current().review(ReviewInput.approve()),
-        changeId,
-        admin.email,
-        "Code-Review");
-  }
-
-  @Test
-  public void revisionEndpoints() throws Exception {
-    String changeId = createChange().getChangeId();
-    execute(REVISION_ENDPOINTS, changeId, "current");
-  }
-
-  @Test
-  public void revisionReviewerEndpoints() throws Exception {
-    String changeId = createChange().getChangeId();
-
-    AddReviewerInput addReviewerInput = new AddReviewerInput();
-    addReviewerInput.reviewer = user.email;
-
-    execute(
-        REVISION_REVIEWER_ENDPOINTS,
-        () -> gApi.changes().id(changeId).addReviewer(addReviewerInput),
-        changeId,
-        "current",
-        addReviewerInput.reviewer);
-  }
-
-  @Test
-  public void revisionVoteEndpoints() throws Exception {
-    String changeId = createChange().getChangeId();
-
-    execute(
-        REVISION_VOTE_ENDPOINTS,
-        () -> gApi.changes().id(changeId).current().review(ReviewInput.approve()),
-        changeId,
-        "current",
-        admin.email,
-        "Code-Review");
-  }
-
-  @Test
-  public void draftCommentEndpoints() throws Exception {
-    String changeId = createChange().getChangeId();
-
-    for (RestCall restCall : DRAFT_COMMENT_ENDPOINTS) {
-      DraftInput draftInput = new DraftInput();
-      draftInput.path = Patch.COMMIT_MSG;
-      draftInput.side = Side.REVISION;
-      draftInput.line = 1;
-      draftInput.message = "draft comment";
-      CommentInfo draftInfo = gApi.changes().id(changeId).current().createDraft(draftInput).get();
-
-      execute(restCall, changeId, "current", draftInfo.id);
-    }
-  }
-
-  @Test
-  public void commentEndpoints() throws Exception {
-    String changeId = createChange().getChangeId();
-
-    for (RestCall restCall : COMMENT_ENDPOINTS) {
-      DraftInput draftInput = new DraftInput();
-      draftInput.path = Patch.COMMIT_MSG;
-      draftInput.side = Side.REVISION;
-      draftInput.line = 1;
-      draftInput.message = "draft comment";
-      CommentInfo commentInfo = gApi.changes().id(changeId).current().createDraft(draftInput).get();
-
-      ReviewInput reviewInput = new ReviewInput();
-      reviewInput.drafts = DraftHandling.PUBLISH;
-      gApi.changes().id(changeId).current().review(reviewInput);
-
-      execute(restCall, changeId, "current", commentInfo.id);
-    }
-  }
-
-  @Test
-  public void robotCommentEndpoints() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
-    String changeId = createChange().getChangeId();
-
-    RobotCommentInput robotCommentInput = new RobotCommentInput();
-    robotCommentInput.robotId = "happyRobot";
-    robotCommentInput.robotRunId = "1";
-    robotCommentInput.line = 1;
-    robotCommentInput.message = "nit: trailing whitespace";
-    robotCommentInput.path = Patch.COMMIT_MSG;
-
-    ReviewInput reviewInput = new ReviewInput();
-    reviewInput.robotComments =
-        Collections.singletonMap(robotCommentInput.path, ImmutableList.of(robotCommentInput));
-    reviewInput.message = "robot comment test";
-    gApi.changes().id(changeId).current().review(reviewInput);
-
-    List<RobotCommentInfo> robotCommentInfos =
-        gApi.changes().id(changeId).current().robotCommentsAsList();
-    RobotCommentInfo robotCommentInfo = Iterables.getOnlyElement(robotCommentInfos);
-
-    execute(ROBOT_COMMENT_ENDPOINTS, changeId, "current", robotCommentInfo.id);
-  }
-
-  @Test
-  public void fixEndpoints() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
-    String changeId = createChange("Subject", FILENAME, "content").getChangeId();
-
-    RobotCommentInput robotCommentInput = new RobotCommentInput();
-    robotCommentInput.robotId = "happyRobot";
-    robotCommentInput.robotRunId = "1";
-    robotCommentInput.line = 1;
-    robotCommentInput.message = "nit: trailing whitespace";
-    robotCommentInput.path = FILENAME;
-
-    FixReplacementInfo fixReplacementInfo = new FixReplacementInfo();
-    fixReplacementInfo.path = FILENAME;
-    fixReplacementInfo.replacement = "some replacement code";
-    fixReplacementInfo.range = createRange(1, 1, 1, 2);
-
-    FixSuggestionInfo fixSuggestionInfo = new FixSuggestionInfo();
-    fixSuggestionInfo.fixId = "An ID which must be overwritten.";
-    fixSuggestionInfo.description = "A description for a suggested fix.";
-    fixSuggestionInfo.replacements = ImmutableList.of(fixReplacementInfo);
-
-    robotCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo);
-
-    ReviewInput reviewInput = new ReviewInput();
-    reviewInput.robotComments =
-        Collections.singletonMap(robotCommentInput.path, ImmutableList.of(robotCommentInput));
-    reviewInput.message = "robot comment test";
-    gApi.changes().id(changeId).current().review(reviewInput);
-
-    List<RobotCommentInfo> robotCommentInfos =
-        gApi.changes().id(changeId).current().robotCommentsAsList();
-
-    List<String> fixIds = getFixIds(robotCommentInfos);
-    String fixId = Iterables.getOnlyElement(fixIds);
-
-    execute(FIX_ENDPOINTS, changeId, "current", fixId);
-  }
-
-  @Test
-  public void revisionFileEndpoints() throws Exception {
-    String changeId = createChange("Subject", FILENAME, "content").getChangeId();
-    execute(REVISION_FILE_ENDPOINTS, changeId, "current", FILENAME);
-  }
-
-  @Test
-  public void changeMessageEndpoints() throws Exception {
-    String changeId = createChange().getChangeId();
-
-    // A change message is created on change creation.
-    String changeMessageId = Iterables.getOnlyElement(gApi.changes().id(changeId).messages()).id;
-
-    execute(CHANGE_MESSAGE_ENDPOINTS, changeId, changeMessageId);
-  }
-
-  @Test
-  public void changeEditCreateEndpoints() throws Exception {
-    String changeId = createChange("Subject", FILENAME, "content").getChangeId();
-
-    // Each of the REST calls creates the change edit newly.
-    execute(
-        CHANGE_EDIT_CREATE_ENDPOINTS,
-        () -> adminRestSession.delete("/changes/" + changeId + "/edit"),
-        changeId,
-        FILENAME);
-  }
-
-  @Test
-  public void changeEditEndpoints() throws Exception {
-    String changeId = createChange("Subject", FILENAME, "content").getChangeId();
-    gApi.changes().id(changeId).edit().create();
-    execute(CHANGE_EDIT_ENDPOINTS, changeId, FILENAME);
-  }
-
-  private static Comment.Range createRange(
-      int startLine, int startCharacter, int endLine, int endCharacter) {
-    Comment.Range range = new Comment.Range();
-    range.startLine = startLine;
-    range.startCharacter = startCharacter;
-    range.endLine = endLine;
-    range.endCharacter = endCharacter;
-    return range;
-  }
-
-  private static List<String> getFixIds(List<RobotCommentInfo> robotComments) {
-    assertThatList(robotComments).isNotNull();
-    return robotComments.stream()
-        .map(robotCommentInfo -> robotCommentInfo.fixSuggestions)
-        .filter(Objects::nonNull)
-        .flatMap(List::stream)
-        .map(fixSuggestionInfo -> fixSuggestionInfo.fixId)
-        .collect(toList());
-  }
-}
diff --git a/javatests/com/google/gerrit/acceptance/rest/ConfigRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/ConfigRestApiBindingsIT.java
deleted file mode 100644
index 2e5ea00..0000000
--- a/javatests/com/google/gerrit/acceptance/rest/ConfigRestApiBindingsIT.java
+++ /dev/null
@@ -1,111 +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.acceptance.rest;
-
-import static com.google.common.truth.Truth8.assertThat;
-import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-
-import com.google.common.collect.ImmutableList;
-import com.google.gerrit.acceptance.RestResponse;
-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 java.util.List;
-import java.util.Optional;
-import org.junit.Test;
-
-/**
- * Tests for checking the bindings of the config REST API.
- *
- * <p>These tests only verify that the config REST endpoints are correctly bound, they do no test
- * the functionality of the config REST endpoints (for details see JavaDoc on {@link
- * AbstractRestApiBindingsTest}).
- */
-public class ConfigRestApiBindingsIT extends AbstractRestApiBindingsTest {
-  /**
-   * Config REST endpoints to be tested, the URLs contain no placeholders since the only supported
-   * config identifier ('server') can be hard-coded.
-   */
-  private static final ImmutableList<RestCall> CONFIG_ENDPOINTS =
-      ImmutableList.of(
-          RestCall.get("/config/server/version"),
-          RestCall.get("/config/server/info"),
-          RestCall.get("/config/server/preferences"),
-          RestCall.put("/config/server/preferences"),
-          RestCall.get("/config/server/preferences.diff"),
-          RestCall.put("/config/server/preferences.diff"),
-          RestCall.get("/config/server/preferences.edit"),
-          RestCall.put("/config/server/preferences.edit"),
-          RestCall.get("/config/server/top-menus"),
-          RestCall.put("/config/server/email.confirm"),
-          RestCall.post("/config/server/check.consistency"),
-          RestCall.post("/config/server/reload"),
-          RestCall.get("/config/server/summary"),
-          RestCall.get("/config/server/capabilities"),
-          RestCall.get("/config/server/caches"),
-          RestCall.post("/config/server/caches"),
-          RestCall.get("/config/server/tasks"),
-          RestCall.post("/config/server/index.changes"));
-
-  /**
-   * Cache REST endpoints to be tested, the URLs contain a placeholder for the cache identifier.
-   * Since there is only supported a single supported config identifier ('server') it can be
-   * hard-coded.
-   */
-  private static final ImmutableList<RestCall> CACHE_ENDPOINTS =
-      ImmutableList.of(RestCall.get("/config/server/caches/%s"));
-
-  /**
-   * Task REST endpoints to be tested, the URLs contain a placeholder for the task identifier. Since
-   * there is only supported a single supported config identifier ('server') it can be hard-coded.
-   */
-  private static final ImmutableList<RestCall> TASK_ENDPOINTS =
-      ImmutableList.of(
-          RestCall.get("/config/server/tasks/%s"),
-
-          // Task deletion must be tested last
-          RestCall.delete("/config/server/tasks/%s"));
-
-  @Test
-  public void configEndpoints() throws Exception {
-    // 'Access Database' is needed for the '/config/server/check.consistency' REST endpoint
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
-
-    execute(CONFIG_ENDPOINTS);
-  }
-
-  @Test
-  public void cacheEndpoints() throws Exception {
-    execute(CACHE_ENDPOINTS, ProjectCacheImpl.CACHE_NAME);
-  }
-
-  @Test
-  public void taskEndpoints() throws Exception {
-    RestResponse r = adminRestSession.get("/config/server/tasks/");
-    List<TaskInfo> result =
-        newGson().fromJson(r.getReader(), new TypeToken<List<TaskInfo>>() {}.getType());
-    r.consume();
-
-    Optional<String> id =
-        result.stream()
-            .filter(t -> "Log File Compressor".equals(t.command))
-            .map(t -> t.id)
-            .findFirst();
-    assertThat(id).isPresent();
-
-    execute(TASK_ENDPOINTS, id.get());
-  }
-}
diff --git a/javatests/com/google/gerrit/acceptance/rest/DeleteOnCollectionIT.java b/javatests/com/google/gerrit/acceptance/rest/DeleteOnCollectionIT.java
index 4de436a..d891d6e 100644
--- a/javatests/com/google/gerrit/acceptance/rest/DeleteOnCollectionIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/DeleteOnCollectionIT.java
@@ -36,13 +36,8 @@
       public void configure() {
         deleteOnCollection(BRANCH_KIND)
             .toInstance(
-                new RestCollectionModifyView<ProjectResource, BranchResource, Object>() {
-                  @Override
-                  public Object apply(ProjectResource parentResource, Object input)
-                      throws Exception {
-                    return Response.none();
-                  }
-                });
+                (RestCollectionModifyView<ProjectResource, BranchResource, Object>)
+                    (parentResource, input) -> Response.none());
       }
     };
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/GroupsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/GroupsRestApiBindingsIT.java
deleted file mode 100644
index 4538f75..0000000
--- a/javatests/com/google/gerrit/acceptance/rest/GroupsRestApiBindingsIT.java
+++ /dev/null
@@ -1,100 +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.acceptance.rest;
-
-import com.google.common.collect.ImmutableList;
-import org.junit.Test;
-
-/**
- * Tests for checking the bindings of the groups REST API.
- *
- * <p>These tests only verify that the group REST endpoints are correctly bound, they do no test the
- * functionality of the group REST endpoints (for details see JavaDoc on {@link
- * AbstractRestApiBindingsTest}).
- */
-public class GroupsRestApiBindingsIT extends AbstractRestApiBindingsTest {
-  /**
-   * Group REST endpoints to be tested, each URL contains a placeholder for the group identifier.
-   */
-  private static final ImmutableList<RestCall> GROUP_ENDPOINTS =
-      ImmutableList.of(
-          RestCall.get("/groups/%s"),
-          RestCall.put("/groups/%s"),
-          RestCall.get("/groups/%s/detail"),
-          RestCall.get("/groups/%s/name"),
-          RestCall.put("/groups/%s/name"),
-          RestCall.get("/groups/%s/description"),
-          RestCall.put("/groups/%s/description"),
-          RestCall.delete("/groups/%s/description"),
-          RestCall.get("/groups/%s/owner"),
-          RestCall.put("/groups/%s/owner"),
-          RestCall.get("/groups/%s/options"),
-          RestCall.put("/groups/%s/options"),
-          RestCall.post("/groups/%s/members"),
-          RestCall.post("/groups/%s/members.add"),
-          RestCall.post("/groups/%s/members.delete"),
-          RestCall.post("/groups/%s/groups"),
-          RestCall.post("/groups/%s/groups.add"),
-          RestCall.post("/groups/%s/groups.delete"),
-          RestCall.get("/groups/%s/log.audit"),
-          RestCall.post("/groups/%s/index"),
-          RestCall.get("/groups/%s/members"),
-          RestCall.get("/groups/%s/groups"));
-
-  /**
-   * Member REST endpoints to be tested, each URL contains placeholders for the group identifier and
-   * the member identifier.
-   */
-  private static final ImmutableList<RestCall> MEMBER_ENDPOINTS =
-      ImmutableList.of(
-          RestCall.get("/groups/%s/members/%s"),
-          RestCall.put("/groups/%s/members/%s"),
-
-          // Member deletion must be tested last
-          RestCall.delete("/groups/%s/members/%s"));
-
-  /**
-   * Subgroup REST endpoints to be tested, each URL contains placeholders for the group identifier
-   * and the subgroup identifier.
-   */
-  private static final ImmutableList<RestCall> SUBGROUP_ENDPOINTS =
-      ImmutableList.of(
-          RestCall.get("/groups/%s/groups/%s"),
-          RestCall.put("/groups/%s/groups/%s"),
-
-          // Subgroup deletion must be tested last
-          RestCall.delete("/groups/%s/groups/%s"));
-
-  @Test
-  public void groupEndpoints() throws Exception {
-    String group = gApi.groups().create("test-group").get().name;
-    execute(GROUP_ENDPOINTS, group);
-  }
-
-  @Test
-  public void memberEndpoints() throws Exception {
-    String group = gApi.groups().create("test-group").get().name;
-    gApi.groups().id(group).addMembers(admin.email);
-    execute(MEMBER_ENDPOINTS, group, admin.email);
-  }
-
-  @Test
-  public void subgroupEndpoints() throws Exception {
-    String group = gApi.groups().create("test-group").get().name;
-    String subgroup = gApi.groups().create("test-subgroup").get().name;
-    gApi.groups().id(group).addGroups(subgroup);
-    execute(SUBGROUP_ENDPOINTS, group, subgroup);
-  }
-}
diff --git a/javatests/com/google/gerrit/acceptance/rest/PluginsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/PluginsRestApiBindingsIT.java
deleted file mode 100644
index 07ea3d0..0000000
--- a/javatests/com/google/gerrit/acceptance/rest/PluginsRestApiBindingsIT.java
+++ /dev/null
@@ -1,68 +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.acceptance.rest;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.common.collect.ImmutableList;
-import com.google.gerrit.acceptance.GerritConfig;
-import com.google.gerrit.common.RawInputUtil;
-import com.google.gerrit.extensions.api.plugins.InstallPluginInput;
-import com.google.gerrit.extensions.restapi.RawInput;
-import org.junit.Test;
-
-/**
- * Tests for checking the bindings of the plugins REST API.
- *
- * <p>These tests only verify that the plugin REST endpoints are correctly bound, they do no test
- * the functionality of the plugin REST endpoints (for details see JavaDoc on {@link
- * AbstractRestApiBindingsTest}).
- */
-public class PluginsRestApiBindingsIT extends AbstractRestApiBindingsTest {
-  /**
-   * Plugin REST endpoints to be tested, each URL contains a placeholder for the plugin identifier.
-   */
-  private static final ImmutableList<RestCall> PLUGIN_ENDPOINTS =
-      ImmutableList.of(
-          RestCall.put("/plugins/%s"),
-
-          // For GET requests prefixing the view name with 'gerrit~' is required.
-          RestCall.get("/plugins/%s/gerrit~status"),
-
-          // POST (and PUT) requests don't require the 'gerrit~' prefix in front of the view name.
-          RestCall.post("/plugins/%s/gerrit~enable"),
-          RestCall.post("/plugins/%s/gerrit~disable"),
-          RestCall.post("/plugins/%s/gerrit~reload"),
-
-          // Plugin deletion must be tested last
-          RestCall.delete("/plugins/%s"));
-
-  private static final String JS_PLUGIN = "Gerrit.install(function(self){});\n";
-  private static final RawInput JS_PLUGIN_CONTENT = RawInputUtil.create(JS_PLUGIN.getBytes(UTF_8));
-
-  @Test
-  @GerritConfig(name = "plugins.allowRemoteAdmin", value = "true")
-  public void pluginEndpoints() throws Exception {
-    String pluginName = "my-plugin";
-    installPlugin(pluginName);
-    execute(PLUGIN_ENDPOINTS, pluginName);
-  }
-
-  private void installPlugin(String pluginName) throws Exception {
-    InstallPluginInput input = new InstallPluginInput();
-    input.raw = JS_PLUGIN_CONTENT;
-    gApi.plugins().install(pluginName + ".js", input);
-  }
-}
diff --git a/javatests/com/google/gerrit/acceptance/rest/ProjectsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/ProjectsRestApiBindingsIT.java
deleted file mode 100644
index 6563de3..0000000
--- a/javatests/com/google/gerrit/acceptance/rest/ProjectsRestApiBindingsIT.java
+++ /dev/null
@@ -1,248 +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.acceptance.rest;
-
-import static com.google.gerrit.acceptance.GitUtil.assertPushOk;
-import static com.google.gerrit.acceptance.GitUtil.pushHead;
-import static com.google.gerrit.acceptance.rest.AbstractRestApiBindingsTest.Method.GET;
-import static com.google.gerrit.reviewdb.client.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;
-
-import com.google.common.collect.ImmutableList;
-import com.google.gerrit.acceptance.GitUtil;
-import com.google.gerrit.common.data.Permission;
-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 org.eclipse.jgit.junit.TestRepository;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.transport.PushResult;
-import org.junit.Test;
-
-/**
- * Tests for checking the bindings of the projects REST API.
- *
- * <p>These tests only verify that the project REST endpoints are correctly bound, they do no test
- * the functionality of the project REST endpoints (for details see JavaDoc on {@link
- * AbstractRestApiBindingsTest}).
- */
-public class ProjectsRestApiBindingsIT extends AbstractRestApiBindingsTest {
-  private static final ImmutableList<RestCall> PROJECT_ENDPOINTS =
-      ImmutableList.of(
-          RestCall.get("/projects/%s"),
-          RestCall.put("/projects/%s"),
-          RestCall.get("/projects/%s/description"),
-          RestCall.put("/projects/%s/description"),
-          RestCall.delete("/projects/%s/description"),
-          RestCall.get("/projects/%s/parent"),
-          RestCall.put("/projects/%s/parent"),
-          RestCall.get("/projects/%s/config"),
-          RestCall.put("/projects/%s/config"),
-          RestCall.get("/projects/%s/HEAD"),
-          RestCall.put("/projects/%s/HEAD"),
-          RestCall.get("/projects/%s/access"),
-          RestCall.post("/projects/%s/access"),
-          RestCall.put("/projects/%s/access:review"),
-          RestCall.get("/projects/%s/check.access"),
-          RestCall.post("/projects/%s/check.access"),
-          RestCall.put("/projects/%s/ban"),
-          RestCall.get("/projects/%s/statistics.git"),
-          RestCall.post("/projects/%s/index"),
-          RestCall.post("/projects/%s/gc"),
-          RestCall.get("/projects/%s/children"),
-          RestCall.get("/projects/%s/branches"),
-          RestCall.post("/projects/%s/branches:delete"),
-          RestCall.put("/projects/%s/branches/new-branch"),
-          RestCall.get("/projects/%s/tags"),
-          RestCall.post("/projects/%s/tags:delete"),
-          RestCall.put("/projects/%s/tags/new-tag"),
-          RestCall.builder(GET, "/projects/%s/commits")
-              // GET /projects/<project>/branches/<branch>/commits is not implemented
-              .expectedResponseCode(SC_NOT_FOUND)
-              .build(),
-          RestCall.get("/projects/%s/dashboards"));
-
-  /**
-   * Child project REST endpoints to be tested, each URL contains placeholders for the parent
-   * project identifier and the child project identifier.
-   */
-  private static final ImmutableList<RestCall> CHILD_PROJECT_ENDPOINTS =
-      ImmutableList.of(RestCall.get("/projects/%s/children/%s"));
-
-  /**
-   * Branch REST endpoints to be tested, each URL contains placeholders for the project identifier
-   * and the branch identifier.
-   */
-  private static final ImmutableList<RestCall> BRANCH_ENDPOINTS =
-      ImmutableList.of(
-          RestCall.get("/projects/%s/branches/%s"),
-          RestCall.put("/projects/%s/branches/%s"),
-          RestCall.get("/projects/%s/branches/%s/mergeable"),
-          RestCall.builder(GET, "/projects/%s/branches/%s/reflog")
-              // The tests use DfsRepository which does not support getting the reflog.
-              .expectedResponseCode(SC_METHOD_NOT_ALLOWED)
-              .expectedMessage("reflog not supported on")
-              .build(),
-          RestCall.builder(GET, "/projects/%s/branches/%s/files")
-              // GET /projects/<project>/branches/<branch>/files is not implemented
-              .expectedResponseCode(SC_NOT_FOUND)
-              .build(),
-
-          // Branch deletion must be tested last
-          RestCall.delete("/projects/%s/branches/%s"));
-
-  /**
-   * Branch file REST endpoints to be tested, each URL contains placeholders for the project
-   * identifier, the branch identifier and the file identifier.
-   */
-  private static final ImmutableList<RestCall> BRANCH_FILE_ENDPOINTS =
-      ImmutableList.of(RestCall.get("/projects/%s/branches/%s/files/%s/content"));
-
-  /**
-   * Dashboard REST endpoints to be tested, each URL contains placeholders for the project
-   * identifier and the dashboard identifier.
-   */
-  private static final ImmutableList<RestCall> DASHBOARD_ENDPOINTS =
-      ImmutableList.of(
-          RestCall.get("/projects/%s/dashboards/%s"),
-          RestCall.put("/projects/%s/dashboards/%s"),
-
-          // Dashboard deletion must be tested last
-          RestCall.delete("/projects/%s/dashboards/%s"));
-
-  /**
-   * Tag REST endpoints to be tested, each URL contains placeholders for the project identifier and
-   * the tag identifier.
-   */
-  private static final ImmutableList<RestCall> TAG_ENDPOINTS =
-      ImmutableList.of(
-          RestCall.get("/projects/%s/tags/%s"),
-          RestCall.put("/projects/%s/tags/%s"),
-          RestCall.delete("/projects/%s/tags/%s"));
-
-  /**
-   * Commit REST endpoints to be tested, each URL contains placeholders for the project identifier
-   * and the commit identifier.
-   */
-  private static final ImmutableList<RestCall> COMMIT_ENDPOINTS =
-      ImmutableList.of(
-          RestCall.get("/projects/%s/commits/%s"),
-          RestCall.get("/projects/%s/commits/%s/in"),
-          RestCall.get("/projects/%s/commits/%s/files"),
-          RestCall.post("/projects/%s/commits/%s/cherrypick"));
-
-  /**
-   * Commit file REST endpoints to be tested, each URL contains placeholders for the project
-   * identifier, the commit identifier and the file identifier.
-   */
-  private static final ImmutableList<RestCall> COMMIT_FILE_ENDPOINTS =
-      ImmutableList.of(RestCall.get("/projects/%s/commits/%s/files/%s/content"));
-
-  private static final String FILENAME = "test.txt";
-
-  @Test
-  public void projectEndpoints() throws Exception {
-    execute(PROJECT_ENDPOINTS, project.get());
-  }
-
-  @Test
-  public void childProjectEndpoints() throws Exception {
-    Project.NameKey childProject = createProject("test-child-repo", project);
-    execute(CHILD_PROJECT_ENDPOINTS, project.get(), childProject.get());
-  }
-
-  @Test
-  public void branchEndpoints() throws Exception {
-    execute(BRANCH_ENDPOINTS, project.get(), "master");
-  }
-
-  @Test
-  public void branchFileEndpoints() throws Exception {
-    createAndSubmitChange(FILENAME);
-    execute(BRANCH_FILE_ENDPOINTS, project.get(), "master", FILENAME);
-  }
-
-  @Test
-  public void dashboardEndpoints() throws Exception {
-    createDefaultDashboard();
-    execute(DASHBOARD_ENDPOINTS, project.get(), DEFAULT_DASHBOARD_NAME);
-  }
-
-  @Test
-  public void tagEndpoints() throws Exception {
-    String tag = "test-tag";
-    gApi.projects().name(project.get()).tag(tag).create(new TagInput());
-    execute(TAG_ENDPOINTS, project.get(), tag);
-  }
-
-  @Test
-  public void commitEndpoints() throws Exception {
-    String commit = createAndSubmitChange(FILENAME);
-    execute(COMMIT_ENDPOINTS, project.get(), commit);
-  }
-
-  @Test
-  public void commitFileEndpoints() throws Exception {
-    String commit = createAndSubmitChange(FILENAME);
-    execute(COMMIT_FILE_ENDPOINTS, project.get(), commit, FILENAME);
-  }
-
-  private String createAndSubmitChange(String filename) throws Exception {
-    RevCommit c =
-        testRepo
-            .commit()
-            .message("A change")
-            .parent(getRemoteHead())
-            .add(filename, "content")
-            .insertChangeId()
-            .create();
-    String id = GitUtil.getChangeId(testRepo, c).get();
-    testRepo.reset(c);
-
-    String r = "refs/for/master";
-    PushResult pr = pushHead(testRepo, r, false);
-    assertPushOk(pr, r);
-
-    gApi.changes().id(id).current().review(ReviewInput.approve());
-    gApi.changes().id(id).current().submit();
-    return c.name();
-  }
-
-  private void createDefaultDashboard() throws Exception {
-    String dashboardRef = REFS_DASHBOARDS + "team";
-    grant(project, "refs/meta/*", Permission.CREATE);
-    gApi.projects().name(project.get()).branch(dashboardRef).create(new BranchInput());
-
-    try (Repository r = repoManager.openRepository(project)) {
-      TestRepository<Repository>.CommitBuilder cb =
-          new TestRepository<>(r).branch(dashboardRef).commit();
-      StringBuilder content = new StringBuilder("[dashboard]\n");
-      content.append("title = ").append("Open Changes").append("\n");
-      content.append("[section \"").append("open").append("\"]\n");
-      content.append("query = ").append("is:open").append("\n");
-      cb.add("overview", content.toString());
-      cb.create();
-    }
-
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      u.getConfig().getProject().setLocalDefaultDashboard(dashboardRef + ":overview");
-      u.save();
-    }
-  }
-}
diff --git a/javatests/com/google/gerrit/acceptance/rest/RootCollectionsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/RootCollectionsRestApiBindingsIT.java
deleted file mode 100644
index a2c4ea6..0000000
--- a/javatests/com/google/gerrit/acceptance/rest/RootCollectionsRestApiBindingsIT.java
+++ /dev/null
@@ -1,56 +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.acceptance.rest;
-
-import static com.google.gerrit.acceptance.rest.AbstractRestApiBindingsTest.Method.GET;
-import static org.apache.http.HttpStatus.SC_NOT_FOUND;
-
-import com.google.common.collect.ImmutableList;
-import com.google.gerrit.acceptance.GerritConfig;
-import org.junit.Test;
-
-/**
- * Tests for checking the bindings of the root REST API.
- *
- * <p>These tests only verify that the root REST endpoints are correctly bound, they do no test the
- * functionality of the root REST endpoints (for details see JavaDoc on {@link
- * AbstractRestApiBindingsTest}).
- */
-public class RootCollectionsRestApiBindingsIT extends AbstractRestApiBindingsTest {
-  /** Root REST endpoints to be tested, the URLs contain no placeholders. */
-  private static final ImmutableList<RestCall> ROOT_ENDPOINTS =
-      ImmutableList.of(
-          RestCall.get("/access/"),
-          RestCall.get("/accounts/"),
-          RestCall.put("/accounts/new-account"),
-          RestCall.builder(GET, "/config/")
-              // GET /config/ is not implemented
-              .expectedResponseCode(SC_NOT_FOUND)
-              .build(),
-          RestCall.get("/changes/"),
-          RestCall.post("/changes/"),
-          RestCall.get("/groups/"),
-          RestCall.put("/groups/new-group"),
-          RestCall.get("/plugins/"),
-          RestCall.put("/plugins/new-plugin"),
-          RestCall.get("/projects/"),
-          RestCall.put("/projects/new-project"));
-
-  @Test
-  @GerritConfig(name = "plugins.allowRemoteAdmin", value = "true")
-  public void rootEndpoints() throws Exception {
-    execute(ROOT_ENDPOINTS);
-  }
-}
diff --git a/javatests/com/google/gerrit/acceptance/rest/TraceIT.java b/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
index 137dc21..b30dc41 100644
--- a/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
@@ -171,7 +171,7 @@
 
   @Test
   public void pushWithoutTrace() throws Exception {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result r = push.to("refs/heads/master");
     r.assertOkStatus();
     assertThat(commitValidationListener.traceId).isNull();
@@ -180,7 +180,7 @@
 
   @Test
   public void pushWithTrace() throws Exception {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     push.setPushOptions(ImmutableList.of("trace"));
     PushOneCommit.Result r = push.to("refs/heads/master");
     r.assertOkStatus();
@@ -190,7 +190,7 @@
 
   @Test
   public void pushWithTraceAndProvidedTraceId() throws Exception {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     push.setPushOptions(ImmutableList.of("trace=issue/123"));
     PushOneCommit.Result r = push.to("refs/heads/master");
     r.assertOkStatus();
@@ -200,7 +200,7 @@
 
   @Test
   public void pushForReviewWithoutTrace() throws Exception {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
     assertThat(commitValidationListener.traceId).isNull();
@@ -209,7 +209,7 @@
 
   @Test
   public void pushForReviewWithTrace() throws Exception {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     push.setPushOptions(ImmutableList.of("trace"));
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
@@ -219,7 +219,7 @@
 
   @Test
   public void pushForReviewWithTraceAndProvidedTraceId() throws Exception {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     push.setPushOptions(ImmutableList.of("trace=issue/123"));
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/AccountAssert.java b/javatests/com/google/gerrit/acceptance/rest/account/AccountAssert.java
index 2baaef8..5e652c0 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/AccountAssert.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/AccountAssert.java
@@ -25,9 +25,9 @@
 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);
+    assertThat(a.id().get()).isEqualTo(ai._accountId);
+    assertThat(a.fullName()).isEqualTo(ai.name);
+    assertThat(a.email()).isEqualTo(ai.email);
   }
 
   public static void assertAccountInfos(List<TestAccount> expected, List<AccountInfo> actual) {
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/BUILD b/javatests/com/google/gerrit/acceptance/rest/account/BUILD
index 6bd232a..66ea6f3 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/BUILD
+++ b/javatests/com/google/gerrit/acceptance/rest/account/BUILD
@@ -19,7 +19,6 @@
     deps = [
         "//java/com/google/gerrit/acceptance:lib",
         "//java/com/google/gerrit/reviewdb:server",
-        "//lib:gwtorm",
         "//lib:junit",
     ],
 )
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/EmailIT.java b/javatests/com/google/gerrit/acceptance/rest/account/EmailIT.java
index f3fe68a..84f218d 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/EmailIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/EmailIT.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 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.extensions.api.accounts.EmailApi;
 import com.google.gerrit.extensions.api.accounts.EmailInput;
 import com.google.gerrit.extensions.common.EmailInfo;
@@ -30,7 +31,6 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.AccountsUpdate;
@@ -43,9 +43,8 @@
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.CanonicalWebUrl;
-import com.google.gerrit.server.config.DisableReverseDnsLookup;
+import com.google.gerrit.server.config.EnableReverseDnsLookup;
 import com.google.gson.reflect.TypeToken;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.util.List;
@@ -54,15 +53,15 @@
 import org.junit.Test;
 
 public class EmailIT extends AbstractDaemonTest {
-  @Inject private @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider;
-  @Inject private ExternalIds externalIds;
-  @Inject private SchemaFactory<ReviewDb> reviewDbProvider;
-  @Inject private AuthConfig authConfig;
   @Inject private @AnonymousCowardName String anonymousCowardName;
   @Inject private @CanonicalWebUrl Provider<String> canonicalUrl;
-  @Inject private @DisableReverseDnsLookup Boolean disableReverseDnsLookup;
+  @Inject private @EnableReverseDnsLookup boolean enableReverseDnsLookup;
+  @Inject private @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider;
+  @Inject private AuthConfig authConfig;
   @Inject private EmailExpander emailExpander;
+  @Inject private ExternalIds externalIds;
   @Inject private Provider<Emails> emails;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
   public void addEmail() throws Exception {
@@ -123,7 +122,7 @@
     createEmail(email);
     assertThat(gApi.accounts().self().get().email).isNotEqualTo(email);
 
-    resetCurrentApiUser();
+    requestScopeOperations.resetCurrentApiUser();
     gApi.accounts().self().email(email).setPreferred();
     assertThat(gApi.accounts().self().get().email).isEqualTo(email);
   }
@@ -135,14 +134,14 @@
         .get()
         .update(
             "Add External ID",
-            admin.id,
+            admin.id(),
             u ->
                 u.addExternalId(
                     ExternalId.createWithEmail(
-                        ExternalId.SCHEME_EXTERNAL, "foo", admin.id, email)));
+                        ExternalId.SCHEME_EXTERNAL, "foo", admin.id(), email)));
     assertThat(gApi.accounts().self().get().email).isNotEqualTo(email);
 
-    resetCurrentApiUser();
+    requestScopeOperations.resetCurrentApiUser();
     gApi.accounts().self().email(email).setPreferred();
     assertThat(gApi.accounts().self().get().email).isEqualTo(email);
   }
@@ -158,8 +157,8 @@
   @Test
   public void setPreferredEmailToEmailOfOtherAccount() throws Exception {
     exception.expect(ResourceNotFoundException.class);
-    exception.expectMessage("Not found: " + user.email);
-    gApi.accounts().self().email(user.email).setPreferred();
+    exception.expectMessage("Not found: " + user.email());
+    gApi.accounts().self().email(user.email()).setPreferred();
   }
 
   @Test
@@ -168,7 +167,7 @@
     createEmail(email);
     assertThat(gApi.accounts().self().get().email).isNotEqualTo(email);
 
-    resetCurrentApiUser();
+    requestScopeOperations.resetCurrentApiUser();
     String emailOtherCase = email.toUpperCase();
     gApi.accounts().self().email(emailOtherCase).setPreferred();
     assertThat(gApi.accounts().self().get().email).isEqualTo(email);
@@ -182,12 +181,12 @@
     assertThat(externalIds.get(mailtoExtIdKey)).isEmpty();
     assertThat(gApi.accounts().self().get().email).isNotEqualTo(email);
 
-    Context oldCtx = createContextWithCustomRealm(new RealmWithAdditionalEmails(admin.id, email));
+    Context oldCtx = createContextWithCustomRealm(new RealmWithAdditionalEmails(admin.id(), email));
     try {
       gApi.accounts().self().email(email).setPreferred();
       Optional<ExternalId> mailtoExtId = externalIds.get(mailtoExtIdKey);
       assertThat(mailtoExtId).isPresent();
-      assertThat(mailtoExtId.get().accountId()).isEqualTo(admin.id);
+      assertThat(mailtoExtId.get().accountId()).isEqualTo(admin.id());
       assertThat(gApi.accounts().self().get().email).isEqualTo(email);
     } finally {
       atrScope.set(oldCtx);
@@ -196,15 +195,15 @@
 
   @Test
   public void setPreferredEmailToEmailFromCustomRealmThatBelongsToOtherAccount() throws Exception {
-    ExternalId mailToExtId = ExternalId.createEmail(user.id, user.email);
+    ExternalId mailToExtId = ExternalId.createEmail(user.id(), user.email());
     assertThat(externalIds.get(mailToExtId.key())).isPresent();
 
     Context oldCtx =
-        createContextWithCustomRealm(new RealmWithAdditionalEmails(admin.id, user.email));
+        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();
+      gApi.accounts().self().email(user.email()).setPreferred();
     } finally {
       atrScope.set(oldCtx);
     }
@@ -224,7 +223,7 @@
     assertThat(gApi.accounts().self().get().email).isNotEqualTo(email);
 
     // Get email
-    resetCurrentApiUser();
+    requestScopeOperations.resetCurrentApiUser();
     EmailApi emailApi = gApi.accounts().self().email(email);
     EmailInfo emailInfo = emailApi.get();
     assertThat(emailInfo.email).isEqualTo(email);
@@ -236,7 +235,7 @@
     assertThat(gApi.accounts().self().get().email).isEqualTo(email);
 
     // Get email again (now it's the preferred email)
-    resetCurrentApiUser();
+    requestScopeOperations.resetCurrentApiUser();
     emailApi = gApi.accounts().self().email(email);
     emailInfo = emailApi.get();
     assertThat(emailInfo.email).isEqualTo(email);
@@ -248,7 +247,7 @@
     assertThat(getEmails()).doesNotContain(email);
 
     // Now the email is no longer found
-    resetCurrentApiUser();
+    requestScopeOperations.resetCurrentApiUser();
     emailApi = gApi.accounts().self().email(email);
     exception.expect(ResourceNotFoundException.class);
     emailApi.get();
@@ -276,10 +275,10 @@
             realm,
             anonymousCowardName,
             canonicalUrl,
-            disableReverseDnsLookup,
+            enableReverseDnsLookup,
             accountCache,
             groupBackend);
-    return atrScope.set(atrScope.newContext(reviewDbProvider, null, userFactory.create(admin.id)));
+    return atrScope.set(atrScope.newContext(null, userFactory.create(admin.id())));
   }
 
   private class RealmWithAdditionalEmails extends DefaultRealm {
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
index 57e0afa..f08fa44 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
@@ -33,8 +33,10 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.TestAccount;
+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.exceptions.DuplicateKeyException;
 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;
@@ -53,7 +55,6 @@
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gson.reflect.TypeToken;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -90,10 +91,11 @@
   @Inject private ExternalIds externalIds;
   @Inject private ExternalIdReader externalIdReader;
   @Inject private ExternalIdNotes.Factory externalIdNotesFactory;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
   public void getExternalIds() throws Exception {
-    Collection<ExternalId> expectedIds = getAccountState(user.getId()).getExternalIds();
+    Collection<ExternalId> expectedIds = getAccountState(user.id()).getExternalIds();
     List<AccountExternalIdInfo> expectedIdInfos = toExternalIdInfos(expectedIds);
 
     RestResponse response = userRestSession.get("/accounts/self/external.ids");
@@ -109,20 +111,20 @@
 
   @Test
   public void getExternalIdsOfOtherUserNotAllowed() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("access database not permitted");
-    gApi.accounts().id(admin.id.get()).getExternalIds();
+    gApi.accounts().id(admin.id().get()).getExternalIds();
   }
 
   @Test
   public void getExternalIdsOfOtherUserWithAccessDatabase() throws Exception {
     allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
 
-    Collection<ExternalId> expectedIds = getAccountState(admin.getId()).getExternalIds();
+    Collection<ExternalId> expectedIds = getAccountState(admin.id()).getExternalIds();
     List<AccountExternalIdInfo> expectedIdInfos = toExternalIdInfos(expectedIds);
 
-    RestResponse response = userRestSession.get("/accounts/" + admin.id + "/external.ids");
+    RestResponse response = userRestSession.get("/accounts/" + admin.id() + "/external.ids");
     response.assertOK();
 
     List<AccountExternalIdInfo> results =
@@ -135,7 +137,7 @@
 
   @Test
   public void deleteExternalIds() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     List<AccountExternalIdInfo> externalIds = gApi.accounts().self().getExternalIds();
 
     List<String> toDelete = new ArrayList<>();
@@ -162,18 +164,18 @@
   @Test
   public void deleteExternalIdsOfOtherUserNotAllowed() throws Exception {
     List<AccountExternalIdInfo> extIds = gApi.accounts().self().getExternalIds();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("access database not permitted");
     gApi.accounts()
-        .id(admin.id.get())
+        .id(admin.id().get())
         .deleteExternalIds(extIds.stream().map(e -> e.identity).collect(toList()));
   }
 
   @Test
   public void deleteExternalIdOfOtherUserUnderOwnAccount_UnprocessableEntity() throws Exception {
     List<AccountExternalIdInfo> extIds = gApi.accounts().self().getExternalIds();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(UnprocessableEntityException.class);
     exception.expectMessage(String.format("External id %s does not exist", extIds.get(0).identity));
     gApi.accounts()
@@ -199,11 +201,11 @@
 
     assertThat(toDelete).hasSize(1);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     RestResponse response =
-        userRestSession.post("/accounts/" + admin.id + "/external.ids:delete", toDelete);
+        userRestSession.post("/accounts/" + admin.id() + "/external.ids:delete", toDelete);
     response.assertNoContent();
-    List<AccountExternalIdInfo> results = gApi.accounts().id(admin.id.get()).getExternalIds();
+    List<AccountExternalIdInfo> results = gApi.accounts().id(admin.id().get()).getExternalIds();
     // The external ID in WebSession will not be set for tests, resulting that
     // "mailto:user@example.com" can be deleted while "username:user" can't.
     assertThat(results).hasSize(1);
@@ -225,7 +227,7 @@
   @Test
   public void deleteExternalIds_Conflict() throws Exception {
     List<String> toDelete = new ArrayList<>();
-    String externalIdStr = "username:" + user.username;
+    String externalIdStr = "username:" + user.username();
     toDelete.add(externalIdStr);
     RestResponse response = userRestSession.post("/accounts/self/external.ids:delete", toDelete);
     response.assertConflict();
@@ -402,7 +404,7 @@
   @Test
   public void readExternalIdsWhenInvalidExternalIdsExist() throws Exception {
     allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
-    resetCurrentApiUser();
+    requestScopeOperations.resetCurrentApiUser();
 
     insertValidExternalIds();
     insertInvalidButParsableExternalIds();
@@ -423,7 +425,7 @@
   @Test
   public void checkConsistency() throws Exception {
     allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
-    resetCurrentApiUser();
+    requestScopeOperations.resetCurrentApiUser();
 
     insertValidExternalIds();
 
@@ -461,10 +463,10 @@
     insertExtId(
         ExternalId.createWithPassword(
             ExternalId.Key.parse(nextId(scheme, i)),
-            admin.id,
+            admin.id(),
             "admin.other@example.com",
             "secret-password"));
-    insertExtId(ExternalId.createEmail(admin.id, "admin.other@example.com"));
+    insertExtId(ExternalId.createEmail(admin.id(), "admin.other@example.com"));
     insertExtId(createExternalIdWithOtherCaseEmail(nextId(scheme, i)));
   }
 
@@ -562,7 +564,10 @@
 
   private ExternalId createExternalIdWithOtherCaseEmail(String externalId) {
     return ExternalId.createWithPassword(
-        ExternalId.Key.parse(externalId), admin.id, admin.email.toUpperCase(Locale.US), "password");
+        ExternalId.Key.parse(externalId),
+        admin.id(),
+        admin.email().toUpperCase(Locale.US),
+        "password");
   }
 
   private String insertExternalIdWithoutAccountId(Repository repo, RevWalk rw, String externalId)
@@ -571,7 +576,7 @@
         repo,
         rw,
         (ins, noteMap) -> {
-          ExternalId extId = ExternalId.create(ExternalId.Key.parse(externalId), admin.id);
+          ExternalId extId = ExternalId.create(ExternalId.Key.parse(externalId), admin.id());
           ObjectId noteId = extId.key().sha1();
           Config c = new Config();
           extId.writeToConfig(c);
@@ -589,7 +594,7 @@
         repo,
         rw,
         (ins, noteMap) -> {
-          ExternalId extId = ExternalId.create(ExternalId.Key.parse(externalId), admin.id);
+          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);
@@ -639,8 +644,8 @@
       CommitBuilder cb = new CommitBuilder();
       cb.setMessage("Update external IDs");
       cb.setTreeId(noteMap.writeTree(ins));
-      cb.setAuthor(admin.getIdent());
-      cb.setCommitter(admin.getIdent());
+      cb.setAuthor(admin.newIdent());
+      cb.setCommitter(admin.newIdent());
       if (!rev.equals(ObjectId.zeroId())) {
         cb.setParentId(rev);
       } else {
@@ -687,17 +692,18 @@
   }
 
   private ExternalId createExternalIdWithInvalidEmail(String externalId) {
-    return ExternalId.createWithEmail(ExternalId.Key.parse(externalId), admin.id, "invalid-email");
+    return ExternalId.createWithEmail(
+        ExternalId.Key.parse(externalId), admin.id(), "invalid-email");
   }
 
   private ExternalId createExternalIdWithDuplicateEmail(String externalId) {
-    return ExternalId.createWithEmail(ExternalId.Key.parse(externalId), user.id, admin.email);
+    return ExternalId.createWithEmail(ExternalId.Key.parse(externalId), user.id(), admin.email());
   }
 
   private ExternalId createExternalIdWithBadPassword(String username) {
     return ExternalId.create(
         ExternalId.Key.create(SCHEME_USERNAME, username),
-        admin.id,
+        admin.id(),
         null,
         "non-hashed-password-is-not-allowed");
   }
@@ -722,29 +728,30 @@
 
   @Test
   public void checkNoReloadAfterUpdate() throws Exception {
-    Set<ExternalId> expectedExtIds = new HashSet<>(externalIds.byAccount(admin.id));
+    Set<ExternalId> expectedExtIds = new HashSet<>(externalIds.byAccount(admin.id()));
     try (AutoCloseable ctx = createFailOnLoadContext()) {
       // insert external ID
-      ExternalId extId = ExternalId.create("foo", "bar", admin.id);
+      ExternalId extId = ExternalId.create("foo", "bar", admin.id());
       insertExtId(extId);
       expectedExtIds.add(extId);
-      assertThat(externalIds.byAccount(admin.id)).containsExactlyElementsIn(expectedExtIds);
+      assertThat(externalIds.byAccount(admin.id())).containsExactlyElementsIn(expectedExtIds);
 
       // update external ID
       expectedExtIds.remove(extId);
-      ExternalId extId2 = ExternalId.createWithEmail("foo", "bar", admin.id, "foo.bar@example.com");
+      ExternalId extId2 =
+          ExternalId.createWithEmail("foo", "bar", admin.id(), "foo.bar@example.com");
       accountsUpdateProvider
           .get()
-          .update("Update External ID", admin.id, u -> u.updateExternalId(extId2));
+          .update("Update External ID", admin.id(), u -> u.updateExternalId(extId2));
       expectedExtIds.add(extId2);
-      assertThat(externalIds.byAccount(admin.id)).containsExactlyElementsIn(expectedExtIds);
+      assertThat(externalIds.byAccount(admin.id())).containsExactlyElementsIn(expectedExtIds);
 
       // delete external ID
       accountsUpdateProvider
           .get()
-          .update("Delete External ID", admin.id, u -> u.deleteExternalId(extId));
+          .update("Delete External ID", admin.id(), u -> u.deleteExternalId(extId));
       expectedExtIds.remove(extId2);
-      assertThat(externalIds.byAccount(admin.id)).containsExactlyElementsIn(expectedExtIds);
+      assertThat(externalIds.byAccount(admin.id())).containsExactlyElementsIn(expectedExtIds);
     }
   }
 
@@ -752,10 +759,10 @@
   public void byAccountFailIfReadingExternalIdsFails() throws Exception {
     try (AutoCloseable ctx = createFailOnLoadContext()) {
       // update external ID branch so that external IDs need to be reloaded
-      insertExtIdBehindGerritsBack(ExternalId.create("foo", "bar", admin.id));
+      insertExtIdBehindGerritsBack(ExternalId.create("foo", "bar", admin.id()));
 
       exception.expect(IOException.class);
-      externalIds.byAccount(admin.id);
+      externalIds.byAccount(admin.id());
     }
   }
 
@@ -763,28 +770,28 @@
   public void byEmailFailIfReadingExternalIdsFails() throws Exception {
     try (AutoCloseable ctx = createFailOnLoadContext()) {
       // update external ID branch so that external IDs need to be reloaded
-      insertExtIdBehindGerritsBack(ExternalId.create("foo", "bar", admin.id));
+      insertExtIdBehindGerritsBack(ExternalId.create("foo", "bar", admin.id()));
 
       exception.expect(IOException.class);
-      externalIds.byEmail(admin.email);
+      externalIds.byEmail(admin.email());
     }
   }
 
   @Test
   public void byAccountUpdateExternalIdsBehindGerritsBack() throws Exception {
-    Set<ExternalId> expectedExternalIds = new HashSet<>(externalIds.byAccount(admin.id));
-    ExternalId newExtId = ExternalId.create("foo", "bar", admin.id);
+    Set<ExternalId> expectedExternalIds = new HashSet<>(externalIds.byAccount(admin.id()));
+    ExternalId newExtId = ExternalId.create("foo", "bar", admin.id());
     insertExtIdBehindGerritsBack(newExtId);
     expectedExternalIds.add(newExtId);
-    assertThat(externalIds.byAccount(admin.id)).containsExactlyElementsIn(expectedExternalIds);
+    assertThat(externalIds.byAccount(admin.id())).containsExactlyElementsIn(expectedExternalIds);
   }
 
   @Test
   public void unsetEmail() throws Exception {
-    ExternalId extId = ExternalId.createWithEmail("x", "1", user.id, "x@example.com");
+    ExternalId extId = ExternalId.createWithEmail("x", "1", user.id(), "x@example.com");
     insertExtId(extId);
 
-    ExternalId extIdWithoutEmail = ExternalId.create("x", "1", user.id);
+    ExternalId extIdWithoutEmail = ExternalId.create("x", "1", user.id());
     try (Repository allUsersRepo = repoManager.openRepository(allUsers);
         MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
       ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
@@ -798,10 +805,10 @@
   @Test
   public void unsetHttpPassword() throws Exception {
     ExternalId extId =
-        ExternalId.createWithPassword(ExternalId.Key.create("y", "1"), user.id, null, "secret");
+        ExternalId.createWithPassword(ExternalId.Key.create("y", "1"), user.id(), null, "secret");
     insertExtId(extId);
 
-    ExternalId extIdWithoutPassword = ExternalId.create("y", "1", user.id);
+    ExternalId extIdWithoutPassword = ExternalId.create("y", "1", user.id());
     try (Repository allUsersRepo = repoManager.openRepository(allUsers);
         MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
       ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
@@ -817,22 +824,22 @@
     // Insert external ID for different accounts
     TestAccount user1 = accountCreator.create("user1");
     TestAccount user2 = accountCreator.create("user2");
-    ExternalId extId1 = ExternalId.create("foo", "1", user1.id);
-    ExternalId extId2 = ExternalId.create("foo", "2", user1.id);
-    ExternalId extId3 = ExternalId.create("foo", "3", user2.id);
+    ExternalId extId1 = ExternalId.create("foo", "1", user1.id());
+    ExternalId extId2 = ExternalId.create("foo", "2", user1.id());
+    ExternalId extId3 = ExternalId.create("foo", "3", user2.id());
     try (Repository allUsersRepo = repoManager.openRepository(allUsers);
         MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
       ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
       extIdNotes.insert(ImmutableSet.of(extId1, extId2, extId3));
       RevCommit c = extIdNotes.commit(md);
       assertThat(getFooters(c))
-          .containsExactly("Account: " + user1.getId(), "Account: " + user2.getId())
+          .containsExactly("Account: " + user1.id(), "Account: " + user2.id())
           .inOrder();
     }
 
     // Insert external ID with different emails
-    ExternalId extId4 = ExternalId.createWithEmail("foo", "4", user1.id, "foo4@example.com");
-    ExternalId extId5 = ExternalId.createWithEmail("foo", "5", user2.id, "foo5@example.com");
+    ExternalId extId4 = ExternalId.createWithEmail("foo", "4", user1.id(), "foo4@example.com");
+    ExternalId extId5 = ExternalId.createWithEmail("foo", "5", user2.id(), "foo5@example.com");
     try (Repository allUsersRepo = repoManager.openRepository(allUsers);
         MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
       ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
@@ -840,22 +847,22 @@
       RevCommit c = extIdNotes.commit(md);
       assertThat(getFooters(c))
           .containsExactly(
-              "Account: " + user1.getId(),
-              "Account: " + user2.getId(),
+              "Account: " + user1.id(),
+              "Account: " + user2.id(),
               "Email: foo4@example.com",
               "Email: foo5@example.com")
           .inOrder();
     }
 
     // Update external ID - Add Email
-    ExternalId extId1a = ExternalId.createWithEmail("foo", "1", user1.id, "foo1@example.com");
+    ExternalId extId1a = ExternalId.createWithEmail("foo", "1", user1.id(), "foo1@example.com");
     try (Repository allUsersRepo = repoManager.openRepository(allUsers);
         MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
       ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
       extIdNotes.upsert(extId1a);
       RevCommit c = extIdNotes.commit(md);
       assertThat(getFooters(c))
-          .containsExactly("Account: " + user1.getId(), "Email: foo1@example.com")
+          .containsExactly("Account: " + user1.id(), "Email: foo1@example.com")
           .inOrder();
     }
 
@@ -866,7 +873,7 @@
       extIdNotes.upsert(extId1);
       RevCommit c = extIdNotes.commit(md);
       assertThat(getFooters(c))
-          .containsExactly("Account: " + user1.getId(), "Email: foo1@example.com")
+          .containsExactly("Account: " + user1.id(), "Email: foo1@example.com")
           .inOrder();
     }
 
@@ -878,7 +885,7 @@
       RevCommit c = extIdNotes.commit(md);
       assertThat(getFooters(c))
           .containsExactly(
-              "Account: " + user1.getId(), "Account: " + user2.getId(), "Email: foo5@example.com")
+              "Account: " + user1.id(), "Account: " + user2.id(), "Email: foo5@example.com")
           .inOrder();
     }
 
@@ -888,7 +895,7 @@
       ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
       extIdNotes.delete(extId2.accountId(), extId2.key());
       RevCommit c = extIdNotes.commit(md);
-      assertThat(getFooters(c)).containsExactly("Account: " + user1.getId()).inOrder();
+      assertThat(getFooters(c)).containsExactly("Account: " + user1.id()).inOrder();
     }
 
     // Delete external ID by key with email
@@ -898,7 +905,7 @@
       extIdNotes.delete(extId4.accountId(), extId4.key());
       RevCommit c = extIdNotes.commit(md);
       assertThat(getFooters(c))
-          .containsExactly("Account: " + user1.getId(), "Email: foo4@example.com")
+          .containsExactly("Account: " + user1.id(), "Email: foo4@example.com")
           .inOrder();
     }
   }
@@ -927,21 +934,21 @@
       extIdNotes.insert(extId);
       try (MetaDataUpdate metaDataUpdate =
           new MetaDataUpdate(GitReferenceUpdated.DISABLED, null, repo)) {
-        metaDataUpdate.getCommitBuilder().setAuthor(admin.getIdent());
-        metaDataUpdate.getCommitBuilder().setCommitter(admin.getIdent());
+        metaDataUpdate.getCommitBuilder().setAuthor(admin.newIdent());
+        metaDataUpdate.getCommitBuilder().setCommitter(admin.newIdent());
         extIdNotes.commit(metaDataUpdate);
       }
     }
   }
 
   private void addExtId(TestRepository<?> testRepo, ExternalId... extIds)
-      throws IOException, OrmDuplicateKeyException, ConfigInvalidException {
+      throws IOException, DuplicateKeyException, ConfigInvalidException {
     ExternalIdNotes extIdNotes = externalIdNotesFactory.load(testRepo.getRepository());
     extIdNotes.insert(Arrays.asList(extIds));
     try (MetaDataUpdate metaDataUpdate =
         new MetaDataUpdate(GitReferenceUpdated.DISABLED, null, testRepo.getRepository())) {
-      metaDataUpdate.getCommitBuilder().setAuthor(admin.getIdent());
-      metaDataUpdate.getCommitBuilder().setCommitter(admin.getIdent());
+      metaDataUpdate.getCommitBuilder().setAuthor(admin.newIdent());
+      metaDataUpdate.getCommitBuilder().setCommitter(admin.newIdent());
       extIdNotes.commit(metaDataUpdate);
       extIdNotes.updateCaches();
     }
@@ -981,12 +988,7 @@
 
   private AutoCloseable createFailOnLoadContext() {
     externalIdReader.setFailOnLoad(true);
-    return new AutoCloseable() {
-      @Override
-      public void close() {
-        externalIdReader.setFailOnLoad(false);
-      }
-    };
+    return () -> externalIdReader.setFailOnLoad(false);
   }
 
   @FunctionalInterface
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java b/javatests/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java
index 07bc394..27ae8b12 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java
@@ -26,10 +26,10 @@
 public class GetAccountDetailIT extends AbstractDaemonTest {
   @Test
   public void getDetail() throws Exception {
-    RestResponse r = adminRestSession.get("/accounts/" + admin.username + "/detail/");
+    RestResponse r = adminRestSession.get("/accounts/" + admin.username() + "/detail/");
     AccountDetailInfo info = newGson().fromJson(r.getReader(), AccountDetailInfo.class);
     assertAccountInfo(admin, info);
-    Account account = getAccount(admin.getId());
+    Account account = getAccount(admin.id());
     assertThat(info.registeredOn).isEqualTo(account.getRegisteredOn());
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/GetAccountIT.java b/javatests/com/google/gerrit/acceptance/rest/account/GetAccountIT.java
index ed7abd2..11f7c0f 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/GetAccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/GetAccountIT.java
@@ -32,19 +32,19 @@
   @Test
   public void getAccount() throws Exception {
     // by formatted string
-    testGetAccount(admin.fullName + " <" + admin.email + ">", admin);
+    testGetAccount(admin.fullName() + " <" + admin.email() + ">", admin);
 
     // by email
-    testGetAccount(admin.email, admin);
+    testGetAccount(admin.email(), admin);
 
     // by full name
-    testGetAccount(admin.fullName, admin);
+    testGetAccount(admin.fullName(), admin);
 
     // by account ID
-    testGetAccount(Integer.toString(admin.id.get()), admin);
+    testGetAccount(Integer.toString(admin.id().get()), admin);
 
     // by user name
-    testGetAccount(admin.username, admin);
+    testGetAccount(admin.username(), admin);
 
     // by 'self'
     testGetAccount("self", admin);
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java b/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
index 65c95f8..4dec505 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.acceptance.rest.account;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.TruthJUnit.assume;
 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;
@@ -29,6 +28,7 @@
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.RestSession;
 import com.google.gerrit.acceptance.TestAccount;
+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;
@@ -50,7 +50,6 @@
 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.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.Comment;
 import com.google.gerrit.reviewdb.client.Patch;
@@ -71,12 +70,10 @@
 
 public class ImpersonationIT extends AbstractDaemonTest {
   @Inject private AccountControl.Factory accountControlFactory;
-
   @Inject private ApprovalsUtil approvalsUtil;
-
   @Inject private ChangeMessagesUtil cmUtil;
-
   @Inject private CommentsUtil commentsUtil;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   private RestSession anonRestSession;
   private TestAccount admin2;
@@ -88,7 +85,7 @@
     admin2 = accountCreator.admin2();
     GroupInput gi = new GroupInput();
     gi.name = name("New-Group");
-    gi.members = ImmutableList.of(user.id.toString());
+    gi.members = ImmutableList.of(user.id().toString());
     newGroup = gApi.groups().create(gi).get();
   }
 
@@ -104,22 +101,22 @@
     RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
 
     ReviewInput in = ReviewInput.recommend();
-    in.onBehalfOf = user.id.toString();
+    in.onBehalfOf = user.id().toString();
     in.message = "Message on behalf of";
     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.getAccountId()).isEqualTo(user.id());
     assertThat(psa.getValue()).isEqualTo(1);
-    assertThat(psa.getRealAccountId()).isEqualTo(admin.id);
+    assertThat(psa.getRealAccountId()).isEqualTo(admin.id());
 
     ChangeData cd = r.getChange();
-    ChangeMessage m = Iterables.getLast(cmUtil.byChange(db, cd.notes()));
+    ChangeMessage m = Iterables.getLast(cmUtil.byChange(cd.notes()));
     assertThat(m.getMessage()).endsWith(in.message);
-    assertThat(m.getAuthor()).isEqualTo(user.id);
-    assertThat(m.getRealAuthor()).isEqualTo(admin.id);
+    assertThat(m.getAuthor()).isEqualTo(user.id());
+    assertThat(m.getRealAuthor()).isEqualTo(admin.id());
   }
 
   @Test
@@ -129,7 +126,7 @@
     RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
 
     ReviewInput in = new ReviewInput();
-    in.onBehalfOf = user.id.toString();
+    in.onBehalfOf = user.id().toString();
     in.message = "Message on behalf of";
 
     exception.expect(AuthException.class);
@@ -144,7 +141,7 @@
 
     String changeId = createChange().getChangeId();
     ReviewInput in = new ReviewInput().label("Not-A-Label", 5);
-    in.onBehalfOf = user.id.toString();
+    in.onBehalfOf = user.id().toString();
 
     exception.expect(BadRequestException.class);
     exception.expectMessage("label \"Not-A-Label\" is not a configured label");
@@ -157,7 +154,7 @@
 
     String changeId = createChange().getChangeId();
     ReviewInput in = new ReviewInput().label("Code-Review", 1).label("Not-A-Label", 5);
-    in.onBehalfOf = user.id.toString();
+    in.onBehalfOf = user.id().toString();
     gApi.changes().id(changeId).current().review(in);
 
     assertThat(gApi.changes().id(changeId).get().labels).doesNotContainKey("Not-A-Label");
@@ -175,7 +172,7 @@
     RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
 
     ReviewInput in = new ReviewInput();
-    in.onBehalfOf = user.id.toString();
+    in.onBehalfOf = user.id().toString();
     in.label("Verified", 1);
 
     exception.expect(AuthException.class);
@@ -191,7 +188,6 @@
 
   @Test
   public void voteOnBehalfOfWithCommentWritingJson() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
     testVoteOnBehalfOfWithComment();
   }
 
@@ -200,7 +196,7 @@
     PushOneCommit.Result r = createChange();
 
     ReviewInput in = new ReviewInput();
-    in.onBehalfOf = user.id.toString();
+    in.onBehalfOf = user.id().toString();
     in.label("Code-Review", 1);
     CommentInput ci = new CommentInput();
     ci.path = Patch.COMMIT_MSG;
@@ -213,25 +209,24 @@
     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.getAccountId()).isEqualTo(user.id());
     assertThat(psa.getValue()).isEqualTo(1);
-    assertThat(psa.getRealAccountId()).isEqualTo(admin.id);
+    assertThat(psa.getRealAccountId()).isEqualTo(admin.id());
 
     ChangeData cd = r.getChange();
-    Comment c = Iterables.getOnlyElement(commentsUtil.publishedByChange(db, cd.notes()));
+    Comment c = Iterables.getOnlyElement(commentsUtil.publishedByChange(cd.notes()));
     assertThat(c.message).isEqualTo(ci.message);
-    assertThat(c.author.getId()).isEqualTo(user.id);
-    assertThat(c.getRealAuthor().getId()).isEqualTo(admin.id);
+    assertThat(c.author.getId()).isEqualTo(user.id());
+    assertThat(c.getRealAuthor().getId()).isEqualTo(admin.id());
   }
 
   @Test
   public void voteOnBehalfOfWithRobotComment() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
     allowCodeReviewOnBehalfOf();
     PushOneCommit.Result r = createChange();
 
     ReviewInput in = new ReviewInput();
-    in.onBehalfOf = user.id.toString();
+    in.onBehalfOf = user.id().toString();
     in.label("Code-Review", 1);
     RobotCommentInput ci = new RobotCommentInput();
     ci.robotId = "my-robot";
@@ -248,8 +243,8 @@
     assertThat(c.message).isEqualTo(ci.message);
     assertThat(c.robotId).isEqualTo(ci.robotId);
     assertThat(c.robotRunId).isEqualTo(ci.robotRunId);
-    assertThat(c.author.getId()).isEqualTo(user.id);
-    assertThat(c.getRealAuthor().getId()).isEqualTo(admin.id);
+    assertThat(c.author.getId()).isEqualTo(user.id());
+    assertThat(c.getRealAuthor().getId()).isEqualTo(admin.id());
   }
 
   @Test
@@ -257,7 +252,7 @@
     allowCodeReviewOnBehalfOf();
     PushOneCommit.Result r = createChange();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     DraftInput di = new DraftInput();
     di.path = Patch.COMMIT_MSG;
     di.side = Side.REVISION;
@@ -265,9 +260,9 @@
     di.message = "message";
     gApi.changes().id(r.getChangeId()).current().createDraft(di);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     ReviewInput in = new ReviewInput();
-    in.onBehalfOf = user.id.toString();
+    in.onBehalfOf = user.id().toString();
     in.label("Code-Review", 1);
     in.drafts = DraftHandling.PUBLISH;
 
@@ -301,11 +296,11 @@
     RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
 
     ReviewInput in = new ReviewInput();
-    in.onBehalfOf = user.id.toString();
+    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");
+    exception.expectMessage("on_behalf_of account " + user.id() + " cannot see change");
     revision.review(in);
   }
 
@@ -313,14 +308,14 @@
   @Test
   public void voteOnBehalfOfInvisibleUserNotAllowed() throws Exception {
     allowCodeReviewOnBehalfOf();
-    setApiUser(accountCreator.user2());
-    assertThat(accountControlFactory.get().canSee(user.id)).isFalse();
+    requestScopeOperations.setApiUser(accountCreator.user2().id());
+    assertThat(accountControlFactory.get().canSee(user.id())).isFalse();
 
     PushOneCommit.Result r = createChange();
     RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
 
     ReviewInput in = new ReviewInput();
-    in.onBehalfOf = user.id.toString();
+    in.onBehalfOf = user.id().toString();
     in.label("Code-Review", 1);
 
     exception.expect(UnprocessableEntityException.class);
@@ -336,15 +331,15 @@
     String changeId = project.get() + "~master~" + r.getChangeId();
     gApi.changes().id(changeId).current().review(ReviewInput.approve());
     SubmitInput in = new SubmitInput();
-    in.onBehalfOf = admin2.email;
+    in.onBehalfOf = admin2.email();
     gApi.changes().id(changeId).current().submit(in);
 
     ChangeData cd = r.getChange();
-    assertThat(cd.change().getStatus()).isEqualTo(Change.Status.MERGED);
+    assertThat(cd.change().isMerged()).isTrue();
     PatchSetApproval submitter =
-        approvalsUtil.getSubmitter(db, cd.notes(), cd.change().currentPatchSetId());
-    assertThat(submitter.getAccountId()).isEqualTo(admin2.id);
-    assertThat(submitter.getRealAccountId()).isEqualTo(admin.id);
+        approvalsUtil.getSubmitter(cd.notes(), cd.change().currentPatchSetId());
+    assertThat(submitter.getAccountId()).isEqualTo(admin2.id());
+    assertThat(submitter.getRealAccountId()).isEqualTo(admin.id());
   }
 
   @Test
@@ -369,7 +364,7 @@
         .current()
         .review(ReviewInput.approve());
     SubmitInput in = new SubmitInput();
-    in.onBehalfOf = admin2.email;
+    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);
@@ -384,9 +379,9 @@
     String changeId = project.get() + "~master~" + r.getChangeId();
     gApi.changes().id(changeId).current().review(ReviewInput.approve());
     SubmitInput in = new SubmitInput();
-    in.onBehalfOf = user.email;
+    in.onBehalfOf = user.email();
     exception.expect(UnprocessableEntityException.class);
-    exception.expectMessage("on_behalf_of account " + user.id + " cannot see change");
+    exception.expectMessage("on_behalf_of account " + user.id() + " cannot see change");
     gApi.changes().id(changeId).current().submit(in);
   }
 
@@ -394,14 +389,14 @@
   @Test
   public void submitOnBehalfOfInvisibleUserNotAllowed() throws Exception {
     allowSubmitOnBehalfOf();
-    setApiUser(accountCreator.user2());
-    assertThat(accountControlFactory.get().canSee(user.id)).isFalse();
+    requestScopeOperations.setApiUser(accountCreator.user2().id());
+    assertThat(accountControlFactory.get().canSee(user.id())).isFalse();
 
     PushOneCommit.Result r = createChange();
     String changeId = project.get() + "~master~" + r.getChangeId();
     gApi.changes().id(changeId).current().review(ReviewInput.approve());
     SubmitInput in = new SubmitInput();
-    in.onBehalfOf = user.email;
+    in.onBehalfOf = user.email();
     exception.expect(UnprocessableEntityException.class);
     exception.expectMessage("not found");
     exception.expectMessage(in.onBehalfOf);
@@ -411,17 +406,17 @@
   @Test
   public void runAsValidUser() throws Exception {
     allowRunAs();
-    RestResponse res = adminRestSession.getWithHeader("/accounts/self", runAsHeader(user.id));
+    RestResponse res = adminRestSession.getWithHeader("/accounts/self", runAsHeader(user.id()));
     res.assertOK();
     AccountInfo account = newGson().fromJson(res.getEntityContent(), AccountInfo.class);
-    assertThat(account._accountId).isEqualTo(user.id.get());
+    assertThat(account._accountId).isEqualTo(user.id().get());
   }
 
   @GerritConfig(name = "auth.enableRunAs", value = "false")
   @Test
   public void runAsDisabledByConfig() throws Exception {
     allowRunAs();
-    RestResponse res = adminRestSession.getWithHeader("/changes/", runAsHeader(user.id));
+    RestResponse res = adminRestSession.getWithHeader("/changes/", runAsHeader(user.id()));
     res.assertForbidden();
     assertThat(res.getEntityContent())
         .isEqualTo("X-Gerrit-RunAs disabled by auth.enableRunAs = false");
@@ -429,7 +424,7 @@
 
   @Test
   public void runAsNotPermitted() throws Exception {
-    RestResponse res = adminRestSession.getWithHeader("/changes/", runAsHeader(user.id));
+    RestResponse res = adminRestSession.getWithHeader("/changes/", runAsHeader(user.id()));
     res.assertForbidden();
     assertThat(res.getEntityContent()).isEqualTo("not permitted to use X-Gerrit-RunAs");
   }
@@ -437,7 +432,7 @@
   @Test
   public void runAsNeverPermittedForAnonymousUsers() throws Exception {
     allowRunAs();
-    RestResponse res = anonRestSession.getWithHeader("/changes/", runAsHeader(user.id));
+    RestResponse res = anonRestSession.getWithHeader("/changes/", runAsHeader(user.id()));
     res.assertForbidden();
     assertThat(res.getEntityContent()).isEqualTo("not permitted to use X-Gerrit-RunAs");
   }
@@ -455,14 +450,14 @@
     allowRunAs();
     PushOneCommit.Result r = createChange();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     DraftInput di = new DraftInput();
     di.path = Patch.COMMIT_MSG;
     di.side = Side.REVISION;
     di.line = 1;
     di.message = "inline comment";
     gApi.changes().id(r.getChangeId()).current().createDraft(di);
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
 
     // Things that aren't allowed with on_behalf_of:
     //  - no labels.
@@ -472,19 +467,21 @@
     in.drafts = DraftHandling.PUBLISH;
     RestResponse res =
         adminRestSession.postWithHeader(
-            "/changes/" + r.getChangeId() + "/revisions/current/review", runAsHeader(user.id), in);
+            "/changes/" + r.getChangeId() + "/revisions/current/review",
+            runAsHeader(user.id()),
+            in);
     res.assertOK();
 
     ChangeMessageInfo m = Iterables.getLast(gApi.changes().id(r.getChangeId()).get().messages);
     assertThat(m.message).endsWith(in.message);
-    assertThat(m.author._accountId).isEqualTo(user.id.get());
+    assertThat(m.author._accountId).isEqualTo(user.id().get());
 
     CommentInfo c =
         Iterables.getOnlyElement(gApi.changes().id(r.getChangeId()).comments().get(di.path));
-    assertThat(c.author._accountId).isEqualTo(user.id.get());
+    assertThat(c.author._accountId).isEqualTo(user.id().get());
     assertThat(c.message).isEqualTo(di.message);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     assertThat(gApi.changes().id(r.getChangeId()).drafts()).isEmpty();
   }
 
@@ -500,30 +497,30 @@
 
     PushOneCommit.Result r = createChange();
     ReviewInput in = new ReviewInput();
-    in.onBehalfOf = user.id.toString();
+    in.onBehalfOf = user.id().toString();
     in.message = "Message on behalf of";
 
     String endpoint = "/changes/" + r.getChangeId() + "/revisions/current/review";
-    RestResponse res = adminRestSession.postWithHeader(endpoint, runAsHeader(user2.id), in);
+    RestResponse res = adminRestSession.postWithHeader(endpoint, runAsHeader(user2.id()), in);
     res.assertForbidden();
     assertThat(res.getEntityContent())
         .isEqualTo("label required to post review on behalf of \"" + in.onBehalfOf + '"');
 
     in.label("Code-Review", 1);
-    adminRestSession.postWithHeader(endpoint, runAsHeader(user2.id), in).assertOK();
+    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.getAccountId()).isEqualTo(user.id());
     assertThat(psa.getValue()).isEqualTo(1);
-    assertThat(psa.getRealAccountId()).isEqualTo(admin.id); // not user2
+    assertThat(psa.getRealAccountId()).isEqualTo(admin.id()); // not user2
 
     ChangeData cd = r.getChange();
-    ChangeMessage m = Iterables.getLast(cmUtil.byChange(db, cd.notes()));
+    ChangeMessage m = Iterables.getLast(cmUtil.byChange(cd.notes()));
     assertThat(m.getMessage()).endsWith(in.message);
-    assertThat(m.getAuthor()).isEqualTo(user.id);
-    assertThat(m.getRealAuthor()).isEqualTo(admin.id); // not user2
+    assertThat(m.getAuthor()).isEqualTo(user.id());
+    assertThat(m.getRealAuthor()).isEqualTo(admin.id()); // not user2
   }
 
   @Test
@@ -532,11 +529,11 @@
 
     PushOneCommit.Result r = createChange();
     ReviewInput in = new ReviewInput();
-    in.onBehalfOf = user.id.toString();
+    in.onBehalfOf = user.id().toString();
     in.message = "Message on behalf of";
     in.label("Code-Review", 1);
 
-    setApiUser(accountCreator.user2());
+    requestScopeOperations.setApiUser(accountCreator.user2().id());
     gApi.changes().id(r.getChangeId()).revision(r.getPatchSetId().getId()).review(in);
 
     ChangeInfo info = gApi.changes().id(r.getChangeId()).get(MESSAGES);
@@ -544,7 +541,8 @@
 
     ChangeMessageInfo changeMessageInfo = Iterables.getLast(info.messages);
     assertThat(changeMessageInfo.realAuthor).isNotNull();
-    assertThat(changeMessageInfo.realAuthor._accountId).isEqualTo(accountCreator.user2().id.get());
+    assertThat(changeMessageInfo.realAuthor._accountId)
+        .isEqualTo(accountCreator.user2().id().get());
   }
 
   private void allowCodeReviewOnBehalfOf() throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java b/javatests/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java
index ea71281..e05d0db 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java
@@ -27,7 +27,7 @@
     UsernameInput in = new UsernameInput();
     in.username = "myUsername";
     RestResponse r =
-        adminRestSession.put("/accounts/" + accountCreator.create().id.get() + "/username", in);
+        adminRestSession.put("/accounts/" + accountCreator.create().id().get() + "/username", in);
     r.assertOK();
     assertThat(newGson().fromJson(r.getReader(), String.class)).isEqualTo(in.username);
   }
@@ -35,9 +35,9 @@
   @Test
   public void setExisting_Conflict() throws Exception {
     UsernameInput in = new UsernameInput();
-    in.username = admin.username;
+    in.username = admin.username();
     adminRestSession
-        .put("/accounts/" + accountCreator.create().id.get() + "/username", in)
+        .put("/accounts/" + accountCreator.create().id().get() + "/username", in)
         .assertConflict();
   }
 
@@ -45,11 +45,13 @@
   public void setNew_MethodNotAllowed() throws Exception {
     UsernameInput in = new UsernameInput();
     in.username = "newUsername";
-    adminRestSession.put("/accounts/" + admin.username + "/username", in).assertMethodNotAllowed();
+    adminRestSession
+        .put("/accounts/" + admin.username() + "/username", in)
+        .assertMethodNotAllowed();
   }
 
   @Test
   public void delete_MethodNotAllowed() throws Exception {
-    adminRestSession.put("/accounts/" + admin.username + "/username").assertMethodNotAllowed();
+    adminRestSession.put("/accounts/" + admin.username() + "/username").assertMethodNotAllowed();
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java b/javatests/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java
index bc84593..d0c1fa4 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java
@@ -18,21 +18,27 @@
 
 import com.google.common.collect.Lists;
 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.extensions.client.ProjectWatchInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.inject.Inject;
 import java.util.ArrayList;
 import java.util.List;
 import org.junit.Test;
 
 public class WatchedProjectsIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   private static final String NEW_PROJECT_NAME = "newProjectAccess";
 
   @Test
   public void setAndGetWatchedProjects() throws Exception {
-    String projectName1 = createProject(NEW_PROJECT_NAME).get();
-    String projectName2 = createProject(NEW_PROJECT_NAME + "2").get();
+    String projectName1 = projectOperations.newProject().name(NEW_PROJECT_NAME).create().get();
+    String projectName2 =
+        projectOperations.newProject().name(NEW_PROJECT_NAME + "2").create().get();
 
     List<ProjectWatchInfo> projectsToWatch = new ArrayList<>(2);
 
@@ -52,13 +58,13 @@
 
     List<ProjectWatchInfo> persistedWatchedProjects =
         gApi.accounts().self().setWatchedProjects(projectsToWatch);
-    assertThat(persistedWatchedProjects).containsAllIn(projectsToWatch).inOrder();
+    assertThat(persistedWatchedProjects).containsAtLeastElementsIn(projectsToWatch).inOrder();
   }
 
   @Test
   public void setAndDeleteWatchedProjects() throws Exception {
-    String projectName1 = createProject(NEW_PROJECT_NAME).get();
-    String projectName2 = createProject(NEW_PROJECT_NAME + "2").get();
+    String projectName1 = projectOperations.newProject().create().get();
+    String projectName2 = projectOperations.newProject().create().get();
 
     List<ProjectWatchInfo> projectsToWatch = new ArrayList<>();
 
@@ -86,12 +92,12 @@
     List<ProjectWatchInfo> persistedWatchedProjects = gApi.accounts().self().getWatchedProjects();
 
     assertThat(persistedWatchedProjects).doesNotContain(pwi);
-    assertThat(persistedWatchedProjects).containsAllIn(projectsToWatch);
+    assertThat(persistedWatchedProjects).containsAtLeastElementsIn(projectsToWatch);
   }
 
   @Test
   public void setConflictingWatches() throws Exception {
-    String projectName = createProject(NEW_PROJECT_NAME).get();
+    String projectName = projectOperations.newProject().create().get();
 
     List<ProjectWatchInfo> projectsToWatch = new ArrayList<>();
 
@@ -115,7 +121,7 @@
 
   @Test
   public void setAndGetEmptyWatch() throws Exception {
-    String projectName = createProject(NEW_PROJECT_NAME).get();
+    String projectName = projectOperations.newProject().create().get();
 
     List<ProjectWatchInfo> projectsToWatch = new ArrayList<>();
 
@@ -125,7 +131,7 @@
 
     gApi.accounts().self().setWatchedProjects(projectsToWatch);
     List<ProjectWatchInfo> persistedWatchedProjects = gApi.accounts().self().getWatchedProjects();
-    assertThat(persistedWatchedProjects).containsAllIn(projectsToWatch);
+    assertThat(persistedWatchedProjects).containsAtLeastElementsIn(projectsToWatch);
   }
 
   @Test
@@ -150,7 +156,7 @@
     String projectName = project.get();
 
     // Let another user watch a project
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     List<ProjectWatchInfo> projectsToWatch = new ArrayList<>();
 
     ProjectWatchInfo pwi = new ProjectWatchInfo();
@@ -167,7 +173,7 @@
     gApi.accounts().self().deleteWatchedProjects(d);
 
     // Check that trying to delete a non-existing watch doesn't fail
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     gApi.accounts().self().deleteWatchedProjects(d);
   }
 
@@ -176,7 +182,7 @@
     String projectName = project.get();
 
     // Let another user watch a project
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     List<ProjectWatchInfo> projectsToWatch = new ArrayList<>();
 
     ProjectWatchInfo pwi = new ProjectWatchInfo();
@@ -199,7 +205,7 @@
 
     List<ProjectWatchInfo> watchedProjects = gApi.accounts().self().getWatchedProjects();
 
-    assertThat(watchedProjects).containsAllIn(projectsToWatch);
+    assertThat(watchedProjects).containsAtLeastElementsIn(projectsToWatch);
   }
 
   @Test
@@ -233,11 +239,11 @@
     List<ProjectWatchInfo> persistedWatchedProjects = gApi.accounts().self().getWatchedProjects();
 
     assertThat(persistedWatchedProjects).doesNotContain(pwi);
-    assertThat(persistedWatchedProjects).containsAllIn(projectsToWatch);
+    assertThat(persistedWatchedProjects).containsAtLeastElementsIn(projectsToWatch);
   }
 
   @Test
   public void postWithoutBody() throws Exception {
-    adminRestSession.post("/accounts/" + admin.username + "/watched.projects").assertOK();
+    adminRestSession.post("/accounts/" + admin.username() + "/watched.projects").assertOK();
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/AccountsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/AccountsRestApiBindingsIT.java
new file mode 100644
index 0000000..8e5eaa4
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/AccountsRestApiBindingsIT.java
@@ -0,0 +1,195 @@
+// 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.binding;
+
+import static com.google.gerrit.acceptance.rest.util.RestApiCallHelper.execute;
+import static com.google.gerrit.acceptance.rest.util.RestCall.Method.PUT;
+import static com.google.gerrit.gpg.testing.TestKeys.validKeyWithoutExpiration;
+import static org.apache.http.HttpStatus.SC_METHOD_NOT_ALLOWED;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GerritConfig;
+import com.google.gerrit.acceptance.UseSsh;
+import com.google.gerrit.acceptance.rest.util.RestCall;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.gpg.testing.TestKey;
+import com.google.gerrit.server.ServerInitiated;
+import com.google.gerrit.server.account.AccountsUpdate;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import org.junit.Test;
+
+/**
+ * Tests for checking the bindings of the accounts REST API.
+ *
+ * <p>These tests only verify that the account REST endpoints are correctly bound, they do no test
+ * the functionality of the account REST endpoints.
+ */
+public class AccountsRestApiBindingsIT extends AbstractDaemonTest {
+  @Inject private @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider;
+  @Inject private RequestScopeOperations requestScopeOperations;
+
+  /**
+   * Account REST endpoints to be tested, each URL contains a placeholder for the account
+   * identifier.
+   */
+  private static final ImmutableList<RestCall> ACCOUNT_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/accounts/%s"),
+          RestCall.put("/accounts/%s"),
+          RestCall.get("/accounts/%s/detail"),
+          RestCall.get("/accounts/%s/name"),
+          RestCall.put("/accounts/%s/name"),
+          RestCall.delete("/accounts/%s/name"),
+          RestCall.get("/accounts/%s/username"),
+          RestCall.builder(PUT, "/accounts/%s/username")
+              // Changing the username is not allowed.
+              .expectedResponseCode(SC_METHOD_NOT_ALLOWED)
+              .expectedMessage("Username cannot be changed.")
+              .build(),
+          RestCall.get("/accounts/%s/active"),
+          RestCall.put("/accounts/%s/active"),
+          RestCall.delete("/accounts/%s/active"),
+          RestCall.put("/accounts/%s/password.http"),
+          RestCall.delete("/accounts/%s/password.http"),
+          RestCall.get("/accounts/%s/status"),
+          RestCall.put("/accounts/%s/status"),
+          RestCall.get("/accounts/%s/avatar"),
+          RestCall.get("/accounts/%s/avatar.change.url"),
+          RestCall.get("/accounts/%s/emails/"),
+          RestCall.put("/accounts/%s/emails/new-email@foo.com"),
+          RestCall.get("/accounts/%s/sshkeys/"),
+          RestCall.post("/accounts/%s/sshkeys/"),
+          RestCall.get("/accounts/%s/watched.projects"),
+          RestCall.post("/accounts/%s/watched.projects"),
+          RestCall.post("/accounts/%s/watched.projects:delete"),
+          RestCall.get("/accounts/%s/groups"),
+          RestCall.get("/accounts/%s/preferences"),
+          RestCall.put("/accounts/%s/preferences"),
+          RestCall.get("/accounts/%s/preferences.diff"),
+          RestCall.put("/accounts/%s/preferences.diff"),
+          RestCall.get("/accounts/%s/preferences.edit"),
+          RestCall.put("/accounts/%s/preferences.edit"),
+          RestCall.get("/accounts/%s/starred.changes"),
+          RestCall.get("/accounts/%s/stars.changes"),
+          RestCall.post("/accounts/%s/index"),
+          RestCall.get("/accounts/%s/agreements"),
+          RestCall.put("/accounts/%s/agreements"),
+          RestCall.get("/accounts/%s/external.ids"),
+          RestCall.post("/accounts/%s/external.ids:delete"),
+          RestCall.post("/accounts/%s/drafts:delete"),
+          RestCall.get("/accounts/%s/oauthtoken"),
+          RestCall.get("/accounts/%s/capabilities"),
+          RestCall.get("/accounts/%s/capabilities/viewPlugins"),
+          RestCall.get("/accounts/%s/gpgkeys"),
+          RestCall.post("/accounts/%s/gpgkeys"));
+
+  /**
+   * Email REST endpoints to be tested, each URL contains a placeholders for the account and email
+   * identifier.
+   */
+  private static final ImmutableList<RestCall> EMAIL_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/accounts/%s/emails/%s"),
+          RestCall.put("/accounts/%s/emails/%s"),
+          RestCall.put("/accounts/%s/emails/%s/preferred"),
+
+          // email deletion must be tested last
+          RestCall.delete("/accounts/%s/emails/%s"));
+
+  /**
+   * GPG key REST endpoints to be tested, each URL contains a placeholders for the account
+   * identifier and the GPG key identifier.
+   */
+  private static final ImmutableList<RestCall> GPG_KEY_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/accounts/%s/gpgkeys/%s"),
+
+          // GPG key deletion must be tested last
+          RestCall.delete("/accounts/%s/gpgkeys/%s"));
+
+  /**
+   * SSH key REST endpoints to be tested, each URL contains a placeholders for the account and SSH
+   * key identifier.
+   */
+  private static final ImmutableList<RestCall> SSH_KEY_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/accounts/%s/sshkeys/%s"),
+
+          // SSH key deletion must be tested last
+          RestCall.delete("/accounts/%s/sshkeys/%s"));
+
+  /**
+   * Star REST endpoints to be tested, each URL contains a placeholders for the account and change
+   * identifier.
+   */
+  private static final ImmutableList<RestCall> STAR_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.put("/accounts/%s/starred.changes/%s"),
+          RestCall.delete("/accounts/%s/starred.changes/%s"),
+          RestCall.get("/accounts/%s/stars.changes/%s"),
+          RestCall.post("/accounts/%s/stars.changes/%s"));
+
+  @Test
+  public void accountEndpoints() throws Exception {
+    execute(adminRestSession, ACCOUNT_ENDPOINTS, "self");
+  }
+
+  @Test
+  public void emailEndpoints() throws Exception {
+    execute(adminRestSession, EMAIL_ENDPOINTS, "self", admin.email());
+  }
+
+  @Test
+  @GerritConfig(name = "receive.enableSignedPush", value = "true")
+  public void gpgKeyEndpoints() throws Exception {
+    TestKey key = validKeyWithoutExpiration();
+    String id = key.getKeyIdString();
+
+    String email = "test1@example.com"; // email that is hard-coded in the test GPG key
+    accountsUpdateProvider
+        .get()
+        .update(
+            "Add Email",
+            admin.id(),
+            u ->
+                u.addExternalId(
+                    ExternalId.createWithEmail(name("test"), email, admin.id(), email)));
+
+    requestScopeOperations.setApiUser(admin.id());
+    gApi.accounts()
+        .self()
+        .putGpgKeys(ImmutableList.of(key.getPublicKeyArmored()), ImmutableList.of());
+
+    execute(adminRestSession, GPG_KEY_ENDPOINTS, "self", id);
+  }
+
+  @Test
+  @UseSsh
+  public void sshKeyEndpoints() throws Exception {
+    String sshKeySeq = Integer.toString(gApi.accounts().self().listSshKeys().size());
+    execute(adminRestSession, SSH_KEY_ENDPOINTS, "self", sshKeySeq);
+  }
+
+  @Test
+  public void starEndpoints() throws Exception {
+    ChangeInput ci = new ChangeInput(project.get(), "master", "Test change");
+    String changeId = gApi.changes().create(ci).get().id;
+    execute(adminRestSession, STAR_ENDPOINTS, "self", changeId);
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/BUILD b/javatests/com/google/gerrit/acceptance/rest/binding/BUILD
new file mode 100644
index 0000000..e4242a9
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/BUILD
@@ -0,0 +1,11 @@
+load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
+
+acceptance_tests(
+    srcs = glob(["*IT.java"]),
+    group = "rest_bindings",
+    labels = ["rest"],
+    deps = [
+        "//java/com/google/gerrit/server/logging",
+        "//javatests/com/google/gerrit/acceptance/rest/util",
+    ],
+)
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
new file mode 100644
index 0000000..55744cc
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
@@ -0,0 +1,498 @@
+// 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.binding;
+
+import static com.google.gerrit.acceptance.rest.util.RestCall.Method.GET;
+import static com.google.gerrit.extensions.common.testing.RobotCommentInfoSubject.assertThatList;
+import static java.util.stream.Collectors.toList;
+import static org.apache.http.HttpStatus.SC_NOT_FOUND;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+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.extensions.api.changes.AddReviewerInput;
+import com.google.gerrit.extensions.api.changes.DraftInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
+import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
+import com.google.gerrit.extensions.client.Comment;
+import com.google.gerrit.extensions.client.Side;
+import com.google.gerrit.extensions.common.CommentInfo;
+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;
+import org.junit.Test;
+
+/**
+ * Tests for checking the bindings of the changes REST API.
+ *
+ * <p>These tests only verify that the change REST endpoints are correctly bound, they do no test
+ * the functionality of the change REST endpoints.
+ */
+public class ChangesRestApiBindingsIT extends AbstractDaemonTest {
+  /**
+   * Change REST endpoints to be tested, each URL contains a placeholder for the change identifier.
+   */
+  private static final ImmutableList<RestCall> CHANGE_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/changes/%s"),
+          RestCall.get("/changes/%s/detail"),
+          RestCall.get("/changes/%s/topic"),
+          RestCall.put("/changes/%s/topic"),
+          RestCall.delete("/changes/%s/topic"),
+          RestCall.get("/changes/%s/in"),
+          RestCall.get("/changes/%s/hashtags"),
+          RestCall.get("/changes/%s/comments"),
+          RestCall.get("/changes/%s/robotcomments"),
+          RestCall.get("/changes/%s/drafts"),
+          RestCall.get("/changes/%s/assignee"),
+          RestCall.get("/changes/%s/past_assignees"),
+          RestCall.put("/changes/%s/assignee"),
+          RestCall.delete("/changes/%s/assignee"),
+          RestCall.post("/changes/%s/private"),
+          RestCall.post("/changes/%s/private.delete"),
+          RestCall.delete("/changes/%s/private"),
+          RestCall.post("/changes/%s/wip"),
+          RestCall.post("/changes/%s/ready"),
+          RestCall.put("/changes/%s/ignore"),
+          RestCall.put("/changes/%s/unignore"),
+          RestCall.put("/changes/%s/reviewed"),
+          RestCall.put("/changes/%s/unreviewed"),
+          RestCall.get("/changes/%s/messages"),
+          RestCall.put("/changes/%s/message"),
+          RestCall.post("/changes/%s/merge"),
+          RestCall.post("/changes/%s/abandon"),
+          RestCall.post("/changes/%s/move"),
+          RestCall.post("/changes/%s/rebase"),
+          RestCall.post("/changes/%s/restore"),
+          RestCall.post("/changes/%s/revert"),
+          RestCall.get("/changes/%s/pure_revert"),
+          RestCall.post("/changes/%s/submit"),
+          RestCall.get("/changes/%s/submitted_together"),
+          RestCall.post("/changes/%s/index"),
+          RestCall.get("/changes/%s/check"),
+          RestCall.post("/changes/%s/check"),
+          RestCall.get("/changes/%s/reviewers"),
+          RestCall.post("/changes/%s/reviewers"),
+          RestCall.get("/changes/%s/suggest_reviewers"),
+          RestCall.builder(GET, "/changes/%s/revisions")
+              // GET /changes/<change-id>/revisions is not implemented
+              .expectedResponseCode(SC_NOT_FOUND)
+              .build(),
+          RestCall.get("/changes/%s/edit"),
+          RestCall.post("/changes/%s/edit"),
+          RestCall.post("/changes/%s/edit:rebase"),
+          RestCall.get("/changes/%s/edit:message"),
+          RestCall.put("/changes/%s/edit:message"),
+
+          // Publish edit and create a new edit
+          RestCall.post("/changes/%s/edit:publish"),
+          RestCall.put("/changes/%s/edit/a.txt"),
+
+          // Deletion of change edit and change must be tested last
+          RestCall.delete("/changes/%s/edit"),
+          RestCall.delete("/changes/%s"));
+
+  /**
+   * Reviewer REST endpoints to be tested, each URL contains placeholders for the change identifier
+   * and the reviewer identifier.
+   */
+  private static final ImmutableList<RestCall> REVIEWER_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/changes/%s/reviewers/%s"),
+          RestCall.get("/changes/%s/reviewers/%s/votes"),
+          RestCall.post("/changes/%s/reviewers/%s/delete"),
+          RestCall.delete("/changes/%s/reviewers/%s"));
+
+  /**
+   * Vote REST endpoints to be tested, each URL contains placeholders for the change identifier, the
+   * reviewer identifier and the label identifier.
+   */
+  private static final ImmutableList<RestCall> VOTE_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.post("/changes/%s/reviewers/%s/votes/%s/delete"),
+          RestCall.delete("/changes/%s/reviewers/%s/votes/%s"));
+
+  /**
+   * Revision REST endpoints to be tested, each URL contains placeholders for the change identifier
+   * and the revision identifier.
+   */
+  private static final ImmutableList<RestCall> REVISION_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/changes/%s/revisions/%s/actions"),
+          RestCall.post("/changes/%s/revisions/%s/cherrypick"),
+          RestCall.get("/changes/%s/revisions/%s/commit"),
+          RestCall.get("/changes/%s/revisions/%s/mergeable"),
+          RestCall.get("/changes/%s/revisions/%s/related"),
+          RestCall.get("/changes/%s/revisions/%s/review"),
+          RestCall.post("/changes/%s/revisions/%s/review"),
+          RestCall.get("/changes/%s/revisions/%s/preview_submit"),
+          RestCall.post("/changes/%s/revisions/%s/submit"),
+          RestCall.get("/changes/%s/revisions/%s/submit_type"),
+          RestCall.post("/changes/%s/revisions/%s/test.submit_rule"),
+          RestCall.post("/changes/%s/revisions/%s/test.submit_type"),
+          RestCall.post("/changes/%s/revisions/%s/rebase"),
+          RestCall.get("/changes/%s/revisions/%s/description"),
+          RestCall.put("/changes/%s/revisions/%s/description"),
+          RestCall.get("/changes/%s/revisions/%s/patch"),
+          RestCall.get("/changes/%s/revisions/%s/archive"),
+          RestCall.get("/changes/%s/revisions/%s/mergelist"),
+          RestCall.get("/changes/%s/revisions/%s/reviewers"),
+          RestCall.get("/changes/%s/revisions/%s/drafts"),
+          RestCall.put("/changes/%s/revisions/%s/drafts"),
+          RestCall.get("/changes/%s/revisions/%s/comments"),
+          RestCall.get("/changes/%s/revisions/%s/robotcomments"),
+          RestCall.builder(GET, "/changes/%s/revisions/%s/fixes")
+              // GET /changes/<change>/revisions/<revision>/fixes is not implemented
+              .expectedResponseCode(SC_NOT_FOUND)
+              .build(),
+          RestCall.get("/changes/%s/revisions/%s/files"));
+
+  /**
+   * Revision reviewer REST endpoints to be tested, each URL contains placeholders for the change
+   * identifier, the revision identifier and the reviewer identifier.
+   */
+  private static final ImmutableList<RestCall> REVISION_REVIEWER_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/changes/%s/revisions/%s/reviewers/%s"),
+          RestCall.get("/changes/%s/revisions/%s/reviewers/%s/votes"),
+          RestCall.post("/changes/%s/revisions/%s/reviewers/%s/delete"),
+          RestCall.delete("/changes/%s/revisions/%s/reviewers/%s"));
+
+  /**
+   * Revision vote REST endpoints to be tested, each URL contains placeholders for the change
+   * identifier, the revision identifier, the reviewer identifier and the label identifier.
+   */
+  private static final ImmutableList<RestCall> REVISION_VOTE_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.post("/changes/%s/revisions/%s/reviewers/%s/votes/%s/delete"),
+          RestCall.delete("/changes/%s/revisions/%s/reviewers/%s/votes/%s"));
+
+  /**
+   * Draft comment REST endpoints to be tested, each URL contains placeholders for the change
+   * identifier, the revision identifier and the draft comment identifier.
+   */
+  private static final ImmutableList<RestCall> DRAFT_COMMENT_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/changes/%s/revisions/%s/drafts/%s"),
+          RestCall.put("/changes/%s/revisions/%s/drafts/%s"),
+          RestCall.delete("/changes/%s/revisions/%s/drafts/%s"));
+
+  /**
+   * Comment REST endpoints to be tested, each URL contains placeholders for the change identifier,
+   * the revision identifier and the comment identifier.
+   */
+  private static final ImmutableList<RestCall> COMMENT_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/changes/%s/revisions/%s/comments/%s"),
+          RestCall.delete("/changes/%s/revisions/%s/comments/%s"),
+          RestCall.post("/changes/%s/revisions/%s/comments/%s/delete"));
+
+  /**
+   * Robot comment REST endpoints to be tested, each URL contains placeholders for the change
+   * identifier, the revision identifier and the robot comment identifier.
+   */
+  private static final ImmutableList<RestCall> ROBOT_COMMENT_ENDPOINTS =
+      ImmutableList.of(RestCall.get("/changes/%s/revisions/%s/robotcomments/%s"));
+
+  /**
+   * Fix REST endpoints to be tested, each URL contains placeholders for the change identifier, the
+   * revision identifier and the fix identifier.
+   */
+  private static final ImmutableList<RestCall> FIX_ENDPOINTS =
+      ImmutableList.of(RestCall.post("/changes/%s/revisions/%s/fixes/%s/apply"));
+
+  /**
+   * Revision file REST endpoints to be tested, each URL contains placeholders for the change
+   * identifier, the revision identifier and the file identifier.
+   */
+  private static final ImmutableList<RestCall> REVISION_FILE_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.put("/changes/%s/revisions/%s/files/%s/reviewed"),
+          RestCall.delete("/changes/%s/revisions/%s/files/%s/reviewed"),
+          RestCall.get("/changes/%s/revisions/%s/files/%s/content"),
+          RestCall.get("/changes/%s/revisions/%s/files/%s/download"),
+          RestCall.get("/changes/%s/revisions/%s/files/%s/diff"),
+          RestCall.get("/changes/%s/revisions/%s/files/%s/blame"));
+
+  /**
+   * Change message REST endpoints to be tested, each URL contains placeholders for the change
+   * identifier and the change message identifier.
+   */
+  private static final ImmutableList<RestCall> CHANGE_MESSAGE_ENDPOINTS =
+      ImmutableList.of(RestCall.get("/changes/%s/messages/%s"));
+
+  /**
+   * Change edit REST endpoints that create an edit to be tested, each URL contains placeholders for
+   * the change identifier and the change edit identifier.
+   */
+  private static final ImmutableList<RestCall> CHANGE_EDIT_CREATE_ENDPOINTS =
+      ImmutableList.of(
+          // Create change edit by editing an existing file.
+          RestCall.put("/changes/%s/edit/%s"),
+
+          // Create change edit by deleting an existing file.
+          RestCall.delete("/changes/%s/edit/%s"));
+
+  /**
+   * Change edit REST endpoints to be tested, each URL contains placeholders for the change
+   * identifier and the change edit identifier.
+   */
+  private static final ImmutableList<RestCall> CHANGE_EDIT_ENDPOINTS =
+      ImmutableList.of(
+          // Calls on existing change edit.
+          RestCall.get("/changes/%s/edit/%s"),
+          RestCall.put("/changes/%s/edit/%s"),
+          RestCall.get("/changes/%s/edit/%s/meta"),
+
+          // Delete content of a file in an existing change edit.
+          RestCall.delete("/changes/%s/edit/%s"));
+
+  private static final String FILENAME = "test.txt";
+
+  @Test
+  public void changeEndpoints() throws Exception {
+    String changeId = createChange().getChangeId();
+    gApi.changes().id(changeId).edit().create();
+    RestApiCallHelper.execute(adminRestSession, CHANGE_ENDPOINTS, changeId);
+  }
+
+  @Test
+  public void reviewerEndpoints() throws Exception {
+    String changeId = createChange().getChangeId();
+
+    AddReviewerInput addReviewerInput = new AddReviewerInput();
+    addReviewerInput.reviewer = user.email();
+
+    RestApiCallHelper.execute(
+        adminRestSession,
+        REVIEWER_ENDPOINTS,
+        () -> gApi.changes().id(changeId).addReviewer(addReviewerInput),
+        changeId,
+        addReviewerInput.reviewer);
+  }
+
+  @Test
+  public void voteEndpoints() throws Exception {
+    String changeId = createChange().getChangeId();
+
+    RestApiCallHelper.execute(
+        adminRestSession,
+        VOTE_ENDPOINTS,
+        () -> gApi.changes().id(changeId).current().review(ReviewInput.approve()),
+        changeId,
+        admin.email(),
+        "Code-Review");
+  }
+
+  @Test
+  public void revisionEndpoints() throws Exception {
+    String changeId = createChange().getChangeId();
+    RestApiCallHelper.execute(adminRestSession, REVISION_ENDPOINTS, changeId, "current");
+  }
+
+  @Test
+  public void revisionReviewerEndpoints() throws Exception {
+    String changeId = createChange().getChangeId();
+
+    AddReviewerInput addReviewerInput = new AddReviewerInput();
+    addReviewerInput.reviewer = user.email();
+
+    RestApiCallHelper.execute(
+        adminRestSession,
+        REVISION_REVIEWER_ENDPOINTS,
+        () -> gApi.changes().id(changeId).addReviewer(addReviewerInput),
+        changeId,
+        "current",
+        addReviewerInput.reviewer);
+  }
+
+  @Test
+  public void revisionVoteEndpoints() throws Exception {
+    String changeId = createChange().getChangeId();
+
+    RestApiCallHelper.execute(
+        adminRestSession,
+        REVISION_VOTE_ENDPOINTS,
+        () -> gApi.changes().id(changeId).current().review(ReviewInput.approve()),
+        changeId,
+        "current",
+        admin.email(),
+        "Code-Review");
+  }
+
+  @Test
+  public void draftCommentEndpoints() throws Exception {
+    String changeId = createChange().getChangeId();
+
+    for (RestCall restCall : DRAFT_COMMENT_ENDPOINTS) {
+      DraftInput draftInput = new DraftInput();
+      draftInput.path = Patch.COMMIT_MSG;
+      draftInput.side = Side.REVISION;
+      draftInput.line = 1;
+      draftInput.message = "draft comment";
+      CommentInfo draftInfo = gApi.changes().id(changeId).current().createDraft(draftInput).get();
+
+      RestApiCallHelper.execute(adminRestSession, restCall, changeId, "current", draftInfo.id);
+    }
+  }
+
+  @Test
+  public void commentEndpoints() throws Exception {
+    String changeId = createChange().getChangeId();
+
+    for (RestCall restCall : COMMENT_ENDPOINTS) {
+      DraftInput draftInput = new DraftInput();
+      draftInput.path = Patch.COMMIT_MSG;
+      draftInput.side = Side.REVISION;
+      draftInput.line = 1;
+      draftInput.message = "draft comment";
+      CommentInfo commentInfo = gApi.changes().id(changeId).current().createDraft(draftInput).get();
+
+      ReviewInput reviewInput = new ReviewInput();
+      reviewInput.drafts = DraftHandling.PUBLISH;
+      gApi.changes().id(changeId).current().review(reviewInput);
+
+      RestApiCallHelper.execute(adminRestSession, restCall, changeId, "current", commentInfo.id);
+    }
+  }
+
+  @Test
+  public void robotCommentEndpoints() throws Exception {
+    String changeId = createChange().getChangeId();
+
+    RobotCommentInput robotCommentInput = new RobotCommentInput();
+    robotCommentInput.robotId = "happyRobot";
+    robotCommentInput.robotRunId = "1";
+    robotCommentInput.line = 1;
+    robotCommentInput.message = "nit: trailing whitespace";
+    robotCommentInput.path = Patch.COMMIT_MSG;
+
+    ReviewInput reviewInput = new ReviewInput();
+    reviewInput.robotComments =
+        Collections.singletonMap(robotCommentInput.path, ImmutableList.of(robotCommentInput));
+    reviewInput.message = "robot comment test";
+    gApi.changes().id(changeId).current().review(reviewInput);
+
+    List<RobotCommentInfo> robotCommentInfos =
+        gApi.changes().id(changeId).current().robotCommentsAsList();
+    RobotCommentInfo robotCommentInfo = Iterables.getOnlyElement(robotCommentInfos);
+
+    RestApiCallHelper.execute(
+        adminRestSession, ROBOT_COMMENT_ENDPOINTS, changeId, "current", robotCommentInfo.id);
+  }
+
+  @Test
+  public void fixEndpoints() throws Exception {
+    String changeId = createChange("Subject", FILENAME, "content").getChangeId();
+
+    RobotCommentInput robotCommentInput = new RobotCommentInput();
+    robotCommentInput.robotId = "happyRobot";
+    robotCommentInput.robotRunId = "1";
+    robotCommentInput.line = 1;
+    robotCommentInput.message = "nit: trailing whitespace";
+    robotCommentInput.path = FILENAME;
+
+    FixReplacementInfo fixReplacementInfo = new FixReplacementInfo();
+    fixReplacementInfo.path = FILENAME;
+    fixReplacementInfo.replacement = "some replacement code";
+    fixReplacementInfo.range = createRange(1, 1, 1, 2);
+
+    FixSuggestionInfo fixSuggestionInfo = new FixSuggestionInfo();
+    fixSuggestionInfo.fixId = "An ID which must be overwritten.";
+    fixSuggestionInfo.description = "A description for a suggested fix.";
+    fixSuggestionInfo.replacements = ImmutableList.of(fixReplacementInfo);
+
+    robotCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo);
+
+    ReviewInput reviewInput = new ReviewInput();
+    reviewInput.robotComments =
+        Collections.singletonMap(robotCommentInput.path, ImmutableList.of(robotCommentInput));
+    reviewInput.message = "robot comment test";
+    gApi.changes().id(changeId).current().review(reviewInput);
+
+    List<RobotCommentInfo> robotCommentInfos =
+        gApi.changes().id(changeId).current().robotCommentsAsList();
+
+    List<String> fixIds = getFixIds(robotCommentInfos);
+    String fixId = Iterables.getOnlyElement(fixIds);
+
+    RestApiCallHelper.execute(adminRestSession, FIX_ENDPOINTS, changeId, "current", fixId);
+  }
+
+  @Test
+  public void revisionFileEndpoints() throws Exception {
+    String changeId = createChange("Subject", FILENAME, "content").getChangeId();
+    RestApiCallHelper.execute(
+        adminRestSession, REVISION_FILE_ENDPOINTS, changeId, "current", FILENAME);
+  }
+
+  @Test
+  public void changeMessageEndpoints() throws Exception {
+    String changeId = createChange().getChangeId();
+
+    // A change message is created on change creation.
+    String changeMessageId = Iterables.getOnlyElement(gApi.changes().id(changeId).messages()).id;
+
+    RestApiCallHelper.execute(
+        adminRestSession, CHANGE_MESSAGE_ENDPOINTS, changeId, changeMessageId);
+  }
+
+  @Test
+  public void changeEditCreateEndpoints() throws Exception {
+    String changeId = createChange("Subject", FILENAME, "content").getChangeId();
+
+    // Each of the REST calls creates the change edit newly.
+    RestApiCallHelper.execute(
+        adminRestSession,
+        CHANGE_EDIT_CREATE_ENDPOINTS,
+        () -> adminRestSession.delete("/changes/" + changeId + "/edit"),
+        changeId,
+        FILENAME);
+  }
+
+  @Test
+  public void changeEditEndpoints() throws Exception {
+    String changeId = createChange("Subject", FILENAME, "content").getChangeId();
+    gApi.changes().id(changeId).edit().create();
+    RestApiCallHelper.execute(adminRestSession, CHANGE_EDIT_ENDPOINTS, changeId, FILENAME);
+  }
+
+  private static Comment.Range createRange(
+      int startLine, int startCharacter, int endLine, int endCharacter) {
+    Comment.Range range = new Comment.Range();
+    range.startLine = startLine;
+    range.startCharacter = startCharacter;
+    range.endLine = endLine;
+    range.endCharacter = endCharacter;
+    return range;
+  }
+
+  private static List<String> getFixIds(List<RobotCommentInfo> robotComments) {
+    assertThatList(robotComments).isNotNull();
+    return robotComments.stream()
+        .map(robotCommentInfo -> robotCommentInfo.fixSuggestions)
+        .filter(Objects::nonNull)
+        .flatMap(List::stream)
+        .map(fixSuggestionInfo -> fixSuggestionInfo.fixId)
+        .collect(toList());
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ConfigRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ConfigRestApiBindingsIT.java
new file mode 100644
index 0000000..5f210cc
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ConfigRestApiBindingsIT.java
@@ -0,0 +1,113 @@
+// 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.binding;
+
+import static com.google.common.truth.Truth8.assertThat;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+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.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 java.util.List;
+import java.util.Optional;
+import org.junit.Test;
+
+/**
+ * Tests for checking the bindings of the config REST API.
+ *
+ * <p>These tests only verify that the config REST endpoints are correctly bound, they do no test
+ * the functionality of the config REST endpoints.
+ */
+public class ConfigRestApiBindingsIT extends AbstractDaemonTest {
+  /**
+   * Config REST endpoints to be tested, the URLs contain no placeholders since the only supported
+   * config identifier ('server') can be hard-coded.
+   */
+  private static final ImmutableList<RestCall> CONFIG_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/config/server/version"),
+          RestCall.get("/config/server/info"),
+          RestCall.get("/config/server/preferences"),
+          RestCall.put("/config/server/preferences"),
+          RestCall.get("/config/server/preferences.diff"),
+          RestCall.put("/config/server/preferences.diff"),
+          RestCall.get("/config/server/preferences.edit"),
+          RestCall.put("/config/server/preferences.edit"),
+          RestCall.get("/config/server/top-menus"),
+          RestCall.put("/config/server/email.confirm"),
+          RestCall.post("/config/server/check.consistency"),
+          RestCall.post("/config/server/reload"),
+          RestCall.get("/config/server/summary"),
+          RestCall.get("/config/server/capabilities"),
+          RestCall.get("/config/server/caches"),
+          RestCall.post("/config/server/caches"),
+          RestCall.get("/config/server/tasks"),
+          RestCall.post("/config/server/index.changes"));
+
+  /**
+   * Cache REST endpoints to be tested, the URLs contain a placeholder for the cache identifier.
+   * Since there is only supported a single supported config identifier ('server') it can be
+   * hard-coded.
+   */
+  private static final ImmutableList<RestCall> CACHE_ENDPOINTS =
+      ImmutableList.of(RestCall.get("/config/server/caches/%s"));
+
+  /**
+   * Task REST endpoints to be tested, the URLs contain a placeholder for the task identifier. Since
+   * there is only supported a single supported config identifier ('server') it can be hard-coded.
+   */
+  private static final ImmutableList<RestCall> TASK_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/config/server/tasks/%s"),
+
+          // Task deletion must be tested last
+          RestCall.delete("/config/server/tasks/%s"));
+
+  @Test
+  public void configEndpoints() throws Exception {
+    // 'Access Database' is needed for the '/config/server/check.consistency' REST endpoint
+    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+
+    RestApiCallHelper.execute(adminRestSession, CONFIG_ENDPOINTS);
+  }
+
+  @Test
+  public void cacheEndpoints() throws Exception {
+    RestApiCallHelper.execute(adminRestSession, CACHE_ENDPOINTS, ProjectCacheImpl.CACHE_NAME);
+  }
+
+  @Test
+  public void taskEndpoints() throws Exception {
+    RestResponse r = adminRestSession.get("/config/server/tasks/");
+    List<TaskInfo> result =
+        newGson().fromJson(r.getReader(), new TypeToken<List<TaskInfo>>() {}.getType());
+    r.consume();
+
+    Optional<String> id =
+        result.stream()
+            .filter(t -> "Log File Compressor".equals(t.command))
+            .map(t -> t.id)
+            .findFirst();
+    assertThat(id).isPresent();
+
+    RestApiCallHelper.execute(adminRestSession, TASK_ENDPOINTS, id.get());
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/GroupsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/GroupsRestApiBindingsIT.java
new file mode 100644
index 0000000..bb12172
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/GroupsRestApiBindingsIT.java
@@ -0,0 +1,102 @@
+// 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.binding;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.rest.util.RestApiCallHelper;
+import com.google.gerrit.acceptance.rest.util.RestCall;
+import org.junit.Test;
+
+/**
+ * Tests for checking the bindings of the groups REST API.
+ *
+ * <p>These tests only verify that the group REST endpoints are correctly bound, they do no test the
+ * functionality of the group REST endpoints.
+ */
+public class GroupsRestApiBindingsIT extends AbstractDaemonTest {
+  /**
+   * Group REST endpoints to be tested, each URL contains a placeholder for the group identifier.
+   */
+  private static final ImmutableList<RestCall> GROUP_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/groups/%s"),
+          RestCall.put("/groups/%s"),
+          RestCall.get("/groups/%s/detail"),
+          RestCall.get("/groups/%s/name"),
+          RestCall.put("/groups/%s/name"),
+          RestCall.get("/groups/%s/description"),
+          RestCall.put("/groups/%s/description"),
+          RestCall.delete("/groups/%s/description"),
+          RestCall.get("/groups/%s/owner"),
+          RestCall.put("/groups/%s/owner"),
+          RestCall.get("/groups/%s/options"),
+          RestCall.put("/groups/%s/options"),
+          RestCall.post("/groups/%s/members"),
+          RestCall.post("/groups/%s/members.add"),
+          RestCall.post("/groups/%s/members.delete"),
+          RestCall.post("/groups/%s/groups"),
+          RestCall.post("/groups/%s/groups.add"),
+          RestCall.post("/groups/%s/groups.delete"),
+          RestCall.get("/groups/%s/log.audit"),
+          RestCall.post("/groups/%s/index"),
+          RestCall.get("/groups/%s/members"),
+          RestCall.get("/groups/%s/groups"));
+
+  /**
+   * Member REST endpoints to be tested, each URL contains placeholders for the group identifier and
+   * the member identifier.
+   */
+  private static final ImmutableList<RestCall> MEMBER_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/groups/%s/members/%s"),
+          RestCall.put("/groups/%s/members/%s"),
+
+          // Member deletion must be tested last
+          RestCall.delete("/groups/%s/members/%s"));
+
+  /**
+   * Subgroup REST endpoints to be tested, each URL contains placeholders for the group identifier
+   * and the subgroup identifier.
+   */
+  private static final ImmutableList<RestCall> SUBGROUP_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/groups/%s/groups/%s"),
+          RestCall.put("/groups/%s/groups/%s"),
+
+          // Subgroup deletion must be tested last
+          RestCall.delete("/groups/%s/groups/%s"));
+
+  @Test
+  public void groupEndpoints() throws Exception {
+    String group = gApi.groups().create("test-group").get().name;
+    RestApiCallHelper.execute(adminRestSession, GROUP_ENDPOINTS, group);
+  }
+
+  @Test
+  public void memberEndpoints() throws Exception {
+    String group = gApi.groups().create("test-group").get().name;
+    gApi.groups().id(group).addMembers(admin.email());
+    RestApiCallHelper.execute(adminRestSession, MEMBER_ENDPOINTS, group, admin.email());
+  }
+
+  @Test
+  public void subgroupEndpoints() throws Exception {
+    String group = gApi.groups().create("test-group").get().name;
+    String subgroup = gApi.groups().create("test-subgroup").get().name;
+    gApi.groups().id(group).addGroups(subgroup);
+    RestApiCallHelper.execute(adminRestSession, SUBGROUP_ENDPOINTS, group, subgroup);
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedChildRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedChildRestApiBindingsIT.java
new file mode 100644
index 0000000..27df565
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedChildRestApiBindingsIT.java
@@ -0,0 +1,160 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.binding;
+
+import static com.google.gerrit.server.change.RevisionResource.REVISION_KIND;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+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.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestApiModule;
+import com.google.gerrit.extensions.restapi.RestCollectionModifyView;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+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.change.RevisionResource;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import org.junit.Test;
+
+/**
+ * Tests for checking plugin-provided REST API bindings nested under a core collection.
+ *
+ * <p>These tests only verify that the plugin-provided REST endpoints are correctly bound, they do
+ * not test the functionality of the plugin REST endpoints.
+ */
+public class PluginProvidedChildRestApiBindingsIT extends AbstractDaemonTest {
+
+  /** Resource to bind a child collection. */
+  public static final TypeLiteral<RestView<TestPluginResource>> TEST_KIND =
+      new TypeLiteral<RestView<TestPluginResource>>() {};
+
+  private static final String PLUGIN_NAME = "my-plugin";
+
+  private static final ImmutableSet<RestCall> 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/"),
+          RestCall.get("/changes/%s/revisions/%s/" + PLUGIN_NAME + "~test-collection/1/detail"),
+          RestCall.post("/changes/%s/revisions/%s/" + PLUGIN_NAME + "~test-collection/"),
+          RestCall.post("/changes/%s/revisions/%s/" + PLUGIN_NAME + "~test-collection/1/update"),
+          // Same tests but without the plugin name as part of the collection name. This works as
+          // long as there is no core collection with the same name (which takes precedence) and no
+          // other plugin binds a collection with the same name. We highly encourage plugin authors
+          // to use the fully qualified collection name instead.
+          RestCall.get("/changes/%s/revisions/%s/test-collection/"),
+          RestCall.get("/changes/%s/revisions/%s/test-collection/1/detail"),
+          RestCall.post("/changes/%s/revisions/%s/test-collection/"),
+          RestCall.post("/changes/%s/revisions/%s/test-collection/1/update"));
+
+  /**
+   * Module for all sys bindings.
+   *
+   * <p>TODO: This should actually just move into MyPluginHttpModule. However, that doesn't work
+   * currently. This TODO is for fixing this bug.
+   */
+  static class MyPluginSysModule extends AbstractModule {
+    @Override
+    public void configure() {
+      install(
+          new RestApiModule() {
+            @Override
+            public void configure() {
+              DynamicMap.mapOf(binder(), TEST_KIND);
+              child(REVISION_KIND, "test-collection").to(TestChildCollection.class);
+
+              postOnCollection(TEST_KIND).to(TestPostOnCollection.class);
+              post(TEST_KIND, "update").to(TestPost.class);
+              get(TEST_KIND, "detail").to(TestGet.class);
+            }
+          });
+    }
+  }
+
+  static class TestPluginResource implements RestResource {}
+
+  @Singleton
+  static class TestChildCollection
+      implements ChildCollection<RevisionResource, TestPluginResource> {
+    private final DynamicMap<RestView<TestPluginResource>> views;
+
+    @Inject
+    TestChildCollection(DynamicMap<RestView<TestPluginResource>> views) {
+      this.views = views;
+    }
+
+    @Override
+    public RestView<RevisionResource> list() throws RestApiException {
+      return (RestReadView<RevisionResource>) resource -> ImmutableList.of("one", "two");
+    }
+
+    @Override
+    public TestPluginResource parse(RevisionResource parent, IdString id) throws Exception {
+      return new TestPluginResource();
+    }
+
+    @Override
+    public DynamicMap<RestView<TestPluginResource>> views() {
+      return views;
+    }
+  }
+
+  @Singleton
+  static class TestPostOnCollection
+      implements RestCollectionModifyView<RevisionResource, TestPluginResource, String> {
+    @Override
+    public Object apply(RevisionResource parentResource, String input) throws Exception {
+      return "test";
+    }
+  }
+
+  @Singleton
+  static class TestPost implements RestModifyView<TestPluginResource, String> {
+    @Override
+    public String apply(TestPluginResource resource, String input) throws Exception {
+      return "test";
+    }
+  }
+
+  @Singleton
+  static class TestGet implements RestReadView<TestPluginResource> {
+    @Override
+    public String apply(TestPluginResource resource) throws Exception {
+      return "test";
+    }
+  }
+
+  @Test
+  public void testEndpoints() 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));
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedRootRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedRootRestApiBindingsIT.java
new file mode 100644
index 0000000..178a326
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedRootRestApiBindingsIT.java
@@ -0,0 +1,205 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.binding;
+
+import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+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.acceptance.rest.util.RestCall.Method;
+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.RestApiException;
+import com.google.gerrit.extensions.restapi.RestApiModule;
+import com.google.gerrit.extensions.restapi.RestCollectionModifyView;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+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.extensions.restapi.TopLevelResource;
+import com.google.gerrit.httpd.restapi.RestApiServlet;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import com.google.inject.servlet.ServletModule;
+import java.io.IOException;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.Test;
+
+/**
+ * Tests for checking plugin-provided REST API bindings directly under {@code /}.
+ *
+ * <p>These tests only verify that the plugin-provided REST endpoints are correctly bound, they do
+ * not test the functionality of the plugin REST endpoints.
+ */
+public class PluginProvidedRootRestApiBindingsIT extends AbstractDaemonTest {
+
+  /** Resource to bind a child collection. */
+  public static final TypeLiteral<RestView<TestPluginResource>> TEST_KIND =
+      new TypeLiteral<RestView<TestPluginResource>>() {};
+
+  private static final String PLUGIN_NAME = "my-plugin";
+
+  private static final ImmutableSet<RestCall> TEST_CALLS =
+      ImmutableSet.of(
+          RestCall.get("/plugins/" + PLUGIN_NAME + "/test-collection/"),
+          RestCall.get("/plugins/" + PLUGIN_NAME + "/test-collection/1/detail"),
+          RestCall.post("/plugins/" + PLUGIN_NAME + "/test-collection/"),
+          RestCall.post("/plugins/" + PLUGIN_NAME + "/test-collection/1/update"),
+          RestCall.builder(Method.GET, "/plugins/" + PLUGIN_NAME + "/not-found")
+              .expectedResponseCode(SC_NOT_FOUND)
+              .build());
+
+  /** Module for all HTTP bindings. */
+  static class MyPluginHttpModule extends ServletModule {
+    @Override
+    public void configureServlets() {
+      bind(TestRootCollection.class);
+
+      install(
+          new RestApiModule() {
+            @Override
+            public void configure() {
+              DynamicMap.mapOf(binder(), TEST_KIND);
+
+              postOnCollection(TEST_KIND).to(TestPostOnCollection.class);
+              post(TEST_KIND, "update").to(TestPost.class);
+              get(TEST_KIND, "detail").to(TestGet.class);
+            }
+          });
+
+      serveRegex("/(?:a/)?test-collection/(.*)$").with(TestRestApiServlet.class);
+    }
+  }
+
+  @Singleton
+  static class TestRestApiServlet extends RestApiServlet {
+    private static final long serialVersionUID = 1L;
+
+    @Inject
+    TestRestApiServlet(RestApiServlet.Globals globals, Provider<TestRootCollection> collection) {
+      super(globals, collection);
+    }
+
+    @Override
+    public void service(ServletRequest servletRequest, ServletResponse servletResponse)
+        throws ServletException, IOException {
+      // This is...unfortunate. HttpPluginServlet (and/or ContextMapper) doesn't properly set the
+      // servlet path on the wrapped request. Based on what RestApiServlet produces for non-plugin
+      // requests, it should be:
+      //   contextPath = "/plugins/checks"
+      //   servletPath = "/checkers/"
+      //   pathInfo = checkerUuid
+      // Instead it does:
+      //   contextPath = "/plugins/checks"
+      //   servletPath = ""
+      //   pathInfo = "/checkers/" + checkerUuid
+      // This results in RestApiServlet splitting the pathInfo into ["", "checkers", checkerUuid],
+      // and it passes the "" to CheckersCollection#parse, which understandably, but unfortunately,
+      // fails.
+      //
+      // This frankly seems like a bug that should be fixed, but it would quite likely break
+      // existing plugins in confusing ways. So, we work around it by introducing our own request
+      // wrapper with the correct paths.
+      HttpServletRequest req = (HttpServletRequest) servletRequest;
+      String pathInfo = req.getPathInfo();
+      String correctServletPath = "/test-collection/";
+      String fixedPathInfo = pathInfo.substring(correctServletPath.length());
+      HttpServletRequestWrapper wrapped =
+          new HttpServletRequestWrapper(req) {
+            @Override
+            public String getServletPath() {
+              return correctServletPath;
+            }
+
+            @Override
+            public String getPathInfo() {
+              return fixedPathInfo;
+            }
+          };
+
+      super.service(wrapped, (HttpServletResponse) servletResponse);
+    }
+  }
+
+  static class TestPluginResource implements RestResource {}
+
+  @Singleton
+  static class TestRootCollection implements ChildCollection<TopLevelResource, TestPluginResource> {
+    private final DynamicMap<RestView<TestPluginResource>> views;
+
+    @Inject
+    TestRootCollection(DynamicMap<RestView<TestPluginResource>> views) {
+      this.views = views;
+    }
+
+    @Override
+    public RestView<TopLevelResource> list() throws RestApiException {
+      return (RestReadView<TopLevelResource>) resource -> ImmutableList.of("one", "two");
+    }
+
+    @Override
+    public TestPluginResource parse(TopLevelResource parent, IdString id) throws Exception {
+      return new TestPluginResource();
+    }
+
+    @Override
+    public DynamicMap<RestView<TestPluginResource>> views() {
+      return views;
+    }
+  }
+
+  @Singleton
+  static class TestPostOnCollection
+      implements RestCollectionModifyView<TopLevelResource, TestPluginResource, String> {
+    @Override
+    public Object apply(TopLevelResource parentResource, String input) throws Exception {
+      return "test";
+    }
+  }
+
+  @Singleton
+  static class TestPost implements RestModifyView<TestPluginResource, String> {
+    @Override
+    public String apply(TestPluginResource resource, String input) throws Exception {
+      return "test";
+    }
+  }
+
+  @Singleton
+  static class TestGet implements RestReadView<TestPluginResource> {
+    @Override
+    public String apply(TestPluginResource resource) throws Exception {
+      return "test";
+    }
+  }
+
+  @Test
+  public void testEndpoints() throws Exception {
+    try (AutoCloseable ignored = installPlugin(PLUGIN_NAME, null, MyPluginHttpModule.class, null)) {
+      RestApiCallHelper.execute(adminRestSession, TEST_CALLS.asList());
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/PluginsRemoteAdminRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/PluginsRemoteAdminRestApiBindingsIT.java
new file mode 100644
index 0000000..d60148e
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/PluginsRemoteAdminRestApiBindingsIT.java
@@ -0,0 +1,70 @@
+// 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.binding;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GerritConfig;
+import com.google.gerrit.acceptance.rest.util.RestApiCallHelper;
+import com.google.gerrit.acceptance.rest.util.RestCall;
+import com.google.gerrit.common.RawInputUtil;
+import com.google.gerrit.extensions.api.plugins.InstallPluginInput;
+import com.google.gerrit.extensions.restapi.RawInput;
+import org.junit.Test;
+
+/**
+ * Tests for checking the remote administration bindings of the plugins REST API.
+ *
+ * <p>These tests only verify that the plugin REST endpoints are correctly bound, they do no test
+ * the functionality of the plugin REST endpoints.
+ */
+public class PluginsRemoteAdminRestApiBindingsIT extends AbstractDaemonTest {
+  /**
+   * Plugin REST endpoints to be tested, each URL contains a placeholder for the plugin identifier.
+   */
+  private static final ImmutableList<RestCall> PLUGIN_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.put("/plugins/%s"),
+
+          // For GET requests prefixing the view name with 'gerrit~' is required.
+          RestCall.get("/plugins/%s/gerrit~status"),
+
+          // POST (and PUT) requests don't require the 'gerrit~' prefix in front of the view name.
+          RestCall.post("/plugins/%s/gerrit~enable"),
+          RestCall.post("/plugins/%s/gerrit~disable"),
+          RestCall.post("/plugins/%s/gerrit~reload"),
+
+          // Plugin deletion must be tested last
+          RestCall.delete("/plugins/%s"));
+
+  private static final String JS_PLUGIN = "Gerrit.install(function(self){});\n";
+  private static final RawInput JS_PLUGIN_CONTENT = RawInputUtil.create(JS_PLUGIN.getBytes(UTF_8));
+
+  @Test
+  @GerritConfig(name = "plugins.allowRemoteAdmin", value = "true")
+  public void pluginEndpoints() throws Exception {
+    String pluginName = "my-plugin";
+    installPlugin(pluginName);
+    RestApiCallHelper.execute(adminRestSession, PLUGIN_ENDPOINTS, pluginName);
+  }
+
+  private void installPlugin(String pluginName) throws Exception {
+    InstallPluginInput input = new InstallPluginInput();
+    input.raw = JS_PLUGIN_CONTENT;
+    gApi.plugins().install(pluginName + ".js", input);
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
new file mode 100644
index 0000000..ed09ddd
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
@@ -0,0 +1,256 @@
+// 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.binding;
+
+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.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;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GitUtil;
+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.Permission;
+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;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.PushResult;
+import org.junit.Test;
+
+/**
+ * Tests for checking the bindings of the projects REST API.
+ *
+ * <p>These tests only verify that the project REST endpoints are correctly bound, they do no test
+ * the functionality of the project REST endpoints.
+ */
+public class ProjectsRestApiBindingsIT extends AbstractDaemonTest {
+  private static final ImmutableList<RestCall> PROJECT_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/projects/%s"),
+          RestCall.put("/projects/%s"),
+          RestCall.get("/projects/%s/description"),
+          RestCall.put("/projects/%s/description"),
+          RestCall.delete("/projects/%s/description"),
+          RestCall.get("/projects/%s/parent"),
+          RestCall.put("/projects/%s/parent"),
+          RestCall.get("/projects/%s/config"),
+          RestCall.put("/projects/%s/config"),
+          RestCall.get("/projects/%s/HEAD"),
+          RestCall.put("/projects/%s/HEAD"),
+          RestCall.get("/projects/%s/access"),
+          RestCall.post("/projects/%s/access"),
+          RestCall.put("/projects/%s/access:review"),
+          RestCall.get("/projects/%s/check.access"),
+          RestCall.put("/projects/%s/ban"),
+          RestCall.get("/projects/%s/statistics.git"),
+          RestCall.post("/projects/%s/index"),
+          RestCall.post("/projects/%s/gc"),
+          RestCall.get("/projects/%s/children"),
+          RestCall.get("/projects/%s/branches"),
+          RestCall.post("/projects/%s/branches:delete"),
+          RestCall.put("/projects/%s/branches/new-branch"),
+          RestCall.get("/projects/%s/tags"),
+          RestCall.post("/projects/%s/tags:delete"),
+          RestCall.put("/projects/%s/tags/new-tag"),
+          RestCall.builder(GET, "/projects/%s/commits")
+              // GET /projects/<project>/branches/<branch>/commits is not implemented
+              .expectedResponseCode(SC_NOT_FOUND)
+              .build(),
+          RestCall.get("/projects/%s/dashboards"));
+
+  /**
+   * Child project REST endpoints to be tested, each URL contains placeholders for the parent
+   * project identifier and the child project identifier.
+   */
+  private static final ImmutableList<RestCall> CHILD_PROJECT_ENDPOINTS =
+      ImmutableList.of(RestCall.get("/projects/%s/children/%s"));
+
+  /**
+   * Branch REST endpoints to be tested, each URL contains placeholders for the project identifier
+   * and the branch identifier.
+   */
+  private static final ImmutableList<RestCall> BRANCH_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/projects/%s/branches/%s"),
+          RestCall.put("/projects/%s/branches/%s"),
+          RestCall.get("/projects/%s/branches/%s/mergeable"),
+          RestCall.builder(GET, "/projects/%s/branches/%s/reflog")
+              // The tests use DfsRepository which does not support getting the reflog.
+              .expectedResponseCode(SC_METHOD_NOT_ALLOWED)
+              .expectedMessage("reflog not supported on")
+              .build(),
+          RestCall.builder(GET, "/projects/%s/branches/%s/files")
+              // GET /projects/<project>/branches/<branch>/files is not implemented
+              .expectedResponseCode(SC_NOT_FOUND)
+              .build(),
+
+          // Branch deletion must be tested last
+          RestCall.delete("/projects/%s/branches/%s"));
+
+  /**
+   * Branch file REST endpoints to be tested, each URL contains placeholders for the project
+   * identifier, the branch identifier and the file identifier.
+   */
+  private static final ImmutableList<RestCall> BRANCH_FILE_ENDPOINTS =
+      ImmutableList.of(RestCall.get("/projects/%s/branches/%s/files/%s/content"));
+
+  /**
+   * Dashboard REST endpoints to be tested, each URL contains placeholders for the project
+   * identifier and the dashboard identifier.
+   */
+  private static final ImmutableList<RestCall> DASHBOARD_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/projects/%s/dashboards/%s"),
+          RestCall.put("/projects/%s/dashboards/%s"),
+
+          // Dashboard deletion must be tested last
+          RestCall.delete("/projects/%s/dashboards/%s"));
+
+  /**
+   * Tag REST endpoints to be tested, each URL contains placeholders for the project identifier and
+   * the tag identifier.
+   */
+  private static final ImmutableList<RestCall> TAG_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/projects/%s/tags/%s"),
+          RestCall.put("/projects/%s/tags/%s"),
+          RestCall.delete("/projects/%s/tags/%s"));
+
+  /**
+   * Commit REST endpoints to be tested, each URL contains placeholders for the project identifier
+   * and the commit identifier.
+   */
+  private static final ImmutableList<RestCall> COMMIT_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/projects/%s/commits/%s"),
+          RestCall.get("/projects/%s/commits/%s/in"),
+          RestCall.get("/projects/%s/commits/%s/files"),
+          RestCall.post("/projects/%s/commits/%s/cherrypick"));
+
+  /**
+   * Commit file REST endpoints to be tested, each URL contains placeholders for the project
+   * identifier, the commit identifier and the file identifier.
+   */
+  private static final ImmutableList<RestCall> COMMIT_FILE_ENDPOINTS =
+      ImmutableList.of(RestCall.get("/projects/%s/commits/%s/files/%s/content"));
+
+  private static final String FILENAME = "test.txt";
+  @Inject private ProjectOperations projectOperations;
+
+  @Test
+  public void projectEndpoints() throws Exception {
+    RestApiCallHelper.execute(adminRestSession, PROJECT_ENDPOINTS, project.get());
+  }
+
+  @Test
+  public void childProjectEndpoints() throws Exception {
+    Project.NameKey childProject = projectOperations.newProject().parent(project).create();
+    RestApiCallHelper.execute(
+        adminRestSession, CHILD_PROJECT_ENDPOINTS, project.get(), childProject.get());
+  }
+
+  @Test
+  public void branchEndpoints() throws Exception {
+    RestApiCallHelper.execute(adminRestSession, BRANCH_ENDPOINTS, project.get(), "master");
+  }
+
+  @Test
+  public void branchFileEndpoints() throws Exception {
+    createAndSubmitChange(FILENAME);
+    RestApiCallHelper.execute(
+        adminRestSession, BRANCH_FILE_ENDPOINTS, project.get(), "master", FILENAME);
+  }
+
+  @Test
+  public void dashboardEndpoints() throws Exception {
+    createDefaultDashboard();
+    RestApiCallHelper.execute(
+        adminRestSession, DASHBOARD_ENDPOINTS, project.get(), DEFAULT_DASHBOARD_NAME);
+  }
+
+  @Test
+  public void tagEndpoints() throws Exception {
+    String tag = "test-tag";
+    gApi.projects().name(project.get()).tag(tag).create(new TagInput());
+    RestApiCallHelper.execute(adminRestSession, TAG_ENDPOINTS, project.get(), tag);
+  }
+
+  @Test
+  public void commitEndpoints() throws Exception {
+    String commit = createAndSubmitChange(FILENAME);
+    RestApiCallHelper.execute(adminRestSession, COMMIT_ENDPOINTS, project.get(), commit);
+  }
+
+  @Test
+  public void commitFileEndpoints() throws Exception {
+    String commit = createAndSubmitChange(FILENAME);
+    RestApiCallHelper.execute(
+        adminRestSession, COMMIT_FILE_ENDPOINTS, project.get(), commit, FILENAME);
+  }
+
+  private String createAndSubmitChange(String filename) throws Exception {
+    RevCommit c =
+        testRepo
+            .commit()
+            .message("A change")
+            .parent(getRemoteHead())
+            .add(filename, "content")
+            .insertChangeId()
+            .create();
+    String id = GitUtil.getChangeId(testRepo, c).get();
+    testRepo.reset(c);
+
+    String r = "refs/for/master";
+    PushResult pr = pushHead(testRepo, r, false);
+    assertPushOk(pr, r);
+
+    gApi.changes().id(id).current().review(ReviewInput.approve());
+    gApi.changes().id(id).current().submit();
+    return c.name();
+  }
+
+  private void createDefaultDashboard() throws Exception {
+    String dashboardRef = REFS_DASHBOARDS + "team";
+    grant(project, "refs/meta/*", Permission.CREATE);
+    gApi.projects().name(project.get()).branch(dashboardRef).create(new BranchInput());
+
+    try (Repository r = repoManager.openRepository(project);
+        TestRepository<Repository> tr = new TestRepository<>(r)) {
+      TestRepository<Repository>.CommitBuilder cb = tr.branch(dashboardRef).commit();
+      StringBuilder content = new StringBuilder("[dashboard]\n");
+      content.append("title = ").append("Open Changes").append("\n");
+      content.append("[section \"").append("open").append("\"]\n");
+      content.append("query = ").append("is:open").append("\n");
+      cb.add("overview", content.toString());
+      cb.create();
+    }
+
+    try (ProjectConfigUpdate u = updateProject(project)) {
+      u.getConfig().getProject().setLocalDefaultDashboard(dashboardRef + ":overview");
+      u.save();
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/RootCollectionsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/RootCollectionsRestApiBindingsIT.java
new file mode 100644
index 0000000..6d140c6
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/RootCollectionsRestApiBindingsIT.java
@@ -0,0 +1,58 @@
+// 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.binding;
+
+import static com.google.gerrit.acceptance.rest.util.RestApiCallHelper.execute;
+import static com.google.gerrit.acceptance.rest.util.RestCall.Method.GET;
+import static org.apache.http.HttpStatus.SC_NOT_FOUND;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GerritConfig;
+import com.google.gerrit.acceptance.rest.util.RestCall;
+import org.junit.Test;
+
+/**
+ * Tests for checking the bindings of the root REST API.
+ *
+ * <p>These tests only verify that the root REST endpoints are correctly bound, they do no test the
+ * functionality of the root REST endpoints.
+ */
+public class RootCollectionsRestApiBindingsIT extends AbstractDaemonTest {
+  /** Root REST endpoints to be tested, the URLs contain no placeholders. */
+  private static final ImmutableList<RestCall> ROOT_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/access/"),
+          RestCall.get("/accounts/"),
+          RestCall.put("/accounts/new-account"),
+          RestCall.builder(GET, "/config/")
+              // GET /config/ is not implemented
+              .expectedResponseCode(SC_NOT_FOUND)
+              .build(),
+          RestCall.get("/changes/"),
+          RestCall.post("/changes/"),
+          RestCall.get("/groups/"),
+          RestCall.put("/groups/new-group"),
+          RestCall.get("/plugins/"),
+          RestCall.put("/plugins/new-plugin"),
+          RestCall.get("/projects/"),
+          RestCall.put("/projects/new-project"));
+
+  @Test
+  @GerritConfig(name = "plugins.allowRemoteAdmin", value = "true")
+  public void rootEndpoints() throws Exception {
+    execute(adminRestSession, ROOT_ENDPOINTS);
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index 08a76e4..f191e08 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -37,7 +37,8 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.TestProjectInput;
-import com.google.gerrit.common.Nullable;
+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.ChangeApi;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
@@ -81,7 +82,6 @@
 import com.google.gerrit.server.validators.ValidationException;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.TestTimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -116,14 +116,13 @@
   }
 
   @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;
 
-  @Inject private IdentifiedUser.GenericFactory userFactory;
-
-  @Inject private DynamicSet<OnSubmitValidationListener> onSubmitValidationListeners;
   private RegistrationHandle onSubmitValidatorHandle;
-
   private String systemTimeZone;
 
   @Before
@@ -139,11 +138,6 @@
   }
 
   @After
-  public void cleanup() {
-    db.close();
-  }
-
-  @After
   public void removeOnSubmitValidator() {
     if (onSubmitValidatorHandle != null) {
       onSubmitValidatorHandle.remove();
@@ -155,12 +149,11 @@
   @Test
   @TestProjectInput(createEmptyCommit = false)
   public void submitToEmptyRepo() throws Exception {
-    assertThat(getRemoteHead()).isNull();
+    assertThat(projectOperations.project(project).hasHead("master")).isFalse();
     PushOneCommit.Result change = createChange();
     assertThat(change.getCommit().getParents()).isEmpty();
     Map<Branch.NameKey, ObjectId> actual = fetchFromSubmitPreview(change.getChangeId());
-    RevCommit headAfterSubmitPreview = getRemoteHead();
-    assertThat(headAfterSubmitPreview).isNull();
+    assertThat(projectOperations.project(project).hasHead("master")).isFalse();
     assertThat(actual).hasSize(1);
 
     submit(change.getChangeId());
@@ -315,11 +308,11 @@
   @Test
   public void submitNoPermission() throws Exception {
     // create project where submit is blocked
-    Project.NameKey p = createProject("p");
+    Project.NameKey p = projectOperations.newProject().create();
     block(p, "refs/*", Permission.SUBMIT, REGISTERED_USERS);
 
     TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), repo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), repo);
     PushOneCommit.Result result = push.to("refs/for/master");
     result.assertOkStatus();
 
@@ -329,7 +322,7 @@
   @Test
   public void noSelfSubmit() throws Exception {
     // create project where submit is blocked for the change owner
-    Project.NameKey p = createProject("p");
+    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/*");
@@ -339,23 +332,23 @@
     }
 
     TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), repo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), repo);
     PushOneCommit.Result result = push.to("refs/for/master");
     result.assertOkStatus();
 
     ChangeInfo change = gApi.changes().id(result.getChangeId()).get();
-    assertThat(change.owner._accountId).isEqualTo(admin.id.get());
+    assertThat(change.owner._accountId).isEqualTo(admin.id().get());
 
     submit(result.getChangeId(), new SubmitInput(), AuthException.class, "submit not permitted");
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     submit(result.getChangeId());
   }
 
   @Test
   public void onlySelfSubmit() throws Exception {
     // create project where only the change owner can submit
-    Project.NameKey p = createProject("p");
+    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/*");
@@ -365,17 +358,17 @@
     }
 
     TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), repo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), repo);
     PushOneCommit.Result result = push.to("refs/for/master");
     result.assertOkStatus();
 
     ChangeInfo change = gApi.changes().id(result.getChangeId()).get();
-    assertThat(change.owner._accountId).isEqualTo(admin.id.get());
+    assertThat(change.owner._accountId).isEqualTo(admin.id().get());
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     submit(result.getChangeId(), new SubmitInput(), AuthException.class, "submit not permitted");
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     submit(result.getChangeId());
   }
 
@@ -385,8 +378,10 @@
     String topic = "test-topic";
 
     // Create test projects
-    TestRepository<?> repoA = createProjectWithPush("project-a", null, getSubmitType());
-    TestRepository<?> repoB = createProjectWithPush("project-b", null, getSubmitType());
+    Project.NameKey keyA = createProjectForPush(getSubmitType());
+    TestRepository<?> repoA = cloneProject(keyA);
+    Project.NameKey keyB = createProjectForPush(getSubmitType());
+    TestRepository<?> repoB = cloneProject(keyB);
 
     // Create changes on project-a
     PushOneCommit.Result change1 =
@@ -419,15 +414,15 @@
     String topic = "test-topic";
 
     // Create test project
-    String projectName = "project-a";
-    TestRepository<?> repoA = createProjectWithPush(projectName, null, getSubmitType());
+    Project.NameKey keyA = createProjectForPush(getSubmitType());
+    TestRepository<?> repoA = cloneProject(keyA);
 
-    RevCommit initialHead = getRemoteHead(new Project.NameKey(name(projectName)), "master");
+    RevCommit initialHead = getRemoteHead(keyA, "master");
 
     // Create the dev branch on the test project
     BranchInput in = new BranchInput();
     in.revision = initialHead.name();
-    gApi.projects().name(name(projectName)).branch("dev").create(in);
+    gApi.projects().name(keyA.get()).branch("dev").create(in);
 
     // Create changes on master
     PushOneCommit.Result change1 =
@@ -487,7 +482,7 @@
     assertThat(log).hasSize(expectedCommitCount);
 
     assertThat(commitsInRepo)
-        .containsAllOf("Initial empty repository", "Change 1", "Change 2", "Change 3");
+        .containsAtLeast("Initial empty repository", "Change 1", "Change 2", "Change 3");
     if (getSubmitType() == SubmitType.MERGE_ALWAYS) {
       assertThat(commitsInRepo).contains("Merge changes from topic \"" + expectedTopic + "\"");
     }
@@ -584,13 +579,12 @@
     }
 
     PushOneCommit push1 =
-        pushFactory.create(
-            db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
+        pushFactory.create(admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
     PushOneCommit.Result c1 = push1.to("refs/heads/topic");
     c1.assertOkStatus();
     PushOneCommit push2 =
         pushFactory.create(
-            db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
+            admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
     PushOneCommit.Result c2 = push2.to("refs/heads/topic");
     c2.assertOkStatus();
 
@@ -614,11 +608,10 @@
     //
     RevCommit master = getRemoteHead(project, "master");
     PushOneCommit stableTip =
-        pushFactory.create(
-            db, admin.getIdent(), testRepo, "Tip of branch stable", "stable.txt", "");
+        pushFactory.create(admin.newIdent(), testRepo, "Tip of branch stable", "stable.txt", "");
     PushOneCommit.Result stable = stableTip.to("refs/heads/stable");
     PushOneCommit mergeCommit =
-        pushFactory.create(db, admin.getIdent(), testRepo, "The merge commit", "merge.txt", "");
+        pushFactory.create(admin.newIdent(), testRepo, "The merge commit", "merge.txt", "");
     mergeCommit.setParents(ImmutableList.of(master, stable.getCommit()));
     PushOneCommit.Result mergeReview = mergeCommit.to("refs/for/master");
     approve(mergeReview.getChangeId());
@@ -645,11 +638,11 @@
     // push directly to stable to S1
     PushOneCommit.Result s1 =
         pushFactory
-            .create(db, admin.getIdent(), testRepo, "new commit into stable", "stable1.txt", "")
+            .create(admin.newIdent(), testRepo, "new commit into stable", "stable1.txt", "")
             .to("refs/heads/stable");
     // move the stable tip ahead to S2
     pushFactory
-        .create(db, admin.getIdent(), testRepo, "Tip of branch stable", "stable2.txt", "")
+        .create(admin.newIdent(), testRepo, "Tip of branch stable", "stable2.txt", "")
         .to("refs/heads/stable");
 
     testRepo.reset(initial);
@@ -657,12 +650,12 @@
     // move the master ahead
     PushOneCommit.Result m =
         pushFactory
-            .create(db, admin.getIdent(), testRepo, "Move master ahead", "master.txt", "")
+            .create(admin.newIdent(), testRepo, "Move master ahead", "master.txt", "")
             .to("refs/heads/master");
 
     // create merge change
     PushOneCommit mc =
-        pushFactory.create(db, admin.getIdent(), testRepo, "The merge commit", "merge.txt", "");
+        pushFactory.create(admin.newIdent(), testRepo, "The merge commit", "merge.txt", "");
     mc.setParents(ImmutableList.of(m.getCommit(), s1.getCommit()));
     PushOneCommit.Result mergeReview = mc.to("refs/for/master");
     approve(mergeReview.getChangeId());
@@ -742,18 +735,15 @@
   public void submitWithValidation() throws Exception {
     AtomicBoolean called = new AtomicBoolean(false);
     this.addOnSubmitValidationListener(
-        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);
-            }
+        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);
           }
         });
 
@@ -769,8 +759,10 @@
     String topic = "test-topic";
 
     // Create test projects
-    TestRepository<?> repoA = createProjectWithPush("project-a", null, getSubmitType());
-    TestRepository<?> repoB = createProjectWithPush("project-b", null, getSubmitType());
+    Project.NameKey keyA = createProjectForPush(getSubmitType());
+    TestRepository<?> repoA = cloneProject(keyA);
+    Project.NameKey keyB = createProjectForPush(getSubmitType());
+    TestRepository<?> repoB = cloneProject(keyB);
 
     // Create changes on project-a
     PushOneCommit.Result change1 =
@@ -794,36 +786,31 @@
     // succeed.
     List<String> projectsCalled = new ArrayList<>(4);
     this.addOnSubmitValidationListener(
-        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");
-            }
+        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);
+          }
+          projectsCalled.add(args.getProject().get());
+          if (projectsCalled.size() == 2) {
+            throw new ValidationException("time to fail");
           }
         });
     submitWithConflict(change4.getChangeId(), "time to fail");
-    assertThat(projectsCalled).containsExactly(name("project-a"), name("project-b"));
+    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(
-            name("project-a"), name("project-b"), name("project-a"), name("project-b"));
+    assertThat(projectsCalled).containsExactly(keyA.get(), keyB.get(), keyA.get(), keyB.get());
     for (PushOneCommit.Result change : changes) {
       change.assertChange(Change.Status.MERGED, name(topic), admin);
     }
@@ -838,7 +825,7 @@
     // Create a stable branch and bootstrap it.
     gApi.projects().name(project.get()).branch("stable").create(new BranchInput());
     PushOneCommit push =
-        pushFactory.create(db, user.getIdent(), testRepo, "initial commit", "a.txt", "a");
+        pushFactory.create(user.newIdent(), testRepo, "initial commit", "a.txt", "a");
     PushOneCommit.Result change = push.to("refs/heads/stable");
 
     RevCommit stable = getRemoteHead(project, "stable");
@@ -902,8 +889,6 @@
 
   @Test
   public void retrySubmitSingleChangeOnLockFailure() throws Exception {
-    assume().that(notesMigration.disableChangeReviewDb()).isTrue();
-
     PushOneCommit.Result change = createChange();
     String id = change.getChangeId();
     approve(id);
@@ -929,13 +914,14 @@
 
   @Test
   public void retrySubmitAfterTornTopicOnLockFailure() throws Exception {
-    assume().that(notesMigration.disableChangeReviewDb()).isTrue();
     assume().that(isSubmitWholeTopicEnabled()).isTrue();
 
     String topic = "test-topic";
 
-    TestRepository<?> repoA = createProjectWithPush("project-a", null, getSubmitType());
-    TestRepository<?> repoB = createProjectWithPush("project-b", null, getSubmitType());
+    Project.NameKey keyA = createProjectForPush(getSubmitType());
+    Project.NameKey keyB = createProjectForPush(getSubmitType());
+    TestRepository<?> repoA = cloneProject(keyA);
+    TestRepository<?> repoB = cloneProject(keyB);
 
     PushOneCommit.Result change1 =
         createChange(repoA, "master", "Change 1", "a.txt", "content", topic);
@@ -962,13 +948,13 @@
 
     repoA.git().fetch().call();
     RevWalk rwA = repoA.getRevWalk();
-    RevCommit masterA = rwA.parseCommit(getRemoteHead(name("project-a"), "master"));
+    RevCommit masterA = rwA.parseCommit(getRemoteHead(keyA, "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(name("project-b"), "master"));
+    RevCommit masterB = rwB.parseCommit(getRemoteHead(keyB, "master"));
     RevCommit change2Ps = parseCurrentRevision(rwB, change2.getChangeId());
     assertThat(rwB.isMergedInto(change2Ps, masterB)).isTrue();
 
@@ -1075,12 +1061,11 @@
   @Test
   @TestProjectInput(createEmptyCommit = false, rejectEmptyCommit = InheritableBoolean.TRUE)
   public void submitNonemptyCommitToEmptyRepoWithRejectEmptyCommit_allowed() throws Exception {
-    assertThat(getRemoteHead()).isNull();
+    assertThat(projectOperations.project(project).hasHead("master")).isFalse();
     PushOneCommit.Result change = createChange();
     assertThat(change.getCommit().getParents()).isEmpty();
     Map<Branch.NameKey, ObjectId> actual = fetchFromSubmitPreview(change.getChangeId());
-    RevCommit headAfterSubmitPreview = getRemoteHead();
-    assertThat(headAfterSubmitPreview).isNull();
+    assertThat(projectOperations.project(project).hasHead("master")).isFalse();
     assertThat(actual).hasSize(1);
 
     submit(change.getChangeId());
@@ -1091,10 +1076,10 @@
   @Test
   @TestProjectInput(createEmptyCommit = false, rejectEmptyCommit = InheritableBoolean.TRUE)
   public void submitEmptyCommitToEmptyRepoWithRejectEmptyCommit_allowed() throws Exception {
-    assertThat(getRemoteHead()).isNull();
+    assertThat(projectOperations.project(project).hasHead("master")).isFalse();
     PushOneCommit.Result change =
         pushFactory
-            .create(db, admin.getIdent(), testRepo, "Change 1", ImmutableMap.of())
+            .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
@@ -1102,8 +1087,7 @@
         .isEqualTo(ObjectId.fromString("4b825dc642cb6eb9a060e54bf8d69288fbee4904"));
 
     Map<Branch.NameKey, ObjectId> actual = fetchFromSubmitPreview(change.getChangeId());
-    RevCommit headAfterSubmitPreview = getRemoteHead();
-    assertThat(headAfterSubmitPreview).isNull();
+    assertThat(projectOperations.project(project).hasHead("master")).isFalse();
     assertThat(actual).hasSize(1);
 
     submit(change.getChangeId());
@@ -1114,12 +1098,12 @@
   private void setChangeStatusToNew(PushOneCommit.Result... changes) throws Exception {
     for (PushOneCommit.Result change : changes) {
       try (BatchUpdate bu =
-          batchUpdateFactory.create(db, project, userFactory.create(admin.id), TimeUtil.nowTs())) {
+          batchUpdateFactory.create(project, userFactory.create(admin.id()), TimeUtil.nowTs())) {
         bu.addOp(
             change.getChange().getId(),
             new BatchUpdateOp() {
               @Override
-              public boolean updateChange(ChangeContext ctx) throws OrmException {
+              public boolean updateChange(ChangeContext ctx) {
                 ctx.getChange().setStatus(Change.Status.NEW);
                 ctx.getUpdate(ctx.getChange().currentPatchSetId()).setStatus(Change.Status.NEW);
                 return true;
@@ -1248,7 +1232,7 @@
     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.getId());
+    assertThat(new Account.Id(cr.all.get(0)._accountId)).isEqualTo(user.id());
   }
 
   protected void assertMerged(String changeId) throws RestApiException {
@@ -1274,19 +1258,19 @@
 
   protected void assertSubmitter(String changeId, int psId, TestAccount user) throws Exception {
     Change c = getOnlyElement(queryProvider.get().byKeyPrefix(changeId)).change();
-    ChangeNotes cn = notesFactory.createChecked(db, c);
+    ChangeNotes cn = notesFactory.createChecked(c);
     PatchSetApproval submitter =
-        approvalsUtil.getSubmitter(db, cn, new PatchSet.Id(cn.getChangeId(), psId));
+        approvalsUtil.getSubmitter(cn, new PatchSet.Id(cn.getChangeId(), psId));
     assertThat(submitter).isNotNull();
     assertThat(submitter.isLegacySubmit()).isTrue();
-    assertThat(submitter.getAccountId()).isEqualTo(user.getId());
+    assertThat(submitter.getAccountId()).isEqualTo(user.id());
   }
 
   protected void assertNoSubmitter(String changeId, int psId) throws Exception {
     Change c = getOnlyElement(queryProvider.get().byKeyPrefix(changeId)).change();
-    ChangeNotes cn = notesFactory.createChecked(db, c);
+    ChangeNotes cn = notesFactory.createChecked(c);
     PatchSetApproval submitter =
-        approvalsUtil.getSubmitter(db, cn, new PatchSet.Id(cn.getChangeId(), psId));
+        approvalsUtil.getSubmitter(cn, new PatchSet.Id(cn.getChangeId(), psId));
     assertThat(submitter).isNull();
   }
 
@@ -1300,7 +1284,7 @@
 
   protected void assertRebase(TestRepository<?> testRepo, boolean contentMerge) throws Exception {
     Repository repo = testRepo.getRepository();
-    RevCommit localHead = getHead(repo);
+    RevCommit localHead = getHead(repo, "HEAD");
     RevCommit remoteHead = getRemoteHead();
     assertThat(localHead.getId()).isNotEqualTo(remoteHead.getId());
     assertThat(remoteHead.getParentCount()).isEqualTo(1);
@@ -1353,18 +1337,17 @@
     }
   }
 
-  private TestRepository<?> createProjectWithPush(
-      String name, @Nullable Project.NameKey parent, SubmitType submitType) throws Exception {
-    Project.NameKey project = createProject(name, parent, true, submitType);
+  // TODO(hanwen): the submodule tests have a similar method; maybe we could share code?
+  protected Project.NameKey createProjectForPush(SubmitType submitType) throws Exception {
+    Project.NameKey project = projectOperations.newProject().submitType(submitType).create();
     grant(project, "refs/heads/*", Permission.PUSH);
     grant(project, "refs/for/refs/heads/*", Permission.SUBMIT);
-    return cloneProject(project);
+    return project;
   }
 
   protected PushOneCommit.Result createChange(
       String subject, String fileName, String content, String topic) throws Exception {
-    PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), testRepo, subject, fileName, content);
+    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 29a81ca..36a09fd 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
@@ -17,21 +17,10 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
 
-import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestProjectInput;
-import com.google.gerrit.extensions.api.changes.SubmitInput;
-import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.InheritableBoolean;
-import com.google.gerrit.extensions.common.ChangeInfo;
-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.change.TestSubmitInput;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
 import org.junit.Test;
 
 public abstract class AbstractSubmitByMerge extends AbstractSubmit {
@@ -108,10 +97,10 @@
     assume().that(isSubmitWholeTopicEnabled()).isTrue();
     PushOneCommit.Result change1 =
         pushFactory
-            .create(db, admin.getIdent(), testRepo, "Change 1", "a", "a")
+            .create(admin.newIdent(), testRepo, "Change 1", "a", "a")
             .to("refs/for/master/" + name("topic"));
 
-    PushOneCommit push2 = pushFactory.create(db, admin.getIdent(), testRepo, "Change 2", "b", "b");
+    PushOneCommit push2 = pushFactory.create(admin.newIdent(), testRepo, "Change 2", "b", "b");
     push2.noParents();
     PushOneCommit.Result change2 = push2.to("refs/for/master/" + name("topic"));
     change2.assertOkStatus();
@@ -124,58 +113,4 @@
     assertThat(head.getParent(0)).isEqualTo(change1.getCommit());
     assertThat(head.getParent(1)).isEqualTo(change2.getCommit());
   }
-
-  @Test
-  public void repairChangeStateAfterFailure() throws Exception {
-    // In NoteDb-only mode, repo and meta updates are atomic (at least in InMemoryRepository).
-    assume().that(notesMigration.disableChangeReviewDb()).isFalse();
-
-    RevCommit initialHead = getRemoteHead();
-    PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
-    submit(change.getChangeId());
-    RevCommit afterChange1Head = getRemoteHead();
-
-    testRepo.reset(initialHead);
-    PushOneCommit.Result change2 = createChange("Change 2", "b.txt", "other content");
-    Change.Id id2 = change2.getChange().getId();
-    TestSubmitInput failInput = new TestSubmitInput();
-    failInput.failAfterRefUpdates = true;
-    submit(
-        change2.getChangeId(),
-        failInput,
-        ResourceConflictException.class,
-        "Failing after ref updates");
-
-    // Bad: ref advanced but change wasn't updated.
-    PatchSet.Id psId1 = new PatchSet.Id(id2, 1);
-    ChangeInfo info = gApi.changes().id(id2.get()).get();
-    assertThat(info.status).isEqualTo(ChangeStatus.NEW);
-    assertThat(info.revisions.get(info.currentRevision)._number).isEqualTo(1);
-
-    RevCommit tip;
-    try (Repository repo = repoManager.openRepository(project);
-        RevWalk rw = new RevWalk(repo)) {
-      ObjectId rev1 = repo.exactRef(psId1.toRefName()).getObjectId();
-      assertThat(rev1).isNotNull();
-
-      tip = rw.parseCommit(repo.exactRef("refs/heads/master").getObjectId());
-      assertThat(tip.getParentCount()).isEqualTo(2);
-      assertThat(tip.getParent(0)).isEqualTo(afterChange1Head);
-      assertThat(tip.getParent(1)).isEqualTo(change2.getCommit());
-    }
-
-    submit(change2.getChangeId(), new SubmitInput(), null, null);
-
-    // Change status and patch set entities were updated, and branch tip stayed
-    // the same.
-    info = gApi.changes().id(id2.get()).get();
-    assertThat(info.status).isEqualTo(ChangeStatus.MERGED);
-    assertThat(info.revisions.get(info.currentRevision)._number).isEqualTo(1);
-    assertThat(Iterables.getLast(info.messages).message)
-        .isEqualTo("Change has been successfully merged by Administrator");
-
-    try (Repository repo = repoManager.openRepository(project)) {
-      assertThat(repo.exactRef("refs/heads/master").getObjectId()).isEqualTo(tip);
-    }
-  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
index 0a92cfb..c12adfa 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
@@ -15,28 +15,24 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.acceptance.GitUtil.getChangeId;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 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.extensions.restapi.ResourceConflictException;
 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.change.TestSubmitInput;
 import com.google.gerrit.server.project.testing.Util;
+import com.google.inject.Inject;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -44,6 +40,7 @@
 import org.junit.Test;
 
 public abstract class AbstractSubmitByRebase extends AbstractSubmit {
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   @Override
   protected abstract SubmitType getSubmitType();
@@ -73,8 +70,9 @@
     submitWithRebase(user);
   }
 
-  private void submitWithRebase(TestAccount submitter) throws Exception {
-    setApiUser(submitter);
+  protected ImmutableList<PushOneCommit.Result> submitWithRebase(TestAccount submitter)
+      throws Exception {
+    requestScopeOperations.setApiUser(submitter.id());
     RevCommit initialHead = getRemoteHead();
     PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
@@ -90,8 +88,8 @@
     assertCurrentRevision(change2.getChangeId(), 2, headAfterSecondSubmit);
     assertSubmitter(change2.getChangeId(), 1, submitter);
     assertSubmitter(change2.getChangeId(), 2, submitter);
-    assertPersonEquals(admin.getIdent(), headAfterSecondSubmit.getAuthorIdent());
-    assertPersonEquals(submitter.getIdent(), headAfterSecondSubmit.getCommitterIdent());
+    assertPersonEquals(admin.newIdent(), headAfterSecondSubmit.getAuthorIdent());
+    assertPersonEquals(submitter.newIdent(), headAfterSecondSubmit.getCommitterIdent());
 
     assertRefUpdatedEvents(
         initialHead, headAfterFirstSubmit, headAfterFirstSubmit, headAfterSecondSubmit);
@@ -100,6 +98,7 @@
         headAfterFirstSubmit.name(),
         change2.getChangeId(),
         headAfterSecondSubmit.name());
+    return ImmutableList.of(change, change2);
   }
 
   @Test
@@ -180,7 +179,7 @@
     PushOneCommit.Result change1 = createChange("Added a", "a.txt", "");
 
     PushOneCommit change2Push =
-        pushFactory.create(db, admin.getIdent(), testRepo, "Merge to master", "m.txt", "");
+        pushFactory.create(admin.newIdent(), testRepo, "Merge to master", "m.txt", "");
     change2Push.setParents(ImmutableList.of(initialHead, change1.getCommit()));
     PushOneCommit.Result change2 = change2Push.to("refs/for/master");
 
@@ -242,80 +241,6 @@
     assertChangeMergedEvents(change.getChangeId(), headAfterFirstSubmit.name());
   }
 
-  @Test
-  public void repairChangeStateAfterFailure() throws Exception {
-    // In NoteDb-only mode, repo and meta updates are atomic (at least in InMemoryRepository).
-    assume().that(notesMigration.disableChangeReviewDb()).isFalse();
-
-    RevCommit initialHead = getRemoteHead();
-    PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
-    submit(change.getChangeId());
-
-    RevCommit headAfterFirstSubmit = getRemoteHead();
-    testRepo.reset(initialHead);
-    PushOneCommit.Result change2 = createChange("Change 2", "b.txt", "other content");
-    Change.Id id2 = change2.getChange().getId();
-    TestSubmitInput failInput = new TestSubmitInput();
-    failInput.failAfterRefUpdates = true;
-    submit(
-        change2.getChangeId(),
-        failInput,
-        ResourceConflictException.class,
-        "Failing after ref updates");
-    RevCommit headAfterFailedSubmit = getRemoteHead();
-
-    // Bad: ref advanced but change wasn't updated.
-    PatchSet.Id psId1 = new PatchSet.Id(id2, 1);
-    PatchSet.Id psId2 = new PatchSet.Id(id2, 2);
-    ChangeInfo info = gApi.changes().id(id2.get()).get();
-    assertThat(info.status).isEqualTo(ChangeStatus.NEW);
-    assertThat(info.revisions.get(info.currentRevision)._number).isEqualTo(1);
-    assertThat(getPatchSet(psId2)).isNull();
-
-    ObjectId rev2;
-    try (Repository repo = repoManager.openRepository(project);
-        RevWalk rw = new RevWalk(repo)) {
-      ObjectId rev1 = repo.exactRef(psId1.toRefName()).getObjectId();
-      assertThat(rev1).isNotNull();
-
-      rev2 = repo.exactRef(psId2.toRefName()).getObjectId();
-      assertThat(rev2).isNotNull();
-      assertThat(rev2).isNotEqualTo(rev1);
-      assertThat(rw.parseCommit(rev2).getParent(0)).isEqualTo(headAfterFirstSubmit);
-
-      assertThat(repo.exactRef("refs/heads/master").getObjectId()).isEqualTo(rev2);
-    }
-
-    submit(change2.getChangeId());
-    RevCommit headAfterSecondSubmit = getRemoteHead();
-    assertThat(headAfterSecondSubmit).isEqualTo(headAfterFailedSubmit);
-
-    // Change status and patch set entities were updated, and branch tip stayed
-    // the same.
-    info = gApi.changes().id(id2.get()).get();
-    assertThat(info.status).isEqualTo(ChangeStatus.MERGED);
-    assertThat(info.revisions.get(info.currentRevision)._number).isEqualTo(2);
-    PatchSet ps2 = getPatchSet(psId2);
-    assertThat(ps2).isNotNull();
-    assertThat(ps2.getRevision().get()).isEqualTo(rev2.name());
-    assertThat(Iterables.getLast(info.messages).message)
-        .isEqualTo(
-            "Change has been successfully rebased and submitted as "
-                + rev2.name()
-                + " by Administrator");
-
-    try (Repository repo = repoManager.openRepository(project)) {
-      assertThat(repo.exactRef("refs/heads/master").getObjectId()).isEqualTo(rev2);
-    }
-
-    assertRefUpdatedEvents(initialHead, headAfterFirstSubmit);
-    assertChangeMergedEvents(
-        change.getChangeId(),
-        headAfterFirstSubmit.name(),
-        change2.getChangeId(),
-        headAfterSecondSubmit.name());
-  }
-
   protected RevCommit parse(ObjectId id) throws Exception {
     try (Repository repo = repoManager.openRepository(project);
         RevWalk rw = new RevWalk(repo)) {
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
index 0f394aa7..e7f3d54 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.extensions.api.changes.ActionVisitor;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.client.ListChangesOption;
@@ -54,9 +55,9 @@
     return submitWholeTopicEnabledConfig();
   }
 
-  @Inject private RevisionJson.Factory revisionJsonFactory;
-
   @Inject private DynamicSet<ActionVisitor> actionVisitors;
+  @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private RevisionJson.Factory revisionJsonFactory;
 
   private RegistrationHandle visitorHandle;
 
@@ -156,25 +157,25 @@
     String change = createChangeWithTopic().getChangeId();
     approve(change);
 
-    setApiUserAnonymous();
+    requestScopeOperations.setApiUserAnonymous();
     String etag1 = getETag(change);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     approve(parent);
 
-    setApiUserAnonymous();
+    requestScopeOperations.setApiUserAnonymous();
     String etag2 = getETag(change);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     String changeWithSameTopic = createChangeWithTopic().getChangeId();
 
-    setApiUserAnonymous();
+    requestScopeOperations.setApiUserAnonymous();
     String etag3 = getETag(change);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     approve(changeWithSameTopic);
 
-    setApiUserAnonymous();
+    requestScopeOperations.setApiUserAnonymous();
     String etag4 = getETag(change);
 
     if (isSubmitWholeTopicEnabled()) {
@@ -193,13 +194,13 @@
     String change = createChange().getChangeId();
     approve(change);
 
-    setApiUserAnonymous();
+    requestScopeOperations.setApiUserAnonymous();
     String etag1 = getETag(change);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     approve(parent);
 
-    setApiUserAnonymous();
+    requestScopeOperations.setApiUserAnonymous();
     String etag2 = getETag(change);
     assertThat(etag2).isEqualTo(etag1);
   }
@@ -327,7 +328,7 @@
     }
 
     Map<String, ActionInfo> origActions = origChange.actions;
-    assertThat(origActions.keySet()).containsAllOf("followup", "abandon");
+    assertThat(origActions.keySet()).containsAtLeast("followup", "abandon");
     assertThat(origActions.get("abandon").label).isEqualTo("Abandon");
 
     Visitor v = new Visitor();
@@ -376,7 +377,7 @@
     }
 
     Map<String, ActionInfo> origActions = gApi.changes().id(id).current().actions();
-    assertThat(origActions.keySet()).containsAllOf("cherrypick", "rebase");
+    assertThat(origActions.keySet()).containsAtLeast("cherrypick", "rebase");
     assertThat(origActions.get("rebase").label).isEqualTo("Rebase");
 
     Visitor v = new Visitor();
@@ -393,7 +394,7 @@
     visitedCurrentRevisionActionsAssertions(origActions, revisionInfo.actions);
 
     // ...via ChangeJson directly.
-    ChangeData cd = changeDataFactory.create(db, project, changeId);
+    ChangeData cd = changeDataFactory.create(project, changeId);
     revisionJsonFactory.create(opts).getRevisionInfo(cd, cd.patchSet(new PatchSet.Id(changeId, 1)));
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
index 69035f2..2d6227b 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.common.truth.Truth.assert_;
 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;
@@ -24,15 +24,17 @@
 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.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.extensions.restapi.UnprocessableEntityException;
 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;
@@ -42,6 +44,7 @@
 
 @NoHttpd
 public class AssigneeIT extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   @BeforeClass
   public static void setTimeForTesting() {
@@ -62,66 +65,58 @@
   @Test
   public void addGetAssignee() throws Exception {
     PushOneCommit.Result r = createChange();
-    assertThat(setAssignee(r, user.email)._accountId).isEqualTo(user.getId().get());
-    assertThat(getAssignee(r)._accountId).isEqualTo(user.getId().get());
+    assertThat(setAssignee(r, user.email())._accountId).isEqualTo(user.id().get());
+    assertThat(getAssignee(r)._accountId).isEqualTo(user.id().get());
 
     assertThat(sender.getMessages()).hasSize(1);
     Message m = sender.getMessages().get(0);
-    assertThat(m.rcpt()).containsExactly(user.emailAddress);
+    assertThat(m.rcpt()).containsExactly(user.getEmailAddress());
   }
 
   @Test
   public void setNewAssigneeWhenExists() throws Exception {
     PushOneCommit.Result r = createChange();
-    setAssignee(r, user.email);
-    assertThat(setAssignee(r, user.email)._accountId).isEqualTo(user.getId().get());
+    setAssignee(r, user.email());
+    assertThat(setAssignee(r, user.email())._accountId).isEqualTo(user.id().get());
   }
 
   @Test
   public void getPastAssignees() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
     PushOneCommit.Result r = createChange();
-    setAssignee(r, user.email);
-    setAssignee(r, admin.email);
+    setAssignee(r, user.email());
+    setAssignee(r, admin.email());
     List<AccountInfo> assignees = getPastAssignees(r);
     assertThat(assignees).hasSize(2);
     Iterator<AccountInfo> itr = assignees.iterator();
-    assertThat(itr.next()._accountId).isEqualTo(user.getId().get());
-    assertThat(itr.next()._accountId).isEqualTo(admin.getId().get());
+    assertThat(itr.next()._accountId).isEqualTo(user.id().get());
+    assertThat(itr.next()._accountId).isEqualTo(admin.id().get());
   }
 
   @Test
   public void assigneeAddedAsReviewer() throws Exception {
-    ReviewerState state;
-    // Assignee is added as CC, if back-end is reviewDb (that does not support
-    // CC) CC is stored as REVIEWER
-    if (notesMigration.readChanges()) {
-      state = ReviewerState.CC;
-    } else {
-      state = ReviewerState.REVIEWER;
-    }
+    ReviewerState state = ReviewerState.CC;
     PushOneCommit.Result r = createChange();
     Iterable<AccountInfo> reviewers = getReviewers(r, state);
     assertThat(reviewers).isNull();
-    assertThat(setAssignee(r, user.email)._accountId).isEqualTo(user.getId().get());
+    assertThat(setAssignee(r, user.email())._accountId).isEqualTo(user.id().get());
     reviewers = getReviewers(r, state);
     assertThat(reviewers).hasSize(1);
     AccountInfo reviewer = Iterables.getFirst(reviewers, null);
-    assertThat(reviewer._accountId).isEqualTo(user.getId().get());
+    assertThat(reviewer._accountId).isEqualTo(user.id().get());
   }
 
   @Test
   public void setAlreadyExistingAssignee() throws Exception {
     PushOneCommit.Result r = createChange();
-    setAssignee(r, user.email);
-    assertThat(setAssignee(r, user.email)._accountId).isEqualTo(user.getId().get());
+    setAssignee(r, user.email());
+    assertThat(setAssignee(r, user.email())._accountId).isEqualTo(user.id().get());
   }
 
   @Test
   public void deleteAssignee() throws Exception {
     PushOneCommit.Result r = createChange();
-    assertThat(setAssignee(r, user.email)._accountId).isEqualTo(user.getId().get());
-    assertThat(deleteAssignee(r)._accountId).isEqualTo(user.getId().get());
+    assertThat(setAssignee(r, user.email())._accountId).isEqualTo(user.id().get());
+    assertThat(deleteAssignee(r)._accountId).isEqualTo(user.id().get());
     assertThat(getAssignee(r)).isNull();
   }
 
@@ -134,10 +129,29 @@
   @Test
   public void setAssigneeToInactiveUser() throws Exception {
     PushOneCommit.Result r = createChange();
-    gApi.accounts().id(user.getId().get()).setActive(false);
-    exception.expect(UnprocessableEntityException.class);
-    exception.expectMessage("is not active");
-    setAssignee(r, user.email);
+    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>");
+    }
+  }
+
+  @Test
+  public void setAssigneeToInactiveUserById() throws Exception {
+    PushOneCommit.Result r = createChange();
+    gApi.accounts().id(user.id().get()).setActive(false);
+    setAssignee(r, user.id().toString());
+    assertThat(getAssignee(r)._accountId).isEqualTo(user.id().get());
   }
 
   @Test
@@ -147,24 +161,24 @@
     PushOneCommit.Result r = createChange("refs/for/refs/meta/config");
     exception.expect(AuthException.class);
     exception.expectMessage("read not permitted");
-    setAssignee(r, user.email);
+    setAssignee(r, user.email());
   }
 
   @Test
   public void setAssigneeNotAllowedWithoutPermission() throws Exception {
     PushOneCommit.Result r = createChange();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("not permitted");
-    setAssignee(r, user.email);
+    setAssignee(r, user.email());
   }
 
   @Test
   public void setAssigneeAllowedWithPermission() throws Exception {
     PushOneCommit.Result r = createChange();
     grant(project, "refs/heads/master", Permission.EDIT_ASSIGNEE, false, REGISTERED_USERS);
-    setApiUser(user);
-    assertThat(setAssignee(r, user.email)._accountId).isEqualTo(user.getId().get());
+    requestScopeOperations.setApiUser(user.id());
+    assertThat(setAssignee(r, user.email())._accountId).isEqualTo(user.id().get());
   }
 
   private AccountInfo getAssignee(PushOneCommit.Result r) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeIdIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeIdIT.java
index a2ad7fc..3f1608c 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeIdIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeIdIT.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
+import com.google.gerrit.reviewdb.client.Change;
 import org.junit.Test;
 
 public class ChangeIdIT extends AbstractDaemonTest {
@@ -92,6 +93,43 @@
     res.assertNotFound();
   }
 
+  @Test
+  public void changeNumberRedirects() throws Exception {
+    // 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;
+    RestResponse res = anonymousRestSession.get("/" + changeId);
+    res.assertTemporaryRedirect("/c/" + project.get() + "/+/" + changeId + "/");
+  }
+
+  @Test
+  public void changeNumberRedirectsWithTrailingSlash() throws Exception {
+    int changeId = createChange().getChange().getId().id;
+    RestResponse res = anonymousRestSession.get("/" + changeId + "/");
+    res.assertTemporaryRedirect("/c/" + project.get() + "/+/" + changeId + "/");
+  }
+
+  @Test
+  public void changeNumberOverflowNotFound() throws Exception {
+    RestResponse res = anonymousRestSession.get("/9" + Long.MAX_VALUE);
+    res.assertNotFound();
+  }
+
+  @Test
+  public void unknownChangeNumberNotFound() throws Exception {
+    RestResponse res = anonymousRestSession.get("/10");
+    res.assertNotFound();
+  }
+
+  @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);
+    res.assertNotFound();
+  }
+
   private static String changeDetail(String changeId) {
     return "/changes/" + changeId + "/detail";
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
index 790b884..51c5fc8 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.api.changes.DeleteChangeMessageInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -40,6 +41,7 @@
 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;
 import java.util.Iterator;
@@ -54,6 +56,8 @@
 
 @RunWith(ConfigSuite.class)
 public class ChangeMessagesIT extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   private String systemTimeZone;
 
   @Before
@@ -127,6 +131,20 @@
   }
 
   @Test
+  public void listChangeMessagesSkippedEmpty() throws Exception {
+    // Change message 1: create a change.
+    PushOneCommit.Result result = createChange();
+    String changeId = result.getChangeId();
+    // Will be a new commit with empty change message on the meta branch.
+    addOneReviewWithEmptyChangeMessage(changeId);
+    // Change Message 2: post a review with message "message 1".
+    addOneReview(changeId, "message");
+
+    List<ChangeMessageInfo> messages = gApi.changes().id(changeId).messages();
+    assertThat(messages).hasSize(2);
+  }
+
+  @Test
   public void getOneChangeMessage() throws Exception {
     int changeNum = createOneChangeWithMultipleChangeMessagesInHistory();
     List<ChangeMessageInfo> messages = new ArrayList<>(gApi.changes().id(changeNum).get().messages);
@@ -139,7 +157,7 @@
   @Test
   public void deleteCannotBeAppliedWithoutAdministrateServerCapability() throws Exception {
     int changeNum = createOneChangeWithMultipleChangeMessagesInHistory();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
 
     try {
       deleteOneChangeMessage(changeNum, 0, user, "spam");
@@ -153,7 +171,7 @@
   public void deleteCanBeAppliedWithAdministrateServerCapability() throws Exception {
     allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ADMINISTRATE_SERVER);
     int changeNum = createOneChangeWithMultipleChangeMessagesInHistory();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     deleteOneChangeMessage(changeNum, 0, user, "spam");
   }
 
@@ -209,26 +227,32 @@
   private int createOneChangeWithMultipleChangeMessagesInHistory() throws Exception {
     // Creates the following commit history on the meta branch of the test change.
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     // Commit 1: create a change.
     PushOneCommit.Result result = createChange();
     String changeId = result.getChangeId();
-    // Commit 2: post a review with message "message 1".
-    setApiUser(admin);
+    // Commit 2: post an empty change message.
+    requestScopeOperations.setApiUser(admin.id());
+    addOneReviewWithEmptyChangeMessage(changeId);
+    // Commit 3: post a review with message "message 1".
     addOneReview(changeId, "message 1");
-    // Commit 3: amend a new patch set.
-    setApiUser(user);
+    // Commit 4: amend a new patch set.
+    requestScopeOperations.setApiUser(user.id());
     amendChange(changeId);
-    // Commit 4: post a review with message "message 2".
+    // Commit 5: post a review with message "message 2".
     addOneReview(changeId, "message 2");
-    // Commit 5: amend a new patch set.
+    // Commit 6: amend a new patch set.
     amendChange(changeId);
-    // Commit 6: approve the change.
-    setApiUser(admin);
+    // Commit 7: approve the change.
+    requestScopeOperations.setApiUser(admin.id());
     gApi.changes().id(changeId).current().review(ReviewInput.approve());
-    // commit 7: submit the change.
+    // commit 8: submit the change.
     gApi.changes().id(changeId).current().submit();
 
+    // Verifies there is only 7 change messages although there are 8 commits.
+    List<ChangeMessageInfo> messages = gApi.changes().id(changeId).messages();
+    assertThat(messages).hasSize(7);
+
     return result.getChange().getId().get();
   }
 
@@ -245,23 +269,24 @@
     gApi.changes().id(changeId).current().review(reviewInput);
   }
 
+  private void addOneReviewWithEmptyChangeMessage(String changeId) throws Exception {
+    gApi.changes().id(changeId).current().review(new ReviewInput());
+  }
+
   private void deleteOneChangeMessage(
       int changeNum, int deletedMessageIndex, TestAccount deletedBy, String reason)
       throws Exception {
     List<ChangeMessageInfo> messagesBeforeDeletion = gApi.changes().id(changeNum).messages();
 
     List<CommentInfo> commentsBefore = getChangeSortedComments(changeNum);
-    List<RevCommit> commitsBefore = new ArrayList<>();
-    if (notesMigration.readChanges()) {
-      commitsBefore = getChangeMetaCommitsInReverseOrder(new Change.Id(changeNum));
-    }
+    List<RevCommit> commitsBefore = getChangeMetaCommitsInReverseOrder(new Change.Id(changeNum));
 
     String id = messagesBeforeDeletion.get(deletedMessageIndex).id;
     DeleteChangeMessageInput input = new DeleteChangeMessageInput(reason);
     ChangeMessageInfo info = gApi.changes().id(changeNum).message(id).delete(input);
 
     // Verify the return change message info is as expect.
-    assertThat(info.message).isEqualTo(createNewChangeMessage(deletedBy.fullName, reason));
+    assertThat(info.message).isEqualTo(createNewChangeMessage(deletedBy.fullName(), reason));
     List<ChangeMessageInfo> messagesAfterDeletion = gApi.changes().id(changeNum).messages();
     assertMessagesAfterDeletion(
         messagesBeforeDeletion, messagesAfterDeletion, deletedMessageIndex, deletedBy, reason);
@@ -271,11 +296,8 @@
     List<ChangeInfo> changes = gApi.changes().query("message removed").get();
     assertThat(changes.stream().map(c -> c._number).collect(toSet())).contains(changeNum);
 
-    // Verifies states of commits if NoteDb is on.
-    if (notesMigration.readChanges()) {
-      assertMetaCommitsAfterDeletion(
-          commitsBefore, changeNum, deletedMessageIndex, deletedBy, reason);
-    }
+    // Verifies states of commits.
+    assertMetaCommitsAfterDeletion(commitsBefore, changeNum, id, deletedBy, reason);
   }
 
   private void assertMessagesAfterDeletion(
@@ -304,7 +326,7 @@
 
       if (i == deletedMessageIndex) {
         assertThat(after.message)
-            .isEqualTo(createNewChangeMessage(deletedBy.fullName, deleteReason));
+            .isEqualTo(createNewChangeMessage(deletedBy.fullName(), deleteReason));
       } else {
         assertThat(after.message).isEqualTo(before.message);
       }
@@ -314,7 +336,7 @@
   private void assertMetaCommitsAfterDeletion(
       List<RevCommit> commitsBeforeDeletion,
       int changeNum,
-      int deletedMessageIndex,
+      String deletedMessageId,
       TestAccount deletedBy,
       String deleteReason)
       throws Exception {
@@ -325,7 +347,7 @@
     for (int i = 0; i < commitsBeforeDeletion.size(); i++) {
       RevCommit commitBefore = commitsBeforeDeletion.get(i);
       RevCommit commitAfter = commitsAfterDeletion.get(i);
-      if (i == deletedMessageIndex) {
+      if (commitBefore.getId().getName().equals(deletedMessageId)) {
         byte[] rawBefore = commitBefore.getRawBuffer();
         byte[] rawAfter = commitAfter.getRawBuffer();
         Charset encodingBefore = RawParseUtils.parseEncoding(rawBefore);
@@ -368,7 +390,7 @@
                 rawAfter,
                 rangeAfter.get().changeMessageStart(),
                 rangeAfter.get().changeMessageEnd() + 1);
-        assertThat(message).isEqualTo(createNewChangeMessage(deletedBy.fullName, deleteReason));
+        assertThat(message).isEqualTo(createNewChangeMessage(deletedBy.fullName(), deleteReason));
       } else {
         assertThat(commitAfter.getFullMessage()).isEqualTo(commitBefore.getFullMessage());
       }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
index ac0d0aa..d51221e 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
@@ -19,23 +19,28 @@
 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.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;
 import org.eclipse.jgit.junit.TestRepository;
 import org.junit.Before;
 import org.junit.Test;
 
 public class ChangeOwnerIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   private TestAccount user2;
 
   @Before
   public void setUp() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     user2 = accountCreator.user2();
   }
 
@@ -61,22 +66,22 @@
 
   @Test
   public void testChangeOwner_OwnerACLGrantedOnParentProject() throws Exception {
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     grantApproveToChangeOwner(project);
-    Project.NameKey child = createProject("child", project);
+    Project.NameKey child = projectOperations.newProject().parent(project).create();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     TestRepository<InMemoryRepository> childRepo = cloneProject(child, user);
     approve(user, createMyChange(childRepo));
   }
 
   @Test
   public void testChangeOwner_BlockedOnParentProject() throws Exception {
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     blockApproveForChangeOwner(project);
-    Project.NameKey child = createProject("child", project);
+    Project.NameKey child = projectOperations.newProject().parent(project).create();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     grantApproveToAll(child);
     TestRepository<InMemoryRepository> childRepo = cloneProject(child, user);
     String changeId = createMyChange(childRepo);
@@ -90,11 +95,11 @@
 
   @Test
   public void testChangeOwner_BlockedOnParentProjectAndExclusiveAllowOnChild() throws Exception {
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     blockApproveForChangeOwner(project);
-    Project.NameKey child = createProject("child", project);
+    Project.NameKey child = projectOperations.newProject().parent(project).create();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     grantExclusiveApproveToAll(child);
     TestRepository<InMemoryRepository> childRepo = cloneProject(child, user);
     String changeId = createMyChange(childRepo);
@@ -107,7 +112,7 @@
   }
 
   private void approve(TestAccount a, String changeId) throws Exception {
-    Context old = setApiUser(a);
+    Context old = requestScopeOperations.setApiUser(a.id());
     try {
       gApi.changes().id(changeId).current().review(ReviewInput.approve());
     } finally {
@@ -142,7 +147,7 @@
   }
 
   private String createMyChange(TestRepository<InMemoryRepository> testRepo) throws Exception {
-    PushOneCommit push = pushFactory.create(db, user.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(user.newIdent(), testRepo);
     return push.to("refs/for/master").getChangeId();
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
index dc71c1f..ac00e38 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
 
 import com.google.common.collect.ImmutableList;
@@ -24,6 +23,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerResult;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -36,12 +36,14 @@
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.testing.FakeEmailSender.Message;
 import com.google.gson.reflect.TypeToken;
+import com.google.inject.Inject;
 import java.lang.reflect.Type;
 import java.util.List;
 import org.junit.Before;
 import org.junit.Test;
 
 public class ChangeReviewersByEmailIT extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   @Before
   public void setUp() throws Exception {
@@ -52,7 +54,6 @@
 
   @Test
   public void addByEmail() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
     AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com");
 
     for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) {
@@ -72,9 +73,8 @@
 
   @Test
   public void addByEmailAndById() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
     AccountInfo byEmail = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com");
-    AccountInfo byId = new AccountInfo(user.id.get());
+    AccountInfo byId = new AccountInfo(user.id().get());
 
     for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) {
       PushOneCommit.Result r = createChange();
@@ -85,7 +85,7 @@
       gApi.changes().id(r.getChangeId()).addReviewer(inputByEmail);
 
       AddReviewerInput inputById = new AddReviewerInput();
-      inputById.reviewer = user.email;
+      inputById.reviewer = user.email();
       inputById.state = state;
       gApi.changes().id(r.getChangeId()).addReviewer(inputById);
 
@@ -98,7 +98,6 @@
 
   @Test
   public void listReviewersByEmail() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
     AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com");
 
     for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) {
@@ -126,7 +125,6 @@
 
   @Test
   public void removeByEmail() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
     AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com");
 
     for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) {
@@ -146,7 +144,6 @@
 
   @Test
   public void convertFromCCToReviewer() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
     AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com");
 
     PushOneCommit.Result r = createChange();
@@ -168,7 +165,6 @@
 
   @Test
   public void addedReviewersGetNotified() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
     AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com");
 
     for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) {
@@ -188,7 +184,6 @@
 
   @Test
   public void removingReviewerTriggersNotification() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
     AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com");
 
     for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) {
@@ -202,9 +197,9 @@
       // Review change as user
       ReviewInput reviewInput = new ReviewInput();
       reviewInput.message = "I have a comment";
-      setApiUser(user);
+      requestScopeOperations.setApiUser(user.id());
       revision(r).review(reviewInput);
-      setApiUser(admin);
+      requestScopeOperations.setApiUser(admin.id());
 
       sender.clear();
 
@@ -214,14 +209,13 @@
       List<Message> messages = sender.getMessages();
       assertThat(messages).hasSize(1);
       assertThat(messages.get(0).rcpt())
-          .containsExactly(Address.parse(addInput.reviewer), user.emailAddress);
+          .containsExactly(Address.parse(addInput.reviewer), user.getEmailAddress());
       sender.clear();
     }
   }
 
   @Test
   public void reviewerAndCCReceiveRegularNotification() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
     AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com");
 
     for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) {
@@ -244,8 +238,6 @@
 
   @Test
   public void reviewerAndCCReceiveSameEmail() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     PushOneCommit.Result r = createChange();
     for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) {
       for (int i = 0; i < 10; i++) {
@@ -258,7 +250,7 @@
 
     // Also add user as a regular reviewer
     AddReviewerInput input = new AddReviewerInput();
-    input.reviewer = user.email;
+    input.reviewer = user.email();
     input.state = ReviewerState.REVIEWER;
     gApi.changes().id(r.getChangeId()).addReviewer(input);
 
@@ -270,8 +262,6 @@
 
   @Test
   public void addingMultipleReviewersAndCCsAtOnceSendsOnlyOneEmail() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     PushOneCommit.Result r = createChange();
     ReviewInput reviewInput = new ReviewInput();
     for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) {
@@ -288,7 +278,6 @@
 
   @Test
   public void rejectMissingEmail() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
     PushOneCommit.Result r = createChange();
 
     AddReviewerResult result = gApi.changes().id(r.getChangeId()).addReviewer("");
@@ -298,7 +287,6 @@
 
   @Test
   public void rejectMalformedEmail() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
     PushOneCommit.Result r = createChange();
 
     AddReviewerResult result = gApi.changes().id(r.getChangeId()).addReviewer("Foo Bar <foo.bar@");
@@ -308,8 +296,6 @@
 
   @Test
   public void rejectWhenFeatureIsDisabled() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     ConfigInput conf = new ConfigInput();
     conf.enableReviewerByEmail = InheritableBoolean.FALSE;
     gApi.projects().name(project.get()).config(conf);
@@ -320,13 +306,14 @@
         gApi.changes().id(r.getChangeId()).addReviewer("Foo Bar <foo.bar@gerritcodereview.com>");
     assertThat(result.error)
         .isEqualTo(
-            "Foo Bar <foo.bar@gerritcodereview.com> does not identify a registered user or group");
+            "Account 'Foo Bar <foo.bar@gerritcodereview.com>' not found\n"
+                + "Foo Bar <foo.bar@gerritcodereview.com> does not identify a registered user or"
+                + " group");
     assertThat(result.reviewers).isNull();
   }
 
   @Test
   public void reviewersByEmailAreServedFromIndex() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
     AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com");
 
     for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) {
@@ -337,21 +324,17 @@
       input.state = state;
       gApi.changes().id(r.getChangeId()).addReviewer(input);
 
-      notesMigration.setFailOnLoadForTest(true);
-      try {
+      try (AutoCloseable ignored = disableNoteDb()) {
         ChangeInfo info =
             Iterables.getOnlyElement(
                 gApi.changes().query(r.getChangeId()).withOption(DETAILED_LABELS).get());
         assertThat(info.reviewers).isEqualTo(ImmutableMap.of(state, ImmutableList.of(acc)));
-      } finally {
-        notesMigration.setFailOnLoadForTest(false);
       }
     }
   }
 
   @Test
   public void addExistingReviewerByEmailShortCircuits() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
     PushOneCommit.Result r = createChange();
 
     AddReviewerInput input = new AddReviewerInput();
@@ -369,7 +352,6 @@
 
   @Test
   public void addExistingCcByEmailShortCircuits() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
     PushOneCommit.Result r = createChange();
 
     AddReviewerInput input = new AddReviewerInput();
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
index d1f4d84..173b78d 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.TruthJUnit.assume;
 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;
@@ -31,6 +30,8 @@
 import com.google.gerrit.acceptance.RestResponse;
 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.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerResult;
@@ -52,6 +53,7 @@
 import com.google.gerrit.server.change.ReviewerAdder;
 import com.google.gerrit.testing.FakeEmailSender.Message;
 import com.google.gson.stream.JsonReader;
+import com.google.inject.Inject;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -62,19 +64,23 @@
 import org.junit.Test;
 
 public class ChangeReviewersIT extends AbstractDaemonTest {
+
+  @Inject private GroupOperations groupOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @Test
   public void addGroupAsReviewer() throws Exception {
     // Set up two groups, one that is too large too add as reviewer, and one
     // that is too large to add without confirmation.
-    String largeGroup = createGroup("largeGroup");
-    String mediumGroup = createGroup("mediumGroup");
+    String largeGroup = groupOperations.newGroup().name("largeGroup").create().get();
+    String mediumGroup = groupOperations.newGroup().name("mediumGroup").create().get();
 
     int largeGroupSize = ReviewerAdder.DEFAULT_MAX_REVIEWERS + 1;
     int mediumGroupSize = ReviewerAdder.DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK + 1;
     List<TestAccount> users = createAccounts(largeGroupSize, "addGroupAsReviewer");
     List<String> largeGroupUsernames = new ArrayList<>(mediumGroupSize);
     for (TestAccount u : users) {
-      largeGroupUsernames.add(u.username);
+      largeGroupUsernames.add(u.username());
     }
     List<String> mediumGroupUsernames = largeGroupUsernames.subList(0, mediumGroupSize);
     gApi.groups()
@@ -121,39 +127,26 @@
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     in.state = CC;
     AddReviewerResult result = addReviewer(changeId, in);
 
-    assertThat(result.input).isEqualTo(user.email);
+    assertThat(result.input).isEqualTo(user.email());
     assertThat(result.confirm).isNull();
     assertThat(result.error).isNull();
     ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
-    if (notesMigration.readChanges()) {
-      assertThat(result.reviewers).isNull();
-      assertThat(result.ccs).hasSize(1);
-      AccountInfo ai = result.ccs.get(0);
-      assertThat(ai._accountId).isEqualTo(user.id.get());
-      assertReviewers(c, CC, user);
-    } else {
-      assertThat(result.ccs).isNull();
-      assertThat(result.reviewers).hasSize(1);
-      AccountInfo ai = result.reviewers.get(0);
-      assertThat(ai._accountId).isEqualTo(user.id.get());
-      assertReviewers(c, REVIEWER, user);
-    }
+    assertThat(result.reviewers).isNull();
+    assertThat(result.ccs).hasSize(1);
+    AccountInfo ai = result.ccs.get(0);
+    assertThat(ai._accountId).isEqualTo(user.id().get());
+    assertReviewers(c, CC, user);
 
     // Verify email was sent to CCed account.
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     Message m = messages.get(0);
-    assertThat(m.rcpt()).containsExactly(user.emailAddress);
-    if (notesMigration.readChanges()) {
-      assertThat(m.body()).contains(admin.fullName + " has uploaded this change for review.");
-    } else {
-      assertThat(m.body()).contains("Hello " + user.fullName + ",\n");
-      assertThat(m.body()).contains("I'd like you to do a code review.");
-    }
+    assertThat(m.rcpt()).containsExactly(user.getEmailAddress());
+    assertThat(m.body()).contains(admin.fullName() + " has uploaded this change for review.");
   }
 
   @Test
@@ -161,7 +154,7 @@
     List<TestAccount> users = createAccounts(6, "addCcGroup");
     List<String> usernames = new ArrayList<>(6);
     for (TestAccount u : users) {
-      usernames.add(u.username);
+      usernames.add(u.username());
     }
 
     List<TestAccount> firstUsers = users.subList(0, 3);
@@ -170,7 +163,7 @@
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = createGroup("cc1");
+    in.reviewer = groupOperations.newGroup().name("cc1").create().get();
     in.state = CC;
     gApi.groups()
         .id(in.reviewer)
@@ -180,18 +173,9 @@
     assertThat(result.input).isEqualTo(in.reviewer);
     assertThat(result.confirm).isNull();
     assertThat(result.error).isNull();
-    if (notesMigration.readChanges()) {
-      assertThat(result.reviewers).isNull();
-    } else {
-      assertThat(result.ccs).isNull();
-    }
+    assertThat(result.reviewers).isNull();
     ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
-    if (notesMigration.readChanges()) {
-      assertReviewers(c, CC, firstUsers);
-    } else {
-      assertReviewers(c, REVIEWER, firstUsers);
-      assertReviewers(c, CC);
-    }
+    assertReviewers(c, CC, firstUsers);
 
     // Verify emails were sent to each of the group's accounts.
     List<Message> messages = sender.getMessages();
@@ -199,51 +183,37 @@
     Message m = messages.get(0);
     List<Address> expectedAddresses = new ArrayList<>(firstUsers.size());
     for (TestAccount u : firstUsers) {
-      expectedAddresses.add(u.emailAddress);
+      expectedAddresses.add(u.getEmailAddress());
     }
     assertThat(m.rcpt()).containsExactlyElementsIn(expectedAddresses);
 
     // CC a group that overlaps with some existing reviewers and CCed accounts.
     TestAccount reviewer =
         accountCreator.create(name("reviewer"), "addCcGroup-reviewer@example.com", "Reviewer");
-    result = addReviewer(changeId, reviewer.username);
+    result = addReviewer(changeId, reviewer.username());
     assertThat(result.error).isNull();
     sender.clear();
-    in.reviewer = createGroup("cc2");
+    in.reviewer = groupOperations.newGroup().name("cc2").create().get();
     gApi.groups().id(in.reviewer).addMembers(usernames.toArray(new String[usernames.size()]));
-    gApi.groups().id(in.reviewer).addMembers(reviewer.username);
+    gApi.groups().id(in.reviewer).addMembers(reviewer.username());
     result = addReviewer(changeId, in);
     assertThat(result.input).isEqualTo(in.reviewer);
     assertThat(result.confirm).isNull();
     assertThat(result.error).isNull();
     c = gApi.changes().id(r.getChangeId()).get();
-    if (notesMigration.readChanges()) {
-      assertThat(result.ccs).hasSize(3);
-      assertThat(result.reviewers).isNull();
-      assertReviewers(c, REVIEWER, reviewer);
-      assertReviewers(c, CC, users);
-    } else {
-      assertThat(result.ccs).isNull();
-      assertThat(result.reviewers).hasSize(3);
-      List<TestAccount> expectedUsers = new ArrayList<>(users.size() + 2);
-      expectedUsers.addAll(users);
-      expectedUsers.add(reviewer);
-      assertReviewers(c, REVIEWER, expectedUsers);
-    }
+    assertThat(result.ccs).hasSize(3);
+    assertThat(result.reviewers).isNull();
+    assertReviewers(c, REVIEWER, reviewer);
+    assertReviewers(c, CC, users);
 
     messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     m = messages.get(0);
     expectedAddresses = new ArrayList<>(4);
     for (int i = 0; i < 3; i++) {
-      expectedAddresses.add(users.get(users.size() - i - 1).emailAddress);
+      expectedAddresses.add(users.get(users.size() - i - 1).getEmailAddress());
     }
-    if (!notesMigration.readChanges()) {
-      for (int i = 0; i < 3; i++) {
-        expectedAddresses.add(users.get(i).emailAddress);
-      }
-    }
-    expectedAddresses.add(reviewer.emailAddress);
+    expectedAddresses.add(reviewer.getEmailAddress());
     assertThat(m.rcpt()).containsExactlyElementsIn(expectedAddresses);
   }
 
@@ -252,17 +222,12 @@
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     in.state = CC;
     addReviewer(changeId, in);
     ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
-    if (notesMigration.readChanges()) {
-      assertReviewers(c, REVIEWER);
-      assertReviewers(c, CC, user);
-    } else {
-      assertReviewers(c, REVIEWER, user);
-      assertReviewers(c, CC);
-    }
+    assertReviewers(c, REVIEWER);
+    assertReviewers(c, CC, user);
 
     in.state = REVIEWER;
     addReviewer(changeId, in);
@@ -288,15 +253,8 @@
 
     // Verify user is added to CC list.
     ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
-    if (notesMigration.readChanges()) {
-      assertReviewers(c, REVIEWER);
-      assertReviewers(c, CC, user);
-    } else {
-      // If we aren't reading from NoteDb, the user will appear as a
-      // reviewer.
-      assertReviewers(c, REVIEWER, user);
-      assertReviewers(c, CC);
-    }
+    assertReviewers(c, REVIEWER);
+    assertReviewers(c, CC, user);
   }
 
   @Test
@@ -305,7 +263,7 @@
     PushOneCommit.Result r = createChange();
 
     // user adds self as REVIEWER.
-    ReviewInput input = new ReviewInput().reviewer(user.username);
+    ReviewInput input = new ReviewInput().reviewer(user.username());
     RestResponse resp =
         userRestSession.post(
             "/changes/" + r.getChangeId() + "/revisions/" + r.getCommit().getName() + "/review",
@@ -324,7 +282,7 @@
     assertThat(label.all).isNotNull();
     assertThat(label.all).hasSize(1);
     ApprovalInfo approval = label.all.get(0);
-    assertThat(approval._accountId).isEqualTo(user.getId().get());
+    assertThat(approval._accountId).isEqualTo(user.id().get());
   }
 
   @Test
@@ -333,7 +291,7 @@
     PushOneCommit.Result r = createChange();
 
     // user adds self as CC.
-    ReviewInput input = new ReviewInput().reviewer(user.username, CC, false);
+    ReviewInput input = new ReviewInput().reviewer(user.username(), CC, false);
     RestResponse resp =
         userRestSession.post(
             "/changes/" + r.getChangeId() + "/revisions/" + r.getCommit().getName() + "/review",
@@ -345,26 +303,13 @@
 
     // Verify reviewer state.
     ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
-    if (notesMigration.readChanges()) {
-      assertReviewers(c, REVIEWER);
-      assertReviewers(c, CC, user);
-      // Verify no approvals were added.
-      assertThat(c.labels).isNotNull();
-      LabelInfo label = c.labels.get("Code-Review");
-      assertThat(label).isNotNull();
-      assertThat(label.all).isNull();
-    } else {
-      // When approvals are stored in ReviewDb, we still create a label for
-      // the reviewing user, and force them into the REVIEWER state.
-      assertReviewers(c, REVIEWER, user);
-      assertReviewers(c, CC);
-      LabelInfo label = c.labels.get("Code-Review");
-      assertThat(label).isNotNull();
-      assertThat(label.all).isNotNull();
-      assertThat(label.all).hasSize(1);
-      ApprovalInfo approval = label.all.get(0);
-      assertThat(approval._accountId).isEqualTo(user.getId().get());
-    }
+    assertReviewers(c, REVIEWER);
+    assertReviewers(c, CC, user);
+    // Verify no approvals were added.
+    assertThat(c.labels).isNotNull();
+    LabelInfo label = c.labels.get("Code-Review");
+    assertThat(label).isNotNull();
+    assertThat(label.all).isNull();
   }
 
   @Test
@@ -381,7 +326,7 @@
     assertThat(label.all).isNull();
 
     // Add user as REVIEWER.
-    ReviewInput input = new ReviewInput().reviewer(user.username);
+    ReviewInput input = new ReviewInput().reviewer(user.username());
     ReviewResult result = review(r.getChangeId(), r.getCommit().name(), input);
     assertThat(result.labels).isNull();
     assertThat(result.reviewers).isNotNull();
@@ -400,8 +345,8 @@
     for (ApprovalInfo approval : label.all) {
       approvals.put(approval._accountId, approval.value);
     }
-    assertThat(approvals).containsEntry(admin.getId().get(), 0);
-    assertThat(approvals).containsEntry(user.getId().get(), 0);
+    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
     // then replace it with the default value.
@@ -425,8 +370,8 @@
     for (ApprovalInfo approval : label.all) {
       approvals.put(approval._accountId, approval.value);
     }
-    assertThat(approvals).containsEntry(admin.getId().get(), 0);
-    assertThat(approvals).containsEntry(user.getId().get(), 0);
+    assertThat(approvals).containsEntry(admin.id().get(), 0);
+    assertThat(approvals).containsEntry(user.id().get(), 0);
   }
 
   @Test
@@ -434,7 +379,7 @@
     TestAccount observer = accountCreator.user2();
     PushOneCommit.Result r = createChange();
     ReviewInput input =
-        ReviewInput.approve().reviewer(user.email).reviewer(observer.email, CC, false);
+        ReviewInput.approve().reviewer(user.email()).reviewer(observer.email(), CC, false);
 
     ReviewResult result = review(r.getChangeId(), r.getCommit().name(), input);
     assertThat(result.labels).isNotNull();
@@ -444,28 +389,22 @@
     // Verify reviewer and CC were added. If not in NoteDb read mode, both
     // parties will be returned as CCed.
     ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
-    if (notesMigration.readChanges()) {
-      assertReviewers(c, REVIEWER, admin, user);
-      assertReviewers(c, CC, observer);
-    } else {
-      // In legacy mode, everyone should be a reviewer.
-      assertReviewers(c, REVIEWER, admin, user, observer);
-      assertReviewers(c, CC);
-    }
+    assertReviewers(c, REVIEWER, admin, user);
+    assertReviewers(c, CC, observer);
 
     // Verify emails were sent to added reviewers.
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(2);
 
     Message m = messages.get(0);
-    assertThat(m.rcpt()).containsExactly(user.emailAddress, observer.emailAddress);
-    assertThat(m.body()).contains(admin.fullName + " has posted comments on this change.");
+    assertThat(m.rcpt()).containsExactly(user.getEmailAddress(), observer.getEmailAddress());
+    assertThat(m.body()).contains(admin.fullName() + " has posted comments on this change.");
     assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
     assertThat(m.body()).contains("Patch Set 1: Code-Review+2");
 
     m = messages.get(1);
-    assertThat(m.rcpt()).containsExactly(user.emailAddress, observer.emailAddress);
-    assertThat(m.body()).contains("Hello " + user.fullName + ",\n");
+    assertThat(m.rcpt()).containsExactly(user.getEmailAddress(), observer.getEmailAddress());
+    assertThat(m.body()).contains("Hello " + user.fullName() + ",\n");
     assertThat(m.body()).contains("I'd like you to do a code review.");
   }
 
@@ -476,11 +415,11 @@
     List<TestAccount> users = createAccounts(largeGroupSize, "reviewAndAddGroupReviewers");
     List<String> usernames = new ArrayList<>(largeGroupSize);
     for (TestAccount u : users) {
-      usernames.add(u.username);
+      usernames.add(u.username());
     }
 
-    String largeGroup = createGroup("largeGroup");
-    String mediumGroup = createGroup("mediumGroup");
+    String largeGroup = groupOperations.newGroup().name("largeGroup").create().get();
+    String mediumGroup = groupOperations.newGroup().name("mediumGroup").create().get();
     gApi.groups().id(largeGroup).addMembers(usernames.toArray(new String[largeGroupSize]));
     gApi.groups()
         .id(mediumGroup)
@@ -492,8 +431,8 @@
     // Attempt to add overly large group as reviewers.
     ReviewInput input =
         ReviewInput.approve()
-            .reviewer(user.email)
-            .reviewer(observer.email, CC, false)
+            .reviewer(user.email())
+            .reviewer(observer.email(), CC, false)
             .reviewer(largeGroup);
     ReviewResult result = review(r.getChangeId(), r.getCommit().name(), input, SC_BAD_REQUEST);
     assertThat(result.labels).isNull();
@@ -515,8 +454,8 @@
     // confirmation, as reviewers.
     input =
         ReviewInput.approve()
-            .reviewer(user.email)
-            .reviewer(observer.email, CC, false)
+            .reviewer(user.email())
+            .reviewer(observer.email(), CC, false)
             .reviewer(mediumGroup);
     result = review(r.getChangeId(), r.getCommit().name(), input, SC_BAD_REQUEST);
     assertThat(result.labels).isNull();
@@ -535,7 +474,7 @@
     assertThat(c.reviewers.get(CC)).isNull();
 
     // Retrying with confirmation should successfully approve and add reviewers/CCs.
-    input = ReviewInput.approve().reviewer(user.email).reviewer(mediumGroup, CC, true);
+    input = ReviewInput.approve().reviewer(user.email()).reviewer(mediumGroup, CC, true);
     result = review(r.getChangeId(), r.getCommit().name(), input);
     assertThat(result.labels).isNotNull();
     assertThat(result.reviewers).isNotNull();
@@ -544,27 +483,16 @@
     c = gApi.changes().id(r.getChangeId()).get();
     assertThat(c.messages).hasSize(2);
 
-    if (notesMigration.readChanges()) {
-      assertReviewers(c, REVIEWER, admin, user);
-      assertReviewers(c, CC, users.subList(0, mediumGroupSize));
-    } else {
-      // If not in NoteDb mode, then everyone is a REVIEWER.
-      List<TestAccount> expected = users.subList(0, mediumGroupSize);
-      expected.add(admin);
-      expected.add(user);
-      assertReviewers(c, REVIEWER, expected);
-      assertReviewers(c, CC);
-    }
+    assertReviewers(c, REVIEWER, admin, user);
+    assertReviewers(c, CC, users.subList(0, mediumGroupSize));
   }
 
   @Test
   public void noteDbAddReviewerToReviewerChangeInfo() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     in.state = CC;
     addReviewer(changeId, in);
 
@@ -573,7 +501,7 @@
 
     gApi.changes().id(changeId).current().review(ReviewInput.dislike());
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     // NoteDb adds reviewer to a change on every review.
     gApi.changes().id(changeId).current().review(ReviewInput.dislike());
 
@@ -586,28 +514,28 @@
     Iterator<ReviewerUpdateInfo> it = c.reviewerUpdates.iterator();
     ReviewerUpdateInfo reviewerChange = it.next();
     assertThat(reviewerChange.state).isEqualTo(CC);
-    assertThat(reviewerChange.reviewer._accountId).isEqualTo(user.getId().get());
-    assertThat(reviewerChange.updatedBy._accountId).isEqualTo(admin.getId().get());
+    assertThat(reviewerChange.reviewer._accountId).isEqualTo(user.id().get());
+    assertThat(reviewerChange.updatedBy._accountId).isEqualTo(admin.id().get());
 
     reviewerChange = it.next();
     assertThat(reviewerChange.state).isEqualTo(REVIEWER);
-    assertThat(reviewerChange.reviewer._accountId).isEqualTo(user.getId().get());
-    assertThat(reviewerChange.updatedBy._accountId).isEqualTo(admin.getId().get());
+    assertThat(reviewerChange.reviewer._accountId).isEqualTo(user.id().get());
+    assertThat(reviewerChange.updatedBy._accountId).isEqualTo(admin.id().get());
 
     reviewerChange = it.next();
     assertThat(reviewerChange.state).isEqualTo(REMOVED);
-    assertThat(reviewerChange.reviewer._accountId).isEqualTo(user.getId().get());
-    assertThat(reviewerChange.updatedBy._accountId).isEqualTo(admin.getId().get());
+    assertThat(reviewerChange.reviewer._accountId).isEqualTo(user.id().get());
+    assertThat(reviewerChange.updatedBy._accountId).isEqualTo(admin.id().get());
   }
 
   @Test
   public void addDuplicateReviewers() throws Exception {
     PushOneCommit.Result r = createChange();
-    ReviewInput input = ReviewInput.approve().reviewer(user.email).reviewer(user.email);
+    ReviewInput input = ReviewInput.approve().reviewer(user.email()).reviewer(user.email());
     ReviewResult result = review(r.getChangeId(), r.getCommit().name(), input);
     assertThat(result.reviewers).isNotNull();
     assertThat(result.reviewers).hasSize(1);
-    AddReviewerResult reviewerResult = result.reviewers.get(user.email);
+    AddReviewerResult reviewerResult = result.reviewers.get(user.email());
     assertThat(reviewerResult).isNotNull();
     assertThat(reviewerResult.confirm).isNull();
     assertThat(reviewerResult.error).isNull();
@@ -622,10 +550,10 @@
         accountCreator.create(name("user2"), emailPrefix + "user2@example.com", "User2");
     TestAccount user3 =
         accountCreator.create(name("user3"), emailPrefix + "user3@example.com", "User3");
-    String group1 = createGroup("group1");
-    String group2 = createGroup("group2");
-    gApi.groups().id(group1).addMembers(user1.username, user2.username);
-    gApi.groups().id(group2).addMembers(user2.username, user3.username);
+    String group1 = groupOperations.newGroup().name("group1").create().get();
+    String group2 = groupOperations.newGroup().name("group2").create().get();
+    gApi.groups().id(group1).addMembers(user1.username(), user2.username());
+    gApi.groups().id(group2).addMembers(user2.username(), user3.username());
 
     PushOneCommit.Result r = createChange();
     ReviewInput input = ReviewInput.approve().reviewer(group1).reviewer(group2);
@@ -640,9 +568,6 @@
     assertThat(reviewerResult.reviewers).hasSize(1);
 
     // Repeat the above for CCs
-    if (!notesMigration.readChanges()) {
-      return;
-    }
     r = createChange();
     input = ReviewInput.approve().reviewer(group1, CC, false).reviewer(group2, CC, false);
     result = review(r.getChangeId(), r.getCommit().name(), input);
@@ -675,7 +600,7 @@
   public void removingReviewerRemovesTheirVote() throws Exception {
     String crLabel = "Code-Review";
     PushOneCommit.Result r = createChange();
-    ReviewInput input = ReviewInput.approve().reviewer(admin.email);
+    ReviewInput input = ReviewInput.approve().reviewer(admin.email());
     ReviewResult addResult = review(r.getChangeId(), r.getCommit().name(), input);
     assertThat(addResult.reviewers).isNotNull();
     assertThat(addResult.reviewers).hasSize(1);
@@ -690,7 +615,7 @@
     assertThat(changeLabels.get(crLabel).all).isNull();
 
     // Check that the vote is gone even after the reviewer is added back
-    addReviewer(r.getChangeId(), admin.email);
+    addReviewer(r.getChangeId(), admin.email());
     changeLabels = getChangeLabels(r.getChangeId());
     assertThat(changeLabels.get(crLabel).all).isNull();
   }
@@ -701,15 +626,15 @@
     TestAccount userToNotify = createAccounts(1, "notify-details-post-review").get(0);
 
     ReviewInput reviewInput = new ReviewInput();
-    reviewInput.reviewer(user.email, ReviewerState.REVIEWER, true);
+    reviewInput.reviewer(user.email(), ReviewerState.REVIEWER, true);
     reviewInput.notify = NotifyHandling.NONE;
     reviewInput.notifyDetails =
-        ImmutableMap.of(RecipientType.TO, new NotifyInfo(ImmutableList.of(userToNotify.email)));
+        ImmutableMap.of(RecipientType.TO, new NotifyInfo(ImmutableList.of(userToNotify.email())));
 
     sender.clear();
     gApi.changes().id(r.getChangeId()).current().review(reviewInput);
     assertThat(sender.getMessages()).hasSize(1);
-    assertThat(sender.getMessages().get(0).rcpt()).containsExactly(userToNotify.emailAddress);
+    assertThat(sender.getMessages().get(0).rcpt()).containsExactly(userToNotify.getEmailAddress());
   }
 
   @Test
@@ -718,15 +643,15 @@
     TestAccount userToNotify = createAccounts(1, "notify-details-post-reviewers").get(0);
 
     AddReviewerInput addReviewer = new AddReviewerInput();
-    addReviewer.reviewer = user.email;
+    addReviewer.reviewer = user.email();
     addReviewer.notify = NotifyHandling.NONE;
     addReviewer.notifyDetails =
-        ImmutableMap.of(RecipientType.TO, new NotifyInfo(ImmutableList.of(userToNotify.email)));
+        ImmutableMap.of(RecipientType.TO, new NotifyInfo(ImmutableList.of(userToNotify.email())));
 
     sender.clear();
     gApi.changes().id(r.getChangeId()).addReviewer(addReviewer);
     assertThat(sender.getMessages()).hasSize(1);
-    assertThat(sender.getMessages().get(0).rcpt()).containsExactly(userToNotify.emailAddress);
+    assertThat(sender.getMessages().get(0).rcpt()).containsExactly(userToNotify.getEmailAddress());
   }
 
   @Test
@@ -734,12 +659,12 @@
     PushOneCommit.Result r = createChange();
     TestAccount newUser = createAccounts(1, name("foo")).get(0);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(r.getChangeId()).current().review(new ReviewInput().label("Code-Review", 1));
-    setApiUser(newUser);
+    requestScopeOperations.setApiUser(newUser.id());
     exception.expect(AuthException.class);
     exception.expectMessage("remove reviewer not permitted");
-    gApi.changes().id(r.getChangeId()).reviewer(user.email).remove();
+    gApi.changes().id(r.getChangeId()).reviewer(user.email()).remove();
   }
 
   @Test
@@ -751,10 +676,10 @@
     TestAccount newUser = createAccounts(1, name("foo")).get(0);
     grant(project, RefNames.REFS + "*", Permission.REMOVE_REVIEWER, false, REGISTERED_USERS);
 
-    gApi.changes().id(r.getChangeId()).addReviewer(user.email);
+    gApi.changes().id(r.getChangeId()).addReviewer(user.email());
     assertThatUserIsOnlyReviewer(r.getChangeId());
-    setApiUser(newUser);
-    gApi.changes().id(r.getChangeId()).reviewer(user.email).remove();
+    requestScopeOperations.setApiUser(newUser.id());
+    gApi.changes().id(r.getChangeId()).reviewer(user.email()).remove();
     assertThat(gApi.changes().id(r.getChangeId()).get().reviewers).isEmpty();
   }
 
@@ -763,11 +688,11 @@
     PushOneCommit.Result r = createChange();
     TestAccount newUser = createAccounts(1, name("foo")).get(0);
 
-    gApi.changes().id(r.getChangeId()).addReviewer(user.email);
-    setApiUser(newUser);
+    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();
+    gApi.changes().id(r.getChangeId()).reviewer(user.email()).remove();
   }
 
   @Test
@@ -776,53 +701,51 @@
     TestAccount newUser = createAccounts(1, name("foo")).get(0);
 
     AddReviewerInput input = new AddReviewerInput();
-    input.reviewer = user.email;
+    input.reviewer = user.email();
     input.state = ReviewerState.CC;
     gApi.changes().id(r.getChangeId()).addReviewer(input);
-    setApiUser(newUser);
+    requestScopeOperations.setApiUser(newUser.id());
     exception.expect(AuthException.class);
     exception.expectMessage("remove reviewer not permitted");
-    gApi.changes().id(r.getChangeId()).reviewer(user.email).remove();
+    gApi.changes().id(r.getChangeId()).reviewer(user.email()).remove();
   }
 
   @Test
   public void addExistingReviewerShortCircuits() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
     PushOneCommit.Result r = createChange();
 
     AddReviewerInput input = new AddReviewerInput();
-    input.reviewer = user.email;
+    input.reviewer = user.email();
     input.state = ReviewerState.REVIEWER;
 
     AddReviewerResult result = gApi.changes().id(r.getChangeId()).addReviewer(input);
     assertThat(result.reviewers).hasSize(1);
     ReviewerInfo info = result.reviewers.get(0);
-    assertThat(info._accountId).isEqualTo(user.id.get());
+    assertThat(info._accountId).isEqualTo(user.id().get());
 
     assertThat(gApi.changes().id(r.getChangeId()).addReviewer(input).reviewers).isEmpty();
   }
 
   @Test
   public void addExistingCcShortCircuits() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
     PushOneCommit.Result r = createChange();
 
     AddReviewerInput input = new AddReviewerInput();
-    input.reviewer = user.email;
+    input.reviewer = user.email();
     input.state = ReviewerState.CC;
 
     AddReviewerResult result = gApi.changes().id(r.getChangeId()).addReviewer(input);
     assertThat(result.ccs).hasSize(1);
     AccountInfo info = result.ccs.get(0);
-    assertThat(info._accountId).isEqualTo(user.id.get());
+    assertThat(info._accountId).isEqualTo(user.id().get());
 
     assertThat(gApi.changes().id(r.getChangeId()).addReviewer(input).ccs).isEmpty();
   }
 
   private void assertThatUserIsOnlyReviewer(String changeId) throws Exception {
-    AccountInfo userInfo = new AccountInfo(user.fullName, user.emailAddress.getEmail());
-    userInfo._accountId = user.id.get();
-    userInfo.username = user.username;
+    AccountInfo userInfo = new AccountInfo(user.fullName(), user.getEmailAddress().getEmail());
+    userInfo._accountId = user.id().get();
+    userInfo.username = user.username();
     assertThat(gApi.changes().id(changeId).get().reviewers)
         .containsExactly(ReviewerState.REVIEWER, ImmutableList.of(userInfo));
   }
@@ -849,7 +772,7 @@
   }
 
   private RestResponse deleteReviewer(String changeId, TestAccount account) throws Exception {
-    return adminRestSession.delete("/changes/" + changeId + "/reviewers/" + account.getId().get());
+    return adminRestSession.delete("/changes/" + changeId + "/reviewers/" + account.id().get());
   }
 
   private ReviewResult review(String changeId, String revisionId, ReviewInput in) throws Exception {
@@ -893,7 +816,7 @@
     }
     List<Integer> expectedAccountIds = new ArrayList<>();
     for (TestAccount account : accounts) {
-      expectedAccountIds.add(account.getId().get());
+      expectedAccountIds.add(account.id().get());
     }
     assertThat(actualAccountIds).containsExactlyElementsIn(expectedAccountIds);
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
index baf56de..9a907aa 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
@@ -31,6 +32,7 @@
 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;
@@ -43,6 +45,8 @@
 import org.junit.Test;
 
 public class ConfigChangeIT extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @Before
   public void setUp() throws Exception {
     try (ProjectConfigUpdate u = updateProject(project)) {
@@ -52,7 +56,7 @@
       u.save();
     }
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     fetchRefsMetaConfig();
   }
 
@@ -96,13 +100,13 @@
   @Test
   @TestProjectInput(cloneAs = "user")
   public void onlyAdminMayUpdateProjectParent() throws Exception {
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     ProjectInput parent = new ProjectInput();
     parent.name = name("parent");
     parent.permissionsOnly = true;
     gApi.projects().create(parent);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     Config cfg = readProjectConfig();
     assertThat(cfg.getString("access", null, "inheritFrom")).isAnyOf(null, allProjects.get());
     cfg.setString("access", null, "inheritFrom", parent.name);
@@ -132,7 +136,7 @@
     assertThat(readProjectConfig().getString("access", null, "inheritFrom"))
         .isAnyOf(null, allProjects.get());
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     gApi.changes().id(id).current().submit();
     assertThat(gApi.changes().id(id).info().status).isEqualTo(ChangeStatus.MERGED);
     assertThat(gApi.projects().name(project.get()).get().parent).isEqualTo(parent.name);
@@ -142,10 +146,10 @@
 
   @Test
   public void rejectDoubleInheritance() throws Exception {
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     // Create separate projects to test the config
-    Project.NameKey parent = createProject("projectToInheritFrom");
-    Project.NameKey child = createProject("projectWithMalformedConfig");
+    Project.NameKey parent = createProjectOverAPI("projectToInheritFrom", null, true, null);
+    Project.NameKey child = createProjectOverAPI("projectWithMalformedConfig", null, true, null);
 
     String config =
         gApi.projects()
@@ -164,7 +168,7 @@
     GitUtil.fetch(childRepo, RefNames.REFS_CONFIG + ":cfg");
     childRepo.reset("cfg");
     PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), childRepo, "Subject", "project.config", config);
+        pushFactory.create(admin.newIdent(), childRepo, "Subject", "project.config", config);
     PushOneCommit.Result res = push.to(RefNames.REFS_CONFIG);
     res.assertErrorStatus();
     res.assertMessage("cannot inherit from multiple projects");
@@ -190,12 +194,7 @@
     PushOneCommit.Result r =
         pushFactory
             .create(
-                db,
-                user.getIdent(),
-                testRepo,
-                "Update project config",
-                "project.config",
-                cfg.toText())
+                user.newIdent(), testRepo, "Update project config", "project.config", cfg.toText())
             .to("refs/for/refs/meta/config");
     r.assertOkStatus();
     return r;
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/CorsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/CorsIT.java
index 0af9708..9c42542 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/CorsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/CorsIT.java
@@ -34,6 +34,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit.Result;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -44,6 +45,7 @@
 import java.util.stream.Stream;
 import org.apache.http.Header;
 import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
 import org.apache.http.client.fluent.Executor;
 import org.apache.http.client.fluent.Request;
 import org.apache.http.cookie.Cookie;
@@ -99,6 +101,30 @@
   }
 
   @Test
+  public void originsOnNotFoundException() throws Exception {
+    String url = "/changes/999/detail";
+    check(url, true, "http://example.com", adminRestSession, 404);
+    check(url, false, "http://friendsly", adminRestSession, 404);
+  }
+
+  @Test
+  public void originsOnBadRequestException() throws Exception {
+    String url = "/config/server/caches/?format=NONSENSE";
+    check(url, true, "http://example.com", adminRestSession, 400);
+    check(url, false, "http://friendsly", adminRestSession, 400);
+  }
+
+  @Test
+  public void originsOnForbidden() throws Exception {
+    Result change = createChange();
+    // Make change private to hide it
+    gApi.changes().id(change.getChangeId()).setPrivate(true, "now private");
+    String url = "/changes/" + change.getChangeId() + "/detail";
+    check(url, true, "http://example.com", anonymousRestSession, 404);
+    check(url, false, "http://friendsly", anonymousRestSession, 404);
+  }
+
+  @Test
   public void putWithServerOriginAcceptedWithNoCorsResponseHeaders() throws Exception {
     Result change = createChange();
     String origin = adminRestSession.url();
@@ -179,7 +205,7 @@
     BasicCookieStore cookies = new BasicCookieStore();
     Executor http = Executor.newInstance().cookieStore(cookies);
 
-    Request req = Request.Get(canonicalWebUrl.get() + "/login/?account_id=" + admin.id.get());
+    Request req = Request.Get(canonicalWebUrl.get() + "/login/?account_id=" + admin.id().get());
     http.execute(req);
     String auth = null;
     for (Cookie c : cookies.getCookies()) {
@@ -247,9 +273,15 @@
   }
 
   private void check(String url, boolean accept, String origin) throws Exception {
+    check(url, accept, origin, adminRestSession, HttpStatus.SC_OK);
+  }
+
+  private void check(
+      String url, boolean accept, String origin, RestSession restSession, int httpStatusCode)
+      throws Exception {
     Header hdr = new BasicHeader(ORIGIN, origin);
-    RestResponse r = adminRestSession.getWithHeader(url, hdr);
-    r.assertOK();
+    RestResponse r = restSession.getWithHeader(url, hdr);
+    r.assertStatus(httpStatusCode);
     checkCors(r, accept, origin);
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index e388d6c..10428c5 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.common.data.Permission.READ;
 import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
@@ -28,6 +27,7 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.PushOneCommit.Result;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
@@ -44,9 +44,12 @@
 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;
 import java.util.Map;
 import org.eclipse.jgit.lib.ObjectId;
@@ -60,6 +63,8 @@
 import org.junit.Test;
 
 public class CreateChangeIT extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @BeforeClass
   public static void setTimeForTesting() {
     TestTimeUtil.resetWithClockStep(1, SECONDS);
@@ -78,6 +83,13 @@
   }
 
   @Test
+  public void createEmptyChange_NonExistingBranch() throws Exception {
+    ChangeInput ci = newChangeInput(ChangeStatus.NEW);
+    ci.branch = "non-existing";
+    assertCreateFails(ci, BadRequestException.class, "Destination branch does not exist");
+  }
+
+  @Test
   public void createEmptyChange_MissingMessage() throws Exception {
     ChangeInput ci = new ChangeInput();
     ci.project = project.get();
@@ -145,18 +157,18 @@
 
   @Test
   public void notificationsOnChangeCreation() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     watch(project.get());
 
     // check that watcher is notified
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     assertCreateSucceeds(newChangeInput(ChangeStatus.NEW));
 
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     Message m = messages.get(0);
-    assertThat(m.rcpt()).containsExactly(user.emailAddress);
-    assertThat(m.body()).contains(admin.fullName + " has uploaded this change for review.");
+    assertThat(m.rcpt()).containsExactly(user.getEmailAddress());
+    assertThat(m.body()).contains(admin.fullName() + " has uploaded this change for review.");
 
     // check that watcher is not notified if notify=NONE
     sender.clear();
@@ -175,7 +187,7 @@
       assertThat(message)
           .contains(
               String.format(
-                  "%sAdministrator <%s>", SIGNED_OFF_BY_TAG, admin.getIdent().getEmailAddress()));
+                  "%sAdministrator <%s>", SIGNED_OFF_BY_TAG, admin.newIdent().getEmailAddress()));
     } finally {
       setSignedOffByFooter(false);
     }
@@ -196,7 +208,7 @@
       assertThat(message)
           .contains(
               String.format(
-                  "%sAdministrator <%s>", SIGNED_OFF_BY_TAG, admin.getIdent().getEmailAddress()));
+                  "%sAdministrator <%s>", SIGNED_OFF_BY_TAG, admin.newIdent().getEmailAddress()));
     } finally {
       setSignedOffByFooter(false);
     }
@@ -234,6 +246,16 @@
   }
 
   @Test
+  public void createChangeWithNonExistingParentCommitFails() throws Exception {
+    ChangeInput input = newChangeInput(ChangeStatus.NEW);
+    input.baseCommit = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+    assertCreateFails(
+        input,
+        UnprocessableEntityException.class,
+        String.format("Base %s doesn't exist", input.baseCommit));
+  }
+
+  @Test
   public void createChangeWithParentCommitOnWrongBranchFails() throws Exception {
     Map<String, PushOneCommit.Result> setup =
         changeInTwoBranches("foo", "foo.txt", "bar", "bar.txt");
@@ -247,6 +269,20 @@
   }
 
   @Test
+  public void createChangeWithParentCommitWithNonExistingTargetBranch() throws Exception {
+    Result initialCommit =
+        pushFactory
+            .create(user.newIdent(), testRepo, "initial commit", "readme.txt", "initial commit")
+            .to("refs/heads/master");
+    initialCommit.assertOkStatus();
+
+    ChangeInput input = newChangeInput(ChangeStatus.NEW);
+    input.branch = "non-existing";
+    input.baseCommit = initialCommit.getCommit().getName();
+    assertCreateFails(input, BadRequestException.class, "Destination branch does not exist");
+  }
+
+  @Test
   public void createChangeOnNonExistingBaseChangeFails() throws Exception {
     ChangeInput input = newChangeInput(ChangeStatus.NEW);
     input.baseChange = "999999";
@@ -268,19 +304,7 @@
   }
 
   @Test
-  public void createChangeOnInvisibleBranchFails() throws Exception {
-    changeInTwoBranches("invisible-branch", "a.txt", "branchB", "b.txt");
-    block(project, "refs/heads/invisible-branch", READ, REGISTERED_USERS);
-
-    ChangeInput in = newChangeInput(ChangeStatus.NEW);
-    in.branch = "invisible-branch";
-    assertCreateFails(in, ResourceNotFoundException.class, "");
-  }
-
-  @Test
   public void noteDbCommit() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     ChangeInfo c = assertCreateSucceeds(newChangeInput(ChangeStatus.NEW));
     try (Repository repo = repoManager.openRepository(project);
         RevWalk rw = new RevWalk(repo)) {
@@ -290,7 +314,7 @@
       assertThat(commit.getShortMessage()).isEqualTo("Create change");
 
       PersonIdent expectedAuthor =
-          changeNoteUtil.newIdent(getAccount(admin.id), c.created, serverIdent.get());
+          changeNoteUtil.newIdent(getAccount(admin.id()), c.created, serverIdent.get());
       assertThat(commit.getAuthorIdent()).isEqualTo(expectedAuthor);
 
       assertThat(commit.getCommitterIdent())
@@ -321,6 +345,62 @@
   }
 
   @Test
+  public void createMergeChangeFailsWithConflictIfThereAreTooManyCommonPredecessors()
+      throws Exception {
+    // Create an initial commit in master.
+    Result initialCommit =
+        pushFactory
+            .create(user.newIdent(), testRepo, "initial commit", "readme.txt", "initial commit")
+            .to("refs/heads/master");
+    initialCommit.assertOkStatus();
+
+    String file = "shared.txt";
+    List<RevCommit> parents = new ArrayList<>();
+    // RecursiveMerger#MAX_BASES = 200, cannot use RecursiveMerger#MAX_BASES as it is not static.
+    int maxBases = 200;
+
+    // Create more than RecursiveMerger#MAX_BASES base commits.
+    for (int i = 1; i <= maxBases + 1; i++) {
+      parents.add(
+          testRepo
+              .commit()
+              .message("Base " + i)
+              .add(file, "content " + i)
+              .parent(initialCommit.getCommit())
+              .create());
+    }
+
+    // Create 2 branches.
+    String branchA = "branchA";
+    String branchB = "branchB";
+    createBranch(new Branch.NameKey(project, branchA));
+    createBranch(new Branch.NameKey(project, branchB));
+
+    // Push an octopus merge to both of the branches.
+    Result octopusA =
+        pushFactory
+            .create(user.newIdent(), testRepo)
+            .setParents(parents)
+            .to("refs/heads/" + branchA);
+    octopusA.assertOkStatus();
+
+    Result octopusB =
+        pushFactory
+            .create(user.newIdent(), testRepo)
+            .setParents(parents)
+            .to("refs/heads/" + branchB);
+    octopusB.assertOkStatus();
+
+    // Creating a merge commit for the 2 octopus commits fails, because they have more than
+    // RecursiveMerger#MAX_BASES common predecessors.
+    assertCreateFails(
+        newMergeChangeInput("branchA", "branchB", ""),
+        ResourceConflictException.class,
+        "Cannot create merge commit: No merge base could be determined."
+            + " Reason=TOO_MANY_MERGE_BASES.");
+  }
+
+  @Test
   public void invalidSource() throws Exception {
     changeInTwoBranches("branchA", "a.txt", "branchB", "b.txt");
     ChangeInput in = newMergeChangeInput("branchA", "invalid", "");
@@ -404,6 +484,48 @@
     assertCreateSucceeds(in);
   }
 
+  @Test
+  public void createChangeOnExistingBranchNotPermitted() throws Exception {
+    createBranch(new Branch.NameKey(project, "foo"));
+    blockRead("refs/heads/*");
+    requestScopeOperations.setApiUser(user.id());
+    ChangeInput input = newChangeInput(ChangeStatus.NEW);
+    input.branch = "foo";
+
+    assertCreateFails(input, ResourceNotFoundException.class, "ref refs/heads/foo not found");
+  }
+
+  @Test
+  public void createChangeOnNonExistingBranch() throws Exception {
+    requestScopeOperations.setApiUser(user.id());
+    ChangeInput input = newChangeInput(ChangeStatus.NEW);
+    input.branch = "foo";
+    input.newBranch = true;
+    assertCreateSucceeds(input);
+  }
+
+  @Test
+  public void createChangeOnNonExistingBranchNotPermitted() throws Exception {
+    blockRead("refs/heads/*");
+    requestScopeOperations.setApiUser(user.id());
+    ChangeInput input = newChangeInput(ChangeStatus.NEW);
+    input.branch = "foo";
+    // sets this option to be true to make sure permission check happened before this option could
+    // be considered.
+    input.newBranch = true;
+
+    assertCreateFails(input, ResourceNotFoundException.class, "ref refs/heads/foo not found");
+  }
+
+  @Test
+  public void createMergeChangeOnNonExistingBranchNotPossible() throws Exception {
+    requestScopeOperations.setApiUser(user.id());
+    ChangeInput input = newMergeChangeInput("foo", "master", "");
+    input.newBranch = true;
+    assertCreateFails(
+        input, BadRequestException.class, "Cannot create merge: destination branch does not exist");
+  }
+
   private ChangeInput newChangeInput(ChangeStatus status) {
     ChangeInput in = new ChangeInput();
     in.project = project.get();
@@ -417,12 +539,20 @@
   private ChangeInfo assertCreateSucceeds(ChangeInput in) throws Exception {
     ChangeInfo out = gApi.changes().create(in).get();
     assertThat(out.project).isEqualTo(in.project);
-    assertThat(out.branch).isEqualTo(in.branch);
+    assertThat(RefNames.fullName(out.branch)).isEqualTo(RefNames.fullName(in.branch));
     assertThat(out.subject).isEqualTo(in.subject.split("\n")[0]);
     assertThat(out.topic).isEqualTo(in.topic);
     assertThat(out.status).isEqualTo(in.status);
-    assertThat(out.isPrivate).isEqualTo(in.isPrivate);
-    assertThat(out.workInProgress).isEqualTo(in.workInProgress);
+    if (in.isPrivate) {
+      assertThat(out.isPrivate).isTrue();
+    } else {
+      assertThat(out.isPrivate).isNull();
+    }
+    if (in.workInProgress) {
+      assertThat(out.workInProgress).isTrue();
+    } else {
+      assertThat(out.workInProgress).isNull();
+    }
     assertThat(out.revisions).hasSize(1);
     assertThat(out.submitted).isNull();
     assertThat(in.status).isEqualTo(ChangeStatus.NEW);
@@ -439,12 +569,12 @@
 
   // TODO(davido): Expose setting of account preferences in the API
   private void setSignedOffByFooter(boolean value) throws Exception {
-    RestResponse r = adminRestSession.get("/accounts/" + admin.email + "/preferences");
+    RestResponse r = adminRestSession.get("/accounts/" + admin.email() + "/preferences");
     r.assertOK();
     GeneralPreferencesInfo i = newGson().fromJson(r.getReader(), GeneralPreferencesInfo.class);
     i.signedOffBy = value;
 
-    r = adminRestSession.put("/accounts/" + admin.email + "/preferences", i);
+    r = adminRestSession.put("/accounts/" + admin.email() + "/preferences", i);
     r.assertOK();
     GeneralPreferencesInfo o = newGson().fromJson(r.getReader(), GeneralPreferencesInfo.class);
 
@@ -454,7 +584,7 @@
       assertThat(o.signedOffBy).isNull();
     }
 
-    resetCurrentApiUser();
+    requestScopeOperations.resetCurrentApiUser();
   }
 
   private ChangeInput newMergeChangeInput(String targetBranch, String sourceRef, String strategy) {
@@ -488,7 +618,7 @@
     // create a initial commit in master
     Result initialCommit =
         pushFactory
-            .create(db, user.getIdent(), testRepo, "initial commit", "readme.txt", "initial commit")
+            .create(user.newIdent(), testRepo, "initial commit", "readme.txt", "initial commit")
             .to("refs/heads/master");
     initialCommit.assertOkStatus();
 
@@ -499,13 +629,13 @@
     // create a commit in branchA
     Result changeA =
         pushFactory
-            .create(db, user.getIdent(), testRepo, "change A", fileA, "A content")
+            .create(user.newIdent(), testRepo, "change A", fileA, "A content")
             .to("refs/heads/" + branchA);
     changeA.assertOkStatus();
 
     // create a commit in branchB
     PushOneCommit commitB =
-        pushFactory.create(db, user.getIdent(), testRepo, "change B", fileB, "B content");
+        pushFactory.create(user.newIdent(), testRepo, "change B", fileB, "B content");
     commitB.setParent(initialCommit.getCommit());
     Result changeB = commitB.to("refs/heads/" + branchB);
     changeB.assertOkStatus();
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java b/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java
index 940f2cf..286980f 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -29,12 +30,15 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.testing.FakeEmailSender;
 import com.google.gson.reflect.TypeToken;
+import com.google.inject.Inject;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 import org.junit.Test;
 
 public class DeleteVoteIT extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @Test
   public void deleteVoteOnChange() throws Exception {
     deleteVote(false);
@@ -51,7 +55,7 @@
 
     PushOneCommit.Result r2 = amendChange(r.getChangeId());
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     recommend(r.getChangeId());
 
     sender.clear();
@@ -60,7 +64,7 @@
             + r.getChangeId()
             + (onRevisionLevel ? ("/revisions/" + r2.getCommit().getName()) : "")
             + "/reviewers/"
-            + user.getId().toString()
+            + user.id().toString()
             + "/votes/Code-Review";
 
     RestResponse response = adminRestSession.delete(endPoint);
@@ -69,17 +73,17 @@
     List<FakeEmailSender.Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     FakeEmailSender.Message msg = messages.get(0);
-    assertThat(msg.rcpt()).containsExactly(user.emailAddress);
-    assertThat(msg.body()).contains(admin.fullName + " has removed a vote from this change.");
+    assertThat(msg.rcpt()).containsExactly(user.getEmailAddress());
+    assertThat(msg.body()).contains(admin.fullName() + " has removed a vote from this change.");
     assertThat(msg.body())
-        .contains("Removed Code-Review+1 by " + user.fullName + " <" + user.email + ">\n");
+        .contains("Removed Code-Review+1 by " + user.fullName() + " <" + user.email() + ">\n");
 
     endPoint =
         "/changes/"
             + r.getChangeId()
             + (onRevisionLevel ? ("/revisions/" + r2.getCommit().getName()) : "")
             + "/reviewers/"
-            + user.getId().toString()
+            + user.id().toString()
             + "/votes";
 
     response = adminRestSession.get(endPoint);
@@ -93,10 +97,10 @@
     ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
 
     ChangeMessageInfo message = Iterables.getLast(c.messages);
-    assertThat(message.author._accountId).isEqualTo(admin.getId().get());
+    assertThat(message.author._accountId).isEqualTo(admin.id().get());
     assertThat(message.message).isEqualTo("Removed Code-Review+1 by User <user@example.com>\n");
     assertThat(getReviewers(c.reviewers.get(REVIEWER)))
-        .containsExactlyElementsIn(ImmutableSet.of(admin.getId(), user.getId()));
+        .containsExactlyElementsIn(ImmutableSet.of(admin.id(), user.id()));
   }
 
   private Iterable<Account.Id> getReviewers(Collection<AccountInfo> r) {
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/HashtagsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
index 864f08d..c13df49 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static java.util.Objects.requireNonNull;
 import static java.util.concurrent.TimeUnit.SECONDS;
@@ -26,24 +25,20 @@
 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.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.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
 @NoHttpd
 public class HashtagsIT extends AbstractDaemonTest {
-  @Before
-  public void before() {
-    assume().that(notesMigration.readChanges()).isTrue();
-  }
-
   @BeforeClass
   public static void setTimeForTesting() {
     TestTimeUtil.resetWithClockStep(1, SECONDS);
@@ -54,6 +49,8 @@
     TestTimeUtil.useSystemTime();
   }
 
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @Test
   public void getNoHashtags() throws Exception {
     // Get on a change with no hashtags returns an empty list.
@@ -260,7 +257,7 @@
   @Test
   public void addHashtagWithoutPermissionNotAllowed() throws Exception {
     PushOneCommit.Result r = createChange();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("edit hashtags not permitted");
     addHashtags(r, "MyHashtag");
@@ -270,7 +267,7 @@
   public void addHashtagWithPermissionAllowed() throws Exception {
     PushOneCommit.Result r = createChange();
     grant(project, "refs/heads/master", Permission.EDIT_HASHTAGS, false, REGISTERED_USERS);
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     addHashtags(r, "MyHashtag");
     assertThatGet(r).containsExactly("MyHashtag");
     assertMessage(r, "Hashtag added: MyHashtag");
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
index 6555fe8..f49d1fb 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
@@ -20,17 +20,25 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 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.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;
 import org.eclipse.jgit.junit.TestRepository;
 import org.junit.Test;
 
 public class IndexChangeIT extends AbstractDaemonTest {
+  @Inject private GroupOperations groupOperations;
+  @Inject private ProjectOperations projectOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @Test
   public void indexChange() throws Exception {
     String changeId = createChange().getChangeId();
@@ -48,11 +56,12 @@
   public void indexChangeAfterOwnerLosesVisibility() throws Exception {
     // Create a test group with 2 users as members
     TestAccount user2 = accountCreator.user2();
-    String group = createGroup("test");
-    gApi.groups().id(group).addMembers("admin", "user", user2.username);
+    AccountGroup.UUID groupId = groupOperations.newGroup().name("test").create();
+    String group = groupOperations.group(groupId).get().name();
+    gApi.groups().id(group).addMembers("admin", "user", user2.username());
 
     // Create a project and restrict its visibility to the group
-    Project.NameKey p = createProject("p");
+    Project.NameKey p = projectOperations.newProject().create();
     try (ProjectConfigUpdate u = updateProject(p)) {
       Util.allow(
           u.getConfig(),
@@ -65,39 +74,39 @@
 
     // Clone it and push a change as a regular user
     TestRepository<InMemoryRepository> repo = cloneProject(p, user);
-    PushOneCommit push = pushFactory.create(db, user.getIdent(), repo);
+    PushOneCommit push = pushFactory.create(user.newIdent(), repo);
     PushOneCommit.Result result = push.to("refs/for/master");
     result.assertOkStatus();
-    assertThat(result.getChange().change().getOwner()).isEqualTo(user.id);
+    assertThat(result.getChange().change().getOwner()).isEqualTo(user.id());
     String changeId = result.getChangeId();
 
     // User can see the change and it is mergeable
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     List<ChangeInfo> changes = gApi.changes().query(changeId).get();
     assertThat(changes).hasSize(1);
     assertThat(changes.get(0).mergeable).isNotNull();
 
     // Other user can see the change and it is mergeable
-    setApiUser(user2);
+    requestScopeOperations.setApiUser(user2.id());
     changes = gApi.changes().query(changeId).get();
     assertThat(changes).hasSize(1);
     assertThat(changes.get(0).mergeable).isTrue();
 
     // Remove the user from the group so they can no longer see the project
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     gApi.groups().id(group).removeMembers("user");
 
     // User can no longer see the change
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     changes = gApi.changes().query(changeId).get();
     assertThat(changes).isEmpty();
 
     // Reindex the change
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     gApi.changes().id(changeId).index();
 
     // Other user can still see the change and it is still mergeable
-    setApiUser(user2);
+    requestScopeOperations.setApiUser(user2.id());
     changes = gApi.changes().query(changeId).get();
     assertThat(changes).hasSize(1);
     assertThat(changes.get(0).mergeable).isTrue();
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ListChangesOptionsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ListChangesOptionsIT.java
index 174280d..0db3508 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ListChangesOptionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ListChangesOptionsIT.java
@@ -48,8 +48,7 @@
     String subject = "Change subject";
     String fileName = "a.txt";
     PushOneCommit push =
-        pushFactory.create(
-            db, admin.getIdent(), testRepo, subject, fileName, content, baseChangeId);
+        pushFactory.create(admin.newIdent(), testRepo, subject, fileName, content, baseChangeId);
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
     return r;
@@ -66,7 +65,7 @@
   public void currentRevision() throws Exception {
     ChangeInfo c = get(changeId, CURRENT_REVISION);
     assertThat(c.currentRevision).isEqualTo(commitId(2));
-    assertThat(c.revisions.keySet()).containsAllIn(ImmutableSet.of(commitId(2)));
+    assertThat(c.revisions.keySet()).containsAtLeastElementsIn(ImmutableSet.of(commitId(2)));
     assertThat(c.revisions.get(commitId(2))._number).isEqualTo(3);
   }
 
@@ -75,7 +74,7 @@
     ChangeInfo c = get(changeId, CURRENT_REVISION, MESSAGES);
     assertThat(c.revisions).hasSize(1);
     assertThat(c.currentRevision).isEqualTo(commitId(2));
-    assertThat(c.revisions.keySet()).containsAllIn(ImmutableSet.of(commitId(2)));
+    assertThat(c.revisions.keySet()).containsAtLeastElementsIn(ImmutableSet.of(commitId(2)));
     assertThat(c.revisions.get(commitId(2))._number).isEqualTo(3);
   }
 
@@ -84,7 +83,7 @@
     ChangeInfo c = get(changeId, ALL_REVISIONS);
     assertThat(c.currentRevision).isEqualTo(commitId(2));
     assertThat(c.revisions.keySet())
-        .containsAllIn(ImmutableSet.of(commitId(0), commitId(1), commitId(2)));
+        .containsAtLeastElementsIn(ImmutableSet.of(commitId(0), commitId(1), commitId(2)));
     assertThat(c.revisions.get(commitId(0))._number).isEqualTo(1);
     assertThat(c.revisions.get(commitId(1))._number).isEqualTo(2);
     assertThat(c.revisions.get(commitId(2))._number).isEqualTo(3);
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
index 206cc68..2448ff881c 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
@@ -19,9 +19,11 @@
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
 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.request.RequestScopeOperations;
 import com.google.gerrit.common.data.LabelFunction;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.Permission;
@@ -30,12 +32,14 @@
 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.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.inject.Inject;
 import java.util.Arrays;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -44,6 +48,8 @@
 
 @NoHttpd
 public class MoveChangeIT extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @Test
   public void moveChangeWithShortRef() throws Exception {
     // Move change to a different branch using short ref name
@@ -141,8 +147,8 @@
         .parent(r1.getCommit())
         .parent(r2.getCommit())
         .message("Move change Merge Commit")
-        .author(admin.getIdent())
-        .committer(new PersonIdent(admin.getIdent(), testRepo.getDate()));
+        .author(admin.newIdent())
+        .committer(new PersonIdent(admin.newIdent(), testRepo.getDate()));
     RevCommit c = commitBuilder.create();
     pushHead(testRepo, "refs/for/master", false, false);
 
@@ -180,7 +186,7 @@
         r.getChange().change().getDest().get(),
         Permission.ABANDON,
         systemGroupBackend.getGroup(REGISTERED_USERS).getUUID());
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("move not permitted");
     move(r.getChangeId(), newBranch.get());
@@ -273,9 +279,9 @@
     input.label(testLabelC, -1);
     gApi.changes().id(changeId).current().review(input);
 
-    assertThat(gApi.changes().id(changeId).current().reviewer(admin.email).votes().keySet())
+    assertThat(gApi.changes().id(changeId).current().reviewer(admin.email()).votes().keySet())
         .containsExactly(codeReviewLabel, testLabelA, testLabelB, testLabelC);
-    assertThat(gApi.changes().id(changeId).current().reviewer(admin.email).votes().values())
+    assertThat(gApi.changes().id(changeId).current().reviewer(admin.email()).votes().values())
         .containsExactly((short) -2, (short) -1, (short) -1, (short) -1);
 
     // Move the change to the 'foo' branch.
@@ -284,13 +290,13 @@
     assertThat(gApi.changes().id(changeId).get().branch).isEqualTo("foo");
 
     // 'Code-Review -2' and 'Label-A -1' will be kept.
-    assertThat(gApi.changes().id(changeId).current().reviewer(admin.email).votes().values())
+    assertThat(gApi.changes().id(changeId).current().reviewer(admin.email()).votes().values())
         .containsExactly((short) -2, (short) -1, (short) 0, (short) 0);
 
     // Move the change back to 'master'.
     move(changeId, "master");
     assertThat(gApi.changes().id(changeId).get().branch).isEqualTo("master");
-    assertThat(gApi.changes().id(changeId).current().reviewer(admin.email).votes().values())
+    assertThat(gApi.changes().id(changeId).current().reviewer(admin.email()).votes().values())
         .containsExactly((short) -2, (short) -1, (short) 0, (short) 0);
   }
 
@@ -313,9 +319,9 @@
     input.label(testLabelA, -1);
     gApi.changes().id(changeId).current().review(input);
 
-    assertThat(gApi.changes().id(changeId).current().reviewer(admin.email).votes().keySet())
+    assertThat(gApi.changes().id(changeId).current().reviewer(admin.email()).votes().keySet())
         .containsExactly(testLabelA);
-    assertThat(gApi.changes().id(changeId).current().reviewer(admin.email).votes().values())
+    assertThat(gApi.changes().id(changeId).current().reviewer(admin.email()).votes().values())
         .containsExactly((short) -1);
 
     move(changeId, "foo");
@@ -332,6 +338,16 @@
     move(r.getChangeId(), null);
   }
 
+  @Test
+  @GerritConfig(name = "change.move", value = "false")
+  public void moveCanBeDisabledByConfig() throws Exception {
+    PushOneCommit.Result r = createChange();
+
+    exception.expect(MethodNotAllowedException.class);
+    exception.expectMessage("move changes endpoint is disabled");
+    move(r.getChangeId(), null);
+  }
+
   private void move(int changeNum, String destination) throws RestApiException {
     gApi.changes().id(changeNum).move(destination);
   }
@@ -348,7 +364,7 @@
   }
 
   private PushOneCommit.Result createChange(String branch, String changeId) throws Exception {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo, changeId);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo, changeId);
     PushOneCommit.Result result = push.to("refs/for/" + branch);
     result.assertOkStatus();
     return result;
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/PluginFieldsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/PluginFieldsIT.java
new file mode 100644
index 0000000..649c7ae
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/change/PluginFieldsIT.java
@@ -0,0 +1,151 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.gerrit.acceptance.AbstractPluginFieldsTest;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.common.Nullable;
+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;
+import java.util.Map;
+import org.junit.Test;
+
+public class PluginFieldsIT extends AbstractPluginFieldsTest {
+  private static final Gson GSON = OutputFormat.JSON.newGson();
+
+  @Test
+  public void queryChangeWithNullAttribute() throws Exception {
+    getChangeWithNullAttribute(
+        id -> pluginInfoFromSingletonList(adminRestSession.get(changeQueryUrl(id))));
+  }
+
+  @Test
+  public void getChangeWithNullAttribute() throws Exception {
+    getChangeWithNullAttribute(id -> pluginInfoFromChangeInfo(adminRestSession.get(changeUrl(id))));
+  }
+
+  @Test
+  public void getChangeDetailWithNullAttribute() throws Exception {
+    getChangeWithNullAttribute(
+        id -> pluginInfoFromChangeInfo(adminRestSession.get(changeDetailUrl(id))));
+  }
+
+  @Test
+  public void queryChangeWithSimpleAttribute() throws Exception {
+    getChangeWithSimpleAttribute(
+        id -> pluginInfoFromSingletonList(adminRestSession.get(changeQueryUrl(id))));
+  }
+
+  @Test
+  public void getChangeWithSimpleAttribute() throws Exception {
+    getChangeWithSimpleAttribute(
+        id -> pluginInfoFromChangeInfo(adminRestSession.get(changeUrl(id))));
+  }
+
+  @Test
+  public void getChangeDetailWithSimpleAttribute() throws Exception {
+    getChangeWithSimpleAttribute(
+        id -> pluginInfoFromChangeInfo(adminRestSession.get(changeDetailUrl(id))));
+  }
+
+  @Test
+  public void queryChangeWithOption() throws Exception {
+    getChangeWithOption(
+        id -> pluginInfoFromSingletonList(adminRestSession.get(changeQueryUrl(id))),
+        (id, opts) -> pluginInfoFromSingletonList(adminRestSession.get(changeQueryUrl(id, opts))));
+  }
+
+  @Test
+  public void getChangeWithOption() throws Exception {
+    getChangeWithOption(
+        id -> pluginInfoFromChangeInfo(adminRestSession.get(changeUrl(id))),
+        (id, opts) -> pluginInfoFromChangeInfo(adminRestSession.get(changeUrl(id, opts))));
+  }
+
+  @Test
+  public void getChangeDetailWithOption() throws Exception {
+    getChangeWithOption(
+        id -> pluginInfoFromChangeInfo(adminRestSession.get(changeDetailUrl(id))),
+        (id, opts) -> pluginInfoFromChangeInfo(adminRestSession.get(changeDetailUrl(id, opts))));
+  }
+
+  private String changeQueryUrl(Change.Id id) {
+    return changeQueryUrl(id, ImmutableListMultimap.of());
+  }
+
+  private String changeQueryUrl(Change.Id id, ImmutableListMultimap<String, String> opts) {
+    String url = "/changes/?q=" + id;
+    String queryString = buildQueryString(opts);
+    if (!queryString.isEmpty()) {
+      url += "&" + queryString;
+    }
+    return url;
+  }
+
+  private String changeUrl(Change.Id id) {
+    return changeUrl(id, ImmutableListMultimap.of());
+  }
+
+  private String changeUrl(Change.Id id, ImmutableListMultimap<String, String> pluginOptions) {
+    return changeUrl(id, "", pluginOptions);
+  }
+
+  private String changeDetailUrl(Change.Id id) {
+    return changeDetailUrl(id, ImmutableListMultimap.of());
+  }
+
+  private String changeDetailUrl(
+      Change.Id id, ImmutableListMultimap<String, String> pluginOptions) {
+    return changeUrl(id, "/detail", pluginOptions);
+  }
+
+  private String changeUrl(
+      Change.Id id, String suffix, ImmutableListMultimap<String, String> pluginOptions) {
+    String url = "/changes/" + project + "~" + id + suffix;
+    String queryString = buildQueryString(pluginOptions);
+    if (!queryString.isEmpty()) {
+      url += "?" + queryString;
+    }
+    return url;
+  }
+
+  private static String buildQueryString(ImmutableListMultimap<String, String> opts) {
+    return Joiner.on('&').withKeyValueSeparator('=').join(opts.entries());
+  }
+
+  @Nullable
+  private static List<MyInfo> pluginInfoFromSingletonList(RestResponse res) throws Exception {
+    res.assertOK();
+    List<Map<String, Object>> changeInfos =
+        GSON.fromJson(res.getReader(), new TypeToken<List<Map<String, Object>>>() {}.getType());
+    assertThat(changeInfos).hasSize(1);
+    return decodeRawPluginsList(GSON, changeInfos.get(0).get("plugins"));
+  }
+
+  @Nullable
+  private List<MyInfo> pluginInfoFromChangeInfo(RestResponse res) throws Exception {
+    res.assertOK();
+    Map<String, Object> changeInfo =
+        GSON.fromJson(res.getReader(), new TypeToken<Map<String, Object>>() {}.getType());
+    return decodeRawPluginsList(GSON, changeInfo.get("plugins"));
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java b/javatests/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java
index 0ece00a..ca4288f6 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java
@@ -19,12 +19,14 @@
 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.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;
@@ -34,11 +36,12 @@
 public class PrivateByDefaultIT extends AbstractDaemonTest {
   private Project.NameKey project1;
   private Project.NameKey project2;
+  @Inject private ProjectOperations projectOperations;
 
   @Before
   public void setUp() throws Exception {
-    project1 = createProject("project-1");
-    project2 = createProject("project-2", project1);
+    project1 = projectOperations.newProject().create();
+    project2 = projectOperations.newProject().parent(project1).create();
     setPrivateByDefault(project1, InheritableBoolean.FALSE);
   }
 
@@ -118,7 +121,7 @@
 
     TestRepository<InMemoryRepository> testRepo = cloneProject(project2);
     PushOneCommit.Result result =
-        pushFactory.create(db, admin.getIdent(), testRepo).to("refs/for/master%private");
+        pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master%private");
     result.assertErrorStatus();
   }
 
@@ -127,14 +130,14 @@
   public void pushDraftsWithPrivateByDefaultAndDisablePrivateChangesTrue() throws Exception {
     setPrivateByDefault(project2, InheritableBoolean.TRUE);
 
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = getRemoteHead(project2, "master");
     TestRepository<InMemoryRepository> testRepo = cloneProject(project2);
     PushOneCommit.Result result =
-        pushFactory.create(db, admin.getIdent(), testRepo).to("refs/for/master%draft");
+        pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master%draft");
     result.assertErrorStatus();
 
     testRepo.reset(initialHead);
-    result = pushFactory.create(db, admin.getIdent(), testRepo).to("refs/drafts/master");
+    result = pushFactory.create(admin.newIdent(), testRepo).to("refs/drafts/master");
     result.assertErrorStatus();
   }
 
@@ -151,7 +154,7 @@
 
   private PushOneCommit.Result createChange(Project.NameKey proj, String ref) throws Exception {
     TestRepository<InMemoryRepository> testRepo = cloneProject(proj);
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result result = push.to(ref);
     result.assertOkStatus();
     return result;
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
index 8160d9a..2ad4aca 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
 import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
 
@@ -31,19 +30,12 @@
 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.extensions.restapi.ResourceConflictException;
-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.change.TestSubmitInput;
 import com.google.gerrit.server.git.ChangeMessageModifier;
 import com.google.gerrit.server.submit.CommitMergeStatus;
 import com.google.inject.Inject;
 import java.util.List;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
 import org.junit.Test;
 
 public class SubmitByCherryPickIT extends AbstractSubmit {
@@ -84,8 +76,8 @@
     assertCurrentRevision(change2.getChangeId(), 2, newHead);
     assertSubmitter(change2.getChangeId(), 1);
     assertSubmitter(change2.getChangeId(), 2);
-    assertPersonEquals(admin.getIdent(), newHead.getAuthorIdent());
-    assertPersonEquals(admin.getIdent(), newHead.getCommitterIdent());
+    assertPersonEquals(admin.newIdent(), newHead.getAuthorIdent());
+    assertPersonEquals(admin.newIdent(), newHead.getCommitterIdent());
 
     assertRefUpdatedEvents(initialHead, headAfterFirstSubmit, headAfterFirstSubmit, newHead);
     assertChangeMergedEvents(
@@ -98,16 +90,8 @@
     RegistrationHandle handle =
         changeMessageModifiers.add(
             "gerrit",
-            new ChangeMessageModifier() {
-              @Override
-              public String onSubmit(
-                  String newCommitMessage,
-                  RevCommit original,
-                  RevCommit mergeTip,
-                  Branch.NameKey destination) {
-                return newCommitMessage + "Custom: " + destination.get();
-              }
-            });
+            (newCommitMessage, original, mergeTip, destination) ->
+                newCommitMessage + "Custom: " + destination.get());
     try {
       submit(change.getChangeId());
     } finally {
@@ -389,76 +373,4 @@
         change2.getChangeId(),
         headAfterFirstSubmit.name());
   }
-
-  @Test
-  public void repairChangeStateAfterFailure() throws Exception {
-    // In NoteDb-only mode, repo and meta updates are atomic (at least in InMemoryRepository).
-    assume().that(notesMigration.disableChangeReviewDb()).isFalse();
-
-    RevCommit initialHead = getRemoteHead();
-    PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
-    submit(change.getChangeId());
-
-    RevCommit headAfterFirstSubmit = getRemoteHead();
-    testRepo.reset(initialHead);
-    PushOneCommit.Result change2 = createChange("Change 2", "b.txt", "other content");
-    Change.Id id2 = change2.getChange().getId();
-    TestSubmitInput failInput = new TestSubmitInput();
-    failInput.failAfterRefUpdates = true;
-    submit(
-        change2.getChangeId(),
-        failInput,
-        ResourceConflictException.class,
-        "Failing after ref updates");
-    RevCommit headAfterFailedSubmit = getRemoteHead();
-
-    // Bad: ref advanced but change wasn't updated.
-    PatchSet.Id psId1 = new PatchSet.Id(id2, 1);
-    PatchSet.Id psId2 = new PatchSet.Id(id2, 2);
-    ChangeInfo info = gApi.changes().id(id2.get()).get();
-    assertThat(info.status).isEqualTo(ChangeStatus.NEW);
-    assertThat(info.revisions.get(info.currentRevision)._number).isEqualTo(1);
-    assertThat(getPatchSet(psId2)).isNull();
-
-    ObjectId rev2;
-    try (Repository repo = repoManager.openRepository(project);
-        RevWalk rw = new RevWalk(repo)) {
-      ObjectId rev1 = repo.exactRef(psId1.toRefName()).getObjectId();
-      assertThat(rev1).isNotNull();
-
-      rev2 = repo.exactRef(psId2.toRefName()).getObjectId();
-      assertThat(rev2).isNotNull();
-      assertThat(rev2).isNotEqualTo(rev1);
-      assertThat(rw.parseCommit(rev2).getParent(0)).isEqualTo(headAfterFirstSubmit);
-
-      assertThat(repo.exactRef("refs/heads/master").getObjectId()).isEqualTo(rev2);
-    }
-
-    submit(change2.getChangeId());
-
-    // Change status and patch set entities were updated, and branch tip stayed
-    // the same.
-    RevCommit headAfterSecondSubmit = getRemoteHead();
-    assertThat(headAfterSecondSubmit).isEqualTo(headAfterFailedSubmit);
-    info = gApi.changes().id(id2.get()).get();
-    assertThat(info.status).isEqualTo(ChangeStatus.MERGED);
-    assertThat(info.revisions.get(info.currentRevision)._number).isEqualTo(2);
-    PatchSet ps2 = getPatchSet(psId2);
-    assertThat(ps2).isNotNull();
-    assertThat(ps2.getRevision().get()).isEqualTo(rev2.name());
-    assertThat(Iterables.getLast(info.messages).message)
-        .isEqualTo(
-            "Change has been successfully cherry-picked as " + rev2.name() + " by Administrator");
-
-    try (Repository repo = repoManager.openRepository(project)) {
-      assertThat(repo.exactRef("refs/heads/master").getObjectId()).isEqualTo(rev2);
-    }
-
-    assertRefUpdatedEvents(initialHead, headAfterFirstSubmit);
-    assertChangeMergedEvents(
-        change.getChangeId(),
-        headAfterFirstSubmit.name(),
-        change2.getChangeId(),
-        headAfterSecondSubmit.name());
-  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
index ea8b98a..974180c 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
@@ -15,26 +15,16 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
 
-import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ActionInfo;
-import com.google.gerrit.extensions.common.ChangeInfo;
-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.change.TestSubmitInput;
 import java.util.Map;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.PushResult;
 import org.junit.Test;
 
@@ -80,8 +70,8 @@
     assertSubmitter(change.getChangeId(), 1);
     assertSubmitter(change2.getChangeId(), 1);
     assertSubmitter(change3.getChangeId(), 1);
-    assertPersonEquals(admin.getIdent(), updatedHead.getAuthorIdent());
-    assertPersonEquals(admin.getIdent(), updatedHead.getCommitterIdent());
+    assertPersonEquals(admin.newIdent(), updatedHead.getAuthorIdent());
+    assertPersonEquals(admin.newIdent(), updatedHead.getCommitterIdent());
     assertSubmittedTogether(id1, id3, id2, id1);
     assertSubmittedTogether(id2, id3, id2, id1);
     assertSubmittedTogether(id3, id3, id2, id1);
@@ -146,52 +136,6 @@
   }
 
   @Test
-  public void repairChangeStateAfterFailure() throws Exception {
-    // In NoteDb-only mode, repo and meta updates are atomic (at least in InMemoryRepository).
-    assume().that(notesMigration.disableChangeReviewDb()).isFalse();
-
-    PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
-    Change.Id id = change.getChange().getId();
-    TestSubmitInput failInput = new TestSubmitInput();
-    failInput.failAfterRefUpdates = true;
-    submit(
-        change.getChangeId(),
-        failInput,
-        ResourceConflictException.class,
-        "Failing after ref updates");
-
-    // Bad: ref advanced but change wasn't updated.
-    PatchSet.Id psId = new PatchSet.Id(id, 1);
-    ChangeInfo info = gApi.changes().id(id.get()).get();
-    assertThat(info.status).isEqualTo(ChangeStatus.NEW);
-    assertThat(info.revisions.get(info.currentRevision)._number).isEqualTo(1);
-
-    ObjectId rev;
-    try (Repository repo = repoManager.openRepository(project);
-        RevWalk rw = new RevWalk(repo)) {
-      rev = repo.exactRef(psId.toRefName()).getObjectId();
-      assertThat(rev).isNotNull();
-      assertThat(repo.exactRef("refs/heads/master").getObjectId()).isEqualTo(rev);
-    }
-
-    submit(change.getChangeId());
-
-    // Change status was updated, and branch tip stayed the same.
-    info = gApi.changes().id(id.get()).get();
-    assertThat(info.status).isEqualTo(ChangeStatus.MERGED);
-    assertThat(info.revisions.get(info.currentRevision)._number).isEqualTo(1);
-    assertThat(Iterables.getLast(info.messages).message)
-        .isEqualTo("Change has been successfully merged by Administrator");
-
-    try (Repository repo = repoManager.openRepository(project)) {
-      assertThat(repo.exactRef("refs/heads/master").getObjectId()).isEqualTo(rev);
-    }
-
-    assertRefUpdatedEvents();
-    assertChangeMergedEvents(change.getChangeId(), getRemoteHead().name());
-  }
-
-  @Test
   public void submitSameCommitsAsInExperimentalBranch() throws Exception {
     RevCommit initialHead = getRemoteHead();
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java
index 4af27ab..9bc5a2f 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java
@@ -38,7 +38,7 @@
     assertThat(headAfterSubmit.getParent(0)).isEqualTo(initialHead);
     assertThat(headAfterSubmit.getParent(1)).isEqualTo(change.getCommit());
     assertSubmitter(change.getChangeId(), 1);
-    assertPersonEquals(admin.getIdent(), headAfterSubmit.getAuthorIdent());
+    assertPersonEquals(admin.newIdent(), headAfterSubmit.getAuthorIdent());
     assertPersonEquals(serverIdent.get(), headAfterSubmit.getCommitterIdent());
 
     assertRefUpdatedEvents(initialHead, headAfterSubmit);
@@ -82,7 +82,7 @@
     assertThat(headAfterSecondSubmit.getParent(0).getShortMessage())
         .isEqualTo(headAfterFirstSubmit.getShortMessage());
     assertThat(headAfterSecondSubmit.getParent(0).getId()).isEqualTo(headAfterFirstSubmit.getId());
-    assertPersonEquals(admin.getIdent(), headAfterSecondSubmit.getAuthorIdent());
+    assertPersonEquals(admin.newIdent(), headAfterSecondSubmit.getAuthorIdent());
     assertPersonEquals(serverIdent.get(), headAfterSecondSubmit.getCommitterIdent());
 
     assertRefUpdatedEvents(
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
index c1dda00..5ebcd85 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
@@ -15,25 +15,28 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
 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.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.changes.SubmitInput;
 import com.google.gerrit.extensions.api.projects.BranchInput;
-import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.extensions.restapi.AuthException;
 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;
 import java.nio.file.Files;
@@ -51,6 +54,8 @@
 import org.junit.Test;
 
 public class SubmitByMergeIfNecessaryIT extends AbstractSubmitByMerge {
+  @Inject private ProjectOperations projectOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   @Override
   protected SubmitType getSubmitType() {
@@ -66,8 +71,8 @@
     assertThat(updatedHead.getId()).isEqualTo(change.getCommit());
     assertThat(updatedHead.getParent(0)).isEqualTo(initialHead);
     assertSubmitter(change.getChangeId(), 1);
-    assertPersonEquals(admin.getIdent(), updatedHead.getAuthorIdent());
-    assertPersonEquals(admin.getIdent(), updatedHead.getCommitterIdent());
+    assertPersonEquals(admin.newIdent(), updatedHead.getAuthorIdent());
+    assertPersonEquals(admin.newIdent(), updatedHead.getCommitterIdent());
 
     assertRefUpdatedEvents(initialHead, updatedHead);
     assertChangeMergedEvents(change.getChangeId(), updatedHead.name());
@@ -95,8 +100,8 @@
     assertThat(headAfterFirstSubmit.getShortMessage())
         .isEqualTo(change2.getCommit().getShortMessage());
     assertThat(headAfterFirstSubmit.getParent(0).getId()).isEqualTo(initialHead.getId());
-    assertPersonEquals(admin.getIdent(), headAfterFirstSubmit.getAuthorIdent());
-    assertPersonEquals(admin.getIdent(), headAfterFirstSubmit.getCommitterIdent());
+    assertPersonEquals(admin.newIdent(), headAfterFirstSubmit.getAuthorIdent());
+    assertPersonEquals(admin.newIdent(), headAfterFirstSubmit.getCommitterIdent());
 
     // We need to merge changes 3, 4 and 5.
     approve(change3.getChangeId());
@@ -109,7 +114,7 @@
     assertThat(headAfterSecondSubmit.getParent(0).getShortMessage())
         .isEqualTo(change2.getCommit().getShortMessage());
 
-    assertPersonEquals(admin.getIdent(), headAfterSecondSubmit.getAuthorIdent());
+    assertPersonEquals(admin.newIdent(), headAfterSecondSubmit.getAuthorIdent());
     assertPersonEquals(serverIdent.get(), headAfterSecondSubmit.getCommitterIdent());
 
     // First change stays untouched.
@@ -132,9 +137,9 @@
 
   @Test
   public void submitChangesAcrossRepos() throws Exception {
-    Project.NameKey p1 = createProject("project-where-we-submit");
-    Project.NameKey p2 = createProject("project-impacted-via-topic");
-    Project.NameKey p3 = createProject("project-impacted-indirectly-via-topic");
+    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");
@@ -209,9 +214,9 @@
 
   @Test
   public void submitChangesAcrossReposBlocked() throws Exception {
-    Project.NameKey p1 = createProject("project-where-we-submit");
-    Project.NameKey p2 = createProject("project-impacted-via-topic");
-    Project.NameKey p3 = createProject("project-impacted-indirectly-via-topic");
+    Project.NameKey p1 = projectOperations.newProject().create();
+    Project.NameKey p2 = projectOperations.newProject().create();
+    Project.NameKey p3 = projectOperations.newProject().create();
 
     TestRepository<?> repo1 = cloneProject(p1);
     TestRepository<?> repo2 = cloneProject(p2);
@@ -388,7 +393,7 @@
             "3",
             "a-topic-here");
 
-    Project.NameKey p3 = createProject("project-related-to-change3");
+    Project.NameKey p3 = projectOperations.newProject().create();
     TestRepository<?> repo3 = cloneProject(p3);
     RevCommit repo3Head = getRemoteHead(p3, "master");
     PushOneCommit.Result change3b =
@@ -440,8 +445,7 @@
     gApi.projects().name(project.get()).branch("stable").create(new BranchInput());
 
     // Push a change to master
-    PushOneCommit push =
-        pushFactory.create(db, user.getIdent(), testRepo, "small fix", "a.txt", "2");
+    PushOneCommit push = pushFactory.create(user.newIdent(), testRepo, "small fix", "a.txt", "2");
     PushOneCommit.Result change = push.to("refs/for/master");
     submit(change.getChangeId());
     RevCommit headAfterFirstSubmit = getRemoteLog(project, "master").get(0);
@@ -493,8 +497,7 @@
     gApi.projects().name(project.get()).branch("stable").create(new BranchInput());
 
     // Propose a change for master, but leave it open for master!
-    PushOneCommit change =
-        pushFactory.create(db, user.getIdent(), testRepo, "small fix", "a.txt", "2");
+    PushOneCommit change = pushFactory.create(user.newIdent(), testRepo, "small fix", "a.txt", "2");
     PushOneCommit.Result change2result = change.to("refs/for/master");
 
     // Now cherry pick to stable
@@ -531,13 +534,13 @@
   @Test
   public void dependencyOnOutdatedPatchSetPreventsMerge() throws Exception {
     // Create a change
-    PushOneCommit change = pushFactory.create(db, user.getIdent(), testRepo, "fix", "a.txt", "foo");
+    PushOneCommit change = pushFactory.create(user.newIdent(), testRepo, "fix", "a.txt", "foo");
     PushOneCommit.Result changeResult = change.to("refs/for/master");
     PatchSet.Id patchSetId = changeResult.getPatchSetId();
 
     // Create a successor change.
     PushOneCommit change2 =
-        pushFactory.create(db, user.getIdent(), testRepo, "feature", "b.txt", "bar");
+        pushFactory.create(user.newIdent(), testRepo, "feature", "b.txt", "bar");
     PushOneCommit.Result change2Result = change2.to("refs/for/master");
 
     // Create new patch set for first change.
@@ -573,12 +576,12 @@
   @Test
   public void dependencyOnDeletedChangePreventsMerge() throws Exception {
     // Create a change
-    PushOneCommit change = pushFactory.create(db, user.getIdent(), testRepo, "fix", "a.txt", "foo");
+    PushOneCommit change = pushFactory.create(user.newIdent(), testRepo, "fix", "a.txt", "foo");
     PushOneCommit.Result changeResult = change.to("refs/for/master");
 
     // Create a successor change.
     PushOneCommit change2 =
-        pushFactory.create(db, user.getIdent(), testRepo, "feature", "b.txt", "bar");
+        pushFactory.create(user.newIdent(), testRepo, "feature", "b.txt", "bar");
     PushOneCommit.Result change2Result = change2.to("refs/for/master");
 
     // Delete first change.
@@ -596,7 +599,9 @@
             + " depends on commit "
             + changeResult.getCommit().name()
             + " which cannot be merged."
-            + " Is the change of this commit not visible or was it deleted?");
+            + " Is the change of this commit not visible to '"
+            + admin.username()
+            + "' or was it deleted?");
 
     assertRefUpdatedEvents();
     assertChangeMergedEvents();
@@ -608,14 +613,13 @@
     grant(project, "refs/*", Permission.SUBMIT, false, REGISTERED_USERS);
 
     // Create a change
-    PushOneCommit change =
-        pushFactory.create(db, admin.getIdent(), testRepo, "fix", "a.txt", "foo");
+    PushOneCommit change = pushFactory.create(admin.newIdent(), testRepo, "fix", "a.txt", "foo");
     PushOneCommit.Result changeResult = change.to("refs/for/master");
     approve(changeResult.getChangeId());
 
     // Create a successor change.
     PushOneCommit change2 =
-        pushFactory.create(db, admin.getIdent(), testRepo, "feature", "b.txt", "bar");
+        pushFactory.create(admin.newIdent(), testRepo, "feature", "b.txt", "bar");
     PushOneCommit.Result change2Result = change2.to("refs/for/master");
 
     // Move the first change to a destination branch that is non-visible to user so that user cannot
@@ -628,7 +632,7 @@
     gApi.changes().id(changeResult.getChangeId()).move(secretBranch.get());
     block(secretBranch.get(), "read", ANONYMOUS_USERS);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
 
     // Verify that user cannot see the first change.
     try {
@@ -650,33 +654,34 @@
             + " depends on commit "
             + changeResult.getCommit().name()
             + " which cannot be merged."
-            + " Is the change of this commit not visible or was it deleted?");
+            + " Is the change of this commit not visible to '"
+            + user.username()
+            + "' or was it deleted?");
 
     assertRefUpdatedEvents();
     assertChangeMergedEvents();
   }
 
   @Test
-  public void dependencyOnHiddenChangeShouldPreventMergeButDoesnt() throws Exception {
+  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);
 
     // Create a change
-    PushOneCommit change =
-        pushFactory.create(db, admin.getIdent(), testRepo, "fix", "a.txt", "foo");
+    PushOneCommit change = pushFactory.create(admin.newIdent(), testRepo, "fix", "a.txt", "foo");
     PushOneCommit.Result changeResult = change.to("refs/for/master");
     approve(changeResult.getChangeId());
 
     // Create a successor change.
     PushOneCommit change2 =
-        pushFactory.create(db, admin.getIdent(), testRepo, "feature", "b.txt", "bar");
+        pushFactory.create(admin.newIdent(), testRepo, "feature", "b.txt", "bar");
     PushOneCommit.Result change2Result = change2.to("refs/for/master");
     approve(change2Result.getChangeId());
 
     // Mark the first change private so that it's not visible to user.
     gApi.changes().id(changeResult.getChangeId()).setPrivate(true, "nobody should see this");
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
 
     // Verify that user cannot see the first change.
     try {
@@ -686,23 +691,84 @@
       assertThat(e.getMessage()).isEqualTo("Not found: " + changeResult.getChangeId());
     }
 
-    // Submit the second change which has a dependency on the first change which is not visible to
-    // the user. We would expect the submit to fail, but instead the submit succeeds and the hidden
-    // change gets submitted too.
-    // TODO(ekempin): Make this submit fail.
-    gApi.changes().id(change2Result.getChangeId()).current().submit(new SubmitInput());
+    // 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");
+    }
+    assertRefUpdatedEvents();
+    assertChangeMergedEvents();
+  }
 
-    // Verify that both changes have been submitted.
-    setApiUser(admin);
-    assertThat(gApi.changes().id(changeResult.getChangeId()).get().status)
-        .isEqualTo(ChangeStatus.MERGED);
-    assertThat(gApi.changes().id(change2Result.getChangeId()).get().status)
-        .isEqualTo(ChangeStatus.MERGED);
+  @Test
+  public void dependencyOnHiddenChangeUsingTopicPreventsMerge() throws Exception {
+    // 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)
+    //                      |
+    //                    (c2a) <= private
+    assume().that(isSubmitWholeTopicEnabled()).isTrue();
+
+    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);
+
+    TestRepository<?> repo1 = cloneProject(p1);
+    TestRepository<?> repo2 = cloneProject(p2);
+
+    PushOneCommit.Result change1 =
+        createChange(repo1, "master", "A fresh change in repo1", "a.txt", "1", "topic-to-submit");
+    approve(change1.getChangeId());
+    PushOneCommit push =
+        pushFactory.create(admin.newIdent(), repo2, "An ancestor change in repo2", "a.txt", "2");
+    PushOneCommit.Result change2a = push.to("refs/for/master");
+    approve(change2a.getChangeId());
+    PushOneCommit.Result change2b =
+        createChange(
+            repo2, "master", "A topic-linked change in repo2", "a.txt", "2", "topic-to-submit");
+    approve(change2b.getChangeId());
+
+    // Mark change2a private so that it's not visible to user.
+    gApi.changes().id(change2a.getChangeId()).setPrivate(true, "nobody should see this");
+
+    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());
+    }
+
+    // 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");
+    }
+    assertRefUpdatedEvents();
+    assertChangeMergedEvents();
   }
 
   @Test
   public void testPreviewSubmitTgz() throws Exception {
-    Project.NameKey p1 = createProject("project-name");
+    Project.NameKey p1 = projectOperations.newProject().create();
 
     TestRepository<?> repo1 = cloneProject(p1);
     PushOneCommit.Result change1 = createChange(repo1, "master", "test", "a.txt", "1", "topic");
@@ -727,6 +793,6 @@
         untarredFiles.add(entry.getName());
       }
     }
-    assertThat(untarredFiles).containsExactly(name("project-name") + ".git");
+    assertThat(untarredFiles).containsExactly(p1.get() + ".git");
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java
index 3d8d06e..eb8dea5 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java
@@ -15,26 +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 com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestProjectInput;
 import com.google.gerrit.common.FooterConstants;
 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.reviewdb.client.Branch;
+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.List;
+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;
 
   @Override
   protected SubmitType getSubmitType() {
@@ -55,8 +64,8 @@
     assertCurrentRevision(change.getChangeId(), 2, head);
     assertSubmitter(change.getChangeId(), 1);
     assertSubmitter(change.getChangeId(), 2);
-    assertPersonEquals(admin.getIdent(), head.getAuthorIdent());
-    assertPersonEquals(admin.getIdent(), head.getCommitterIdent());
+    assertPersonEquals(admin.newIdent(), head.getAuthorIdent());
+    assertPersonEquals(admin.newIdent(), head.getCommitterIdent());
     assertRefUpdatedEvents(oldHead, head);
     assertChangeMergedEvents(change.getChangeId(), head.name());
   }
@@ -80,43 +89,86 @@
   }
 
   @Test
-  @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
-  public void changeMessageOnSubmit() throws Exception {
-    PushOneCommit.Result change1 = createChange();
-    PushOneCommit.Result change2 = createChange();
+  public void rebaseInvokesChangeMessageModifiers() throws Exception {
+    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";
 
-    RegistrationHandle handle =
-        changeMessageModifiers.add(
-            "gerrit",
-            new ChangeMessageModifier() {
-              @Override
-              public String onSubmit(
-                  String newCommitMessage,
-                  RevCommit original,
-                  RevCommit mergeTip,
-                  Branch.NameKey destination) {
-                List<String> custom = mergeTip.getFooterLines("Custom");
-                if (!custom.isEmpty()) {
-                  newCommitMessage += "Custom-Parent: " + custom.get(0) + "\n";
-                }
-                return newCommitMessage + "Custom: " + destination.get();
-              }
-            });
-    try {
-      // change1 is a fast-forward, but should be rebased in cherry pick style
-      // anyway, making change2 not a fast-forward, requiring a rebase.
-      approve(change1.getChangeId());
-      submit(change2.getChangeId());
-    } finally {
-      handle.remove();
+    try (AutoCloseable ignored = installChangeMessageModifiers(modifier1, modifier2, 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();
+
+      assertThat(gApi.changes().id(cd2.getId().get()).revision(2).commit(false).message)
+          .isEqualTo(
+              "Change 2\n\n"
+                  + ("Change-Id: " + cd2.change().getKey() + "\n")
+                  + ("Reviewed-on: "
+                      + urlFormatter.get().getChangeViewUrl(project, cd2.getId()).get()
+                      + "\n")
+                  + "Reviewed-by: Administrator <admin@example.com>\n"
+                  + ("This-change-before-rebase: " + change2Ps1Commit + "\n")
+                  + ("Previous-step-tip: " + change1CurrentCommit + "\n")
+                  + "Dest: master\n");
     }
-    // ... but both changes should get custom footers.
-    assertThat(getCurrentCommit(change1).getFooterLines("Custom"))
-        .containsExactly("refs/heads/master");
-    assertThat(getCurrentCommit(change2).getFooterLines("Custom"))
-        .containsExactly("refs/heads/master");
-    assertThat(getCurrentCommit(change2).getFooterLines("Custom-Parent"))
-        .containsExactly("refs/heads/master");
+  }
+
+  @Test
+  public void failingChangeMessageModifierShortCircuits() throws Exception {
+    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");
+      }
+    }
+  }
+
+  @Test
+  public void changeMessageModifierReturningNullShortCircuits() throws Exception {
+    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");
+      }
+    }
+  }
+
+  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 {
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java
index 19f1706..7bb31a0 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java
@@ -42,8 +42,8 @@
     assertApproved(change.getChangeId());
     assertCurrentRevision(change.getChangeId(), 1, head);
     assertSubmitter(change.getChangeId(), 1);
-    assertPersonEquals(admin.getIdent(), head.getAuthorIdent());
-    assertPersonEquals(admin.getIdent(), head.getCommitterIdent());
+    assertPersonEquals(admin.newIdent(), head.getAuthorIdent());
+    assertPersonEquals(admin.newIdent(), head.getCommitterIdent());
     assertRefUpdatedEvents(oldHead, head);
     assertChangeMergedEvents(change.getChangeId(), head.name());
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
index e9ac07a..87fc9f6 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
@@ -29,7 +29,6 @@
 import com.google.gerrit.server.submit.ChangeSet;
 import com.google.gerrit.server.submit.MergeSuperSet;
 import com.google.gerrit.testing.ConfigSuite;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -305,9 +304,9 @@
   }
 
   private void assertChangeSetMergeable(ChangeData change, boolean expected)
-      throws MissingObjectException, IncorrectObjectTypeException, IOException, OrmException,
+      throws MissingObjectException, IncorrectObjectTypeException, IOException,
           PermissionBackendException {
-    ChangeSet cs = mergeSuperSet.get().completeChangeSet(db, change.change(), user(admin));
+    ChangeSet cs = mergeSuperSet.get().completeChangeSet(change.change(), user(admin));
     assertThat(submit.unmergeableChanges(cs).isEmpty()).isEqualTo(expected);
   }
 
@@ -333,7 +332,7 @@
       List<RevCommit> parents,
       String ref)
       throws Exception {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), repo, subject, fileName, content);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), repo, subject, fileName, content);
 
     if (!parents.isEmpty()) {
       push.setParents(parents);
@@ -351,7 +350,7 @@
 
   private PushOneCommit.Result createChange(TestRepository<?> repo, String subject)
       throws Exception {
-    return createChange(repo, subject, "x", "x", new ArrayList<RevCommit>(), "refs/for/master");
+    return createChange(repo, subject, "x", "x", new ArrayList<>(), "refs/for/master");
   }
 
   private PushOneCommit.Result createChange(
@@ -366,8 +365,7 @@
 
   @Override
   protected PushOneCommit.Result createChange(String subject) throws Exception {
-    return createChange(
-        testRepo, subject, "", "", Collections.<RevCommit>emptyList(), "refs/for/master");
+    return createChange(testRepo, subject, "", "", Collections.emptyList(), "refs/for/master");
   }
 
   private PushOneCommit.Result createChange(String subject, List<RevCommit> parents)
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
index b0a89c3..9bbe1dd 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.acceptance.rest.change;
 
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static java.util.stream.Collectors.toList;
@@ -23,31 +24,35 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.TestAccount;
+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.data.GlobalCapability;
 import com.google.gerrit.extensions.api.accounts.EmailInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.common.ChangeInput;
-import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
-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.reviewdb.client.Project;
-import com.google.gerrit.server.group.InternalGroup;
-import com.google.gerrit.server.restapi.group.CreateGroup;
 import com.google.inject.Inject;
-import java.util.Arrays;
 import java.util.List;
+import java.util.Set;
+import java.util.stream.IntStream;
 import org.junit.Before;
 import org.junit.Test;
 
 public class SuggestReviewersIT extends AbstractDaemonTest {
-  @Inject private CreateGroup createGroup;
+  @Inject private AccountOperations accountOperations;
+  @Inject private GroupOperations groupOperations;
+  @Inject private ProjectOperations projectOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
-  private InternalGroup group1;
-  private InternalGroup group2;
-  private InternalGroup group3;
+  private AccountGroup.UUID group1;
+  private AccountGroup.UUID group2;
+  private AccountGroup.UUID group3;
 
   private TestAccount user1;
   private TestAccount user2;
@@ -56,14 +61,16 @@
 
   @Before
   public void setUp() throws Exception {
-    group1 = newGroup("users1");
-    group2 = newGroup("users2");
-    group3 = newGroup("users3");
-
-    user1 = user("user1", "First1 Last1", group1);
-    user2 = user("user2", "First2 Last2", group2);
-    user3 = user("user3", "First3 Last3", group1, group2);
+    user1 = user("user1", "First1 Last1");
+    user2 = user("user2", "First2 Last2");
+    user3 = user("user3", "First3 Last3");
     user4 = user("jdoe", "John Doe", "JDOE");
+
+    group1 =
+        groupOperations.newGroup().name(name("users1")).members(user1.id(), user3.id()).create();
+    group2 =
+        groupOperations.newGroup().name(name("users2")).members(user2.id(), user3.id()).create();
+    group3 = groupOperations.newGroup().name(name("users3")).members(user1.id()).create();
   }
 
   @Test
@@ -105,7 +112,8 @@
     assertReviewers(
         reviewers, ImmutableList.of(user1, user2, user3), ImmutableList.of(group1, group2));
 
-    reviewers = suggestReviewers(changeId, group3.getName(), 10);
+    String group3Name = groupOperations.group(group3).get().name();
+    reviewers = suggestReviewers(changeId, group3Name, 10);
     assertReviewers(reviewers, ImmutableList.of(), ImmutableList.of(group3));
 
     // Suggested accounts are ordered by activity. All users have no activity,
@@ -116,7 +124,9 @@
     assertThat(reviewers.get(0).account).isNotNull();
     assertThat(ImmutableList.of(reviewers.get(0).account._accountId))
         .containsAnyIn(
-            ImmutableList.of(user1, user2, user3).stream().map(u -> u.id.get()).collect(toList()));
+            ImmutableList.of(user1, user2, user3).stream()
+                .map(u -> u.id().get())
+                .collect(toList()));
   }
 
   @Test
@@ -125,23 +135,23 @@
     String changeId = createChange().getChangeId();
     List<SuggestedReviewerInfo> reviewers;
 
-    reviewers = suggestReviewers(changeId, user2.username, 2);
+    reviewers = suggestReviewers(changeId, user2.username(), 2);
     assertThat(reviewers).hasSize(1);
-    assertThat(Iterables.getOnlyElement(reviewers).account.name).isEqualTo(user2.fullName);
+    assertThat(Iterables.getOnlyElement(reviewers).account.name).isEqualTo(user2.fullName());
 
-    setApiUser(user1);
-    reviewers = suggestReviewers(changeId, user2.fullName, 2);
+    requestScopeOperations.setApiUser(user1.id());
+    reviewers = suggestReviewers(changeId, user2.fullName(), 2);
     assertThat(reviewers).isEmpty();
 
-    setApiUser(user2);
-    reviewers = suggestReviewers(changeId, user2.username, 2);
+    requestScopeOperations.setApiUser(user2.id());
+    reviewers = suggestReviewers(changeId, user2.username(), 2);
     assertThat(reviewers).hasSize(1);
-    assertThat(Iterables.getOnlyElement(reviewers).account.name).isEqualTo(user2.fullName);
+    assertThat(Iterables.getOnlyElement(reviewers).account.name).isEqualTo(user2.fullName());
 
-    setApiUser(user3);
-    reviewers = suggestReviewers(changeId, user2.username, 2);
+    requestScopeOperations.setApiUser(user3.id());
+    reviewers = suggestReviewers(changeId, user2.username(), 2);
     assertThat(reviewers).hasSize(1);
-    assertThat(Iterables.getOnlyElement(reviewers).account.name).isEqualTo(user2.fullName);
+    assertThat(Iterables.getOnlyElement(reviewers).account.name).isEqualTo(user2.fullName());
   }
 
   @Test
@@ -149,10 +159,10 @@
     String changeId = createChange().getChangeId();
     List<SuggestedReviewerInfo> reviewers;
 
-    setApiUser(user3);
+    requestScopeOperations.setApiUser(user3.id());
     block("refs/*", "read", ANONYMOUS_USERS);
-    allow("refs/*", "read", group1.getGroupUUID());
-    reviewers = suggestReviewers(changeId, user2.username, 2);
+    allow("refs/*", "read", group1);
+    reviewers = suggestReviewers(changeId, user2.username(), 2);
     assertThat(reviewers).isEmpty();
   }
 
@@ -162,15 +172,16 @@
     String changeId = createChange().getChangeId();
     List<SuggestedReviewerInfo> reviewers;
 
-    setApiUser(user1);
-    reviewers = suggestReviewers(changeId, user2.username, 2);
+    requestScopeOperations.setApiUser(user1.id());
+    reviewers = suggestReviewers(changeId, user2.username(), 2);
     assertThat(reviewers).isEmpty();
 
-    setApiUser(user1); // Clear cached group info.
-    allowGlobalCapabilities(group1.getGroupUUID(), GlobalCapability.VIEW_ALL_ACCOUNTS);
-    reviewers = suggestReviewers(changeId, user2.username, 2);
+    // Clear cached group info.
+    requestScopeOperations.setApiUser(user1.id());
+    allowGlobalCapabilities(group1, GlobalCapability.VIEW_ALL_ACCOUNTS);
+    reviewers = suggestReviewers(changeId, user2.username(), 2);
     assertThat(reviewers).hasSize(1);
-    assertThat(Iterables.getOnlyElement(reviewers).account.name).isEqualTo(user2.fullName);
+    assertThat(Iterables.getOnlyElement(reviewers).account.name).isEqualTo(user2.fullName());
   }
 
   @Test
@@ -216,44 +227,38 @@
     reviewers = suggestReviewers(changeId, name("user"));
     assertThat(reviewers).hasSize(6);
 
-    reviewers = suggestReviewers(changeId, user1.username);
+    reviewers = suggestReviewers(changeId, user1.username());
     assertThat(reviewers).hasSize(1);
 
     reviewers = suggestReviewers(changeId, "example.com");
     assertThat(reviewers).hasSize(5);
 
-    reviewers = suggestReviewers(changeId, user1.email);
+    reviewers = suggestReviewers(changeId, user1.email());
     assertThat(reviewers).hasSize(1);
 
-    reviewers = suggestReviewers(changeId, user1.username + " example");
+    reviewers = suggestReviewers(changeId, user1.username() + " example");
     assertThat(reviewers).hasSize(1);
 
-    reviewers = suggestReviewers(changeId, user4.email.toLowerCase());
+    reviewers = suggestReviewers(changeId, user4.email().toLowerCase());
     assertThat(reviewers).hasSize(1);
-    assertThat(reviewers.get(0).account.email).isEqualTo(user4.email);
+    assertThat(reviewers.get(0).account.email).isEqualTo(user4.email());
   }
 
   @Test
   public void suggestReviewersWithoutLimitOptionSpecified() throws Exception {
     String changeId = createChange().getChangeId();
-    String query = user3.username;
+    String query = user3.username();
     List<SuggestedReviewerInfo> suggestedReviewerInfos =
         gApi.changes().id(changeId).suggestReviewers(query).get();
     assertThat(suggestedReviewerInfos).hasSize(1);
   }
 
   @Test
-  @GerritConfig(name = "addreviewer.maxAllowed", value = "2")
+  @GerritConfig(name = "addreviewer.maxAllowed", value = "1")
   @GerritConfig(name = "addreviewer.maxWithoutConfirmation", value = "1")
-  public void suggestReviewersGroupSizeConsiderations() throws Exception {
-    InternalGroup largeGroup = newGroup("large");
-    InternalGroup mediumGroup = newGroup("medium");
-
-    // Both groups have Administrator as a member. Add two users to large
-    // group to push it past maxAllowed, and one to medium group to push it
-    // past maxWithoutConfirmation.
-    user("individual 0", "Test0 Last0", largeGroup, mediumGroup);
-    user("individual 1", "Test1 Last1", largeGroup);
+  public void confirmationIsNeverRequestedForAccounts() throws Exception {
+    user("individual 0", "Test0 Last0");
+    user("individual 1", "Test1 Last1");
 
     String changeId = createChange().getChangeId();
     List<SuggestedReviewerInfo> reviewers;
@@ -265,60 +270,107 @@
     reviewer = reviewers.get(0);
     assertThat(reviewer.count).isEqualTo(1);
     assertThat(reviewer.confirm).isNull();
+  }
+
+  @Test
+  @GerritConfig(name = "addreviewer.maxAllowed", value = "2")
+  @GerritConfig(name = "addreviewer.maxWithoutConfirmation", value = "1")
+  public void suggestReviewersGroupSizeConsiderations() throws Exception {
+    AccountGroup.UUID largeGroup = createGroupWithArbitraryMembers(3);
+    String largeGroupName = groupOperations.group(largeGroup).get().name();
+    AccountGroup.UUID mediumGroup = createGroupWithArbitraryMembers(2);
+    String mediumGroupName = groupOperations.group(mediumGroup).get().name();
+
+    String changeId = createChange().getChangeId();
+    List<SuggestedReviewerInfo> reviewers;
+    SuggestedReviewerInfo reviewer;
 
     // Large group should never be suggested.
-    reviewers = suggestReviewers(changeId, largeGroup.getName(), 10);
+    reviewers = suggestReviewers(changeId, largeGroupName, 10);
     assertThat(reviewers).isEmpty();
 
     // Medium group should be suggested with appropriate count and confirm.
-    reviewers = suggestReviewers(changeId, mediumGroup.getName(), 10);
+    reviewers = suggestReviewers(changeId, mediumGroupName, 10);
     assertThat(reviewers).hasSize(1);
     reviewer = reviewers.get(0);
-    assertThat(reviewer.group.name).isEqualTo(mediumGroup.getName());
+    assertThat(reviewer.group.id).isEqualTo(mediumGroup.get());
     assertThat(reviewer.count).isEqualTo(2);
     assertThat(reviewer.confirm).isTrue();
   }
 
   @Test
+  @GerritConfig(name = "addreviewer.maxAllowed", value = "20")
+  @GerritConfig(name = "addreviewer.maxWithoutConfirmation", value = "0")
+  public void confirmationIsNotNecessaryForLargeGroupWhenLimitIsRemoved() throws Exception {
+    String changeId = createChange().getChangeId();
+    int numMembers = 15;
+    AccountGroup.UUID largeGroup = createGroupWithArbitraryMembers(numMembers);
+    String groupName = groupOperations.group(largeGroup).get().name();
+
+    List<SuggestedReviewerInfo> reviewers = suggestReviewers(changeId, groupName, 10);
+
+    assertThat(reviewers).hasSize(1);
+    SuggestedReviewerInfo reviewer = Iterables.getOnlyElement(reviewers);
+    assertThat(reviewer.group.id).isEqualTo(largeGroup.get());
+    // Confirmation should not be necessary.
+    assertThat(reviewer.confirm).isNull();
+  }
+
+  @Test
+  @GerritConfig(name = "addreviewer.maxAllowed", value = "0")
+  public void largeGroupIsSuggestedWhenLimitIsRemoved() throws Exception {
+    String changeId = createChange().getChangeId();
+    int numMembers = 30;
+    AccountGroup.UUID largeGroup = createGroupWithArbitraryMembers(numMembers);
+    String groupName = groupOperations.group(largeGroup).get().name();
+
+    List<SuggestedReviewerInfo> reviewers = suggestReviewers(changeId, groupName, 10);
+
+    assertThat(reviewers).hasSize(1);
+    SuggestedReviewerInfo reviewer = Iterables.getOnlyElement(reviewers);
+    assertThat(reviewer.group.id).isEqualTo(largeGroup.get());
+  }
+
+  @Test
   public void defaultReviewerSuggestion() throws Exception {
     TestAccount user1 = user("customuser1", "User1");
     TestAccount reviewer1 = user("customuser2", "User2");
     TestAccount reviewer2 = user("customuser3", "User3");
 
-    setApiUser(user1);
+    requestScopeOperations.setApiUser(user1.id());
     String changeId1 = createChangeFromApi();
 
-    setApiUser(reviewer1);
+    requestScopeOperations.setApiUser(reviewer1.id());
     reviewChange(changeId1);
 
-    setApiUser(user1);
+    requestScopeOperations.setApiUser(user1.id());
     String changeId2 = createChangeFromApi();
 
-    setApiUser(reviewer1);
+    requestScopeOperations.setApiUser(reviewer1.id());
     reviewChange(changeId2);
 
-    setApiUser(reviewer2);
+    requestScopeOperations.setApiUser(reviewer2.id());
     reviewChange(changeId2);
 
-    setApiUser(user1);
+    requestScopeOperations.setApiUser(user1.id());
     String changeId3 = createChangeFromApi();
     List<SuggestedReviewerInfo> reviewers = suggestReviewers(changeId3, null, 4);
     assertThat(reviewers.stream().map(r -> r.account._accountId).collect(toList()))
-        .containsExactly(reviewer1.id.get(), reviewer2.id.get())
+        .containsExactly(reviewer1.id().get(), reviewer2.id().get())
         .inOrder();
 
     // check that existing reviewers are filtered out
-    gApi.changes().id(changeId3).addReviewer(reviewer1.email);
+    gApi.changes().id(changeId3).addReviewer(reviewer1.email());
     reviewers = suggestReviewers(changeId3, null, 4);
     assertThat(reviewers.stream().map(r -> r.account._accountId).collect(toList()))
-        .containsExactly(reviewer2.id.get())
+        .containsExactly(reviewer2.id().get())
         .inOrder();
   }
 
   @Test
   public void defaultReviewerSuggestionOnFirstChange() throws Exception {
     TestAccount user1 = user("customuser1", "User1");
-    setApiUser(user1);
+    requestScopeOperations.setApiUser(user1.id());
     List<SuggestedReviewerInfo> reviewers = suggestReviewers(createChange().getChangeId(), "", 4);
     assertThat(reviewers).isEmpty();
   }
@@ -337,23 +389,23 @@
     TestAccount userWhoLooksForSuggestions = user("customuser5", fullName);
 
     // Create a change as userWhoOwns and add some reviews
-    setApiUser(userWhoOwns);
+    requestScopeOperations.setApiUser(userWhoOwns.id());
     String changeId1 = createChangeFromApi();
 
-    setApiUser(reviewer1);
+    requestScopeOperations.setApiUser(reviewer1.id());
     reviewChange(changeId1);
 
-    setApiUser(user1);
+    requestScopeOperations.setApiUser(user1.id());
     String changeId2 = createChangeFromApi();
 
-    setApiUser(reviewer1);
+    requestScopeOperations.setApiUser(reviewer1.id());
     reviewChange(changeId2);
 
-    setApiUser(reviewer2);
+    requestScopeOperations.setApiUser(reviewer2.id());
     reviewChange(changeId2);
 
     // Create a comment as a different user
-    setApiUser(userWhoComments);
+    requestScopeOperations.setApiUser(userWhoComments.id());
     ReviewInput ri = new ReviewInput();
     ri.message = "Test";
     gApi.changes().id(changeId1).revision(1).review(ri);
@@ -361,18 +413,21 @@
     // Create a change as a new user to assert that we receive the correct
     // ranking
 
-    setApiUser(userWhoLooksForSuggestions);
+    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())
+            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 = createProject("test");
+    Project.NameKey newProject = projectOperations.newProject().create();
 
     // Create users who review changes in both the default and the new project
     String fullName = "Primum Finalis";
@@ -380,31 +435,31 @@
     TestAccount reviewer1 = user("customuser2", fullName);
     TestAccount reviewer2 = user("customuser3", fullName);
 
-    setApiUser(userWhoOwns);
+    requestScopeOperations.setApiUser(userWhoOwns.id());
     String changeId1 = createChangeFromApi();
 
-    setApiUser(reviewer1);
+    requestScopeOperations.setApiUser(reviewer1.id());
     reviewChange(changeId1);
 
-    setApiUser(userWhoOwns);
+    requestScopeOperations.setApiUser(userWhoOwns.id());
     String changeId2 = createChangeFromApi(newProject);
 
-    setApiUser(reviewer2);
+    requestScopeOperations.setApiUser(reviewer2.id());
     reviewChange(changeId2);
 
-    setApiUser(userWhoOwns);
+    requestScopeOperations.setApiUser(userWhoOwns.id());
     String changeId3 = createChangeFromApi(newProject);
 
-    setApiUser(reviewer2);
+    requestScopeOperations.setApiUser(reviewer2.id());
     reviewChange(changeId3);
 
-    setApiUser(userWhoOwns);
+    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())
+        .containsExactly(reviewer1.id().get(), reviewer2.id().get())
         .inOrder();
   }
 
@@ -412,17 +467,17 @@
   public void suggestNoInactiveAccounts() throws Exception {
     String name = name("foo");
     TestAccount foo1 = accountCreator.create(name + "-1");
-    assertThat(gApi.accounts().id(foo1.username).getActive()).isTrue();
+    assertThat(gApi.accounts().id(foo1.username()).getActive()).isTrue();
 
     TestAccount foo2 = accountCreator.create(name + "-2");
-    assertThat(gApi.accounts().id(foo2.username).getActive()).isTrue();
+    assertThat(gApi.accounts().id(foo2.username()).getActive()).isTrue();
 
     String changeId = createChange().getChangeId();
     assertReviewers(
         suggestReviewers(changeId, name), ImmutableList.of(foo1, foo2), ImmutableList.of());
 
-    gApi.accounts().id(foo2.username).setActive(false);
-    assertThat(gApi.accounts().id(foo2.username).getActive()).isFalse();
+    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());
   }
 
@@ -444,7 +499,7 @@
     String secondaryEmail = "foo.secondary@example.com";
     createAccountWithSecondaryEmail("foo", secondaryEmail);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     List<SuggestedReviewerInfo> reviewers =
         suggestReviewers(createChange().getChangeId(), secondaryEmail, 4);
     assertThat(reviewers).isEmpty();
@@ -464,7 +519,7 @@
     assertThat(Iterables.getOnlyElement(reviewers).account.secondaryEmails)
         .containsExactly(secondaryEmail);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     reviewers = suggestReviewers(createChange().getChangeId(), "foo", 4);
     assertReviewers(reviewers, ImmutableList.of(foo), ImmutableList.of());
     assertThat(Iterables.getOnlyElement(reviewers).account.secondaryEmails).isNull();
@@ -476,7 +531,7 @@
     EmailInput input = new EmailInput();
     input.email = secondaryEmail;
     input.noConfirmation = true;
-    gApi.accounts().id(foo.id.get()).addEmail(input);
+    gApi.accounts().id(foo.id().get()).addEmail(input);
     return foo;
   }
 
@@ -490,21 +545,20 @@
     return gApi.changes().id(changeId).suggestReviewers(query).withLimit(n).get();
   }
 
-  private InternalGroup newGroup(String name) throws Exception {
-    GroupInfo group =
-        createGroup.apply(TopLevelResource.INSTANCE, IdString.fromDecoded(name(name)), null);
-    return group(new AccountGroup.UUID(group.id));
+  private AccountGroup.UUID createGroupWithArbitraryMembers(int numMembers) {
+    Set<Account.Id> members =
+        IntStream.rangeClosed(1, numMembers)
+            .mapToObj(i -> accountOperations.newAccount().create())
+            .collect(toImmutableSet());
+    return groupOperations.newGroup().members(members).create();
   }
 
-  private TestAccount user(String name, String fullName, String emailName, InternalGroup... groups)
-      throws Exception {
-    String[] groupNames = Arrays.stream(groups).map(InternalGroup::getName).toArray(String[]::new);
-    return accountCreator.create(
-        name(name), name(emailName) + "@example.com", fullName, groupNames);
+  private TestAccount user(String name, String fullName, String emailName) throws Exception {
+    return accountCreator.create(name(name), name(emailName) + "@example.com", fullName);
   }
 
-  private TestAccount user(String name, String fullName, InternalGroup... groups) throws Exception {
-    return user(name, fullName, name, groups);
+  private TestAccount user(String name, String fullName) throws Exception {
+    return user(name, fullName, name);
   }
 
   private void reviewChange(String changeId) throws RestApiException {
@@ -525,23 +579,23 @@
     return gApi.changes().create(ci).get().changeId;
   }
 
-  private void assertReviewers(
+  private static void assertReviewers(
       List<SuggestedReviewerInfo> actual,
       List<TestAccount> expectedUsers,
-      List<InternalGroup> expectedGroups) {
+      List<AccountGroup.UUID> expectedGroups) {
     List<Integer> actualAccountIds =
         actual.stream()
             .filter(i -> i.account != null)
             .map(i -> i.account._accountId)
             .collect(toList());
     assertThat(actualAccountIds)
-        .containsExactlyElementsIn(expectedUsers.stream().map(u -> u.id.get()).collect(toList()));
+        .containsExactlyElementsIn(expectedUsers.stream().map(u -> u.id().get()).collect(toList()));
 
     List<String> actualGroupIds =
         actual.stream().filter(i -> i.group != null).map(i -> i.group.id).collect(toList());
     assertThat(actualGroupIds)
         .containsExactlyElementsIn(
-            expectedGroups.stream().map(g -> g.getGroupUUID().get()).collect(toList()))
+            expectedGroups.stream().map(AccountGroup.UUID::get).collect(toList()))
         .inOrder();
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/WorkInProgressByDefaultIT.java b/javatests/com/google/gerrit/acceptance/rest/change/WorkInProgressByDefaultIT.java
index 34d87d0..49692dd 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/WorkInProgressByDefaultIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/WorkInProgressByDefaultIT.java
@@ -18,12 +18,15 @@
 
 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.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;
@@ -31,21 +34,24 @@
 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 = createProject("project-1");
-    project2 = createProject("project-2", project1);
+    project1 = projectOperations.newProject().create();
+    project2 = projectOperations.newProject().parent(project1).create();
   }
 
   @After
   public void tearDown() throws Exception {
-    setApiUser(admin);
-    GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id.get()).getPreferences();
+    requestScopeOperations.setApiUser(admin.id());
+    GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id().get()).getPreferences();
     prefs.workInProgressByDefault = false;
-    gApi.accounts().id(admin.id.get()).setPreferences(prefs);
+    gApi.accounts().id(admin.id().get()).setPreferences(prefs);
   }
 
   @Test
@@ -139,9 +145,9 @@
   }
 
   private void setWorkInProgressByDefaultForUser() throws Exception {
-    GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id.get()).getPreferences();
+    GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id().get()).getPreferences();
     prefs.workInProgressByDefault = true;
-    gApi.accounts().id(admin.id.get()).setPreferences(prefs);
+    gApi.accounts().id(admin.id().get()).setPreferences(prefs);
   }
 
   private PushOneCommit.Result createChange(Project.NameKey p) throws Exception {
@@ -150,7 +156,7 @@
 
   private PushOneCommit.Result createChange(Project.NameKey p, String r) throws Exception {
     TestRepository<InMemoryRepository> testRepo = cloneProject(p);
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result result = push.to(r);
     result.assertOkStatus();
     return result;
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/ConfirmEmailIT.java b/javatests/com/google/gerrit/acceptance/rest/config/ConfirmEmailIT.java
index 7133580..99fdbc8 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/ConfirmEmailIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/ConfirmEmailIT.java
@@ -16,9 +16,9 @@
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.server.mail.EmailTokenVerifier;
+import com.google.gerrit.server.mail.SignedToken;
 import com.google.gerrit.server.restapi.config.ConfirmEmail;
 import com.google.gerrit.testing.ConfigSuite;
-import com.google.gwtjsonrpc.server.SignedToken;
 import com.google.inject.Inject;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Test;
@@ -36,14 +36,14 @@
   @Test
   public void confirm() throws Exception {
     ConfirmEmail.Input in = new ConfirmEmail.Input();
-    in.token = emailTokenVerifier.encode(admin.getId(), "new.mail@example.com");
+    in.token = emailTokenVerifier.encode(admin.id(), "new.mail@example.com");
     adminRestSession.put("/config/server/email.confirm", in).assertNoContent();
   }
 
   @Test
   public void confirmForOtherUser_UnprocessableEntity() throws Exception {
     ConfirmEmail.Input in = new ConfirmEmail.Input();
-    in.token = emailTokenVerifier.encode(user.getId(), "new.mail@example.com");
+    in.token = emailTokenVerifier.encode(user.id(), "new.mail@example.com");
     adminRestSession.put("/config/server/email.confirm", in).assertUnprocessableEntity();
   }
 
@@ -57,7 +57,7 @@
   @Test
   public void confirmAlreadyInUse_UnprocessableEntity() throws Exception {
     ConfirmEmail.Input in = new ConfirmEmail.Input();
-    in.token = emailTokenVerifier.encode(admin.getId(), user.email);
+    in.token = emailTokenVerifier.encode(admin.id(), user.email());
     adminRestSession.put("/config/server/email.confirm", in).assertUnprocessableEntity();
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java b/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
index 874e04e..4a74018 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
@@ -70,8 +70,6 @@
   // gerrit
   @GerritConfig(name = "gerrit.allProjects", value = "Root")
   @GerritConfig(name = "gerrit.allUsers", value = "Users")
-  @GerritConfig(name = "gerrit.enableGwtUi", value = "true")
-  @GerritConfig(name = "gerrit.reportBugText", value = "REPORT BUG")
   @GerritConfig(name = "gerrit.reportBugUrl", value = "https://example.com/report")
 
   // suggest
@@ -113,7 +111,6 @@
     assertThat(i.gerrit.allProjects).isEqualTo("Root");
     assertThat(i.gerrit.allUsers).isEqualTo("Users");
     assertThat(i.gerrit.reportBugUrl).isEqualTo("https://example.com/report");
-    assertThat(i.gerrit.reportBugText).isEqualTo("REPORT BUG");
 
     // plugin
     assertThat(i.plugin.jsResourcePaths).isEmpty();
@@ -128,10 +125,7 @@
     assertThat(i.user.anonymousCowardName).isEqualTo("Unnamed User");
 
     // notedb
-    notesMigration.setReadChanges(true);
     assertThat(gApi.config().server().getInfo().noteDbEnabled).isTrue();
-    notesMigration.setReadChanges(false);
-    assertThat(gApi.config().server().getInfo().noteDbEnabled).isNull();
   }
 
   @Test
@@ -183,7 +177,6 @@
     assertThat(i.gerrit.allProjects).isEqualTo(AllProjectsNameProvider.DEFAULT);
     assertThat(i.gerrit.allUsers).isEqualTo(AllUsersNameProvider.DEFAULT);
     assertThat(i.gerrit.reportBugUrl).isNull();
-    assertThat(i.gerrit.reportBugText).isNull();
 
     // plugin
     assertThat(i.plugin.jsResourcePaths).isEmpty();
@@ -197,13 +190,4 @@
     // user
     assertThat(i.user.anonymousCowardName).isEqualTo(AnonymousCowardNameProvider.DEFAULT);
   }
-
-  @Test
-  @GerritConfig(name = "auth.contributorAgreements", value = "true")
-  public void anonymousAccess() throws Exception {
-    configureContributorAgreement(true);
-
-    setApiUserAnonymous();
-    gApi.config().server().getInfo();
-  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java b/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java
index 95bc5a6..3d987a3 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java
@@ -188,7 +188,7 @@
     if (force) {
       testRepo.reset(initialHead);
     }
-    commit(user.getIdent(), "subject");
+    commit(user.newIdent(), "subject");
 
     boolean createTag = tagName == null;
     tagName = MoreObjects.firstNonNull(tagName, "v1_" + System.nanoTime());
@@ -197,9 +197,9 @@
         break;
       case ANNOTATED:
         if (createTag) {
-          createAnnotatedTag(testRepo, tagName, user.getIdent());
+          createAnnotatedTag(testRepo, tagName, user.newIdent());
         } else {
-          updateAnnotatedTag(testRepo, tagName, user.getIdent());
+          updateAnnotatedTag(testRepo, tagName, user.newIdent());
         }
         break;
       default:
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
index 2722648..72af075 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
@@ -21,6 +21,8 @@
 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.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.Permission;
@@ -63,20 +65,20 @@
 
 public class AccessIT extends AbstractDaemonTest {
 
-  private static final String PROJECT_NAME = "newProject";
-
   private static final String REFS_ALL = Constants.R_REFS + "*";
   private static final String REFS_HEADS = Constants.R_HEADS + "*";
 
   private static final String LABEL_CODE_REVIEW = "Code-Review";
 
-  private Project.NameKey newProjectName;
-
   @Inject private DynamicSet<FileHistoryWebLink> fileHistoryWebLinkDynamicSet;
+  @Inject private ProjectOperations projectOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
+
+  private Project.NameKey newProjectName;
 
   @Before
   public void setUp() throws Exception {
-    newProjectName = createProject(PROJECT_NAME);
+    newProjectName = projectOperations.newProject().create();
   }
 
   @Test
@@ -90,14 +92,8 @@
     RegistrationHandle handle =
         fileHistoryWebLinkDynamicSet.add(
             "gerrit",
-            new FileHistoryWebLink() {
-              @Override
-              public WebLinkInfo getFileHistoryWebLink(
-                  String projectName, String revision, String fileName) {
-                return new WebLinkInfo(
-                    "name", "imageURL", "http://view/" + projectName + "/" + fileName);
-              }
-            });
+            (projectName, revision, fileName) ->
+                new WebLinkInfo("name", "imageURL", "http://view/" + projectName + "/" + fileName));
     try {
       ProjectAccessInfo info = pApi().access();
       assertThat(info.configWebLinks).hasSize(1);
@@ -113,14 +109,8 @@
     RegistrationHandle handle =
         fileHistoryWebLinkDynamicSet.add(
             "gerrit",
-            new FileHistoryWebLink() {
-              @Override
-              public WebLinkInfo getFileHistoryWebLink(
-                  String projectName, String revision, String fileName) {
-                return new WebLinkInfo(
-                    "name", "imageURL", "http://view/" + projectName + "/" + fileName);
-              }
-            });
+            (projectName, revision, fileName) ->
+                new WebLinkInfo("name", "imageURL", "http://view/" + projectName + "/" + fileName));
     try (Repository repo = repoManager.openRepository(newProjectName)) {
       RefUpdate u = repo.updateRef(RefNames.REFS_CONFIG);
       u.setForceUpdate(true);
@@ -181,7 +171,7 @@
   public void createAccessChange() throws Exception {
     allow(newProjectName, RefNames.REFS_CONFIG, Permission.READ, REGISTERED_USERS);
     // User can see the branch
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     pApi().branch("refs/heads/master").get();
 
     ProjectAccessInput accessInput = newProjectAccessInput();
@@ -196,7 +186,7 @@
     accessSection.permissions.put(Permission.READ, read);
     accessInput.add.put(REFS_HEADS, accessSection);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     ChangeInfo out = pApi().accessChange(accessInput);
 
     assertThat(out.project).isEqualTo(newProjectName.get());
@@ -204,7 +194,7 @@
     assertThat(out.status).isEqualTo(ChangeStatus.NEW);
     assertThat(out.submitted).isNull();
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
 
     ChangeInfo c = gApi.changes().id(out._number).get(MESSAGES);
     assertThat(c.messages.stream().map(m -> m.message)).containsExactly("Uploaded patch set 1");
@@ -215,7 +205,7 @@
     gApi.changes().id(out._number).current().submit();
 
     // check that the change took effect.
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     try {
       BranchInfo info = pApi().branch("refs/heads/master").get();
       fail("wanted failure, got " + newGson().toJson(info));
@@ -226,16 +216,16 @@
     // Restore.
     accessInput.add.clear();
     accessInput.remove.put(REFS_HEADS, accessSection);
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     out = pApi().accessChange(accessInput);
 
     gApi.changes().id(out._number).current().review(reviewIn);
     gApi.changes().id(out._number).current().submit();
 
     // Now it works again.
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     pApi().branch("refs/heads/master").get();
   }
 
@@ -335,7 +325,7 @@
     accessInput.add.put(REFS_ALL, accessSectionInfo);
     pApi().access(accessInput);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(ResourceNotFoundException.class);
     pApi().access();
   }
@@ -355,7 +345,7 @@
     AccessSectionInfo accessSectionInfoToApply = createDefaultAccessSectionInfo();
     accessInfoToApply.add.put(REFS_HEADS, accessSectionInfoToApply);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(ResourceNotFoundException.class);
     pApi().access();
   }
@@ -402,7 +392,7 @@
     assertThat(owners.includes).isNull();
 
     // PROJECT_OWNERS is invisible to anonymous user, but GetAccess disregards visibility.
-    setApiUserAnonymous();
+    requestScopeOperations.setApiUserAnonymous();
     ProjectAccessInfo anonResult = pApi().access();
     assertThat(anonResult.groups.keySet())
         .containsExactly(
@@ -412,13 +402,13 @@
   @Test
   public void updateParentAsUser() throws Exception {
     // Create child
-    String newParentProjectName = createProject(PROJECT_NAME + "PA").get();
+    String newParentProjectName = projectOperations.newProject().create().get();
 
     // Set new parent
     ProjectAccessInput accessInput = newProjectAccessInput();
     accessInput.parent = newParentProjectName;
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("administrate server not permitted");
     pApi().access(accessInput);
@@ -427,7 +417,7 @@
   @Test
   public void updateParentAsAdministrator() throws Exception {
     // Create parent
-    String newParentProjectName = createProject(PROJECT_NAME + "PA").get();
+    String newParentProjectName = projectOperations.newProject().create().get();
 
     // Set new parent
     ProjectAccessInput accessInput = newProjectAccessInput();
@@ -445,7 +435,7 @@
 
     accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     gApi.projects().name(allProjects.get()).access(accessInput);
   }
@@ -465,7 +455,7 @@
                 .get(AccessSection.GLOBAL_CAPABILITIES)
                 .permissions
                 .keySet())
-        .containsAllIn(accessSectionInfo.permissions.keySet());
+        .containsAtLeastElementsIn(accessSectionInfo.permissions.keySet());
   }
 
   @Test
@@ -501,7 +491,7 @@
 
     accessInput.remove.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     gApi.projects().name(allProjects.get()).access(accessInput);
   }
@@ -527,7 +517,7 @@
                 .get(AccessSection.GLOBAL_CAPABILITIES)
                 .permissions
                 .keySet())
-        .containsAllIn(accessSectionInfo.permissions.keySet());
+        .containsAtLeastElementsIn(accessSectionInfo.permissions.keySet());
 
     // Remove
     accessInput.add.clear();
@@ -571,7 +561,7 @@
     config = cfg.toText();
     PushOneCommit push =
         pushFactory.create(
-            db, admin.getIdent(), allProjectsRepo, "Subject", ProjectConfig.PROJECT_CONFIG, config);
+            admin.newIdent(), allProjectsRepo, "Subject", ProjectConfig.PROJECT_CONFIG, config);
     push.to(RefNames.REFS_CONFIG).assertOkStatus();
 
     // Verify that unknownPermission is present
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/BUILD b/javatests/com/google/gerrit/acceptance/rest/project/BUILD
index e8a13c9..200b26a 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/BUILD
+++ b/javatests/com/google/gerrit/acceptance/rest/project/BUILD
@@ -9,6 +9,7 @@
         ":project",
         ":push_tag_util",
         ":refassert",
+        "//lib/commons:lang",
     ],
 )
 
@@ -34,7 +35,6 @@
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib/truth",
     ],
 )
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
index c49a62b..698f7e0 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
@@ -21,8 +21,8 @@
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.projects.BranchApi;
 import com.google.gerrit.extensions.api.projects.BranchInfo;
@@ -35,11 +35,14 @@
 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 RequestScopeOperations requestScopeOperations;
+
   private Branch.NameKey testBranch;
 
   @Before
@@ -62,7 +65,7 @@
 
   @Test
   public void createBranch_Forbidden() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     assertCreateFails(testBranch, AuthException.class, "not permitted: create on refs/heads/test");
   }
 
@@ -80,7 +83,7 @@
   @Test
   public void createBranchByProjectOwner() throws Exception {
     grantOwner();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     assertCreateSucceeds(testBranch);
   }
 
@@ -94,7 +97,7 @@
   public void createBranchByProjectOwnerCreateReferenceBlocked_Forbidden() throws Exception {
     grantOwner();
     blockCreateReference();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     assertCreateFails(testBranch, AuthException.class, "not permitted: create on refs/heads/test");
   }
 
@@ -112,13 +115,12 @@
     allow(allUsers, RefNames.REFS_USERS + "*", Permission.PUSH, REGISTERED_USERS);
     assertCreateFails(
         new Branch.NameKey(allUsers, RefNames.refsUsers(new Account.Id(1))),
-        RefNames.refsUsers(admin.getId()),
+        RefNames.refsUsers(admin.id()),
         ResourceConflictException.class,
         "Not allowed to create user branch.");
   }
 
   @Test
-  @GerritConfig(name = "noteDb.groups.write", value = "true")
   public void createGroupBranch_Conflict() throws Exception {
     allow(allUsers, RefNames.REFS_GROUPS + "*", Permission.CREATE, REGISTERED_USERS);
     allow(allUsers, RefNames.REFS_GROUPS + "*", Permission.PUSH, REGISTERED_USERS);
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
index 97558c4..463532f 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.api.projects.ConfigInfo;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
@@ -48,6 +49,7 @@
 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;
 import java.util.Collections;
 import java.util.Optional;
 import java.util.Set;
@@ -70,6 +72,8 @@
 import org.junit.Test;
 
 public class CreateProjectIT extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @Test
   public void createProjectHttp() throws Exception {
     String newProjectName = name("newProject");
@@ -117,7 +121,7 @@
         Future<RestResponse> r1 = executor.submit(createProjectFoo);
         Future<RestResponse> r2 = executor.submit(createProjectFoo);
         assertThat(ImmutableList.of(r1.get().getStatusCode(), r2.get().getStatusCode()))
-            .containsAllOf(HttpStatus.SC_CREATED, HttpStatus.SC_CONFLICT);
+            .containsAtLeast(HttpStatus.SC_CREATED, HttpStatus.SC_CONFLICT);
       }
     } finally {
       executor.shutdown();
@@ -321,7 +325,7 @@
   public void createProjectWithCapability() throws Exception {
     allowGlobalCapabilities(SystemGroupBackend.REGISTERED_USERS, GlobalCapability.CREATE_PROJECT);
     try {
-      setApiUser(user);
+      requestScopeOperations.setApiUser(user.id());
       ProjectInput in = new ProjectInput();
       in.name = name("newProject");
       ProjectInfo p = gApi.projects().create(in).get();
@@ -334,7 +338,7 @@
 
   @Test
   public void createProjectWithoutCapability_Forbidden() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     ProjectInput in = new ProjectInput();
     in.name = name("newProject");
     assertCreateFails(in, AuthException.class);
@@ -353,7 +357,7 @@
     parent.setState(com.google.gerrit.extensions.client.ProjectState.HIDDEN);
     allowGlobalCapabilities(SystemGroupBackend.REGISTERED_USERS, GlobalCapability.CREATE_PROJECT);
     try {
-      setApiUser(user);
+      requestScopeOperations.setApiUser(user.id());
       ProjectInput in = new ProjectInput();
       in.name = name("newProject");
       ProjectInfo p = gApi.projects().create(in).get();
@@ -470,8 +474,8 @@
   }
 
   private Optional<String> readProjectConfig(String projectName) throws Exception {
-    try (Repository repo = repoManager.openRepository(new Project.NameKey(projectName))) {
-      TestRepository<?> tr = new TestRepository<>(repo);
+    try (Repository repo = repoManager.openRepository(new Project.NameKey(projectName));
+        TestRepository<Repository> tr = new TestRepository<>(repo)) {
       RevWalk rw = tr.getRevWalk();
       Ref ref = repo.exactRef(RefNames.REFS_CONFIG);
       if (ref == null) {
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
index 18e6dc8..ccdc497 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
@@ -21,8 +21,9 @@
 import static org.eclipse.jgit.lib.Constants.R_HEADS;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.GerritConfig;
 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.BranchApi;
 import com.google.gerrit.extensions.api.projects.BranchInput;
@@ -33,23 +34,26 @@
 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;
 
 public class DeleteBranchIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   private Branch.NameKey testBranch;
 
   @Before
   public void setUp() throws Exception {
-    project = createProject(name("p"));
+    project = projectOperations.newProject().create();
     testBranch = new Branch.NameKey(project, "test");
     branch(testBranch).create(new BranchInput());
   }
 
   @Test
   public void deleteBranch_Forbidden() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     assertDeleteForbidden(testBranch);
   }
 
@@ -61,7 +65,7 @@
   @Test
   public void deleteBranchByProjectOwner() throws Exception {
     grantOwner();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     assertDeleteSucceeds(testBranch);
   }
 
@@ -75,21 +79,21 @@
   public void deleteBranchByProjectOwnerForcePushBlocked_Forbidden() throws Exception {
     grantOwner();
     blockForcePush();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     assertDeleteForbidden(testBranch);
   }
 
   @Test
   public void deleteBranchByUserWithForcePushPermission() throws Exception {
     grantForcePush();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     assertDeleteSucceeds(testBranch);
   }
 
   @Test
   public void deleteBranchByUserWithDeletePermission() throws Exception {
     grantDelete();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     assertDeleteSucceeds(testBranch);
   }
 
@@ -136,11 +140,10 @@
 
     exception.expect(ResourceConflictException.class);
     exception.expectMessage("Not allowed to delete user branch.");
-    branch(new Branch.NameKey(allUsers, RefNames.refsUsers(admin.id))).delete();
+    branch(new Branch.NameKey(allUsers, RefNames.refsUsers(admin.id()))).delete();
   }
 
   @Test
-  @GerritConfig(name = "noteDb.groups.write", value = "true")
   public void deleteGroupBranch_Conflict() throws Exception {
     allow(allUsers, RefNames.REFS_GROUPS + "*", Permission.CREATE, REGISTERED_USERS);
     allow(allUsers, RefNames.REFS_GROUPS + "*", Permission.PUSH, REGISTERED_USERS);
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
index ca7517c..4d72123 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
@@ -25,6 +25,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.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.api.projects.DeleteBranchesInput;
@@ -34,6 +35,7 @@
 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;
 import org.eclipse.jgit.lib.Repository;
@@ -46,6 +48,8 @@
   private static final ImmutableList<String> BRANCHES =
       ImmutableList.of("refs/heads/test-1", "refs/heads/test-2", "test-3", "refs/meta/foo");
 
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @Before
   public void setUp() throws Exception {
     allow("refs/*", Permission.CREATE, REGISTERED_USERS);
@@ -72,14 +76,14 @@
 
     DeleteBranchesInput input = new DeleteBranchesInput();
     input.branches = branchToDelete;
-    setApiUser(user);
+    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");
     }
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     assertBranches(BRANCHES);
   }
 
@@ -87,14 +91,14 @@
   public void deleteMultiBranchesWithoutPermissionForbidden() throws Exception {
     DeleteBranchesInput input = new DeleteBranchesInput();
     input.branches = BRANCHES;
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     try {
       project().deleteBranches(input);
       fail("Expected ResourceConflictException");
     } catch (ResourceConflictException e) {
       assertThat(e).hasMessageThat().isEqualTo(errorMessageForBranches(BRANCHES));
     }
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     assertBranches(BRANCHES);
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java
index 3ae0b44..07bb2b1 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java
@@ -21,18 +21,22 @@
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.projects.TagApi;
 import com.google.gerrit.extensions.api.projects.TagInfo;
 import com.google.gerrit.extensions.api.projects.TagInput;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.inject.Inject;
 import org.junit.Before;
 import org.junit.Test;
 
 public class DeleteTagIT extends AbstractDaemonTest {
   private static final String TAG = "refs/tags/test";
 
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @Before
   public void setUp() throws Exception {
     tag().create(new TagInput());
@@ -40,7 +44,7 @@
 
   @Test
   public void deleteTag_Forbidden() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     assertDeleteForbidden();
   }
 
@@ -52,7 +56,7 @@
   @Test
   public void deleteTagByProjectOwner() throws Exception {
     grantOwner();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     assertDeleteSucceeds();
   }
 
@@ -66,21 +70,21 @@
   public void deleteTagByProjectOwnerForcePushBlocked_Forbidden() throws Exception {
     grantOwner();
     blockForcePush();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     assertDeleteForbidden();
   }
 
   @Test
   public void deleteTagByUserWithForcePushPermission() throws Exception {
     grantForcePush();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     assertDeleteSucceeds();
   }
 
   @Test
   public void deleteTagByUserWithDeletePermission() throws Exception {
     grantDelete();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     assertDeleteSucceeds();
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagsIT.java
index 2ada724..fae9d00 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagsIT.java
@@ -23,11 +23,13 @@
 import com.google.common.collect.Lists;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.extensions.api.projects.DeleteTagsInput;
 import com.google.gerrit.extensions.api.projects.ProjectApi;
 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.inject.Inject;
 import java.util.HashMap;
 import java.util.List;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -39,6 +41,8 @@
   private static final ImmutableList<String> TAGS =
       ImmutableList.of("refs/tags/test-1", "refs/tags/test-2", "refs/tags/test-3", "test-4");
 
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @Before
   public void setUp() throws Exception {
     for (String name : TAGS) {
@@ -61,14 +65,14 @@
   public void deleteTagsForbidden() throws Exception {
     DeleteTagsInput input = new DeleteTagsInput();
     input.tags = TAGS;
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     try {
       project().deleteTags(input);
       fail("Expected ResourceConflictException");
     } catch (ResourceConflictException e) {
       assertThat(e).hasMessageThat().isEqualTo(errorMessageForTags(TAGS));
     }
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     assertTags(TAGS);
   }
 
@@ -154,6 +158,6 @@
   }
 
   private void assertTagsDeleted() throws Exception {
-    assertTags(ImmutableList.<String>of());
+    assertTags(ImmutableList.of());
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java b/javatests/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java
index 78d0270..48527af 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java
@@ -18,12 +18,14 @@
 import com.google.gerrit.acceptance.GcAssert;
 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.inject.Inject;
 import org.junit.Before;
 import org.junit.Test;
 
 public class GarbageCollectionIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
 
   @Inject private GcAssert gcAssert;
 
@@ -31,7 +33,7 @@
 
   @Before
   public void setUp() throws Exception {
-    project2 = createProject("p2");
+    project2 = projectOperations.newProject().create();
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java b/javatests/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java
index d5e811d..d736578 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java
@@ -18,13 +18,16 @@
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 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;
 
 @NoHttpd
 public class GetChildProjectIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
 
   @Test
   public void getNonExistingChildProject_NotFound() throws Exception {
@@ -33,15 +36,15 @@
 
   @Test
   public void getNonChildProject_NotFound() throws Exception {
-    Project.NameKey p1 = createProject("p1");
-    Project.NameKey p2 = createProject("p2");
+    Project.NameKey p1 = projectOperations.newProject().create();
+    Project.NameKey p2 = projectOperations.newProject().create();
 
     assertChildNotFound(p1, p2.get());
   }
 
   @Test
   public void getChildProject() throws Exception {
-    Project.NameKey child = createProject("p1");
+    Project.NameKey child = projectOperations.newProject().create();
     ProjectInfo childInfo = gApi.projects().name(allProjects.get()).child(child.get()).get();
 
     assertProjectInfo(projectCache.get(child).getProject(), childInfo);
@@ -49,16 +52,16 @@
 
   @Test
   public void getGrandChildProject_NotFound() throws Exception {
-    Project.NameKey child = createProject("p1");
-    Project.NameKey grandChild = createProject("p1.1", child);
+    Project.NameKey child = projectOperations.newProject().create();
+    Project.NameKey grandChild = projectOperations.newProject().parent(child).create();
 
     assertChildNotFound(allProjects, grandChild.get());
   }
 
   @Test
   public void getGrandChildProjectWithRecursiveFlag() throws Exception {
-    Project.NameKey child = createProject("p1");
-    Project.NameKey grandChild = createProject("p1.1", child);
+    Project.NameKey child = projectOperations.newProject().create();
+    Project.NameKey grandChild = projectOperations.newProject().parent(child).create();
 
     ProjectInfo grandChildInfo =
         gApi.projects().name(allProjects.get()).child(grandChild.get()).get(true);
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/GetCommitIT.java b/javatests/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
index 0632221..18c706b 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
@@ -85,8 +85,7 @@
   @Test
   public void getOpenChange_Found() throws Exception {
     unblockRead();
-    PushOneCommit.Result r =
-        pushFactory.create(db, admin.getIdent(), testRepo).to("refs/for/master");
+    PushOneCommit.Result r = pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master");
     r.assertOkStatus();
 
     CommitInfo info = getCommit(r.getCommit());
@@ -108,8 +107,7 @@
 
   @Test
   public void getOpenChange_NotFound() throws Exception {
-    PushOneCommit.Result r =
-        pushFactory.create(db, admin.getIdent(), testRepo).to("refs/for/master");
+    PushOneCommit.Result r = pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master");
     r.assertOkStatus();
     assertNotFound(r.getCommit());
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
index b46b087..ec1c708 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
@@ -20,15 +20,19 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 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 RequestScopeOperations requestScopeOperations;
+
   @Test
   public void listBranchesOfNonExistingProject_NotFound() throws Exception {
     exception.expect(ResourceNotFoundException.class);
@@ -38,7 +42,7 @@
   @Test
   public void listBranchesOfNonVisibleProject_NotFound() throws Exception {
     blockRead("refs/*");
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(ResourceNotFoundException.class);
     gApi.projects().name(project.get()).branches().get();
   }
@@ -70,7 +74,7 @@
     blockRead("refs/heads/dev");
     String master = pushTo("refs/heads/master").getCommit().name();
     pushTo("refs/heads/dev");
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     // refs/meta/config is hidden since user is no project owner
     assertRefs(
         ImmutableList.of(
@@ -83,7 +87,7 @@
     blockRead("refs/heads/master");
     pushTo("refs/heads/master");
     String dev = pushTo("refs/heads/dev").getCommit().name();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     // refs/meta/config is hidden since user is no project owner
     assertRefs(ImmutableList.of(branch("refs/heads/dev", dev, false)), list().get());
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
index 90906ba..7746820 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
@@ -18,12 +18,16 @@
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 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;
 
 @NoHttpd
 public class ListChildProjectsIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
 
   @Test
   public void listChildrenOfNonExistingProject_NotFound() throws Exception {
@@ -39,32 +43,38 @@
 
   @Test
   public void listChildren() throws Exception {
-    Project.NameKey child1 = createProject("p1");
-    Project.NameKey child1_1 = createProject("p1.1", child1);
-    Project.NameKey child1_2 = createProject("p1.2", child1);
+    Project.NameKey child1 = projectOperations.newProject().create();
+    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())
-        .containsExactly(child1_1, child1_2)
-        .inOrder();
+        .containsExactly(child1_1, child1_2);
   }
 
   @Test
   public void listChildrenWithLimit() throws Exception {
-    Project.NameKey child1 = createProject("p1");
-    Project.NameKey child1_1 = createProject("p1.1", child1);
-    createProject("p1.2", child1);
+    String prefix = RandomStringUtils.randomAlphabetic(8);
+    Project.NameKey child1 = projectOperations.newProject().name(prefix + "p1").create();
+    Project.NameKey child1_1 =
+        projectOperations.newProject().parent(child1).name(prefix + "p1.1").create();
+    projectOperations.newProject().parent(child1).name(prefix + "p1.2").create();
 
     assertThatNameList(gApi.projects().name(child1.get()).children(1)).containsExactly(child1_1);
   }
 
   @Test
   public void listChildrenRecursively() throws Exception {
-    Project.NameKey child1 = createProject("p1");
-    createProject("p2");
-    Project.NameKey child1_1 = createProject("p1.1", child1);
-    Project.NameKey child1_2 = createProject("p1.2", child1);
-    Project.NameKey child1_1_1 = createProject("p1.1.1", child1_1);
-    Project.NameKey child1_1_1_1 = createProject("p1.1.1.1", child1_1_1);
+    String prefix = RandomStringUtils.randomAlphabetic(8);
+    Project.NameKey child1 = projectOperations.newProject().name(prefix + "p1").create();
+    Project.NameKey child1_1 =
+        projectOperations.newProject().parent(child1).name(prefix + "p1.1").create();
+    Project.NameKey child1_2 =
+        projectOperations.newProject().parent(child1).name(prefix + "p1.2").create();
+    Project.NameKey child1_1_1 =
+        projectOperations.newProject().parent(child1_1).name(prefix + "p1.1.1").create();
+    Project.NameKey child1_1_1_1 =
+        projectOperations.newProject().parent(child1_1_1).name(prefix + "p1.1.1.1").create();
 
     assertThatNameList(gApi.projects().name(child1.get()).children(true))
         .containsExactly(child1_1, child1_1_1, child1_1_1_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 dfce883..9dba8cf 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
@@ -28,6 +28,8 @@
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.Sandboxed;
 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.extensions.api.projects.ConfigInfo;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
@@ -36,9 +38,8 @@
 import com.google.gerrit.extensions.client.ProjectState;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.json.OutputFormat;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.OutputFormat;
 import com.google.gerrit.server.project.ProjectCacheImpl;
 import com.google.gerrit.server.project.testing.Util;
 import com.google.gerrit.server.restapi.project.ListProjects;
@@ -55,19 +56,21 @@
 @NoHttpd
 @Sandboxed
 public class ListProjectsIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
   @Inject private ListProjects listProjects;
 
   @Test
   public void listProjects() throws Exception {
-    Project.NameKey someProject = createProject("some-project");
-    assertThatNameList(filter(gApi.projects().list().get()))
-        .containsExactly(allProjects, allUsers, project, someProject)
-        .inOrder();
+    Project.NameKey someProject = projectOperations.newProject().create();
+    assertThatNameList(gApi.projects().list().get())
+        .containsExactly(allProjects, allUsers, project, someProject);
+    assertThatNameList(gApi.projects().list().get()).isOrdered();
   }
 
   @Test
   public void listProjectsFiltersInvisibleProjects() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     assertThatNameList(gApi.projects().list().get()).contains(project);
 
     try (ProjectConfigUpdate u = updateProject(project)) {
@@ -75,7 +78,7 @@
       u.save();
     }
 
-    assertThatNameList(filter(gApi.projects().list().get())).doesNotContain(project);
+    assertThatNameList(gApi.projects().list().get()).doesNotContain(project);
   }
 
   @Test
@@ -104,16 +107,15 @@
   @Test
   public void listProjectsWithLimit() throws Exception {
     ProjectCacheImpl projectCacheImpl = (ProjectCacheImpl) projectCache;
-    for (int i = 0; i < 5; i++) {
-      createProject("someProject" + i);
+    String pre = "lpwl-someProject";
+    int n = 6;
+    for (int i = 0; i < n; i++) {
+      projectOperations.newProject().name(pre + i).create();
     }
 
-    String p = name("");
-    // 5, plus p which was automatically created.
-    int n = 6;
     projectCacheImpl.evictAllByName();
     for (int i = 1; i <= n + 2; i++) {
-      assertThatNameList(gApi.projects().list().withPrefix(p).withLimit(i).get())
+      assertThatNameList(gApi.projects().list().withPrefix(pre).withLimit(i).get())
           .hasSize(Math.min(i, n));
       assertThat(projectCacheImpl.sizeAllByName())
           .isAtMost((long) (i + 2)); // 2 = AllProjects + AllUsers
@@ -189,91 +191,87 @@
 
   private List<String> createProjects(String prefix, int numProjects) {
     return IntStream.range(0, numProjects)
-        .mapToObj(
-            i -> {
-              String projectName = prefix + i;
-              try {
-                return createProject(projectName);
-              } catch (RestApiException e) {
-                throw new IllegalStateException("Unable to create project " + projectName, e);
-              }
-            })
+        .mapToObj(i -> projectOperations.newProject().name(prefix + i).create())
         .map(Project.NameKey::get)
         .collect(toList());
   }
 
   @Test
   public void listProjectsWithPrefix() throws Exception {
-    Project.NameKey someProject = createProject("some-project");
-    Project.NameKey someOtherProject = createProject("some-other-project");
-    createProject("project-awesome");
+    Project.NameKey someProject = projectOperations.newProject().name("listtest-p1").create();
+    Project.NameKey someOtherProject = projectOperations.newProject().name("listtest-p2").create();
+    projectOperations.newProject().name("other-prefix-project").create();
 
-    String p = name("some");
+    String p = "listtest";
     assertBadRequest(gApi.projects().list().withPrefix(p).withRegex(".*"));
     assertBadRequest(gApi.projects().list().withPrefix(p).withSubstring(p));
-    assertThatNameList(filter(gApi.projects().list().withPrefix(p).get()))
-        .containsExactly(someOtherProject, someProject)
-        .inOrder();
-    p = name("SOME");
-    assertThatNameList(filter(gApi.projects().list().withPrefix(p).get())).isEmpty();
+    assertThatNameList(gApi.projects().list().withPrefix(p).get())
+        .containsExactly(someOtherProject, someProject);
+    p = "notlisttest";
+    assertThatNameList(gApi.projects().list().withPrefix(p).get()).isEmpty();
   }
 
   @Test
   public void listProjectsWithRegex() throws Exception {
-    Project.NameKey someProject = createProject("some-project");
-    Project.NameKey someOtherProject = createProject("some-other-project");
-    Project.NameKey projectAwesome = createProject("project-awesome");
+    Project.NameKey someProject = projectOperations.newProject().name("lpwr-some-project").create();
+    Project.NameKey someOtherProject =
+        projectOperations.newProject().name("lpwr-some-other-project").create();
+    Project.NameKey projectAwesome =
+        projectOperations.newProject().name("lpwr-project-awesome").create();
 
     assertBadRequest(gApi.projects().list().withRegex("[.*"));
     assertBadRequest(gApi.projects().list().withRegex(".*").withPrefix("p"));
     assertBadRequest(gApi.projects().list().withRegex(".*").withSubstring("p"));
 
-    assertThatNameList(filter(gApi.projects().list().withRegex(".*some").get()))
+    assertThatNameList(gApi.projects().list().withRegex(".*some").get())
         .containsExactly(projectAwesome);
-    String r = name("some-project$").replace(".", "\\.");
-    assertThatNameList(filter(gApi.projects().list().withRegex(r).get()))
-        .containsExactly(someProject);
-    assertThatNameList(filter(gApi.projects().list().withRegex(".*").get()))
+    String r = ("lpwr-some-project$").replace(".", "\\.");
+    assertThatNameList(gApi.projects().list().withRegex(r).get()).containsExactly(someProject);
+    assertThatNameList(gApi.projects().list().withRegex(".*").get())
         .containsExactly(
-            allProjects, allUsers, project, projectAwesome, someOtherProject, someProject)
-        .inOrder();
+            allProjects, allUsers, project, projectAwesome, someOtherProject, someProject);
   }
 
   @Test
   public void listProjectsWithStart() throws Exception {
+    String pre = "lpws-";
     for (int i = 0; i < 5; i++) {
-      createProject(new Project.NameKey("someProject" + i).get());
+      projectOperations.newProject().name(pre + i).create();
     }
 
-    String p = name("");
-    List<ProjectInfo> all = gApi.projects().list().withPrefix(p).get();
-    // 5, plus p which was automatically created.
-    int n = 6;
+    List<ProjectInfo> all = gApi.projects().list().withPrefix(pre).get();
+    int n = 5;
     assertThat(all).hasSize(n);
-    assertThatNameList(gApi.projects().list().withPrefix(p).withStart(n - 1).get())
+    assertThatNameList(gApi.projects().list().withPrefix(pre).withStart(n - 1).get())
         .containsExactly(new Project.NameKey(Iterables.getLast(all).name));
   }
 
   @Test
   public void listProjectsWithSubstring() throws Exception {
-    Project.NameKey someProject = createProject("some-project");
-    Project.NameKey someOtherProject = createProject("some-other-project");
-    Project.NameKey projectAwesome = createProject("project-awesome");
+    Project.NameKey someProject = projectOperations.newProject().name("some-project").create();
+    Project.NameKey someOtherProject =
+        projectOperations.newProject().name("some-other-project").create();
+    Project.NameKey projectAwesome =
+        projectOperations.newProject().name("project-awesome").create();
 
     assertBadRequest(gApi.projects().list().withSubstring("some").withRegex(".*"));
     assertBadRequest(gApi.projects().list().withSubstring("some").withPrefix("some"));
-    assertThatNameList(filter(gApi.projects().list().withSubstring("some").get()))
-        .containsExactly(projectAwesome, someOtherProject, someProject)
-        .inOrder();
-    assertThatNameList(filter(gApi.projects().list().withSubstring("SOME").get()))
-        .containsExactly(projectAwesome, someOtherProject, someProject)
-        .inOrder();
+    assertThatNameList(gApi.projects().list().withSubstring("some").get())
+        .containsExactly(projectAwesome, someOtherProject, someProject);
+    assertThatNameList(gApi.projects().list().withSubstring("SOME").get())
+        .containsExactly(projectAwesome, someOtherProject, someProject);
   }
 
   @Test
   public void listProjectsWithTree() throws Exception {
-    Project.NameKey someParentProject = createProject("some-parent-project");
-    Project.NameKey someChildProject = createProject("some-child-project", someParentProject);
+    Project.NameKey someParentProject =
+        projectOperations.newProject().name("some-parent-project").create();
+    Project.NameKey someChildProject =
+        projectOperations
+            .newProject()
+            .name("some-child-project")
+            .parent(someParentProject)
+            .create();
 
     Map<String, ProjectInfo> result = gApi.projects().list().withTree(true).getAsMap();
     assertThat(result).containsKey(someChildProject.get());
@@ -286,36 +284,14 @@
         gApi.projects().list().withType(FilterType.PERMISSIONS).getAsMap();
     assertThat(result.keySet()).containsExactly(allProjects.get(), allUsers.get());
 
-    assertThatNameList(filter(gApi.projects().list().withType(FilterType.ALL).get()))
-        .containsExactly(allProjects, allUsers, project)
-        .inOrder();
-  }
-
-  @Test
-  public void listParentCandidates() throws Exception {
-    Map<String, ProjectInfo> result =
-        gApi.projects().list().withType(FilterType.PARENT_CANDIDATES).getAsMap();
-    assertThat(result).hasSize(1);
-    assertThat(result).containsKey(allProjects.get());
-
-    // Create a new project with 'project' as parent
-    Project.NameKey testProject = createProject(name("test"), project);
-
-    // Parent candidates are All-Projects and 'project'
-    assertThatNameList(filter(gApi.projects().list().withType(FilterType.PARENT_CANDIDATES).get()))
-        .containsExactly(allProjects, project)
-        .inOrder();
-
-    // All projects are listed
-    assertThatNameList(filter(gApi.projects().list().get()))
-        .containsExactly(allProjects, allUsers, testProject, project)
-        .inOrder();
+    assertThatNameList(gApi.projects().list().withType(FilterType.ALL).get())
+        .containsExactly(allProjects, allUsers, project);
   }
 
   @Test
   public void listWithHiddenAndReadonlyProjects() throws Exception {
-    Project.NameKey hidden = createProject("project-to-hide");
-    Project.NameKey readonly = createProject("project-to-read");
+    Project.NameKey hidden = projectOperations.newProject().create();
+    Project.NameKey readonly = projectOperations.newProject().create();
 
     // Set project read-only
     ConfigInput input = new ConfigInput();
@@ -326,8 +302,7 @@
     // The hidden project is included because it was not hidden yet.
     // The read-only project is included.
     assertThatNameList(gApi.projects().list().get())
-        .containsExactly(allProjects, allUsers, project, hidden, readonly)
-        .inOrder();
+        .containsExactly(allProjects, allUsers, project, hidden, readonly);
 
     // Hide the project
     input.state = ProjectState.HIDDEN;
@@ -339,18 +314,15 @@
 
     // Hidden project is not included in the list
     assertThatNameList(gApi.projects().list().get())
-        .containsExactly(allProjects, allUsers, project, readonly)
-        .inOrder();
+        .containsExactly(allProjects, allUsers, project, readonly);
 
     // ALL filter applies to type, and doesn't include hidden state
     assertThatNameList(gApi.projects().list().withType(FilterType.ALL).get())
-        .containsExactly(allProjects, allUsers, project, readonly)
-        .inOrder();
+        .containsExactly(allProjects, allUsers, project, readonly);
 
     // "All" boolean option causes hidden projects to be included
     assertThatNameList(gApi.projects().list().withAll(true).get())
-        .containsExactly(allProjects, allUsers, project, hidden, readonly)
-        .inOrder();
+        .containsExactly(allProjects, allUsers, project, hidden, readonly);
 
     // "State" option causes only the projects in that state to be included
     assertThatNameList(gApi.projects().list().withState(ProjectState.HIDDEN).get())
@@ -358,8 +330,7 @@
     assertThatNameList(gApi.projects().list().withState(ProjectState.READ_ONLY).get())
         .containsExactly(readonly);
     assertThatNameList(gApi.projects().list().withState(ProjectState.ACTIVE).get())
-        .containsExactly(allProjects, allUsers, project)
-        .inOrder();
+        .containsExactly(allProjects, allUsers, project);
 
     // Cannot use "all" and "state" together
     assertBadRequest(gApi.projects().list().withAll(true).withState(ProjectState.ACTIVE));
@@ -373,16 +344,4 @@
       // Expected.
     }
   }
-
-  private Iterable<ProjectInfo> filter(Iterable<ProjectInfo> infos) {
-    String prefix = name("");
-    return Iterables.filter(
-        infos,
-        p -> {
-          return p.name != null
-              && (p.name.equals(allProjects.get())
-                  || p.name.equals(allUsers.get())
-                  || p.name.startsWith(prefix));
-        });
-  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/PluginAccessIT.java b/javatests/com/google/gerrit/acceptance/rest/project/PluginAccessIT.java
index 1e6afa8..e7663f7 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/PluginAccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/PluginAccessIT.java
@@ -26,15 +26,21 @@
 import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
 import com.google.gerrit.extensions.api.access.ProjectAccessInput;
 import com.google.gerrit.extensions.config.CapabilityDefinition;
+import com.google.gerrit.extensions.config.PluginProjectPermissionDefinition;
 import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.permissions.PluginPermissionsUtil;
 import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
 import com.google.inject.Module;
+import java.util.Set;
 import org.junit.Test;
 
-public class PluginAccessIT extends AbstractDaemonTest {
+public final class PluginAccessIT extends AbstractDaemonTest {
+  private static final String TEST_PLUGIN_NAME = "gerrit";
+  private static final String TEST_PLUGIN_CAPABILITY = "aPluginCapability";
+  private static final String TEST_PLUGIN_PROJECT_PERMISSION = "aPluginProjectPermission";
 
-  private static final String CORE_PLUGIN_PREFIX = "gerrit-";
-  private static final String PLUGIN_CAPABILITY = "printHello";
+  @Inject PluginPermissionsUtil pluginPermissionsUtil;
 
   @Override
   public Module createModule() {
@@ -42,12 +48,21 @@
       @Override
       protected void configure() {
         bind(CapabilityDefinition.class)
-            .annotatedWith(Exports.named(PLUGIN_CAPABILITY))
+            .annotatedWith(Exports.named(TEST_PLUGIN_CAPABILITY))
             .toInstance(
                 new CapabilityDefinition() {
                   @Override
                   public String getDescription() {
-                    return "Print Hello";
+                    return "A Plugin Capability";
+                  }
+                });
+        bind(PluginProjectPermissionDefinition.class)
+            .annotatedWith(Exports.named(TEST_PLUGIN_PROJECT_PERMISSION))
+            .toInstance(
+                new PluginProjectPermissionDefinition() {
+                  @Override
+                  public String getDescription() {
+                    return "A Plugin Project Permission";
                   }
                 });
       }
@@ -55,24 +70,47 @@
   }
 
   @Test
-  public void addPluginCapability() throws Exception {
-    ProjectAccessInput accessInput = new ProjectAccessInput();
-    AccessSectionInfo accessSectionInfo = new AccessSectionInfo();
-    PermissionInfo email = new PermissionInfo(null, null);
-    PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
+  public void setAccessAddPluginCapabilitySucceed() throws Exception {
+    String pluginCapability = TEST_PLUGIN_NAME + "-" + TEST_PLUGIN_CAPABILITY;
+    ProjectAccessInput accessInput =
+        createAccessInput(AccessSection.GLOBAL_CAPABILITIES, pluginCapability);
 
-    email.rules = ImmutableMap.of(SystemGroupBackend.REGISTERED_USERS.get(), pri);
-    accessSectionInfo.permissions = ImmutableMap.of(CORE_PLUGIN_PREFIX + PLUGIN_CAPABILITY, email);
-    accessInput.add = ImmutableMap.of(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
-
-    ProjectAccessInfo updatedAccessSectionInfo =
+    ProjectAccessInfo projectAccessInfo =
         gApi.projects().name(allProjects.get()).access(accessInput);
-    assertThat(
-            updatedAccessSectionInfo
-                .local
-                .get(AccessSection.GLOBAL_CAPABILITIES)
-                .permissions
-                .keySet())
-        .containsAllIn(accessSectionInfo.permissions.keySet());
+
+    Set<String> capabilities =
+        projectAccessInfo.local.get(AccessSection.GLOBAL_CAPABILITIES).permissions.keySet();
+    assertThat(capabilities).contains(pluginCapability);
+    // Verifies the plugin defined capability could be listed.
+    assertThat(pluginPermissionsUtil.collectPluginCapabilities()).containsKey(pluginCapability);
+  }
+
+  @Test
+  public void setAccessAddPluginProjectPermissionSucceed() throws Exception {
+    String pluginProjectPermission =
+        "plugin-" + TEST_PLUGIN_NAME + "-" + TEST_PLUGIN_PROJECT_PERMISSION;
+    String accessSection = "refs/heads/plugin-permission";
+    ProjectAccessInput accessInput = createAccessInput(accessSection, pluginProjectPermission);
+
+    ProjectAccessInfo projectAccessInfo =
+        gApi.projects().name(allProjects.get()).access(accessInput);
+
+    Set<String> permissions = projectAccessInfo.local.get(accessSection).permissions.keySet();
+    assertThat(permissions).contains(pluginProjectPermission);
+    // Verifies the plugin defined capability could be listed.
+    assertThat(pluginPermissionsUtil.collectPluginProjectPermissions())
+        .containsKey(pluginProjectPermission);
+  }
+
+  private static ProjectAccessInput createAccessInput(String accessSection, String permissionName) {
+    ProjectAccessInput accessInput = new ProjectAccessInput();
+    PermissionRuleInfo ruleInfo = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
+    PermissionInfo email = new PermissionInfo(null, null);
+    email.rules = ImmutableMap.of(SystemGroupBackend.REGISTERED_USERS.get(), ruleInfo);
+    AccessSectionInfo accessSectionInfo = new AccessSectionInfo();
+    accessSectionInfo.permissions = ImmutableMap.of(permissionName, email);
+    accessInput.add = ImmutableMap.of(accessSection, accessSectionInfo);
+
+    return accessInput;
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java
index c78b47b..bf2a534 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java
@@ -19,9 +19,11 @@
 
 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.server.project.ProjectState;
+import com.google.inject.Inject;
 import java.util.Arrays;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
@@ -29,6 +31,8 @@
 import org.junit.Test;
 
 public class ProjectLevelConfigIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
+
   @Before
   public void setUp() throws Exception {
     fetch(testRepo, RefNames.REFS_CONFIG + ":refs/heads/config");
@@ -43,12 +47,7 @@
     cfg.setString("s2", "ss", "k2", "v2");
     PushOneCommit push =
         pushFactory.create(
-            db,
-            admin.getIdent(),
-            testRepo,
-            "Create Project Level Config",
-            configName,
-            cfg.toText());
+            admin.newIdent(), testRepo, "Create Project Level Config", configName, cfg.toText());
     push.to(RefNames.REFS_CONFIG);
 
     ProjectState state = projectCache.get(project);
@@ -73,8 +72,7 @@
 
     pushFactory
         .create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             "Create Project Level Config",
             configName,
@@ -82,7 +80,7 @@
         .to(RefNames.REFS_CONFIG)
         .assertOkStatus();
 
-    Project.NameKey childProject = createProject("child", project);
+    Project.NameKey childProject = projectOperations.newProject().parent(project).create();
     TestRepository<?> childTestRepo = cloneProject(childProject);
     fetch(childTestRepo, RefNames.REFS_CONFIG + ":refs/heads/config");
     childTestRepo.reset("refs/heads/config");
@@ -93,8 +91,7 @@
 
     pushFactory
         .create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             childTestRepo,
             "Create Project Level Config",
             configName,
@@ -128,8 +125,7 @@
 
     pushFactory
         .create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             "Create Project Level Config",
             configName,
@@ -137,7 +133,7 @@
         .to(RefNames.REFS_CONFIG)
         .assertOkStatus();
 
-    Project.NameKey childProject = createProject("child", project);
+    Project.NameKey childProject = projectOperations.newProject().parent(project).create();
     TestRepository<?> childTestRepo = cloneProject(childProject);
     fetch(childTestRepo, RefNames.REFS_CONFIG + ":refs/heads/config");
     childTestRepo.reset("refs/heads/config");
@@ -154,8 +150,7 @@
 
     pushFactory
         .create(
-            db,
-            admin.getIdent(),
+            admin.newIdent(),
             childTestRepo,
             "Create Project Level Config",
             configName,
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
index 610ce92..27e923e 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
@@ -23,6 +23,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.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.projects.ProjectApi.ListRefsRequest;
 import com.google.gerrit.extensions.api.projects.TagApi;
@@ -33,6 +34,7 @@
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.inject.Inject;
 import java.sql.Timestamp;
 import java.util.List;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -57,6 +59,8 @@
           + "=XFeC\n"
           + "-----END PGP SIGNATURE-----";
 
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @Test
   public void listTagsOfNonExistingProject() throws Exception {
     exception.expect(ResourceNotFoundException.class);
@@ -72,7 +76,7 @@
   @Test
   public void listTagsOfNonVisibleProject() throws Exception {
     blockRead("refs/*");
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(ResourceNotFoundException.class);
     gApi.projects().name(project.get()).tags().get();
   }
@@ -127,7 +131,7 @@
   public void listTagsOfNonVisibleBranch() throws Exception {
     grantTagPermissions();
 
-    PushOneCommit push1 = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit push1 = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result r1 = push1.to("refs/heads/master");
     r1.assertOkStatus();
     TagInput tag1 = new TagInput();
@@ -138,7 +142,7 @@
     assertThat(result.revision).isEqualTo(tag1.revision);
 
     pushTo("refs/heads/hidden");
-    PushOneCommit push2 = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit push2 = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result r2 = push2.to("refs/heads/hidden");
     r2.assertOkStatus();
 
@@ -167,7 +171,7 @@
   public void lightweightTag() throws Exception {
     grantTagPermissions();
 
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result r = push.to("refs/heads/master");
     r.assertOkStatus();
 
@@ -188,7 +192,7 @@
     assertThat(result.canDelete).isTrue();
     assertThat(result.created).isEqualTo(timestamp(r));
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     result = tag(input.ref).get();
     assertThat(result.canDelete).isNull();
 
@@ -199,7 +203,7 @@
   public void annotatedTag() throws Exception {
     grantTagPermissions();
 
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result r = push.to("refs/heads/master");
     r.assertOkStatus();
 
@@ -212,8 +216,8 @@
     assertThat(result.ref).isEqualTo(R_TAGS + input.ref);
     assertThat(result.object).isEqualTo(input.revision);
     assertThat(result.message).isEqualTo(input.message);
-    assertThat(result.tagger.name).isEqualTo(admin.fullName);
-    assertThat(result.tagger.email).isEqualTo(admin.email);
+    assertThat(result.tagger.name).isEqualTo(admin.fullName());
+    assertThat(result.tagger.email).isEqualTo(admin.email());
     assertThat(result.created).isEqualTo(result.tagger.date);
 
     eventRecorder.assertRefUpdatedEvents(project.get(), result.ref, null, result.revision);
@@ -227,8 +231,8 @@
     assertThat(result2.ref).isEqualTo(input2.ref);
     assertThat(result2.object).isEqualTo(input2.revision);
     assertThat(result2.message).isEqualTo(input2.message);
-    assertThat(result2.tagger.name).isEqualTo(admin.fullName);
-    assertThat(result2.tagger.email).isEqualTo(admin.email);
+    assertThat(result2.tagger.name).isEqualTo(admin.fullName());
+    assertThat(result2.tagger.email).isEqualTo(admin.email());
     assertThat(result2.created).isEqualTo(result2.tagger.date);
 
     eventRecorder.assertRefUpdatedEvents(project.get(), result2.ref, null, result2.revision);
diff --git a/javatests/com/google/gerrit/acceptance/rest/util/BUILD b/javatests/com/google/gerrit/acceptance/rest/util/BUILD
new file mode 100644
index 0000000..1d3fe65
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/util/BUILD
@@ -0,0 +1,12 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
+java_library(
+    name = "util",
+    testonly = True,
+    srcs = glob(["*.java"]),
+    visibility = ["//visibility:public"],
+    deps = [
+        "//java/com/google/gerrit/acceptance:lib",
+        "//lib/commons:lang",
+    ],
+)
diff --git a/javatests/com/google/gerrit/acceptance/rest/util/RestApiCallHelper.java b/javatests/com/google/gerrit/acceptance/rest/util/RestApiCallHelper.java
new file mode 100644
index 0000000..52e72fe
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/util/RestApiCallHelper.java
@@ -0,0 +1,104 @@
+// 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.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;
+import static org.apache.http.HttpStatus.SC_NOT_FOUND;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import java.util.List;
+import org.junit.Ignore;
+
+/** Helper to execute REST API calls using the HTTP client. */
+@Ignore
+public class RestApiCallHelper {
+  /** @see #execute(RestSession, List, BeforeRestCall, String...) */
+  public static void execute(RestSession restSession, List<RestCall> restCalls, String... args)
+      throws Exception {
+    execute(restSession, restCalls, () -> {}, args);
+  }
+
+  /** @see #execute(RestSession, List, BeforeRestCall, String...) */
+  public static void execute(
+      RestSession restSession,
+      List<RestCall> restCalls,
+      BeforeRestCall beforeRestCall,
+      String... args)
+      throws Exception {
+    for (RestCall restCall : restCalls) {
+      beforeRestCall.run();
+      execute(restSession, restCall, args);
+    }
+  }
+
+  /**
+   * This method sends a request to a given REST endpoint and verifies that an implementation is
+   * found (no '404 Not Found' response) and that the request doesn't fail (no '500 Internal Server
+   * Error' response). It doesn't verify that the REST endpoint works correctly. This is okay since
+   * the purpose of the test is only to verify that the REST endpoint implementations are correctly
+   * bound.
+   */
+  public static void execute(RestSession restSession, RestCall restCall, String... args)
+      throws Exception {
+    String method = restCall.httpMethod().name();
+    String uri = restCall.uri(args);
+
+    RestResponse response;
+    switch (restCall.httpMethod()) {
+      case GET:
+        response = restSession.get(uri);
+        break;
+      case PUT:
+        response = restSession.put(uri);
+        break;
+      case POST:
+        response = restSession.post(uri);
+        break;
+      case DELETE:
+        response = restSession.delete(uri);
+        break;
+      default:
+        assert_().fail(String.format("unsupported method: %s", restCall.httpMethod().name()));
+        throw new IllegalStateException();
+    }
+
+    int status = response.getStatusCode();
+    String body = response.hasContent() ? response.getEntityContent() : "";
+
+    String msg = String.format("%s %s returned %d: %s", method, uri, status, body);
+    if (restCall.expectedResponseCode().isPresent()) {
+      assertWithMessage(msg).that(status).isEqualTo(restCall.expectedResponseCode().get());
+      if (restCall.expectedMessage().isPresent()) {
+        assertWithMessage(msg).that(body).contains(restCall.expectedMessage().get());
+      }
+    } else {
+      assertWithMessage(msg)
+          .that(status)
+          .isNotIn(ImmutableList.of(SC_FORBIDDEN, SC_NOT_FOUND, SC_METHOD_NOT_ALLOWED));
+      assertWithMessage(msg).that(status).isLessThan(SC_INTERNAL_SERVER_ERROR);
+    }
+  }
+
+  @FunctionalInterface
+  public interface BeforeRestCall {
+    void run() throws Exception;
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/util/RestCall.java b/javatests/com/google/gerrit/acceptance/rest/util/RestCall.java
new file mode 100644
index 0000000..7b0002c
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/util/RestCall.java
@@ -0,0 +1,88 @@
+// 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.util;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.auto.value.AutoValue;
+import java.util.Optional;
+import org.apache.commons.lang.StringUtils;
+import org.junit.Ignore;
+
+/** Data container for test REST requests. */
+@Ignore
+@AutoValue
+public abstract class RestCall {
+  public enum Method {
+    GET,
+    PUT,
+    POST,
+    DELETE
+  }
+
+  public static RestCall get(String uriFormat) {
+    return builder(Method.GET, uriFormat).build();
+  }
+
+  public static RestCall put(String uriFormat) {
+    return builder(Method.PUT, uriFormat).build();
+  }
+
+  public static RestCall post(String uriFormat) {
+    return builder(Method.POST, uriFormat).build();
+  }
+
+  public static RestCall delete(String uriFormat) {
+    return builder(Method.DELETE, uriFormat).build();
+  }
+
+  public static Builder builder(Method httpMethod, String uriFormat) {
+    return new AutoValue_RestCall.Builder().httpMethod(httpMethod).uriFormat(uriFormat);
+  }
+
+  public abstract Method httpMethod();
+
+  public abstract String uriFormat();
+
+  public abstract Optional<Integer> expectedResponseCode();
+
+  public abstract Optional<String> expectedMessage();
+
+  public String uri(String... args) {
+    String uriFormat = uriFormat();
+    int expectedArgNum = StringUtils.countMatches(uriFormat, "%s");
+    checkState(
+        args.length == expectedArgNum,
+        "uriFormat %s needs %s arguments, got only %s: %s",
+        uriFormat,
+        expectedArgNum,
+        args.length,
+        args);
+    return String.format(uriFormat, (Object[]) args);
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+    public abstract Builder httpMethod(Method httpMethod);
+
+    public abstract Builder uriFormat(String uriFormat);
+
+    public abstract Builder expectedResponseCode(int expectedResponseCode);
+
+    public abstract Builder expectedMessage(String expectedMessage);
+
+    public abstract RestCall build();
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/server/account/AccountResolverIT.java b/javatests/com/google/gerrit/acceptance/server/account/AccountResolverIT.java
new file mode 100644
index 0000000..fa1b467
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/account/AccountResolverIT.java
@@ -0,0 +1,357 @@
+// 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.account;
+
+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 com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+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.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;
+import com.google.gerrit.server.account.AccountResolver.Result;
+import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.AccountsUpdate;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.notedb.Sequences;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.util.Optional;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+
+public class AccountResolverIT extends AbstractDaemonTest {
+  @ConfigSuite.Default
+  public static Config defaultConfig() {
+    Config cfg = new Config();
+    cfg.setEnum("accounts", null, "visibility", AccountVisibility.SAME_GROUP);
+    return cfg;
+  }
+
+  @Inject @ServerInitiated private Provider<AccountsUpdate> accountsUpdateProvider;
+  @Inject private AccountOperations accountOperations;
+  @Inject private AccountResolver accountResolver;
+  @Inject private Provider<CurrentUser> self;
+  @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private Sequences sequences;
+
+  @Test
+  public void bySelf() throws Exception {
+    assertThat(resolve("Self")).isEmpty();
+    accountOperations.newAccount().fullname("self").create();
+
+    Result result = resolveAsResult("self");
+    assertThat(result.asIdSet()).containsExactly(admin.id());
+    assertThat(result.isSelf()).isTrue();
+    assertThat(result.asUniqueUser()).isSameInstanceAs(self.get());
+
+    result = resolveAsResult("me");
+    assertThat(result.asIdSet()).containsExactly(admin.id());
+    assertThat(result.isSelf()).isTrue();
+    assertThat(result.asUniqueUser()).isSameInstanceAs(self.get());
+
+    requestScopeOperations.setApiUserAnonymous();
+    checkBySelfFails();
+
+    requestScopeOperations.setApiUserInternal();
+    checkBySelfFails();
+  }
+
+  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();
+    }
+  }
+
+  @Test
+  public void bySelfInactive() throws Exception {
+    gApi.accounts().id(user.id().get()).setActive(false);
+
+    requestScopeOperations.setApiUser(user.id());
+    assertThat(gApi.accounts().id("self").getActive()).isFalse();
+
+    Result result = resolveAsResult("self");
+    assertThat(result.asIdSet()).containsExactly(user.id());
+    assertThat(result.isSelf()).isTrue();
+    assertThat(result.asUniqueUser()).isSameInstanceAs(self.get());
+  }
+
+  @Test
+  public void byExactAccountId() throws Exception {
+    Account.Id existingId = accountOperations.newAccount().create();
+    Account.Id idWithExistingIdAsFullname =
+        accountOperations.newAccount().fullname(existingId.toString()).create();
+
+    Account.Id nonexistentId = new Account.Id(sequences.nextAccountId());
+    accountOperations.newAccount().fullname(nonexistentId.toString()).create();
+
+    assertThat(resolve(existingId)).containsExactly(existingId);
+    assertThat(resolve(nonexistentId)).isEmpty();
+
+    assertThat(resolveByNameOrEmail(existingId)).containsExactly(idWithExistingIdAsFullname);
+  }
+
+  @Test
+  public void byParenthesizedAccountId() throws Exception {
+    Account.Id existingId = accountOperations.newAccount().fullname("Test User").create();
+    accountOperations.newAccount().fullname(existingId.toString()).create();
+
+    Account.Id nonexistentId = new Account.Id(sequences.nextAccountId());
+    accountOperations.newAccount().fullname("Any Name (" + nonexistentId + ")").create();
+    accountOperations.newAccount().fullname(nonexistentId.toString()).create();
+
+    String existingInput = "Any Name (" + existingId + ")";
+    assertThat(resolve(existingInput)).containsExactly(existingId);
+    assertThat(resolve("Any Name (" + nonexistentId + ")")).isEmpty();
+
+    assertThat(resolveByNameOrEmail(existingInput)).isEmpty();
+  }
+
+  @Test
+  public void byUsername() throws Exception {
+    String existingUsername = "myusername";
+    Account.Id idWithUsername = accountOperations.newAccount().username(existingUsername).create();
+    Account.Id idWithExistingUsernameAsFullname =
+        accountOperations.newAccount().fullname(existingUsername).create();
+
+    String nonexistentUsername = "anotherusername";
+    Account.Id idWithFullname = accountOperations.newAccount().fullname("anotherusername").create();
+
+    assertThat(resolve(existingUsername)).containsExactly(idWithUsername);
+
+    // Doesn't short-circuit just because the input looks like a valid username.
+    assertThat(ExternalId.isValidUsername(nonexistentUsername)).isTrue();
+    assertThat(resolve(nonexistentUsername)).containsExactly(idWithFullname);
+
+    assertThat(resolveByNameOrEmail(existingUsername))
+        .containsExactly(idWithExistingUsernameAsFullname);
+  }
+
+  @Test
+  public void byNameAndEmail() throws Exception {
+    String email = name("user@example.com");
+    Account.Id idWithEmail = accountOperations.newAccount().preferredEmail(email).create();
+    accountOperations.newAccount().fullname(email).create();
+
+    String input = "First Last <" + email + ">";
+    assertThat(resolve(input)).containsExactly(idWithEmail);
+    assertThat(resolveByNameOrEmail(input)).containsExactly(idWithEmail);
+  }
+
+  @Test
+  public void byNameAndEmailPrefersAccountsWithMatchingFullName() throws Exception {
+    String email = name("user@example.com");
+    Account.Id id1 = accountOperations.newAccount().fullname("Aaa Bbb").create();
+    setPreferredEmailBypassingUniquenessCheck(id1, email);
+    Account.Id id2 = accountOperations.newAccount().fullname("Ccc Ddd").create();
+    setPreferredEmailBypassingUniquenessCheck(id2, email);
+
+    String input = "First Last <" + email + ">";
+    assertThat(resolve(input)).containsExactly(id1, id2);
+    assertThat(resolveByNameOrEmail(input)).containsExactly(id1, id2);
+
+    Account.Id id3 = accountOperations.newAccount().fullname("First Last").create();
+    setPreferredEmailBypassingUniquenessCheck(id3, email);
+    assertThat(resolve(input)).containsExactly(id3);
+    assertThat(resolveByNameOrEmail(input)).containsExactly(id3);
+
+    Account.Id id4 = accountOperations.newAccount().fullname("First Last").create();
+    setPreferredEmailBypassingUniquenessCheck(id4, email);
+    assertThat(resolve(input)).containsExactly(id3, id4);
+    assertThat(resolveByNameOrEmail(input)).containsExactly(id3, id4);
+  }
+
+  @Test
+  public void byEmail() throws Exception {
+    String email = name("user@example.com");
+    Account.Id idWithEmail = accountOperations.newAccount().preferredEmail(email).create();
+    accountOperations.newAccount().fullname(email).create();
+
+    assertThat(resolve(email)).containsExactly(idWithEmail);
+    assertThat(resolveByNameOrEmail(email)).containsExactly(idWithEmail);
+  }
+
+  // Can't test for ByRealm because DefaultRealm with the default (OPENID) auth type doesn't support
+  // email expansion, so anything that would return a non-null value from DefaultRealm#lookup would
+  // just be an email address, handled by other tests. This could be avoided if we inject some sort
+  // of custom test realm instance, but the ugliness is not worth it for this small bit of test
+  // coverage.
+
+  @Test
+  public void byFullName() throws Exception {
+    Account.Id id1 = accountOperations.newAccount().fullname("Somebodys Name").create();
+    accountOperations.newAccount().fullname("A totally different name").create();
+    String input = "Somebodys name";
+    assertThat(resolve(input)).containsExactly(id1);
+    assertThat(resolveByNameOrEmail(input)).containsExactly(id1);
+  }
+
+  @Test
+  public void byDefaultSearch() throws Exception {
+    Account.Id id1 = accountOperations.newAccount().fullname("John Doe").create();
+    Account.Id id2 = accountOperations.newAccount().fullname("Jane Doe").create();
+    assertThat(resolve("doe")).containsExactly(id1, id2);
+    assertThat(resolveByNameOrEmail("doe")).containsExactly(id1, id2);
+  }
+
+  @Test
+  public void onlyExactIdReturnsInactiveAccounts() throws Exception {
+    TestAccount account =
+        accountOperations
+            .account(
+                accountOperations
+                    .newAccount()
+                    .fullname("Inactiveuser Name")
+                    .preferredEmail("inactiveuser@example.com")
+                    .username("inactiveusername")
+                    .create())
+            .get();
+    Account.Id id = account.accountId();
+    String nameEmail = account.fullname().get() + " <" + account.preferredEmail().get() + ">";
+    ImmutableList<String> inputs =
+        ImmutableList.of(
+            account.fullname().get() + " (" + account.accountId() + ")",
+            account.fullname().get(),
+            account.preferredEmail().get(),
+            nameEmail,
+            Splitter.on(' ').splitToList(account.fullname().get()).get(0));
+
+    assertThat(resolve(account.accountId())).containsExactly(id);
+    for (String input : inputs) {
+      assertThat(resolve(input)).named("results for %s (active)", 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)
+          .isEmpty();
+    }
+  }
+
+  @Test
+  public void filterVisibility() throws Exception {
+    Account.Id id1 =
+        accountOperations
+            .newAccount()
+            .fullname("John Doe")
+            .preferredEmail("johndoe@example.com")
+            .create();
+    Account.Id id2 =
+        accountOperations
+            .newAccount()
+            .fullname("Jane Doe")
+            .preferredEmail("janedoe@example.com")
+            .create();
+
+    // Admin can see all accounts. Use a variety of searches, including with/without
+    // callerMayAssumeCandidatesAreVisible.
+    assertThat(resolve(id1)).containsExactly(id1);
+    assertThat(resolve("John Doe")).containsExactly(id1);
+    assertThat(resolve("johndoe@example.com")).containsExactly(id1);
+    assertThat(resolve(id2)).containsExactly(id2);
+    assertThat(resolve("Jane Doe")).containsExactly(id2);
+    assertThat(resolve("janedoe@example.com")).containsExactly(id2);
+    assertThat(resolve("doe")).containsExactly(id1, id2);
+
+    // id2 can't see id1, and vice versa.
+    requestScopeOperations.setApiUser(id1);
+    assertThat(resolve(id1)).containsExactly(id1);
+    assertThat(resolve("John Doe")).containsExactly(id1);
+    assertThat(resolve("johndoe@example.com")).containsExactly(id1);
+    assertThat(resolve(id2)).isEmpty();
+    assertThat(resolve("Jane Doe")).isEmpty();
+    assertThat(resolve("janedoe@example.com")).isEmpty();
+    assertThat(resolve("doe")).containsExactly(id1);
+
+    requestScopeOperations.setApiUser(id2);
+    assertThat(resolve(id1)).isEmpty();
+    assertThat(resolve("John Doe")).isEmpty();
+    assertThat(resolve("johndoe@example.com")).isEmpty();
+    assertThat(resolve(id2)).containsExactly(id2);
+    assertThat(resolve("Jane Doe")).containsExactly(id2);
+    assertThat(resolve("janedoe@example.com")).containsExactly(id2);
+    assertThat(resolve("doe")).containsExactly(id2);
+  }
+
+  private ImmutableSet<Account.Id> resolve(Object input) throws Exception {
+    return resolveAsResult(input).asIdSet();
+  }
+
+  private Result resolveAsResult(Object input) throws Exception {
+    return accountResolver.resolve(input.toString());
+  }
+
+  @SuppressWarnings("deprecation")
+  private ImmutableSet<Account.Id> resolveByNameOrEmail(Object input) throws Exception {
+    return accountResolver.resolveByNameOrEmail(input.toString()).asIdSet();
+  }
+
+  private void setPreferredEmailBypassingUniquenessCheck(Account.Id id, String email)
+      throws Exception {
+    Optional<AccountState> result =
+        accountsUpdateProvider
+            .get()
+            .update("Force set preferred email", id, (s, u) -> u.setPreferredEmail(email));
+    assertThat(result.map(a -> a.getAccount().getPreferredEmail())).hasValue(email);
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/server/account/BUILD b/javatests/com/google/gerrit/acceptance/server/account/BUILD
new file mode 100644
index 0000000..48fac99
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/account/BUILD
@@ -0,0 +1,7 @@
+load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
+
+acceptance_tests(
+    srcs = glob(["*IT.java"]),
+    group = "server_account",
+    labels = ["server"],
+)
diff --git a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
index fb2441b..d5e882d 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -16,7 +16,6 @@
 
 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.PushOneCommit.FILE_NAME;
 import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
 import static java.util.stream.Collectors.groupingBy;
@@ -28,9 +27,9 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.extensions.api.changes.DeleteCommentInput;
 import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -79,20 +78,17 @@
 
 @NoHttpd
 public class CommentsIT extends AbstractDaemonTest {
-
-  @Inject private Provider<ChangesCollection> changes;
-
-  @Inject private Provider<PostReview> postReview;
-
-  @Inject private FakeEmailSender email;
-
   @Inject private ChangeNoteUtil noteUtil;
+  @Inject private FakeEmailSender email;
+  @Inject private Provider<ChangesCollection> changes;
+  @Inject private Provider<PostReview> postReview;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   private final Integer[] lines = {0, 1};
 
   @Before
   public void setUp() {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
   }
 
   @Test
@@ -157,7 +153,7 @@
       String file = "file";
       String contents = "contents " + line;
       PushOneCommit push =
-          pushFactory.create(db, admin.getIdent(), testRepo, "first subject", file, contents);
+          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();
@@ -181,7 +177,7 @@
       String file = "file";
       String contents = "contents " + line;
       PushOneCommit push =
-          pushFactory.create(db, admin.getIdent(), testRepo, "first subject", file, contents);
+          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();
@@ -213,7 +209,7 @@
       String file = "file";
       String contents = "contents " + line;
       PushOneCommit push =
-          pushFactory.create(db, admin.getIdent(), testRepo, "first subject", file, contents);
+          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();
@@ -293,7 +289,7 @@
   public void postCommentsUnreachableData() throws Exception {
     String file = "file";
     PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), testRepo, "first subject", file, "l1\nl2\n");
+        pushFactory.create(admin.newIdent(), testRepo, "first subject", file, "l1\nl2\n");
 
     String dest = "refs/for/master";
     PushOneCommit.Result r1 = push.to(dest);
@@ -304,7 +300,7 @@
     PushOneCommit.Result r2 = amendChange(r1.getChangeId());
     r2.assertOkStatus();
 
-    String draftRefName = RefNames.refsDraftComments(r1.getChange().getId(), admin.getId());
+    String draftRefName = RefNames.refsDraftComments(r1.getChange().getId(), admin.id());
 
     DraftInput draft = newDraft(file, Side.REVISION, 1, "comment");
     addDraft(changeId, "1", draft);
@@ -332,7 +328,7 @@
   public void listComments() throws Exception {
     String file = "file";
     PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), testRepo, "first subject", file, "contents");
+        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();
@@ -446,7 +442,7 @@
       String file = "file";
       String contents = "contents " + line;
       PushOneCommit push =
-          pushFactory.create(db, admin.getIdent(), testRepo, "first subject", file, contents);
+          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();
@@ -489,7 +485,7 @@
 
     PushOneCommit.Result r2 =
         pushFactory
-            .create(db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "content")
+            .create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "content")
             .to("refs/for/master");
     changeId = r2.getChangeId();
     revId = r2.getCommit().getName();
@@ -504,11 +500,10 @@
 
     PushOneCommit.Result r2 =
         pushFactory
-            .create(
-                db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "new content", r1.getChangeId())
+            .create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "new content", r1.getChangeId())
             .to("refs/for/master");
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     addDraft(
         r1.getChangeId(),
         r1.getCommit().getName(),
@@ -518,13 +513,13 @@
         r2.getCommit().getName(),
         newDraft(FILE_NAME, Side.REVISION, 1, "typo: content"));
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     addDraft(
         r2.getChangeId(),
         r2.getCommit().getName(),
         newDraft(FILE_NAME, Side.REVISION, 1, "+1, please fix"));
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     Map<String, List<CommentInfo>> actual = gApi.changes().id(r1.getChangeId()).drafts();
     assertThat(actual.keySet()).containsExactly(FILE_NAME);
     List<CommentInfo> comments = actual.get(FILE_NAME);
@@ -551,8 +546,7 @@
 
     PushOneCommit.Result r2 =
         pushFactory
-            .create(
-                db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "new cntent", r1.getChangeId())
+            .create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "new cntent", r1.getChangeId())
             .to("refs/for/master");
 
     addComment(r1, "nit: trailing whitespace");
@@ -565,14 +559,14 @@
     assertThat(comments).hasSize(2);
 
     CommentInfo c1 = comments.get(0);
-    assertThat(c1.author._accountId).isEqualTo(user.getId().get());
+    assertThat(c1.author._accountId).isEqualTo(user.id().get());
     assertThat(c1.patchSet).isEqualTo(1);
     assertThat(c1.message).isEqualTo("nit: trailing whitespace");
     assertThat(c1.side).isNull();
     assertThat(c1.line).isEqualTo(1);
 
     CommentInfo c2 = comments.get(1);
-    assertThat(c2.author._accountId).isEqualTo(user.getId().get());
+    assertThat(c2.author._accountId).isEqualTo(user.id().get());
     assertThat(c2.patchSet).isEqualTo(2);
     assertThat(c2.message).isEqualTo("typo: content");
     assertThat(c2.side).isNull();
@@ -597,19 +591,18 @@
     String changeId = result.getChangeId();
 
     pushFactory
-        .create(db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "initial content\n", changeId)
+        .create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "initial content\n", changeId)
         .to("refs/heads/master");
 
     PushOneCommit.Result r1 =
         pushFactory
-            .create(db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "old boring content\n")
+            .create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "old boring content\n")
             .to("refs/for/master");
 
     PushOneCommit.Result r2 =
         pushFactory
             .create(
-                db,
-                admin.getIdent(),
+                admin.newIdent(),
                 testRepo,
                 SUBJECT,
                 FILE_NAME,
@@ -649,11 +642,11 @@
         other.getCommit().getName(),
         newDraft(FILE_NAME, Side.REVISION, 1, "unrelated comment"));
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     // Drafts by other users aren't returned.
     addDraft(
         r2.getChangeId(), r2.getCommit().getName(), newDraft(FILE_NAME, Side.REVISION, 2, "oops"));
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
 
     ReviewInput reviewInput = new ReviewInput();
     reviewInput.drafts = DraftHandling.PUBLISH_ALL_REVISIONS;
@@ -803,7 +796,7 @@
   }
 
   @Test
-  public void queryChangesWithUnresolvedCommentCount() throws Exception {
+  public void queryChangesWithCommentCount() throws Exception {
     // PS1 has three comments in three different threads, PS2 has one comment in one thread.
     PushOneCommit.Result result = createChange("change 1", FILE_NAME, "content 1");
     String changeId1 = result.getChangeId();
@@ -832,16 +825,16 @@
     assertThat(comments.get(FILE_NAME)).hasSize(1);
     addComment(result, "comment 2", false, true, comments.get(FILE_NAME).get(0).id);
 
-    AcceptanceTestRequestScope.Context ctx = disableDb();
-    try {
+    try (AutoCloseable ignored = disableNoteDb()) {
       ChangeInfo changeInfo1 = Iterables.getOnlyElement(query(changeId1));
       ChangeInfo changeInfo2 = Iterables.getOnlyElement(query(changeId2));
       ChangeInfo changeInfo3 = Iterables.getOnlyElement(query(changeId3));
       assertThat(changeInfo1.unresolvedCommentCount).isEqualTo(2);
+      assertThat(changeInfo1.totalCommentCount).isEqualTo(4);
       assertThat(changeInfo2.unresolvedCommentCount).isEqualTo(0);
+      assertThat(changeInfo2.totalCommentCount).isEqualTo(2);
       assertThat(changeInfo3.unresolvedCommentCount).isEqualTo(1);
-    } finally {
-      enableDb(ctx);
+      assertThat(changeInfo3.totalCommentCount).isEqualTo(2);
     }
   }
 
@@ -859,7 +852,7 @@
     String uuid = commentsMap.get(targetComment.path).get(0).id;
     DeleteCommentInput input = new DeleteCommentInput("contains confidential information");
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     gApi.changes().id(result.getChangeId()).current().comment(uuid).delete(input);
   }
@@ -930,12 +923,9 @@
     // PS4 has comments [c7, c8].
     assertThat(getRevisionComments(changeId, ps4)).hasSize(2);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     for (int i = 0; i < commentsBeforeDelete.size(); i++) {
-      List<RevCommit> commitsBeforeDelete = new ArrayList<>();
-      if (notesMigration.commitChangeWrites()) {
-        commitsBeforeDelete = getChangeMetaCommitsInReverseOrder(id);
-      }
+      List<RevCommit> commitsBeforeDelete = getChangeMetaCommitsInReverseOrder(id);
 
       CommentInfo comment = commentsBeforeDelete.get(i);
       String uuid = comment.id;
@@ -948,15 +938,13 @@
           gApi.changes().id(changeId).revision(patchSet).comment(uuid).delete(input);
 
       String expectedMsg =
-          String.format("Comment removed by: %s; Reason: %s", admin.fullName, input.reason);
+          String.format("Comment removed by: %s; Reason: %s", admin.fullName(), input.reason);
       assertThat(updatedComment.message).isEqualTo(expectedMsg);
       oldComment.message = expectedMsg;
       assertThat(updatedComment).isEqualTo(oldComment);
 
       // Check the NoteDb state after the deletion.
-      if (notesMigration.commitChangeWrites()) {
-        assertMetaBranchCommitsAfterRewriting(commitsBeforeDelete, id, uuid, expectedMsg);
-      }
+      assertMetaBranchCommitsAfterRewriting(commitsBeforeDelete, id, uuid, expectedMsg);
 
       comment.message = expectedMsg;
       commentsBeforeDelete.set(i, comment);
@@ -1003,12 +991,9 @@
     String uuid = targetComment.get().id;
     CommentInfo oldComment = gApi.changes().id(changeId).revision(ps1).comment(uuid).get();
 
-    List<RevCommit> commitsBeforeDelete = new ArrayList<>();
-    if (notesMigration.commitChangeWrites()) {
-      commitsBeforeDelete = getChangeMetaCommitsInReverseOrder(id);
-    }
+    List<RevCommit> commitsBeforeDelete = getChangeMetaCommitsInReverseOrder(id);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     for (int i = 0; i < 3; i++) {
       DeleteCommentInput input = new DeleteCommentInput("delete comment 2, iteration: " + i);
       gApi.changes().id(changeId).revision(ps1).comment(uuid).delete(input);
@@ -1017,26 +1002,24 @@
     CommentInfo updatedComment = gApi.changes().id(changeId).revision(ps1).comment(uuid).get();
     String expectedMsg =
         String.format(
-            "Comment removed by: %s; Reason: %s", admin.fullName, "delete comment 2, iteration: 2");
+            "Comment removed by: %s; Reason: %s",
+            admin.fullName(), "delete comment 2, iteration: 2");
     assertThat(updatedComment.message).isEqualTo(expectedMsg);
     oldComment.message = expectedMsg;
     assertThat(updatedComment).isEqualTo(oldComment);
 
-    if (notesMigration.commitChangeWrites()) {
-      assertMetaBranchCommitsAfterRewriting(commitsBeforeDelete, id, uuid, expectedMsg);
-    }
+    assertMetaBranchCommitsAfterRewriting(commitsBeforeDelete, id, uuid, expectedMsg);
     assertThat(getChangeSortedComments(id.get())).hasSize(3);
   }
 
   @Test
   public void jsonCommentHasLegacyFormatFalse() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
     PushOneCommit.Result result = createChange();
     Change.Id changeId = result.getChange().getId();
     addComment(result.getChangeId(), "comment");
 
     Collection<com.google.gerrit.reviewdb.client.Comment> comments =
-        notesFactory.createChecked(db, project, changeId).getComments().values();
+        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");
diff --git a/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java b/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
index 2af90a8..7c375bd 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
@@ -15,11 +15,9 @@
 package com.google.gerrit.acceptance.server.change;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.extensions.common.ProblemInfo.Status.FIXED;
 import static com.google.gerrit.extensions.common.ProblemInfo.Status.FIX_FAILED;
 import static com.google.gerrit.testing.TestChanges.newPatchSet;
-import static java.util.Collections.singleton;
 import static java.util.Objects.requireNonNull;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -28,34 +26,28 @@
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.api.changes.FixInput;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
 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.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.Sequences;
 import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.change.ConsistencyChecker;
+import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.change.PatchSetInserter;
-import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.notedb.ChangeNoteUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
+import com.google.gerrit.server.notedb.Sequences;
 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.RepoContext;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gerrit.testing.InMemoryRepositoryManager;
-import com.google.gerrit.testing.NoteDbMode;
 import com.google.gerrit.testing.TestChanges;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -88,8 +80,6 @@
 
   @Inject private ChangeNoteUtil noteUtil;
 
-  @Inject @AnonymousCowardName private String anonymousCowardName;
-
   @Inject private Sequences sequences;
 
   private RevCommit tip;
@@ -97,11 +87,6 @@
   private ConsistencyChecker checker;
   private TestRepository<InMemoryRepository> serverSideTestRepo;
 
-  private void assumeNoteDbDisabled() {
-    assume().that(notesMigration.readChanges()).isFalse();
-    assume().that(NoteDbMode.get()).isNotEqualTo(NoteDbMode.CHECK);
-  }
-
   @Before
   public void setUp() throws Exception {
     serverSideTestRepo =
@@ -110,7 +95,7 @@
         serverSideTestRepo
             .getRevWalk()
             .parseCommit(serverSideTestRepo.getRepository().exactRef("HEAD").getObjectId());
-    adminId = admin.getId();
+    adminId = admin.id();
     checker = checkerProvider.get();
   }
 
@@ -129,41 +114,9 @@
   public void missingOwner() throws Exception {
     TestAccount owner = accountCreator.create("missing");
     ChangeNotes notes = insertChange(owner);
-    deleteUserBranch(owner.getId());
+    deleteUserBranch(owner.id());
 
-    assertProblems(notes, null, problem("Missing change owner: " + owner.getId()));
-  }
-
-  @Test
-  public void missingRepo() throws Exception {
-    // NoteDb can't have a change without a repo.
-    assumeNoteDbDisabled();
-
-    ChangeNotes notes = insertChange();
-    Project.NameKey name = notes.getProjectName();
-    ((InMemoryRepositoryManager) repoManager).deleteRepository(name);
-    assertThat(checker.check(notes, null).problems())
-        .containsExactly(problem("Destination repository not found: " + name));
-  }
-
-  @Test
-  public void invalidRevision() throws Exception {
-    // NoteDb always parses the revision when inserting a patch set, so we can't
-    // create an invalid patch set.
-    assumeNoteDbDisabled();
-
-    ChangeNotes notes = insertChange();
-    PatchSet ps =
-        newPatchSet(
-            notes.getChange().currentPatchSetId(),
-            "fooooooooooooooooooooooooooooooooooooooo",
-            adminId);
-    db.patchSets().update(singleton(ps));
-
-    assertProblems(
-        notes,
-        null,
-        problem("Invalid revision on patch set 1: fooooooooooooooooooooooooooooooooooooooo"));
+    assertProblems(notes, null, problem("Missing change owner: " + owner.id()));
   }
 
   // No test for ref existing but object missing; InMemoryRepository won't let
@@ -201,7 +154,7 @@
   public void patchSetRefMissing() throws Exception {
     ChangeNotes notes = insertChange();
     serverSideTestRepo.update(
-        "refs/other/foo", ObjectId.fromString(psUtil.current(db, notes).getRevision().get()));
+        "refs/other/foo", ObjectId.fromString(psUtil.current(notes).getRevision().get()));
     String refName = notes.getChange().currentPatchSetId().toRefName();
     deleteRef(refName);
 
@@ -211,7 +164,7 @@
   @Test
   public void patchSetRefMissingWithFix() throws Exception {
     ChangeNotes notes = insertChange();
-    String rev = psUtil.current(db, notes).getRevision().get();
+    String rev = psUtil.current(notes).getRevision().get();
     serverSideTestRepo.update("refs/other/foo", ObjectId.fromString(rev));
     String refName = notes.getChange().currentPatchSetId().toRefName();
     deleteRef(refName);
@@ -225,7 +178,7 @@
   @Test
   public void patchSetObjectAndRefMissingWithDeletingPatchSet() throws Exception {
     ChangeNotes notes = insertChange();
-    PatchSet ps1 = psUtil.current(db, notes);
+    PatchSet ps1 = psUtil.current(notes);
 
     String rev2 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
     PatchSet ps2 = insertMissingPatchSet(notes, rev2);
@@ -241,20 +194,20 @@
 
     notes = reload(notes);
     assertThat(notes.getChange().currentPatchSetId().get()).isEqualTo(1);
-    assertThat(psUtil.get(db, notes, ps1.getId())).isNotNull();
-    assertThat(psUtil.get(db, notes, ps2.getId())).isNull();
+    assertThat(psUtil.get(notes, ps1.getId())).isNotNull();
+    assertThat(psUtil.get(notes, ps2.getId())).isNull();
   }
 
   @Test
   public void patchSetMultipleObjectsMissingWithDeletingPatchSets() throws Exception {
     ChangeNotes notes = insertChange();
-    PatchSet ps1 = psUtil.current(db, notes);
+    PatchSet ps1 = psUtil.current(notes);
 
     String rev2 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
     PatchSet ps2 = insertMissingPatchSet(notes, rev2);
 
     notes = incrementPatchSet(reload(notes));
-    PatchSet ps3 = psUtil.current(db, notes);
+    PatchSet ps3 = psUtil.current(notes);
 
     String rev4 = "c0ffeeeec0ffeeeec0ffeeeec0ffeeeec0ffeeee";
     PatchSet ps4 = insertMissingPatchSet(notes, rev4);
@@ -272,27 +225,20 @@
 
     notes = reload(notes);
     assertThat(notes.getChange().currentPatchSetId().get()).isEqualTo(3);
-    assertThat(psUtil.get(db, notes, ps1.getId())).isNotNull();
-    assertThat(psUtil.get(db, notes, ps2.getId())).isNull();
-    assertThat(psUtil.get(db, notes, ps3.getId())).isNotNull();
-    assertThat(psUtil.get(db, notes, ps4.getId())).isNull();
+    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();
   }
 
   @Test
   public void onlyPatchSetObjectMissingWithFix() throws Exception {
-    Change c = TestChanges.newChange(project, admin.getId(), sequences.nextChangeId());
-
-    // Set review started, mimicking Schema_153, so tests pass with NoteDbMode.CHECK.
-    c.setReviewStarted(true);
+    Change c = TestChanges.newChange(project, admin.id(), sequences.nextChangeId());
 
     PatchSet.Id psId = c.currentPatchSetId();
     String rev = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
     PatchSet ps = newPatchSet(psId, rev, adminId);
 
-    if (notesMigration.changePrimaryStorage() == PrimaryStorage.REVIEW_DB) {
-      db.changes().insert(singleton(c));
-      db.patchSets().insert(singleton(ps));
-    }
     addNoteDbCommit(
         c.getId(),
         "Create change\n"
@@ -311,8 +257,8 @@
             + "Groups: "
             + rev
             + "\n");
-    indexer.index(db, c.getProject(), c.getId());
-    ChangeNotes notes = changeNotesFactory.create(db, c.getProject(), c.getId());
+    indexer.index(c.getProject(), c.getId());
+    ChangeNotes notes = changeNotesFactory.create(c.getProject(), c.getId());
 
     FixInput fix = new FixInput();
     fix.deletePatchSetIfCommitMissing = true;
@@ -327,23 +273,13 @@
 
     notes = reload(notes);
     assertThat(notes.getChange().currentPatchSetId().get()).isEqualTo(1);
-    assertThat(psUtil.current(db, notes)).isNotNull();
-  }
-
-  @Test
-  public void currentPatchSetMissing() throws Exception {
-    // NoteDb can't create a change without a patch set.
-    assumeNoteDbDisabled();
-
-    ChangeNotes notes = insertChange();
-    db.patchSets().deleteKeys(singleton(notes.getChange().currentPatchSetId()));
-    assertProblems(notes, null, problem("Current patch set 1 not found"));
+    assertThat(psUtil.current(notes)).isNotNull();
   }
 
   @Test
   public void duplicatePatchSetRevisions() throws Exception {
     ChangeNotes notes = insertChange();
-    PatchSet ps1 = psUtil.current(db, notes);
+    PatchSet ps1 = psUtil.current(notes);
     String rev = ps1.getRevision().get();
 
     notes =
@@ -376,7 +312,7 @@
           notes.getChangeId(),
           new BatchUpdateOp() {
             @Override
-            public boolean updateChange(ChangeContext ctx) throws OrmException {
+            public boolean updateChange(ChangeContext ctx) {
               ctx.getChange().setStatus(Change.Status.MERGED);
               ctx.getUpdate(ctx.getChange().currentPatchSetId()).fixStatus(Change.Status.MERGED);
               return true;
@@ -386,7 +322,7 @@
     }
     notes = reload(notes);
 
-    String rev = psUtil.current(db, notes).getRevision().get();
+    String rev = psUtil.current(notes).getRevision().get();
     ObjectId tip = getDestRef(notes);
     assertProblems(
         notes,
@@ -403,7 +339,7 @@
   @Test
   public void newChangeIsMerged() throws Exception {
     ChangeNotes notes = insertChange();
-    String rev = psUtil.current(db, notes).getRevision().get();
+    String rev = psUtil.current(notes).getRevision().get();
     serverSideTestRepo
         .branch(notes.getChange().getDest().get())
         .update(serverSideTestRepo.getRevWalk().parseCommit(ObjectId.fromString(rev)));
@@ -423,7 +359,7 @@
   @Test
   public void newChangeIsMergedWithFix() throws Exception {
     ChangeNotes notes = insertChange();
-    String rev = psUtil.current(db, notes).getRevision().get();
+    String rev = psUtil.current(notes).getRevision().get();
     serverSideTestRepo
         .branch(notes.getChange().getDest().get())
         .update(serverSideTestRepo.getRevWalk().parseCommit(ObjectId.fromString(rev)));
@@ -442,14 +378,14 @@
             "Marked change as merged"));
 
     notes = reload(notes);
-    assertThat(notes.getChange().getStatus()).isEqualTo(Change.Status.MERGED);
+    assertThat(notes.getChange().isMerged()).isTrue();
     assertNoProblems(notes, null);
   }
 
   @Test
   public void extensionApiReturnsUpdatedValueAfterFix() throws Exception {
     ChangeNotes notes = insertChange();
-    String rev = psUtil.current(db, notes).getRevision().get();
+    String rev = psUtil.current(notes).getRevision().get();
     serverSideTestRepo
         .branch(notes.getChange().getDest().get())
         .update(serverSideTestRepo.getRevWalk().parseCommit(ObjectId.fromString(rev)));
@@ -464,7 +400,7 @@
   @Test
   public void expectedMergedCommitIsLatestPatchSet() throws Exception {
     ChangeNotes notes = insertChange();
-    String rev = psUtil.current(db, notes).getRevision().get();
+    String rev = psUtil.current(notes).getRevision().get();
     serverSideTestRepo
         .branch(notes.getChange().getDest().get())
         .update(serverSideTestRepo.getRevWalk().parseCommit(ObjectId.fromString(rev)));
@@ -485,14 +421,14 @@
             "Marked change as merged"));
 
     notes = reload(notes);
-    assertThat(notes.getChange().getStatus()).isEqualTo(Change.Status.MERGED);
+    assertThat(notes.getChange().isMerged()).isTrue();
     assertNoProblems(notes, null);
   }
 
   @Test
   public void expectedMergedCommitNotMergedIntoDestination() throws Exception {
     ChangeNotes notes = insertChange();
-    String rev = psUtil.current(db, notes).getRevision().get();
+    String rev = psUtil.current(notes).getRevision().get();
     RevCommit commit = serverSideTestRepo.getRevWalk().parseCommit(ObjectId.fromString(rev));
     serverSideTestRepo.branch(notes.getChange().getDest().get()).update(commit);
 
@@ -515,7 +451,7 @@
   public void createNewPatchSetForExpectedMergeCommitWithNoChangeId() throws Exception {
     ChangeNotes notes = insertChange();
     String dest = notes.getChange().getDest().get();
-    String rev = psUtil.current(db, notes).getRevision().get();
+    String rev = psUtil.current(notes).getRevision().get();
     RevCommit commit = serverSideTestRepo.getRevWalk().parseCommit(ObjectId.fromString(rev));
 
     RevCommit mergedAs =
@@ -547,7 +483,7 @@
     notes = reload(notes);
     PatchSet.Id psId2 = new PatchSet.Id(notes.getChangeId(), 2);
     assertThat(notes.getChange().currentPatchSetId()).isEqualTo(psId2);
-    assertThat(psUtil.get(db, notes, psId2).getRevision().get()).isEqualTo(mergedAs.name());
+    assertThat(psUtil.get(notes, psId2).getRevision().get()).isEqualTo(mergedAs.name());
 
     assertNoProblems(notes, null);
   }
@@ -556,7 +492,7 @@
   public void createNewPatchSetForExpectedMergeCommitWithChangeId() throws Exception {
     ChangeNotes notes = insertChange();
     String dest = notes.getChange().getDest().get();
-    String rev = psUtil.current(db, notes).getRevision().get();
+    String rev = psUtil.current(notes).getRevision().get();
     RevCommit commit = serverSideTestRepo.getRevWalk().parseCommit(ObjectId.fromString(rev));
 
     RevCommit mergedAs =
@@ -595,7 +531,7 @@
     notes = reload(notes);
     PatchSet.Id psId2 = new PatchSet.Id(notes.getChangeId(), 2);
     assertThat(notes.getChange().currentPatchSetId()).isEqualTo(psId2);
-    assertThat(psUtil.get(db, notes, psId2).getRevision().get()).isEqualTo(mergedAs.name());
+    assertThat(psUtil.get(notes, psId2).getRevision().get()).isEqualTo(mergedAs.name());
 
     assertNoProblems(notes, null);
   }
@@ -603,10 +539,10 @@
   @Test
   public void expectedMergedCommitIsOldPatchSetOfSameChange() throws Exception {
     ChangeNotes notes = insertChange();
-    PatchSet ps1 = psUtil.current(db, notes);
+    PatchSet ps1 = psUtil.current(notes);
     String rev1 = ps1.getRevision().get();
     notes = incrementPatchSet(notes);
-    PatchSet ps2 = psUtil.current(db, notes);
+    PatchSet ps2 = psUtil.current(notes);
     serverSideTestRepo
         .branch(notes.getChange().getDest().get())
         .update(serverSideTestRepo.getRevWalk().parseCommit(ObjectId.fromString(rev1)));
@@ -635,15 +571,15 @@
     notes = reload(notes);
     PatchSet.Id psId3 = new PatchSet.Id(notes.getChangeId(), 3);
     assertThat(notes.getChange().currentPatchSetId()).isEqualTo(psId3);
-    assertThat(notes.getChange().getStatus()).isEqualTo(Change.Status.MERGED);
-    assertThat(psUtil.byChangeAsMap(db, notes).keySet()).containsExactly(ps2.getId(), psId3);
-    assertThat(psUtil.get(db, notes, psId3).getRevision().get()).isEqualTo(rev1);
+    assertThat(notes.getChange().isMerged()).isTrue();
+    assertThat(psUtil.byChangeAsMap(notes).keySet()).containsExactly(ps2.getId(), psId3);
+    assertThat(psUtil.get(notes, psId3).getRevision().get()).isEqualTo(rev1);
   }
 
   @Test
   public void expectedMergedCommitIsDanglingPatchSetOlderThanCurrent() throws Exception {
     ChangeNotes notes = insertChange();
-    PatchSet ps1 = psUtil.current(db, notes);
+    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);
@@ -652,7 +588,7 @@
     serverSideTestRepo.branch(psId2.toRefName()).update(commit2);
 
     notes = incrementPatchSet(notes);
-    PatchSet ps3 = psUtil.current(db, notes);
+    PatchSet ps3 = psUtil.current(notes);
     assertThat(ps3.getId().get()).isEqualTo(3);
 
     serverSideTestRepo
@@ -683,16 +619,16 @@
     notes = reload(notes);
     PatchSet.Id psId4 = new PatchSet.Id(notes.getChangeId(), 4);
     assertThat(notes.getChange().currentPatchSetId()).isEqualTo(psId4);
-    assertThat(notes.getChange().getStatus()).isEqualTo(Change.Status.MERGED);
-    assertThat(psUtil.byChangeAsMap(db, notes).keySet())
+    assertThat(notes.getChange().isMerged()).isTrue();
+    assertThat(psUtil.byChangeAsMap(notes).keySet())
         .containsExactly(ps1.getId(), ps3.getId(), psId4);
-    assertThat(psUtil.get(db, notes, psId4).getRevision().get()).isEqualTo(rev2);
+    assertThat(psUtil.get(notes, psId4).getRevision().get()).isEqualTo(rev2);
   }
 
   @Test
   public void expectedMergedCommitIsDanglingPatchSetNewerThanCurrent() throws Exception {
     ChangeNotes notes = insertChange();
-    PatchSet ps1 = psUtil.current(db, notes);
+    PatchSet ps1 = psUtil.current(notes);
 
     // Create dangling ref with no patch set.
     PatchSet.Id psId2 = new PatchSet.Id(notes.getChangeId(), 2);
@@ -720,9 +656,9 @@
 
     notes = reload(notes);
     assertThat(notes.getChange().currentPatchSetId()).isEqualTo(psId2);
-    assertThat(notes.getChange().getStatus()).isEqualTo(Change.Status.MERGED);
-    assertThat(psUtil.byChangeAsMap(db, notes).keySet()).containsExactly(ps1.getId(), psId2);
-    assertThat(psUtil.get(db, notes, psId2).getRevision().get()).isEqualTo(rev2);
+    assertThat(notes.getChange().isMerged()).isTrue();
+    assertThat(psUtil.byChangeAsMap(notes).keySet()).containsExactly(ps1.getId(), psId2);
+    assertThat(psUtil.get(notes, psId2).getRevision().get()).isEqualTo(rev2);
   }
 
   @Test
@@ -730,7 +666,7 @@
     ChangeNotes notes = insertChange();
     String dest = notes.getChange().getDest().get();
     RevCommit parent = serverSideTestRepo.branch(dest).commit().message("parent").create();
-    String rev = psUtil.current(db, notes).getRevision().get();
+    String rev = psUtil.current(notes).getRevision().get();
     RevCommit commit = serverSideTestRepo.getRevWalk().parseCommit(ObjectId.fromString(rev));
     serverSideTestRepo.branch(dest).update(commit);
 
@@ -764,19 +700,19 @@
   @Test
   public void expectedMergedCommitMatchesMultiplePatchSets() throws Exception {
     ChangeNotes notes1 = insertChange();
-    PatchSet.Id psId1 = psUtil.current(db, notes1).getId();
+    PatchSet.Id psId1 = psUtil.current(notes1).getId();
     String dest = notes1.getChange().getDest().get();
-    String rev = psUtil.current(db, notes1).getRevision().get();
+    String rev = psUtil.current(notes1).getRevision().get();
     RevCommit commit = serverSideTestRepo.getRevWalk().parseCommit(ObjectId.fromString(rev));
     serverSideTestRepo.branch(dest).update(commit);
 
     ChangeNotes notes2 = insertChange();
     notes2 = incrementPatchSet(notes2, commit);
-    PatchSet.Id psId2 = psUtil.current(db, notes2).getId();
+    PatchSet.Id psId2 = psUtil.current(notes2).getId();
 
     ChangeNotes notes3 = insertChange();
     notes3 = incrementPatchSet(notes3, commit);
-    PatchSet.Id psId3 = psUtil.current(db, notes3).getId();
+    PatchSet.Id psId3 = psUtil.current(notes3).getId();
 
     FixInput fix = new FixInput();
     fix.expectMergedAs = commit.name();
@@ -796,7 +732,7 @@
   }
 
   private BatchUpdate newUpdate(Account.Id owner) {
-    return batchUpdateFactory.create(db, project, userFactory.create(owner), TimeUtil.nowTs());
+    return batchUpdateFactory.create(project, userFactory.create(owner), TimeUtil.nowTs());
   }
 
   private ChangeNotes insertChange() throws Exception {
@@ -810,18 +746,18 @@
   private ChangeNotes insertChange(TestAccount owner, String dest) throws Exception {
     Change.Id id = new Change.Id(sequences.nextChangeId());
     ChangeInserter ins;
-    try (BatchUpdate bu = newUpdate(owner.getId())) {
+    try (BatchUpdate bu = newUpdate(owner.id())) {
       RevCommit commit = patchSetCommit(new PatchSet.Id(id, 1));
+      bu.setNotify(NotifyResolver.Result.none());
       ins =
           changeInserterFactory
               .create(id, commit, dest)
               .setValidate(false)
-              .setNotify(NotifyHandling.NONE)
               .setFireRevisionCreated(false)
               .setSendMail(false);
       bu.insertChange(ins).execute();
     }
-    return changeNotesFactory.create(db, project, ins.getChange().getId());
+    return changeNotesFactory.create(project, ins.getChange().getId());
   }
 
   private PatchSet.Id nextPatchSetId(ChangeNotes notes) throws Exception {
@@ -836,19 +772,19 @@
   private ChangeNotes incrementPatchSet(ChangeNotes notes, RevCommit commit) throws Exception {
     PatchSetInserter ins;
     try (BatchUpdate bu = newUpdate(notes.getChange().getOwner())) {
+      bu.setNotify(NotifyResolver.Result.none());
       ins =
           patchSetInserterFactory
               .create(notes, nextPatchSetId(notes), commit)
               .setValidate(false)
-              .setFireRevisionCreated(false)
-              .setNotify(NotifyHandling.NONE);
+              .setFireRevisionCreated(false);
       bu.addOp(notes.getChangeId(), ins).execute();
     }
     return reload(notes);
   }
 
   private ChangeNotes reload(ChangeNotes notes) throws Exception {
-    return changeNotesFactory.create(db, notes.getChange().getProject(), notes.getChangeId());
+    return changeNotesFactory.create(notes.getChange().getProject(), notes.getChangeId());
   }
 
   private RevCommit patchSetCommit(PatchSet.Id psId) throws Exception {
@@ -865,11 +801,6 @@
     c.setCurrentPatchSet(psId, subject, c.getOriginalSubject());
     PatchSet ps = newPatchSet(psId, rev, adminId);
 
-    if (PrimaryStorage.of(c) == PrimaryStorage.REVIEW_DB) {
-      db.patchSets().insert(singleton(ps));
-      db.changes().update(singleton(c));
-    }
-
     addNoteDbCommit(
         c.getId(),
         "Update patch set "
@@ -885,7 +816,7 @@
             + "Subject: "
             + subject
             + "\n");
-    indexer.index(db, c.getProject(), c.getId());
+    indexer.index(c.getProject(), c.getId());
 
     return ps;
   }
@@ -897,12 +828,8 @@
   }
 
   private void addNoteDbCommit(Change.Id id, String commitMessage) throws Exception {
-    if (!notesMigration.commitChangeWrites()) {
-      return;
-    }
     PersonIdent committer = serverIdent.get();
-    PersonIdent author =
-        noteUtil.newIdent(getAccount(admin.getId()), committer.getWhen(), committer);
+    PersonIdent author = noteUtil.newIdent(getAccount(admin.id()), committer.getWhen(), committer);
     serverSideTestRepo
         .branch(RefNames.changeMetaRef(id))
         .commit()
@@ -921,7 +848,7 @@
 
   private ChangeNotes mergeChange(ChangeNotes notes) throws Exception {
     final ObjectId oldId = getDestRef(notes);
-    final ObjectId newId = ObjectId.fromString(psUtil.current(db, notes).getRevision().get());
+    final ObjectId newId = ObjectId.fromString(psUtil.current(notes).getRevision().get());
     final String dest = notes.getChange().getDest().get();
 
     try (BatchUpdate bu = newUpdate(adminId)) {
@@ -934,7 +861,7 @@
             }
 
             @Override
-            public boolean updateChange(ChangeContext ctx) throws OrmException {
+            public boolean updateChange(ChangeContext ctx) {
               ctx.getChange().setStatus(Change.Status.MERGED);
               ctx.getUpdate(ctx.getChange().currentPatchSetId()).fixStatus(Change.Status.MERGED);
               return true;
diff --git a/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java b/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
index dd47c01..b9b7ab3 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
@@ -22,17 +22,28 @@
 
 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.testsuite.account.AccountOperations;
+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.GlobalCapability;
+import com.google.gerrit.common.data.PermissionRule;
 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;
@@ -42,10 +53,12 @@
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.TestTimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 import java.util.Optional;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
@@ -66,6 +79,10 @@
     return cfg;
   }
 
+  @Inject private AccountOperations accountOperations;
+  @Inject private GroupOperations groupOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   private String systemTimeZone;
 
   @Before
@@ -85,7 +102,7 @@
 
   @Test
   public void getRelatedNoResult() throws Exception {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     assertRelated(push.to("refs/for/master").getPatchSetId());
   }
 
@@ -516,7 +533,7 @@
 
     // Pretend PS1,1 was pushed before the groups field was added.
     clearGroups(psId1_1);
-    indexer.index(changeDataFactory.create(db, project, psId1_1.getParentKey()));
+    indexer.index(changeDataFactory.create(project, psId1_1.getParentKey()));
 
     // PS1,1 has no groups, so disappeared from related changes.
     assertRelated(psId2_1);
@@ -558,7 +575,6 @@
 
   @Test
   public void getRelatedManyGroups() throws Exception {
-    List<RevCommit> commits = new ArrayList<>();
     RevCommit last = null;
     int n = 2 * MAX_TERMS;
     assertThat(n).isGreaterThan(indexConfig.maxTerms());
@@ -567,22 +583,82 @@
       last = cb.add("a.txt", Integer.toString(i)).message("subject: " + i).create();
       testRepo.reset(last);
       assertPushOk(pushHead(testRepo, "refs/for/master", false), "refs/for/master");
-      commits.add(last);
     }
 
     ChangeData cd = getChange(last);
     assertThat(cd.patchSets()).hasSize(n);
-    assertThat(GetRelated.getAllGroups(cd.notes(), db, psUtil)).hasSize(n);
+    assertThat(GetRelated.getAllGroups(cd.notes(), psUtil)).hasSize(n);
 
     assertRelated(cd.change().currentPatchSetId());
   }
 
-  private List<RelatedChangeAndCommitInfo> getRelated(PatchSet.Id ps) throws Exception {
-    return getRelated(ps.getParentKey(), ps.get());
+  @Test
+  public void getRelatedManyChanges() throws Exception {
+    List<ObjectId> commitIds = new ArrayList<>();
+    for (int i = 1; i <= 5; i++) {
+      commitIds.add(commitBuilder().add(i + ".txt", "i").message("subject: " + i).create().copy());
+    }
+    pushHead(testRepo, "refs/for/master", false);
+
+    List<RelatedChangeAndCommitInfo> expected = new ArrayList<>(commitIds.size());
+    for (ObjectId commitId : commitIds) {
+      expected.add(changeAndCommit(getPatchSetId(commitId), commitId, 1));
+    }
+    Collections.reverse(expected);
+
+    PatchSet.Id lastPsId = getPatchSetId(Iterables.getLast(commitIds));
+    assertRelated(lastPsId, expected);
+
+    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();
+    }
+    requestScopeOperations.setApiUser(accountId);
+
+    assertRelated(lastPsId, expected);
   }
 
-  private List<RelatedChangeAndCommitInfo> getRelated(Change.Id changeId, int ps) throws Exception {
-    return gApi.changes().id(changeId.get()).revision(ps).related().changes;
+  @Test
+  public void stateOfRelatedChangesMatchesDocumentedValues() throws Exception {
+    // Set up three related changes, one new, the other abandoned, and the third merged.
+    RevCommit commit1 =
+        commitBuilder().add("a.txt", "File content 1").message("Subject 1").create();
+    RevCommit commit2 =
+        commitBuilder().add("b.txt", "File content 2").message("Subject 2").create();
+    RevCommit commit3 =
+        commitBuilder().add("c.txt", "File content 3").message("Subject 3").create();
+    pushHead(testRepo, "refs/for/master", false);
+    Change change1 = getChange(commit1).change();
+    Change change2 = getChange(commit2).change();
+    Change change3 = getChange(commit3).change();
+    gApi.changes().id(change1.getChangeId()).current().review(ReviewInput.approve());
+    gApi.changes().id(change1.getChangeId()).current().submit();
+    gApi.changes().id(change2.getChangeId()).abandon();
+
+    List<RelatedChangeAndCommitInfo> relatedChanges =
+        gApi.changes().id(change3.getChangeId()).current().related().changes;
+
+    // Ensure that our REST API returns the states exactly as documented (and required by the
+    // frontend).
+    assertThat(relatedChanges)
+        .comparingElementsUsing(getRelatedChangeToStatusCorrespondence())
+        .containsExactly("NEW", "ABANDONED", "MERGED");
+  }
+
+  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);
+          }
+        },
+        "has status");
   }
 
   private RevCommit parseBody(RevCommit c) throws Exception {
@@ -612,15 +688,14 @@
   }
 
   private void clearGroups(PatchSet.Id psId) throws Exception {
-    try (BatchUpdate bu = batchUpdateFactory.create(db, project, user(user), TimeUtil.nowTs())) {
+    try (BatchUpdate bu = batchUpdateFactory.create(project, user(user), TimeUtil.nowTs())) {
       bu.addOp(
           psId.getParentKey(),
           new BatchUpdateOp() {
             @Override
-            public boolean updateChange(ChangeContext ctx) throws OrmException {
-              PatchSet ps = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
-              psUtil.setGroups(ctx.getDb(), ctx.getUpdate(psId), ps, ImmutableList.<String>of());
-              ctx.dontBumpLastUpdatedOn();
+            public boolean updateChange(ChangeContext ctx) {
+              PatchSet ps = psUtil.get(ctx.getNotes(), psId);
+              psUtil.setGroups(ctx.getUpdate(psId), ps, ImmutableList.of());
               return true;
             }
           });
@@ -630,12 +705,18 @@
 
   private void assertRelated(PatchSet.Id psId, RelatedChangeAndCommitInfo... expected)
       throws Exception {
-    List<RelatedChangeAndCommitInfo> actual = getRelated(psId);
-    assertThat(actual).named("related to " + psId).hasSize(expected.length);
+    assertRelated(psId, Arrays.asList(expected));
+  }
+
+  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());
     for (int i = 0; i < actual.size(); i++) {
       String name = "index " + i + " related to " + psId;
       RelatedChangeAndCommitInfo a = actual.get(i);
-      RelatedChangeAndCommitInfo e = expected[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);
       // Don't bother checking changeId; assume _changeNumber is sufficient.
@@ -644,6 +725,7 @@
       assertThat(a._currentRevisionNumber)
           .named("current revision of " + name)
           .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 424dbac..42fdae4 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java
@@ -275,7 +275,7 @@
     PatchListCacheImpl.LargeObjectTombstone tombstone =
         new PatchListCacheImpl.LargeObjectTombstone();
     abstractPatchListCache.put(key, tombstone);
-    assertThat(abstractPatchListCache.getIfPresent(key)).isSameAs(tombstone);
+    assertThat(abstractPatchListCache.getIfPresent(key)).isSameInstanceAs(tombstone);
   }
 
   private static void assertAdded(String expectedNewName, PatchListEntry e) {
diff --git a/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java b/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
index 304a1e4..389859c 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
@@ -21,6 +21,8 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GitUtil;
 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.extensions.api.changes.SubmittedTogetherInfo;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.ListChangesOption;
@@ -29,6 +31,7 @@
 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;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
@@ -41,6 +44,9 @@
     return submitWholeTopicEnabledConfig();
   }
 
+  @Inject private ProjectOperations projectOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @Test
   public void doesNotIncludeCurrentFiles() throws Exception {
     RevCommit c1_1 = commitBuilder().add("a.txt", "1").message("subject: 1").create();
@@ -100,7 +106,7 @@
     RevCommit b = commitBuilder().add("b", "1").message("change 2").create();
     pushHead(testRepo, "refs/for/master", false);
 
-    setApiUserAnonymous();
+    requestScopeOperations.setApiUserAnonymous();
     assertSubmittedTogether(getChangeId(a));
     assertSubmittedTogether(getChangeId(b), getChangeId(b), getChangeId(a));
   }
@@ -139,7 +145,7 @@
     pushHead(testRepo, "refs/for/master/" + name("topic"), false);
     String id2 = getChangeId(b);
 
-    setApiUserAnonymous();
+    requestScopeOperations.setApiUserAnonymous();
     if (isSubmitWholeTopicEnabled()) {
       assertSubmittedTogether(id1, id2, id1);
       assertSubmittedTogether(id2, id2, id1);
@@ -226,7 +232,8 @@
 
   @Test
   public void newBranchTwoChangesTogether() throws Exception {
-    Project.NameKey p1 = createProject("a-new-project", null, false);
+    Project.NameKey p1 = projectOperations.newProject().noEmptyCommit().create();
+
     TestRepository<?> repo1 = cloneProject(p1);
 
     RevCommit c1 =
diff --git a/javatests/com/google/gerrit/acceptance/server/event/CommentAddedEventIT.java b/javatests/com/google/gerrit/acceptance/server/event/CommentAddedEventIT.java
index f4a833f..4f98dd0 100644
--- a/javatests/com/google/gerrit/acceptance/server/event/CommentAddedEventIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/event/CommentAddedEventIT.java
@@ -74,15 +74,7 @@
       u.save();
     }
 
-    eventListenerRegistration =
-        source.add(
-            "gerrit",
-            new CommentAddedListener() {
-              @Override
-              public void onCommentAdded(Event event) {
-                lastCommentAddedEvent = event;
-              }
-            });
+    eventListenerRegistration = source.add("gerrit", event -> lastCommentAddedEvent = event);
   }
 
   @After
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/AbstractMailIT.java b/javatests/com/google/gerrit/acceptance/server/mail/AbstractMailIT.java
index c1b5cf4..768c269 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/AbstractMailIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/AbstractMailIT.java
@@ -18,23 +18,26 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
 import com.google.gerrit.extensions.client.Comment;
 import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.mail.MailMessage;
+import com.google.inject.Inject;
 import java.time.Instant;
 import java.util.HashMap;
 import org.junit.Ignore;
 
 @Ignore
 public class AbstractMailIT extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   protected MailMessage.Builder messageBuilderWithDefaultFields() {
     MailMessage.Builder b = MailMessage.builder();
     b.id("some id");
-    b.from(user.emailAddress);
-    b.addTo(user.emailAddress); // Not evaluated
+    b.from(user.getEmailAddress());
+    b.addTo(user.getEmailAddress()); // Not evaluated
     b.subject("");
     b.dateReceived(Instant.now());
     return b;
@@ -49,12 +52,12 @@
     String file = "gerrit-server/test.txt";
     String contents = "contents \nlorem \nipsum \nlorem";
     PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), testRepo, "first subject", file, contents);
+        pushFactory.create(admin.newIdent(), testRepo, "first subject", file, contents);
     PushOneCommit.Result r = push.to("refs/for/master");
     String changeId = r.getChangeId();
 
     // Review it
-    setApiUser(reviewer);
+    requestScopeOperations.setApiUser(reviewer.id());
     ReviewInput input = new ReviewInput();
     input.message = "I have two comments";
     input.comments = new HashMap<>();
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
index 209d0a2..85b6955 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.acceptance.server.mail;
 
-import static com.google.common.truth.TruthJUnit.assume;
 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;
@@ -32,6 +31,7 @@
 import com.google.common.truth.Truth;
 import com.google.gerrit.acceptance.AbstractNotificationTest;
 import com.google.gerrit.acceptance.TestAccount;
+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.extensions.api.changes.AbandonInput;
@@ -47,16 +47,22 @@
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.ReviewerState;
+import com.google.gerrit.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;
+import org.eclipse.jgit.lib.Repository;
 import org.junit.Before;
 import org.junit.Test;
 
 public class ChangeNotificationsIT extends AbstractNotificationTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   /*
    * Set up for extra standard test accounts and permissions.
    */
@@ -74,11 +80,14 @@
 
   @Before
   public void grantPermissions() throws Exception {
-    grant(project, "refs/*", Permission.FORGE_COMMITTER, false, REGISTERED_USERS);
-    grant(project, "refs/*", Permission.SUBMIT, false, REGISTERED_USERS);
-    grant(project, "refs/heads/master", Permission.ABANDON, false, REGISTERED_USERS);
-    ProjectConfig cfg = projectCache.get(project).getConfig();
-    Util.allow(cfg, Permission.forLabel("Code-Review"), -2, +2, REGISTERED_USERS, "refs/*");
+    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();
+    }
   }
 
   /*
@@ -96,6 +105,7 @@
         .bcc(sc.starrer)
         .bcc(ABANDONED_CHANGES)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -110,6 +120,7 @@
         .bcc(sc.starrer)
         .bcc(ABANDONED_CHANGES)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -125,6 +136,7 @@
         .bcc(sc.starrer)
         .bcc(ABANDONED_CHANGES)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -140,6 +152,7 @@
         .bcc(sc.starrer)
         .bcc(ABANDONED_CHANGES)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -151,13 +164,14 @@
         .cc(sc.reviewer, sc.ccer)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void abandonReviewableChangeNotifyOwner() throws Exception {
     StagedChange sc = stageReviewableChange();
     abandon(sc.changeId, sc.owner, OWNER);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -167,7 +181,7 @@
     // Self-CC applies *after* need for sending notification is determined.
     // Since there are no recipients before including the user taking action,
     // there should no notification sent.
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -176,20 +190,21 @@
     TestAccount other = accountCreator.create("other", "other@example.com", "other");
     abandon(sc.changeId, other, CC_ON_OWN_COMMENTS, OWNER);
     assertThat(sender).sent("abandon", sc).to(sc.owner).cc(other).noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void abandonReviewableChangeNotifyNone() throws Exception {
     StagedChange sc = stageReviewableChange();
     abandon(sc.changeId, sc.owner, NONE);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void abandonReviewableChangeNotifyNoneCcingSelf() throws Exception {
     StagedChange sc = stageReviewableChange();
     abandon(sc.changeId, sc.owner, CC_ON_OWN_COMMENTS, NONE);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -203,13 +218,14 @@
         .bcc(sc.starrer)
         .bcc(ABANDONED_CHANGES)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void abandonWipChange() throws Exception {
     StagedChange sc = stageWipChange();
     abandon(sc.changeId, sc.owner);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -223,6 +239,7 @@
         .bcc(sc.starrer)
         .bcc(ABANDONED_CHANGES)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   private void abandon(String changeId, TestAccount by) throws Exception {
@@ -243,7 +260,7 @@
       String changeId, TestAccount by, EmailStrategy emailStrategy, @Nullable NotifyHandling notify)
       throws Exception {
     setEmailStrategy(by, emailStrategy);
-    setApiUser(by);
+    requestScopeOperations.setApiUser(by.id());
     AbandonInput in = new AbandonInput();
     if (notify != null) {
       in.notify = notify;
@@ -255,34 +272,10 @@
    * AddReviewerSender tests.
    */
 
-  private void addReviewerToReviewableChangeInReviewDb(Adder adder) throws Exception {
-    assume().that(notesMigration.readChanges()).isFalse();
+  private void addReviewerToReviewableChange(Adder adder) throws Exception {
     StagedChange sc = stageReviewableChange();
     TestAccount reviewer = accountCreator.create("added", "added@example.com", "added");
-    addReviewer(adder, sc.changeId, sc.owner, reviewer.email);
-    assertThat(sender)
-        .sent("newchange", sc)
-        .to(reviewer)
-        .cc(sc.reviewer, sc.ccer)
-        .cc(sc.reviewerByEmail, sc.ccerByEmail)
-        .noOneElse();
-  }
-
-  @Test
-  public void addReviewerToReviewableChangeInReviewDbSingly() throws Exception {
-    addReviewerToReviewableChangeInReviewDb(singly());
-  }
-
-  @Test
-  public void addReviewerToReviewableChangeInReviewDbBatch() throws Exception {
-    addReviewerToReviewableChangeInReviewDb(batch());
-  }
-
-  private void addReviewerToReviewableChangeInNoteDb(Adder adder) throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-    StagedChange sc = stageReviewableChange();
-    TestAccount reviewer = accountCreator.create("added", "added@example.com", "added");
-    addReviewer(adder, sc.changeId, sc.owner, reviewer.email);
+    addReviewer(adder, sc.changeId, sc.owner, reviewer.email());
     // TODO(logan): Should CCs be included?
     assertThat(sender)
         .sent("newchange", sc)
@@ -290,23 +283,23 @@
         .cc(sc.reviewer)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void addReviewerToReviewableChangeInNoteDbSingly() throws Exception {
-    addReviewerToReviewableChangeInNoteDb(singly());
+  public void addReviewerToReviewableChangeSingly() throws Exception {
+    addReviewerToReviewableChange(singly());
   }
 
   @Test
-  public void addReviewerToReviewableChangeInNoteDbBatch() throws Exception {
-    addReviewerToReviewableChangeInNoteDb(batch());
+  public void addReviewerToReviewableChangeBatch() throws Exception {
+    addReviewerToReviewableChange(batch());
   }
 
-  private void addReviewerToReviewableChangeByOwnerCcingSelfInNoteDb(Adder adder) throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
+  private void addReviewerToReviewableChangeByOwnerCcingSelf(Adder adder) throws Exception {
     StagedChange sc = stageReviewableChange();
     TestAccount reviewer = accountCreator.create("added", "added@example.com", "added");
-    addReviewer(adder, sc.changeId, sc.owner, reviewer.email, CC_ON_OWN_COMMENTS, null);
+    addReviewer(adder, sc.changeId, sc.owner, reviewer.email(), CC_ON_OWN_COMMENTS, null);
     // TODO(logan): Should CCs be included?
     assertThat(sender)
         .sent("newchange", sc)
@@ -314,24 +307,24 @@
         .cc(sc.owner, sc.reviewer)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void addReviewerToReviewableChangeByOwnerCcingSelfInNoteDbSingly() throws Exception {
-    addReviewerToReviewableChangeByOwnerCcingSelfInNoteDb(singly());
+  public void addReviewerToReviewableChangeByOwnerCcingSelfSingly() throws Exception {
+    addReviewerToReviewableChangeByOwnerCcingSelf(singly());
   }
 
   @Test
-  public void addReviewerToReviewableChangeByOwnerCcingSelfInNoteDbBatch() throws Exception {
-    addReviewerToReviewableChangeByOwnerCcingSelfInNoteDb(batch());
+  public void addReviewerToReviewableChangeByOwnerCcingSelfBatch() throws Exception {
+    addReviewerToReviewableChangeByOwnerCcingSelf(batch());
   }
 
-  private void addReviewerToReviewableChangeByOtherInNoteDb(Adder adder) throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
+  private void addReviewerToReviewableChangeByOther(Adder adder) throws Exception {
     TestAccount other = accountCreator.create("other", "other@example.com", "other");
     StagedChange sc = stageReviewableChange();
     TestAccount reviewer = accountCreator.create("added", "added@example.com", "added");
-    addReviewer(adder, sc.changeId, other, reviewer.email);
+    addReviewer(adder, sc.changeId, other, reviewer.email());
     // TODO(logan): Should CCs be included?
     assertThat(sender)
         .sent("newchange", sc)
@@ -339,24 +332,24 @@
         .cc(sc.owner, sc.reviewer)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void addReviewerToReviewableChangeByOtherInNoteDbSingly() throws Exception {
-    addReviewerToReviewableChangeByOtherInNoteDb(singly());
+  public void addReviewerToReviewableChangeByOtherSingly() throws Exception {
+    addReviewerToReviewableChangeByOther(singly());
   }
 
   @Test
-  public void addReviewerToReviewableChangeByOtherInNoteDbBatch() throws Exception {
-    addReviewerToReviewableChangeByOtherInNoteDb(batch());
+  public void addReviewerToReviewableChangeByOtherBatch() throws Exception {
+    addReviewerToReviewableChangeByOther(batch());
   }
 
-  private void addReviewerToReviewableChangeByOtherCcingSelfInNoteDb(Adder adder) throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
+  private void addReviewerToReviewableChangeByOtherCcingSelf(Adder adder) throws Exception {
     TestAccount other = accountCreator.create("other", "other@example.com", "other");
     StagedChange sc = stageReviewableChange();
     TestAccount reviewer = accountCreator.create("added", "added@example.com", "added");
-    addReviewer(adder, sc.changeId, other, reviewer.email, CC_ON_OWN_COMMENTS, null);
+    addReviewer(adder, sc.changeId, other, reviewer.email(), CC_ON_OWN_COMMENTS, null);
     // TODO(logan): Should CCs be included?
     assertThat(sender)
         .sent("newchange", sc)
@@ -364,38 +357,20 @@
         .cc(sc.owner, sc.reviewer, other)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void addReviewerToReviewableChangeByOtherCcingSelfInNoteDbSingly() throws Exception {
-    addReviewerToReviewableChangeByOtherCcingSelfInNoteDb(singly());
+  public void addReviewerToReviewableChangeByOtherCcingSelfSingly() throws Exception {
+    addReviewerToReviewableChangeByOtherCcingSelf(singly());
   }
 
   @Test
-  public void addReviewerToReviewableChangeByOtherCcingSelfInNoteDbBatch() throws Exception {
-    addReviewerToReviewableChangeByOtherCcingSelfInNoteDb(batch());
+  public void addReviewerToReviewableChangeByOtherCcingSelfBatch() throws Exception {
+    addReviewerToReviewableChangeByOtherCcingSelf(batch());
   }
 
-  private void addReviewerByEmailToReviewableChangeInReviewDb(Adder adder) throws Exception {
-    assume().that(notesMigration.readChanges()).isFalse();
-    String email = "addedbyemail@example.com";
-    StagedChange sc = stageReviewableChange();
-    addReviewer(adder, sc.changeId, sc.owner, email);
-    assertThat(sender).notSent();
-  }
-
-  @Test
-  public void addReviewerByEmailToReviewableChangeInReviewDbSingly() throws Exception {
-    addReviewerByEmailToReviewableChangeInReviewDb(singly());
-  }
-
-  @Test
-  public void addReviewerByEmailToReviewableChangeInReviewDbBatch() throws Exception {
-    addReviewerByEmailToReviewableChangeInReviewDb(batch());
-  }
-
-  private void addReviewerByEmailToReviewableChangeInNoteDb(Adder adder) throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
+  private void addReviewerByEmailToReviewableChange(Adder adder) throws Exception {
     String email = "addedbyemail@example.com";
     StagedChange sc = stageReviewableChange();
     addReviewer(adder, sc.changeId, sc.owner, email);
@@ -406,23 +381,24 @@
         .cc(sc.reviewer)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void addReviewerByEmailToReviewableChangeInNoteDbSingly() throws Exception {
-    addReviewerByEmailToReviewableChangeInNoteDb(singly());
+  public void addReviewerByEmailToReviewableChangeSingly() throws Exception {
+    addReviewerByEmailToReviewableChange(singly());
   }
 
   @Test
-  public void addReviewerByEmailToReviewableChangeInNoteDbBatch() throws Exception {
-    addReviewerByEmailToReviewableChangeInNoteDb(batch());
+  public void addReviewerByEmailToReviewableChangeBatch() throws Exception {
+    addReviewerByEmailToReviewableChange(batch());
   }
 
   private void addReviewerToWipChange(Adder adder) throws Exception {
     StagedChange sc = stageWipChange();
     TestAccount reviewer = accountCreator.create("added", "added@example.com", "added");
-    addReviewer(adder, sc.changeId, sc.owner, reviewer.email);
-    assertThat(sender).notSent();
+    addReviewer(adder, sc.changeId, sc.owner, reviewer.email());
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -435,29 +411,26 @@
     addReviewerToWipChange(batch());
   }
 
-  private void addReviewerToReviewableWipChange(Adder adder) throws Exception {
-    StagedChange sc = stageReviewableWipChange();
-    TestAccount reviewer = accountCreator.create("added", "added@example.com", "added");
-    addReviewer(adder, sc.changeId, sc.owner, reviewer.email);
-    assertThat(sender).notSent();
-  }
-
   @Test
   public void addReviewerToReviewableWipChangeSingly() throws Exception {
-    addReviewerToReviewableWipChange(singly());
+    StagedChange sc = stageReviewableWipChange();
+    TestAccount reviewer = accountCreator.create("added", "added@example.com", "added");
+    addReviewer(singly(), sc.changeId, sc.owner, reviewer.email());
+    // TODO(dborowitz): In theory this should match the batch case, but we don't currently pass
+    // enough info into AddReviewersEmail#emailReviewers to distinguish the reviewStarted case.
+    // Complicating the emailReviewers arguments is not the answer; this needs to be rewritten.
+    // Tolerate the difference for now.
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void addReviewerToReviewableWipChangeBatch() throws Exception {
-    addReviewerToReviewableWipChange(batch());
-  }
-
-  private void addReviewerToWipChangeInNoteDbNotifyAll(Adder adder) throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-    StagedChange sc = stageWipChange();
+    StagedChange sc = stageReviewableWipChange();
     TestAccount reviewer = accountCreator.create("added", "added@example.com", "added");
-    addReviewer(adder, sc.changeId, sc.owner, reviewer.email, NotifyHandling.ALL);
-    // TODO(logan): Should CCs be included?
+    addReviewer(batch(), sc.changeId, sc.owner, reviewer.email());
+    // For a review-started WIP change, same as in the notify=ALL case. It's not especially
+    // important to notify just because a reviewer is added, but we do want to notify in the other
+    // case that hits this codepath: posting an actual review.
     assertThat(sender)
         .sent("newchange", sc)
         .to(reviewer)
@@ -466,45 +439,10 @@
         .noOneElse();
   }
 
-  @Test
-  public void addReviewerToWipChangeInNoteDbNotifyAllSingly() throws Exception {
-    addReviewerToWipChangeInNoteDbNotifyAll(singly());
-  }
-
-  @Test
-  public void addReviewerToWipChangeInNoteDbNotifyAllBatch() throws Exception {
-    addReviewerToWipChangeInNoteDbNotifyAll(batch());
-  }
-
-  private void addReviewerToWipChangeInReviewDbNotifyAll(Adder adder) throws Exception {
-    assume().that(notesMigration.readChanges()).isFalse();
+  private void addReviewerToWipChangeNotifyAll(Adder adder) throws Exception {
     StagedChange sc = stageWipChange();
     TestAccount reviewer = accountCreator.create("added", "added@example.com", "added");
-    addReviewer(adder, sc.changeId, sc.owner, reviewer.email, NotifyHandling.ALL);
-    assertThat(sender)
-        .sent("newchange", sc)
-        .to(reviewer)
-        .cc(sc.reviewer, sc.ccer)
-        .cc(sc.reviewerByEmail, sc.ccerByEmail)
-        .noOneElse();
-  }
-
-  @Test
-  public void addReviewerToWipChangeInReviewDbNotifyAllSingly() throws Exception {
-    addReviewerToWipChangeInReviewDbNotifyAll(singly());
-  }
-
-  @Test
-  public void addReviewerToWipChangeInReviewDbNotifyAllBatch() throws Exception {
-    addReviewerToWipChangeInReviewDbNotifyAll(batch());
-  }
-
-  private void addReviewerToReviewableChangeInNoteDbNotifyOwnerReviewers(Adder adder)
-      throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-    StagedChange sc = stageReviewableChange();
-    TestAccount reviewer = accountCreator.create("added", "added@example.com", "added");
-    addReviewer(adder, sc.changeId, sc.owner, reviewer.email, OWNER_REVIEWERS);
+    addReviewer(adder, sc.changeId, sc.owner, reviewer.email(), NotifyHandling.ALL);
     // TODO(logan): Should CCs be included?
     assertThat(sender)
         .sent("newchange", sc)
@@ -512,62 +450,80 @@
         .cc(sc.reviewer)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void addReviewerToReviewableChangeInNoteDbNotifyOwnerReviewersSingly() throws Exception {
-    addReviewerToReviewableChangeInNoteDbNotifyOwnerReviewers(singly());
+  public void addReviewerToWipChangeNotifyAllSingly() throws Exception {
+    addReviewerToWipChangeNotifyAll(singly());
   }
 
   @Test
-  public void addReviewerToReviewableChangeInNoteDbNotifyOwnerReviewersBatch() throws Exception {
-    addReviewerToReviewableChangeInNoteDbNotifyOwnerReviewers(batch());
+  public void addReviewerToWipChangeNotifyAllBatch() throws Exception {
+    addReviewerToWipChangeNotifyAll(batch());
   }
 
-  private void addReviewerToReviewableChangeInNoteDbByOwnerCcingSelfNotifyOwner(Adder adder)
-      throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
+  private void addReviewerToReviewableChangeNotifyOwnerReviewers(Adder adder) throws Exception {
     StagedChange sc = stageReviewableChange();
     TestAccount reviewer = accountCreator.create("added", "added@example.com", "added");
-    addReviewer(adder, sc.changeId, sc.owner, reviewer.email, CC_ON_OWN_COMMENTS, OWNER);
-    assertThat(sender).notSent();
+    addReviewer(adder, sc.changeId, sc.owner, reviewer.email(), OWNER_REVIEWERS);
+    // TODO(logan): Should CCs be included?
+    assertThat(sender)
+        .sent("newchange", sc)
+        .to(reviewer)
+        .cc(sc.reviewer)
+        .cc(sc.reviewerByEmail, sc.ccerByEmail)
+        .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void addReviewerToReviewableChangeInNoteDbByOwnerCcingSelfNotifyOwnerSingly()
-      throws Exception {
-    addReviewerToReviewableChangeInNoteDbByOwnerCcingSelfNotifyOwner(singly());
+  public void addReviewerToReviewableChangeNotifyOwnerReviewersSingly() throws Exception {
+    addReviewerToReviewableChangeNotifyOwnerReviewers(singly());
   }
 
   @Test
-  public void addReviewerToReviewableChangeInNoteDbByOwnerCcingSelfNotifyOwnerBatch()
-      throws Exception {
-    addReviewerToReviewableChangeInNoteDbByOwnerCcingSelfNotifyOwner(batch());
+  public void addReviewerToReviewableChangeNotifyOwnerReviewersBatch() throws Exception {
+    addReviewerToReviewableChangeNotifyOwnerReviewers(batch());
   }
 
-  private void addReviewerToReviewableChangeInNoteDbByOwnerCcingSelfNotifyNone(Adder adder)
+  private void addReviewerToReviewableChangeByOwnerCcingSelfNotifyOwner(Adder adder)
       throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
     StagedChange sc = stageReviewableChange();
     TestAccount reviewer = accountCreator.create("added", "added@example.com", "added");
-    addReviewer(adder, sc.changeId, sc.owner, reviewer.email, CC_ON_OWN_COMMENTS, NONE);
-    assertThat(sender).notSent();
+    addReviewer(adder, sc.changeId, sc.owner, reviewer.email(), CC_ON_OWN_COMMENTS, OWNER);
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void addReviewerToReviewableChangeInNoteDbByOwnerCcingSelfNotifyNoneSingly()
-      throws Exception {
-    addReviewerToReviewableChangeInNoteDbByOwnerCcingSelfNotifyNone(singly());
+  public void addReviewerToReviewableChangeByOwnerCcingSelfNotifyOwnerSingly() throws Exception {
+    addReviewerToReviewableChangeByOwnerCcingSelfNotifyOwner(singly());
   }
 
   @Test
-  public void addReviewerToReviewableChangeInNoteDbByOwnerCcingSelfNotifyNoneBatch()
-      throws Exception {
-    addReviewerToReviewableChangeInNoteDbByOwnerCcingSelfNotifyNone(batch());
+  public void addReviewerToReviewableChangeByOwnerCcingSelfNotifyOwnerBatch() throws Exception {
+    addReviewerToReviewableChangeByOwnerCcingSelfNotifyOwner(batch());
   }
 
-  private void addNonUserReviewerByEmailInNoteDb(Adder adder) throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
+  private void addReviewerToReviewableChangeByOwnerCcingSelfNotifyNone(Adder adder)
+      throws Exception {
+    StagedChange sc = stageReviewableChange();
+    TestAccount reviewer = accountCreator.create("added", "added@example.com", "added");
+    addReviewer(adder, sc.changeId, sc.owner, reviewer.email(), CC_ON_OWN_COMMENTS, NONE);
+    assertThat(sender).didNotSend();
+  }
+
+  @Test
+  public void addReviewerToReviewableChangeByOwnerCcingSelfNotifyNoneSingly() throws Exception {
+    addReviewerToReviewableChangeByOwnerCcingSelfNotifyNone(singly());
+  }
+
+  @Test
+  public void addReviewerToReviewableChangeByOwnerCcingSelfNotifyNoneBatch() throws Exception {
+    addReviewerToReviewableChangeByOwnerCcingSelfNotifyNone(batch());
+  }
+
+  private void addNonUserReviewerByEmail(Adder adder) throws Exception {
     StagedChange sc = stageReviewableChange();
     addReviewer(adder, sc.changeId, sc.owner, "nonexistent@example.com");
     assertThat(sender)
@@ -576,20 +532,20 @@
         .cc(sc.reviewer)
         .cc(sc.ccerByEmail, sc.reviewerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void addNonUserReviewerByEmailInNoteDbSingly() throws Exception {
-    addNonUserReviewerByEmailInNoteDb(singly(ReviewerState.REVIEWER));
+  public void addNonUserReviewerByEmailSingly() throws Exception {
+    addNonUserReviewerByEmail(singly(ReviewerState.REVIEWER));
   }
 
   @Test
-  public void addNonUserReviewerByEmailInNoteDbBatch() throws Exception {
-    addNonUserReviewerByEmailInNoteDb(batch(ReviewerState.REVIEWER));
+  public void addNonUserReviewerByEmailBatch() throws Exception {
+    addNonUserReviewerByEmail(batch(ReviewerState.REVIEWER));
   }
 
-  private void addNonUserCcByEmailInNoteDb(Adder adder) throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
+  private void addNonUserCcByEmail(Adder adder) throws Exception {
     StagedChange sc = stageReviewableChange();
     addReviewer(adder, sc.changeId, sc.owner, "nonexistent@example.com");
     assertThat(sender)
@@ -598,16 +554,17 @@
         .cc(sc.reviewer)
         .cc(sc.ccerByEmail, sc.reviewerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void addNonUserCcByEmailInNoteDbSingly() throws Exception {
-    addNonUserCcByEmailInNoteDb(singly(ReviewerState.CC));
+  public void addNonUserCcByEmailSingly() throws Exception {
+    addNonUserCcByEmail(singly(ReviewerState.CC));
   }
 
   @Test
-  public void addNonUserCcByEmailInNoteDbBatch() throws Exception {
-    addNonUserCcByEmailInNoteDb(batch(ReviewerState.CC));
+  public void addNonUserCcByEmailBatch() throws Exception {
+    addNonUserCcByEmail(batch(ReviewerState.CC));
   }
 
   private interface Adder {
@@ -666,7 +623,7 @@
       @Nullable NotifyHandling notify)
       throws Exception {
     setEmailStrategy(by, emailStrategy);
-    setApiUser(by);
+    requestScopeOperations.setApiUser(by.id());
     adder.addReviewer(changeId, reviewer, notify);
   }
 
@@ -685,6 +642,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -699,6 +657,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -713,6 +672,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -727,6 +687,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -742,6 +703,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -757,6 +719,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -768,13 +731,14 @@
         .cc(sc.reviewer, sc.ccer)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void commentOnReviewableChangeByOwnerNotifyOwner() throws Exception {
     StagedChange sc = stageReviewableChange();
     review(sc.owner, sc.changeId, ENABLED, OWNER);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -782,14 +746,14 @@
     StagedChange sc = stageReviewableChange();
     setEmailStrategy(sc.owner, CC_ON_OWN_COMMENTS);
     review(sc.owner, sc.changeId, ENABLED, OWNER);
-    assertThat(sender).notSent(); // TODO(logan): Why not send to owner?
+    assertThat(sender).didNotSend(); // TODO(logan): Why not send to owner?
   }
 
   @Test
   public void commentOnReviewableChangeByOwnerNotifyNone() throws Exception {
     StagedChange sc = stageReviewableChange();
     review(sc.owner, sc.changeId, ENABLED, NONE);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -797,7 +761,7 @@
     StagedChange sc = stageReviewableChange();
     setEmailStrategy(sc.owner, CC_ON_OWN_COMMENTS);
     review(sc.owner, sc.changeId, ENABLED, NONE);
-    assertThat(sender).notSent(); // TODO(logan): Why not send to owner?
+    assertThat(sender).didNotSend(); // TODO(logan): Why not send to owner?
   }
 
   @Test
@@ -811,20 +775,21 @@
         .cc(sc.reviewer, sc.ccer)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void commentOnWipChangeByOwner() throws Exception {
     StagedChange sc = stageWipChange();
     review(sc.owner, sc.changeId, ENABLED);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void commentOnWipChangeByOwnerCcingSelf() throws Exception {
     StagedChange sc = stageWipChange();
     review(sc.owner, sc.changeId, CC_ON_OWN_COMMENTS);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -838,6 +803,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -846,6 +812,7 @@
     TestAccount bot = sc.testAccount("bot");
     review(bot, sc.changeId, ENABLED, null, "autogenerated:tag");
     assertThat(sender).sent("comment", sc).to(sc.owner).noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -854,6 +821,7 @@
     TestAccount bot = sc.testAccount("bot");
     review(bot, sc.changeId, ENABLED, null, "autogenerated:tag");
     assertThat(sender).sent("comment", sc).to(sc.owner).noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -869,6 +837,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -882,6 +851,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -889,7 +859,7 @@
     StagedChange sc = stageReviewableChange();
     ReviewInput in = ReviewInput.noScore().setWorkInProgress(true);
     gApi.changes().id(sc.changeId).revision("current").review(in);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -904,7 +874,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -919,14 +889,13 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void addReviewerOnWipChangeAndStartReviewInNoteDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
+  public void addReviewerOnWipChangeAndStartReview() throws Exception {
     StagedChange sc = stageWipChange();
-    ReviewInput in = ReviewInput.noScore().reviewer(other.email).setWorkInProgress(false);
+    ReviewInput in = ReviewInput.noScore().reviewer(other.email()).setWorkInProgress(false);
     gApi.changes().id(sc.changeId).revision("current").review(in);
     assertThat(sender)
         .sent("comment", sc)
@@ -942,29 +911,7 @@
         .cc(sc.reviewer)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
-    assertThat(sender).notSent();
-  }
-
-  @Test
-  public void addReviewerOnWipChangeAndStartReviewInReviewDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isFalse();
-    StagedChange sc = stageWipChange();
-    ReviewInput in = ReviewInput.noScore().reviewer(other.email).setWorkInProgress(false);
-    gApi.changes().id(sc.changeId).revision("current").review(in);
-    assertThat(sender)
-        .sent("comment", sc)
-        .cc(sc.reviewer, sc.ccer, other)
-        .cc(sc.reviewerByEmail, sc.ccerByEmail)
-        .bcc(sc.starrer)
-        .bcc(ALL_COMMENTS)
-        .noOneElse();
-    assertThat(sender)
-        .sent("newchange", sc)
-        .to(other)
-        .cc(sc.reviewer, sc.ccer)
-        .cc(sc.reviewerByEmail, sc.ccerByEmail)
-        .noOneElse();
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1018,12 +965,13 @@
         .to(spc.watchingProjectOwner)
         .bcc(NEW_CHANGES, NEW_PATCHSETS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void createWipChange() throws Exception {
     stagePreChange("refs/for/master%wip");
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1031,7 +979,7 @@
     setWorkInProgressByDefault(project, InheritableBoolean.TRUE);
     StagedPreChange spc = stagePreChange("refs/for/master");
     Truth.assertThat(gApi.changes().id(spc.changeId).get().workInProgress).isTrue();
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1039,41 +987,41 @@
     // Make sure owner user is created
     StagedChange sc = stageReviewableChange();
     // All was cleaned already
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
 
     // Toggle workInProgress flag for owner
-    GeneralPreferencesInfo prefs = gApi.accounts().id(sc.owner.id.get()).getPreferences();
+    GeneralPreferencesInfo prefs = gApi.accounts().id(sc.owner.id().get()).getPreferences();
     prefs.workInProgressByDefault = true;
-    gApi.accounts().id(sc.owner.id.get()).setPreferences(prefs);
+    gApi.accounts().id(sc.owner.id().get()).setPreferences(prefs);
 
     // Create another change without notification that should be wip
     StagedPreChange spc = stagePreChange("refs/for/master");
     Truth.assertThat(gApi.changes().id(spc.changeId).get().workInProgress).isTrue();
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
 
     // Clean up workInProgressByDefault by owner
-    prefs = gApi.accounts().id(sc.owner.id.get()).getPreferences();
+    prefs = gApi.accounts().id(sc.owner.id().get()).getPreferences();
     Truth.assertThat(prefs.workInProgressByDefault).isTrue();
     prefs.workInProgressByDefault = false;
-    gApi.accounts().id(sc.owner.id.get()).setPreferences(prefs);
+    gApi.accounts().id(sc.owner.id().get()).setPreferences(prefs);
   }
 
   @Test
   public void createReviewableChangeWithNotifyOwnerReviewers() throws Exception {
     stagePreChange("refs/for/master%notify=OWNER_REVIEWERS");
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void createReviewableChangeWithNotifyOwner() throws Exception {
     stagePreChange("refs/for/master%notify=OWNER");
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void createReviewableChangeWithNotifyNone() throws Exception {
     stagePreChange("refs/for/master%notify=OWNER");
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1084,6 +1032,7 @@
         .to(spc.watchingProjectOwner)
         .bcc(NEW_CHANGES, NEW_PATCHSETS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1091,21 +1040,17 @@
     StagedPreChange spc =
         stagePreChange(
             "refs/for/master",
-            users -> ImmutableList.of("r=" + users.reviewer.username, "cc=" + users.ccer.username));
+            users ->
+                ImmutableList.of("r=" + users.reviewer.username(), "cc=" + users.ccer.username()));
     FakeEmailSenderSubject subject =
         assertThat(sender).sent("newchange", spc).to(spc.reviewer, spc.watchingProjectOwner);
-    if (notesMigration.readChanges()) {
-      subject.cc(spc.ccer);
-    } else {
-      // CCs are considered reviewers in the storage layer.
-      subject.to(spc.ccer);
-    }
+    subject.cc(spc.ccer);
     subject.bcc(NEW_CHANGES, NEW_PATCHSETS).noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void createReviewableChangeWithReviewersAndCcsByEmailInNoteDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
+  public void createReviewableChangeWithReviewersAndCcsByEmail() throws Exception {
     StagedPreChange spc =
         stagePreChange(
             "refs/for/master",
@@ -1118,6 +1063,7 @@
         .cc("nobody2@example.com")
         .bcc(NEW_CHANGES, NEW_PATCHSETS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   /*
@@ -1127,7 +1073,7 @@
   @Test
   public void deleteReviewerFromReviewableChange() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.id());
     removeReviewer(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteReviewer", sc)
@@ -1137,6 +1083,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1152,12 +1099,13 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void deleteReviewerFromReviewableChangeByAdmin() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     removeReviewer(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteReviewer", sc)
@@ -1167,13 +1115,14 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void deleteReviewerFromReviewableChangeByAdminCcingSelf() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     setEmailStrategy(admin, EmailStrategy.CC_ON_OWN_COMMENTS);
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     removeReviewer(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteReviewer", sc)
@@ -1183,12 +1132,13 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void deleteCcerFromReviewableChange() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.id());
     removeReviewer(sc, extraCcer);
     assertThat(sender)
         .sent("deleteReviewer", sc)
@@ -1198,12 +1148,13 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void deleteReviewerFromReviewableChangeNotifyOwnerReviewers() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.id());
     removeReviewer(sc, extraReviewer, NotifyHandling.OWNER_REVIEWERS);
     assertThat(sender)
         .sent("deleteReviewer", sc)
@@ -1211,13 +1162,14 @@
         .cc(extraCcer, sc.reviewer, sc.ccer)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void deleteReviewerFromReviewableChangeNotifyOwner() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     removeReviewer(sc, extraReviewer, NotifyHandling.OWNER);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1226,13 +1178,14 @@
     setEmailStrategy(sc.owner, EmailStrategy.CC_ON_OWN_COMMENTS);
     removeReviewer(sc, extraReviewer, NotifyHandling.OWNER);
     assertThat(sender).sent("deleteReviewer", sc).to(sc.owner, extraReviewer).noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void deleteReviewerFromReviewableChangeNotifyNone() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     removeReviewer(sc, extraReviewer, NotifyHandling.NONE);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1240,27 +1193,27 @@
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     setEmailStrategy(sc.owner, EmailStrategy.CC_ON_OWN_COMMENTS);
     removeReviewer(sc, extraReviewer, NotifyHandling.NONE);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void deleteReviewerFromReviewableWipChange() throws Exception {
     StagedChange sc = stageReviewableWipChangeWithExtraReviewer();
     removeReviewer(sc, extraReviewer);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void deleteReviewerFromWipChange() throws Exception {
     StagedChange sc = stageWipChangeWithExtraReviewer();
     removeReviewer(sc, extraReviewer);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void deleteReviewerFromWipChangeNotifyAll() throws Exception {
     StagedChange sc = stageWipChangeWithExtraReviewer();
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.id());
     removeReviewer(sc, extraReviewer, NotifyHandling.ALL);
     assertThat(sender)
         .sent("deleteReviewer", sc)
@@ -1270,15 +1223,17 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void deleteReviewerWithApprovalFromWipChange() throws Exception {
     StagedChange sc = stageWipChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.id());
     removeReviewer(sc, extraReviewer);
     assertThat(sender).sent("deleteReviewer", sc).to(extraReviewer).noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1286,19 +1241,18 @@
     StagedChange sc = stageWipChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
     removeReviewer(sc, extraReviewer, NotifyHandling.OWNER);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void deleteReviewerByEmailFromWipChangeInNoteDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
+  public void deleteReviewerByEmailFromWipChange() throws Exception {
     StagedChange sc = stageWipChangeWithExtraReviewer();
     gApi.changes().id(sc.changeId).reviewer(sc.reviewerByEmail).remove();
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   private void recommend(StagedChange sc, TestAccount by) throws Exception {
-    setApiUser(by);
+    requestScopeOperations.setApiUser(by.id());
     gApi.changes().id(sc.changeId).revision("current").review(ReviewInput.recommend());
   }
 
@@ -1310,15 +1264,18 @@
     StagedChange sc = stager.stage();
     ReviewInput in =
         ReviewInput.noScore()
-            .reviewer(extraReviewer.email)
-            .reviewer(extraCcer.email, ReviewerState.CC, false);
-    setApiUser(extraReviewer);
+            .reviewer(extraReviewer.email())
+            .reviewer(extraCcer.email(), ReviewerState.CC, false);
+    requestScopeOperations.setApiUser(extraReviewer.id());
     gApi.changes().id(sc.changeId).revision("current").review(in);
+    sender.clear();
     return sc;
   }
 
   private StagedChange stageReviewableChangeWithExtraReviewer() throws Exception {
-    return stageChangeWithExtraReviewer(this::stageReviewableChange);
+    StagedChange sc = stageChangeWithExtraReviewer(this::stageReviewableChange);
+    sender.clear();
+    return sc;
   }
 
   private StagedChange stageReviewableWipChangeWithExtraReviewer() throws Exception {
@@ -1326,12 +1283,14 @@
   }
 
   private StagedChange stageWipChangeWithExtraReviewer() throws Exception {
-    return stageChangeWithExtraReviewer(this::stageWipChange);
+    StagedChange sc = stageChangeWithExtraReviewer(this::stageWipChange);
+    assertThat(sender).didNotSend();
+    return sc;
   }
 
   private void removeReviewer(StagedChange sc, TestAccount account) throws Exception {
     sender.clear();
-    gApi.changes().id(sc.changeId).reviewer(account.email).remove();
+    gApi.changes().id(sc.changeId).reviewer(account.email()).remove();
   }
 
   private void removeReviewer(StagedChange sc, TestAccount account, NotifyHandling notify)
@@ -1339,7 +1298,7 @@
     sender.clear();
     DeleteReviewerInput in = new DeleteReviewerInput();
     in.notify = notify;
-    gApi.changes().id(sc.changeId).reviewer(account.email).remove(in);
+    gApi.changes().id(sc.changeId).reviewer(account.email()).remove(in);
   }
 
   /*
@@ -1350,7 +1309,7 @@
   public void deleteVoteFromReviewableChange() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.id());
     deleteVote(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteVote", sc)
@@ -1359,6 +1318,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1366,7 +1326,7 @@
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
     setEmailStrategy(sc.owner, CC_ON_OWN_COMMENTS);
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.id());
     deleteVote(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteVote", sc)
@@ -1376,13 +1336,14 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void deleteVoteFromReviewableChangeByAdmin() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     deleteVote(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteVote", sc)
@@ -1392,6 +1353,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1399,7 +1361,7 @@
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
     setEmailStrategy(admin, EmailStrategy.CC_ON_OWN_COMMENTS);
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     deleteVote(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteVote", sc)
@@ -1409,19 +1371,21 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void deleteVoteFromReviewableChangeNotifyOwnerReviewers() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.id());
     deleteVote(sc, extraReviewer, NotifyHandling.OWNER_REVIEWERS);
     assertThat(sender)
         .sent("deleteVote", sc)
         .cc(sc.reviewer, sc.ccer, extraReviewer, extraCcer)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1429,7 +1393,7 @@
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
     setEmailStrategy(sc.owner, CC_ON_OWN_COMMENTS);
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.id());
     deleteVote(sc, extraReviewer, NotifyHandling.OWNER_REVIEWERS);
     assertThat(sender)
         .sent("deleteVote", sc)
@@ -1437,24 +1401,26 @@
         .cc(sc.reviewer, sc.ccer, extraReviewer, extraCcer)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void deleteVoteFromReviewableChangeNotifyOwner() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     deleteVote(sc, extraReviewer, NotifyHandling.OWNER);
     assertThat(sender).sent("deleteVote", sc).to(sc.owner).noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void deleteVoteFromReviewableChangeNotifyNone() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.id());
     deleteVote(sc, extraReviewer, NotifyHandling.NONE);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1462,16 +1428,16 @@
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
     setEmailStrategy(sc.owner, CC_ON_OWN_COMMENTS);
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.id());
     deleteVote(sc, extraReviewer, NotifyHandling.NONE);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void deleteVoteFromReviewableWipChange() throws Exception {
     StagedChange sc = stageReviewableWipChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.id());
     deleteVote(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteVote", sc)
@@ -1480,13 +1446,14 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void deleteVoteFromWipChange() throws Exception {
     StagedChange sc = stageWipChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.id());
     deleteVote(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteVote", sc)
@@ -1495,11 +1462,12 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   private void deleteVote(StagedChange sc, TestAccount account) throws Exception {
     sender.clear();
-    gApi.changes().id(sc.changeId).reviewer(account.email).deleteVote("Code-Review");
+    gApi.changes().id(sc.changeId).reviewer(account.email()).deleteVote("Code-Review");
   }
 
   private void deleteVote(StagedChange sc, TestAccount account, NotifyHandling notify)
@@ -1508,7 +1476,7 @@
     DeleteVoteInput in = new DeleteVoteInput();
     in.label = "Code-Review";
     in.notify = notify;
-    gApi.changes().id(sc.changeId).reviewer(account.email).deleteVote(in);
+    gApi.changes().id(sc.changeId).reviewer(account.email()).deleteVote(in);
   }
 
   /*
@@ -1516,16 +1484,48 @@
    */
 
   @Test
-  public void mergeByOwner() throws Exception {
-    StagedChange sc = stageChangeReadyForMerge();
-    merge(sc.changeId, sc.owner);
-    assertThat(sender)
-        .sent("merged", sc)
-        .cc(sc.reviewer, sc.ccer)
-        .cc(sc.reviewerByEmail, sc.ccerByEmail)
-        .bcc(sc.starrer)
-        .bcc(ALL_COMMENTS, SUBMITTED_CHANGES)
-        .noOneElse();
+  public void mergeByOwnerAllSubmitStrategies() throws Exception {
+    mergeByOwnerAllSubmitStrategies(false);
+  }
+
+  @Test
+  public void mergeByOwnerAllSubmitStrategiesWithAdvancingBranch() throws Exception {
+    mergeByOwnerAllSubmitStrategies(true);
+  }
+
+  private void mergeByOwnerAllSubmitStrategies(boolean advanceBranchBeforeSubmitting)
+      throws Exception {
+    for (SubmitType submitType : SubmitType.values()) {
+      try (ProjectConfigUpdate u = updateProject(project)) {
+        u.getConfig().getProject().setSubmitType(submitType);
+        u.save();
+      }
+
+      StagedChange sc = stageChangeReadyForMerge();
+
+      String name = submitType + " sender";
+      if (advanceBranchBeforeSubmitting) {
+        if (submitType == SubmitType.FAST_FORWARD_ONLY) {
+          continue;
+        }
+        try (Repository repo = repoManager.openRepository(project);
+            TestRepository<Repository> tr = new TestRepository<>(repo)) {
+          tr.branch("master").commit().create();
+        }
+        name += " after branch has advanced";
+      }
+
+      merge(sc.changeId, sc.owner);
+      assertThat(sender)
+          .named(name)
+          .sent("merged", sc)
+          .cc(sc.reviewer, sc.ccer)
+          .cc(sc.reviewerByEmail, sc.ccerByEmail)
+          .bcc(sc.starrer)
+          .bcc(ALL_COMMENTS, SUBMITTED_CHANGES)
+          .noOneElse();
+      assertThat(sender).named(name).didNotSend();
+    }
   }
 
   @Test
@@ -1540,6 +1540,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS, SUBMITTED_CHANGES)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1554,6 +1555,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS, SUBMITTED_CHANGES)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1568,6 +1570,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS, SUBMITTED_CHANGES)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1580,6 +1583,7 @@
         .cc(sc.reviewer, sc.ccer)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1587,6 +1591,7 @@
     StagedChange sc = stageChangeReadyForMerge();
     merge(sc.changeId, other, OWNER);
     assertThat(sender).sent("merged", sc).to(sc.owner).noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1595,13 +1600,14 @@
     setEmailStrategy(other, EmailStrategy.CC_ON_OWN_COMMENTS);
     merge(sc.changeId, other, OWNER);
     assertThat(sender).sent("merged", sc).to(sc.owner).noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void mergeByOtherNotifyNone() throws Exception {
     StagedChange sc = stageChangeReadyForMerge();
     merge(sc.changeId, other, NONE);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1609,7 +1615,7 @@
     StagedChange sc = stageChangeReadyForMerge();
     setEmailStrategy(other, EmailStrategy.CC_ON_OWN_COMMENTS);
     merge(sc.changeId, other, NONE);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   private void merge(String changeId, TestAccount by) throws Exception {
@@ -1619,7 +1625,7 @@
   private void merge(String changeId, TestAccount by, EmailStrategy emailStrategy)
       throws Exception {
     setEmailStrategy(by, emailStrategy);
-    setApiUser(by);
+    requestScopeOperations.setApiUser(by.id());
     gApi.changes().id(changeId).revision("current").submit();
   }
 
@@ -1631,7 +1637,7 @@
       String changeId, TestAccount by, EmailStrategy emailStrategy, NotifyHandling notify)
       throws Exception {
     setEmailStrategy(by, emailStrategy);
-    setApiUser(by);
+    requestScopeOperations.setApiUser(by.id());
     SubmitInput in = new SubmitInput();
     in.notify = notify;
     gApi.changes().id(changeId).revision("current").submit(in);
@@ -1639,7 +1645,7 @@
 
   private StagedChange stageChangeReadyForMerge() throws Exception {
     StagedChange sc = stageReviewableChange();
-    setApiUser(sc.reviewer);
+    requestScopeOperations.setApiUser(sc.reviewer.id());
     gApi.changes().id(sc.changeId).revision("current").review(ReviewInput.approve());
     sender.clear();
     return sc;
@@ -1650,8 +1656,7 @@
    */
 
   @Test
-  public void newPatchSetByOwnerOnReviewableChangeInNoteDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
+  public void newPatchSetByOwnerOnReviewableChange() throws Exception {
     StagedChange sc = stageReviewableChange();
     pushTo(sc, "refs/for/master", sc.owner);
     assertThat(sender)
@@ -1662,24 +1667,11 @@
         .bcc(sc.starrer)
         .bcc(NEW_PATCHSETS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void newPatchSetByOwnerOnReviewableChangeInReviewDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isFalse();
-    StagedChange sc = stageReviewableChange();
-    pushTo(sc, "refs/for/master", sc.owner);
-    assertThat(sender)
-        .sent("newpatchset", sc)
-        .to(sc.reviewer, sc.ccer)
-        .bcc(sc.starrer)
-        .bcc(NEW_PATCHSETS)
-        .noOneElse();
-  }
-
-  @Test
-  public void newPatchSetByOtherOnReviewableChangeInNoteDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
+  public void newPatchSetByOtherOnReviewableChange() throws Exception {
     StagedChange sc = stageReviewableChange();
     pushTo(sc, "refs/for/master", other);
     assertThat(sender)
@@ -1691,25 +1683,11 @@
         .bcc(sc.starrer)
         .bcc(NEW_PATCHSETS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void newPatchSetByOtherOnReviewableChangeInReviewDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isFalse();
-    StagedChange sc = stageReviewableChange();
-    pushTo(sc, "refs/for/master", other);
-    assertThat(sender)
-        .sent("newpatchset", sc)
-        .notTo(sc.owner) // TODO(logan): This email shouldn't come from the owner.
-        .to(sc.reviewer, sc.ccer, other)
-        .bcc(sc.starrer)
-        .bcc(NEW_PATCHSETS)
-        .noOneElse();
-  }
-
-  @Test
-  public void newPatchSetByOtherOnReviewableChangeOwnerSelfCcInNoteDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
+  public void newPatchSetByOtherOnReviewableChangeOwnerSelfCc() throws Exception {
     StagedChange sc = stageReviewableChange();
     pushTo(sc, "refs/for/master", other, EmailStrategy.CC_ON_OWN_COMMENTS);
     assertThat(sender)
@@ -1721,25 +1699,11 @@
         .bcc(sc.starrer)
         .bcc(NEW_PATCHSETS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void newPatchSetByOtherOnReviewableChangeOwnerSelfCcInReviewDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isFalse();
-    StagedChange sc = stageReviewableChange();
-    pushTo(sc, "refs/for/master", other, EmailStrategy.CC_ON_OWN_COMMENTS);
-    assertThat(sender)
-        .sent("newpatchset", sc)
-        .notTo(sc.owner) // TODO(logan): This shouldn't be sent *from* the owner.
-        .to(sc.reviewer, sc.ccer, other)
-        .bcc(sc.starrer)
-        .bcc(NEW_PATCHSETS)
-        .noOneElse();
-  }
-
-  @Test
-  public void newPatchSetByOtherOnReviewableChangeNotifyOwnerReviewersInNoteDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
+  public void newPatchSetByOtherOnReviewableChangeNotifyOwnerReviewers() throws Exception {
     StagedChange sc = stageReviewableChange();
     pushTo(sc, "refs/for/master%notify=OWNER_REVIEWERS", other);
     assertThat(sender)
@@ -1750,26 +1714,12 @@
         .cc(sc.ccer)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void newPatchSetByOtherOnReviewableChangeNotifyOwnerReviewersInReviewDb()
+  public void newPatchSetByOtherOnReviewableChangeOwnerSelfCcNotifyOwnerReviewers()
       throws Exception {
-    assume().that(notesMigration.readChanges()).isFalse();
-    StagedChange sc = stageReviewableChange();
-    pushTo(sc, "refs/for/master%notify=OWNER_REVIEWERS", other);
-    assertThat(sender)
-        .sent("newpatchset", sc)
-        .notTo(sc.owner) // TODO(logan): This shouldn't be sent *from* the owner.
-        .to(sc.reviewer, sc.ccer)
-        .to(other)
-        .noOneElse();
-  }
-
-  @Test
-  public void newPatchSetByOtherOnReviewableChangeOwnerSelfCcNotifyOwnerReviewersInNoteDb()
-      throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
     StagedChange sc = stageReviewableChange();
     pushTo(sc, "refs/for/master%notify=OWNER_REVIEWERS", other, EmailStrategy.CC_ON_OWN_COMMENTS);
     assertThat(sender)
@@ -1780,27 +1730,14 @@
         .cc(sc.ccer)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
-  }
-
-  @Test
-  public void newPatchSetByOtherOnReviewableChangeOwnerSelfCcNotifyOwnerReviewersInReviewDb()
-      throws Exception {
-    assume().that(notesMigration.readChanges()).isFalse();
-    StagedChange sc = stageReviewableChange();
-    pushTo(sc, "refs/for/master%notify=OWNER_REVIEWERS", other, EmailStrategy.CC_ON_OWN_COMMENTS);
-    assertThat(sender)
-        .sent("newpatchset", sc)
-        .to(sc.reviewer, sc.ccer)
-        .to(other)
-        .notTo(sc.owner) // TODO(logan): This shouldn't be sent *from* the owner.
-        .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void newPatchSetByOtherOnReviewableChangeNotifyOwner() throws Exception {
     StagedChange sc = stageReviewableChange();
     pushTo(sc, "refs/for/master%notify=OWNER", other);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1809,7 +1746,7 @@
     pushTo(sc, "refs/for/master%notify=OWNER", other, EmailStrategy.CC_ON_OWN_COMMENTS);
     // TODO(logan): This email shouldn't come from the owner, and that's why
     // no email is currently sent (owner isn't CCing self).
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1818,33 +1755,32 @@
     pushTo(sc, "refs/for/master%notify=NONE", other);
     // TODO(logan): This email shouldn't come from the owner, and that's why
     // no email is currently sent (owner isn't CCing self).
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void newPatchSetByOtherOnReviewableChangeOwnerSelfCcNotifyNone() throws Exception {
     StagedChange sc = stageReviewableChange();
     pushTo(sc, "refs/for/master%notify=NONE", other, EmailStrategy.CC_ON_OWN_COMMENTS);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void newPatchSetByOwnerOnReviewableChangeToWip() throws Exception {
     StagedChange sc = stageReviewableChange();
     pushTo(sc, "refs/for/master%wip", sc.owner);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void newPatchSetOnWipChange() throws Exception {
     StagedChange sc = stageWipChange();
     pushTo(sc, "refs/for/master%wip", sc.owner);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void newPatchSetOnWipChangeNotifyAllInNoteDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
+  public void newPatchSetOnWipChangeNotifyAll() throws Exception {
     StagedChange sc = stageWipChange();
     pushTo(sc, "refs/for/master%wip,notify=ALL", sc.owner);
     assertThat(sender)
@@ -1855,24 +1791,11 @@
         .bcc(sc.starrer)
         .bcc(NEW_PATCHSETS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void newPatchSetOnWipChangeNotifyAllInReviewDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isFalse();
-    StagedChange sc = stageWipChange();
-    pushTo(sc, "refs/for/master%wip,notify=ALL", sc.owner);
-    assertThat(sender)
-        .sent("newpatchset", sc)
-        .to(sc.reviewer, sc.ccer)
-        .bcc(sc.starrer)
-        .bcc(NEW_PATCHSETS)
-        .noOneElse();
-  }
-
-  @Test
-  public void newPatchSetOnWipChangeToReadyInNoteDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
+  public void newPatchSetOnWipChangeToReady() throws Exception {
     StagedChange sc = stageWipChange();
     pushTo(sc, "refs/for/master%ready", sc.owner);
     assertThat(sender)
@@ -1883,34 +1806,21 @@
         .bcc(sc.starrer)
         .bcc(NEW_PATCHSETS)
         .noOneElse();
-  }
-
-  @Test
-  public void newPatchSetOnWipChangeToReadyInReviewDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isFalse();
-    StagedChange sc = stageWipChange();
-    pushTo(sc, "refs/for/master%ready", sc.owner);
-    assertThat(sender)
-        .sent("newpatchset", sc)
-        .to(sc.reviewer, sc.ccer)
-        .bcc(sc.starrer)
-        .bcc(NEW_PATCHSETS)
-        .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void newPatchSetOnReviewableWipChange() throws Exception {
     StagedChange sc = stageReviewableWipChange();
     pushTo(sc, "refs/for/master%wip", sc.owner);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void newPatchSetOnReviewableChangeAddingReviewerInNoteDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
+  public void newPatchSetOnReviewableChangeAddingReviewer() throws Exception {
     StagedChange sc = stageReviewableChange();
     TestAccount newReviewer = sc.testAccount("newReviewer");
-    pushTo(sc, "refs/for/master%r=" + newReviewer.username, sc.owner);
+    pushTo(sc, "refs/for/master%r=" + newReviewer.username(), sc.owner);
     assertThat(sender)
         .sent("newpatchset", sc)
         .to(sc.reviewer, newReviewer)
@@ -1919,39 +1829,22 @@
         .bcc(sc.starrer)
         .bcc(NEW_PATCHSETS)
         .noOneElse();
-    assertThat(sender).notSent();
-  }
-
-  @Test
-  public void newPatchSetOnReviewableChangeAddingReviewerInReviewDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isFalse();
-    StagedChange sc = stageReviewableChange();
-    TestAccount newReviewer = sc.testAccount("newReviewer");
-    pushTo(sc, "refs/for/master%r=" + newReviewer.username, sc.owner);
-    assertThat(sender)
-        .sent("newpatchset", sc)
-        .to(sc.reviewer, sc.ccer, newReviewer)
-        .cc(sc.reviewerByEmail, sc.ccerByEmail)
-        .bcc(sc.starrer)
-        .bcc(NEW_PATCHSETS)
-        .noOneElse();
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void newPatchSetOnWipChangeAddingReviewer() throws Exception {
     StagedChange sc = stageWipChange();
     TestAccount newReviewer = sc.testAccount("newReviewer");
-    pushTo(sc, "refs/for/master%r=" + newReviewer.username, sc.owner);
-    assertThat(sender).notSent();
+    pushTo(sc, "refs/for/master%r=" + newReviewer.username(), sc.owner);
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void newPatchSetOnWipChangeAddingReviewerNotifyAllInNoteDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
+  public void newPatchSetOnWipChangeAddingReviewerNotifyAll() throws Exception {
     StagedChange sc = stageWipChange();
     TestAccount newReviewer = sc.testAccount("newReviewer");
-    pushTo(sc, "refs/for/master%notify=ALL,r=" + newReviewer.username, sc.owner);
+    pushTo(sc, "refs/for/master%notify=ALL,r=" + newReviewer.username(), sc.owner);
     assertThat(sender)
         .sent("newpatchset", sc)
         .to(sc.reviewer, newReviewer)
@@ -1960,28 +1853,11 @@
         .bcc(sc.starrer)
         .bcc(NEW_PATCHSETS)
         .noOneElse();
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void newPatchSetOnWipChangeAddingReviewerNotifyAllInReviewDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isFalse();
-    StagedChange sc = stageWipChange();
-    TestAccount newReviewer = sc.testAccount("newReviewer");
-    pushTo(sc, "refs/for/master%notify=ALL,r=" + newReviewer.username, sc.owner);
-    assertThat(sender)
-        .sent("newpatchset", sc)
-        .to(sc.reviewer, sc.ccer, newReviewer)
-        .cc(sc.reviewerByEmail, sc.ccerByEmail)
-        .bcc(sc.starrer)
-        .bcc(NEW_PATCHSETS)
-        .noOneElse();
-    assertThat(sender).notSent();
-  }
-
-  @Test
-  public void newPatchSetOnWipChangeSettingReadyInNoteDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
+  public void newPatchSetOnWipChangeSettingReady() throws Exception {
     StagedChange sc = stageWipChange();
     pushTo(sc, "refs/for/master%ready", sc.owner);
     assertThat(sender)
@@ -1992,22 +1868,7 @@
         .bcc(sc.starrer)
         .bcc(NEW_PATCHSETS)
         .noOneElse();
-    assertThat(sender).notSent();
-  }
-
-  @Test
-  public void newPatchSetOnWipChangeSettingReadyInReviewDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isFalse();
-    StagedChange sc = stageWipChange();
-    pushTo(sc, "refs/for/master%ready", sc.owner);
-    assertThat(sender)
-        .sent("newpatchset", sc)
-        .to(sc.reviewer, sc.ccer)
-        .cc(sc.reviewerByEmail, sc.ccerByEmail)
-        .bcc(sc.starrer)
-        .bcc(NEW_PATCHSETS)
-        .noOneElse();
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   private void pushTo(StagedChange sc, String ref, TestAccount by) throws Exception {
@@ -2017,12 +1878,11 @@
   private void pushTo(StagedChange sc, String ref, TestAccount by, EmailStrategy emailStrategy)
       throws Exception {
     setEmailStrategy(by, emailStrategy);
-    pushFactory.create(db, by.getIdent(), sc.repo, sc.changeId).to(ref).assertOkStatus();
+    pushFactory.create(by.newIdent(), sc.repo, sc.changeId).to(ref).assertOkStatus();
   }
 
   @Test
-  public void editCommitMessageEditByOwnerOnReviewableChangeInNoteDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
+  public void editCommitMessageEditByOwnerOnReviewableChange() throws Exception {
     StagedChange sc = stageReviewableChange();
     editCommitMessage(sc, sc.owner);
     assertThat(sender)
@@ -2033,24 +1893,11 @@
         .bcc(sc.starrer)
         .bcc(NEW_PATCHSETS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void editCommitMessageEditByOwnerOnReviewableChangeInReviewDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isFalse();
-    StagedChange sc = stageReviewableChange();
-    editCommitMessage(sc, sc.owner);
-    assertThat(sender)
-        .sent("newpatchset", sc)
-        .to(sc.reviewer, sc.ccer)
-        .bcc(sc.starrer)
-        .bcc(NEW_PATCHSETS)
-        .noOneElse();
-  }
-
-  @Test
-  public void editCommitMessageEditByOtherOnReviewableChangeInNoteDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
+  public void editCommitMessageEditByOtherOnReviewableChange() throws Exception {
     StagedChange sc = stageReviewableChange();
     editCommitMessage(sc, other);
     assertThat(sender)
@@ -2061,24 +1908,11 @@
         .bcc(sc.starrer)
         .bcc(NEW_PATCHSETS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void editCommitMessageEditByOtherOnReviewableChangeInReviewDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isFalse();
-    StagedChange sc = stageReviewableChange();
-    editCommitMessage(sc, other);
-    assertThat(sender)
-        .sent("newpatchset", sc)
-        .to(sc.owner, sc.reviewer, sc.ccer)
-        .bcc(sc.starrer)
-        .bcc(NEW_PATCHSETS)
-        .noOneElse();
-  }
-
-  @Test
-  public void editCommitMessageByOtherOnReviewableChangeOwnerSelfCcInNoteDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
+  public void editCommitMessageByOtherOnReviewableChangeOwnerSelfCc() throws Exception {
     StagedChange sc = stageReviewableChange();
     editCommitMessage(sc, other, CC_ON_OWN_COMMENTS);
     assertThat(sender)
@@ -2089,25 +1923,11 @@
         .bcc(sc.starrer)
         .bcc(NEW_PATCHSETS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void editCommitMessageByOtherOnReviewableChangeOwnerSelfCcInReviewDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isFalse();
-    StagedChange sc = stageReviewableChange();
-    editCommitMessage(sc, other, CC_ON_OWN_COMMENTS);
-    assertThat(sender)
-        .sent("newpatchset", sc)
-        .to(sc.owner, sc.reviewer, sc.ccer, other)
-        .bcc(sc.starrer)
-        .bcc(NEW_PATCHSETS)
-        .noOneElse();
-  }
-
-  @Test
-  public void editCommitMessageByOtherOnReviewableChangeNotifyOwnerReviewersInNoteDb()
-      throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
+  public void editCommitMessageByOtherOnReviewableChangeNotifyOwnerReviewers() throws Exception {
     StagedChange sc = stageReviewableChange();
     editCommitMessage(sc, other, OWNER_REVIEWERS);
     assertThat(sender)
@@ -2116,21 +1936,12 @@
         .cc(sc.ccer)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void editCommitMessageByOtherOnReviewableChangeNotifyOwnerReviewersInReviewDb()
+  public void editCommitMessageByOtherOnReviewableChangeOwnerSelfCcNotifyOwnerReviewers()
       throws Exception {
-    assume().that(notesMigration.readChanges()).isFalse();
-    StagedChange sc = stageReviewableChange();
-    editCommitMessage(sc, other, OWNER_REVIEWERS);
-    assertThat(sender).sent("newpatchset", sc).to(sc.owner, sc.reviewer, sc.ccer).noOneElse();
-  }
-
-  @Test
-  public void editCommitMessageByOtherOnReviewableChangeOwnerSelfCcNotifyOwnerReviewersInNoteDb()
-      throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
     StagedChange sc = stageReviewableChange();
     editCommitMessage(sc, other, OWNER_REVIEWERS, CC_ON_OWN_COMMENTS);
     assertThat(sender)
@@ -2139,19 +1950,7 @@
         .cc(sc.ccer, other)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
-  }
-
-  @Test
-  public void editCommitMessageByOtherOnReviewableChangeOwnerSelfCcNotifyOwnerReviewersInReviewDb()
-      throws Exception {
-    assume().that(notesMigration.readChanges()).isFalse();
-    StagedChange sc = stageReviewableChange();
-    editCommitMessage(sc, other, OWNER_REVIEWERS, CC_ON_OWN_COMMENTS);
-    assertThat(sender)
-        .sent("newpatchset", sc)
-        .to(sc.owner, sc.reviewer, sc.ccer)
-        .cc(other)
-        .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2159,6 +1958,7 @@
     StagedChange sc = stageReviewableChange();
     editCommitMessage(sc, other, OWNER);
     assertThat(sender).sent("newpatchset", sc).to(sc.owner).noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2166,27 +1966,28 @@
     StagedChange sc = stageReviewableChange();
     editCommitMessage(sc, other, OWNER, CC_ON_OWN_COMMENTS);
     assertThat(sender).sent("newpatchset", sc).to(sc.owner).cc(other).noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void editCommitMessageByOtherOnReviewableChangeNotifyNone() throws Exception {
     StagedChange sc = stageReviewableChange();
     editCommitMessage(sc, other, NONE);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void editCommitMessageByOtherOnReviewableChangeOwnerSelfCcNotifyNone() throws Exception {
     StagedChange sc = stageReviewableChange();
     editCommitMessage(sc, other, NONE, CC_ON_OWN_COMMENTS);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void editCommitMessageOnWipChange() throws Exception {
     StagedChange sc = stageWipChange();
     editCommitMessage(sc, sc.owner);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2194,6 +1995,7 @@
     StagedChange sc = stageWipChange();
     editCommitMessage(sc, other);
     assertThat(sender).sent("newpatchset", sc).to(sc.owner).noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2201,11 +2003,11 @@
     StagedChange sc = stageWipChange();
     editCommitMessage(sc, other, CC_ON_OWN_COMMENTS);
     assertThat(sender).sent("newpatchset", sc).to(sc.owner).cc(other).noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void editCommitMessageOnWipChangeNotifyAllInNoteDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
+  public void editCommitMessageOnWipChangeNotifyAll() throws Exception {
     StagedChange sc = stageWipChange();
     editCommitMessage(sc, sc.owner, ALL);
     assertThat(sender)
@@ -2216,19 +2018,7 @@
         .bcc(sc.starrer)
         .bcc(NEW_PATCHSETS)
         .noOneElse();
-  }
-
-  @Test
-  public void editCommitMessageOnWipChangeNotifyAllInReviewDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isFalse();
-    StagedChange sc = stageWipChange();
-    editCommitMessage(sc, sc.owner, ALL);
-    assertThat(sender)
-        .sent("newpatchset", sc)
-        .to(sc.reviewer, sc.ccer)
-        .bcc(sc.starrer)
-        .bcc(NEW_PATCHSETS)
-        .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   private void editCommitMessage(StagedChange sc, TestAccount by) throws Exception {
@@ -2271,6 +2061,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2284,6 +2075,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2297,6 +2089,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2311,6 +2104,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2325,6 +2119,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2339,6 +2134,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   private void restore(String changeId, TestAccount by) throws Exception {
@@ -2348,7 +2144,7 @@
   private void restore(String changeId, TestAccount by, EmailStrategy emailStrategy)
       throws Exception {
     setEmailStrategy(by, emailStrategy);
-    setApiUser(by);
+    requestScopeOperations.setApiUser(by.id());
     gApi.changes().id(changeId).restore();
   }
 
@@ -2357,31 +2153,7 @@
    */
 
   @Test
-  public void revertChangeByOwnerInReviewDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isFalse();
-    StagedChange sc = stageChange();
-    revert(sc, sc.owner);
-
-    // email for the newly created revert change
-    assertThat(sender)
-        .sent("newchange", sc)
-        .to(sc.reviewer, sc.ccer, sc.watchingProjectOwner, admin)
-        .bcc(NEW_CHANGES, NEW_PATCHSETS)
-        .noOneElse();
-
-    // email for the change that is reverted
-    assertThat(sender)
-        .sent("revert", sc)
-        .cc(sc.reviewer, sc.ccer, admin)
-        .cc(sc.reviewerByEmail)
-        .bcc(sc.starrer)
-        .bcc(ALL_COMMENTS)
-        .noOneElse();
-  }
-
-  @Test
-  public void revertChangeByOwnerInNoteDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
+  public void revertChangeByOwner() throws Exception {
     StagedChange sc = stageChange();
     revert(sc, sc.owner);
 
@@ -2401,36 +2173,11 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void revertChangeByOwnerCcingSelfInReviewDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isFalse();
-    StagedChange sc = stageChange();
-    revert(sc, sc.owner, CC_ON_OWN_COMMENTS);
-
-    // email for the newly created revert change
-    assertThat(sender)
-        .sent("newchange", sc)
-        .to(sc.reviewer, sc.ccer, sc.watchingProjectOwner, admin)
-        .cc(sc.owner)
-        .bcc(NEW_CHANGES, NEW_PATCHSETS)
-        .noOneElse();
-
-    // email for the change that is reverted
-    assertThat(sender)
-        .sent("revert", sc)
-        .to(sc.owner)
-        .cc(sc.reviewer, sc.ccer, admin)
-        .cc(sc.reviewerByEmail)
-        .bcc(sc.starrer)
-        .bcc(ALL_COMMENTS)
-        .noOneElse();
-  }
-
-  @Test
-  public void revertChangeByOwnerCcingSelfInNoteDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
+  public void revertChangeByOwnerCcingSelf() throws Exception {
     StagedChange sc = stageChange();
     revert(sc, sc.owner, CC_ON_OWN_COMMENTS);
 
@@ -2451,35 +2198,11 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void revertChangeByOtherInReviewDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isFalse();
-    StagedChange sc = stageChange();
-    revert(sc, other);
-
-    // email for the newly created revert change
-    assertThat(sender)
-        .sent("newchange", sc)
-        .to(sc.owner, sc.reviewer, sc.ccer, sc.watchingProjectOwner, admin)
-        .bcc(NEW_CHANGES, NEW_PATCHSETS)
-        .noOneElse();
-
-    // email for the change that is reverted
-    assertThat(sender)
-        .sent("revert", sc)
-        .to(sc.owner)
-        .cc(sc.reviewer, sc.ccer, admin)
-        .cc(sc.reviewerByEmail)
-        .bcc(sc.starrer)
-        .bcc(ALL_COMMENTS)
-        .noOneElse();
-  }
-
-  @Test
-  public void revertChangeByOtherInNoteDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
+  public void revertChangeByOther() throws Exception {
     StagedChange sc = stageChange();
     revert(sc, other);
 
@@ -2500,36 +2223,11 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void revertChangeByOtherCcingSelfInReviewDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isFalse();
-    StagedChange sc = stageChange();
-    revert(sc, other, CC_ON_OWN_COMMENTS);
-
-    // email for the newly created revert change
-    assertThat(sender)
-        .sent("newchange", sc)
-        .to(sc.owner, sc.reviewer, sc.ccer, sc.watchingProjectOwner, admin)
-        .cc(other)
-        .bcc(NEW_CHANGES, NEW_PATCHSETS)
-        .noOneElse();
-
-    // email for the change that is reverted
-    assertThat(sender)
-        .sent("revert", sc)
-        .to(sc.owner)
-        .cc(other, sc.reviewer, sc.ccer, admin)
-        .cc(sc.reviewerByEmail)
-        .bcc(sc.starrer)
-        .bcc(ALL_COMMENTS)
-        .noOneElse();
-  }
-
-  @Test
-  public void revertChangeByOtherCcingSelfInNoteDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
+  public void revertChangeByOtherCcingSelf() throws Exception {
     StagedChange sc = stageChange();
     revert(sc, other, CC_ON_OWN_COMMENTS);
 
@@ -2550,11 +2248,12 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   private StagedChange stageChange() throws Exception {
     StagedChange sc = stageReviewableChange();
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     gApi.changes().id(sc.changeId).revision("current").review(ReviewInput.approve());
     gApi.changes().id(sc.changeId).revision("current").submit();
     sender.clear();
@@ -2568,7 +2267,7 @@
   private void revert(StagedChange sc, TestAccount by, EmailStrategy emailStrategy)
       throws Exception {
     setEmailStrategy(by, emailStrategy);
-    setApiUser(by);
+    requestScopeOperations.setApiUser(by.id());
     gApi.changes().id(sc.changeId).revert();
   }
 
@@ -2585,6 +2284,7 @@
         .cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended!
         .to(sc.assignee)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2597,6 +2297,7 @@
         .cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended!
         .to(sc.assignee)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2608,6 +2309,7 @@
         .cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended!
         .to(sc.assignee)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2620,25 +2322,18 @@
         .cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended!
         .to(sc.assignee)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void setAssigneeToSelfOnReviewableChangeInNoteDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
+  public void setAssigneeToSelfOnReviewableChange() throws Exception {
     StagedChange sc = stageReviewableChange();
     assign(sc, sc.owner, sc.owner);
     assertThat(sender)
         .sent("setassignee", sc)
         .cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended!
         .noOneElse();
-  }
-
-  @Test
-  public void setAssigneeToSelfOnReviewableChangeInReviewDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isFalse();
-    StagedChange sc = stageReviewableChange();
-    assign(sc, sc.owner, sc.owner);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2653,11 +2348,11 @@
         .cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended!
         .to(sc.assignee)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void changeAssigneeToSelfOnReviewableChangeInNoteDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
+  public void changeAssigneeToSelfOnReviewableChange() throws Exception {
     StagedChange sc = stageReviewableChange();
     assign(sc, sc.owner, sc.assignee);
     sender.clear();
@@ -2666,16 +2361,7 @@
         .sent("setassignee", sc)
         .cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended!
         .noOneElse();
-  }
-
-  @Test
-  public void changeAssigneeToSelfOnReviewableChangeInReviewDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isFalse();
-    StagedChange sc = stageReviewableChange();
-    assign(sc, sc.owner, sc.assignee);
-    sender.clear();
-    assign(sc, sc.owner, sc.owner);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2687,6 +2373,7 @@
         .cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended!
         .to(sc.assignee)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2698,6 +2385,7 @@
         .cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended!
         .to(sc.assignee)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   private void assign(StagedChange sc, TestAccount by, TestAccount to) throws Exception {
@@ -2707,9 +2395,9 @@
   private void assign(StagedChange sc, TestAccount by, TestAccount to, EmailStrategy emailStrategy)
       throws Exception {
     setEmailStrategy(by, emailStrategy);
-    setApiUser(by);
+    requestScopeOperations.setApiUser(by.id());
     AssigneeInput in = new AssigneeInput();
-    in.assignee = to.email;
+    in.assignee = to.email();
     gApi.changes().id(sc.changeId).setAssignee(in);
   }
 
@@ -2728,6 +2416,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2743,20 +2432,19 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void setWorkInProgress() throws Exception {
     StagedChange sc = stageReviewableChange();
     gApi.changes().id(sc.changeId).setWorkInProgress();
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   private void startReview(StagedChange sc) throws Exception {
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.id());
     gApi.changes().id(sc.changeId).setReadyForReview();
-    // PolyGerrit current immediately follows up with a review.
-    gApi.changes().id(sc.changeId).revision("current").review(ReviewInput.noScore());
   }
 
   private void setWorkInProgressByDefault(Project.NameKey p, InheritableBoolean v)
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
index a0170d2..1386aec 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.mail.EmailHeader;
@@ -27,6 +28,7 @@
 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;
 import java.time.ZonedDateTime;
@@ -41,6 +43,8 @@
 
 /** Tests the presence of required metadata in email headers, text and html. */
 public class MailMetadataIT extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   private String systemTimeZone;
 
   @Before
@@ -58,7 +62,7 @@
   @Test
   public void metadataOnNewChange() throws Exception {
     PushOneCommit.Result newChange = createChange();
-    gApi.changes().id(newChange.getChangeId()).addReviewer(user.getId().toString());
+    gApi.changes().id(newChange.getChangeId()).addReviewer(user.id().toString());
 
     List<FakeEmailSender.Message> emails = sender.getMessages();
     assertThat(emails).hasSize(1);
@@ -85,14 +89,14 @@
   @Test
   public void metadataOnNewComment() throws Exception {
     PushOneCommit.Result newChange = createChange();
-    gApi.changes().id(newChange.getChangeId()).addReviewer(user.getId().toString());
+    gApi.changes().id(newChange.getChangeId()).addReviewer(user.id().toString());
     sender.clear();
 
     // Review change
     ReviewInput input = new ReviewInput();
     input.message = "Test";
     revision(newChange).review(input);
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     Collection<ChangeMessageInfo> result =
         gApi.changes().id(newChange.getChangeId()).get().messages;
     assertThat(result).isNotEmpty();
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
index 9ff2c05..f917fd8 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.common.CommentInfo;
@@ -33,6 +34,7 @@
 
 public class MailProcessorIT extends AbstractMailIT {
   @Inject private MailProcessor mailProcessor;
+  @Inject private AccountOperations accountOperations;
 
   @Test
   public void parseAndPersistChangeMessage() throws Exception {
@@ -163,16 +165,13 @@
     b.textContent(txt + textFooterForChange(changeInfo._number, ts));
 
     // Set account state to inactive
-    gApi.accounts().id("user").setActive(false);
+    accountOperations.account(user.id()).forUpdate().inactive().update();
 
     mailProcessor.process(b.build());
     comments = gApi.changes().id(changeId).current().commentsAsList();
 
     // Check that comment size has not changed
     assertThat(comments).hasSize(2);
-
-    // Reset
-    gApi.accounts().id("user").setActive(true);
   }
 
   @Test
@@ -190,7 +189,7 @@
         newPlaintextBody(getChangeUrl(changeInfo) + "/1", "Test Message", null, null, null);
     MailMessage.Builder b =
         messageBuilderWithDefaultFields()
-            .from(user.emailAddress)
+            .from(user.getEmailAddress())
             .textContent(txt + textFooterForChange(changeInfo._number, ts));
 
     sender.clear();
@@ -212,7 +211,7 @@
         newPlaintextBody(getChangeUrl(changeInfo) + "/1", "Test Message", null, null, null);
     MailMessage.Builder b =
         messageBuilderWithDefaultFields()
-            .from(user.emailAddress)
+            .from(user.getEmailAddress())
             .textContent(txt + textFooterForChange(changeInfo._number, ts));
 
     sender.clear();
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailSenderIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailSenderIT.java
index 8d21b5b..c395c81 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailSenderIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailSenderIT.java
@@ -41,7 +41,7 @@
     // Check that the user's email was added as Reply-To
     assertThat(sender.getMessages()).hasSize(1);
     Map<String, EmailHeader> headers = sender.getMessages().iterator().next().headers();
-    assertThat(headerString(headers, "Reply-To")).contains(user.email);
+    assertThat(headerString(headers, "Reply-To")).contains(user.email());
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/NotificationMailFormatIT.java b/javatests/com/google/gerrit/acceptance/server/mail/NotificationMailFormatIT.java
index c8292ba..628b90c 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/NotificationMailFormatIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/NotificationMailFormatIT.java
@@ -18,24 +18,27 @@
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailFormat;
 import com.google.gerrit.testing.FakeEmailSender;
+import com.google.inject.Inject;
 import org.junit.Test;
 
 public class NotificationMailFormatIT extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
   public void userReceivesPlaintextEmail() throws Exception {
     // Set user preference to receive only plaintext content
     GeneralPreferencesInfo i = new GeneralPreferencesInfo();
     i.emailFormat = EmailFormat.PLAINTEXT;
-    gApi.accounts().id(admin.getId().toString()).setPreferences(i);
+    gApi.accounts().id(admin.id().toString()).setPreferences(i);
 
     // Create change as admin and review as user
     PushOneCommit.Result r = createChange();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(r.getChangeId()).current().review(ReviewInput.recommend());
 
     // Check that admin has received only plaintext content
@@ -43,20 +46,20 @@
     FakeEmailSender.Message m = sender.getMessages().get(0);
     assertThat(m.body()).isNotNull();
     assertThat(m.htmlBody()).isNull();
-    assertMailReplyTo(m, admin.email);
-    assertMailReplyTo(m, user.email);
+    assertMailReplyTo(m, admin.email());
+    assertMailReplyTo(m, user.email());
 
     // Reset user preference
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     i.emailFormat = EmailFormat.HTML_PLAINTEXT;
-    gApi.accounts().id(admin.getId().toString()).setPreferences(i);
+    gApi.accounts().id(admin.id().toString()).setPreferences(i);
   }
 
   @Test
   public void userReceivesHtmlAndPlaintextEmail() throws Exception {
     // Create change as admin and review as user
     PushOneCommit.Result r = createChange();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(r.getChangeId()).current().review(ReviewInput.recommend());
 
     // Check that admin has received both HTML and plaintext content
@@ -64,7 +67,7 @@
     FakeEmailSender.Message m = sender.getMessages().get(0);
     assertThat(m.body()).isNotNull();
     assertThat(m.htmlBody()).isNotNull();
-    assertMailReplyTo(m, admin.email);
-    assertMailReplyTo(m, user.email);
+    assertMailReplyTo(m, admin.email());
+    assertMailReplyTo(m, user.email());
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java b/javatests/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
deleted file mode 100644
index 5ebfa9e..0000000
--- a/javatests/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
+++ /dev/null
@@ -1,1594 +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.acceptance.server.notedb;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
-import static com.google.common.truth.TruthJUnit.assume;
-import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
-import static com.google.gerrit.reviewdb.client.RefNames.refsDraftComments;
-import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.concurrent.TimeUnit.DAYS;
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
-import static java.util.concurrent.TimeUnit.SECONDS;
-import static java.util.stream.Collectors.joining;
-import static java.util.stream.Collectors.toList;
-import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Ordering;
-import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
-import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.acceptance.TestAccount;
-import com.google.gerrit.common.data.GlobalCapability;
-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.CommentInfo;
-import com.google.gerrit.extensions.common.Input;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
-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.Patch;
-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.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.reviewdb.server.ReviewDbUtil;
-import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.CommentsUtil;
-import com.google.gerrit.server.Sequences;
-import com.google.gerrit.server.change.RevisionResource;
-import com.google.gerrit.server.git.RepoRefCache;
-import com.google.gerrit.server.notedb.ChangeBundle;
-import com.google.gerrit.server.notedb.ChangeBundleReader;
-import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.notedb.NoteDbChangeState;
-import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
-import com.google.gerrit.server.notedb.NoteDbUpdateManager;
-import com.google.gerrit.server.notedb.TestChangeRebuilderWrapper;
-import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder.NoPatchSetsException;
-import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gerrit.server.patch.PatchListNotAvailableException;
-import com.google.gerrit.server.patch.PatchSetInfoFactory;
-import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
-import com.google.gerrit.server.project.testing.Util;
-import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.restapi.change.PostReview;
-import com.google.gerrit.server.restapi.change.Rebuild;
-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.UpdateException;
-import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gerrit.testing.ConfigSuite;
-import com.google.gerrit.testing.NoteDbChecker;
-import com.google.gerrit.testing.NoteDbMode;
-import com.google.gerrit.testing.TestChanges;
-import com.google.gerrit.testing.TestTimeUtil;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.OrmRuntimeException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.sql.Timestamp;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.concurrent.TimeUnit;
-import java.util.stream.Stream;
-import org.apache.http.Header;
-import org.apache.http.message.BasicHeader;
-import org.eclipse.jgit.junit.TestRepository;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.Repository;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-public class ChangeRebuilderIT extends AbstractDaemonTest {
-  @ConfigSuite.Default
-  public static Config defaultConfig() {
-    Config cfg = new Config();
-    cfg.setBoolean("noteDb", null, "testRebuilderWrapper", true);
-
-    // Disable async reindex-if-stale check after index update. This avoids
-    // unintentional auto-rebuilding of the change in NoteDb during the read
-    // path of the reindex-if-stale check. For the purposes of this test, we
-    // want precise control over when auto-rebuilding happens.
-    cfg.setBoolean("index", null, "autoReindexIfStale", false);
-
-    // setNotesMigration tries to keep IDs in sync between ReviewDb and NoteDb, which is behavior
-    // unique to this test. This gets prohibitively slow if we use the default sequence gap.
-    cfg.setInt("noteDb", "changes", "initialSequenceGap", 0);
-
-    return cfg;
-  }
-
-  @Inject private NoteDbChecker checker;
-
-  @Inject private Rebuild rebuildHandler;
-
-  @Inject private Provider<ReviewDb> dbProvider;
-
-  @Inject private CommentsUtil commentsUtil;
-
-  @Inject private Provider<PostReview> postReview;
-
-  @Inject private TestChangeRebuilderWrapper rebuilderWrapper;
-
-  @Inject private Sequences seq;
-
-  @Inject private ChangeBundleReader bundleReader;
-
-  @Inject private PatchSetInfoFactory patchSetInfoFactory;
-
-  @Inject private PatchListCache patchListCache;
-
-  @Before
-  public void setUp() throws Exception {
-    assume().that(NoteDbMode.get()).isEqualTo(NoteDbMode.OFF);
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
-    setNotesMigration(false, false);
-  }
-
-  @After
-  public void tearDown() {
-    TestTimeUtil.useSystemTime();
-  }
-
-  @SuppressWarnings("deprecation")
-  private void setNotesMigration(boolean writeChanges, boolean readChanges) throws Exception {
-    notesMigration.setWriteChanges(writeChanges);
-    notesMigration.setReadChanges(readChanges);
-    db = atrScope.reopenDb().getReviewDbProvider().get();
-
-    if (notesMigration.readChangeSequence()) {
-      // Copy next ReviewDb ID to NoteDb.
-      seq.getChangeIdRepoSequence().set(db.nextChangeId());
-    } else {
-      // Copy next NoteDb ID to ReviewDb.
-      while (db.nextChangeId() < seq.getChangeIdRepoSequence().next()) {}
-    }
-  }
-
-  @Test
-  public void changeFields() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getPatchSetId().getParentKey();
-    gApi.changes().id(id.get()).topic(name("a-topic"));
-    checker.rebuildAndCheckChanges(id);
-  }
-
-  @Test
-  public void patchSets() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getPatchSetId().getParentKey();
-    amendChange(r.getChangeId());
-    checker.rebuildAndCheckChanges(id);
-  }
-
-  @Test
-  public void publishedComment() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getPatchSetId().getParentKey();
-    putComment(user, id, 1, "comment", null);
-    checker.rebuildAndCheckChanges(id);
-  }
-
-  @Test
-  public void publishedCommentAndReply() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getPatchSetId().getParentKey();
-    putComment(user, id, 1, "comment", null);
-    Map<String, List<CommentInfo>> comments = getPublishedComments(id);
-    String parentUuid = comments.get("a.txt").get(0).id;
-    putComment(user, id, 1, "comment", parentUuid);
-    checker.rebuildAndCheckChanges(id);
-  }
-
-  @Test
-  public void patchSetWithNullGroups() throws Exception {
-    Timestamp ts = TimeUtil.nowTs();
-    Change c = TestChanges.newChange(project, user.getId(), seq.nextChangeId());
-    c.setCreatedOn(ts);
-    c.setLastUpdatedOn(ts);
-    c.setReviewStarted(true);
-    PatchSet ps =
-        TestChanges.newPatchSet(
-            c.currentPatchSetId(), "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef", user.getId());
-    ps.setCreatedOn(ts);
-    db.changes().insert(Collections.singleton(c));
-    db.patchSets().insert(Collections.singleton(ps));
-
-    assertThat(ps.getGroups()).isEmpty();
-    checker.rebuildAndCheckChanges(c.getId());
-  }
-
-  @Test
-  public void draftComment() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getPatchSetId().getParentKey();
-    putDraft(user, id, 1, "comment", null);
-    checker.rebuildAndCheckChanges(id);
-  }
-
-  @Test
-  public void draftAndPublishedComment() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getPatchSetId().getParentKey();
-    putDraft(user, id, 1, "draft comment", null);
-    putComment(user, id, 1, "published comment", null);
-    checker.rebuildAndCheckChanges(id);
-  }
-
-  @Test
-  public void publishDraftComment() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getPatchSetId().getParentKey();
-    putDraft(user, id, 1, "draft comment", null);
-    publishDrafts(user, id);
-    checker.rebuildAndCheckChanges(id);
-  }
-
-  @Test
-  public void nullAccountId() throws Exception {
-    PushOneCommit.Result r = createChange();
-    PatchSet.Id psId = r.getPatchSetId();
-    Change.Id id = psId.getParentKey();
-
-    // Events need to be otherwise identical for the account ID to be compared.
-    ChangeMessage msg1 = insertMessage(id, psId, user.getId(), TimeUtil.nowTs(), "message 1");
-    insertMessage(id, psId, null, msg1.getWrittenOn(), "message 2");
-
-    checker.rebuildAndCheckChanges(id);
-  }
-
-  @Test
-  public void nullPatchSetId() throws Exception {
-    PushOneCommit.Result r = createChange();
-    PatchSet.Id psId1 = r.getPatchSetId();
-    Change.Id id = psId1.getParentKey();
-
-    // Events need to be otherwise identical for the PatchSet.ID to be compared.
-    ChangeMessage msg1 = insertMessage(id, null, user.getId(), TimeUtil.nowTs(), "message 1");
-    insertMessage(id, null, user.getId(), msg1.getWrittenOn(), "message 2");
-
-    PatchSet.Id psId2 = amendChange(r.getChangeId()).getPatchSetId();
-
-    ChangeMessage msg3 = insertMessage(id, null, user.getId(), TimeUtil.nowTs(), "message 3");
-    insertMessage(id, null, user.getId(), msg3.getWrittenOn(), "message 4");
-
-    checker.rebuildAndCheckChanges(id);
-
-    setNotesMigration(true, true);
-
-    ChangeNotes notes = notesFactory.create(db, project, id);
-    Map<String, PatchSet.Id> psIds = new HashMap<>();
-    for (ChangeMessage msg : notes.getChangeMessages()) {
-      PatchSet.Id psId = msg.getPatchSetId();
-      assertThat(psId).named("patchset for " + msg).isNotNull();
-      psIds.put(msg.getMessage(), psId);
-    }
-    // Patch set IDs were replaced during conversion process.
-    assertThat(psIds).containsEntry("message 1", psId1);
-    assertThat(psIds).containsEntry("message 2", psId1);
-    assertThat(psIds).containsEntry("message 3", psId2);
-    assertThat(psIds).containsEntry("message 4", psId2);
-  }
-
-  @Test
-  public void noWriteToNewRef() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getPatchSetId().getParentKey();
-    checker.assertNoChangeRef(project, id);
-
-    setNotesMigration(true, false);
-    gApi.changes().id(id.get()).topic(name("a-topic"));
-
-    // First write doesn't create the ref, but rebuilding works.
-    checker.assertNoChangeRef(project, id);
-    assertThat(getUnwrappedDb().changes().get(id).getNoteDbState()).isNull();
-    checker.rebuildAndCheckChanges(id);
-
-    // Now that there is a ref, writes are "turned on" for this change, and
-    // NoteDb stays up to date without explicit rebuilding.
-    gApi.changes().id(id.get()).topic(name("new-topic"));
-    assertThat(getUnwrappedDb().changes().get(id).getNoteDbState()).isNotNull();
-    checker.checkChanges(id);
-  }
-
-  @Test
-  public void restApiNotFoundWhenNoteDbDisabled() throws Exception {
-    PushOneCommit.Result r = createChange();
-    exception.expect(ResourceNotFoundException.class);
-    rebuildHandler.apply(parseChangeResource(r.getChangeId()), new Input());
-  }
-
-  @Test
-  public void rebuildViaRestApi() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getPatchSetId().getParentKey();
-    setNotesMigration(true, false);
-
-    checker.assertNoChangeRef(project, id);
-    rebuildHandler.apply(parseChangeResource(r.getChangeId()), new Input());
-    checker.checkChanges(id);
-  }
-
-  @Test
-  public void writeToNewRefForNewChange() throws Exception {
-    PushOneCommit.Result r1 = createChange();
-    Change.Id id1 = r1.getPatchSetId().getParentKey();
-
-    setNotesMigration(true, false);
-    gApi.changes().id(id1.get()).topic(name("a-topic"));
-    PushOneCommit.Result r2 = createChange();
-    Change.Id id2 = r2.getPatchSetId().getParentKey();
-
-    // Second change was created after NoteDb writes were turned on, so it was
-    // allowed to write to a new ref.
-    checker.checkChanges(id2);
-
-    // First change was created before NoteDb writes were turned on, so its meta
-    // ref doesn't exist until a manual rebuild.
-    checker.assertNoChangeRef(project, id1);
-    checker.rebuildAndCheckChanges(id1);
-  }
-
-  @Test
-  public void noteDbChangeState() throws Exception {
-    setNotesMigration(true, true);
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getPatchSetId().getParentKey();
-
-    ObjectId changeMetaId = getMetaRef(project, changeMetaRef(id));
-    assertThat(getUnwrappedDb().changes().get(id).getNoteDbState()).isEqualTo(changeMetaId.name());
-
-    putDraft(user, id, 1, "comment by user", null);
-    ObjectId userDraftsId = getMetaRef(allUsers, refsDraftComments(id, user.getId()));
-    assertThat(getUnwrappedDb().changes().get(id).getNoteDbState())
-        .isEqualTo(changeMetaId.name() + "," + user.getId() + "=" + userDraftsId.name());
-
-    putDraft(admin, id, 2, "comment by admin", null);
-    ObjectId adminDraftsId = getMetaRef(allUsers, refsDraftComments(id, admin.getId()));
-    assertThat(admin.getId().get()).isLessThan(user.getId().get());
-    assertThat(getUnwrappedDb().changes().get(id).getNoteDbState())
-        .isEqualTo(
-            changeMetaId.name()
-                + ","
-                + admin.getId()
-                + "="
-                + adminDraftsId.name()
-                + ","
-                + user.getId()
-                + "="
-                + userDraftsId.name());
-
-    putDraft(admin, id, 2, "revised comment by admin", null);
-    adminDraftsId = getMetaRef(allUsers, refsDraftComments(id, admin.getId()));
-    assertThat(getUnwrappedDb().changes().get(id).getNoteDbState())
-        .isEqualTo(
-            changeMetaId.name()
-                + ","
-                + admin.getId()
-                + "="
-                + adminDraftsId.name()
-                + ","
-                + user.getId()
-                + "="
-                + userDraftsId.name());
-  }
-
-  @Test
-  public void rebuildAutomaticallyWhenChangeOutOfDate() throws Exception {
-    setNotesMigration(true, true);
-
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getPatchSetId().getParentKey();
-    assertChangeUpToDate(true, id);
-
-    // Make a ReviewDb change behind NoteDb's back and ensure it's detected.
-    setNotesMigration(false, false);
-    gApi.changes().id(id.get()).topic(name("a-topic"));
-    setInvalidNoteDbState(id);
-    assertChangeUpToDate(false, id);
-
-    // On next NoteDb read, the change is transparently rebuilt.
-    setNotesMigration(true, true);
-    assertThat(gApi.changes().id(id.get()).info().topic).isEqualTo(name("a-topic"));
-    assertChangeUpToDate(true, id);
-
-    // Check that the bundles are equal.
-    ChangeBundle actual =
-        ChangeBundle.fromNotes(commentsUtil, notesFactory.create(dbProvider.get(), project, id));
-    ChangeBundle expected = bundleReader.fromReviewDb(getUnwrappedDb(), id);
-    assertThat(actual.differencesFrom(expected)).isEmpty();
-  }
-
-  @Test
-  public void rebuildAutomaticallyWithinBatchUpdate() throws Exception {
-    setNotesMigration(true, true);
-
-    PushOneCommit.Result r = createChange();
-    final Change.Id id = r.getPatchSetId().getParentKey();
-    assertChangeUpToDate(true, id);
-
-    // Update ReviewDb and NoteDb, then revert the corresponding NoteDb change
-    // to simulate it failing.
-    NoteDbChangeState oldState = NoteDbChangeState.parse(getUnwrappedDb().changes().get(id));
-    String topic = name("a-topic");
-    gApi.changes().id(id.get()).topic(topic);
-    try (Repository repo = repoManager.openRepository(project)) {
-      new TestRepository<>(repo).update(RefNames.changeMetaRef(id), oldState.getChangeMetaId());
-    }
-    assertChangeUpToDate(false, id);
-
-    // Next NoteDb read comes inside the transaction started by BatchUpdate. In
-    // reality this could be caused by a failed update happening between when
-    // the change is parsed by ChangesCollection and when the BatchUpdate
-    // executes. We simulate it here by using BatchUpdate directly and not going
-    // through an API handler.
-    final String msg = "message from BatchUpdate";
-    try (BatchUpdate bu =
-        batchUpdateFactory.create(
-            db, project, identifiedUserFactory.create(user.getId()), TimeUtil.nowTs())) {
-      bu.addOp(
-          id,
-          new BatchUpdateOp() {
-            @Override
-            public boolean updateChange(ChangeContext ctx) throws OrmException {
-              PatchSet.Id psId = ctx.getChange().currentPatchSetId();
-              ChangeMessage cm =
-                  new ChangeMessage(
-                      new ChangeMessage.Key(id, ChangeUtil.messageUuid()),
-                      ctx.getAccountId(),
-                      ctx.getWhen(),
-                      psId);
-              cm.setMessage(msg);
-              ctx.getDb().changeMessages().insert(Collections.singleton(cm));
-              ctx.getUpdate(psId).setChangeMessage(msg);
-              return true;
-            }
-          });
-      try {
-        bu.execute();
-        fail("expected update to fail");
-      } catch (UpdateException e) {
-        assertThat(e.getMessage()).contains("cannot copy ChangeNotesState");
-      }
-    }
-
-    // TODO(dborowitz): Re-enable these assertions once we fix auto-rebuilding
-    // in the BatchUpdate path.
-    // As an implementation detail, change wasn't actually rebuilt inside the
-    // BatchUpdate transaction, but it was rebuilt during read for the
-    // subsequent reindex. Thus it's impossible to actually observe an
-    // out-of-date state in the caller.
-    // assertChangeUpToDate(true, id);
-
-    // Check that the bundles are equal.
-    // ChangeNotes notes = notesFactory.create(dbProvider.get(), project, id);
-    // ChangeBundle actual = ChangeBundle.fromNotes(commentsUtil, notes);
-    // ChangeBundle expected = bundleReader.fromReviewDb(getUnwrappedDb(), id);
-    // assertThat(actual.differencesFrom(expected)).isEmpty();
-    // assertThat(
-    //        Iterables.transform(
-    //            notes.getChangeMessages(),
-    //            ChangeMessage::getMessage))
-    //    .contains(msg);
-    // assertThat(actual.getChange().getTopic()).isEqualTo(topic);
-  }
-
-  @Test
-  public void rebuildIgnoresErrorIfChangeIsUpToDateAfter() throws Exception {
-    setNotesMigration(true, true);
-
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getPatchSetId().getParentKey();
-    assertChangeUpToDate(true, id);
-
-    // Make a ReviewDb change behind NoteDb's back and ensure it's detected.
-    setNotesMigration(false, false);
-    gApi.changes().id(id.get()).topic(name("a-topic"));
-    setInvalidNoteDbState(id);
-    assertChangeUpToDate(false, id);
-
-    // Force the next rebuild attempt to fail but also rebuild the change in the
-    // background.
-    rebuilderWrapper.stealNextUpdate();
-    setNotesMigration(true, true);
-    assertThat(gApi.changes().id(id.get()).info().topic).isEqualTo(name("a-topic"));
-    assertChangeUpToDate(true, id);
-
-    // Check that the bundles are equal.
-    ChangeBundle actual =
-        ChangeBundle.fromNotes(commentsUtil, notesFactory.create(dbProvider.get(), project, id));
-    ChangeBundle expected = bundleReader.fromReviewDb(getUnwrappedDb(), id);
-    assertThat(actual.differencesFrom(expected)).isEmpty();
-  }
-
-  @Test
-  public void rebuildReturnsCorrectResultEvenIfSavingToNoteDbFailed() throws Exception {
-    setNotesMigration(true, true);
-
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getPatchSetId().getParentKey();
-    assertChangeUpToDate(true, id);
-    ObjectId oldMetaId = getMetaRef(project, changeMetaRef(id));
-
-    // Make a ReviewDb change behind NoteDb's back.
-    setNotesMigration(false, false);
-    gApi.changes().id(id.get()).topic(name("a-topic"));
-    setInvalidNoteDbState(id);
-    assertChangeUpToDate(false, id);
-    assertThat(getMetaRef(project, changeMetaRef(id))).isEqualTo(oldMetaId);
-
-    // Force the next rebuild attempt to fail.
-    rebuilderWrapper.failNextUpdate();
-    setNotesMigration(true, true);
-    ChangeNotes notes = notesFactory.create(dbProvider.get(), project, id);
-
-    // Not up to date, but the actual returned state matches anyway.
-    assertChangeUpToDate(false, id);
-    assertThat(getMetaRef(project, changeMetaRef(id))).isEqualTo(oldMetaId);
-    ChangeBundle actual = ChangeBundle.fromNotes(commentsUtil, notes);
-    ChangeBundle expected = bundleReader.fromReviewDb(getUnwrappedDb(), id);
-    assertThat(actual.differencesFrom(expected)).isEmpty();
-    assertChangeUpToDate(false, id);
-
-    // Another rebuild attempt succeeds
-    notesFactory.create(dbProvider.get(), project, id);
-    assertThat(getMetaRef(project, changeMetaRef(id))).isNotEqualTo(oldMetaId);
-    assertChangeUpToDate(true, id);
-  }
-
-  @Test
-  public void rebuildReturnsDraftResultWhenRebuildingInChangeNotesFails() throws Exception {
-    setNotesMigration(true, true);
-
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getPatchSetId().getParentKey();
-    putDraft(user, id, 1, "comment by user", null);
-    assertChangeUpToDate(true, id);
-
-    ObjectId oldMetaId = getMetaRef(allUsers, refsDraftComments(id, user.getId()));
-
-    // Add a draft behind NoteDb's back.
-    setNotesMigration(false, false);
-    putDraft(user, id, 1, "second comment by user", null);
-    setInvalidNoteDbState(id);
-    assertDraftsUpToDate(false, id, user);
-    assertThat(getMetaRef(allUsers, refsDraftComments(id, user.getId()))).isEqualTo(oldMetaId);
-
-    // Force the next rebuild attempt to fail (in ChangeNotes).
-    rebuilderWrapper.failNextUpdate();
-    setNotesMigration(true, true);
-    ChangeNotes notes = notesFactory.create(dbProvider.get(), project, id);
-    notes.getDraftComments(user.getId());
-    assertThat(getMetaRef(allUsers, refsDraftComments(id, user.getId()))).isEqualTo(oldMetaId);
-
-    // Not up to date, but the actual returned state matches anyway.
-    assertDraftsUpToDate(false, id, user);
-    ChangeBundle actual = ChangeBundle.fromNotes(commentsUtil, notes);
-    ChangeBundle expected = bundleReader.fromReviewDb(getUnwrappedDb(), id);
-    assertThat(actual.differencesFrom(expected)).isEmpty();
-
-    // Another rebuild attempt succeeds
-    notesFactory.create(dbProvider.get(), project, id);
-    assertChangeUpToDate(true, id);
-    assertDraftsUpToDate(true, id, user);
-    assertThat(getMetaRef(allUsers, refsDraftComments(id, user.getId()))).isNotEqualTo(oldMetaId);
-  }
-
-  @Test
-  public void rebuildReturnsDraftResultWhenRebuildingInDraftCommentNotesFails() throws Exception {
-    setNotesMigration(true, true);
-
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getPatchSetId().getParentKey();
-    putDraft(user, id, 1, "comment by user", null);
-    assertChangeUpToDate(true, id);
-
-    ObjectId oldMetaId = getMetaRef(allUsers, refsDraftComments(id, user.getId()));
-
-    // Add a draft behind NoteDb's back.
-    setNotesMigration(false, false);
-    putDraft(user, id, 1, "second comment by user", null);
-
-    ReviewDb db = getUnwrappedDb();
-    Change c = db.changes().get(id);
-    // Leave change meta ID alone so DraftCommentNotes does the rebuild.
-    ObjectId badSha = ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
-    NoteDbChangeState bogusState =
-        new NoteDbChangeState(
-            id,
-            PrimaryStorage.REVIEW_DB,
-            Optional.of(
-                NoteDbChangeState.RefState.create(
-                    NoteDbChangeState.parse(c).getChangeMetaId(),
-                    ImmutableMap.of(user.getId(), badSha))),
-            Optional.empty());
-    c.setNoteDbState(bogusState.toString());
-    db.changes().update(Collections.singleton(c));
-
-    assertDraftsUpToDate(false, id, user);
-    assertThat(getMetaRef(allUsers, refsDraftComments(id, user.getId()))).isEqualTo(oldMetaId);
-
-    // Force the next rebuild attempt to fail (in DraftCommentNotes).
-    rebuilderWrapper.failNextUpdate();
-    setNotesMigration(true, true);
-    ChangeNotes notes = notesFactory.create(dbProvider.get(), project, id);
-    notes.getDraftComments(user.getId());
-    assertThat(getMetaRef(allUsers, refsDraftComments(id, user.getId()))).isEqualTo(oldMetaId);
-
-    // Not up to date, but the actual returned state matches anyway.
-    assertChangeUpToDate(true, id);
-    assertDraftsUpToDate(false, id, user);
-    ChangeBundle actual = ChangeBundle.fromNotes(commentsUtil, notes);
-    ChangeBundle expected = bundleReader.fromReviewDb(getUnwrappedDb(), id);
-    assertThat(actual.differencesFrom(expected)).isEmpty();
-
-    // Another rebuild attempt succeeds
-    notesFactory.create(dbProvider.get(), project, id).getDraftComments(user.getId());
-    assertChangeUpToDate(true, id);
-    assertDraftsUpToDate(true, id, user);
-    assertThat(getMetaRef(allUsers, refsDraftComments(id, user.getId()))).isNotEqualTo(oldMetaId);
-  }
-
-  @Test
-  public void rebuildAutomaticallyWhenDraftsOutOfDate() throws Exception {
-    setNotesMigration(true, true);
-    setApiUser(user);
-
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getPatchSetId().getParentKey();
-    putDraft(user, id, 1, "comment", null);
-    assertDraftsUpToDate(true, id, user);
-
-    // Make a ReviewDb change behind NoteDb's back and ensure it's detected.
-    setNotesMigration(false, false);
-    putDraft(user, id, 1, "comment", null);
-    setInvalidNoteDbState(id);
-    assertDraftsUpToDate(false, id, user);
-
-    // On next NoteDb read, the drafts are transparently rebuilt.
-    setNotesMigration(true, true);
-    assertThat(gApi.changes().id(id.get()).current().drafts()).containsKey(PushOneCommit.FILE_NAME);
-    assertDraftsUpToDate(true, id, user);
-  }
-
-  @Test
-  public void pushCert() throws Exception {
-    // We don't have the code in our test harness to do signed pushes, so just
-    // use a hard-coded cert. This cert was actually generated by C git 2.2.0
-    // (albeit not for sending to Gerrit).
-    String cert =
-        "certificate version 0.1\n"
-            + "pusher Dave Borowitz <dborowitz@google.com> 1433954361 -0700\n"
-            + "pushee git://localhost/repo.git\n"
-            + "nonce 1433954361-bde756572d665bba81d8\n"
-            + "\n"
-            + "0000000000000000000000000000000000000000"
-            + "b981a177396fb47345b7df3e4d3f854c6bea7"
-            + "s/heads/master\n"
-            + "-----BEGIN PGP SIGNATURE-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "iQEcBAABAgAGBQJVeGg5AAoJEPfTicJkUdPkUggH/RKAeI9/i/LduuiqrL/SSdIa\n"
-            + "9tYaSqJKLbXz63M/AW4Sp+4u+dVCQvnAt/a35CVEnpZz6hN4Kn/tiswOWVJf4CO7\n"
-            + "htNubGs5ZMwvD6sLYqKAnrM3WxV/2TbbjzjZW6Jkidz3jz/WRT4SmjGYiEO7aA+V\n"
-            + "4ZdIS9f7sW5VsHHYlNThCA7vH8Uu48bUovFXyQlPTX0pToSgrWV3JnTxDNxfn3iG\n"
-            + "IL0zTY/qwVCdXgFownLcs6J050xrrBWIKqfcWr3u4D2aCLyR0v+S/KArr7ulZygY\n"
-            + "+SOklImn8TAZiNxhWtA6ens66IiammUkZYFv7SSzoPLFZT4dC84SmGPWgf94NoQ=\n"
-            + "=XFeC\n"
-            + "-----END PGP SIGNATURE-----\n";
-
-    PushOneCommit.Result r = createChange();
-    PatchSet.Id psId = r.getPatchSetId();
-    Change.Id id = psId.getParentKey();
-
-    PatchSet ps = db.patchSets().get(psId);
-    ps.setPushCertificate(cert);
-    db.patchSets().update(Collections.singleton(ps));
-    indexer.index(db, project, id);
-
-    checker.rebuildAndCheckChanges(id);
-  }
-
-  @Test
-  public void emptyTopic() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getPatchSetId().getParentKey();
-    Change c = db.changes().get(id);
-    assertThat(c.getTopic()).isNull();
-    c.setTopic("");
-    db.changes().update(Collections.singleton(c));
-
-    checker.rebuildAndCheckChanges(id);
-
-    setNotesMigration(true, true);
-
-    // Rebuild and check was successful, but NoteDb doesn't support storing an
-    // empty topic, so it comes out as null.
-    ChangeNotes notes = notesFactory.create(db, project, id);
-    assertThat(notes.getChange().getTopic()).isNull();
-  }
-
-  @Test
-  public void commentBeforeFirstPatchSet() throws Exception {
-    PushOneCommit.Result r = createChange();
-    PatchSet.Id psId = r.getPatchSetId();
-    Change.Id id = psId.getParentKey();
-
-    Change c = db.changes().get(id);
-    c.setCreatedOn(new Timestamp(c.getCreatedOn().getTime() - 5000));
-    db.changes().update(Collections.singleton(c));
-    indexer.index(db, project, id);
-
-    ReviewInput rin = new ReviewInput();
-    rin.message = "comment";
-
-    Timestamp ts = new Timestamp(c.getCreatedOn().getTime() + 2000);
-    assertThat(ts).isGreaterThan(c.getCreatedOn());
-    assertThat(ts).isLessThan(db.patchSets().get(psId).getCreatedOn());
-    RevisionResource revRsrc = parseCurrentRevisionResource(r.getChangeId());
-    postReview.get().apply(batchUpdateFactory, revRsrc, rin, ts);
-
-    checker.rebuildAndCheckChanges(id);
-  }
-
-  @Test
-  public void commentPredatingChangeBySomeoneOtherThanOwner() throws Exception {
-    PushOneCommit.Result r = createChange();
-    PatchSet.Id psId = r.getPatchSetId();
-    Change.Id id = psId.getParentKey();
-    Change c = db.changes().get(id);
-
-    ReviewInput rin = new ReviewInput();
-    rin.message = "comment";
-
-    Timestamp ts = new Timestamp(c.getCreatedOn().getTime() - 10000);
-    RevisionResource revRsrc = parseCurrentRevisionResource(r.getChangeId());
-    setApiUser(user);
-    postReview.get().apply(batchUpdateFactory, revRsrc, rin, ts);
-
-    checker.rebuildAndCheckChanges(id);
-  }
-
-  @Test
-  public void noteDbUsesOriginalSubjectFromPatchSetAndIgnoresChangeField() throws Exception {
-    PushOneCommit.Result r = createChange();
-    String orig = r.getChange().change().getSubject();
-    r =
-        pushFactory
-            .create(
-                db,
-                admin.getIdent(),
-                testRepo,
-                orig + " v2",
-                PushOneCommit.FILE_NAME,
-                "new contents",
-                r.getChangeId())
-            .to("refs/for/master");
-    r.assertOkStatus();
-
-    PatchSet.Id psId = r.getPatchSetId();
-    Change.Id id = psId.getParentKey();
-    Change c = db.changes().get(id);
-
-    c.setCurrentPatchSet(psId, c.getSubject(), "Bogus original subject");
-    db.changes().update(Collections.singleton(c));
-
-    checker.rebuildAndCheckChanges(id);
-
-    setNotesMigration(true, true);
-    ChangeNotes notes = notesFactory.create(db, project, id);
-    Change nc = notes.getChange();
-    assertThat(nc.getSubject()).isEqualTo(c.getSubject());
-    assertThat(nc.getSubject()).isEqualTo(orig + " v2");
-    assertThat(nc.getOriginalSubject()).isNotEqualTo(c.getOriginalSubject());
-    assertThat(nc.getOriginalSubject()).isEqualTo(orig);
-  }
-
-  @Test
-  public void ignorePatchLineCommentsOnPatchSet0() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change change = r.getChange().change();
-    Change.Id id = change.getId();
-
-    PatchLineComment comment =
-        new PatchLineComment(
-            new PatchLineComment.Key(
-                new Patch.Key(new PatchSet.Id(id, 0), PushOneCommit.FILE_NAME), "uuid"),
-            0,
-            user.getId(),
-            null,
-            TimeUtil.nowTs());
-    comment.setSide((short) 1);
-    comment.setMessage("message");
-    comment.setStatus(PatchLineComment.Status.PUBLISHED);
-    db.patchComments().insert(Collections.singleton(comment));
-    indexer.index(db, change.getProject(), id);
-
-    checker.rebuildAndCheckChanges(id);
-
-    setNotesMigration(true, true);
-    ChangeNotes notes = notesFactory.create(db, project, id);
-    assertThat(notes.getComments()).isEmpty();
-  }
-
-  @Test
-  public void leadingSpacesInSubject() throws Exception {
-    String subj = "   " + PushOneCommit.SUBJECT;
-    PushOneCommit push =
-        pushFactory.create(
-            db,
-            admin.getIdent(),
-            testRepo,
-            subj,
-            PushOneCommit.FILE_NAME,
-            PushOneCommit.FILE_CONTENT);
-    PushOneCommit.Result r = push.to("refs/for/master");
-    r.assertOkStatus();
-    Change change = r.getChange().change();
-    assertThat(change.getSubject()).isEqualTo(subj);
-    Change.Id id = r.getPatchSetId().getParentKey();
-
-    checker.rebuildAndCheckChanges(id);
-
-    setNotesMigration(true, true);
-    ChangeNotes notes = notesFactory.create(db, project, id);
-    assertThat(notes.getChange().getSubject()).isNotEqualTo(subj);
-    assertThat(notes.getChange().getSubject()).isEqualTo(PushOneCommit.SUBJECT);
-  }
-
-  @Test
-  public void allTimestampsExceptUpdatedAreEqualDueToBadMigration() throws Exception {
-    // https://bugs.chromium.org/p/gerrit/issues/detail?id=7397
-    PushOneCommit.Result r = createChange();
-    Change c = r.getChange().change();
-    Change.Id id = c.getId();
-    Timestamp ts = TimeUtil.nowTs();
-    Timestamp origUpdated = c.getLastUpdatedOn();
-
-    c.setCreatedOn(ts);
-    assertThat(c.getCreatedOn()).isGreaterThan(c.getLastUpdatedOn());
-    db.changes().update(Collections.singleton(c));
-
-    List<ChangeMessage> cm = db.changeMessages().byChange(id).toList();
-    cm.forEach(m -> m.setWrittenOn(ts));
-    db.changeMessages().update(cm);
-
-    List<PatchSet> ps = db.patchSets().byChange(id).toList();
-    ps.forEach(p -> p.setCreatedOn(ts));
-    db.patchSets().update(ps);
-
-    List<PatchSetApproval> psa = db.patchSetApprovals().byChange(id).toList();
-    psa.forEach(p -> p.setGranted(ts));
-    db.patchSetApprovals().update(psa);
-
-    List<PatchLineComment> plc = db.patchComments().byChange(id).toList();
-    plc.forEach(p -> p.setWrittenOn(ts));
-    db.patchComments().update(plc);
-
-    checker.rebuildAndCheckChanges(id);
-
-    setNotesMigration(true, true);
-    ChangeNotes notes = notesFactory.create(db, project, id);
-    assertThat(notes.getChange().getCreatedOn()).isEqualTo(origUpdated);
-    assertThat(notes.getChange().getLastUpdatedOn()).isAtLeast(origUpdated);
-    assertThat(notes.getPatchSets().get(new PatchSet.Id(id, 1)).getCreatedOn())
-        .isEqualTo(origUpdated);
-  }
-
-  @Test
-  public void createWithAutoRebuildingDisabled() throws Exception {
-    ReviewDb oldDb = db;
-    setNotesMigration(true, true);
-
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getPatchSetId().getParentKey();
-    ChangeNotes oldNotes = notesFactory.create(db, project, id);
-
-    // Make a ReviewDb change behind NoteDb's back.
-    Change c = oldDb.changes().get(id);
-    assertThat(c.getTopic()).isNull();
-    String topic = name("a-topic");
-    c.setTopic(topic);
-    oldDb.changes().update(Collections.singleton(c));
-
-    c = oldDb.changes().get(c.getId());
-    ChangeNotes newNotes = notesFactory.createWithAutoRebuildingDisabled(c, null);
-    assertThat(newNotes.getChange().getTopic()).isNotEqualTo(topic);
-    assertThat(newNotes.getChange().getTopic()).isEqualTo(oldNotes.getChange().getTopic());
-  }
-
-  @Test
-  public void rebuildDeletesOldDraftRefs() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getPatchSetId().getParentKey();
-    putDraft(user, id, 1, "comment", null);
-
-    Account.Id otherAccountId = new Account.Id(user.getId().get() + 1234);
-    String otherDraftRef = refsDraftComments(id, otherAccountId);
-
-    try (Repository repo = repoManager.openRepository(allUsers);
-        ObjectInserter ins = repo.newObjectInserter()) {
-      ObjectId sha = ins.insert(OBJ_BLOB, "garbage data".getBytes(UTF_8));
-      ins.flush();
-      RefUpdate ru = repo.updateRef(otherDraftRef);
-      ru.setExpectedOldObjectId(ObjectId.zeroId());
-      ru.setNewObjectId(sha);
-      assertThat(ru.update()).isEqualTo(RefUpdate.Result.NEW);
-    }
-
-    checker.rebuildAndCheckChanges(id);
-
-    try (Repository repo = repoManager.openRepository(allUsers)) {
-      assertThat(repo.exactRef(otherDraftRef)).isNull();
-    }
-  }
-
-  @Test
-  public void failWhenWritesDisabled() throws Exception {
-    setNotesMigration(true, true);
-
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getPatchSetId().getParentKey();
-    assertChangeUpToDate(true, id);
-    assertThat(gApi.changes().id(id.get()).info().topic).isNull();
-
-    // Turning off writes causes failure.
-    setNotesMigration(false, true);
-    try {
-      gApi.changes().id(id.get()).topic(name("a-topic"));
-      fail("Expected write to fail");
-    } catch (RestApiException e) {
-      assertChangesReadOnly(e);
-    }
-
-    // Update was not written.
-    assertThat(gApi.changes().id(id.get()).info().topic).isNull();
-    assertChangeUpToDate(true, id);
-  }
-
-  @Test
-  public void rebuildWhenWritesDisabledWorksButDoesNotWrite() throws Exception {
-    setNotesMigration(true, true);
-
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getPatchSetId().getParentKey();
-    assertChangeUpToDate(true, id);
-
-    // Make a ReviewDb change behind NoteDb's back and ensure it's detected.
-    setNotesMigration(false, false);
-    gApi.changes().id(id.get()).topic(name("a-topic"));
-    setInvalidNoteDbState(id);
-    assertChangeUpToDate(false, id);
-
-    // On next NoteDb read, change is rebuilt in-memory but not stored.
-    setNotesMigration(false, true);
-    assertThat(gApi.changes().id(id.get()).info().topic).isEqualTo(name("a-topic"));
-    assertChangeUpToDate(false, id);
-
-    // Attempting to write directly causes failure.
-    try {
-      gApi.changes().id(id.get()).topic(name("other-topic"));
-      fail("Expected write to fail");
-    } catch (RestApiException e) {
-      assertChangesReadOnly(e);
-    }
-
-    // Update was not written.
-    assertThat(gApi.changes().id(id.get()).info().topic).isEqualTo(name("a-topic"));
-    assertChangeUpToDate(false, id);
-  }
-
-  @Test
-  public void rebuildChangeWithNoPatchSets() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getPatchSetId().getParentKey();
-    db.changes().beginTransaction(id);
-    try {
-      db.patchSets().delete(db.patchSets().byChange(id));
-      db.commit();
-    } finally {
-      db.rollback();
-    }
-
-    try {
-      checker.rebuildAndCheckChanges(id);
-      assert_().fail("expected NoPatchSetsException");
-    } catch (NoPatchSetsException e) {
-      // Expected.
-    }
-
-    Change c = db.changes().get(id);
-    assertThat(c.getNoteDbState()).isNull();
-    checker.assertNoChangeRef(project, id);
-  }
-
-  @Test
-  public void rebuildChangeWithNoEntitiesOtherThanChange() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getPatchSetId().getParentKey();
-    db.changes().beginTransaction(id);
-    try {
-      db.changeMessages().delete(db.changeMessages().byChange(id));
-      db.patchSets().delete(db.patchSets().byChange(id));
-      db.patchSetApprovals().delete(db.patchSetApprovals().byChange(id));
-      db.patchComments().delete(db.patchComments().byChange(id));
-      db.commit();
-    } finally {
-      db.rollback();
-    }
-
-    try {
-      checker.rebuildAndCheckChanges(id);
-      assert_().fail("expected NoPatchSetsException");
-    } catch (NoPatchSetsException e) {
-      // Expected.
-    }
-
-    Change c = db.changes().get(id);
-    assertThat(c.getNoteDbState()).isNull();
-    checker.assertNoChangeRef(project, id);
-  }
-
-  @Test
-  public void rebuildEntitiesCreatedByImpersonation() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getPatchSetId().getParentKey();
-    PatchSet.Id psId = new PatchSet.Id(id, 1);
-    String prefix = "/changes/" + id + "/revisions/current/";
-
-    // For each of the entities that have a real user field, create one entity
-    // without impersonation and one with.
-    CommentInput ci = new CommentInput();
-    ci.path = Patch.COMMIT_MSG;
-    ci.side = Side.REVISION;
-    ci.line = 1;
-    ci.message = "comment without impersonation";
-    ReviewInput ri = new ReviewInput();
-    ri.label("Code-Review", -1);
-    ri.message = "message without impersonation";
-    ri.drafts = DraftHandling.KEEP;
-    ri.comments = ImmutableMap.of(ci.path, ImmutableList.of(ci));
-    userRestSession.post(prefix + "review", ri).assertOK();
-
-    DraftInput di = new DraftInput();
-    di.path = Patch.COMMIT_MSG;
-    di.side = Side.REVISION;
-    di.line = 1;
-    di.message = "draft without impersonation";
-    userRestSession.put(prefix + "drafts", di).assertCreated();
-
-    allowRunAs();
-    try {
-      Header runAs = new BasicHeader("X-Gerrit-RunAs", user.id.toString());
-      ci.message = "comment with impersonation";
-      ri.message = "message with impersonation";
-      ri.label("Code-Review", 1);
-      adminRestSession.postWithHeader(prefix + "review", runAs, ri).assertOK();
-
-      di.message = "draft with impersonation";
-      adminRestSession.putWithHeader(prefix + "drafts", runAs, di).assertCreated();
-    } finally {
-      removeRunAs();
-    }
-
-    List<ChangeMessage> msgs =
-        Ordering.natural()
-            .onResultOf(ChangeMessage::getWrittenOn)
-            .sortedCopy(db.changeMessages().byChange(id));
-    assertThat(msgs).hasSize(3);
-    assertThat(msgs.get(1).getMessage()).endsWith("message without impersonation");
-    assertThat(msgs.get(1).getAuthor()).isEqualTo(user.id);
-    assertThat(msgs.get(1).getRealAuthor()).isEqualTo(user.id);
-    assertThat(msgs.get(2).getMessage()).endsWith("message with impersonation");
-    assertThat(msgs.get(2).getAuthor()).isEqualTo(user.id);
-    assertThat(msgs.get(2).getRealAuthor()).isEqualTo(admin.id);
-
-    List<PatchSetApproval> psas = db.patchSetApprovals().byChange(id).toList();
-    assertThat(psas).hasSize(1);
-    assertThat(psas.get(0).getLabel()).isEqualTo("Code-Review");
-    assertThat(psas.get(0).getValue()).isEqualTo(1);
-    assertThat(psas.get(0).getAccountId()).isEqualTo(user.id);
-    assertThat(psas.get(0).getRealAccountId()).isEqualTo(admin.id);
-
-    Ordering<PatchLineComment> commentOrder =
-        Ordering.natural().onResultOf(PatchLineComment::getWrittenOn);
-    List<PatchLineComment> drafts =
-        commentOrder.sortedCopy(db.patchComments().draftByPatchSetAuthor(psId, user.id));
-    assertThat(drafts).hasSize(2);
-    assertThat(drafts.get(0).getMessage()).isEqualTo("draft without impersonation");
-    assertThat(drafts.get(0).getAuthor()).isEqualTo(user.id);
-    assertThat(drafts.get(0).getRealAuthor()).isEqualTo(user.id);
-    assertThat(drafts.get(1).getMessage()).isEqualTo("draft with impersonation");
-    assertThat(drafts.get(1).getAuthor()).isEqualTo(user.id);
-    assertThat(drafts.get(1).getRealAuthor()).isEqualTo(admin.id);
-
-    List<PatchLineComment> pub =
-        commentOrder.sortedCopy(db.patchComments().publishedByPatchSet(psId));
-    assertThat(pub).hasSize(2);
-    assertThat(pub.get(0).getMessage()).isEqualTo("comment without impersonation");
-    assertThat(pub.get(0).getAuthor()).isEqualTo(user.id);
-    assertThat(pub.get(0).getRealAuthor()).isEqualTo(user.id);
-    assertThat(pub.get(1).getMessage()).isEqualTo("comment with impersonation");
-    assertThat(pub.get(1).getAuthor()).isEqualTo(user.id);
-    assertThat(pub.get(1).getRealAuthor()).isEqualTo(admin.id);
-  }
-
-  @Test
-  public void laterEventsDependingOnEarlierPatchSetDontIntefereWithOtherPatchSets()
-      throws Exception {
-    PushOneCommit.Result r1 = createChange();
-    ChangeData cd = r1.getChange();
-    Change.Id id = cd.getId();
-    amendChange(cd.change().getKey().get());
-    TestTimeUtil.incrementClock(90, TimeUnit.DAYS);
-
-    ReviewInput rin = ReviewInput.approve();
-    rin.message = "Some very late message on PS1";
-    gApi.changes().id(id.get()).revision(1).review(rin);
-
-    checker.rebuildAndCheckChanges(id);
-  }
-
-  @Test
-  public void ignoreChangeMessageBeyondCurrentPatchSet() throws Exception {
-    PushOneCommit.Result r = createChange();
-    PatchSet.Id psId1 = r.getPatchSetId();
-    Change.Id id = psId1.getParentKey();
-    gApi.changes().id(id.get()).current().review(ReviewInput.recommend());
-
-    r = amendChange(r.getChangeId());
-    PatchSet.Id psId2 = r.getPatchSetId();
-
-    assertThat(db.patchSets().byChange(id)).hasSize(2);
-    assertThat(db.changeMessages().byPatchSet(psId2)).hasSize(1);
-    db.patchSets().deleteKeys(Collections.singleton(psId2));
-
-    checker.rebuildAndCheckChanges(psId2.getParentKey());
-    setNotesMigration(true, true);
-
-    ChangeData cd = changeDataFactory.create(db, project, id);
-    assertThat(cd.change().currentPatchSetId()).isEqualTo(psId1);
-    assertThat(cd.patchSets().stream().map(PatchSet::getId).collect(toList()))
-        .containsExactly(psId1);
-    PatchSet ps = cd.currentPatchSet();
-    assertThat(ps).isNotNull();
-    assertThat(ps.getId()).isEqualTo(psId1);
-  }
-
-  @Test
-  public void highestNumberedPatchSetIsNotCurrent() throws Exception {
-    PushOneCommit.Result r1 = createChange();
-    PatchSet.Id psId1 = r1.getPatchSetId();
-    Change.Id id = psId1.getParentKey();
-    PushOneCommit.Result r2 = amendChange(r1.getChangeId());
-    PatchSet.Id psId2 = r2.getPatchSetId();
-
-    try (BatchUpdate bu =
-        batchUpdateFactory.create(
-            db, project, identifiedUserFactory.create(user.getId()), TimeUtil.nowTs())) {
-      bu.addOp(
-          id,
-          new BatchUpdateOp() {
-            @Override
-            public boolean updateChange(ChangeContext ctx)
-                throws PatchSetInfoNotAvailableException {
-              ctx.getChange()
-                  .setCurrentPatchSet(patchSetInfoFactory.get(ctx.getDb(), ctx.getNotes(), psId1));
-              return true;
-            }
-          });
-      bu.execute();
-    }
-    ChangeNotes notes = notesFactory.create(db, project, id);
-    assertThat(psUtil.byChangeAsMap(db, notes).keySet()).containsExactly(psId1, psId2);
-    assertThat(notes.getChange().currentPatchSetId()).isEqualTo(psId1);
-
-    assertThat(db.changes().get(id).currentPatchSetId()).isEqualTo(psId1);
-
-    checker.rebuildAndCheckChanges(id);
-    setNotesMigration(true, true);
-
-    notes = notesFactory.create(db, project, id);
-    assertThat(psUtil.byChangeAsMap(db, notes).keySet()).containsExactly(psId1, psId2);
-    assertThat(notes.getChange().currentPatchSetId()).isEqualTo(psId1);
-  }
-
-  @Test
-  public void resolveCommentsInheritsValueFromParentWhenUnspecified() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getPatchSetId().getParentKey();
-    putDraft(user, id, 1, "comment", true);
-    putDraft(user, id, 1, "newComment", null);
-
-    Map<String, List<CommentInfo>> comments = gApi.changes().id(id.get()).current().drafts();
-    for (List<CommentInfo> cList : comments.values()) {
-      for (CommentInfo ci : cList) {
-        assertThat(ci.unresolved).isTrue();
-      }
-    }
-  }
-
-  @Test
-  public void rebuilderRespectsReadOnlyInNoteDbChangeState() throws Exception {
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
-    PushOneCommit.Result r = createChange();
-    PatchSet.Id psId1 = r.getPatchSetId();
-    Change.Id id = psId1.getParentKey();
-
-    checker.rebuildAndCheckChanges(id);
-    setNotesMigration(true, true);
-
-    ReviewDb db = getUnwrappedDb();
-    Change c = db.changes().get(id);
-    NoteDbChangeState state = NoteDbChangeState.parse(c);
-    Timestamp until = new Timestamp(TimeUtil.nowMs() + MILLISECONDS.convert(1, DAYS));
-    state = state.withReadOnlyUntil(until);
-    c.setNoteDbState(state.toString());
-    db.changes().update(Collections.singleton(c));
-
-    try {
-      rebuilderWrapper.rebuild(db, id);
-      fail("expected rebuild to fail");
-    } catch (OrmRuntimeException e) {
-      assertThat(e.getMessage()).contains("read-only until");
-    }
-
-    TestTimeUtil.setClock(new Timestamp(until.getTime() + MILLISECONDS.convert(1, SECONDS)));
-    rebuilderWrapper.rebuild(db, id);
-  }
-
-  @Test
-  public void commitWithCrLineEndings() throws Exception {
-    PushOneCommit.Result r =
-        createChange("Subject\r\rBody\r", PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT);
-    Change c = r.getChange().change();
-
-    // This assertion demonstrates an arguable bug in JGit's commit subject
-    // parsing, and shows how this kind of data might have gotten into
-    // ReviewDb. If that bug ever gets fixed upstream, this assert may start
-    // failing. If that happens, this test can be rewritten to directly set the
-    // subject field in ReviewDb.
-    assertThat(c.getSubject()).isEqualTo("Subject\r\rBody");
-
-    checker.rebuildAndCheckChanges(c.getId());
-  }
-
-  @Test
-  public void patchSetsOutOfOrder() throws Exception {
-    String id = createChange().getChangeId();
-    amendChange(id);
-    PushOneCommit.Result r = amendChange(id);
-
-    ChangeData cd = r.getChange();
-    PatchSet.Id psId3 = cd.change().currentPatchSetId();
-    assertThat(psId3.get()).isEqualTo(3);
-
-    PatchSet ps1 = db.patchSets().get(new PatchSet.Id(cd.getId(), 1));
-    PatchSet ps3 = db.patchSets().get(psId3);
-    assertThat(ps1.getCreatedOn()).isLessThan(ps3.getCreatedOn());
-
-    // Simulate an old Gerrit bug by setting the created timestamp of the latest
-    // patch set ID to the timestamp of PS1.
-    ps3.setCreatedOn(ps1.getCreatedOn());
-    db.patchSets().update(Collections.singleton(ps3));
-
-    checker.rebuildAndCheckChanges(cd.getId());
-
-    setNotesMigration(true, true);
-    cd = changeDataFactory.create(db, project, cd.getId());
-    assertThat(cd.change().currentPatchSetId()).isEqualTo(psId3);
-
-    List<PatchSet> patchSets = ImmutableList.copyOf(cd.patchSets());
-    assertThat(patchSets).hasSize(3);
-
-    PatchSet newPs1 = patchSets.get(0);
-    assertThat(newPs1.getId()).isEqualTo(ps1.getId());
-    assertThat(newPs1.getCreatedOn()).isEqualTo(ps1.getCreatedOn());
-
-    PatchSet newPs2 = patchSets.get(1);
-    assertThat(newPs2.getCreatedOn()).isGreaterThan(newPs1.getCreatedOn());
-
-    PatchSet newPs3 = patchSets.get(2);
-    assertThat(newPs3.getId()).isEqualTo(ps3.getId());
-    // Migrated with a newer timestamp than the original, to preserve ordering.
-    assertThat(newPs3.getCreatedOn()).isAtLeast(newPs2.getCreatedOn());
-    assertThat(newPs3.getCreatedOn()).isGreaterThan(ps1.getCreatedOn());
-  }
-
-  @Test
-  public void ignoreNoteDbStateWithNoCorrespondingRefWhenWritesAndReadsDisabled() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getChange().getId();
-    ReviewDb db = getUnwrappedDb();
-    Change c = db.changes().get(id);
-    c.setNoteDbState("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
-    db.changes().update(Collections.singleton(c));
-    c = db.changes().get(id);
-
-    String refName = RefNames.changeMetaRef(id);
-    assertThat(getMetaRef(project, refName)).isNull();
-
-    ChangeNotes notes = notesFactory.create(dbProvider.get(), project, id);
-    assertThat(notes.getChange().getRowVersion()).isEqualTo(c.getRowVersion());
-
-    notes = notesFactory.createChecked(dbProvider.get(), project, id);
-    assertThat(notes.getChange().getRowVersion()).isEqualTo(c.getRowVersion());
-
-    assertThat(getMetaRef(project, refName)).isNull();
-  }
-
-  @Test
-  public void autoRebuildMissingRefWriteOnly() throws Exception {
-    setNotesMigration(true, false);
-    testAutoRebuildMissingRef();
-  }
-
-  @Test
-  public void autoRebuildMissingRefReadWrite() throws Exception {
-    setNotesMigration(true, true);
-    testAutoRebuildMissingRef();
-  }
-
-  private void testAutoRebuildMissingRef() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getChange().getId();
-
-    assertChangeUpToDate(true, id);
-    notesFactory.createChecked(db, project, id);
-
-    try (Repository repo = repoManager.openRepository(project)) {
-      RefUpdate ru = repo.updateRef(RefNames.changeMetaRef(id));
-      ru.setForceUpdate(true);
-      assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
-    }
-    assertChangeUpToDate(false, id);
-
-    notesFactory.createChecked(db, project, id);
-    assertChangeUpToDate(true, id);
-  }
-
-  @Test
-  public void missingPatchSetCommitOkForCommentsNotOnParentSide() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getChange().getId();
-
-    putDraft(user, id, 1, "draft comment", null, Side.REVISION);
-    putComment(user, id, 1, "published comment", null, Side.REVISION);
-
-    ReviewDb db = getUnwrappedDb();
-    PatchSet ps = db.patchSets().get(new PatchSet.Id(id, 1));
-    ps.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
-    db.patchSets().update(Collections.singleton(ps));
-
-    try {
-      patchListCache.getOldId(db.changes().get(id), ps, null);
-      assert_().fail("Expected PatchListNotAvailableException");
-    } catch (PatchListNotAvailableException e) {
-      // Expected.
-    }
-
-    checker.rebuildAndCheckChanges(id);
-  }
-
-  @Test
-  public void missingPatchSetCommitOmitsCommentsOnParentSide() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getChange().getId();
-
-    CommentInfo draftInfo = putDraft(user, id, 1, "draft comment", null, Side.PARENT);
-    putComment(user, id, 1, "published comment", null, Side.PARENT);
-    CommentInfo commentInfo =
-        gApi.changes().id(id.get()).comments().values().stream()
-            .flatMap(List::stream)
-            .findFirst()
-            .get();
-
-    ReviewDb db = getUnwrappedDb();
-    PatchSet ps = db.patchSets().get(new PatchSet.Id(id, 1));
-    ps.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
-    db.patchSets().update(Collections.singleton(ps));
-
-    try {
-      patchListCache.getOldId(db.changes().get(id), ps, null);
-      assert_().fail("Expected PatchListNotAvailableException");
-    } catch (PatchListNotAvailableException e) {
-      // Expected.
-    }
-
-    checker.rebuildAndCheckChange(
-        id,
-        Stream.of(draftInfo.id, commentInfo.id)
-            .sorted()
-            .map(c -> id + ",1," + PushOneCommit.FILE_NAME + "," + c)
-            .collect(
-                joining(", ", "PatchLineComment.Key sets differ: [", "] only in A; [] only in B")));
-  }
-
-  private void assertChangesReadOnly(RestApiException e) throws Exception {
-    Throwable cause = e.getCause();
-    assertThat(cause).isInstanceOf(UpdateException.class);
-    assertThat(cause.getCause()).isInstanceOf(OrmException.class);
-    assertThat(cause.getCause()).hasMessageThat().isEqualTo(NoteDbUpdateManager.CHANGES_READ_ONLY);
-  }
-
-  private void setInvalidNoteDbState(Change.Id id) throws Exception {
-    ReviewDb db = getUnwrappedDb();
-    Change c = db.changes().get(id);
-    // In reality we would have NoteDb writes enabled, which would write a real
-    // state into this field. For tests however, we turn NoteDb writes off, so
-    // just use a dummy state to force ChangeNotes to view the notes as
-    // out-of-date.
-    c.setNoteDbState("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
-    db.changes().update(Collections.singleton(c));
-  }
-
-  private void assertChangeUpToDate(boolean expected, Change.Id id) throws Exception {
-    try (Repository repo = repoManager.openRepository(project)) {
-      Change c = getUnwrappedDb().changes().get(id);
-      assertThat(c).isNotNull();
-      assertThat(c.getNoteDbState()).isNotNull();
-      NoteDbChangeState state = NoteDbChangeState.parse(c);
-      assertThat(state).isNotNull();
-      assertThat(state.getPrimaryStorage()).isEqualTo(PrimaryStorage.REVIEW_DB);
-      assertThat(state.isChangeUpToDate(new RepoRefCache(repo))).isEqualTo(expected);
-    }
-  }
-
-  private void assertDraftsUpToDate(boolean expected, Change.Id changeId, TestAccount account)
-      throws Exception {
-    try (Repository repo = repoManager.openRepository(allUsers)) {
-      Change c = getUnwrappedDb().changes().get(changeId);
-      assertThat(c).isNotNull();
-      assertThat(c.getNoteDbState()).isNotNull();
-      NoteDbChangeState state = NoteDbChangeState.parse(c);
-      assertThat(state.areDraftsUpToDate(new RepoRefCache(repo), account.getId()))
-          .isEqualTo(expected);
-    }
-  }
-
-  private ObjectId getMetaRef(Project.NameKey p, String name) throws Exception {
-    try (Repository repo = repoManager.openRepository(p)) {
-      Ref ref = repo.exactRef(name);
-      return ref != null ? ref.getObjectId() : null;
-    }
-  }
-
-  private CommentInfo putDraft(
-      TestAccount account, Change.Id id, int line, String msg, Boolean unresolved)
-      throws Exception {
-    return putDraft(account, id, line, msg, unresolved, Side.REVISION);
-  }
-
-  private CommentInfo putDraft(
-      TestAccount account, Change.Id id, int line, String msg, Boolean unresolved, Side side)
-      throws Exception {
-    DraftInput in = new DraftInput();
-    in.side = side;
-    in.line = line;
-    in.message = msg;
-    in.path = PushOneCommit.FILE_NAME;
-    in.unresolved = unresolved;
-    AcceptanceTestRequestScope.Context old = setApiUser(account);
-    try {
-      return gApi.changes().id(id.get()).current().createDraft(in).get();
-    } finally {
-      atrScope.set(old);
-    }
-  }
-
-  private void putComment(TestAccount account, Change.Id id, int line, String msg, String inReplyTo)
-      throws Exception {
-    putComment(account, id, line, msg, inReplyTo, Side.REVISION);
-  }
-
-  private void putComment(
-      TestAccount account, Change.Id id, int line, String msg, String inReplyTo, Side side)
-      throws Exception {
-    CommentInput in = new CommentInput();
-    in.side = side;
-    in.line = line;
-    in.message = msg;
-    in.inReplyTo = inReplyTo;
-    ReviewInput rin = new ReviewInput();
-    rin.comments = new HashMap<>();
-    rin.comments.put(PushOneCommit.FILE_NAME, ImmutableList.of(in));
-    rin.drafts = ReviewInput.DraftHandling.KEEP;
-    AcceptanceTestRequestScope.Context old = setApiUser(account);
-    try {
-      gApi.changes().id(id.get()).current().review(rin);
-    } finally {
-      atrScope.set(old);
-    }
-  }
-
-  private void publishDrafts(TestAccount account, Change.Id id) throws Exception {
-    ReviewInput rin = new ReviewInput();
-    rin.drafts = ReviewInput.DraftHandling.PUBLISH_ALL_REVISIONS;
-    AcceptanceTestRequestScope.Context old = setApiUser(account);
-    try {
-      gApi.changes().id(id.get()).current().review(rin);
-    } finally {
-      atrScope.set(old);
-    }
-  }
-
-  private ChangeMessage insertMessage(
-      Change.Id id, PatchSet.Id psId, Account.Id author, Timestamp ts, String message)
-      throws Exception {
-    ChangeMessage msg =
-        new ChangeMessage(new ChangeMessage.Key(id, ChangeUtil.messageUuid()), author, ts, psId);
-    msg.setMessage(message);
-    db.changeMessages().insert(Collections.singleton(msg));
-
-    Change c = db.changes().get(id);
-    if (ts.compareTo(c.getLastUpdatedOn()) > 0) {
-      c.setLastUpdatedOn(ts);
-      db.changes().update(Collections.singleton(c));
-    }
-
-    return msg;
-  }
-
-  private ReviewDb getUnwrappedDb() {
-    ReviewDb db = dbProvider.get();
-    return ReviewDbUtil.unwrapDb(db);
-  }
-
-  private void allowRunAs() throws Exception {
-    try (ProjectConfigUpdate u = updateProject(allProjects)) {
-      Util.allow(
-          u.getConfig(),
-          GlobalCapability.RUN_AS,
-          systemGroupBackend.getGroup(REGISTERED_USERS).getUUID());
-      u.save();
-    }
-  }
-
-  private void removeRunAs() throws Exception {
-    try (ProjectConfigUpdate u = updateProject(allProjects)) {
-      Util.remove(
-          u.getConfig(),
-          GlobalCapability.RUN_AS,
-          systemGroupBackend.getGroup(REGISTERED_USERS).getUUID());
-      u.save();
-    }
-  }
-
-  private Map<String, List<CommentInfo>> getPublishedComments(Change.Id id) throws Exception {
-    return gApi.changes().id(id.get()).current().comments();
-  }
-}
diff --git a/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java b/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
index f4cedbb..748c4ea 100644
--- a/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
@@ -16,7 +16,6 @@
 
 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.extensions.client.ListChangesOption.MESSAGES;
 import static java.util.stream.Collectors.toList;
 
@@ -56,7 +55,6 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevSort;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.junit.Before;
 import org.junit.Test;
 
 public class NoteDbOnlyIT extends AbstractDaemonTest {
@@ -70,14 +68,8 @@
 
   @Inject private RetryHelper retryHelper;
 
-  @Before
-  public void setUp() throws Exception {
-    assume().that(notesMigration.disableChangeReviewDb()).isTrue();
-  }
-
   @Test
   public void updateChangeFailureRollsBackRefUpdate() throws Exception {
-    assume().that(notesMigration.disableChangeReviewDb()).isTrue();
     PushOneCommit.Result r = createChange();
     Change.Id id = r.getChange().getId();
 
@@ -149,7 +141,6 @@
 
   @Test
   public void retryOnLockFailureWithAtomicUpdates() throws Exception {
-    assume().that(notesMigration.disableChangeReviewDb()).isTrue();
     PushOneCommit.Result r = createChange();
     Change.Id id = r.getChange().getId();
     String master = "refs/heads/master";
@@ -198,8 +189,8 @@
   @Test
   public void missingChange() throws Exception {
     Change.Id changeId = new Change.Id(1234567);
-    assertNoSuchChangeException(() -> notesFactory.create(db, project, changeId));
-    assertNoSuchChangeException(() -> notesFactory.createChecked(db, project, changeId));
+    assertNoSuchChangeException(() -> notesFactory.create(project, changeId));
+    assertNoSuchChangeException(() -> notesFactory.createChecked(project, changeId));
   }
 
   private void assertNoSuchChangeException(Callable<?> callable) throws Exception {
@@ -285,7 +276,7 @@
   }
 
   private BatchUpdate newBatchUpdate(BatchUpdate.Factory buf) {
-    return buf.create(db, project, identifiedUserFactory.create(user.getId()), TimeUtil.nowTs());
+    return buf.create(project, identifiedUserFactory.create(user.id()), TimeUtil.nowTs());
   }
 
   private Optional<ObjectId> getRef(String name) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java b/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java
deleted file mode 100644
index 486a0de..0000000
--- a/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java
+++ /dev/null
@@ -1,518 +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.acceptance.server.notedb;
-
-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.server.notedb.NoteDbChangeState.PrimaryStorage.REVIEW_DB;
-import static com.google.gerrit.server.notedb.NoteDbUtil.formatTime;
-import static java.util.concurrent.TimeUnit.DAYS;
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
-import static java.util.concurrent.TimeUnit.SECONDS;
-import static java.util.stream.Collectors.toList;
-
-import com.github.rholder.retry.Retryer;
-import com.github.rholder.retry.RetryerBuilder;
-import com.github.rholder.retry.StopStrategies;
-import com.google.common.base.Throwables;
-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.NoHttpd;
-import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.common.Nullable;
-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.client.ChangeStatus;
-import com.google.gerrit.extensions.common.ApprovalInfo;
-import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.common.CommentInfo;
-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.RefNames;
-import com.google.gerrit.reviewdb.server.ReviewDbUtil;
-import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.CommentsUtil;
-import com.google.gerrit.server.InternalUser;
-import com.google.gerrit.server.git.RepoRefCache;
-import com.google.gerrit.server.notedb.ChangeBundle;
-import com.google.gerrit.server.notedb.ChangeBundleReader;
-import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.notedb.NoteDbChangeState;
-import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
-import com.google.gerrit.server.notedb.PrimaryStorageMigrator;
-import com.google.gerrit.server.notedb.TestChangeRebuilderWrapper;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gerrit.testing.ConfigSuite;
-import com.google.gerrit.testing.NoteDbMode;
-import com.google.gerrit.testing.TestTimeUtil;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.OrmRuntimeException;
-import com.google.inject.Inject;
-import com.google.inject.util.Providers;
-import java.sql.Timestamp;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Optional;
-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.eclipse.jgit.revwalk.RevWalk;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-@NoHttpd
-public class NoteDbPrimaryIT extends AbstractDaemonTest {
-  @ConfigSuite.Default
-  public static Config defaultConfig() {
-    Config cfg = new Config();
-    cfg.setString("notedb", null, "concurrentWriterTimeout", "0s");
-    cfg.setString("notedb", null, "primaryStorageMigrationTimeout", "1d");
-    cfg.setBoolean("noteDb", null, "testRebuilderWrapper", true);
-    return cfg;
-  }
-
-  @Inject private ChangeBundleReader bundleReader;
-  @Inject private CommentsUtil commentsUtil;
-  @Inject private TestChangeRebuilderWrapper rebuilderWrapper;
-  @Inject private ChangeNotes.Factory changeNotesFactory;
-  @Inject private ChangeUpdate.Factory updateFactory;
-  @Inject private InternalUser.Factory internalUserFactory;
-  @Inject private RetryHelper retryHelper;
-
-  private PrimaryStorageMigrator migrator;
-
-  @Before
-  public void setUp() throws Exception {
-    assume().that(NoteDbMode.get()).isEqualTo(NoteDbMode.READ_WRITE);
-    db = ReviewDbUtil.unwrapDb(db);
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
-    migrator = newMigrator(null);
-  }
-
-  private PrimaryStorageMigrator newMigrator(
-      @Nullable Retryer<NoteDbChangeState> ensureRebuiltRetryer) {
-    return new PrimaryStorageMigrator(
-        cfg,
-        Providers.of(db),
-        repoManager,
-        allUsers,
-        rebuilderWrapper,
-        ensureRebuiltRetryer,
-        changeNotesFactory,
-        queryProvider,
-        updateFactory,
-        internalUserFactory,
-        retryHelper);
-  }
-
-  @After
-  public void tearDown() {
-    TestTimeUtil.useSystemTime();
-  }
-
-  @Test
-  public void updateChange() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getChange().getId();
-    setNoteDbPrimary(id);
-
-    gApi.changes().id(id.get()).current().review(ReviewInput.approve());
-    gApi.changes().id(id.get()).current().submit();
-
-    ChangeInfo info = gApi.changes().id(id.get()).get();
-    assertThat(info.status).isEqualTo(ChangeStatus.MERGED);
-    ApprovalInfo approval = Iterables.getOnlyElement(info.labels.get("Code-Review").all);
-    assertThat(approval._accountId).isEqualTo(admin.id.get());
-    assertThat(approval.value).isEqualTo(2);
-    assertThat(info.messages).hasSize(3);
-    assertThat(Iterables.getLast(info.messages).message)
-        .isEqualTo("Change has been successfully merged by " + admin.fullName);
-
-    ChangeNotes notes = notesFactory.create(db, project, id);
-    assertThat(notes.getChange().getStatus()).isEqualTo(Change.Status.MERGED);
-    assertThat(notes.getChange().getNoteDbState())
-        .isEqualTo(NoteDbChangeState.NOTE_DB_PRIMARY_STATE);
-
-    // Writes weren't reflected in ReviewDb.
-    assertThat(db.changes().get(id).getStatus()).isEqualTo(Change.Status.NEW);
-    assertThat(db.patchSetApprovals().byChange(id)).isEmpty();
-    assertThat(db.changeMessages().byChange(id)).hasSize(1);
-  }
-
-  @Test
-  public void deleteDraftComment() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getChange().getId();
-    setNoteDbPrimary(id);
-
-    DraftInput din = new DraftInput();
-    din.path = PushOneCommit.FILE_NAME;
-    din.line = 1;
-    din.message = "A comment";
-    gApi.changes().id(id.get()).current().createDraft(din);
-
-    CommentInfo di =
-        Iterables.getOnlyElement(
-            gApi.changes().id(id.get()).current().drafts().get(PushOneCommit.FILE_NAME));
-    assertThat(di.message).isEqualTo(din.message);
-
-    assertThat(db.patchComments().draftByChangeFileAuthor(id, din.path, admin.id)).isEmpty();
-
-    gApi.changes().id(id.get()).current().draft(di.id).delete();
-    assertThat(gApi.changes().id(id.get()).current().drafts()).isEmpty();
-  }
-
-  @Test
-  public void deleteVote() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getChange().getId();
-    setNoteDbPrimary(id);
-
-    gApi.changes().id(id.get()).current().review(ReviewInput.approve());
-    List<ApprovalInfo> approvals = gApi.changes().id(id.get()).get().labels.get("Code-Review").all;
-    assertThat(approvals).hasSize(1);
-    assertThat(approvals.get(0).value).isEqualTo(2);
-
-    gApi.changes().id(id.get()).reviewer(admin.id.toString()).deleteVote("Code-Review");
-
-    approvals = gApi.changes().id(id.get()).get().labels.get("Code-Review").all;
-    assertThat(approvals).hasSize(1);
-    assertThat(approvals.get(0).value).isEqualTo(0);
-  }
-
-  @Test
-  public void deleteVoteViaReview() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getChange().getId();
-    setNoteDbPrimary(id);
-
-    gApi.changes().id(id.get()).current().review(ReviewInput.approve());
-    List<ApprovalInfo> approvals = gApi.changes().id(id.get()).get().labels.get("Code-Review").all;
-    assertThat(approvals).hasSize(1);
-    assertThat(approvals.get(0).value).isEqualTo(2);
-
-    gApi.changes().id(id.get()).current().review(ReviewInput.noScore());
-
-    approvals = gApi.changes().id(id.get()).get().labels.get("Code-Review").all;
-    assertThat(approvals).hasSize(1);
-    assertThat(approvals.get(0).value).isEqualTo(0);
-  }
-
-  @Test
-  public void deleteReviewer() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getChange().getId();
-    setNoteDbPrimary(id);
-
-    gApi.changes().id(id.get()).addReviewer(user.id.toString());
-    assertThat(getReviewers(id)).containsExactly(user.id);
-    gApi.changes().id(id.get()).reviewer(user.id.toString()).remove();
-    assertThat(getReviewers(id)).isEmpty();
-  }
-
-  @Test
-  public void readOnlyReviewDb() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getChange().getId();
-    testReadOnly(id);
-  }
-
-  @Test
-  public void readOnlyNoteDb() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getChange().getId();
-    setNoteDbPrimary(id);
-    testReadOnly(id);
-  }
-
-  private void testReadOnly(Change.Id id) throws Exception {
-    Timestamp before = TimeUtil.nowTs();
-    Timestamp until = new Timestamp(before.getTime() + 1000 * 3600);
-
-    // Set read-only.
-    Change c = db.changes().get(id);
-    assertThat(c).named("change " + id).isNotNull();
-    NoteDbChangeState state = NoteDbChangeState.parse(c);
-    state = state.withReadOnlyUntil(until);
-    c.setNoteDbState(state.toString());
-    db.changes().update(Collections.singleton(c));
-
-    assertThat(gApi.changes().id(id.get()).get().subject).isEqualTo(PushOneCommit.SUBJECT);
-    assertThat(gApi.changes().id(id.get()).get().topic).isNull();
-    try {
-      gApi.changes().id(id.get()).topic("a-topic");
-      fail("expected read-only exception");
-    } catch (RestApiException e) {
-      Optional<Throwable> oe =
-          Throwables.getCausalChain(e).stream()
-              .filter(x -> x instanceof OrmRuntimeException)
-              .findFirst();
-      assertThat(oe).named("OrmRuntimeException in causal chain of " + e).isPresent();
-      assertThat(oe.get().getMessage()).contains("read-only");
-    }
-    assertThat(gApi.changes().id(id.get()).get().topic).isNull();
-
-    TestTimeUtil.setClock(new Timestamp(until.getTime() + 1000));
-    assertThat(gApi.changes().id(id.get()).get().subject).isEqualTo(PushOneCommit.SUBJECT);
-    gApi.changes().id(id.get()).topic("a-topic");
-    assertThat(gApi.changes().id(id.get()).get().topic).isEqualTo("a-topic");
-  }
-
-  @Test
-  public void migrateToNoteDb() throws Exception {
-    testMigrateToNoteDb(createChange().getChange().getId());
-  }
-
-  @Test
-  public void migrateToNoteDbWithRebuildingFirst() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getChange().getId();
-
-    Change c = db.changes().get(id);
-    c.setNoteDbState("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
-    db.changes().update(Collections.singleton(c));
-    testMigrateToNoteDb(id);
-  }
-
-  private void testMigrateToNoteDb(Change.Id id) throws Exception {
-    assertThat(PrimaryStorage.of(db.changes().get(id))).isEqualTo(PrimaryStorage.REVIEW_DB);
-    migrator.migrateToNoteDbPrimary(id);
-    assertNoteDbPrimary(id);
-
-    gApi.changes().id(id.get()).topic("a-topic");
-    assertThat(gApi.changes().id(id.get()).get().topic).isEqualTo("a-topic");
-    assertThat(db.changes().get(id).getTopic()).isNull();
-  }
-
-  @Test
-  public void migrateToNoteDbFailsRebuildingOnceAndRetries() throws Exception {
-    Change.Id id = createChange().getChange().getId();
-
-    Change c = db.changes().get(id);
-    c.setNoteDbState("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
-    db.changes().update(Collections.singleton(c));
-    rebuilderWrapper.failNextUpdate();
-
-    migrator =
-        newMigrator(
-            RetryerBuilder.<NoteDbChangeState>newBuilder()
-                .retryIfException()
-                .withStopStrategy(StopStrategies.neverStop())
-                .build());
-    migrator.migrateToNoteDbPrimary(id);
-    assertNoteDbPrimary(id);
-  }
-
-  @Test
-  public void migrateToNoteDbFailsRebuildingAndStops() throws Exception {
-    Change.Id id = createChange().getChange().getId();
-
-    Change c = db.changes().get(id);
-    c.setNoteDbState("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
-    db.changes().update(Collections.singleton(c));
-    rebuilderWrapper.failNextUpdate();
-
-    migrator =
-        newMigrator(
-            RetryerBuilder.<NoteDbChangeState>newBuilder()
-                .retryIfException()
-                .withStopStrategy(StopStrategies.stopAfterAttempt(1))
-                .build());
-    exception.expect(OrmException.class);
-    exception.expectMessage("Retrying failed");
-    migrator.migrateToNoteDbPrimary(id);
-  }
-
-  @Test
-  public void migrateToNoteDbMissingOldState() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getChange().getId();
-
-    Change c = db.changes().get(id);
-    c.setNoteDbState(null);
-    db.changes().update(Collections.singleton(c));
-
-    exception.expect(PrimaryStorageMigrator.NoNoteDbStateException.class);
-    exception.expectMessage("no note_db_state");
-    migrator.migrateToNoteDbPrimary(id);
-  }
-
-  @Test
-  public void migrateToNoteDbLeaseExpires() throws Exception {
-    TestTimeUtil.resetWithClockStep(2, DAYS);
-    exception.expect(OrmRuntimeException.class);
-    exception.expectMessage("read-only lease");
-    migrator.migrateToNoteDbPrimary(createChange().getChange().getId());
-  }
-
-  @Test
-  public void migrateToNoteDbAlreadyReadOnly() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getChange().getId();
-
-    Change c = db.changes().get(id);
-    NoteDbChangeState state = NoteDbChangeState.parse(c);
-    Timestamp until = new Timestamp(TimeUtil.nowMs() + MILLISECONDS.convert(1, DAYS));
-    state = state.withReadOnlyUntil(until);
-    c.setNoteDbState(state.toString());
-    db.changes().update(Collections.singleton(c));
-
-    exception.expect(OrmRuntimeException.class);
-    exception.expectMessage("read-only until " + until);
-    migrator.migrateToNoteDbPrimary(id);
-  }
-
-  @Test
-  public void migrateToNoteDbAlreadyMigrated() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getChange().getId();
-
-    assertThat(PrimaryStorage.of(db.changes().get(id))).isEqualTo(PrimaryStorage.REVIEW_DB);
-    migrator.migrateToNoteDbPrimary(id);
-    assertNoteDbPrimary(id);
-
-    migrator.migrateToNoteDbPrimary(id);
-    assertNoteDbPrimary(id);
-  }
-
-  @Test
-  public void rebuildReviewDb() throws Exception {
-    Change c = createChange().getChange().change();
-    Change.Id id = c.getId();
-
-    CommentInput cin = new CommentInput();
-    cin.line = 1;
-    cin.message = "Published comment";
-    ReviewInput rin = ReviewInput.approve();
-    rin.comments = ImmutableMap.of(PushOneCommit.FILE_NAME, ImmutableList.of(cin));
-    gApi.changes().id(id.get()).current().review(ReviewInput.approve());
-
-    DraftInput din = new DraftInput();
-    din.path = PushOneCommit.FILE_NAME;
-    din.line = 1;
-    din.message = "Draft comment";
-    gApi.changes().id(id.get()).current().createDraft(din);
-    gApi.changes().id(id.get()).current().review(ReviewInput.approve());
-    gApi.changes().id(id.get()).current().createDraft(din);
-
-    assertThat(db.changeMessages().byChange(id)).isNotEmpty();
-    assertThat(db.patchSets().byChange(id)).isNotEmpty();
-    assertThat(db.patchSetApprovals().byChange(id)).isNotEmpty();
-    assertThat(db.patchComments().byChange(id)).isNotEmpty();
-
-    ChangeBundle noteDbBundle =
-        ChangeBundle.fromNotes(commentsUtil, notesFactory.create(db, project, id));
-
-    setNoteDbPrimary(id);
-
-    db.changeMessages().delete(db.changeMessages().byChange(id));
-    db.patchSets().delete(db.patchSets().byChange(id));
-    db.patchSetApprovals().delete(db.patchSetApprovals().byChange(id));
-    db.patchComments().delete(db.patchComments().byChange(id));
-    ChangeMessage bogusMessage =
-        ChangeMessagesUtil.newMessage(
-            c.currentPatchSetId(),
-            identifiedUserFactory.create(admin.getId()),
-            TimeUtil.nowTs(),
-            "some message",
-            null);
-    db.changeMessages().insert(Collections.singleton(bogusMessage));
-
-    rebuilderWrapper.rebuildReviewDb(db, project, id);
-
-    assertThat(db.changeMessages().byChange(id)).isNotEmpty();
-    assertThat(db.patchSets().byChange(id)).isNotEmpty();
-    assertThat(db.patchSetApprovals().byChange(id)).isNotEmpty();
-    assertThat(db.patchComments().byChange(id)).isNotEmpty();
-
-    ChangeBundle reviewDbBundle = bundleReader.fromReviewDb(ReviewDbUtil.unwrapDb(db), id);
-    assertThat(reviewDbBundle.differencesFrom(noteDbBundle)).isEmpty();
-  }
-
-  @Test
-  public void migrateBackToReviewDbPrimary() throws Exception {
-    Change c = createChange().getChange().change();
-    Change.Id id = c.getId();
-
-    migrator.migrateToNoteDbPrimary(id);
-    assertNoteDbPrimary(id);
-
-    gApi.changes().id(id.get()).topic("new-topic");
-    assertThat(gApi.changes().id(id.get()).topic()).isEqualTo("new-topic");
-    assertThat(db.changes().get(id).getTopic()).isNotEqualTo("new-topic");
-
-    migrator.migrateToReviewDbPrimary(id, null);
-    ObjectId metaId;
-    try (Repository repo = repoManager.openRepository(c.getProject());
-        RevWalk rw = new RevWalk(repo)) {
-      metaId = repo.exactRef(RefNames.changeMetaRef(id)).getObjectId();
-      RevCommit commit = rw.parseCommit(metaId);
-      rw.parseBody(commit);
-      assertThat(commit.getFullMessage())
-          .contains("Read-only-until: " + formatTime(serverIdent.get(), new Timestamp(0)));
-    }
-    NoteDbChangeState state = NoteDbChangeState.parse(db.changes().get(id));
-    assertThat(state.getPrimaryStorage()).isEqualTo(PrimaryStorage.REVIEW_DB);
-    assertThat(state.getChangeMetaId()).isEqualTo(metaId);
-    assertThat(gApi.changes().id(id.get()).topic()).isEqualTo("new-topic");
-    assertThat(db.changes().get(id).getTopic()).isEqualTo("new-topic");
-
-    ChangeNotes notes = notesFactory.create(db, project, id);
-    assertThat(notes.getRevision()).isEqualTo(metaId); // No rebuilding, change was up to date.
-    assertThat(notes.getReadOnlyUntil()).isNotNull();
-
-    gApi.changes().id(id.get()).topic("reviewdb-topic");
-    assertThat(db.changes().get(id).getTopic()).isEqualTo("reviewdb-topic");
-  }
-
-  private void setNoteDbPrimary(Change.Id id) throws Exception {
-    Change c = db.changes().get(id);
-    assertThat(c).named("change " + id).isNotNull();
-    NoteDbChangeState state = NoteDbChangeState.parse(c);
-    assertThat(state.getPrimaryStorage()).named("storage of " + id).isEqualTo(REVIEW_DB);
-
-    try (Repository changeRepo = repoManager.openRepository(c.getProject());
-        Repository allUsersRepo = repoManager.openRepository(allUsers)) {
-      assertThat(state.isUpToDate(new RepoRefCache(changeRepo), new RepoRefCache(allUsersRepo)))
-          .named("change " + id + " up to date")
-          .isTrue();
-    }
-
-    c.setNoteDbState(NoteDbChangeState.NOTE_DB_PRIMARY_STATE);
-    db.changes().update(Collections.singleton(c));
-  }
-
-  private void assertNoteDbPrimary(Change.Id id) throws Exception {
-    assertThat(PrimaryStorage.of(db.changes().get(id))).isEqualTo(PrimaryStorage.NOTE_DB);
-  }
-
-  private List<Account.Id> getReviewers(Change.Id id) throws Exception {
-    return gApi.changes().id(id.get()).get().reviewers.values().stream()
-        .flatMap(Collection::stream)
-        .map(a -> new Account.Id(a._accountId))
-        .collect(toList());
-  }
-}
diff --git a/javatests/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java b/javatests/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java
deleted file mode 100644
index c249973..0000000
--- a/javatests/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java
+++ /dev/null
@@ -1,667 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.acceptance.server.notedb;
-
-import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
-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.server.notedb.NoteDbChangeState.NOTE_DB_PRIMARY_STATE;
-import static com.google.gerrit.server.notedb.NotesMigrationState.NOTE_DB;
-import static com.google.gerrit.server.notedb.NotesMigrationState.READ_WRITE_NO_SEQUENCE;
-import static com.google.gerrit.server.notedb.NotesMigrationState.READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY;
-import static com.google.gerrit.server.notedb.NotesMigrationState.READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY;
-import static com.google.gerrit.server.notedb.NotesMigrationState.REVIEW_DB;
-import static com.google.gerrit.server.notedb.NotesMigrationState.WRITE;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.Comparator.naturalOrder;
-import static org.easymock.EasyMock.createStrictMock;
-import static org.easymock.EasyMock.expectLastCall;
-import static org.easymock.EasyMock.replay;
-import static org.easymock.EasyMock.verify;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSortedSet;
-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.Sandboxed;
-import com.google.gerrit.acceptance.UseLocalDisk;
-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.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CommentsUtil;
-import com.google.gerrit.server.Sequences;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.notedb.ChangeBundle;
-import com.google.gerrit.server.notedb.ChangeBundleReader;
-import com.google.gerrit.server.notedb.NoteDbChangeState;
-import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
-import com.google.gerrit.server.notedb.NoteDbChangeState.RefState;
-import com.google.gerrit.server.notedb.NotesMigrationState;
-import com.google.gerrit.server.notedb.rebuild.MigrationException;
-import com.google.gerrit.server.notedb.rebuild.NoteDbMigrator;
-import com.google.gerrit.server.notedb.rebuild.NotesMigrationStateListener;
-import com.google.gerrit.server.schema.ReviewDbFactory;
-import com.google.gerrit.testing.ConfigSuite;
-import com.google.gerrit.testing.NoteDbMode;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
-import java.util.stream.Stream;
-import org.eclipse.jgit.internal.storage.file.FileRepository;
-import org.eclipse.jgit.junit.TestRepository;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectLoader;
-import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.util.FS;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-@Sandboxed
-@UseLocalDisk
-@NoHttpd
-public class OnlineNoteDbMigrationIT extends AbstractDaemonTest {
-  private static final String INVALID_STATE = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
-
-  @ConfigSuite.Default
-  public static Config defaultConfig() {
-    Config cfg = new Config();
-    cfg.setInt("noteDb", "changes", "sequenceBatchSize", 10);
-    cfg.setInt("noteDb", "changes", "initialSequenceGap", 500);
-    return cfg;
-  }
-
-  // Tests in this class are generally interested in the actual ReviewDb contents, but the shifting
-  // migration state may result in various kinds of wrappers showing up unexpectedly.
-  @Inject @ReviewDbFactory private SchemaFactory<ReviewDb> schemaFactory;
-
-  @Inject private ChangeBundleReader changeBundleReader;
-  @Inject private CommentsUtil commentsUtil;
-  @Inject private DynamicSet<NotesMigrationStateListener> listeners;
-  @Inject private Provider<NoteDbMigrator.Builder> migratorBuilderProvider;
-  @Inject private Sequences sequences;
-  @Inject private SitePaths sitePaths;
-
-  private FileBasedConfig noteDbConfig;
-  private List<RegistrationHandle> addedListeners;
-
-  @Before
-  public void setUp() throws Exception {
-    assume().that(NoteDbMode.get()).isEqualTo(NoteDbMode.OFF);
-    // Unlike in the running server, for tests, we don't stack notedb.config on gerrit.config.
-    noteDbConfig = new FileBasedConfig(sitePaths.notedb_config.toFile(), FS.detect());
-    assertNotesMigrationState(REVIEW_DB, false, false);
-    addedListeners = new ArrayList<>();
-  }
-
-  @After
-  public void tearDown() throws Exception {
-    if (addedListeners != null) {
-      addedListeners.forEach(RegistrationHandle::remove);
-      addedListeners = null;
-    }
-  }
-
-  @Test
-  public void preconditionsFail() throws Exception {
-    List<Change.Id> cs = ImmutableList.of(new Change.Id(1));
-    List<Project.NameKey> ps = ImmutableList.of(new Project.NameKey("p"));
-    assertMigrationException(
-        "Cannot rebuild without noteDb.changes.write=true", b -> b, NoteDbMigrator::rebuild);
-    assertMigrationException(
-        "Cannot combine changes, projects and skipProjects",
-        b -> b.setChanges(cs).setProjects(ps),
-        m -> {});
-    assertMigrationException(
-        "Cannot combine changes, projects and skipProjects",
-        b -> b.setChanges(cs).setSkipProjects(ps),
-        m -> {});
-    assertMigrationException(
-        "Cannot combine changes, projects and skipProjects",
-        b -> b.setProjects(ps).setSkipProjects(ps),
-        m -> {});
-    assertMigrationException(
-        "Cannot set changes or projects or skipProjects during full migration",
-        b -> b.setChanges(cs),
-        NoteDbMigrator::migrate);
-    assertMigrationException(
-        "Cannot set changes or projects or skipProjects during full migration",
-        b -> b.setProjects(ps),
-        NoteDbMigrator::migrate);
-    assertMigrationException(
-        "Cannot set changes or projects or skipProjects during full migration",
-        b -> b.setSkipProjects(ps),
-        NoteDbMigrator::migrate);
-
-    setNotesMigrationState(READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY);
-    assertMigrationException(
-        "Migration has already progressed past the endpoint of the \"trial mode\" state",
-        b -> b.setTrialMode(true),
-        NoteDbMigrator::migrate);
-
-    setNotesMigrationState(READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY);
-    assertMigrationException(
-        "Cannot force rebuild changes; NoteDb is already the primary storage for some changes",
-        b -> b.setForceRebuild(true),
-        NoteDbMigrator::migrate);
-  }
-
-  @Test
-  @GerritConfig(name = "noteDb.changes.initialSequenceGap", value = "-7")
-  public void initialSequenceGapMustBeNonNegative() throws Exception {
-    setNotesMigrationState(READ_WRITE_NO_SEQUENCE);
-    assertMigrationException("Sequence gap must be non-negative: -7", b -> b, m -> {});
-  }
-
-  @Test
-  public void rebuildOneChangeTrialModeAndForceRebuild() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getChange().getId();
-
-    migrate(b -> b.setTrialMode(true));
-    assertNotesMigrationState(READ_WRITE_NO_SEQUENCE, false, true);
-
-    ObjectId oldMetaId;
-    try (Repository repo = repoManager.openRepository(project);
-        ReviewDb db = schemaFactory.open()) {
-      Ref ref = repo.exactRef(RefNames.changeMetaRef(id));
-      assertThat(ref).isNotNull();
-      oldMetaId = ref.getObjectId();
-
-      Change c = db.changes().get(id);
-      assertThat(c).isNotNull();
-      NoteDbChangeState state = NoteDbChangeState.parse(c);
-      assertThat(state).isNotNull();
-      assertThat(state.getPrimaryStorage()).isEqualTo(PrimaryStorage.REVIEW_DB);
-      assertThat(state.getRefState()).hasValue(RefState.create(oldMetaId, ImmutableMap.of()));
-
-      // Force change to be out of date, and change topic so it will get rebuilt as something other
-      // than oldMetaId.
-      c.setNoteDbState(INVALID_STATE);
-      c.setTopic(name("a-new-topic"));
-      db.changes().update(ImmutableList.of(c));
-    }
-
-    migrate(b -> b.setTrialMode(true));
-    assertNotesMigrationState(READ_WRITE_NO_SEQUENCE, false, true);
-
-    try (Repository repo = repoManager.openRepository(project);
-        ReviewDb db = schemaFactory.open()) {
-      // Change is out of date, but was not rebuilt without forceRebuild.
-      assertThat(repo.exactRef(RefNames.changeMetaRef(id)).getObjectId()).isEqualTo(oldMetaId);
-      Change c = db.changes().get(id);
-      assertThat(c.getNoteDbState()).isEqualTo(INVALID_STATE);
-    }
-
-    migrate(b -> b.setTrialMode(true).setForceRebuild(true));
-    assertNotesMigrationState(READ_WRITE_NO_SEQUENCE, false, true);
-
-    try (Repository repo = repoManager.openRepository(project);
-        ReviewDb db = schemaFactory.open()) {
-      Ref ref = repo.exactRef(RefNames.changeMetaRef(id));
-      assertThat(ref).isNotNull();
-      ObjectId newMetaId = ref.getObjectId();
-      assertThat(newMetaId).isNotEqualTo(oldMetaId);
-
-      NoteDbChangeState state = NoteDbChangeState.parse(db.changes().get(id));
-      assertThat(state).isNotNull();
-      assertThat(state.getPrimaryStorage()).isEqualTo(PrimaryStorage.REVIEW_DB);
-      assertThat(state.getRefState()).hasValue(RefState.create(newMetaId, ImmutableMap.of()));
-    }
-  }
-
-  @Test
-  public void autoMigrateTrialMode() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getChange().getId();
-
-    migrate(b -> b.setAutoMigrate(true).setTrialMode(true).setStopAtStateForTesting(WRITE));
-    assertNotesMigrationState(WRITE, true, true);
-
-    migrate(b -> b);
-    // autoMigrate is still enabled so that we can continue the migration by only unsetting trial.
-    assertNotesMigrationState(READ_WRITE_NO_SEQUENCE, true, true);
-
-    ObjectId metaId;
-    try (Repository repo = repoManager.openRepository(project);
-        ReviewDb db = schemaFactory.open()) {
-      Ref ref = repo.exactRef(RefNames.changeMetaRef(id));
-      assertThat(ref).isNotNull();
-      metaId = ref.getObjectId();
-      NoteDbChangeState state = NoteDbChangeState.parse(db.changes().get(id));
-      assertThat(state).isNotNull();
-      assertThat(state.getPrimaryStorage()).isEqualTo(PrimaryStorage.REVIEW_DB);
-      assertThat(state.getRefState()).hasValue(RefState.create(metaId, ImmutableMap.of()));
-    }
-
-    // Unset trial mode and the next migration runs to completion.
-    noteDbConfig.load();
-    NoteDbMigrator.setTrialMode(noteDbConfig, false);
-    noteDbConfig.save();
-
-    migrate(b -> b);
-    assertNotesMigrationState(NOTE_DB, false, false);
-
-    try (Repository repo = repoManager.openRepository(project);
-        ReviewDb db = schemaFactory.open()) {
-      Ref ref = repo.exactRef(RefNames.changeMetaRef(id));
-      assertThat(ref).isNotNull();
-      assertThat(ref.getObjectId()).isEqualTo(metaId);
-      NoteDbChangeState state = NoteDbChangeState.parse(db.changes().get(id));
-      assertThat(state).isNotNull();
-      assertThat(state.getPrimaryStorage()).isEqualTo(PrimaryStorage.NOTE_DB);
-    }
-  }
-
-  @Test
-  public void rebuildSubsetOfChanges() throws Exception {
-    setNotesMigrationState(WRITE);
-
-    PushOneCommit.Result r1 = createChange();
-    PushOneCommit.Result r2 = createChange();
-    Change.Id id1 = r1.getChange().getId();
-    Change.Id id2 = r2.getChange().getId();
-
-    invalidateNoteDbState(id1, id2);
-    migrate(b -> b.setChanges(ImmutableList.of(id2)), NoteDbMigrator::rebuild);
-    assertNotRebuilt(id1);
-    assertRebuilt(id2);
-  }
-
-  @Test
-  public void rebuildSubsetOfProjects() throws Exception {
-    setNotesMigrationState(WRITE);
-
-    Project.NameKey p2 = createProject("project2");
-    TestRepository<?> tr2 = cloneProject(p2, admin);
-
-    PushOneCommit.Result r1 = createChange();
-    PushOneCommit.Result r2 = pushFactory.create(db, admin.getIdent(), tr2).to("refs/for/master");
-    Change.Id id1 = r1.getChange().getId();
-    Change.Id id2 = r2.getChange().getId();
-
-    invalidateNoteDbState(id1, id2);
-    migrate(b -> b.setProjects(ImmutableList.of(p2)), NoteDbMigrator::rebuild);
-    assertNotRebuilt(id1);
-    assertRebuilt(id2);
-  }
-
-  @Test
-  public void rebuildNonSkippedProjects() throws Exception {
-    setNotesMigrationState(WRITE);
-
-    Project.NameKey p2 = createProject("project2");
-    TestRepository<?> tr2 = cloneProject(p2, admin);
-    Project.NameKey p3 = createProject("project3");
-    TestRepository<?> tr3 = cloneProject(p3, admin);
-
-    PushOneCommit.Result r1 = createChange();
-    PushOneCommit.Result r2 = pushFactory.create(db, admin.getIdent(), tr2).to("refs/for/master");
-    PushOneCommit.Result r3 = pushFactory.create(db, admin.getIdent(), tr3).to("refs/for/master");
-    Change.Id id1 = r1.getChange().getId();
-    Change.Id id2 = r2.getChange().getId();
-    Change.Id id3 = r3.getChange().getId();
-
-    invalidateNoteDbState(id1, id2, id3);
-    migrate(b -> b.setSkipProjects(ImmutableList.of(p3)), NoteDbMigrator::rebuild);
-    assertRebuilt(id1, id2);
-    assertNotRebuilt(id3);
-  }
-
-  private void invalidateNoteDbState(Change.Id... ids) throws OrmException {
-    List<Change> list = new ArrayList<>(ids.length);
-    try (ReviewDb db = schemaFactory.open()) {
-      for (Change.Id id : ids) {
-        Change c = db.changes().get(id);
-        c.setNoteDbState(INVALID_STATE);
-        list.add(c);
-      }
-      db.changes().update(list);
-    }
-  }
-
-  private void assertRebuilt(Change.Id... ids) throws OrmException {
-    try (ReviewDb db = schemaFactory.open()) {
-      for (Change.Id id : ids) {
-        NoteDbChangeState s = NoteDbChangeState.parse(db.changes().get(id));
-        assertThat(s.getChangeMetaId().name()).isNotEqualTo(INVALID_STATE);
-      }
-    }
-  }
-
-  private void assertNotRebuilt(Change.Id... ids) throws OrmException {
-    try (ReviewDb db = schemaFactory.open()) {
-      for (Change.Id id : ids) {
-        NoteDbChangeState s = NoteDbChangeState.parse(db.changes().get(id));
-        assertThat(s.getChangeMetaId().name()).isEqualTo(INVALID_STATE);
-      }
-    }
-  }
-
-  @Test
-  public void enableSequencesNoGap() throws Exception {
-    testEnableSequences(0, 3, "13");
-  }
-
-  @Test
-  public void enableSequencesWithGap() throws Exception {
-    testEnableSequences(-1, 502, "512");
-  }
-
-  private void testEnableSequences(int builderOption, int expectedFirstId, String expectedRefValue)
-      throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getChange().getId();
-    assertThat(id.get()).isEqualTo(1);
-
-    migrate(
-        b ->
-            b.setSequenceGap(builderOption)
-                .setStopAtStateForTesting(READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY));
-
-    assertThat(sequences.nextChangeId()).isEqualTo(expectedFirstId);
-    assertThat(sequences.nextChangeId()).isEqualTo(expectedFirstId + 1);
-
-    try (Repository repo = repoManager.openRepository(allProjects);
-        ObjectReader reader = repo.newObjectReader()) {
-      Ref ref = repo.exactRef("refs/sequences/changes");
-      assertThat(ref).isNotNull();
-      ObjectLoader loader = reader.open(ref.getObjectId());
-      assertThat(loader.getType()).isEqualTo(Constants.OBJ_BLOB);
-      // Acquired a block of 10 to serve the first nextChangeId call after migration.
-      assertThat(new String(loader.getCachedBytes(), UTF_8)).isEqualTo(expectedRefValue);
-    }
-
-    try (ReviewDb db = schemaFactory.open()) {
-      // Underlying, unused ReviewDb is still on its own sequence.
-      @SuppressWarnings("deprecation")
-      int nextFromReviewDb = db.nextChangeId();
-      assertThat(nextFromReviewDb).isEqualTo(3);
-    }
-  }
-
-  @Test
-  public void fullMigrationSameThread() throws Exception {
-    testFullMigration(1);
-  }
-
-  @Test
-  public void fullMigrationMultipleThreads() throws Exception {
-    testFullMigration(2);
-  }
-
-  private void testFullMigration(int threads) throws Exception {
-    PushOneCommit.Result r1 = createChange();
-    PushOneCommit.Result r2 = createChange();
-    Change.Id id1 = r1.getChange().getId();
-    Change.Id id2 = r2.getChange().getId();
-
-    Set<String> objectFiles = getObjectFiles(project);
-    assertThat(objectFiles).isNotEmpty();
-
-    migrate(b -> b.setThreads(threads));
-
-    assertNotesMigrationState(NOTE_DB, false, false);
-    assertThat(sequences.nextChangeId()).isEqualTo(503);
-    assertThat(getObjectFiles(project)).containsExactlyElementsIn(objectFiles);
-
-    ObjectId oldMetaId = null;
-    int rowVersion = 0;
-    try (ReviewDb db = schemaFactory.open();
-        Repository repo = repoManager.openRepository(project)) {
-      for (Change.Id id : ImmutableList.of(id1, id2)) {
-        String refName = RefNames.changeMetaRef(id);
-        Ref ref = repo.exactRef(refName);
-        assertThat(ref).named(refName).isNotNull();
-
-        Change c = db.changes().get(id);
-        assertThat(c.getTopic()).named("topic of change %s", id).isNull();
-        NoteDbChangeState s = NoteDbChangeState.parse(c);
-        assertThat(s.getPrimaryStorage())
-            .named("primary storage of change %s", id)
-            .isEqualTo(PrimaryStorage.NOTE_DB);
-        assertThat(s.getRefState()).named("ref state of change %s").isEmpty();
-
-        if (id.equals(id1)) {
-          oldMetaId = ref.getObjectId();
-          rowVersion = c.getRowVersion();
-        }
-      }
-    }
-
-    // Do not open a new context, to simulate races with other threads that opened a context earlier
-    // in the migration process; this needs to work.
-    gApi.changes().id(id1.get()).topic(name("a-topic"));
-
-    // Of course, it should also work with a new context.
-    resetCurrentApiUser();
-    gApi.changes().id(id1.get()).topic(name("another-topic"));
-
-    try (ReviewDb db = schemaFactory.open();
-        Repository repo = repoManager.openRepository(project)) {
-      assertThat(repo.exactRef(RefNames.changeMetaRef(id1)).getObjectId()).isNotEqualTo(oldMetaId);
-
-      Change c = db.changes().get(id1);
-      assertThat(c.getTopic()).isNull();
-      assertThat(c.getRowVersion()).isEqualTo(rowVersion);
-    }
-  }
-
-  @Test
-  public void fullMigrationOneChangeWithNoPatchSets() throws Exception {
-    PushOneCommit.Result r1 = createChange();
-    PushOneCommit.Result r2 = createChange();
-    Change.Id id1 = r1.getChange().getId();
-    Change.Id id2 = r2.getChange().getId();
-
-    db.changes().beginTransaction(id2);
-    try {
-      db.patchSets().delete(db.patchSets().byChange(id2));
-      db.commit();
-    } finally {
-      db.rollback();
-    }
-
-    migrate(b -> b);
-    assertNotesMigrationState(NOTE_DB, false, false);
-
-    try (ReviewDb db = schemaFactory.open();
-        Repository repo = repoManager.openRepository(project)) {
-      assertThat(repo.exactRef(RefNames.changeMetaRef(id1))).isNotNull();
-      assertThat(db.changes().get(id1).getNoteDbState()).isEqualTo(NOTE_DB_PRIMARY_STATE);
-
-      // A change with no patch sets is so corrupt that it is completely skipped by the migration
-      // process.
-      assertThat(repo.exactRef(RefNames.changeMetaRef(id2))).isNull();
-      assertThat(db.changes().get(id2).getNoteDbState()).isNull();
-    }
-  }
-
-  @Test
-  public void fullMigrationMissingPatchSetRefs() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getChange().getId();
-
-    try (Repository repo = repoManager.openRepository(project)) {
-      RefUpdate u = repo.updateRef(new PatchSet.Id(id, 1).toRefName());
-      u.setForceUpdate(true);
-      assertThat(u.delete()).isEqualTo(RefUpdate.Result.FORCED);
-    }
-
-    ChangeBundle reviewDbBundle;
-    try (ReviewDb db = schemaFactory.open()) {
-      reviewDbBundle = changeBundleReader.fromReviewDb(db, id);
-    }
-
-    migrate(b -> b);
-    assertNotesMigrationState(NOTE_DB, false, false);
-
-    try (ReviewDb db = schemaFactory.open();
-        Repository repo = repoManager.openRepository(project)) {
-      // Change migrated successfully even though it was missing patch set refs.
-      assertThat(repo.exactRef(RefNames.changeMetaRef(id))).isNotNull();
-      assertThat(db.changes().get(id).getNoteDbState()).isEqualTo(NOTE_DB_PRIMARY_STATE);
-
-      ChangeBundle noteDbBundle =
-          ChangeBundle.fromNotes(commentsUtil, notesFactory.createChecked(db, project, id));
-      assertThat(noteDbBundle.differencesFrom(reviewDbBundle)).isEmpty();
-    }
-  }
-
-  @Test
-  public void autoMigrationConfig() throws Exception {
-    createChange();
-
-    migrate(b -> b.setStopAtStateForTesting(WRITE));
-    assertNotesMigrationState(WRITE, false, false);
-
-    migrate(b -> b.setAutoMigrate(true).setStopAtStateForTesting(READ_WRITE_NO_SEQUENCE));
-    assertNotesMigrationState(READ_WRITE_NO_SEQUENCE, true, false);
-
-    migrate(b -> b);
-    assertNotesMigrationState(NOTE_DB, false, false);
-  }
-
-  @Test
-  public void notesMigrationStateListener() throws Exception {
-    NotesMigrationStateListener listener = createStrictMock(NotesMigrationStateListener.class);
-    listener.preStateChange(REVIEW_DB, WRITE);
-    expectLastCall();
-    listener.preStateChange(WRITE, READ_WRITE_NO_SEQUENCE);
-    expectLastCall();
-    listener.preStateChange(READ_WRITE_NO_SEQUENCE, READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY);
-    expectLastCall();
-    listener.preStateChange(
-        READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY, READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY);
-    listener.preStateChange(READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY, NOTE_DB);
-    expectLastCall();
-    replay(listener);
-    addListener(listener);
-
-    createChange();
-    migrate(b -> b);
-    assertNotesMigrationState(NOTE_DB, false, false);
-    verify(listener);
-  }
-
-  @Test
-  public void notesMigrationStateListenerFails() throws Exception {
-    NotesMigrationStateListener listener = createStrictMock(NotesMigrationStateListener.class);
-    listener.preStateChange(REVIEW_DB, WRITE);
-    expectLastCall();
-    listener.preStateChange(WRITE, READ_WRITE_NO_SEQUENCE);
-    IOException listenerException = new IOException("Listener failed");
-    expectLastCall().andThrow(listenerException);
-    replay(listener);
-    addListener(listener);
-
-    createChange();
-    try {
-      migrate(b -> b);
-      fail("expected IOException");
-    } catch (IOException e) {
-      assertThat(e).isSameAs(listenerException);
-    }
-    assertNotesMigrationState(WRITE, false, false);
-    verify(listener);
-  }
-
-  private void assertNotesMigrationState(
-      NotesMigrationState expected, boolean autoMigrate, boolean trialMode) throws Exception {
-    assertThat(NotesMigrationState.forNotesMigration(notesMigration)).hasValue(expected);
-    noteDbConfig.load();
-    assertThat(NotesMigrationState.forConfig(noteDbConfig)).hasValue(expected);
-    assertThat(NoteDbMigrator.getAutoMigrate(noteDbConfig))
-        .named("noteDb.changes.autoMigrate")
-        .isEqualTo(autoMigrate);
-    assertThat(NoteDbMigrator.getTrialMode(noteDbConfig))
-        .named("noteDb.changes.trial")
-        .isEqualTo(trialMode);
-  }
-
-  private void setNotesMigrationState(NotesMigrationState state) throws Exception {
-    noteDbConfig.load();
-    state.setConfigValues(noteDbConfig);
-    noteDbConfig.save();
-    notesMigration.setFrom(state);
-  }
-
-  @FunctionalInterface
-  interface PrepareBuilder {
-    NoteDbMigrator.Builder prepare(NoteDbMigrator.Builder b) throws Exception;
-  }
-
-  @FunctionalInterface
-  interface RunMigration {
-    void run(NoteDbMigrator m) throws Exception;
-  }
-
-  private void migrate(PrepareBuilder b) throws Exception {
-    migrate(b, NoteDbMigrator::migrate);
-  }
-
-  private void migrate(PrepareBuilder b, RunMigration m) throws Exception {
-    try (NoteDbMigrator migrator = b.prepare(migratorBuilderProvider.get()).build()) {
-      m.run(migrator);
-    }
-  }
-
-  private void assertMigrationException(
-      String expectMessageContains, PrepareBuilder b, RunMigration m) throws Exception {
-    try {
-      migrate(b, m);
-      fail("expected MigrationException");
-    } catch (MigrationException e) {
-      assertThat(e).hasMessageThat().contains(expectMessageContains);
-    }
-  }
-
-  private void addListener(NotesMigrationStateListener listener) {
-    addedListeners.add(listeners.add("gerrit", listener));
-  }
-
-  private ImmutableSortedSet<String> getObjectFiles(Project.NameKey project) throws Exception {
-    try (Repository repo = repoManager.openRepository(project);
-        Stream<Path> paths =
-            Files.walk(((FileRepository) repo).getObjectDatabase().getDirectory().toPath())) {
-      return paths
-          .filter(path -> !Files.isDirectory(path))
-          .map(Path::toString)
-          .filter(name -> !name.endsWith(".pack") && !name.endsWith(".idx"))
-          .collect(toImmutableSortedSet(naturalOrder()));
-    }
-  }
-}
diff --git a/javatests/com/google/gerrit/acceptance/server/permissions/PermissionBackendConditionIT.java b/javatests/com/google/gerrit/acceptance/server/permissions/PermissionBackendConditionIT.java
index 720eeed..2919e5f 100644
--- a/javatests/com/google/gerrit/acceptance/server/permissions/PermissionBackendConditionIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/permissions/PermissionBackendConditionIT.java
@@ -18,6 +18,7 @@
 import static org.junit.Assert.assertNotEquals;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.extensions.conditions.BooleanCondition;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
@@ -34,6 +35,7 @@
 public class PermissionBackendConditionIT extends AbstractDaemonTest {
 
   @Inject PermissionBackend pb;
+  @Inject ProjectOperations projectOperations;
 
   @Test
   public void globalPermissions_sameUserAndPermissionEquals() throws Exception {
@@ -110,7 +112,7 @@
 
   @Test
   public void projectPermissions_differentResourceSameUserDoesNotEqual() throws Exception {
-    Project.NameKey project2 = createProject("p2");
+    Project.NameKey project2 = projectOperations.newProject().create();
     BooleanCondition cond1 = pb.user(user()).project(project).testCond(ProjectPermission.READ);
     BooleanCondition cond2 = pb.user(user()).project(project2).testCond(ProjectPermission.READ);
 
@@ -152,7 +154,7 @@
   @Test
   public void refPermissions_differentResourceAndSameUserDoesNotEqual2() throws Exception {
     Branch.NameKey branch1 = new Branch.NameKey(project, "branch");
-    Branch.NameKey branch2 = new Branch.NameKey(createProject("p2"), "branch");
+    Branch.NameKey branch2 = new Branch.NameKey(projectOperations.newProject().create(), "branch");
     BooleanCondition cond1 = pb.user(user()).ref(branch1).testCond(RefPermission.READ);
     BooleanCondition cond2 = pb.user(user()).ref(branch2).testCond(RefPermission.READ);
 
@@ -161,10 +163,10 @@
   }
 
   private CurrentUser user() {
-    return identifiedUserFactory.create(user.id);
+    return identifiedUserFactory.create(user.id());
   }
 
   private CurrentUser admin() {
-    return identifiedUserFactory.create(admin.id);
+    return identifiedUserFactory.create(admin.id());
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/server/project/CustomLabelIT.java b/javatests/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
index 8b0c56b..6cbe40e 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
@@ -80,21 +80,12 @@
       u.save();
     }
 
-    eventListenerRegistration =
-        source.add(
-            "gerrit",
-            new CommentAddedListener() {
-              @Override
-              public void onCommentAdded(Event event) {
-                lastCommentAddedEvent = event;
-              }
-            });
+    eventListenerRegistration = source.add("gerrit", event -> lastCommentAddedEvent = event);
   }
 
   @After
   public void cleanup() {
     eventListenerRegistration.remove();
-    db.close();
   }
 
   @Test
@@ -187,7 +178,7 @@
     saveLabelConfig();
     PushOneCommit.Result r = createChange();
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     gApi.changes().id(r.getChangeId()).addReviewer(in);
 
     ReviewInput input = new ReviewInput().label(P.getName(), 0);
diff --git a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
index cfdd781..4ed16ee 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
@@ -22,6 +22,8 @@
 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.Permission;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.StarsInput;
@@ -32,6 +34,7 @@
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.server.git.NotifyConfig;
 import com.google.gerrit.testing.FakeEmailSender.Message;
+import com.google.inject.Inject;
 import java.util.EnumSet;
 import java.util.List;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
@@ -40,6 +43,9 @@
 
 @NoHttpd
 public class ProjectWatchIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @Test
   public void newPatchSetsNotifyConfig() throws Exception {
     Address addr = new Address("Watcher", "watcher@example.com");
@@ -57,20 +63,19 @@
 
     PushOneCommit.Result r =
         pushFactory
-            .create(db, admin.getIdent(), testRepo, "original subject", "a", "a1")
+            .create(admin.newIdent(), testRepo, "original subject", "a", "a1")
             .to("refs/for/master");
     r.assertOkStatus();
 
     r =
         pushFactory
-            .create(
-                db, admin.getIdent(), testRepo, "super sekret subject", "a", "a2", r.getChangeId())
+            .create(admin.newIdent(), testRepo, "super sekret subject", "a", "a2", r.getChangeId())
             .to("refs/for/master");
     r.assertOkStatus();
 
     r =
         pushFactory
-            .create(db, admin.getIdent(), testRepo, "back to original subject", "a", "a3")
+            .create(admin.newIdent(), testRepo, "back to original subject", "a", "a3")
             .to("refs/for/master");
     r.assertOkStatus();
 
@@ -99,13 +104,13 @@
     sender.clear();
     PushOneCommit.Result r =
         pushFactory
-            .create(db, admin.getIdent(), testRepo, "private change", "a", "a1")
+            .create(admin.newIdent(), testRepo, "private change", "a", "a1")
             .to("refs/for/master%private");
     r.assertOkStatus();
 
     assertThat(sender.getMessages()).isEmpty();
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     ReviewInput in = new ReviewInput();
     in.message = "comment";
     gApi.changes().id(r.getChangeId()).current().review(in);
@@ -129,16 +134,14 @@
     }
 
     PushOneCommit.Result r =
-        pushFactory
-            .create(db, admin.getIdent(), testRepo, "subject", "a", "a1")
-            .to("refs/for/master");
+        pushFactory.create(admin.newIdent(), testRepo, "subject", "a", "a1").to("refs/for/master");
     r.assertOkStatus();
 
     sender.clear();
 
     r =
         pushFactory
-            .create(db, admin.getIdent(), testRepo, "subject", "a", "a2", r.getChangeId())
+            .create(admin.newIdent(), testRepo, "subject", "a", "a2", r.getChangeId())
             .to("refs/for/master%private");
     r.assertOkStatus();
 
@@ -162,13 +165,13 @@
     sender.clear();
     PushOneCommit.Result r =
         pushFactory
-            .create(db, admin.getIdent(), testRepo, "wip change", "a", "a1")
+            .create(admin.newIdent(), testRepo, "wip change", "a", "a1")
             .to("refs/for/master%wip");
     r.assertOkStatus();
 
     assertThat(sender.getMessages()).isEmpty();
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     ReviewInput in = new ReviewInput();
     in.message = "comment";
     gApi.changes().id(r.getChangeId()).current().review(in);
@@ -191,16 +194,14 @@
     }
 
     PushOneCommit.Result r =
-        pushFactory
-            .create(db, admin.getIdent(), testRepo, "subject", "a", "a1")
-            .to("refs/for/master");
+        pushFactory.create(admin.newIdent(), testRepo, "subject", "a", "a1").to("refs/for/master");
     r.assertOkStatus();
 
     sender.clear();
 
     r =
         pushFactory
-            .create(db, admin.getIdent(), testRepo, "subject", "a", "a2", r.getChangeId())
+            .create(admin.newIdent(), testRepo, "subject", "a", "a2", r.getChangeId())
             .to("refs/for/master%wip");
     r.assertOkStatus();
 
@@ -210,28 +211,28 @@
   @Test
   public void watchProject() throws Exception {
     // watch project
-    String watchedProject = createProject("watchedProject").get();
-    setApiUser(user);
+    String watchedProject = projectOperations.newProject().create().get();
+    requestScopeOperations.setApiUser(user.id());
     watch(watchedProject);
 
     // push a change to watched project -> should trigger email notification
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     TestRepository<InMemoryRepository> watchedRepo =
         cloneProject(new Project.NameKey(watchedProject), admin);
     PushOneCommit.Result r =
         pushFactory
-            .create(db, admin.getIdent(), watchedRepo, "TRIGGER", "a", "a1")
+            .create(admin.newIdent(), watchedRepo, "TRIGGER", "a", "a1")
             .to("refs/for/master");
     r.assertOkStatus();
 
     // push a change to non-watched project -> should not trigger email
     // notification
-    String notWatchedProject = createProject("otherProject").get();
+    String notWatchedProject = projectOperations.newProject().create().get();
     TestRepository<InMemoryRepository> notWatchedRepo =
         cloneProject(new Project.NameKey(notWatchedProject), admin);
     r =
         pushFactory
-            .create(db, admin.getIdent(), notWatchedRepo, "DONT_TRIGGER", "a", "a1")
+            .create(admin.newIdent(), notWatchedRepo, "DONT_TRIGGER", "a", "a1")
             .to("refs/for/master");
     r.assertOkStatus();
 
@@ -239,16 +240,16 @@
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     Message m = messages.get(0);
-    assertThat(m.rcpt()).containsExactly(user.emailAddress);
+    assertThat(m.rcpt()).containsExactly(user.getEmailAddress());
     assertThat(m.body()).contains("Change subject: TRIGGER\n");
     assertThat(m.body()).contains("Gerrit-PatchSet: 1\n");
   }
 
   @Test
   public void watchFile() throws Exception {
-    String watchedProject = createProject("watchedProject").get();
-    String otherWatchedProject = createProject("otherWatchedProject").get();
-    setApiUser(user);
+    String watchedProject = projectOperations.newProject().create().get();
+    String otherWatchedProject = projectOperations.newProject().create().get();
+    requestScopeOperations.setApiUser(user.id());
 
     // watch file in project as user
     watch(watchedProject, "file:a.txt");
@@ -258,12 +259,12 @@
 
     // push a change to watched file -> should trigger email notification for
     // user
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     TestRepository<InMemoryRepository> watchedRepo =
         cloneProject(new Project.NameKey(watchedProject), admin);
     PushOneCommit.Result r =
         pushFactory
-            .create(db, admin.getIdent(), watchedRepo, "TRIGGER", "a.txt", "a1")
+            .create(admin.newIdent(), watchedRepo, "TRIGGER", "a.txt", "a1")
             .to("refs/for/master");
     r.assertOkStatus();
 
@@ -271,21 +272,21 @@
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     Message m = messages.get(0);
-    assertThat(m.rcpt()).containsExactly(user.emailAddress);
+    assertThat(m.rcpt()).containsExactly(user.getEmailAddress());
     assertThat(m.body()).contains("Change subject: TRIGGER\n");
     assertThat(m.body()).contains("Gerrit-PatchSet: 1\n");
     sender.clear();
 
     // watch project as user2
     TestAccount user2 = accountCreator.create("user2", "user2@test.com", "User2");
-    setApiUser(user2);
+    requestScopeOperations.setApiUser(user2.id());
     watch(watchedProject);
 
     // push a change to non-watched file -> should not trigger email
     // notification for user, only for user2
     r =
         pushFactory
-            .create(db, admin.getIdent(), watchedRepo, "TRIGGER_USER2", "b.txt", "b1")
+            .create(admin.newIdent(), watchedRepo, "TRIGGER_USER2", "b.txt", "b1")
             .to("refs/for/master");
     r.assertOkStatus();
 
@@ -293,26 +294,26 @@
     messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     m = messages.get(0);
-    assertThat(m.rcpt()).containsExactly(user2.emailAddress);
+    assertThat(m.rcpt()).containsExactly(user2.getEmailAddress());
     assertThat(m.body()).contains("Change subject: TRIGGER_USER2\n");
     assertThat(m.body()).contains("Gerrit-PatchSet: 1\n");
   }
 
   @Test
   public void watchKeyword() throws Exception {
-    String watchedProject = createProject("watchedProject").get();
-    setApiUser(user);
+    String watchedProject = projectOperations.newProject().create().get();
+    requestScopeOperations.setApiUser(user.id());
 
     // watch keyword in project as user
     watch(watchedProject, "multimaster");
 
     // push a change with keyword -> should trigger email notification
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     TestRepository<InMemoryRepository> watchedRepo =
         cloneProject(new Project.NameKey(watchedProject), admin);
     PushOneCommit.Result r =
         pushFactory
-            .create(db, admin.getIdent(), watchedRepo, "Document multimaster setup", "a.txt", "a1")
+            .create(admin.newIdent(), watchedRepo, "Document multimaster setup", "a.txt", "a1")
             .to("refs/for/master");
     r.assertOkStatus();
 
@@ -320,7 +321,7 @@
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     Message m = messages.get(0);
-    assertThat(m.rcpt()).containsExactly(user.emailAddress);
+    assertThat(m.rcpt()).containsExactly(user.getEmailAddress());
     assertThat(m.body()).contains("Change subject: Document multimaster setup\n");
     assertThat(m.body()).contains("Gerrit-PatchSet: 1\n");
     sender.clear();
@@ -328,8 +329,7 @@
     // push a change without keyword -> should not trigger email notification
     r =
         pushFactory
-            .create(
-                db, admin.getIdent(), watchedRepo, "Cleanup cache implementation", "b.txt", "b1")
+            .create(admin.newIdent(), watchedRepo, "Cleanup cache implementation", "b.txt", "b1")
             .to("refs/for/master");
     r.assertOkStatus();
 
@@ -339,35 +339,33 @@
 
   @Test
   public void watchAllProjects() throws Exception {
-    String anyProject = createProject("anyProject").get();
-    setApiUser(user);
+    String anyProject = projectOperations.newProject().create().get();
+    requestScopeOperations.setApiUser(user.id());
 
     // watch the All-Projects project to watch all projects
     watch(allProjects.get());
 
     // push a change to any project -> should trigger email notification
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     TestRepository<InMemoryRepository> anyRepo =
         cloneProject(new Project.NameKey(anyProject), admin);
     PushOneCommit.Result r =
-        pushFactory
-            .create(db, admin.getIdent(), anyRepo, "TRIGGER", "a", "a1")
-            .to("refs/for/master");
+        pushFactory.create(admin.newIdent(), anyRepo, "TRIGGER", "a", "a1").to("refs/for/master");
     r.assertOkStatus();
 
     // assert email notification
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     Message m = messages.get(0);
-    assertThat(m.rcpt()).containsExactly(user.emailAddress);
+    assertThat(m.rcpt()).containsExactly(user.getEmailAddress());
     assertThat(m.body()).contains("Change subject: TRIGGER\n");
     assertThat(m.body()).contains("Gerrit-PatchSet: 1\n");
   }
 
   @Test
   public void watchFileAllProjects() throws Exception {
-    String anyProject = createProject("anyProject").get();
-    setApiUser(user);
+    String anyProject = projectOperations.newProject().create().get();
+    requestScopeOperations.setApiUser(user.id());
 
     // watch file in All-Projects project as user to watch the file in all
     // projects
@@ -375,12 +373,12 @@
 
     // push a change to watched file in any project -> should trigger email
     // notification for user
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     TestRepository<InMemoryRepository> anyRepo =
         cloneProject(new Project.NameKey(anyProject), admin);
     PushOneCommit.Result r =
         pushFactory
-            .create(db, admin.getIdent(), anyRepo, "TRIGGER", "a.txt", "a1")
+            .create(admin.newIdent(), anyRepo, "TRIGGER", "a.txt", "a1")
             .to("refs/for/master");
     r.assertOkStatus();
 
@@ -388,21 +386,21 @@
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     Message m = messages.get(0);
-    assertThat(m.rcpt()).containsExactly(user.emailAddress);
+    assertThat(m.rcpt()).containsExactly(user.getEmailAddress());
     assertThat(m.body()).contains("Change subject: TRIGGER\n");
     assertThat(m.body()).contains("Gerrit-PatchSet: 1\n");
     sender.clear();
 
     // watch project as user2
     TestAccount user2 = accountCreator.create("user2", "user2@test.com", "User2");
-    setApiUser(user2);
+    requestScopeOperations.setApiUser(user2.id());
     watch(anyProject);
 
     // push a change to non-watched file in any project -> should not trigger
     // email notification for user, only for user2
     r =
         pushFactory
-            .create(db, admin.getIdent(), anyRepo, "TRIGGER_USER2", "b.txt", "b1")
+            .create(admin.newIdent(), anyRepo, "TRIGGER_USER2", "b.txt", "b1")
             .to("refs/for/master");
     r.assertOkStatus();
 
@@ -410,27 +408,27 @@
     messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     m = messages.get(0);
-    assertThat(m.rcpt()).containsExactly(user2.emailAddress);
+    assertThat(m.rcpt()).containsExactly(user2.getEmailAddress());
     assertThat(m.body()).contains("Change subject: TRIGGER_USER2\n");
     assertThat(m.body()).contains("Gerrit-PatchSet: 1\n");
   }
 
   @Test
   public void watchKeywordAllProjects() throws Exception {
-    String anyProject = createProject("anyProject").get();
-    setApiUser(user);
+    String anyProject = projectOperations.newProject().create().get();
+    requestScopeOperations.setApiUser(user.id());
 
     // watch keyword in project as user
     watch(allProjects.get(), "multimaster");
 
     // push a change with keyword to any project -> should trigger email
     // notification
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     TestRepository<InMemoryRepository> anyRepo =
         cloneProject(new Project.NameKey(anyProject), admin);
     PushOneCommit.Result r =
         pushFactory
-            .create(db, admin.getIdent(), anyRepo, "Document multimaster setup", "a.txt", "a1")
+            .create(admin.newIdent(), anyRepo, "Document multimaster setup", "a.txt", "a1")
             .to("refs/for/master");
     r.assertOkStatus();
 
@@ -438,7 +436,7 @@
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     Message m = messages.get(0);
-    assertThat(m.rcpt()).containsExactly(user.emailAddress);
+    assertThat(m.rcpt()).containsExactly(user.getEmailAddress());
     assertThat(m.body()).contains("Change subject: Document multimaster setup\n");
     assertThat(m.body()).contains("Gerrit-PatchSet: 1\n");
     sender.clear();
@@ -447,7 +445,7 @@
     // notification
     r =
         pushFactory
-            .create(db, admin.getIdent(), anyRepo, "Cleanup cache implementation", "b.txt", "b1")
+            .create(admin.newIdent(), anyRepo, "Cleanup cache implementation", "b.txt", "b1")
             .to("refs/for/master");
     r.assertOkStatus();
 
@@ -458,28 +456,28 @@
   @Test
   public void watchProjectNoNotificationForIgnoredChange() throws Exception {
     // watch project
-    String watchedProject = createProject("watchedProject").get();
-    setApiUser(user);
+    String watchedProject = projectOperations.newProject().create().get();
+    requestScopeOperations.setApiUser(user.id());
     watch(watchedProject);
 
     // push a change to watched project
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     TestRepository<InMemoryRepository> watchedRepo =
         cloneProject(new Project.NameKey(watchedProject), admin);
     PushOneCommit.Result r =
         pushFactory
-            .create(db, admin.getIdent(), watchedRepo, "ignored change", "a", "a1")
+            .create(admin.newIdent(), watchedRepo, "ignored change", "a", "a1")
             .to("refs/for/master");
     r.assertOkStatus();
 
     // ignore the change
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     gApi.accounts().self().setStars(r.getChangeId(), new StarsInput(ImmutableSet.of(IGNORE_LABEL)));
 
     sender.clear();
 
     // post a comment -> should not trigger email notification since user ignored the change
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     ReviewInput in = new ReviewInput();
     in.message = "comment";
     gApi.changes().id(r.getChangeId()).current().review(in);
@@ -491,17 +489,17 @@
   @Test
   public void watchProjectNoNotificationForPrivateChange() throws Exception {
     // watch project
-    String watchedProject = createProject("watchedProject").get();
-    setApiUser(user);
+    String watchedProject = projectOperations.newProject().create().get();
+    requestScopeOperations.setApiUser(user.id());
     watch(watchedProject);
 
     // push a private change to watched project -> should not trigger email notification
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     TestRepository<InMemoryRepository> watchedRepo =
         cloneProject(new Project.NameKey(watchedProject), admin);
     PushOneCommit.Result r =
         pushFactory
-            .create(db, admin.getIdent(), watchedRepo, "private change", "a", "a1")
+            .create(admin.newIdent(), watchedRepo, "private change", "a", "a1")
             .to("refs/for/master%private");
     r.assertOkStatus();
 
@@ -511,7 +509,7 @@
 
   @Test
   public void watchProjectNotifyOnPrivateChange() throws Exception {
-    String watchedProject = createProject("watchedProject").get();
+    String watchedProject = projectOperations.newProject().create().get();
 
     // create group that can view all private changes
     GroupInfo groupThatCanViewPrivateChanges =
@@ -524,24 +522,24 @@
         new AccountGroup.UUID(groupThatCanViewPrivateChanges.id));
 
     // watch project as user that can't view private changes
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     watch(watchedProject);
 
     // watch project as user that can view all private change
     TestAccount userThatCanViewPrivateChanges =
         accountCreator.create(
             "user2", "user2@test.com", "User2", groupThatCanViewPrivateChanges.name);
-    setApiUser(userThatCanViewPrivateChanges);
+    requestScopeOperations.setApiUser(userThatCanViewPrivateChanges.id());
     watch(watchedProject);
 
     // push a private change to watched project -> should trigger email notification for
     // userThatCanViewPrivateChanges, but not for user
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     TestRepository<InMemoryRepository> watchedRepo =
         cloneProject(new Project.NameKey(watchedProject), admin);
     PushOneCommit.Result r =
         pushFactory
-            .create(db, admin.getIdent(), watchedRepo, "TRIGGER", "a", "a1")
+            .create(admin.newIdent(), watchedRepo, "TRIGGER", "a", "a1")
             .to("refs/for/master%private");
     r.assertOkStatus();
 
@@ -549,7 +547,7 @@
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     Message m = messages.get(0);
-    assertThat(m.rcpt()).containsExactly(userThatCanViewPrivateChanges.emailAddress);
+    assertThat(m.rcpt()).containsExactly(userThatCanViewPrivateChanges.getEmailAddress());
     assertThat(m.body()).contains("Change subject: TRIGGER\n");
     assertThat(m.body()).contains("Gerrit-PatchSet: 1\n");
   }
diff --git a/javatests/com/google/gerrit/acceptance/server/project/ReflogIT.java b/javatests/com/google/gerrit/acceptance/server/project/ReflogIT.java
index 8abb59d..da3a257 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/ReflogIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/ReflogIT.java
@@ -15,12 +15,12 @@
 package com.google.gerrit.acceptance.server.project;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.groups.GroupApi;
@@ -30,6 +30,7 @@
 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;
 import org.eclipse.jgit.lib.ReflogEntry;
@@ -38,9 +39,10 @@
 
 @UseLocalDisk
 public class ReflogIT extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @Test
   public void guessRestApiInReflog() throws Exception {
-    assume().that(notesMigration.disableChangeReviewDb()).isTrue();
     PushOneCommit.Result r = createChange();
     Change.Id id = r.getChange().getId();
 
@@ -82,7 +84,7 @@
 
   @Test
   public void regularUserIsNotAllowedToGetReflog() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     gApi.projects().name(project.get()).branch("master").reflog();
   }
@@ -98,13 +100,13 @@
       u.save();
     }
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.id());
     gApi.projects().name(project.get()).branch("master").reflog();
   }
 
   @Test
   public void adminUserIsAllowedToGetReflog() throws Exception {
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.id());
     gApi.projects().name(project.get()).branch("master").reflog();
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/server/quota/BUILD b/javatests/com/google/gerrit/acceptance/server/quota/BUILD
new file mode 100644
index 0000000..1988b22
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/quota/BUILD
@@ -0,0 +1,7 @@
+load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
+
+acceptance_tests(
+    srcs = glob(["*IT.java"]),
+    group = "server_quota",
+    labels = ["server"],
+)
diff --git a/javatests/com/google/gerrit/acceptance/server/quota/DefaultQuotaBackendIT.java b/javatests/com/google/gerrit/acceptance/server/quota/DefaultQuotaBackendIT.java
new file mode 100644
index 0000000..adc7807
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/quota/DefaultQuotaBackendIT.java
@@ -0,0 +1,239 @@
+// 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.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 com.google.gerrit.acceptance.AbstractDaemonTest;
+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;
+import com.google.gerrit.server.quota.QuotaException;
+import com.google.gerrit.server.quota.QuotaRequestContext;
+import com.google.gerrit.server.quota.QuotaResponse;
+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 IdentifiedUser identifiedAdmin;
+  @Inject private QuotaBackend quotaBackend;
+
+  @Override
+  public Module createModule() {
+    return new FactoryModule() {
+      @Override
+      public void configure() {
+        bind(QuotaEnforcer.class)
+            .annotatedWith(Exports.named("TestQuotaEnforcer"))
+            .toProvider(() -> quotaEnforcer);
+      }
+    };
+  }
+
+  @Before
+  public void setUp() {
+    identifiedAdmin = identifiedUserFactory.create(admin.id());
+    resetToStrict(quotaEnforcer);
+  }
+
+  @Test
+  public void requestTokenForUser() {
+    QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
+    expect(quotaEnforcer.requestTokens("testGroup", ctx, 1)).andReturn(QuotaResponse.ok());
+    replay(quotaEnforcer);
+    assertThat(quotaBackend.user(identifiedAdmin).requestToken("testGroup"))
+        .isEqualTo(singletonAggregation(QuotaResponse.ok()));
+  }
+
+  @Test
+  public void requestTokenForUserAndAccount() {
+    QuotaRequestContext ctx =
+        QuotaRequestContext.builder().user(identifiedAdmin).account(user.id()).build();
+    expect(quotaEnforcer.requestTokens("testGroup", ctx, 1)).andReturn(QuotaResponse.ok());
+    replay(quotaEnforcer);
+    assertThat(quotaBackend.user(identifiedAdmin).account(user.id()).requestToken("testGroup"))
+        .isEqualTo(singletonAggregation(QuotaResponse.ok()));
+  }
+
+  @Test
+  public void requestTokenForUserAndProject() {
+    QuotaRequestContext ctx =
+        QuotaRequestContext.builder().user(identifiedAdmin).project(project).build();
+    expect(quotaEnforcer.requestTokens("testGroup", ctx, 1)).andReturn(QuotaResponse.ok());
+    replay(quotaEnforcer);
+    assertThat(quotaBackend.user(identifiedAdmin).project(project).requestToken("testGroup"))
+        .isEqualTo(singletonAggregation(QuotaResponse.ok()));
+  }
+
+  @Test
+  public void requestTokenForUserAndChange() throws Exception {
+    Change.Id changeId = retrieveChangeId();
+    QuotaRequestContext ctx =
+        QuotaRequestContext.builder()
+            .user(identifiedAdmin)
+            .change(changeId)
+            .project(project)
+            .build();
+    expect(quotaEnforcer.requestTokens("testGroup", ctx, 1)).andReturn(QuotaResponse.ok());
+    replay(quotaEnforcer);
+    assertThat(
+            quotaBackend.user(identifiedAdmin).change(changeId, project).requestToken("testGroup"))
+        .isEqualTo(singletonAggregation(QuotaResponse.ok()));
+  }
+
+  @Test
+  public void requestTokens() {
+    QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
+    expect(quotaEnforcer.requestTokens("testGroup", ctx, 123)).andReturn(QuotaResponse.ok());
+    replay(quotaEnforcer);
+    assertThat(quotaBackend.user(identifiedAdmin).requestTokens("testGroup", 123))
+        .isEqualTo(singletonAggregation(QuotaResponse.ok()));
+  }
+
+  @Test
+  public void dryRun() {
+    QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
+    expect(quotaEnforcer.dryRun("testGroup", ctx, 123)).andReturn(QuotaResponse.ok());
+    replay(quotaEnforcer);
+    assertThat(quotaBackend.user(identifiedAdmin).dryRun("testGroup", 123))
+        .isEqualTo(singletonAggregation(QuotaResponse.ok()));
+  }
+
+  @Test
+  public void availableTokensForUserAndAccount() {
+    QuotaRequestContext ctx =
+        QuotaRequestContext.builder().user(identifiedAdmin).account(user.id()).build();
+    QuotaResponse r = QuotaResponse.ok(10L);
+    expect(quotaEnforcer.availableTokens("testGroup", ctx)).andReturn(r);
+    replay(quotaEnforcer);
+    assertThat(quotaBackend.user(identifiedAdmin).account(user.id()).availableTokens("testGroup"))
+        .isEqualTo(singletonAggregation(r));
+  }
+
+  @Test
+  public void availableTokensForUserAndProject() {
+    QuotaRequestContext ctx =
+        QuotaRequestContext.builder().user(identifiedAdmin).project(project).build();
+    QuotaResponse r = QuotaResponse.ok(10L);
+    expect(quotaEnforcer.availableTokens("testGroup", ctx)).andReturn(r);
+    replay(quotaEnforcer);
+    assertThat(quotaBackend.user(identifiedAdmin).project(project).availableTokens("testGroup"))
+        .isEqualTo(singletonAggregation(r));
+  }
+
+  @Test
+  public void availableTokensForUserAndChange() throws Exception {
+    Change.Id changeId = retrieveChangeId();
+    QuotaRequestContext ctx =
+        QuotaRequestContext.builder()
+            .user(identifiedAdmin)
+            .change(changeId)
+            .project(project)
+            .build();
+    QuotaResponse r = QuotaResponse.ok(10L);
+    expect(quotaEnforcer.availableTokens("testGroup", ctx)).andReturn(r);
+    replay(quotaEnforcer);
+    assertThat(
+            quotaBackend
+                .user(identifiedAdmin)
+                .change(changeId, project)
+                .availableTokens("testGroup"))
+        .isEqualTo(singletonAggregation(r));
+  }
+
+  @Test
+  public void availableTokens() {
+    QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
+    QuotaResponse r = QuotaResponse.ok(10L);
+    expect(quotaEnforcer.availableTokens("testGroup", ctx)).andReturn(r);
+    replay(quotaEnforcer);
+    assertThat(quotaBackend.user(identifiedAdmin).availableTokens("testGroup"))
+        .isEqualTo(singletonAggregation(r));
+  }
+
+  @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);
+
+    QuotaResponse.Aggregated result = quotaBackend.user(identifiedAdmin).requestToken("testGroup");
+    assertThat(result).isEqualTo(singletonAggregation(QuotaResponse.error("failed")));
+    exception.expect(QuotaException.class);
+    exception.expectMessage("failed");
+    result.throwOnError();
+  }
+
+  @Test
+  public void availableTokensError() throws Exception {
+    QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
+    expect(quotaEnforcer.availableTokens("testGroup", ctx))
+        .andReturn(QuotaResponse.error("failed"));
+    replay(quotaEnforcer);
+    QuotaResponse.Aggregated result =
+        quotaBackend.user(identifiedAdmin).availableTokens("testGroup");
+    assertThat(result).isEqualTo(singletonAggregation(QuotaResponse.error("failed")));
+    exception.expect(QuotaException.class);
+    exception.expectMessage("failed");
+    result.throwOnError();
+  }
+
+  @Test
+  public void requestTokenPluginThrowsAndRethrows() {
+    QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
+    expect(quotaEnforcer.requestTokens("testGroup", ctx, 1)).andThrow(new NullPointerException());
+    replay(quotaEnforcer);
+
+    exception.expect(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);
+
+    exception.expect(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);
+  }
+
+  private static QuotaResponse.Aggregated singletonAggregation(QuotaResponse response) {
+    return QuotaResponse.Aggregated.create(Collections.singleton(response));
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/server/quota/MultipleQuotaPluginsIT.java b/javatests/com/google/gerrit/acceptance/server/quota/MultipleQuotaPluginsIT.java
new file mode 100644
index 0000000..f5f1161
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/quota/MultipleQuotaPluginsIT.java
@@ -0,0 +1,157 @@
+// 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.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 com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.quota.QuotaBackend;
+import com.google.gerrit.server.quota.QuotaEnforcer;
+import com.google.gerrit.server.quota.QuotaRequestContext;
+import com.google.gerrit.server.quota.QuotaResponse;
+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 IdentifiedUser identifiedAdmin;
+  @Inject private QuotaBackend quotaBackend;
+
+  @Override
+  public Module createModule() {
+    return new FactoryModule() {
+      @Override
+      public void configure() {
+        bind(QuotaEnforcer.class)
+            .annotatedWith(Exports.named("TestQuotaEnforcerA"))
+            .toProvider(() -> quotaEnforcerA);
+
+        bind(QuotaEnforcer.class)
+            .annotatedWith(Exports.named("TestQuotaEnforcerB"))
+            .toProvider(() -> quotaEnforcerB);
+      }
+    };
+  }
+
+  @Before
+  public void setUp() {
+    identifiedAdmin = identifiedUserFactory.create(admin.id());
+    resetToStrict(quotaEnforcerA);
+    resetToStrict(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);
+
+    assertThat(quotaBackend.user(identifiedAdmin).requestToken("testGroup"))
+        .isEqualTo(
+            QuotaResponse.Aggregated.create(
+                ImmutableList.of(QuotaResponse.ok(), QuotaResponse.error("fail"))));
+  }
+
+  @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();
+
+    replay(quotaEnforcerA);
+    replay(quotaEnforcerB);
+
+    try {
+      quotaBackend.user(identifiedAdmin).requestToken("testGroup");
+      fail("expected a NullPointerException");
+    } catch (NullPointerException e) {
+      assertThat(exception).isEqualTo(e);
+    }
+
+    verify(quotaEnforcerA);
+  }
+
+  @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);
+
+    assertThat(quotaBackend.user(identifiedAdmin).requestToken("testGroup"))
+        .isEqualTo(
+            QuotaResponse.Aggregated.create(
+                ImmutableList.of(QuotaResponse.error("fail"), QuotaResponse.noOp())));
+  }
+
+  @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);
+
+    OptionalLong tokens =
+        quotaBackend.user(identifiedAdmin).availableTokens("testGroup").availableTokens();
+    assertThat(tokens.isPresent()).isTrue();
+    assertThat(tokens.getAsLong()).isEqualTo(10L);
+  }
+
+  @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);
+
+    OptionalLong tokens =
+        quotaBackend.user(identifiedAdmin).availableTokens("testGroup").availableTokens();
+    assertThat(tokens.isPresent()).isTrue();
+    assertThat(tokens.getAsLong()).isEqualTo(20L);
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/server/quota/RepositorySizeQuotaIT.java b/javatests/com/google/gerrit/acceptance/server/quota/RepositorySizeQuotaIT.java
new file mode 100644
index 0000000..0814230
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/quota/RepositorySizeQuotaIT.java
@@ -0,0 +1,140 @@
+// 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.quota;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.server.quota.QuotaGroupDefinitions.REPOSITORY_SIZE_GROUP;
+import static com.google.gerrit.server.quota.QuotaResponse.ok;
+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 com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.extensions.config.FactoryModule;
+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.eclipse.jgit.api.errors.TooLargeObjectInPackException;
+import org.eclipse.jgit.api.errors.TransportException;
+import org.junit.Before;
+import org.junit.Test;
+
+@UseLocalDisk
+public class RepositorySizeQuotaIT extends AbstractDaemonTest {
+  private static final QuotaBackend.WithResource quotaBackendWithResource =
+      EasyMock.createStrictMock(QuotaBackend.WithResource.class);
+  private static final QuotaBackend.WithUser quotaBackendWithUser =
+      EasyMock.createStrictMock(QuotaBackend.WithUser.class);
+
+  @Override
+  public Module createModule() {
+    return new FactoryModule() {
+      @Override
+      public void configure() {
+        bind(QuotaBackend.class)
+            .toInstance(
+                new QuotaBackend() {
+                  @Override
+                  public WithUser currentUser() {
+                    return quotaBackendWithUser;
+                  }
+
+                  @Override
+                  public WithUser user(CurrentUser user) {
+                    return quotaBackendWithUser;
+                  }
+                });
+      }
+    };
+  }
+
+  @Before
+  public void setUp() {
+    resetToStrict(quotaBackendWithResource);
+    resetToStrict(quotaBackendWithUser);
+  }
+
+  @Test
+  public void pushWithAvailableTokens() throws Exception {
+    expect(quotaBackendWithResource.availableTokens(REPOSITORY_SIZE_GROUP))
+        .andReturn(singletonAggregation(ok(276L)))
+        .times(2);
+    expect(quotaBackendWithResource.requestTokens(eq(REPOSITORY_SIZE_GROUP), anyLong()))
+        .andReturn(singletonAggregation(ok()));
+    expect(quotaBackendWithUser.project(project)).andReturn(quotaBackendWithResource).anyTimes();
+    replay(quotaBackendWithResource);
+    replay(quotaBackendWithUser);
+    pushCommit();
+    verify(quotaBackendWithUser);
+    verify(quotaBackendWithResource);
+  }
+
+  @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);
+    try {
+      pushCommit();
+      assert_().fail("expected TooLargeObjectInPackException");
+    } catch (TooLargeObjectInPackException e) {
+      String msg = e.getMessage();
+      assertThat(msg).contains("Object too large");
+      assertThat(msg)
+          .contains(String.format("Max object size limit is %d bytes.", 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);
+    try {
+      pushCommit();
+      assert_().fail("expected TransportException");
+    } catch (TransportException e) {
+      // TransportException has not much info about the cause
+    }
+    verify(quotaBackendWithUser);
+    verify(quotaBackendWithResource);
+  }
+
+  private void pushCommit() throws Exception {
+    createCommitAndPush(testRepo, "refs/heads/master", "test 01", "file.test", "some content");
+  }
+
+  private static QuotaResponse.Aggregated singletonAggregation(QuotaResponse response) {
+    return QuotaResponse.Aggregated.create(Collections.singleton(response));
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/server/quota/RestApiQuotaIT.java b/javatests/com/google/gerrit/acceptance/server/quota/RestApiQuotaIT.java
new file mode 100644
index 0000000..dc082fe
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/quota/RestApiQuotaIT.java
@@ -0,0 +1,144 @@
+// 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.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 com.google.gerrit.acceptance.AbstractDaemonTest;
+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);
+  private static final QuotaBackend.WithUser quotaBackendWithUser =
+      EasyMock.createStrictMock(QuotaBackend.WithUser.class);
+
+  @Override
+  public Module createModule() {
+    return new FactoryModule() {
+      @Override
+      public void configure() {
+        bind(QuotaBackend.class)
+            .toInstance(
+                new QuotaBackend() {
+                  @Override
+                  public WithUser currentUser() {
+                    return quotaBackendWithUser;
+                  }
+
+                  @Override
+                  public WithUser user(CurrentUser user) {
+                    return quotaBackendWithUser;
+                  }
+                });
+      }
+    };
+  }
+
+  @Before
+  public void setUp() {
+    reset(quotaBackendWithResource);
+    reset(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);
+    adminRestSession.get("/changes/" + changeId + "/detail").assertOK();
+    verify(quotaBackendWithUser);
+    verify(quotaBackendWithResource);
+  }
+
+  @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);
+    adminRestSession.get("/changes/" + changeId + "/revisions/current/actions").assertOK();
+    verify(quotaBackendWithUser);
+    verify(quotaBackendWithResource);
+  }
+
+  @Test
+  public void createChangePost() throws Exception {
+    expect(quotaBackendWithUser.requestToken("/restapi/changes:POST"))
+        .andReturn(singletonAggregation(QuotaResponse.ok()));
+    replay(quotaBackendWithUser);
+    ChangeInput changeInput = new ChangeInput(project.get(), "master", "test");
+    adminRestSession.post("/changes/", changeInput).assertCreated();
+    verify(quotaBackendWithUser);
+  }
+
+  @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);
+    adminRestSession.get("/accounts/self/detail").assertOK();
+    verify(quotaBackendWithUser);
+    verify(quotaBackendWithResource);
+  }
+
+  @Test
+  public void config() throws Exception {
+    expect(quotaBackendWithUser.requestToken("/restapi/config/version:GET"))
+        .andReturn(singletonAggregation(QuotaResponse.ok()));
+    replay(quotaBackendWithUser);
+    adminRestSession.get("/config/server/version").assertOK();
+  }
+
+  @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);
+  }
+
+  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);
+  }
+
+  private static QuotaResponse.Aggregated singletonAggregation(QuotaResponse response) {
+    return QuotaResponse.Aggregated.create(Collections.singleton(response));
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/server/rules/IgnoreSelfApprovalRuleIT.java b/javatests/com/google/gerrit/acceptance/server/rules/IgnoreSelfApprovalRuleIT.java
index d1b05e3..83782c9 100644
--- a/javatests/com/google/gerrit/acceptance/server/rules/IgnoreSelfApprovalRuleIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/rules/IgnoreSelfApprovalRuleIT.java
@@ -63,7 +63,7 @@
 
     // Create change as user
     TestRepository<InMemoryRepository> userTestRepo = cloneProject(project, user);
-    PushOneCommit push = pushFactory.create(db, user.getIdent(), userTestRepo);
+    PushOneCommit push = pushFactory.create(user.newIdent(), userTestRepo);
     PushOneCommit.Result r = push.to("refs/for/master");
 
     // Approve as admin
diff --git a/javatests/com/google/gerrit/acceptance/server/rules/PrologRuleEvaluatorIT.java b/javatests/com/google/gerrit/acceptance/server/rules/PrologRuleEvaluatorIT.java
index 8fc32b4..fa13be4 100644
--- a/javatests/com/google/gerrit/acceptance/server/rules/PrologRuleEvaluatorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/rules/PrologRuleEvaluatorIT.java
@@ -87,17 +87,17 @@
     SubmitRecord.Label submitRecordLabel1 = new SubmitRecord.Label();
     submitRecordLabel1.label = "Verified";
     submitRecordLabel1.status = SubmitRecord.Label.Status.REJECT;
-    submitRecordLabel1.appliedBy = admin.id;
+    submitRecordLabel1.appliedBy = admin.id();
 
     SubmitRecord.Label submitRecordLabel2 = new SubmitRecord.Label();
     submitRecordLabel2.label = "Code-Review";
     submitRecordLabel2.status = SubmitRecord.Label.Status.OK;
-    submitRecordLabel2.appliedBy = admin.id;
+    submitRecordLabel2.appliedBy = admin.id();
 
     SubmitRecord.Label submitRecordLabel3 = new SubmitRecord.Label();
     submitRecordLabel3.label = "Any-Label-Name";
     submitRecordLabel3.status = SubmitRecord.Label.Status.REJECT;
-    submitRecordLabel3.appliedBy = user.id;
+    submitRecordLabel3.appliedBy = user.id();
 
     List<Term> terms = new ArrayList<>();
 
@@ -140,7 +140,7 @@
   }
 
   private static StructureTerm makeLabel(String name, String status, TestAccount account) {
-    StructureTerm user = new StructureTerm("user", new IntegerTerm(account.id.get()));
+    StructureTerm user = new StructureTerm("user", new IntegerTerm(account.id().get()));
     return new StructureTerm("label", new StructureTerm(name), new StructureTerm(status, user));
   }
 
@@ -150,7 +150,7 @@
 
   private ChangeData makeChangeData() {
     ChangeData cd = ChangeData.createForTest(project, new Change.Id(1), 1);
-    cd.setChange(TestChanges.newChange(project, admin.id));
+    cd.setChange(TestChanges.newChange(project, admin.id()));
     return cd;
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/server/rules/RulesIT.java b/javatests/com/google/gerrit/acceptance/server/rules/RulesIT.java
index 98dca3e..536cbbb 100644
--- a/javatests/com/google/gerrit/acceptance/server/rules/RulesIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/rules/RulesIT.java
@@ -26,7 +26,6 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
 import java.util.Collection;
-import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Repository;
 import org.junit.Before;
@@ -74,7 +73,7 @@
     modifySubmitRules(
         String.format(
             "gerrit:commit_author(user(%d), '%s', '%s')",
-            user.getId().get(), user.fullName, user.email));
+            user.id().get(), user.fullName(), user.email()));
     assertThat(statusForRule()).isEqualTo(SubmitRecord.Status.OK);
   }
 
@@ -87,7 +86,7 @@
   private SubmitRecord.Status statusForRule() throws Exception {
     String oldHead = getRemoteHead().name();
     PushOneCommit.Result result1 =
-        pushFactory.create(db, user.getIdent(), testRepo).to("refs/for/master");
+        pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
     testRepo.reset(oldHead);
     ChangeData cd = result1.getChange();
 
@@ -107,13 +106,13 @@
   private void modifySubmitRules(String ruleTested) throws Exception {
     String newContent = String.format(RULE_TEMPLATE, ruleTested);
 
-    try (Repository repo = repoManager.openRepository(project)) {
-      TestRepository<?> testRepo = new TestRepository<>((InMemoryRepository) repo);
+    try (Repository repo = repoManager.openRepository(project);
+        TestRepository<Repository> testRepo = new TestRepository<>(repo)) {
       testRepo
           .branch(RefNames.REFS_CONFIG)
           .commit()
-          .author(admin.getIdent())
-          .committer(admin.getIdent())
+          .author(admin.newIdent())
+          .committer(admin.newIdent())
           .add("rules.pl", newContent)
           .message("Modify rules.pl")
           .create();
diff --git a/javatests/com/google/gerrit/acceptance/ssh/AbstractIndexTests.java b/javatests/com/google/gerrit/acceptance/ssh/AbstractIndexTests.java
index 0403cab..9fb7c2d 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/AbstractIndexTests.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/AbstractIndexTests.java
@@ -45,7 +45,7 @@
   private RegistrationHandle changeIndexedCounterHandle;
 
   /** @param injector injector */
-  public abstract void configureIndex(Injector injector) throws Exception;
+  public void configureIndex(Injector injector) {}
 
   @Before
   public void addChangeIndexedCounter() {
diff --git a/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java b/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
index 5348c53..1f08d5f 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
@@ -40,7 +40,7 @@
   }
 
   @Override
-  public void configureIndex(Injector injector) throws Exception {
+  public void configureIndex(Injector injector) {
     createAllIndexes(injector);
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java b/javatests/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
index 4384ab5..c23f889d 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.UseLocalDisk;
 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.server.git.GarbageCollection;
@@ -40,14 +41,15 @@
   @Inject private GarbageCollectionQueue gcQueue;
 
   @Inject private GcAssert gcAssert;
+  @Inject private ProjectOperations projectOperations;
 
   private Project.NameKey project2;
   private Project.NameKey project3;
 
   @Before
   public void setUp() throws Exception {
-    project2 = createProject("p2");
-    project3 = createProject("p3");
+    project2 = projectOperations.newProject().create();
+    project3 = projectOperations.newProject().create();
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/ssh/IndexIT.java b/javatests/com/google/gerrit/acceptance/ssh/IndexIT.java
index 196a1e5..370fb72 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/IndexIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/IndexIT.java
@@ -14,10 +14,4 @@
 
 package com.google.gerrit.acceptance.ssh;
 
-import com.google.inject.Injector;
-
-public class IndexIT extends AbstractIndexTests {
-
-  @Override
-  public void configureIndex(Injector injector) throws Exception {}
-}
+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
new file mode 100644
index 0000000..e61e2cc
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/ssh/PluginChangeFieldsIT.java
@@ -0,0 +1,88 @@
+// 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 static com.google.common.truth.Truth.assertThat;
+import static java.util.stream.Collectors.joining;
+
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.io.CharStreams;
+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.server.query.change.OutputStreamQuery;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+import org.junit.Test;
+
+@UseSsh
+public class PluginChangeFieldsIT extends AbstractPluginFieldsTest {
+  // No tests for getting a single change over SSH, since the only API is the query API.
+
+  private static final Gson GSON = OutputStreamQuery.GSON;
+
+  @Test
+  public void queryChangeWithNullAttribute() throws Exception {
+    getChangeWithNullAttribute(
+        id -> pluginInfoFromSingletonList(adminSshSession.exec(changeQueryCmd(id))));
+  }
+
+  @Test
+  public void queryChangeWithSimpleAttribute() throws Exception {
+    getChangeWithSimpleAttribute(
+        id -> pluginInfoFromSingletonList(adminSshSession.exec(changeQueryCmd(id))));
+  }
+
+  @Test
+  public void queryChangeWithOption() throws Exception {
+    getChangeWithOption(
+        id -> pluginInfoFromSingletonList(adminSshSession.exec(changeQueryCmd(id))),
+        (id, opts) -> pluginInfoFromSingletonList(adminSshSession.exec(changeQueryCmd(id, opts))));
+  }
+
+  private String changeQueryCmd(Change.Id id) {
+    return changeQueryCmd(id, ImmutableListMultimap.of());
+  }
+
+  private String changeQueryCmd(Change.Id id, ImmutableListMultimap<String, String> pluginOptions) {
+    return "gerrit query --format json "
+        + pluginOptions.entries().stream()
+            .flatMap(e -> Stream.of("--" + e.getKey(), e.getValue()))
+            .collect(joining(" "))
+        + " "
+        + id;
+  }
+
+  @Nullable
+  private static List<MyInfo> pluginInfoFromSingletonList(String sshOutput) throws Exception {
+    List<Map<String, Object>> changeAttrs = new ArrayList<>();
+    for (String line : CharStreams.readLines(new StringReader(sshOutput))) {
+      Map<String, Object> changeAttr =
+          GSON.fromJson(line, new TypeToken<Map<String, Object>>() {}.getType());
+      if (!"stats".equals(changeAttr.get("type"))) {
+        changeAttrs.add(changeAttr);
+      }
+    }
+
+    assertThat(changeAttrs).hasSize(1);
+    return decodeRawPluginsList(GSON, changeAttrs.get(0).get("plugins"));
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/ssh/QueryIT.java b/javatests/com/google/gerrit/acceptance/ssh/QueryIT.java
index 50b2a78d..0f47a4a 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/QueryIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/QueryIT.java
@@ -84,7 +84,7 @@
   public void allReviewersOptionJSON() throws Exception {
     String changeId = createChange().getChangeId();
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     gApi.changes().id(changeId).addReviewer(in);
 
     List<ChangeAttribute> changes = executeSuccessfulQuery(changeId);
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SetReviewersIT.java b/javatests/com/google/gerrit/acceptance/ssh/SetReviewersIT.java
index 9108f68..6998a0a 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.Id;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.testing.ConfigSuite;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Before;
@@ -62,13 +62,14 @@
   }
 
   private void setReviewer(boolean add, String id) throws Exception {
-    session.exec(String.format("gerrit set-reviewers -%s %s %s", add ? "a" : "r", user.email, id));
+    session.exec(
+        String.format("gerrit set-reviewers -%s %s %s", add ? "a" : "r", user.email(), id));
     session.assertSuccess();
-    ImmutableSet<Id> reviewers = change.getChange().getReviewers().all();
+    ImmutableSet<Account.Id> reviewers = change.getChange().getReviewers().all();
     if (add) {
-      assertThat(reviewers).contains(user.id);
+      assertThat(reviewers).contains(user.id());
     } else {
-      assertThat(reviewers).doesNotContain(user.id);
+      assertThat(reviewers).doesNotContain(user.id());
     }
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java b/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
index eb79c18..4e9c4a4 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
@@ -17,7 +17,6 @@
 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.server.group.SystemGroupBackend.REGISTERED_USERS;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -27,7 +26,6 @@
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.Sandboxed;
 import com.google.gerrit.acceptance.UseSsh;
-import com.google.gerrit.common.data.GlobalCapability;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -64,7 +62,6 @@
           "create-branch",
           "create-group",
           "create-project",
-          "gsql",
           "index",
           "query",
           "receive-pack",
@@ -113,9 +110,6 @@
   @Test
   @Sandboxed
   public void sshCommandCanBeExecuted() throws Exception {
-    // Access Database capability is required to run the "gerrit gsql" command
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
-
     testCommandExecution(MASTER_COMMANDS);
 
     restartAsSlave();
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SshTraceIT.java b/javatests/com/google/gerrit/acceptance/ssh/SshTraceIT.java
index 899b0cf..9c1e23d 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/SshTraceIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/SshTraceIT.java
@@ -1,3 +1,17 @@
+// 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.ssh;
 
 import static com.google.common.truth.Truth.assertThat;
diff --git a/javatests/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java b/javatests/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
index 93fc769..03b7143 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.acceptance.ssh;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.TruthJUnit.assume;
 
 import com.google.common.base.Splitter;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -23,7 +22,6 @@
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.UseSsh;
-import com.google.gerrit.testing.NoteDbMode;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.InputStream;
@@ -34,20 +32,12 @@
 import org.eclipse.jgit.transport.PacketLineIn;
 import org.eclipse.jgit.transport.PacketLineOut;
 import org.eclipse.jgit.util.IO;
-import org.junit.Before;
 import org.junit.Test;
 
 @NoHttpd
 @UseSsh
 public class UploadArchiveIT extends AbstractDaemonTest {
 
-  @Before
-  public void setUp() {
-    // There is some Guice request scoping problem preventing this test from
-    // passing in CHECK mode.
-    assume().that(NoteDbMode.get()).isNotEqualTo(NoteDbMode.CHECK);
-  }
-
   @Test
   @GerritConfig(name = "download.archive", value = "off")
   public void archiveFeatureOff() throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImplTest.java b/javatests/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImplTest.java
index 954b0e6..e0d6593 100644
--- a/javatests/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImplTest.java
+++ b/javatests/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImplTest.java
@@ -19,6 +19,7 @@
 
 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.extensions.api.groups.GroupInput;
@@ -31,14 +32,10 @@
 import java.sql.Timestamp;
 import java.util.Objects;
 import java.util.Optional;
-import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 
 public class GroupOperationsImplTest extends AbstractDaemonTest {
 
-  @Rule public ExpectedException expectedException = ExpectedException.none();
-
   @Inject private AccountOperations accountOperations;
 
   @Inject private GroupOperationsImpl groupOperations;
@@ -231,7 +228,7 @@
   public void retrievingNotExistingGroupFails() throws Exception {
     AccountGroup.UUID notExistingGroupUuid = new AccountGroup.UUID("not-existing-group");
 
-    expectedException.expect(IllegalStateException.class);
+    exception.expect(IllegalStateException.class);
     groupOperations.group(notExistingGroupUuid).get();
   }
 
@@ -617,40 +614,34 @@
   }
 
   private static Correspondence<AccountInfo, Account.Id> getAccountToIdCorrespondence() {
-    return new Correspondence<AccountInfo, Account.Id>() {
-      @Override
-      public boolean compare(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);
-      }
-
-      @Override
-      public String toString() {
-        return "has ID";
-      }
-    };
+    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);
+          }
+        },
+        "has ID");
   }
 
   private static Correspondence<GroupInfo, AccountGroup.UUID> getGroupToUuidCorrespondence() {
-    return new Correspondence<GroupInfo, AccountGroup.UUID>() {
-      @Override
-      public boolean compare(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);
-      }
-
-      @Override
-      public String toString() {
-        return "has UUID";
-      }
-    };
+    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);
+          }
+        },
+        "has UUID");
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java b/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java
new file mode 100644
index 0000000..3f537c0
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java
@@ -0,0 +1,56 @@
+// 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.testsuite.project;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.util.stream.Collectors.toList;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.extensions.api.projects.BranchInfo;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.inject.Inject;
+import java.util.List;
+import org.junit.Test;
+
+public class ProjectOperationsImplTest extends AbstractDaemonTest {
+
+  @Inject private ProjectOperations projectOperations;
+
+  @Test
+  public void defaultName() throws Exception {
+    Project.NameKey name = projectOperations.newProject().create();
+    // check that the project was created (throws exception if not found.)
+    gApi.projects().name(name.get());
+    Project.NameKey name2 = projectOperations.newProject().create();
+    assertThat(name2).isNotEqualTo(name);
+  }
+
+  @Test
+  public void specifiedName() throws Exception {
+    String name = "somename";
+    Project.NameKey key = projectOperations.newProject().name(name).create();
+    assertThat(key.get()).isEqualTo(name);
+  }
+
+  @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"));
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImplTest.java b/javatests/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImplTest.java
new file mode 100644
index 0000000..5cbed1b
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImplTest.java
@@ -0,0 +1,147 @@
+// 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.request;
+
+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 com.google.common.collect.ImmutableSet;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
+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.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;
+import com.google.gerrit.server.notedb.Sequences;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.junit.Test;
+
+@UseSsh
+public class RequestScopeOperationsImplTest extends AbstractDaemonTest {
+  private static final AtomicInteger changeCounter = new AtomicInteger();
+
+  @Inject private AccountOperations accountOperations;
+  @Inject private Provider<CurrentUser> userProvider;
+  @Inject private RequestScopeOperationsImpl requestScopeOperations;
+  @Inject private Sequences sequences;
+
+  @Test
+  public void setApiUserToExistingUserById() throws Exception {
+    fastCheckCurrentUser(admin.id());
+    AcceptanceTestRequestScope.Context oldCtx = requestScopeOperations.setApiUser(user.id());
+    assertThat(oldCtx.getUser().getAccountId()).isEqualTo(admin.id());
+    checkCurrentUser(user.id());
+  }
+
+  @Test
+  public void setApiUserToExistingUserByTestAccount() throws Exception {
+    fastCheckCurrentUser(admin.id());
+    TestAccount testAccount =
+        accountOperations.account(accountOperations.newAccount().username("tester").create()).get();
+    AcceptanceTestRequestScope.Context oldCtx = requestScopeOperations.setApiUser(testAccount);
+    assertThat(oldCtx.getUser().getAccountId()).isEqualTo(admin.id());
+    checkCurrentUser(testAccount.accountId());
+  }
+
+  @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.
+    }
+    checkCurrentUser(admin.id());
+  }
+
+  @Test
+  public void resetCurrentApiUserClearsCachedState() throws Exception {
+    requestScopeOperations.setApiUser(user.id());
+    PropertyKey<String> key = PropertyKey.create();
+    atrScope.get().getUser().put(key, "foo");
+    assertThat(atrScope.get().getUser().get(key)).hasValue("foo");
+
+    AcceptanceTestRequestScope.Context oldCtx = requestScopeOperations.resetCurrentApiUser();
+    checkCurrentUser(user.id());
+    assertThat(atrScope.get().getUser().get(key)).isEmpty();
+    assertThat(oldCtx.getUser().get(key)).hasValue("foo");
+  }
+
+  @Test
+  public void setApiUserAnonymousSetsAnonymousUser() throws Exception {
+    fastCheckCurrentUser(admin.id());
+    requestScopeOperations.setApiUserAnonymous();
+    assertThat(userProvider.get()).isInstanceOf(AnonymousUser.class);
+  }
+
+  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")
+        .isTrue();
+    assertThat(userProvider.get().getAccountId()).named("user from provider").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")
+        .isEqualTo(expected.get());
+    AcceptanceTestRequestScope.Context ctx = atrScope.get();
+    assertThat(ctx.getUser().isIdentifiedUser())
+        .named("user from AcceptanceTestRequestScope.Context is an IdentifiedUser")
+        .isTrue();
+    assertThat(ctx.getUser().getAccountId())
+        .named("user from AcceptanceTestRequestScope.Context")
+        .isEqualTo(expected);
+    checkSshUser(expected);
+  }
+
+  private void checkSshUser(Account.Id expected) throws Exception {
+    // No "gerrit whoami" command, so the simplest way to check who the user is over SSH is to query
+    // for owner:self.
+    ChangeInput cin = new ChangeInput();
+    cin.project = project.get();
+    cin.branch = "master";
+    cin.subject = "Test change " + changeCounter.incrementAndGet();
+    String changeId = gApi.changes().create(cin).get().changeId;
+    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)
+        .containsExactly(changeId);
+  }
+
+  private static ImmutableSet<String> findDistinct(String input, String pattern) {
+    Matcher m = Pattern.compile(pattern).matcher(input);
+    ImmutableSet.Builder<String> b = ImmutableSet.builder();
+    while (m.find()) {
+      b.add(m.group(0));
+    }
+    return b.build();
+  }
+}
diff --git a/javatests/com/google/gerrit/common/AutoValueTest.java b/javatests/com/google/gerrit/common/AutoValueTest.java
index 947fe4a..89d7bf4 100644
--- a/javatests/com/google/gerrit/common/AutoValueTest.java
+++ b/javatests/com/google/gerrit/common/AutoValueTest.java
@@ -17,9 +17,10 @@
 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 {
+public class AutoValueTest extends GerritBaseTests {
   @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 c4f2c0a..18ececd 100644
--- a/javatests/com/google/gerrit/common/BUILD
+++ b/javatests/com/google/gerrit/common/BUILD
@@ -1,14 +1,14 @@
 load("//tools/bzl:junit.bzl", "junit_tests")
 
 junit_tests(
-    name = "common_tests",
-    srcs = glob(["**/*.java"]),
+    name = "server_tests",
+    srcs = glob(["*.java"]),
     tags = ["no_windows"],
     deps = [
         "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/common:version",
         "//java/com/google/gerrit/launcher",
-        "//java/com/google/gerrit/reviewdb:server",
+        "//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 a59c015..faf9d6c 100644
--- a/javatests/com/google/gerrit/common/data/AccessSectionTest.java
+++ b/javatests/com/google/gerrit/common/data/AccessSectionTest.java
@@ -17,17 +17,14 @@
 import static com.google.common.truth.Truth.assertThat;
 
 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.Rule;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 
-public class AccessSectionTest {
-  @Rule public ExpectedException exception = ExpectedException.none();
-
+public class AccessSectionTest extends GerritBaseTests {
   private static final String REF_PATTERN = "refs/heads/master";
 
   private AccessSection accessSection;
@@ -155,21 +152,6 @@
   }
 
   @Test
-  public void cannotAddPermissionByModifyingListThatWasRetrievedFromAccessSection() {
-    Permission submitPermission = new Permission(Permission.SUBMIT);
-    accessSection.getPermissions().add(submitPermission);
-    assertThat(accessSection.getPermission(Permission.SUBMIT)).isNull();
-
-    List<Permission> permissions = new ArrayList<>();
-    permissions.add(new Permission(Permission.ABANDON));
-    permissions.add(new Permission(Permission.REBASE));
-    accessSection.setPermissions(permissions);
-    assertThat(accessSection.getPermission(Permission.SUBMIT)).isNull();
-    accessSection.getPermissions().add(submitPermission);
-    assertThat(accessSection.getPermission(Permission.SUBMIT)).isNull();
-  }
-
-  @Test
   public void removePermission() {
     Permission abandonPermission = new Permission(Permission.ABANDON);
     Permission rebasePermission = new Permission(Permission.REBASE);
diff --git a/javatests/com/google/gerrit/common/data/BUILD b/javatests/com/google/gerrit/common/data/BUILD
new file mode 100644
index 0000000..776a5e0
--- /dev/null
+++ b/javatests/com/google/gerrit/common/data/BUILD
@@ -0,0 +1,13 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+
+junit_tests(
+    name = "data_tests",
+    srcs = glob(["*.java"]),
+    deps = [
+        "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/reviewdb:server",
+        "//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 dcd3c05..3dd2db3 100644
--- a/javatests/com/google/gerrit/common/data/EncodePathSeparatorTest.java
+++ b/javatests/com/google/gerrit/common/data/EncodePathSeparatorTest.java
@@ -16,9 +16,10 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class EncodePathSeparatorTest {
+public class EncodePathSeparatorTest extends GerritBaseTests {
   @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 ec71e05..055f57d 100644
--- a/javatests/com/google/gerrit/common/data/FilenameComparatorTest.java
+++ b/javatests/com/google/gerrit/common/data/FilenameComparatorTest.java
@@ -16,9 +16,10 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class FilenameComparatorTest {
+public class FilenameComparatorTest extends GerritBaseTests {
   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 fdc9dc6..8cf486b 100644
--- a/javatests/com/google/gerrit/common/data/GroupReferenceTest.java
+++ b/javatests/com/google/gerrit/common/data/GroupReferenceTest.java
@@ -18,13 +18,10 @@
 
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroup.UUID;
-import org.junit.Rule;
+import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 
-public class GroupReferenceTest {
-  @Rule public ExpectedException exception = ExpectedException.none();
-
+public class GroupReferenceTest extends GerritBaseTests {
   @Test
   public void forGroupDescription() {
     String name = "foo";
diff --git a/javatests/com/google/gerrit/common/data/LabelFunctionTest.java b/javatests/com/google/gerrit/common/data/LabelFunctionTest.java
index 985f514..a534a9e 100644
--- a/javatests/com/google/gerrit/common/data/LabelFunctionTest.java
+++ b/javatests/com/google/gerrit/common/data/LabelFunctionTest.java
@@ -21,15 +21,15 @@
 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.PatchSet.Id;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.testing.GerritBaseTests;
 import java.sql.Date;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.List;
 import org.junit.Test;
 
-public class LabelFunctionTest {
+public class LabelFunctionTest extends GerritBaseTests {
   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);
@@ -102,7 +102,8 @@
     return new PatchSetApproval(key, (short) value, Date.from(Instant.now()));
   }
 
-  private static PatchSetApproval.Key makeKey(Id psId, Account.Id accountId, LabelId labelId) {
+  private static PatchSetApproval.Key makeKey(
+      PatchSet.Id psId, Account.Id accountId, LabelId labelId) {
     return new PatchSetApproval.Key(psId, accountId, labelId);
   }
 
diff --git a/javatests/com/google/gerrit/common/data/LabelTypeTest.java b/javatests/com/google/gerrit/common/data/LabelTypeTest.java
index 6c3befb..db0df2e 100644
--- a/javatests/com/google/gerrit/common/data/LabelTypeTest.java
+++ b/javatests/com/google/gerrit/common/data/LabelTypeTest.java
@@ -17,9 +17,10 @@
 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 {
+public class LabelTypeTest extends GerritBaseTests {
   @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 b646d2b..b22a511 100644
--- a/javatests/com/google/gerrit/common/data/ParameterizedStringTest.java
+++ b/javatests/com/google/gerrit/common/data/ParameterizedStringTest.java
@@ -17,11 +17,12 @@
 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 {
+public class ParameterizedStringTest extends GerritBaseTests {
   @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 14c47b4..f442f39 100644
--- a/javatests/com/google/gerrit/common/data/PermissionRuleTest.java
+++ b/javatests/com/google/gerrit/common/data/PermissionRuleTest.java
@@ -18,14 +18,11 @@
 
 import com.google.gerrit.common.data.PermissionRule.Action;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 
-public class PermissionRuleTest {
-  @Rule public ExpectedException exception = ExpectedException.none();
-
+public class PermissionRuleTest extends GerritBaseTests {
   private GroupReference groupReference;
   private PermissionRule permissionRule;
 
diff --git a/javatests/com/google/gerrit/common/data/PermissionTest.java b/javatests/com/google/gerrit/common/data/PermissionTest.java
index f76323f..23380e7 100644
--- a/javatests/com/google/gerrit/common/data/PermissionTest.java
+++ b/javatests/com/google/gerrit/common/data/PermissionTest.java
@@ -18,12 +18,13 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.testing.GerritBaseTests;
 import java.util.ArrayList;
 import java.util.List;
 import org.junit.Before;
 import org.junit.Test;
 
-public class PermissionTest {
+public class PermissionTest extends GerritBaseTests {
   private static final String PERMISSION_NAME = "foo";
 
   private Permission permission;
@@ -186,22 +187,6 @@
   }
 
   @Test
-  public void cannotAddPermissionByModifyingListThatWasRetrievedFromAccessSection() {
-    GroupReference groupReference1 = new GroupReference(new AccountGroup.UUID("uuid-1"), "group1");
-    PermissionRule permissionRule1 = new PermissionRule(groupReference1);
-    permission.getRules().add(permissionRule1);
-    assertThat(permission.getRule(groupReference1)).isNull();
-
-    List<PermissionRule> rules = new ArrayList<>();
-    rules.add(new PermissionRule(new GroupReference(new AccountGroup.UUID("uuid-2"), "group2")));
-    rules.add(new PermissionRule(new GroupReference(new AccountGroup.UUID("uuid-3"), "group3")));
-    permission.setRules(rules);
-    assertThat(permission.getRule(groupReference1)).isNull();
-    permission.getRules().add(permissionRule1);
-    assertThat(permission.getRule(groupReference1)).isNull();
-  }
-
-  @Test
   public void getNonExistingRule() {
     GroupReference groupReference = new GroupReference(new AccountGroup.UUID("uuid-1"), "group1");
     assertThat(permission.getRule(groupReference)).isNull();
diff --git a/javatests/com/google/gerrit/common/data/SubmitRecordTest.java b/javatests/com/google/gerrit/common/data/SubmitRecordTest.java
index 5386b87..5b9fde7 100644
--- a/javatests/com/google/gerrit/common/data/SubmitRecordTest.java
+++ b/javatests/com/google/gerrit/common/data/SubmitRecordTest.java
@@ -16,11 +16,12 @@
 
 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 {
+public class SubmitRecordTest extends GerritBaseTests {
   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 b5378a4..82a99bd 100644
--- a/javatests/com/google/gerrit/elasticsearch/BUILD
+++ b/javatests/com/google/gerrit/elasticsearch/BUILD
@@ -93,8 +93,10 @@
     tags = ["elastic"],
     deps = [
         "//java/com/google/gerrit/elasticsearch",
+        "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
         "//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 ff7b5ca..9ce1456 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticConfigurationTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticConfigurationTest.java
@@ -24,16 +24,14 @@
 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.Rule;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 
-public class ElasticConfigurationTest {
-  @Rule public ExpectedException exception = ExpectedException.none();
-
+public class ElasticConfigurationTest extends GerritBaseTests {
   @Test
   public void singleServerNoOtherConfig() throws Exception {
     Config cfg = newConfig();
@@ -119,7 +117,7 @@
   }
 
   private void assertHosts(ElasticConfiguration cfg, Object... hostURIs) throws Exception {
-    assertThat(Arrays.asList(cfg.getHosts()).stream().map(h -> h.toURI()).collect(toList()))
+    assertThat(Arrays.asList(cfg.getHosts()).stream().map(HttpHost::toURI).collect(toList()))
         .containsExactly(hostURIs);
   }
 
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java b/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
index 020a158..9aaf4bb 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
@@ -19,7 +19,6 @@
 import com.google.inject.Injector;
 import com.google.inject.Key;
 import com.google.inject.TypeLiteral;
-import java.io.IOException;
 import java.util.Collection;
 import java.util.UUID;
 import org.eclipse.jgit.lib.Config;
@@ -48,7 +47,7 @@
     configure(config, port, prefix, null);
   }
 
-  public static void createAllIndexes(Injector injector) throws IOException {
+  public static void createAllIndexes(Injector injector) {
     Collection<IndexDefinition<?, ?, ?>> indexDefs =
         injector.getInstance(Key.get(new TypeLiteral<Collection<IndexDefinition<?, ?, ?>>>() {}));
     for (IndexDefinition<?, ?, ?> indexDef : indexDefs) {
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryAccountsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryAccountsTest.java
index c8ce54a..27868d2 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryAccountsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryAccountsTest.java
@@ -65,6 +65,6 @@
     String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(
         elasticsearchConfig, nodeInfo.port, indicesPrefix, ElasticVersion.V5_6);
-    return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
+    return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryChangesTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryChangesTest.java
index cfdfa98..2e4e22a 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryChangesTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryChangesTest.java
@@ -65,6 +65,6 @@
     String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(
         elasticsearchConfig, nodeInfo.port, indicesPrefix, ElasticVersion.V5_6);
-    return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
+    return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryGroupsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryGroupsTest.java
index 832a7bd..98c4321 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryGroupsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryGroupsTest.java
@@ -65,6 +65,6 @@
     String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(
         elasticsearchConfig, nodeInfo.port, indicesPrefix, ElasticVersion.V5_6);
-    return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
+    return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryProjectsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryProjectsTest.java
index 29d3fa4..6b4b58c 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryProjectsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryProjectsTest.java
@@ -65,6 +65,6 @@
     String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(
         elasticsearchConfig, nodeInfo.port, indicesPrefix, ElasticVersion.V5_6);
-    return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
+    return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java
index 191b83c..e39c9b4 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java
@@ -64,6 +64,6 @@
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
-    return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
+    return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java
index 94c5a04..f97d23c 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java
@@ -82,6 +82,6 @@
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
-    return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
+    return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java
index 8bc5767..7e52b3b 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java
@@ -64,6 +64,6 @@
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
-    return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
+    return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryProjectsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryProjectsTest.java
index de8e922..21d6c4d 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryProjectsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryProjectsTest.java
@@ -64,6 +64,6 @@
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
-    return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
+    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 5e533fb..799ad25 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java
@@ -64,6 +64,6 @@
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
-    return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
+    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 3c70ed5..8dd9843 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
@@ -82,6 +82,6 @@
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
-    return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
+    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 f2dcdaa..df85198 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java
@@ -64,6 +64,6 @@
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
-    return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
+    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 e2de7fe..feb5fc1 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java
@@ -64,6 +64,6 @@
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
-    return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
+    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 b5c455d..3a3425a 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
@@ -16,13 +16,10 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import org.junit.Rule;
+import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 
-public class ElasticVersionTest {
-  @Rule public ExpectedException exception = ExpectedException.none();
-
+public class ElasticVersionTest extends GerritBaseTests {
   @Test
   public void supportedVersion() throws Exception {
     assertThat(ElasticVersion.forVersion("5.6.0")).isEqualTo(ElasticVersion.V5_6);
diff --git a/javatests/com/google/gerrit/extensions/BUILD b/javatests/com/google/gerrit/extensions/BUILD
index 069c915..94e433c 100644
--- a/javatests/com/google/gerrit/extensions/BUILD
+++ b/javatests/com/google/gerrit/extensions/BUILD
@@ -7,6 +7,8 @@
     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 0be10ee..86dce04 100644
--- a/javatests/com/google/gerrit/extensions/api/lfs/LfsDefinitionsTest.java
+++ b/javatests/com/google/gerrit/extensions/api/lfs/LfsDefinitionsTest.java
@@ -16,11 +16,12 @@
 
 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 {
+public class LfsDefinitionsTest extends GerritBaseTests {
   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
new file mode 100644
index 0000000..4bb9107
--- /dev/null
+++ b/javatests/com/google/gerrit/extensions/client/ListOptionTest.java
@@ -0,0 +1,72 @@
+// 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.client;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.extensions.client.ListOptionTest.MyOption.BAR;
+import static com.google.gerrit.extensions.client.ListOptionTest.MyOption.BAZ;
+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 {
+  enum MyOption implements ListOption {
+    FOO(0),
+    BAR(1),
+    BAZ(17);
+
+    private final int value;
+
+    MyOption(int value) {
+      this.value = value;
+    }
+
+    @Override
+    public int getValue() {
+      return value;
+    }
+  }
+
+  @Test
+  public void fromBits() {
+    assertThat(IntMath.pow(2, BAZ.getValue())).isEqualTo(131072);
+    assertThat(ListOption.fromBits(MyOption.class, 0)).isEmpty();
+    assertThat(ListOption.fromBits(MyOption.class, 1)).containsExactly(FOO);
+    assertThat(ListOption.fromBits(MyOption.class, 2)).containsExactly(BAR);
+    assertThat(ListOption.fromBits(MyOption.class, 131072)).containsExactly(BAZ);
+    assertThat(ListOption.fromBits(MyOption.class, 3)).containsExactly(FOO, BAR);
+    assertThat(ListOption.fromBits(MyOption.class, 131073)).containsExactly(FOO, BAZ);
+    assertThat(ListOption.fromBits(MyOption.class, 131074)).containsExactly(BAR, BAZ);
+    assertThat(ListOption.fromBits(MyOption.class, 131075)).containsExactly(FOO, BAR, BAZ);
+
+    assertFromBitsFails(4);
+    assertFromBitsFails(8);
+    assertFromBitsFails(16);
+    assertFromBitsFails(250);
+  }
+
+  private void assertFromBitsFails(int v) {
+    try {
+      EnumSet<MyOption> opts = ListOption.fromBits(MyOption.class, v);
+      assertWithMessage("expected RuntimeException for fromBits(%s), got: %s", v, opts).fail();
+    } catch (RuntimeException e) {
+      // Expected.
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/extensions/client/RangeTest.java b/javatests/com/google/gerrit/extensions/client/RangeTest.java
index b8938aa..2c713b5 100644
--- a/javatests/com/google/gerrit/extensions/client/RangeTest.java
+++ b/javatests/com/google/gerrit/extensions/client/RangeTest.java
@@ -16,9 +16,10 @@
 
 import static com.google.gerrit.extensions.common.testing.RangeSubject.assertThat;
 
+import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class RangeTest {
+public class RangeTest extends GerritBaseTests {
 
   @Test
   public void rangeOverMultipleLinesWithSmallerEndCharacterIsValid() {
diff --git a/javatests/com/google/gerrit/extensions/conditions/BUILD b/javatests/com/google/gerrit/extensions/conditions/BUILD
index e2d5951..7ad2ad3 100644
--- a/javatests/com/google/gerrit/extensions/conditions/BUILD
+++ b/javatests/com/google/gerrit/extensions/conditions/BUILD
@@ -5,6 +5,7 @@
     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 f9f1fa85..81cb719 100644
--- a/javatests/com/google/gerrit/extensions/conditions/BooleanConditionTest.java
+++ b/javatests/com/google/gerrit/extensions/conditions/BooleanConditionTest.java
@@ -20,9 +20,10 @@
 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 {
+public class BooleanConditionTest extends GerritBaseTests {
 
   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 0542c35..d950224 100644
--- a/javatests/com/google/gerrit/extensions/registration/DynamicSetTest.java
+++ b/javatests/com/google/gerrit/extensions/registration/DynamicSetTest.java
@@ -17,13 +17,14 @@
 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 {
+public class DynamicSetTest extends GerritBaseTests {
   // 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
new file mode 100644
index 0000000..d57d73f
--- /dev/null
+++ b/javatests/com/google/gerrit/git/BUILD
@@ -0,0 +1,36 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+
+MEDIUM_TESTS = ["RefUpdateUtilRepoTest.java"]
+
+junit_tests(
+    name = "medium_tests",
+    size = "medium",
+    timeout = "short",
+    srcs = MEDIUM_TESTS,
+    tags = ["no_windows"],
+    deps = [
+        "//java/com/google/gerrit/git",
+        "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//lib:guava",
+        "//lib:junit",
+        "//lib/jgit/org.eclipse.jgit:jgit",
+        "//lib/jgit/org.eclipse.jgit.junit:junit",
+        "//lib/truth",
+    ],
+)
+
+junit_tests(
+    name = "small_tests",
+    size = "small",
+    srcs = glob(
+        ["*.java"],
+        exclude = MEDIUM_TESTS,
+    ),
+    deps = [
+        "//java/com/google/gerrit/git",
+        "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//lib:guava",
+        "//lib/jgit/org.eclipse.jgit:jgit",
+        "//lib/truth",
+    ],
+)
diff --git a/javatests/com/google/gerrit/git/RefUpdateUtilRepoTest.java b/javatests/com/google/gerrit/git/RefUpdateUtilRepoTest.java
new file mode 100644
index 0000000..59961ff
--- /dev/null
+++ b/javatests/com/google/gerrit/git/RefUpdateUtilRepoTest.java
@@ -0,0 +1,121 @@
+// 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.git;
+
+import static com.google.common.truth.Truth.assertThat;
+
+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;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class RefUpdateUtilRepoTest extends GerritBaseTests {
+  public enum RepoSetup {
+    LOCAL_DISK {
+      @Override
+      Repository setUpRepo() throws Exception {
+        Path p = Files.createTempDirectory("gerrit_repo_");
+        try {
+          Repository repo = new FileRepository(p.toFile());
+          repo.create(true);
+          return repo;
+        } catch (Exception e) {
+          delete(p);
+          throw e;
+        }
+      }
+
+      @Override
+      void tearDownRepo(Repository repo) throws Exception {
+        delete(repo.getDirectory().toPath());
+      }
+
+      private void delete(Path p) throws Exception {
+        MoreFiles.deleteRecursively(p, RecursiveDeleteOption.ALLOW_INSECURE);
+      }
+    },
+
+    IN_MEMORY {
+      @Override
+      Repository setUpRepo() {
+        return new InMemoryRepository(new DfsRepositoryDescription("repo"));
+      }
+
+      @Override
+      void tearDownRepo(Repository repo) {}
+    };
+
+    abstract Repository setUpRepo() throws Exception;
+
+    abstract void tearDownRepo(Repository repo) throws Exception;
+  }
+
+  @Parameters(name = "{0}")
+  public static ImmutableList<RepoSetup[]> data() {
+    return ImmutableList.copyOf(new RepoSetup[][] {{RepoSetup.LOCAL_DISK}, {RepoSetup.IN_MEMORY}});
+  }
+
+  @Parameter public RepoSetup repoSetup;
+
+  private Repository repo;
+
+  @Before
+  public void setUp() throws Exception {
+    repo = repoSetup.setUpRepo();
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    if (repo != null) {
+      repoSetup.tearDownRepo(repo);
+      repo = null;
+    }
+  }
+
+  @Test
+  public void deleteRefNoOp() throws Exception {
+    String ref = "refs/heads/foo";
+    assertThat(repo.exactRef(ref)).isNull();
+    RefUpdateUtil.deleteChecked(repo, "refs/heads/foo");
+    assertThat(repo.exactRef(ref)).isNull();
+  }
+
+  @Test
+  public void deleteRef() throws Exception {
+    String ref = "refs/heads/foo";
+    try (TestRepository<Repository> tr = new TestRepository<>(repo)) {
+      tr.branch(ref).commit().create();
+    }
+
+    assertThat(repo.exactRef(ref)).isNotNull();
+    RefUpdateUtil.deleteChecked(repo, "refs/heads/foo");
+    assertThat(repo.exactRef(ref)).isNull();
+  }
+}
diff --git a/javatests/com/google/gerrit/git/RefUpdateUtilTest.java b/javatests/com/google/gerrit/git/RefUpdateUtilTest.java
new file mode 100644
index 0000000..429583a
--- /dev/null
+++ b/javatests/com/google/gerrit/git/RefUpdateUtilTest.java
@@ -0,0 +1,119 @@
+// 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.git;
+
+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 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;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class RefUpdateUtilTest extends GerritBaseTests {
+  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);
+  private static final Consumer<ReceiveCommand> REJECTED =
+      c -> c.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON);
+  private static final Consumer<ReceiveCommand> ABORTED =
+      c -> {
+        c.setResult(ReceiveCommand.Result.NOT_ATTEMPTED);
+        ReceiveCommand.abort(ImmutableList.of(c));
+        checkState(
+            c.getResult() != ReceiveCommand.Result.NOT_ATTEMPTED
+                && c.getResult() != ReceiveCommand.Result.LOCK_FAILURE
+                && c.getResult() != ReceiveCommand.Result.OK,
+            "unexpected state after abort: %s",
+            c);
+      };
+
+  @Test
+  public void checkBatchRefUpdateResults() throws Exception {
+    checkResults();
+    checkResults(OK);
+    checkResults(OK, OK);
+
+    assertIoException(REJECTED);
+    assertIoException(OK, REJECTED);
+    assertIoException(LOCK_FAILURE, REJECTED);
+    assertIoException(LOCK_FAILURE, OK);
+    assertIoException(LOCK_FAILURE, REJECTED, OK);
+    assertIoException(LOCK_FAILURE, LOCK_FAILURE, REJECTED);
+    assertIoException(LOCK_FAILURE, ABORTED, REJECTED);
+    assertIoException(LOCK_FAILURE, ABORTED, OK);
+
+    assertLockFailureException(LOCK_FAILURE);
+    assertLockFailureException(LOCK_FAILURE, LOCK_FAILURE);
+    assertLockFailureException(LOCK_FAILURE, LOCK_FAILURE, ABORTED);
+    assertLockFailureException(LOCK_FAILURE, LOCK_FAILURE, ABORTED, ABORTED);
+    assertLockFailureException(ABORTED);
+    assertLockFailureException(ABORTED, ABORTED);
+  }
+
+  @SafeVarargs
+  private static void checkResults(Consumer<ReceiveCommand>... resultSetters) throws Exception {
+    RefUpdateUtil.checkResults(newBatchRefUpdate(resultSetters));
+  }
+
+  @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);
+    }
+  }
+
+  @SafeVarargs
+  private static void assertLockFailureException(Consumer<ReceiveCommand>... resultSetters)
+      throws Exception {
+    try {
+      RefUpdateUtil.checkResults(newBatchRefUpdate(resultSetters));
+      assert_().fail("expected LockFailureException");
+    } catch (LockFailureException e) {
+      // Expected.
+    }
+  }
+
+  @SafeVarargs
+  private static BatchRefUpdate newBatchRefUpdate(Consumer<ReceiveCommand>... resultSetters) {
+    try (Repository repo = new InMemoryRepository(new DfsRepositoryDescription("repo"))) {
+      BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
+      for (int i = 0; i < resultSetters.length; i++) {
+        ReceiveCommand cmd =
+            new ReceiveCommand(
+                ObjectId.fromString(String.format("%039x1", i)),
+                ObjectId.fromString(String.format("%039x2", i)),
+                "refs/heads/branch" + i);
+        bru.addCommand(cmd);
+        resultSetters[i].accept(cmd);
+      }
+      return bru;
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/git/testing/BUILD b/javatests/com/google/gerrit/git/testing/BUILD
index 56e9ec2..1309185 100644
--- a/javatests/com/google/gerrit/git/testing/BUILD
+++ b/javatests/com/google/gerrit/git/testing/BUILD
@@ -5,6 +5,7 @@
     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 3bf815b..5ab52d4 100644
--- a/javatests/com/google/gerrit/git/testing/PushResultSubjectTest.java
+++ b/javatests/com/google/gerrit/git/testing/PushResultSubjectTest.java
@@ -18,9 +18,10 @@
 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 {
+public class PushResultSubjectTest extends GerritBaseTests {
   @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 baf65b7..6edfa93 100644
--- a/javatests/com/google/gerrit/gpg/BUILD
+++ b/javatests/com/google/gerrit/gpg/BUILD
@@ -19,7 +19,6 @@
         "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib/bouncycastle:bcpg",
         "//lib/bouncycastle:bcpg-neverlink",
         "//lib/bouncycastle:bcprov",
diff --git a/javatests/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java b/javatests/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
index 685f42d..220361e 100644
--- a/javatests/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
+++ b/javatests/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
@@ -33,8 +33,6 @@
 import com.google.gerrit.gpg.testing.TestKey;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.AccountManager;
@@ -42,16 +40,13 @@
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.schema.SchemaCreator;
-import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
-import com.google.gerrit.testing.InMemoryDatabase;
+import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.testing.InMemoryModule;
-import com.google.gerrit.testing.NoteDbMode;
 import com.google.inject.Guice;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.Provider;
-import com.google.inject.util.Providers;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -69,7 +64,7 @@
 import org.junit.Test;
 
 /** Unit tests for {@link GerritPublicKeyChecker}. */
-public class GerritPublicKeyCheckerTest {
+public class GerritPublicKeyCheckerTest extends GerritBaseTests {
   @Inject @ServerInitiated private Provider<AccountsUpdate> accountsUpdateProvider;
 
   @Inject private AccountManager accountManager;
@@ -78,14 +73,11 @@
 
   @Inject private IdentifiedUser.GenericFactory userFactory;
 
-  @Inject private InMemoryDatabase schemaFactory;
-
   @Inject private SchemaCreator schemaCreator;
 
   @Inject private ThreadLocalRequestContext requestContext;
 
   private LifecycleManager lifecycle;
-  private ReviewDb db;
   private Account.Id userId;
   private IdentifiedUser user;
   private Repository storeRepo;
@@ -102,16 +94,14 @@
         ImmutableList.of(
             Fingerprint.toString(keyB().getPublicKey().getFingerprint()),
             Fingerprint.toString(keyD().getPublicKey().getFingerprint())));
-    Injector injector =
-        Guice.createInjector(new InMemoryModule(cfg, NoteDbMode.newNotesMigrationFromEnv()));
+    Injector injector = Guice.createInjector(new InMemoryModule(cfg));
 
     lifecycle = new LifecycleManager();
     lifecycle.add(injector);
     injector.injectMembers(this);
     lifecycle.start();
 
-    db = schemaFactory.open();
-    schemaCreator.create(db);
+    schemaCreator.create();
     userId = accountManager.authenticate(AuthRequest.forUser("user")).getAccountId();
     // Note: does not match any key in TestKeys.
     accountsUpdateProvider
@@ -119,18 +109,7 @@
         .update("Set Preferred Email", userId, u -> u.setPreferredEmail("user@example.com"));
     user = reloadUser();
 
-    requestContext.setContext(
-        new RequestContext() {
-          @Override
-          public CurrentUser getUser() {
-            return user;
-          }
-
-          @Override
-          public Provider<ReviewDb> getReviewDbProvider() {
-            return Providers.of(db);
-          }
-        });
+    requestContext.setContext(() -> user);
 
     storeRepo = new InMemoryRepository(new DfsRepositoryDescription("repo"));
     store = new PublicKeyStore(storeRepo);
@@ -158,10 +137,6 @@
     if (lifecycle != null) {
       lifecycle.stop();
     }
-    if (db != null) {
-      db.close();
-    }
-    InMemoryDatabase.drop(schemaFactory);
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/gpg/PublicKeyCheckerTest.java b/javatests/com/google/gerrit/gpg/PublicKeyCheckerTest.java
index 48d5266..145b9cf 100644
--- a/javatests/com/google/gerrit/gpg/PublicKeyCheckerTest.java
+++ b/javatests/com/google/gerrit/gpg/PublicKeyCheckerTest.java
@@ -38,6 +38,7 @@
 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;
@@ -57,13 +58,9 @@
 import org.eclipse.jgit.lib.RefUpdate;
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 
-public class PublicKeyCheckerTest {
-  @Rule public ExpectedException thrown = ExpectedException.none();
-
+public class PublicKeyCheckerTest extends GerritBaseTests {
   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 2fc06a6..be65752 100644
--- a/javatests/com/google/gerrit/gpg/PublicKeyStoreTest.java
+++ b/javatests/com/google/gerrit/gpg/PublicKeyStoreTest.java
@@ -21,6 +21,7 @@
 import static com.google.gerrit.gpg.testing.TestKeys.validKeyWithExpiration;
 import static com.google.gerrit.gpg.testing.TestKeys.validKeyWithSecondUserId;
 import static com.google.gerrit.gpg.testing.TestKeys.validKeyWithoutExpiration;
+import static com.google.gerrit.gpg.testing.TestKeys.validKeyWithoutExpirationWithSubkeyWithExpiration;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -28,6 +29,7 @@
 
 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;
@@ -49,7 +51,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class PublicKeyStoreTest {
+public class PublicKeyStoreTest extends GerritBaseTests {
   private TestRepository<?> tr;
   private PublicKeyStore store;
 
@@ -100,6 +102,25 @@
   }
 
   @Test
+  public void getSubkeyReturnsMasterKey() throws Exception {
+    TestKey key1 = validKeyWithoutExpirationWithSubkeyWithExpiration();
+    PGPPublicKeyRing keyRing = key1.getPublicKeyRing();
+    store.add(keyRing);
+
+    assertEquals(RefUpdate.Result.NEW, store.save(newCommitBuilder()));
+
+    long masterKeyId = key1.getKeyId();
+    long subKeyId = 0;
+    for (PGPPublicKey key : keyRing) {
+      if (masterKeyId != subKeyId) {
+        subKeyId = key.getKeyID();
+      }
+    }
+
+    assertKeys(subKeyId, key1);
+  }
+
+  @Test
   public void getMultiple() throws Exception {
     TestKey key1 = validKeyWithoutExpiration();
     TestKey key2 = validKeyWithExpiration();
@@ -201,6 +222,29 @@
   }
 
   @Test
+  public void removeMasterKeyRemovesSubkey() throws Exception {
+    TestKey key1 = validKeyWithoutExpirationWithSubkeyWithExpiration();
+    PGPPublicKeyRing keyRing = key1.getPublicKeyRing();
+    store.add(keyRing);
+
+    assertEquals(RefUpdate.Result.NEW, store.save(newCommitBuilder()));
+
+    long masterKeyId = key1.getKeyId();
+    long subKeyId = 0;
+    for (PGPPublicKey key : keyRing) {
+      if (masterKeyId != subKeyId) {
+        subKeyId = key.getKeyID();
+      }
+    }
+
+    store.remove(key1.getPublicKey().getFingerprint());
+    assertEquals(RefUpdate.Result.FAST_FORWARD, store.save(newCommitBuilder()));
+
+    assertKeys(masterKeyId);
+    assertKeys(subKeyId);
+  }
+
+  @Test
   public void removeNonexisting() throws Exception {
     TestKey key1 = validKeyWithoutExpiration();
     store.add(key1.getPublicKeyRing());
diff --git a/javatests/com/google/gerrit/gpg/PushCertificateCheckerTest.java b/javatests/com/google/gerrit/gpg/PushCertificateCheckerTest.java
index 266f868..67bf050 100644
--- a/javatests/com/google/gerrit/gpg/PushCertificateCheckerTest.java
+++ b/javatests/com/google/gerrit/gpg/PushCertificateCheckerTest.java
@@ -23,6 +23,7 @@
 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;
@@ -53,7 +54,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class PushCertificateCheckerTest {
+public class PushCertificateCheckerTest extends GerritBaseTests {
   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 1c6559b0..e2c58d8 100644
--- a/javatests/com/google/gerrit/httpd/AllRequestFilterFilterProxyTest.java
+++ b/javatests/com/google/gerrit/httpd/AllRequestFilterFilterProxyTest.java
@@ -21,6 +21,7 @@
 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;
@@ -35,7 +36,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class AllRequestFilterFilterProxyTest {
+public class AllRequestFilterFilterProxyTest extends GerritBaseTests {
   /**
    * Set of filters for FilterProxy
    *
diff --git a/javatests/com/google/gerrit/httpd/BUILD b/javatests/com/google/gerrit/httpd/BUILD
index ec2df15..0fbd922 100644
--- a/javatests/com/google/gerrit/httpd/BUILD
+++ b/javatests/com/google/gerrit/httpd/BUILD
@@ -10,11 +10,11 @@
         "//java/com/google/gerrit/httpd",
         "//java/com/google/gerrit/reviewdb:server",
         "//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:gwtorm",
         "//lib:jimfs",
         "//lib:junit",
         "//lib:servlet-api-3_1-without-neverlink",
diff --git a/javatests/com/google/gerrit/httpd/RemoteUserUtilTest.java b/javatests/com/google/gerrit/httpd/RemoteUserUtilTest.java
index f012ee3..e19085d 100644
--- a/javatests/com/google/gerrit/httpd/RemoteUserUtilTest.java
+++ b/javatests/com/google/gerrit/httpd/RemoteUserUtilTest.java
@@ -17,9 +17,10 @@
 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 {
+public class RemoteUserUtilTest extends GerritBaseTests {
   @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 684a241..2de3788 100644
--- a/javatests/com/google/gerrit/httpd/plugins/ContextMapperTest.java
+++ b/javatests/com/google/gerrit/httpd/plugins/ContextMapperTest.java
@@ -16,11 +16,12 @@
 
 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 {
+public class ContextMapperTest extends GerritBaseTests {
 
   private static final String CONTEXT = "/context";
   private static final String PLUGIN_NAME = "my-plugin";
diff --git a/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java b/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java
index 307a23e..340f690 100644
--- a/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java
+++ b/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java
@@ -17,11 +17,12 @@
 import static com.google.common.truth.Truth.assertThat;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import com.google.template.soy.data.SoyMapData;
+import com.google.gerrit.testing.GerritBaseTests;
 import java.net.URISyntaxException;
+import java.util.Map;
 import org.junit.Test;
 
-public class IndexServletTest {
+public class IndexServletTest extends GerritBaseTests {
   static class TestIndexServlet extends IndexServlet {
     private static final long serialVersionUID = 1L;
 
@@ -37,35 +38,34 @@
 
   @Test
   public void noPathAndNoCDN() throws URISyntaxException {
-    SoyMapData data = IndexServlet.getTemplateData("http://example.com/", null, null);
-    assertThat(data.getSingle("canonicalPath").stringValue()).isEqualTo("");
-    assertThat(data.getSingle("staticResourcePath").stringValue()).isEqualTo("");
+    Map<String, Object> data = IndexServlet.getTemplateData("http://example.com/", null, null);
+    assertThat(data.get("canonicalPath")).isEqualTo("");
+    assertThat(data.get("staticResourcePath").toString()).isEqualTo("");
   }
 
   @Test
   public void pathAndNoCDN() throws URISyntaxException {
-    SoyMapData data = IndexServlet.getTemplateData("http://example.com/gerrit/", null, null);
-    assertThat(data.getSingle("canonicalPath").stringValue()).isEqualTo("/gerrit");
-    assertThat(data.getSingle("staticResourcePath").stringValue()).isEqualTo("/gerrit");
+    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");
   }
 
   @Test
   public void noPathAndCDN() throws URISyntaxException {
-    SoyMapData data =
+    Map<String, Object> data =
         IndexServlet.getTemplateData("http://example.com/", "http://my-cdn.com/foo/bar/", null);
-    assertThat(data.getSingle("canonicalPath").stringValue()).isEqualTo("");
-    assertThat(data.getSingle("staticResourcePath").stringValue())
-        .isEqualTo("http://my-cdn.com/foo/bar/");
+    assertThat(data.get("canonicalPath")).isEqualTo("");
+    assertThat(data.get("staticResourcePath").toString()).isEqualTo("http://my-cdn.com/foo/bar/");
   }
 
   @Test
   public void pathAndCDN() throws URISyntaxException {
-    SoyMapData data =
+    Map<String, Object> data =
         IndexServlet.getTemplateData(
             "http://example.com/gerrit", "http://my-cdn.com/foo/bar/", null);
-    assertThat(data.getSingle("canonicalPath").stringValue()).isEqualTo("/gerrit");
-    assertThat(data.getSingle("staticResourcePath").stringValue())
-        .isEqualTo("http://my-cdn.com/foo/bar/");
+    assertThat(data.get("canonicalPath")).isEqualTo("/gerrit");
+    assertThat(data.get("staticResourcePath").toString()).isEqualTo("http://my-cdn.com/foo/bar/");
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/httpd/raw/ResourceServletTest.java b/javatests/com/google/gerrit/httpd/raw/ResourceServletTest.java
index 6dd15bc..cfcc1d0 100644
--- a/javatests/com/google/gerrit/httpd/raw/ResourceServletTest.java
+++ b/javatests/com/google/gerrit/httpd/raw/ResourceServletTest.java
@@ -28,6 +28,7 @@
 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;
@@ -44,7 +45,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class ResourceServletTest {
+public class ResourceServletTest extends GerritBaseTests {
   private static Cache<Path, Resource> newCache(int size) {
     return CacheBuilder.newBuilder().maximumSize(size).recordStats().build();
   }
diff --git a/javatests/com/google/gerrit/httpd/restapi/HttpLogRedactTest.java b/javatests/com/google/gerrit/httpd/restapi/HttpLogRedactTest.java
index fb1ebd9..fa3eaea 100644
--- a/javatests/com/google/gerrit/httpd/restapi/HttpLogRedactTest.java
+++ b/javatests/com/google/gerrit/httpd/restapi/HttpLogRedactTest.java
@@ -16,9 +16,10 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class HttpLogRedactTest {
+public class HttpLogRedactTest extends GerritBaseTests {
   @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 13732b0..30d318b 100644
--- a/javatests/com/google/gerrit/httpd/restapi/ParameterParserTest.java
+++ b/javatests/com/google/gerrit/httpd/restapi/ParameterParserTest.java
@@ -22,13 +22,14 @@
 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 {
+public class ParameterParserTest extends GerritBaseTests {
   @Test
   public void convertFormToJson() throws BadRequestException {
     JsonObject obj =
diff --git a/javatests/com/google/gerrit/index/BUILD b/javatests/com/google/gerrit/index/BUILD
index 5597ed1..a1f60de 100644
--- a/javatests/com/google/gerrit/index/BUILD
+++ b/javatests/com/google/gerrit/index/BUILD
@@ -9,6 +9,8 @@
         "//antlr3:query_parser",
         "//java/com/google/gerrit/index",
         "//java/com/google/gerrit/index:query_exception",
+        "//java/com/google/gerrit/index/query/testing",
+        "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
         "//lib:junit",
         "//lib/antlr:java-runtime",
diff --git a/javatests/com/google/gerrit/index/SchemaUtilTest.java b/javatests/com/google/gerrit/index/SchemaUtilTest.java
index 3c0bbe0..d6b8421 100644
--- a/javatests/com/google/gerrit/index/SchemaUtilTest.java
+++ b/javatests/com/google/gerrit/index/SchemaUtilTest.java
@@ -19,15 +19,12 @@
 import static com.google.gerrit.index.SchemaUtil.getPersonParts;
 import static com.google.gerrit.index.SchemaUtil.schema;
 
+import com.google.gerrit.testing.GerritBaseTests;
 import java.util.Map;
 import org.eclipse.jgit.lib.PersonIdent;
-import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 
-public class SchemaUtilTest {
-  @Rule public ExpectedException exception = ExpectedException.none();
-
+public class SchemaUtilTest extends GerritBaseTests {
   static class TestSchemas {
     static final Schema<String> V1 = schema();
     static final Schema<String> V2 = schema();
diff --git a/javatests/com/google/gerrit/index/query/FieldPredicateTest.java b/javatests/com/google/gerrit/index/query/FieldPredicateTest.java
index 8fe90fc..805f31c 100644
--- a/javatests/com/google/gerrit/index/query/FieldPredicateTest.java
+++ b/javatests/com/google/gerrit/index/query/FieldPredicateTest.java
@@ -58,7 +58,7 @@
   @Test
   public void testCopy() {
     final OperatorPredicate<String> f = f("author", "alice");
-    assertSame(f, f.copy(Collections.<Predicate<String>>emptyList()));
+    assertSame(f, f.copy(Collections.emptyList()));
     assertSame(f, f.copy(f.getChildren()));
 
     exception.expect(IllegalArgumentException.class);
diff --git a/javatests/com/google/gerrit/index/query/NotPredicateTest.java b/javatests/com/google/gerrit/index/query/NotPredicateTest.java
index 74eac61..d10d2df 100644
--- a/javatests/com/google/gerrit/index/query/NotPredicateTest.java
+++ b/javatests/com/google/gerrit/index/query/NotPredicateTest.java
@@ -113,7 +113,7 @@
     assertEquals(sb, n.copy(sb).getChildren());
 
     try {
-      n.copy(Collections.<Predicate>emptyList());
+      n.copy(Collections.emptyList());
       fail("Expected IllegalArgumentException");
     } catch (IllegalArgumentException e) {
       assertEquals("Expected exactly one child", e.getMessage());
diff --git a/javatests/com/google/gerrit/index/query/PredicateTest.java b/javatests/com/google/gerrit/index/query/PredicateTest.java
index 6979d82..2295a60 100644
--- a/javatests/com/google/gerrit/index/query/PredicateTest.java
+++ b/javatests/com/google/gerrit/index/query/PredicateTest.java
@@ -14,14 +14,11 @@
 
 package com.google.gerrit.index.query;
 
+import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Ignore;
-import org.junit.Rule;
-import org.junit.rules.ExpectedException;
 
 @Ignore
-public abstract class PredicateTest {
-  @Rule public ExpectedException exception = ExpectedException.none();
-
+public abstract class PredicateTest extends GerritBaseTests {
   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
new file mode 100644
index 0000000..6a397dc
--- /dev/null
+++ b/javatests/com/google/gerrit/index/query/QueryBuilderTest.java
@@ -0,0 +1,123 @@
+// 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.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 {
+  private static class TestPredicate extends Predicate<Object> {
+    private final String field;
+    private final String value;
+
+    TestPredicate(String field, String value) {
+      this.field = field;
+      this.value = value;
+    }
+
+    @Override
+    public Predicate<Object> copy(Collection<? extends Predicate<Object>> children) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(field, value);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (!(o instanceof TestPredicate)) {
+        return false;
+      }
+      TestPredicate p = (TestPredicate) o;
+      return Objects.equals(field, p.field) && Objects.equals(value, p.value);
+    }
+  }
+
+  private static class TestQueryBuilder extends QueryBuilder<Object, TestQueryBuilder> {
+    TestQueryBuilder() {
+      super(new QueryBuilder.Definition<>(TestQueryBuilder.class), null);
+    }
+
+    @Operator
+    public Predicate<Object> a(String value) {
+      return new TestPredicate("a", value);
+    }
+  }
+
+  @Test
+  public void fieldNameAndValue() throws Exception {
+    assertThat(parse("a:foo")).isEqualTo(new TestPredicate("a", "foo"));
+  }
+
+  @Test
+  public void fieldWithParenthesizedValues() throws Exception {
+    assertThatParseException("a:(foo bar)").hasMessageThat().contains("no viable alternative");
+  }
+
+  @Test
+  public void fieldNameAndValueThatLooksLikeFieldNameColonValue() throws Exception {
+    assertThat(parse("a:foo:bar")).isEqualTo(new TestPredicate("a", "foo:bar"));
+  }
+
+  @Test
+  public void fieldNameAndValueThatLooksLikeWordColonValue() throws Exception {
+    assertThat(parse("a:*:bar")).isEqualTo(new TestPredicate("a", "*:bar"));
+  }
+
+  @Test
+  public void fieldNameAndValueWithMultipleColons() throws Exception {
+    assertThat(parse("a:*:*:*")).isEqualTo(new TestPredicate("a", "*:*:*"));
+  }
+
+  @Test
+  public void exactPhraseWithQuotes() throws Exception {
+    assertThat(parse("a:\"foo bar\"")).isEqualTo(new TestPredicate("a", "foo bar"));
+  }
+
+  @Test
+  public void exactPhraseWithQuotesAndColon() throws Exception {
+    assertThat(parse("a:\"foo:bar\"")).isEqualTo(new TestPredicate("a", "foo:bar"));
+  }
+
+  @Test
+  public void exactPhraseWithBraces() throws Exception {
+    assertThat(parse("a:{foo bar}")).isEqualTo(new TestPredicate("a", "foo bar"));
+  }
+
+  @Test
+  public void exactPhraseWithBracesAndColon() throws Exception {
+    assertThat(parse("a:{foo:bar}")).isEqualTo(new TestPredicate("a", "foo:bar"));
+  }
+
+  private static Predicate<Object> parse(String query) throws Exception {
+    return new TestQueryBuilder().parse(query);
+  }
+
+  private static ThrowableSubject assertThatParseException(String query) {
+    try {
+      new TestQueryBuilder().parse(query);
+      throw new AssertionError("expected QueryParseException for " + query);
+    } catch (QueryParseException e) {
+      return assertThat(e);
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/index/query/QueryParserTest.java b/javatests/com/google/gerrit/index/query/QueryParserTest.java
index 448f292..b4dd1ee 100644
--- a/javatests/com/google/gerrit/index/query/QueryParserTest.java
+++ b/javatests/com/google/gerrit/index/query/QueryParserTest.java
@@ -14,34 +14,203 @@
 
 package com.google.gerrit.index.query;
 
-import static org.junit.Assert.assertEquals;
+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;
+import static com.google.gerrit.index.query.QueryParser.FIELD_NAME;
+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 com.google.gerrit.testing.GerritBaseTests;
 import org.antlr.runtime.tree.Tree;
 import org.junit.Test;
 
-public class QueryParserTest {
+public class QueryParserTest extends GerritBaseTests {
   @Test
-  public void projectBare() throws QueryParseException {
-    Tree r;
-
-    r = parse("project:tools/gerrit");
-    assertSingleWord("project", "tools/gerrit", r);
-
-    r = parse("project:tools/*");
-    assertSingleWord("project", "tools/*", r);
+  public void fieldNameAndValue() throws Exception {
+    Tree r = parse("project:tools/gerrit");
+    assertThat(r).hasType(FIELD_NAME);
+    assertThat(r).hasText("project");
+    assertThat(r).hasChildCount(1);
+    assertThat(r).child(0).hasType(SINGLE_WORD);
+    assertThat(r).child(0).hasText("tools/gerrit");
+    assertThat(r).child(0).hasNoChildren();
   }
 
-  private static void assertSingleWord(String name, String value, Tree r) {
-    assertEquals(QueryParser.FIELD_NAME, r.getType());
-    assertEquals(name, r.getText());
-    assertEquals(1, r.getChildCount());
-    final Tree c = r.getChild(0);
-    assertEquals(QueryParser.SINGLE_WORD, c.getType());
-    assertEquals(value, c.getText());
-    assertEquals(0, c.getChildCount());
+  @Test
+  public void fieldNameAndValueThatLooksLikeFieldNameColon() throws Exception {
+    // This should work, but doesn't due to a known issue.
+    assertParseFails("project:foo:");
   }
 
-  private static Tree parse(String str) throws QueryParseException {
-    return QueryParser.parse(str);
+  @Test
+  public void fieldNameAndValueThatLooksLikeFieldNameColonValue() throws Exception {
+    Tree r = parse("project:foo:bar");
+    assertThat(r).hasType(FIELD_NAME);
+    assertThat(r).hasText("project");
+    assertThat(r).hasChildCount(3);
+    assertThat(r).child(0).hasType(SINGLE_WORD);
+    assertThat(r).child(0).hasText("foo");
+    assertThat(r).child(0).hasNoChildren();
+    assertThat(r).child(1).hasType(COLON);
+    assertThat(r).child(1).hasText(":");
+    assertThat(r).child(1).hasNoChildren();
+    assertThat(r).child(2).hasType(SINGLE_WORD);
+    assertThat(r).child(2).hasText("bar");
+    assertThat(r).child(2).hasNoChildren();
+  }
+
+  @Test
+  public void fieldNameAndValueThatLooksLikeWordColonValue() throws Exception {
+    Tree r = parse("project:x*y:a*b");
+    assertThat(r).hasType(FIELD_NAME);
+    assertThat(r).hasText("project");
+    assertThat(r).hasChildCount(3);
+    assertThat(r).child(0).hasType(SINGLE_WORD);
+    assertThat(r).child(0).hasText("x*y");
+    assertThat(r).child(0).hasNoChildren();
+    assertThat(r).child(1).hasType(COLON);
+    assertThat(r).child(1).hasText(":");
+    assertThat(r).child(1).hasNoChildren();
+    assertThat(r).child(2).hasType(SINGLE_WORD);
+    assertThat(r).child(2).hasText("a*b");
+    assertThat(r).child(2).hasNoChildren();
+  }
+
+  @Test
+  public void fieldNameAndValueThatLooksLikeWordColon() throws Exception {
+    // This should work, but doesn't due to a known issue.
+    assertParseFails("project:x*y:");
+  }
+
+  @Test
+  public void fieldNameAndValueWithMultipleColons() throws Exception {
+    Tree r = parse("project:*:*:*");
+    assertThat(r).hasType(FIELD_NAME);
+    assertThat(r).hasText("project");
+    assertThat(r).hasChildCount(5);
+    assertThat(r).child(0).hasType(SINGLE_WORD);
+    assertThat(r).child(0).hasText("*");
+    assertThat(r).child(0).hasNoChildren();
+    assertThat(r).child(1).hasType(COLON);
+    assertThat(r).child(1).hasText(":");
+    assertThat(r).child(1).hasNoChildren();
+    assertThat(r).child(2).hasType(SINGLE_WORD);
+    assertThat(r).child(2).hasText("*");
+    assertThat(r).child(2).hasNoChildren();
+    assertThat(r).child(3).hasType(COLON);
+    assertThat(r).child(3).hasText(":");
+    assertThat(r).child(3).hasNoChildren();
+    assertThat(r).child(4).hasType(SINGLE_WORD);
+    assertThat(r).child(4).hasText("*");
+    assertThat(r).child(4).hasNoChildren();
+  }
+
+  @Test
+  public void fieldNameAndValueWithColonFollowedByAnotherField() throws Exception {
+    Tree r = parse("project:foo:bar file:baz");
+    assertThat(r).hasType(AND);
+    assertThat(r).hasChildCount(2);
+
+    assertThat(r).child(0).hasType(FIELD_NAME);
+    assertThat(r).child(0).hasText("project");
+    assertThat(r).child(0).hasChildCount(3);
+    assertThat(r).child(0).child(0).hasType(SINGLE_WORD);
+    assertThat(r).child(0).child(0).hasText("foo");
+    assertThat(r).child(0).child(0).hasNoChildren();
+    assertThat(r).child(0).child(1).hasType(COLON);
+    assertThat(r).child(0).child(1).hasText(":");
+    assertThat(r).child(0).child(1).hasNoChildren();
+    assertThat(r).child(0).child(2).hasType(SINGLE_WORD);
+    assertThat(r).child(0).child(2).hasText("bar");
+    assertThat(r).child(0).child(2).hasNoChildren();
+
+    assertThat(r).child(1).hasType(FIELD_NAME);
+    assertThat(r).child(1).hasText("file");
+    assertThat(r).child(1).hasChildCount(1);
+    assertThat(r).child(1).child(0).hasType(SINGLE_WORD);
+    assertThat(r).child(1).child(0).hasText("baz");
+    assertThat(r).child(1).child(0).hasNoChildren();
+  }
+
+  @Test
+  public void fieldNameAndValueWithColonFollowedByOpenParen() throws Exception {
+    Tree r = parse("project:foo:bar (file:baz)");
+    assertThat(r).hasType(AND);
+    assertThat(r).hasChildCount(2);
+
+    assertThat(r).child(0).hasType(FIELD_NAME);
+    assertThat(r).child(0).hasText("project");
+    assertThat(r).child(0).hasChildCount(3);
+    assertThat(r).child(0).child(0).hasType(SINGLE_WORD);
+    assertThat(r).child(0).child(0).hasText("foo");
+    assertThat(r).child(0).child(0).hasNoChildren();
+    assertThat(r).child(0).child(1).hasType(COLON);
+    assertThat(r).child(0).child(1).hasText(":");
+    assertThat(r).child(0).child(1).hasNoChildren();
+    assertThat(r).child(0).child(2).hasType(SINGLE_WORD);
+    assertThat(r).child(0).child(2).hasText("bar");
+    assertThat(r).child(0).child(2).hasNoChildren();
+
+    assertThat(r).child(1).hasType(FIELD_NAME);
+    assertThat(r).child(1).hasText("file");
+    assertThat(r).child(1).hasChildCount(1);
+    assertThat(r).child(1).child(0).hasType(SINGLE_WORD);
+    assertThat(r).child(1).child(0).hasText("baz");
+    assertThat(r).child(1).child(0).hasNoChildren();
+  }
+
+  @Test
+  public void fieldNameAndValueWithColonFollowedByCloseParen() throws Exception {
+    Tree r = parse("(project:foo:bar) file:baz");
+    assertThat(r).hasType(AND);
+    assertThat(r).hasChildCount(2);
+
+    assertThat(r).child(0).hasType(FIELD_NAME);
+    assertThat(r).child(0).hasText("project");
+    assertThat(r).child(0).hasChildCount(3);
+    assertThat(r).child(0).child(0).hasType(SINGLE_WORD);
+    assertThat(r).child(0).child(0).hasText("foo");
+    assertThat(r).child(0).child(0).hasNoChildren();
+    assertThat(r).child(0).child(1).hasType(COLON);
+    assertThat(r).child(0).child(1).hasText(":");
+    assertThat(r).child(0).child(1).hasNoChildren();
+    assertThat(r).child(0).child(2).hasType(SINGLE_WORD);
+    assertThat(r).child(0).child(2).hasText("bar");
+    assertThat(r).child(0).child(2).hasNoChildren();
+
+    assertThat(r).child(1).hasType(FIELD_NAME);
+    assertThat(r).child(1).hasText("file");
+    assertThat(r).child(1).hasChildCount(1);
+    assertThat(r).child(1).child(0).hasType(SINGLE_WORD);
+    assertThat(r).child(1).child(0).hasText("baz");
+    assertThat(r).child(1).child(0).hasNoChildren();
+  }
+
+  @Test
+  public void defaultFieldWithColon() throws Exception {
+    Tree r = parse("CodeReview:+2");
+    assertThat(r).hasType(DEFAULT_FIELD);
+    assertThat(r).hasChildCount(3);
+    assertThat(r).child(0).hasType(SINGLE_WORD);
+    assertThat(r).child(0).hasText("CodeReview");
+    assertThat(r).child(0).hasNoChildren();
+    assertThat(r).child(1).hasType(COLON);
+    assertThat(r).child(1).hasText(":");
+    assertThat(r).child(1).hasNoChildren();
+    assertThat(r).child(2).hasType(SINGLE_WORD);
+    assertThat(r).child(2).hasText("+2");
+    assertThat(r).child(2).hasNoChildren();
+  }
+
+  private static void assertParseFails(String query) {
+    try {
+      parse(query);
+      assert_().fail("expected parse to fail: %s", query);
+    } catch (QueryParseException e) {
+      // Expected.
+    }
   }
 }
diff --git a/javatests/com/google/gerrit/json/BUILD b/javatests/com/google/gerrit/json/BUILD
new file mode 100644
index 0000000..4894cdb
--- /dev/null
+++ b/javatests/com/google/gerrit/json/BUILD
@@ -0,0 +1,11 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+
+junit_tests(
+    name = "json_tests",
+    srcs = glob(["*.java"]),
+    deps = [
+        "//java/com/google/gerrit/json",
+        "//lib:guava",
+        "//lib/truth",
+    ],
+)
diff --git a/javatests/com/google/gerrit/json/JavaSqlTimestampHelperTest.java b/javatests/com/google/gerrit/json/JavaSqlTimestampHelperTest.java
new file mode 100644
index 0000000..a8488a9
--- /dev/null
+++ b/javatests/com/google/gerrit/json/JavaSqlTimestampHelperTest.java
@@ -0,0 +1,87 @@
+// Copyright (C) 2014 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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 static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.json.JavaSqlTimestampHelper.parseTimestamp;
+
+import java.text.SimpleDateFormat;
+import java.util.TimeZone;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class JavaSqlTimestampHelperTest {
+  private SimpleDateFormat format;
+  private TimeZone systemTimeZone;
+
+  @Before
+  public void setUp() throws Exception {
+    synchronized (TimeZone.class) {
+      systemTimeZone = TimeZone.getDefault();
+      TimeZone.setDefault(TimeZone.getTimeZone("GMT-5:00"));
+      format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS Z");
+    }
+  }
+
+  @After
+  public void resetTimeZone() {
+    TimeZone.setDefault(systemTimeZone);
+  }
+
+  @Test
+  public void parseFullTimestamp() {
+    assertThat(reformat("2006-01-02 20:04:05.789000000"))
+        .isEqualTo("2006-01-02 15:04:05.789 -0500");
+    assertThat(reformat("2006-01-02 20:04:05")).isEqualTo("2006-01-02 15:04:05.000 -0500");
+  }
+
+  @Test
+  public void parseDateOnly() {
+    assertThat(reformat("2006-01-02")).isEqualTo("2006-01-01 19:00:00.000 -0500");
+  }
+
+  @Test
+  public void parseTimeZone() {
+    assertThat(reformat("2006-01-02 15:04:05.789 -0100"))
+        .isEqualTo("2006-01-02 11:04:05.789 -0500");
+    assertThat(reformat("2006-01-02 15:04:05.789 -0000"))
+        .isEqualTo("2006-01-02 10:04:05.789 -0500");
+    assertThat(reformat("2006-01-02 15:04:05.789 +0100"))
+        .isEqualTo("2006-01-02 09:04:05.789 -0500");
+  }
+
+  @Test
+  public void parseInvalidTimestamps() {
+    assertInvalid("2006-01-02-15:04:05.789000000");
+    assertInvalid("2006-01-02T15:04:05.789000000");
+    assertInvalid("15:04:05");
+    assertInvalid("15:04:05.999000000");
+  }
+
+  private static void assertInvalid(String input) {
+    try {
+      parseTimestamp(input);
+      assert_().fail("Expected IllegalArgumentException for: " + input);
+    } catch (IllegalArgumentException e) {
+      // Expected;
+    }
+  }
+
+  private String reformat(String input) {
+    return format.format(parseTimestamp(input));
+  }
+}
diff --git a/javatests/com/google/gerrit/mail/AbstractParserTest.java b/javatests/com/google/gerrit/mail/AbstractParserTest.java
index 6ab4ca2..bcff6a7 100644
--- a/javatests/com/google/gerrit/mail/AbstractParserTest.java
+++ b/javatests/com/google/gerrit/mail/AbstractParserTest.java
@@ -18,6 +18,7 @@
 
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.testing.GerritBaseTests;
 import java.sql.Timestamp;
 import java.time.Instant;
 import java.util.ArrayList;
@@ -25,7 +26,7 @@
 import org.junit.Ignore;
 
 @Ignore
-public class AbstractParserTest {
+public class AbstractParserTest extends GerritBaseTests {
   protected static final String CHANGE_URL =
       "https://gerrit-review.googlesource.com/c/project/+/123";
 
@@ -40,7 +41,7 @@
       String message, MailComment comment, Comment inReplyTo) {
     assertThat(comment.fileName).isNull();
     assertThat(comment.message).isEqualTo(message);
-    assertThat(comment.inReplyTo).isEqualTo(inReplyTo);
+    assertThat(comment.inReplyTo.key).isEqualTo(inReplyTo.key);
     assertThat(comment.type).isEqualTo(MailComment.CommentType.INLINE_COMMENT);
   }
 
diff --git a/javatests/com/google/gerrit/mail/BUILD b/javatests/com/google/gerrit/mail/BUILD
index 2fd8f24..54671dd 100644
--- a/javatests/com/google/gerrit/mail/BUILD
+++ b/javatests/com/google/gerrit/mail/BUILD
@@ -21,10 +21,8 @@
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/project/testing:project-test-util",
         "//java/com/google/gerrit/testing:gerrit-test-util",
-        "//java/org/eclipse/jgit:server",
         "//lib:gson",
         "//lib:guava-retrying",
-        "//lib:gwtorm",
         "//lib/commons:codec",
         "//lib/guice",
         "//lib/jgit/org.eclipse.jgit:jgit",
diff --git a/javatests/com/google/gerrit/mail/MailHeaderParserTest.java b/javatests/com/google/gerrit/mail/MailHeaderParserTest.java
index 2d2c2ea..cdc8d7a 100644
--- a/javatests/com/google/gerrit/mail/MailHeaderParserTest.java
+++ b/javatests/com/google/gerrit/mail/MailHeaderParserTest.java
@@ -16,13 +16,14 @@
 
 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 {
+public class MailHeaderParserTest extends GerritBaseTests {
   @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 47a5367..ed40a57 100644
--- a/javatests/com/google/gerrit/mail/ParserUtilTest.java
+++ b/javatests/com/google/gerrit/mail/ParserUtilTest.java
@@ -16,9 +16,10 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class ParserUtilTest {
+public class ParserUtilTest extends GerritBaseTests {
   @Test
   public void trimQuotationLineOnMessageWithoutQuoatationLine() throws Exception {
     assertThat(ParserUtil.trimQuotation("One line")).isEqualTo("One line");
diff --git a/javatests/com/google/gerrit/metrics/dropwizard/BUILD b/javatests/com/google/gerrit/metrics/dropwizard/BUILD
index 98d12b2..63d4452 100644
--- a/javatests/com/google/gerrit/metrics/dropwizard/BUILD
+++ b/javatests/com/google/gerrit/metrics/dropwizard/BUILD
@@ -7,6 +7,7 @@
     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 9b21bf6..d6bcb62 100644
--- a/javatests/com/google/gerrit/metrics/dropwizard/DropWizardMetricMakerTest.java
+++ b/javatests/com/google/gerrit/metrics/dropwizard/DropWizardMetricMakerTest.java
@@ -16,9 +16,10 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class DropWizardMetricMakerTest {
+public class DropWizardMetricMakerTest extends GerritBaseTests {
   DropWizardMetricMaker metrics =
       new DropWizardMetricMaker(null /* MetricRegistry unused in tests */);
 
diff --git a/javatests/com/google/gerrit/metrics/proc/BUILD b/javatests/com/google/gerrit/metrics/proc/BUILD
index 91e5cf6..7d8af90 100644
--- a/javatests/com/google/gerrit/metrics/proc/BUILD
+++ b/javatests/com/google/gerrit/metrics/proc/BUILD
@@ -9,6 +9,7 @@
         "//java/com/google/gerrit/lifecycle",
         "//java/com/google/gerrit/metrics",
         "//java/com/google/gerrit/metrics/dropwizard",
+        "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib/dropwizard:dropwizard-core",
         "//lib/guice",
         "//lib/truth",
diff --git a/javatests/com/google/gerrit/metrics/proc/ProcMetricModuleTest.java b/javatests/com/google/gerrit/metrics/proc/ProcMetricModuleTest.java
index 91b01f6..0a5dabf 100644
--- a/javatests/com/google/gerrit/metrics/proc/ProcMetricModuleTest.java
+++ b/javatests/com/google/gerrit/metrics/proc/ProcMetricModuleTest.java
@@ -30,19 +30,16 @@
 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;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 
-public class ProcMetricModuleTest {
-  @Rule public ExpectedException exception = ExpectedException.none();
-
+public class ProcMetricModuleTest extends GerritBaseTests {
   @Inject MetricMaker metrics;
 
   @Inject MetricRegistry registry;
diff --git a/javatests/com/google/gerrit/pgm/BUILD b/javatests/com/google/gerrit/pgm/BUILD
index f01fa13..84eda60 100644
--- a/javatests/com/google/gerrit/pgm/BUILD
+++ b/javatests/com/google/gerrit/pgm/BUILD
@@ -11,6 +11,8 @@
         "//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:junit",
         "//lib/easymock",
diff --git a/javatests/com/google/gerrit/pgm/init/InitTestCase.java b/javatests/com/google/gerrit/pgm/init/InitTestCase.java
deleted file mode 100644
index 35c0937..0000000
--- a/javatests/com/google/gerrit/pgm/init/InitTestCase.java
+++ /dev/null
@@ -1,27 +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.pgm.init;
-
-import java.io.IOException;
-import java.nio.file.Path;
-import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
-import org.junit.Ignore;
-
-@Ignore
-public abstract class InitTestCase extends LocalDiskRepositoryTestCase {
-  protected Path newSitePath() throws IOException {
-    return createWorkRepository().getWorkTree().toPath().resolve("test_site");
-  }
-}
diff --git a/javatests/com/google/gerrit/pgm/init/LibrariesTest.java b/javatests/com/google/gerrit/pgm/init/LibrariesTest.java
deleted file mode 100644
index af21ad0..0000000
--- a/javatests/com/google/gerrit/pgm/init/LibrariesTest.java
+++ /dev/null
@@ -1,53 +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.pgm.init;
-
-import static org.easymock.EasyMock.createStrictMock;
-import static org.easymock.EasyMock.replay;
-import static org.easymock.EasyMock.verify;
-import static org.junit.Assert.assertNotNull;
-
-import com.google.gerrit.pgm.init.api.ConsoleUI;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.inject.Provider;
-import java.nio.file.Paths;
-import java.util.Collections;
-import org.junit.Test;
-
-public class LibrariesTest {
-  @Test
-  public void create() throws Exception {
-    final SitePaths site = new SitePaths(Paths.get("."));
-    final ConsoleUI ui = createStrictMock(ConsoleUI.class);
-    final StaleLibraryRemover remover = createStrictMock(StaleLibraryRemover.class);
-
-    replay(ui);
-
-    Libraries lib =
-        new Libraries(
-            new Provider<LibraryDownloader>() {
-              @Override
-              public LibraryDownloader get() {
-                return new LibraryDownloader(ui, site, remover);
-              }
-            },
-            Collections.<String>emptyList(),
-            false);
-
-    assertNotNull(lib.mysqlDriver);
-
-    verify(ui);
-  }
-}
diff --git a/javatests/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java b/javatests/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java
deleted file mode 100644
index 7721fca..0000000
--- a/javatests/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java
+++ /dev/null
@@ -1,153 +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.pgm.init;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-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.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-
-import com.google.common.io.ByteStreams;
-import com.google.gerrit.common.FileUtil;
-import com.google.gerrit.pgm.init.api.ConsoleUI;
-import com.google.gerrit.pgm.init.api.InitFlags;
-import com.google.gerrit.pgm.init.api.Section;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.securestore.SecureStore;
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Collections;
-import java.util.List;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.util.FS;
-import org.junit.Test;
-
-public class UpgradeFrom2_0_xTest extends InitTestCase {
-
-  @Test
-  public void upgrade() throws IOException, ConfigInvalidException {
-    final Path p = newSitePath();
-    final SitePaths site = new SitePaths(p);
-    assertTrue(site.isNew);
-    FileUtil.mkdirsOrDie(site.etc_dir, "Failed to create");
-
-    for (String n : UpgradeFrom2_0_x.etcFiles) {
-      Files.write(p.resolve(n), ("# " + n + "\n").getBytes(UTF_8));
-    }
-
-    FileBasedConfig old = new FileBasedConfig(p.resolve("gerrit.config").toFile(), FS.DETECTED);
-
-    old.setString("ldap", null, "username", "ldap.user");
-    old.setString("ldap", null, "password", "ldap.s3kr3t");
-
-    old.setString("sendemail", null, "smtpUser", "email.user");
-    old.setString("sendemail", null, "smtpPass", "email.s3kr3t");
-    old.save();
-
-    final InMemorySecureStore secureStore = new InMemorySecureStore();
-    final InitFlags flags =
-        new InitFlags(site, secureStore, Collections.<String>emptyList(), false);
-    final ConsoleUI ui = createStrictMock(ConsoleUI.class);
-    Section.Factory sections =
-        new Section.Factory() {
-          @Override
-          public Section get(String name, String subsection) {
-            return new Section(flags, site, secureStore, ui, name, subsection);
-          }
-        };
-
-    expect(ui.yesno(eq(true), eq("Upgrade '%s'"), eq(p.toAbsolutePath().normalize())))
-        .andReturn(true);
-    replay(ui);
-
-    UpgradeFrom2_0_x u = new UpgradeFrom2_0_x(site, flags, ui, sections);
-    assertTrue(u.isNeedUpgrade());
-    u.run();
-    assertFalse(u.isNeedUpgrade());
-    verify(ui);
-
-    for (String n : UpgradeFrom2_0_x.etcFiles) {
-      if ("gerrit.config".equals(n) || "secure.config".equals(n)) {
-        continue;
-      }
-      try (InputStream in = Files.newInputStream(site.etc_dir.resolve(n))) {
-        assertEquals("# " + n + "\n", new String(ByteStreams.toByteArray(in), UTF_8));
-      }
-    }
-
-    FileBasedConfig cfg = new FileBasedConfig(site.gerrit_config.toFile(), FS.DETECTED);
-    cfg.load();
-
-    assertEquals("email.user", cfg.getString("sendemail", null, "smtpUser"));
-    assertNull(cfg.getString("sendemail", null, "smtpPass"));
-    assertEquals("email.s3kr3t", secureStore.get("sendemail", null, "smtpPass"));
-
-    assertEquals("ldap.user", cfg.getString("ldap", null, "username"));
-    assertNull(cfg.getString("ldap", null, "password"));
-    assertEquals("ldap.s3kr3t", secureStore.get("ldap", null, "password"));
-
-    u.run();
-  }
-
-  private static class InMemorySecureStore extends SecureStore {
-    private final Config cfg = new Config();
-
-    @Override
-    public String[] getList(String section, String subsection, String name) {
-      return cfg.getStringList(section, subsection, name);
-    }
-
-    @Override
-    public String[] getListForPlugin(
-        String pluginName, String section, String subsection, String name) {
-      throw new UnsupportedOperationException("not used by tests");
-    }
-
-    @Override
-    public void setList(String section, String subsection, String name, List<String> values) {
-      cfg.setStringList(section, subsection, name, values);
-    }
-
-    @Override
-    public void unset(String section, String subsection, String name) {
-      cfg.unset(section, subsection, name);
-    }
-
-    @Override
-    public Iterable<EntryKey> list() {
-      throw new UnsupportedOperationException("not used by tests");
-    }
-
-    @Override
-    public boolean isOutdated() {
-      throw new UnsupportedOperationException("not used by tests");
-    }
-
-    @Override
-    public void reload() {
-      throw new UnsupportedOperationException("not used by tests");
-    }
-  }
-}
diff --git a/javatests/com/google/gerrit/pgm/init/api/AllProjectsConfigTest.java b/javatests/com/google/gerrit/pgm/init/api/AllProjectsConfigTest.java
new file mode 100644
index 0000000..a34007e
--- /dev/null
+++ b/javatests/com/google/gerrit/pgm/init/api/AllProjectsConfigTest.java
@@ -0,0 +1,114 @@
+// 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.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;
+import com.google.gerrit.server.securestore.testing.InMemorySecureStore;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+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.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class AllProjectsConfigTest {
+  private static final String ALL_PROJECTS = "All-The-Projects";
+
+  @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+  private SitePaths sitePaths;
+  private AllProjectsConfig allProjectsConfig;
+  private File allProjectsRepoFile;
+
+  @Before
+  public void setUp() throws Exception {
+    sitePaths = new SitePaths(temporaryFolder.newFolder().toPath());
+    Files.createDirectories(sitePaths.etc_dir);
+
+    Path gitPath = sitePaths.resolve("git");
+
+    StoredConfig gerritConfig =
+        new FileBasedConfig(
+            sitePaths.resolve("etc").resolve("gerrit.config").toFile(), FS.DETECTED);
+    gerritConfig.load();
+    gerritConfig.setString("gerrit", null, "basePath", gitPath.toAbsolutePath().toString());
+    gerritConfig.setString("gerrit", null, "allProjects", ALL_PROJECTS);
+    gerritConfig.save();
+
+    Files.createDirectories(sitePaths.resolve("git"));
+    allProjectsRepoFile = gitPath.resolve("All-The-Projects.git").toFile();
+    try (Repository repo = new FileRepository(allProjectsRepoFile)) {
+      repo.create(true);
+    }
+
+    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 =
+        new AllProjectsConfig(new AllProjectsNameOnInitProvider(sections), sitePaths, flags);
+  }
+
+  @Test
+  public void noBaseConfig() throws Exception {
+    assertThat(getConfig().getString("foo", null, "bar")).isNull();
+
+    try (Repository repo = new FileRepository(allProjectsRepoFile);
+        TestRepository<Repository> tr = new TestRepository<>(repo)) {
+      tr.branch("refs/meta/config").commit().add("project.config", "[foo]\nbar = baz").create();
+    }
+
+    assertThat(getConfig().getString("foo", null, "bar")).isEqualTo("baz");
+  }
+
+  @Test
+  public void baseConfig() throws Exception {
+    assertThat(getConfig().getString("foo", null, "bar")).isNull();
+
+    Path baseConfigPath = sitePaths.etc_dir.resolve(ALL_PROJECTS).resolve("project.config");
+    Files.createDirectories(baseConfigPath.getParent());
+    Files.write(baseConfigPath, ImmutableList.of("[foo]", "bar = base"));
+
+    assertThat(getConfig().getString("foo", null, "bar")).isEqualTo("base");
+
+    try (Repository repo = new FileRepository(allProjectsRepoFile);
+        TestRepository<Repository> tr = new TestRepository<>(repo)) {
+      tr.branch("refs/meta/config").commit().add("project.config", "[foo]\nbar = baz").create();
+    }
+
+    assertThat(getConfig().getString("foo", null, "bar")).isEqualTo("baz");
+  }
+
+  private Config getConfig() throws IOException, ConfigInvalidException {
+    return allProjectsConfig.load().getConfig();
+  }
+}
diff --git a/javatests/com/google/gerrit/proto/BUILD b/javatests/com/google/gerrit/proto/BUILD
index 0940f6b..6b98b72 100644
--- a/javatests/com/google/gerrit/proto/BUILD
+++ b/javatests/com/google/gerrit/proto/BUILD
@@ -4,14 +4,11 @@
     name = "proto_tests",
     srcs = glob(["*.java"]),
     deps = [
-        "//lib/truth:truth-proto-extension",
-        "//proto:reviewdb_java_proto",
-
-        # TODO(dborowitz): These are already runtime_deps of
-        # truth-proto-extension, but either omitting them or adding them as
-        # runtime_deps to this target fails with:
-        #   class file for com.google.common.collect.Multimap not found
-        "//lib:guava",
+        "//java/com/google/gerrit/proto",
+        "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//lib:protobuf",
         "//lib/truth",
+        "//lib/truth:truth-proto-extension",
+        "//proto:cache_java_proto",
     ],
 )
diff --git a/javatests/com/google/gerrit/proto/ProtosTest.java b/javatests/com/google/gerrit/proto/ProtosTest.java
new file mode 100644
index 0000000..29e8fe0
--- /dev/null
+++ b/javatests/com/google/gerrit/proto/ProtosTest.java
@@ -0,0 +1,156 @@
+// 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.proto;
+
+import static com.google.common.truth.Truth.assert_;
+import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+
+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 {
+  @Test
+  public void parseUncheckedByteArrayWrongProtoType() {
+    ChangeNotesKeyProto proto =
+        ChangeNotesKeyProto.newBuilder()
+            .setProject("project")
+            .setChangeId(1234)
+            .setId(ByteString.copyFromUtf8("foo"))
+            .build();
+    byte[] bytes = Protos.toByteArray(proto);
+    try {
+      Protos.parseUnchecked(ChangeNotesStateProto.parser(), bytes);
+      assert_().fail("expected IllegalArgumentException");
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
+  }
+
+  @Test
+  public void parseUncheckedByteArrayInvalidData() {
+    byte[] bytes = new byte[] {0x00};
+    try {
+      Protos.parseUnchecked(ChangeNotesStateProto.parser(), bytes);
+      assert_().fail("expected IllegalArgumentException");
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
+  }
+
+  @Test
+  public void parseUncheckedByteArray() {
+    ChangeNotesKeyProto proto =
+        ChangeNotesKeyProto.newBuilder()
+            .setProject("project")
+            .setChangeId(1234)
+            .setId(ByteString.copyFromUtf8("foo"))
+            .build();
+    byte[] bytes = Protos.toByteArray(proto);
+    assertThat(Protos.parseUnchecked(ChangeNotesKeyProto.parser(), bytes)).isEqualTo(proto);
+  }
+
+  @Test
+  public void parseUncheckedSegmentOfByteArrayWrongProtoType() {
+    ChangeNotesKeyProto proto =
+        ChangeNotesKeyProto.newBuilder()
+            .setProject("project")
+            .setChangeId(1234)
+            .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.
+    }
+  }
+
+  @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.
+    }
+  }
+
+  @Test
+  public void parseUncheckedSegmentOfByteArray() {
+    ChangeNotesKeyProto proto =
+        ChangeNotesKeyProto.newBuilder()
+            .setProject("project")
+            .setChangeId(1234)
+            .setId(ByteString.copyFromUtf8("foo"))
+            .build();
+    byte[] protoBytes = Protos.toByteArray(proto);
+    int offset = 3;
+    int length = protoBytes.length;
+    byte[] bytes = new byte[length + 20];
+    Arrays.fill(bytes, (byte) 1);
+    System.arraycopy(protoBytes, 0, bytes, offset, length);
+
+    ChangeNotesKeyProto parsedProto =
+        Protos.parseUnchecked(ChangeNotesKeyProto.parser(), bytes, offset, length);
+
+    assertThat(parsedProto).isEqualTo(proto);
+  }
+
+  @Test
+  public void parseUncheckedByteStringWrongProtoType() {
+    ChangeNotesKeyProto proto =
+        ChangeNotesKeyProto.newBuilder()
+            .setProject("project")
+            .setChangeId(1234)
+            .setId(ByteString.copyFromUtf8("foo"))
+            .build();
+    ByteString byteString = Protos.toByteString(proto);
+    try {
+      Protos.parseUnchecked(ChangeNotesStateProto.parser(), byteString);
+      assert_().fail("expected IllegalArgumentException");
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
+  }
+
+  @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.
+    }
+  }
+
+  @Test
+  public void parseUncheckedByteString() {
+    ChangeNotesKeyProto proto =
+        ChangeNotesKeyProto.newBuilder()
+            .setProject("project")
+            .setChangeId(1234)
+            .setId(ByteString.copyFromUtf8("foo"))
+            .build();
+    ByteString byteString = Protos.toByteString(proto);
+    assertThat(Protos.parseUnchecked(ChangeNotesKeyProto.parser(), byteString)).isEqualTo(proto);
+  }
+}
diff --git a/javatests/com/google/gerrit/proto/ReviewDbProtoTest.java b/javatests/com/google/gerrit/proto/ReviewDbProtoTest.java
deleted file mode 100644
index 49cd2921..0000000
--- a/javatests/com/google/gerrit/proto/ReviewDbProtoTest.java
+++ /dev/null
@@ -1,31 +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.proto;
-
-import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
-
-import com.google.gerrit.proto.reviewdb.Reviewdb.Change;
-import com.google.gerrit.proto.reviewdb.Reviewdb.Change_Id;
-import org.junit.Test;
-
-public class ReviewDbProtoTest {
-  @Test
-  public void generatedProtoApi() {
-    Change c1 = Change.newBuilder().setChangeId(Change_Id.newBuilder().setId(1234).build()).build();
-    Change c2 = Change.newBuilder().setChangeId(Change_Id.newBuilder().setId(5678).build()).build();
-    assertThat(c1).isEqualTo(c1);
-    assertThat(c1).isNotEqualTo(c2);
-  }
-}
diff --git a/javatests/com/google/gerrit/reviewdb/BUILD b/javatests/com/google/gerrit/reviewdb/BUILD
deleted file mode 100644
index 0fd140e..0000000
--- a/javatests/com/google/gerrit/reviewdb/BUILD
+++ /dev/null
@@ -1,14 +0,0 @@
-load("//tools/bzl:junit.bzl", "junit_tests")
-
-junit_tests(
-    name = "client_tests",
-    srcs = glob(["client/**/*.java"]),
-    deps = [
-        "//java/com/google/gerrit/reviewdb:client",
-        "//java/com/google/gerrit/server/project/testing:project-test-util",
-        "//java/com/google/gerrit/testing:gerrit-test-util",
-        "//lib:guava",
-        "//lib:gwtorm",
-        "//lib/truth",
-    ],
-)
diff --git a/javatests/com/google/gerrit/reviewdb/client/BUILD b/javatests/com/google/gerrit/reviewdb/client/BUILD
new file mode 100644
index 0000000..bc993d5
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/client/BUILD
@@ -0,0 +1,13 @@
+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/AccountIdProtoConverterTest.java b/javatests/com/google/gerrit/reviewdb/converter/AccountIdProtoConverterTest.java
new file mode 100644
index 0000000..123a973
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/converter/AccountIdProtoConverterTest.java
@@ -0,0 +1,67 @@
+// 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.Account;
+import com.google.protobuf.Parser;
+import org.junit.Test;
+
+public class AccountIdProtoConverterTest {
+  private final AccountIdProtoConverter accountIdProtoConverter = AccountIdProtoConverter.INSTANCE;
+
+  @Test
+  public void allValuesConvertedToProto() {
+    Account.Id accountId = new Account.Id(24);
+
+    Entities.Account_Id proto = accountIdProtoConverter.toProto(accountId);
+
+    Entities.Account_Id expectedProto = Entities.Account_Id.newBuilder().setId(24).build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  @Test
+  public void allValuesConvertedToProtoAndBackAgain() {
+    Account.Id accountId = new Account.Id(34832);
+
+    Account.Id convertedAccountId =
+        accountIdProtoConverter.fromProto(accountIdProtoConverter.toProto(accountId));
+
+    assertThat(convertedAccountId).isEqualTo(accountId);
+  }
+
+  @Test
+  public void protoCanBeParsedFromBytes() throws Exception {
+    Entities.Account_Id proto = Entities.Account_Id.newBuilder().setId(24).build();
+    byte[] bytes = proto.toByteArray();
+
+    Parser<Entities.Account_Id> parser = accountIdProtoConverter.getParser();
+    Entities.Account_Id 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(Account.Id.class).hasFields(ImmutableMap.of("id", int.class));
+  }
+}
diff --git a/javatests/com/google/gerrit/reviewdb/converter/BUILD b/javatests/com/google/gerrit/reviewdb/converter/BUILD
new file mode 100644
index 0000000..9cc941c
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/converter/BUILD
@@ -0,0 +1,15 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+
+junit_tests(
+    name = "proto_converter_tests",
+    srcs = glob(["*.java"]),
+    deps = [
+        "//java/com/google/gerrit/proto/testing",
+        "//java/com/google/gerrit/reviewdb:server",
+        "//lib:guava",
+        "//lib:protobuf",
+        "//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/reviewdb/converter/BranchNameKeyProtoConverterTest.java
new file mode 100644
index 0000000..412641f
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/converter/BranchNameKeyProtoConverterTest.java
@@ -0,0 +1,83 @@
+// 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.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.protobuf.Parser;
+import java.lang.reflect.Type;
+import org.junit.Test;
+
+public class BranchNameKeyProtoConverterTest {
+  private final BranchNameKeyProtoConverter branchNameKeyProtoConverter =
+      BranchNameKeyProtoConverter.INSTANCE;
+
+  @Test
+  public void allValuesConvertedToProto() {
+    Branch.NameKey nameKey = new Branch.NameKey(new 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")
+            .build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  @Test
+  public void allValuesConvertedToProtoAndBackAgain() {
+    Branch.NameKey nameKey = new Branch.NameKey(new Project.NameKey("project-52"), "branch 14");
+
+    Branch.NameKey convertedNameKey =
+        branchNameKeyProtoConverter.fromProto(branchNameKeyProtoConverter.toProto(nameKey));
+
+    assertThat(convertedNameKey).isEqualTo(nameKey);
+  }
+
+  @Test
+  public void protoCanBeParsedFromBytes() throws Exception {
+    Entities.Branch_NameKey proto =
+        Entities.Branch_NameKey.newBuilder()
+            .setProjectName(Entities.Project_NameKey.newBuilder().setName("project 1"))
+            .setBranchName("branch 36")
+            .build();
+    byte[] bytes = proto.toByteArray();
+
+    Parser<Entities.Branch_NameKey> parser = branchNameKeyProtoConverter.getParser();
+    Entities.Branch_NameKey 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(Branch.NameKey.class)
+        .hasFields(
+            ImmutableMap.<String, Type>builder()
+                .put("projectName", Project.NameKey.class)
+                .put("branchName", String.class)
+                .build());
+  }
+}
diff --git a/javatests/com/google/gerrit/reviewdb/converter/ChangeIdProtoConverterTest.java b/javatests/com/google/gerrit/reviewdb/converter/ChangeIdProtoConverterTest.java
new file mode 100644
index 0000000..d5ebb51
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/converter/ChangeIdProtoConverterTest.java
@@ -0,0 +1,67 @@
+// 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.Change;
+import com.google.protobuf.Parser;
+import org.junit.Test;
+
+public class ChangeIdProtoConverterTest {
+  private final ChangeIdProtoConverter changeIdProtoConverter = ChangeIdProtoConverter.INSTANCE;
+
+  @Test
+  public void allValuesConvertedToProto() {
+    Change.Id changeId = new Change.Id(94);
+
+    Entities.Change_Id proto = changeIdProtoConverter.toProto(changeId);
+
+    Entities.Change_Id expectedProto = Entities.Change_Id.newBuilder().setId(94).build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  @Test
+  public void allValuesConvertedToProtoAndBackAgain() {
+    Change.Id changeId = new Change.Id(2903482);
+
+    Change.Id convertedChangeId =
+        changeIdProtoConverter.fromProto(changeIdProtoConverter.toProto(changeId));
+
+    assertThat(convertedChangeId).isEqualTo(changeId);
+  }
+
+  @Test
+  public void protoCanBeParsedFromBytes() throws Exception {
+    Entities.Change_Id proto = Entities.Change_Id.newBuilder().setId(94).build();
+    byte[] bytes = proto.toByteArray();
+
+    Parser<Entities.Change_Id> parser = changeIdProtoConverter.getParser();
+    Entities.Change_Id 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(Change.Id.class).hasFields(ImmutableMap.of("id", int.class));
+  }
+}
diff --git a/javatests/com/google/gerrit/reviewdb/converter/ChangeKeyProtoConverterTest.java b/javatests/com/google/gerrit/reviewdb/converter/ChangeKeyProtoConverterTest.java
new file mode 100644
index 0000000..d948706
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/converter/ChangeKeyProtoConverterTest.java
@@ -0,0 +1,67 @@
+// 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.Change;
+import com.google.protobuf.Parser;
+import org.junit.Test;
+
+public class ChangeKeyProtoConverterTest {
+  private final ChangeKeyProtoConverter changeKeyProtoConverter = ChangeKeyProtoConverter.INSTANCE;
+
+  @Test
+  public void allValuesConvertedToProto() {
+    Change.Key changeKey = new Change.Key("change-1");
+
+    Entities.Change_Key proto = changeKeyProtoConverter.toProto(changeKey);
+
+    Entities.Change_Key expectedProto = Entities.Change_Key.newBuilder().setId("change-1").build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  @Test
+  public void allValuesConvertedToProtoAndBackAgain() {
+    Change.Key changeKey = new Change.Key("change-52");
+
+    Change.Key convertedChangeKey =
+        changeKeyProtoConverter.fromProto(changeKeyProtoConverter.toProto(changeKey));
+
+    assertThat(convertedChangeKey).isEqualTo(changeKey);
+  }
+
+  @Test
+  public void protoCanBeParsedFromBytes() throws Exception {
+    Entities.Change_Key proto = Entities.Change_Key.newBuilder().setId("change 36").build();
+    byte[] bytes = proto.toByteArray();
+
+    Parser<Entities.Change_Key> parser = changeKeyProtoConverter.getParser();
+    Entities.Change_Key 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(Change.Key.class).hasFields(ImmutableMap.of("id", String.class));
+  }
+}
diff --git a/javatests/com/google/gerrit/reviewdb/converter/ChangeMessageKeyProtoConverterTest.java b/javatests/com/google/gerrit/reviewdb/converter/ChangeMessageKeyProtoConverterTest.java
new file mode 100644
index 0000000..c8bb2ed
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/converter/ChangeMessageKeyProtoConverterTest.java
@@ -0,0 +1,83 @@
+// 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.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.protobuf.Parser;
+import java.lang.reflect.Type;
+import org.junit.Test;
+
+public class ChangeMessageKeyProtoConverterTest {
+  private final ChangeMessageKeyProtoConverter messageKeyProtoConverter =
+      ChangeMessageKeyProtoConverter.INSTANCE;
+
+  @Test
+  public void allValuesConvertedToProto() {
+    ChangeMessage.Key messageKey = new ChangeMessage.Key(new Change.Id(704), "aabbcc");
+
+    Entities.ChangeMessage_Key proto = messageKeyProtoConverter.toProto(messageKey);
+
+    Entities.ChangeMessage_Key expectedProto =
+        Entities.ChangeMessage_Key.newBuilder()
+            .setChangeId(Entities.Change_Id.newBuilder().setId(704))
+            .setUuid("aabbcc")
+            .build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  @Test
+  public void allValuesConvertedToProtoAndBackAgain() {
+    ChangeMessage.Key messageKey = new ChangeMessage.Key(new Change.Id(704), "aabbcc");
+
+    ChangeMessage.Key convertedMessageKey =
+        messageKeyProtoConverter.fromProto(messageKeyProtoConverter.toProto(messageKey));
+
+    assertThat(convertedMessageKey).isEqualTo(messageKey);
+  }
+
+  @Test
+  public void protoCanBeParsedFromBytes() throws Exception {
+    Entities.ChangeMessage_Key proto =
+        Entities.ChangeMessage_Key.newBuilder()
+            .setChangeId(Entities.Change_Id.newBuilder().setId(704))
+            .setUuid("aabbcc")
+            .build();
+    byte[] bytes = proto.toByteArray();
+
+    Parser<Entities.ChangeMessage_Key> parser = messageKeyProtoConverter.getParser();
+    Entities.ChangeMessage_Key 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(ChangeMessage.Key.class)
+        .hasFields(
+            ImmutableMap.<String, Type>builder()
+                .put("changeId", Change.Id.class)
+                .put("uuid", String.class)
+                .build());
+  }
+}
diff --git a/javatests/com/google/gerrit/reviewdb/converter/ChangeMessageProtoConverterTest.java b/javatests/com/google/gerrit/reviewdb/converter/ChangeMessageProtoConverterTest.java
new file mode 100644
index 0000000..65bdfbb
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/converter/ChangeMessageProtoConverterTest.java
@@ -0,0 +1,214 @@
+// 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.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;
+import org.junit.Test;
+
+public class ChangeMessageProtoConverterTest {
+  private final ChangeMessageProtoConverter changeMessageProtoConverter =
+      ChangeMessageProtoConverter.INSTANCE;
+
+  @Test
+  public void allValuesConvertedToProto() {
+    ChangeMessage changeMessage =
+        new ChangeMessage(
+            new ChangeMessage.Key(new Change.Id(543), "change-message-21"),
+            new Account.Id(63),
+            new Timestamp(9876543),
+            new PatchSet.Id(new Change.Id(34), 13));
+    changeMessage.setMessage("This is a change message.");
+    changeMessage.setTag("An arbitrary tag.");
+    changeMessage.setRealAuthor(new Account.Id(10003));
+
+    Entities.ChangeMessage proto = changeMessageProtoConverter.toProto(changeMessage);
+
+    Entities.ChangeMessage expectedProto =
+        Entities.ChangeMessage.newBuilder()
+            .setKey(
+                Entities.ChangeMessage_Key.newBuilder()
+                    .setChangeId(Entities.Change_Id.newBuilder().setId(543))
+                    .setUuid("change-message-21"))
+            .setAuthorId(Entities.Account_Id.newBuilder().setId(63))
+            .setWrittenOn(9876543)
+            .setMessage("This is a change message.")
+            .setPatchset(
+                Entities.PatchSet_Id.newBuilder()
+                    .setChangeId(Entities.Change_Id.newBuilder().setId(34))
+                    .setPatchSetId(13))
+            .setTag("An arbitrary tag.")
+            .setRealAuthor(Entities.Account_Id.newBuilder().setId(10003))
+            .build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  @Test
+  public void mainValuesConvertedToProto() {
+    ChangeMessage changeMessage =
+        new ChangeMessage(
+            new ChangeMessage.Key(new Change.Id(543), "change-message-21"),
+            new Account.Id(63),
+            new Timestamp(9876543),
+            new PatchSet.Id(new Change.Id(34), 13));
+
+    Entities.ChangeMessage proto = changeMessageProtoConverter.toProto(changeMessage);
+
+    Entities.ChangeMessage expectedProto =
+        Entities.ChangeMessage.newBuilder()
+            .setKey(
+                Entities.ChangeMessage_Key.newBuilder()
+                    .setChangeId(Entities.Change_Id.newBuilder().setId(543))
+                    .setUuid("change-message-21"))
+            .setAuthorId(Entities.Account_Id.newBuilder().setId(63))
+            .setWrittenOn(9876543)
+            .setPatchset(
+                Entities.PatchSet_Id.newBuilder()
+                    .setChangeId(Entities.Change_Id.newBuilder().setId(34))
+                    .setPatchSetId(13))
+            .build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  // This test documents a special behavior which is necessary to ensure binary compatibility.
+  @Test
+  public void realAuthorIsNotAutomaticallySetToAuthorWhenConvertedToProto() {
+    ChangeMessage changeMessage =
+        new ChangeMessage(
+            new ChangeMessage.Key(new Change.Id(543), "change-message-21"),
+            new Account.Id(63),
+            null,
+            null);
+
+    Entities.ChangeMessage proto = changeMessageProtoConverter.toProto(changeMessage);
+
+    Entities.ChangeMessage expectedProto =
+        Entities.ChangeMessage.newBuilder()
+            .setKey(
+                Entities.ChangeMessage_Key.newBuilder()
+                    .setChangeId(Entities.Change_Id.newBuilder().setId(543))
+                    .setUuid("change-message-21"))
+            .setAuthorId(Entities.Account_Id.newBuilder().setId(63))
+            .build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  @Test
+  public void mandatoryValuesConvertedToProto() {
+    // 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);
+
+    Entities.ChangeMessage proto = changeMessageProtoConverter.toProto(changeMessage);
+
+    Entities.ChangeMessage expectedProto =
+        Entities.ChangeMessage.newBuilder()
+            .setKey(
+                Entities.ChangeMessage_Key.newBuilder()
+                    .setChangeId(Entities.Change_Id.newBuilder().setId(543))
+                    .setUuid("change-message-21"))
+            .build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  @Test
+  public void allValuesConvertedToProtoAndBackAgain() {
+    ChangeMessage changeMessage =
+        new ChangeMessage(
+            new ChangeMessage.Key(new Change.Id(543), "change-message-21"),
+            new Account.Id(63),
+            new Timestamp(9876543),
+            new PatchSet.Id(new Change.Id(34), 13));
+    changeMessage.setMessage("This is a change message.");
+    changeMessage.setTag("An arbitrary tag.");
+    changeMessage.setRealAuthor(new Account.Id(10003));
+
+    ChangeMessage convertedChangeMessage =
+        changeMessageProtoConverter.fromProto(changeMessageProtoConverter.toProto(changeMessage));
+    assertThat(convertedChangeMessage).isEqualTo(changeMessage);
+  }
+
+  @Test
+  public void mainValuesConvertedToProtoAndBackAgain() {
+    ChangeMessage changeMessage =
+        new ChangeMessage(
+            new ChangeMessage.Key(new Change.Id(543), "change-message-21"),
+            new Account.Id(63),
+            new Timestamp(9876543),
+            new PatchSet.Id(new Change.Id(34), 13));
+
+    ChangeMessage convertedChangeMessage =
+        changeMessageProtoConverter.fromProto(changeMessageProtoConverter.toProto(changeMessage));
+    assertThat(convertedChangeMessage).isEqualTo(changeMessage);
+  }
+
+  @Test
+  public void mandatoryValuesConvertedToProtoAndBackAgain() {
+    ChangeMessage changeMessage =
+        new ChangeMessage(
+            new ChangeMessage.Key(new Change.Id(543), "change-message-21"), null, null, null);
+
+    ChangeMessage convertedChangeMessage =
+        changeMessageProtoConverter.fromProto(changeMessageProtoConverter.toProto(changeMessage));
+    assertThat(convertedChangeMessage).isEqualTo(changeMessage);
+  }
+
+  @Test
+  public void protoCanBeParsedFromBytes() throws Exception {
+    Entities.ChangeMessage proto =
+        Entities.ChangeMessage.newBuilder()
+            .setKey(
+                Entities.ChangeMessage_Key.newBuilder()
+                    .setChangeId(Entities.Change_Id.newBuilder().setId(543))
+                    .setUuid("change-message-21"))
+            .build();
+    byte[] bytes = proto.toByteArray();
+
+    Parser<Entities.ChangeMessage> parser = changeMessageProtoConverter.getParser();
+    Entities.ChangeMessage 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(ChangeMessage.class)
+        .hasFields(
+            ImmutableMap.<String, Type>builder()
+                .put("key", ChangeMessage.Key.class)
+                .put("author", Account.Id.class)
+                .put("writtenOn", Timestamp.class)
+                .put("message", String.class)
+                .put("patchset", PatchSet.Id.class)
+                .put("tag", String.class)
+                .put("realAuthor", Account.Id.class)
+                .build());
+  }
+}
diff --git a/javatests/com/google/gerrit/reviewdb/converter/ChangeProtoConverterTest.java b/javatests/com/google/gerrit/reviewdb/converter/ChangeProtoConverterTest.java
new file mode 100644
index 0000000..61bf105
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/converter/ChangeProtoConverterTest.java
@@ -0,0 +1,363 @@
+// 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.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;
+import org.junit.Test;
+
+public class ChangeProtoConverterTest {
+  private final ChangeProtoConverter changeProtoConverter = ChangeProtoConverter.INSTANCE;
+
+  @Test
+  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"),
+            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");
+    change.setTopic("my topic");
+    change.setSubmissionId("submission ID 234");
+    change.setAssignee(new Account.Id(100001));
+    change.setPrivate(true);
+    change.setWorkInProgress(true);
+    change.setReviewStarted(true);
+    change.setRevertOf(new Change.Id(180));
+
+    Entities.Change proto = changeProtoConverter.toProto(change);
+
+    Entities.Change expectedProto =
+        Entities.Change.newBuilder()
+            .setChangeId(Entities.Change_Id.newBuilder().setId(14))
+            .setChangeKey(Entities.Change_Key.newBuilder().setId("change 1"))
+            .setRowVersion(0)
+            .setCreatedOn(987654L)
+            .setLastUpdatedOn(1234567L)
+            .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"))
+            .setStatus(Change.STATUS_MERGED)
+            .setCurrentPatchSetId(23)
+            .setSubject("subject XYZ")
+            .setTopic("my topic")
+            .setOriginalSubject("original subject ABC")
+            .setSubmissionId("submission ID 234")
+            .setAssignee(Entities.Account_Id.newBuilder().setId(100001))
+            .setIsPrivate(true)
+            .setWorkInProgress(true)
+            .setReviewStarted(true)
+            .setRevertOf(Entities.Change_Id.newBuilder().setId(180))
+            .build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  @Test
+  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"),
+            new Timestamp(987654L));
+
+    Entities.Change proto = changeProtoConverter.toProto(change);
+
+    Entities.Change expectedProto =
+        Entities.Change.newBuilder()
+            .setChangeId(Entities.Change_Id.newBuilder().setId(14))
+            .setChangeKey(Entities.Change_Key.newBuilder().setId("change 1"))
+            .setCreatedOn(987654L)
+            // Defaults to createdOn if not set.
+            .setLastUpdatedOn(987654L)
+            .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"))
+            // Default values which can't be unset.
+            .setCurrentPatchSetId(0)
+            .setRowVersion(0)
+            .setStatus(Change.STATUS_NEW)
+            .setIsPrivate(false)
+            .setWorkInProgress(false)
+            .setReviewStarted(false)
+            .build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  // This test documents a special behavior which is necessary to ensure binary compatibility.
+  @Test
+  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"),
+            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);
+
+    Entities.Change proto = changeProtoConverter.toProto(change);
+
+    Entities.Change expectedProto =
+        Entities.Change.newBuilder()
+            .setChangeId(Entities.Change_Id.newBuilder().setId(14))
+            .setChangeKey(Entities.Change_Key.newBuilder().setId("change 1"))
+            .setCreatedOn(987654L)
+            // Defaults to createdOn if not set.
+            .setLastUpdatedOn(987654L)
+            .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"))
+            .setCurrentPatchSetId(0)
+            // Default values which can't be unset.
+            .setRowVersion(0)
+            .setStatus(Change.STATUS_NEW)
+            .setIsPrivate(false)
+            .setWorkInProgress(false)
+            .setReviewStarted(false)
+            .build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  // This test documents a special behavior which is necessary to ensure binary compatibility.
+  @Test
+  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"),
+            new Timestamp(987654L));
+    change.setCurrentPatchSet(new PatchSet.Id(new Change.Id(14), 23), "subject ABC", null);
+
+    Entities.Change proto = changeProtoConverter.toProto(change);
+
+    Entities.Change expectedProto =
+        Entities.Change.newBuilder()
+            .setChangeId(Entities.Change_Id.newBuilder().setId(14))
+            .setChangeKey(Entities.Change_Key.newBuilder().setId("change 1"))
+            .setCreatedOn(987654L)
+            // Defaults to createdOn if not set.
+            .setLastUpdatedOn(987654L)
+            .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"))
+            .setCurrentPatchSetId(23)
+            .setSubject("subject ABC")
+            // Default values which can't be unset.
+            .setRowVersion(0)
+            .setStatus(Change.STATUS_NEW)
+            .setIsPrivate(false)
+            .setWorkInProgress(false)
+            .setReviewStarted(false)
+            .build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  @Test
+  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"),
+            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");
+    change.setTopic("my topic");
+    change.setSubmissionId("submission ID 234");
+    change.setAssignee(new Account.Id(100001));
+    change.setPrivate(true);
+    change.setWorkInProgress(true);
+    change.setReviewStarted(true);
+    change.setRevertOf(new Change.Id(180));
+
+    Change convertedChange = changeProtoConverter.fromProto(changeProtoConverter.toProto(change));
+    assertEqualChange(convertedChange, change);
+  }
+
+  @Test
+  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"),
+            new Timestamp(987654L));
+
+    Change convertedChange = changeProtoConverter.fromProto(changeProtoConverter.toProto(change));
+    assertEqualChange(convertedChange, change);
+  }
+
+  // We need this special test as some values are only optional in the protobuf definition but can
+  // never be unset in our entity object.
+  @Test
+  public void protoWithOnlyRequiredValuesCanBeConvertedBack() {
+    Entities.Change proto =
+        Entities.Change.newBuilder().setChangeId(Entities.Change_Id.newBuilder().setId(14)).build();
+    Change change = changeProtoConverter.fromProto(proto);
+
+    assertThat(change.getChangeId()).isEqualTo(14);
+    // Values which can't be null according to ReviewDb's column definition but which are optional.
+    assertThat(change.getKey()).isNull();
+    assertThat(change.getOwner()).isNull();
+    assertThat(change.getDest()).isNull();
+    assertThat(change.getCreatedOn()).isEqualTo(new Timestamp(0));
+    assertThat(change.getLastUpdatedOn()).isEqualTo(new Timestamp(0));
+    assertThat(change.getSubject()).isNull();
+    assertThat(change.currentPatchSetId()).isNull();
+    // Default values for unset protobuf fields which can't be unset in the entity object.
+    assertThat(change.getRowVersion()).isEqualTo(0);
+    assertThat(change.isNew()).isTrue();
+    assertThat(change.isPrivate()).isFalse();
+    assertThat(change.isWorkInProgress()).isFalse();
+    assertThat(change.hasReviewStarted()).isFalse();
+  }
+
+  @Test
+  public void unsetLastUpdatedOnIsAutomaticallySetToCreatedOnWhenConvertedBack() {
+    Entities.Change proto =
+        Entities.Change.newBuilder()
+            .setChangeId(Entities.Change_Id.newBuilder().setId(14))
+            .setChangeKey(Entities.Change_Key.newBuilder().setId("change 1"))
+            .setCreatedOn(987654L)
+            .setOwnerAccountId(Entities.Account_Id.newBuilder().setId(35))
+            .setDest(
+                Entities.Branch_NameKey.newBuilder()
+                    .setProjectName(Entities.Project_NameKey.newBuilder().setName("project 67"))
+                    .setBranchName("branch 74"))
+            .build();
+    Change change = changeProtoConverter.fromProto(proto);
+
+    assertThat(change.getLastUpdatedOn()).isEqualTo(new Timestamp(987654L));
+  }
+
+  @Test
+  public void protoCanBeParsedFromBytes() throws Exception {
+    Entities.Change proto =
+        Entities.Change.newBuilder()
+            .setChangeId(Entities.Change_Id.newBuilder().setId(14))
+            .setChangeKey(Entities.Change_Key.newBuilder().setId("change 1"))
+            .setRowVersion(0)
+            .setCreatedOn(987654L)
+            .setLastUpdatedOn(1234567L)
+            .setOwnerAccountId(Entities.Account_Id.newBuilder().setId(35))
+            .setDest(
+                Entities.Branch_NameKey.newBuilder()
+                    .setProjectName(Entities.Project_NameKey.newBuilder().setName("project 67"))
+                    .setBranchName("branch 74"))
+            .setStatus(Change.STATUS_MERGED)
+            .setCurrentPatchSetId(23)
+            .setSubject("subject XYZ")
+            .setTopic("my topic")
+            .setOriginalSubject("original subject ABC")
+            .setSubmissionId("submission ID 234")
+            .setAssignee(Entities.Account_Id.newBuilder().setId(100001))
+            .setIsPrivate(true)
+            .setWorkInProgress(true)
+            .setReviewStarted(true)
+            .setRevertOf(Entities.Change_Id.newBuilder().setId(180))
+            .build();
+    byte[] bytes = proto.toByteArray();
+
+    Parser<Entities.Change> parser = changeProtoConverter.getParser();
+    Entities.Change 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(Change.class)
+        .hasFields(
+            ImmutableMap.<String, Type>builder()
+                .put("changeId", Change.Id.class)
+                .put("changeKey", Change.Key.class)
+                .put("rowVersion", int.class)
+                .put("createdOn", Timestamp.class)
+                .put("lastUpdatedOn", Timestamp.class)
+                .put("owner", Account.Id.class)
+                .put("dest", Branch.NameKey.class)
+                .put("status", char.class)
+                .put("currentPatchSetId", int.class)
+                .put("subject", String.class)
+                .put("topic", String.class)
+                .put("originalSubject", String.class)
+                .put("submissionId", String.class)
+                .put("assignee", Account.Id.class)
+                .put("isPrivate", boolean.class)
+                .put("workInProgress", boolean.class)
+                .put("reviewStarted", boolean.class)
+                .put("revertOf", Change.Id.class)
+                .build());
+  }
+
+  // Unfortunately, Change doesn't implement equals(). Remove this method when we switch Change to
+  // an AutoValue.
+  private static void assertEqualChange(Change change, Change expectedChange) {
+    assertThat(change.getChangeId()).isEqualTo(expectedChange.getChangeId());
+    assertThat(change.getKey()).isEqualTo(expectedChange.getKey());
+    assertThat(change.getRowVersion()).isEqualTo(expectedChange.getRowVersion());
+    assertThat(change.getCreatedOn()).isEqualTo(expectedChange.getCreatedOn());
+    assertThat(change.getLastUpdatedOn()).isEqualTo(expectedChange.getLastUpdatedOn());
+    assertThat(change.getOwner()).isEqualTo(expectedChange.getOwner());
+    assertThat(change.getDest()).isEqualTo(expectedChange.getDest());
+    assertThat(change.getStatus()).isEqualTo(expectedChange.getStatus());
+    assertThat(change.currentPatchSetId()).isEqualTo(expectedChange.currentPatchSetId());
+    assertThat(change.getSubject()).isEqualTo(expectedChange.getSubject());
+    assertThat(change.getTopic()).isEqualTo(expectedChange.getTopic());
+    assertThat(change.getOriginalSubject()).isEqualTo(expectedChange.getOriginalSubject());
+    assertThat(change.getSubmissionId()).isEqualTo(expectedChange.getSubmissionId());
+    assertThat(change.getAssignee()).isEqualTo(expectedChange.getAssignee());
+    assertThat(change.isPrivate()).isEqualTo(expectedChange.isPrivate());
+    assertThat(change.isWorkInProgress()).isEqualTo(expectedChange.isWorkInProgress());
+    assertThat(change.hasReviewStarted()).isEqualTo(expectedChange.hasReviewStarted());
+    assertThat(change.getRevertOf()).isEqualTo(expectedChange.getRevertOf());
+  }
+}
diff --git a/javatests/com/google/gerrit/reviewdb/converter/LabelIdProtoConverterTest.java b/javatests/com/google/gerrit/reviewdb/converter/LabelIdProtoConverterTest.java
new file mode 100644
index 0000000..41e0f3f
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/converter/LabelIdProtoConverterTest.java
@@ -0,0 +1,67 @@
+// 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.LabelId;
+import com.google.protobuf.Parser;
+import org.junit.Test;
+
+public class LabelIdProtoConverterTest {
+  private final LabelIdProtoConverter labelIdProtoConverter = LabelIdProtoConverter.INSTANCE;
+
+  @Test
+  public void allValuesConvertedToProto() {
+    LabelId labelId = new LabelId("Label ID 42");
+
+    Entities.LabelId proto = labelIdProtoConverter.toProto(labelId);
+
+    Entities.LabelId expectedProto = Entities.LabelId.newBuilder().setId("Label ID 42").build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  @Test
+  public void allValuesConvertedToProtoAndBackAgain() {
+    LabelId labelId = new LabelId("label-5");
+
+    LabelId convertedLabelId =
+        labelIdProtoConverter.fromProto(labelIdProtoConverter.toProto(labelId));
+
+    assertThat(convertedLabelId).isEqualTo(labelId);
+  }
+
+  @Test
+  public void protoCanBeParsedFromBytes() throws Exception {
+    Entities.LabelId proto = Entities.LabelId.newBuilder().setId("label-23").build();
+    byte[] bytes = proto.toByteArray();
+
+    Parser<Entities.LabelId> parser = labelIdProtoConverter.getParser();
+    Entities.LabelId 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(LabelId.class).hasFields(ImmutableMap.of("id", String.class));
+  }
+}
diff --git a/javatests/com/google/gerrit/reviewdb/converter/PatchSetApprovalKeyProtoConverterTest.java b/javatests/com/google/gerrit/reviewdb/converter/PatchSetApprovalKeyProtoConverterTest.java
new file mode 100644
index 0000000..d1ed419
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/converter/PatchSetApprovalKeyProtoConverterTest.java
@@ -0,0 +1,98 @@
+// 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.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;
+
+public class PatchSetApprovalKeyProtoConverterTest {
+  private final PatchSetApprovalKeyProtoConverter protoConverter =
+      PatchSetApprovalKeyProtoConverter.INSTANCE;
+
+  @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"));
+
+    Entities.PatchSetApproval_Key proto = protoConverter.toProto(key);
+
+    Entities.PatchSetApproval_Key expectedProto =
+        Entities.PatchSetApproval_Key.newBuilder()
+            .setPatchSetId(
+                Entities.PatchSet_Id.newBuilder()
+                    .setChangeId(Entities.Change_Id.newBuilder().setId(42))
+                    .setPatchSetId(14))
+            .setAccountId(Entities.Account_Id.newBuilder().setId(100013))
+            .setCategoryId(Entities.LabelId.newBuilder().setId("label-8"))
+            .build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  @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 convertedKey = protoConverter.fromProto(protoConverter.toProto(key));
+
+    assertThat(convertedKey).isEqualTo(key);
+  }
+
+  @Test
+  public void protoCanBeParsedFromBytes() throws Exception {
+    Entities.PatchSetApproval_Key proto =
+        Entities.PatchSetApproval_Key.newBuilder()
+            .setPatchSetId(
+                Entities.PatchSet_Id.newBuilder()
+                    .setChangeId(Entities.Change_Id.newBuilder().setId(42))
+                    .setPatchSetId(14))
+            .setAccountId(Entities.Account_Id.newBuilder().setId(100013))
+            .setCategoryId(Entities.LabelId.newBuilder().setId("label-8"))
+            .build();
+    byte[] bytes = proto.toByteArray();
+
+    Parser<Entities.PatchSetApproval_Key> parser = protoConverter.getParser();
+    Entities.PatchSetApproval_Key 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(PatchSetApproval.Key.class)
+        .hasFields(
+            ImmutableMap.<String, Type>builder()
+                .put("patchSetId", PatchSet.Id.class)
+                .put("accountId", Account.Id.class)
+                .put("categoryId", LabelId.class)
+                .build());
+  }
+}
diff --git a/javatests/com/google/gerrit/reviewdb/converter/PatchSetApprovalProtoConverterTest.java b/javatests/com/google/gerrit/reviewdb/converter/PatchSetApprovalProtoConverterTest.java
new file mode 100644
index 0000000..80b2cc2
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/converter/PatchSetApprovalProtoConverterTest.java
@@ -0,0 +1,203 @@
+// 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.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 java.sql.Timestamp;
+import java.util.Date;
+import org.junit.Test;
+
+public class PatchSetApprovalProtoConverterTest {
+  private final PatchSetApprovalProtoConverter protoConverter =
+      PatchSetApprovalProtoConverter.INSTANCE;
+
+  @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);
+
+    Entities.PatchSetApproval proto = protoConverter.toProto(patchSetApproval);
+
+    Entities.PatchSetApproval expectedProto =
+        Entities.PatchSetApproval.newBuilder()
+            .setKey(
+                Entities.PatchSetApproval_Key.newBuilder()
+                    .setPatchSetId(
+                        Entities.PatchSet_Id.newBuilder()
+                            .setChangeId(Entities.Change_Id.newBuilder().setId(42))
+                            .setPatchSetId(14))
+                    .setAccountId(Entities.Account_Id.newBuilder().setId(100013))
+                    .setCategoryId(Entities.LabelId.newBuilder().setId("label-8")))
+            .setValue(456)
+            .setGranted(987654L)
+            .setTag("tag-21")
+            .setRealAccountId(Entities.Account_Id.newBuilder().setId(612))
+            .setPostSubmit(true)
+            .build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  @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));
+
+    Entities.PatchSetApproval proto = protoConverter.toProto(patchSetApproval);
+
+    Entities.PatchSetApproval expectedProto =
+        Entities.PatchSetApproval.newBuilder()
+            .setKey(
+                Entities.PatchSetApproval_Key.newBuilder()
+                    .setPatchSetId(
+                        Entities.PatchSet_Id.newBuilder()
+                            .setChangeId(Entities.Change_Id.newBuilder().setId(42))
+                            .setPatchSetId(14))
+                    .setAccountId(Entities.Account_Id.newBuilder().setId(100013))
+                    .setCategoryId(Entities.LabelId.newBuilder().setId("label-8")))
+            .setValue(456)
+            .setGranted(987654L)
+            // This value can't be unset when our entity class is given.
+            .setPostSubmit(false)
+            .build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  @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 convertedPatchSetApproval =
+        protoConverter.fromProto(protoConverter.toProto(patchSetApproval));
+    assertThat(convertedPatchSetApproval).isEqualTo(patchSetApproval);
+  }
+
+  @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 convertedPatchSetApproval =
+        protoConverter.fromProto(protoConverter.toProto(patchSetApproval));
+    assertThat(convertedPatchSetApproval).isEqualTo(patchSetApproval);
+  }
+
+  // We need this special test as some values are only optional in the protobuf definition but can
+  // never be unset in our entity object.
+  @Test
+  public void protoWithOnlyRequiredValuesCanBeConvertedBack() {
+    Entities.PatchSetApproval proto =
+        Entities.PatchSetApproval.newBuilder()
+            .setKey(
+                Entities.PatchSetApproval_Key.newBuilder()
+                    .setPatchSetId(
+                        Entities.PatchSet_Id.newBuilder()
+                            .setChangeId(Entities.Change_Id.newBuilder().setId(42))
+                            .setPatchSetId(14))
+                    .setAccountId(Entities.Account_Id.newBuilder().setId(100013))
+                    .setCategoryId(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"));
+    // 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);
+  }
+
+  @Test
+  public void protoCanBeParsedFromBytes() throws Exception {
+    Entities.PatchSetApproval proto =
+        Entities.PatchSetApproval.newBuilder()
+            .setKey(
+                Entities.PatchSetApproval_Key.newBuilder()
+                    .setPatchSetId(
+                        Entities.PatchSet_Id.newBuilder()
+                            .setChangeId(Entities.Change_Id.newBuilder().setId(42))
+                            .setPatchSetId(14))
+                    .setAccountId(Entities.Account_Id.newBuilder().setId(100013))
+                    .setCategoryId(Entities.LabelId.newBuilder().setId("label-8")))
+            .setValue(456)
+            .setGranted(987654L)
+            .build();
+    byte[] bytes = proto.toByteArray();
+
+    Parser<Entities.PatchSetApproval> parser = protoConverter.getParser();
+    Entities.PatchSetApproval 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(PatchSetApproval.class)
+        .hasFields(
+            ImmutableMap.<String, Type>builder()
+                .put("key", PatchSetApproval.Key.class)
+                .put("value", short.class)
+                .put("granted", Timestamp.class)
+                .put("tag", String.class)
+                .put("realAccountId", Account.Id.class)
+                .put("postSubmit", boolean.class)
+                .build());
+  }
+}
diff --git a/javatests/com/google/gerrit/reviewdb/converter/PatchSetIdProtoConverterTest.java b/javatests/com/google/gerrit/reviewdb/converter/PatchSetIdProtoConverterTest.java
new file mode 100644
index 0000000..1598ef2
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/converter/PatchSetIdProtoConverterTest.java
@@ -0,0 +1,83 @@
+// 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.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.protobuf.Parser;
+import java.lang.reflect.Type;
+import org.junit.Test;
+
+public class PatchSetIdProtoConverterTest {
+  private final PatchSetIdProtoConverter patchSetIdProtoConverter =
+      PatchSetIdProtoConverter.INSTANCE;
+
+  @Test
+  public void allValuesConvertedToProto() {
+    PatchSet.Id patchSetId = new PatchSet.Id(new 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)
+            .build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  @Test
+  public void allValuesConvertedToProtoAndBackAgain() {
+    PatchSet.Id patchSetId = new PatchSet.Id(new Change.Id(20), 13);
+
+    PatchSet.Id convertedPatchSetId =
+        patchSetIdProtoConverter.fromProto(patchSetIdProtoConverter.toProto(patchSetId));
+
+    assertThat(convertedPatchSetId).isEqualTo(patchSetId);
+  }
+
+  @Test
+  public void protoCanBeParsedFromBytes() throws Exception {
+    Entities.PatchSet_Id proto =
+        Entities.PatchSet_Id.newBuilder()
+            .setChangeId(Entities.Change_Id.newBuilder().setId(103))
+            .setPatchSetId(73)
+            .build();
+    byte[] bytes = proto.toByteArray();
+
+    Parser<Entities.PatchSet_Id> parser = patchSetIdProtoConverter.getParser();
+    Entities.PatchSet_Id 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.Id.class)
+        .hasFields(
+            ImmutableMap.<String, Type>builder()
+                .put("changeId", Change.Id.class)
+                .put("patchSetId", int.class)
+                .build());
+  }
+}
diff --git a/javatests/com/google/gerrit/reviewdb/converter/PatchSetProtoConverterTest.java b/javatests/com/google/gerrit/reviewdb/converter/PatchSetProtoConverterTest.java
new file mode 100644
index 0000000..b8d2b1e
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/converter/PatchSetProtoConverterTest.java
@@ -0,0 +1,137 @@
+// 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/ProjectNameKeyProtoConverterTest.java b/javatests/com/google/gerrit/reviewdb/converter/ProjectNameKeyProtoConverterTest.java
new file mode 100644
index 0000000..2ad6107
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/converter/ProjectNameKeyProtoConverterTest.java
@@ -0,0 +1,71 @@
+// 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.Project;
+import com.google.protobuf.Parser;
+import org.junit.Test;
+
+public class ProjectNameKeyProtoConverterTest {
+  private final ProjectNameKeyProtoConverter projectNameKeyProtoConverter =
+      ProjectNameKeyProtoConverter.INSTANCE;
+
+  @Test
+  public void allValuesConvertedToProto() {
+    Project.NameKey nameKey = new Project.NameKey("project-72");
+
+    Entities.Project_NameKey proto = projectNameKeyProtoConverter.toProto(nameKey);
+
+    Entities.Project_NameKey expectedProto =
+        Entities.Project_NameKey.newBuilder().setName("project-72").build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  @Test
+  public void allValuesConvertedToProtoAndBackAgain() {
+    Project.NameKey nameKey = new Project.NameKey("project-52");
+
+    Project.NameKey convertedNameKey =
+        projectNameKeyProtoConverter.fromProto(projectNameKeyProtoConverter.toProto(nameKey));
+
+    assertThat(convertedNameKey).isEqualTo(nameKey);
+  }
+
+  @Test
+  public void protoCanBeParsedFromBytes() throws Exception {
+    Entities.Project_NameKey proto =
+        Entities.Project_NameKey.newBuilder().setName("project 36").build();
+    byte[] bytes = proto.toByteArray();
+
+    Parser<Entities.Project_NameKey> parser = projectNameKeyProtoConverter.getParser();
+    Entities.Project_NameKey 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(Project.NameKey.class)
+        .hasFields(ImmutableMap.of("name", String.class));
+  }
+}
diff --git a/javatests/com/google/gerrit/reviewdb/converter/RevIdProtoConverterTest.java b/javatests/com/google/gerrit/reviewdb/converter/RevIdProtoConverterTest.java
new file mode 100644
index 0000000..2c354be
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/converter/RevIdProtoConverterTest.java
@@ -0,0 +1,66 @@
+// 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 63ae008..2fcc842 100644
--- a/javatests/com/google/gerrit/server/BUILD
+++ b/javatests/com/google/gerrit/server/BUILD
@@ -37,13 +37,18 @@
         ":custom-truth-subjects",
         "//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/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/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/cache/serialize",
@@ -54,14 +59,13 @@
         "//java/com/google/gerrit/server/project/testing:project-test-util",
         "//java/com/google/gerrit/server/restapi",
         "//java/com/google/gerrit/server/schema",
+        "//java/com/google/gerrit/server/schema/testing",
         "//java/com/google/gerrit/server/util/time",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//java/com/google/gerrit/truth",
-        "//java/org/eclipse/jgit:server",
         "//lib:gson",
         "//lib:guava",
         "//lib:guava-retrying",
-        "//lib:gwtorm",
         "//lib:protobuf",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
diff --git a/javatests/com/google/gerrit/server/ChangeUtilTest.java b/javatests/com/google/gerrit/server/ChangeUtilTest.java
index 5f73d2c..5cb474d 100644
--- a/javatests/com/google/gerrit/server/ChangeUtilTest.java
+++ b/javatests/com/google/gerrit/server/ChangeUtilTest.java
@@ -16,10 +16,11 @@
 
 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 {
+public class ChangeUtilTest extends GerritBaseTests {
   @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 da6c56d..485de49 100644
--- a/javatests/com/google/gerrit/server/IdentifiedUserTest.java
+++ b/javatests/com/google/gerrit/server/IdentifiedUserTest.java
@@ -25,12 +25,13 @@
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.AnonymousCowardNameProvider;
 import com.google.gerrit.server.config.CanonicalWebUrl;
-import com.google.gerrit.server.config.DisableReverseDnsLookup;
+import com.google.gerrit.server.config.EnableReverseDnsLookup;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.group.SystemGroupBackend;
 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;
@@ -44,7 +45,7 @@
 import org.junit.runner.RunWith;
 
 @RunWith(ConfigSuite.class)
-public class IdentifiedUserTest {
+public class IdentifiedUserTest extends GerritBaseTests {
   @ConfigSuite.Parameter public Config config;
 
   private IdentifiedUser identifiedUser;
@@ -80,8 +81,8 @@
           @Override
           protected void configure() {
             bind(Boolean.class)
-                .annotatedWith(DisableReverseDnsLookup.class)
-                .toInstance(Boolean.FALSE);
+                .annotatedWith(EnableReverseDnsLookup.class)
+                .toInstance(Boolean.TRUE);
             bind(Config.class).annotatedWith(GerritServerConfig.class).toInstance(config);
             bind(String.class)
                 .annotatedWith(AnonymousCowardName.class)
diff --git a/javatests/com/google/gerrit/server/account/AccountResolverTest.java b/javatests/com/google/gerrit/server/account/AccountResolverTest.java
new file mode 100644
index 0000000..6f074dd
--- /dev/null
+++ b/javatests/com/google/gerrit/server/account/AccountResolverTest.java
@@ -0,0 +1,394 @@
+// 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.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 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.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 {
+  private class TestSearcher extends StringSearcher {
+    private final String pattern;
+    private final boolean shortCircuit;
+    private final ImmutableList<AccountState> accounts;
+    private boolean assumeVisible;
+    private boolean filterInactive;
+
+    private TestSearcher(String pattern, boolean shortCircuit, AccountState... accounts) {
+      this.pattern = pattern;
+      this.shortCircuit = shortCircuit;
+      this.accounts = ImmutableList.copyOf(accounts);
+    }
+
+    @Override
+    protected boolean matches(String input) {
+      return input.matches(pattern);
+    }
+
+    @Override
+    public Stream<AccountState> search(String input) {
+      return accounts.stream();
+    }
+
+    @Override
+    public boolean shortCircuitIfNoResults() {
+      return shortCircuit;
+    }
+
+    @Override
+    public boolean callerMayAssumeCandidatesAreVisible() {
+      return assumeVisible;
+    }
+
+    void setCallerMayAssumeCandidatesAreVisible() {
+      this.assumeVisible = true;
+    }
+
+    @Override
+    public boolean callerShouldFilterOutInactiveCandidates() {
+      return filterInactive;
+    }
+
+    void setCallerShouldFilterOutInactiveCandidates() {
+      this.filterInactive = true;
+    }
+
+    @Override
+    public String toString() {
+      return accounts.stream()
+          .map(a -> a.getAccount().getId().toString())
+          .collect(joining(",", pattern + "(", ")"));
+    }
+  }
+
+  @Test
+  public void noShortCircuit() throws Exception {
+    ImmutableList<Searcher<?>> searchers =
+        ImmutableList.of(
+            new TestSearcher("foo", false, newAccount(1)),
+            new TestSearcher("bar", false, newAccount(2), newAccount(3)));
+
+    Result result = search("foo", searchers, allVisible());
+    assertThat(result.input()).isEqualTo("foo");
+    assertThat(result.asIdSet()).containsExactlyElementsIn(ids(1));
+
+    result = search("bar", searchers, allVisible());
+    assertThat(result.input()).isEqualTo("bar");
+    assertThat(result.asIdSet()).containsExactlyElementsIn(ids(2, 3));
+
+    result = search("baz", searchers, allVisible());
+    assertThat(result.input()).isEqualTo("baz");
+    assertThat(result.asIdSet()).isEmpty();
+  }
+
+  @Test
+  public void shortCircuit() throws Exception {
+    ImmutableList<Searcher<?>> searchers =
+        ImmutableList.of(
+            new TestSearcher("f.*", true), new TestSearcher("foo|bar", false, newAccount(1)));
+
+    Result result = search("foo", searchers, allVisible());
+    assertThat(result.input()).isEqualTo("foo");
+    assertThat(result.asIdSet()).isEmpty();
+
+    result = search("bar", searchers, allVisible());
+    assertThat(result.input()).isEqualTo("bar");
+    assertThat(result.asIdSet()).containsExactlyElementsIn(ids(1));
+  }
+
+  @Test
+  public void filterInvisible() throws Exception {
+    ImmutableList<Searcher<?>> searchers =
+        ImmutableList.of(new TestSearcher("foo", false, newAccount(1), newAccount(2)));
+
+    assertThat(search("foo", searchers, allVisible()).asIdSet())
+        .containsExactlyElementsIn(ids(1, 2));
+    assertThat(search("foo", searchers, only(2)).asIdSet()).containsExactlyElementsIn(ids(2));
+  }
+
+  @Test
+  public void skipVisibilityCheck() throws Exception {
+    TestSearcher searcher = new TestSearcher("foo", false, newAccount(1), newAccount(2));
+    ImmutableList<Searcher<?>> searchers = ImmutableList.of(searcher);
+
+    assertThat(search("foo", searchers, only(2)).asIdSet()).containsExactlyElementsIn(ids(2));
+
+    searcher.setCallerMayAssumeCandidatesAreVisible();
+    assertThat(search("foo", searchers, only(2)).asIdSet()).containsExactlyElementsIn(ids(1, 2));
+  }
+
+  @Test
+  public void dontFilterInactive() throws Exception {
+    ImmutableList<Searcher<?>> searchers =
+        ImmutableList.of(
+            new TestSearcher("foo", false, newInactiveAccount(1)),
+            new TestSearcher("f.*", false, newInactiveAccount(2)));
+
+    Result result = search("foo", searchers, allVisible());
+    // 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(filteredInactiveIds(result)).isEmpty();
+  }
+
+  @Test
+  public void shouldUseCustomAccountActivityPredicate() throws Exception {
+    TestSearcher searcher1 = new TestSearcher("foo", false, newInactiveAccount(1));
+    searcher1.setCallerShouldFilterOutInactiveCandidates();
+    TestSearcher searcher2 = new TestSearcher("f.*", false, newInactiveAccount(2));
+    searcher2.setCallerShouldFilterOutInactiveCandidates();
+    ImmutableList<Searcher<?>> searchers = ImmutableList.of(searcher1, searcher2);
+
+    Result result = search("foo", searchers, allVisible(), (a) -> true);
+    // 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(filteredInactiveIds(result)).isEmpty();
+  }
+
+  @Test
+  public void filterInactiveEventuallyFindingResults() throws Exception {
+    TestSearcher searcher1 = new TestSearcher("foo", false, newInactiveAccount(1));
+    searcher1.setCallerShouldFilterOutInactiveCandidates();
+    TestSearcher searcher2 = new TestSearcher("f.*", false, newAccount(2));
+    searcher2.setCallerShouldFilterOutInactiveCandidates();
+    ImmutableList<Searcher<?>> searchers = ImmutableList.of(searcher1, searcher2);
+
+    Result result = search("foo", searchers, allVisible());
+    assertThat(search("foo", searchers, allVisible()).asIdSet()).containsExactlyElementsIn(ids(2));
+    // No info about inactive results exposed if there was at least one active result.
+    assertThat(filteredInactiveIds(result)).isEmpty();
+  }
+
+  @Test
+  public void filterInactiveEventuallyFindingNoResults() throws Exception {
+    TestSearcher searcher1 = new TestSearcher("foo", false, newInactiveAccount(1));
+    searcher1.setCallerShouldFilterOutInactiveCandidates();
+    TestSearcher searcher2 = new TestSearcher("f.*", false, newInactiveAccount(2));
+    searcher2.setCallerShouldFilterOutInactiveCandidates();
+    ImmutableList<Searcher<?>> searchers = ImmutableList.of(searcher1, searcher2);
+
+    Result result = search("foo", searchers, allVisible());
+    assertThat(result.asIdSet()).isEmpty();
+    assertThat(filteredInactiveIds(result)).containsExactlyElementsIn(ids(1, 2));
+  }
+
+  @Test
+  public void dontShortCircuitAfterFilteringInactiveCandidatesResultsInEmptyList()
+      throws Exception {
+    AccountState account1 = newAccount(1);
+    AccountState account2 = newInactiveAccount(2);
+    TestSearcher searcher1 = new TestSearcher("foo", false, account2);
+    searcher1.setCallerShouldFilterOutInactiveCandidates();
+
+    TestSearcher searcher2 = new TestSearcher("foo", false, account1, account2);
+    ImmutableList<Searcher<?>> searchers = ImmutableList.of(searcher1, searcher2);
+
+    // searcher1 matched, but filtered out all candidates because account2 is inactive. Actual
+    // result came from searcher2 instead.
+    Result result = search("foo", searchers, allVisible());
+    assertThat(result.asIdSet()).containsExactlyElementsIn(ids(1, 2));
+  }
+
+  @Test
+  public void shortCircuitAfterFilteringInactiveCandidatesResultsInEmptyList() throws Exception {
+    AccountState account1 = newAccount(1);
+    AccountState account2 = newInactiveAccount(2);
+    TestSearcher searcher1 = new TestSearcher("foo", true, account2);
+    searcher1.setCallerShouldFilterOutInactiveCandidates();
+
+    TestSearcher searcher2 = new TestSearcher("foo", false, account1, account2);
+    ImmutableList<Searcher<?>> searchers = ImmutableList.of(searcher1, searcher2);
+
+    // searcher1 matched and then filtered out all candidates because account2 is inactive, but
+    // still short-circuited.
+    Result result = search("foo", searchers, allVisible());
+    assertThat(result.asIdSet()).isEmpty();
+    assertThat(filteredInactiveIds(result)).containsExactlyElementsIn(ids(2));
+  }
+
+  @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");
+    }
+  }
+
+  @Test
+  public void asUniqueWithOneResult() throws Exception {
+    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());
+  }
+
+  @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)");
+    }
+  }
+
+  @Test
+  public void exceptionMessageNotFound() throws Exception {
+    AccountResolver resolver = newAccountResolver();
+    assertThat(
+            new UnresolvableAccountException(
+                resolver.new Result("foo", ImmutableList.of(), ImmutableList.of())))
+        .hasMessageThat()
+        .isEqualTo("Account 'foo' not found");
+  }
+
+  @Test
+  public void exceptionMessageSelf() throws Exception {
+    AccountResolver resolver = newAccountResolver();
+    UnresolvableAccountException e =
+        new UnresolvableAccountException(
+            resolver.new Result("self", ImmutableList.of(), ImmutableList.of()));
+    assertThat(e.isSelf()).isTrue();
+    assertThat(e).hasMessageThat().isEqualTo("Resolving account 'self' requires login");
+  }
+
+  @Test
+  public void exceptionMessageMe() throws Exception {
+    AccountResolver resolver = newAccountResolver();
+    UnresolvableAccountException e =
+        new UnresolvableAccountException(
+            resolver.new Result("me", ImmutableList.of(), ImmutableList.of()));
+    assertThat(e.isSelf()).isTrue();
+    assertThat(e).hasMessageThat().isEqualTo("Resolving account 'me' requires login");
+  }
+
+  @Test
+  public void exceptionMessageAmbiguous() throws Exception {
+    AccountResolver resolver = newAccountResolver();
+    assertThat(
+            new UnresolvableAccountException(
+                resolver
+                .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)");
+  }
+
+  @Test
+  public void exceptionMessageOnlyInactive() throws Exception {
+    AccountResolver resolver = newAccountResolver();
+    assertThat(
+            new UnresolvableAccountException(
+                resolver
+                .new Result(
+                    "foo",
+                    ImmutableList.of(),
+                    ImmutableList.of(newInactiveAccount(3), newInactiveAccount(1)))))
+        .hasMessageThat()
+        .isEqualTo(
+            "Account 'foo' only matches inactive accounts. To use an inactive account, retry"
+                + " with one of the following exact account IDs:\n"
+                + "1: Anonymous Name (1)\n"
+                + "3: Anonymous Name (3)");
+  }
+
+  private Result search(
+      String input,
+      ImmutableList<Searcher<?>> searchers,
+      Supplier<Predicate<AccountState>> visibilitySupplier)
+      throws Exception {
+    return search(input, searchers, visibilitySupplier, activityPrediate());
+  }
+
+  private Result search(
+      String input,
+      ImmutableList<Searcher<?>> searchers,
+      Supplier<Predicate<AccountState>> visibilitySupplier,
+      Predicate<AccountState> activityPredicate)
+      throws Exception {
+    return newAccountResolver().searchImpl(input, searchers, visibilitySupplier, activityPredicate);
+  }
+
+  private static AccountResolver newAccountResolver() {
+    return new AccountResolver(null, null, null, null, null, null, null, "Anonymous Name");
+  }
+
+  private AccountState newAccount(int id) {
+    return AccountState.forAccount(
+        new AllUsersName("All-Users"), new Account(new Account.Id(id), TimeUtil.nowTs()));
+  }
+
+  private AccountState newInactiveAccount(int id) {
+    Account a = new Account(new Account.Id(id), TimeUtil.nowTs());
+    a.setActive(false);
+    return AccountState.forAccount(new AllUsersName("All-Users"), a);
+  }
+
+  private static ImmutableSet<Account.Id> ids(int... ids) {
+    return Arrays.stream(ids).mapToObj(Account.Id::new).collect(toImmutableSet());
+  }
+
+  private static Supplier<Predicate<AccountState>> allVisible() {
+    return () -> a -> true;
+  }
+
+  private Predicate<AccountState> activityPrediate() {
+    return (AccountState accountState) -> accountState.getAccount().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());
+  }
+
+  private static ImmutableSet<Account.Id> filteredInactiveIds(Result result) {
+    return result.filteredInactive().stream()
+        .map(a -> a.getAccount().getId())
+        .collect(toImmutableSet());
+  }
+}
diff --git a/javatests/com/google/gerrit/server/account/AuthorizedKeysTest.java b/javatests/com/google/gerrit/server/account/AuthorizedKeysTest.java
index 80a15a3..51a34f5 100644
--- a/javatests/com/google/gerrit/server/account/AuthorizedKeysTest.java
+++ b/javatests/com/google/gerrit/server/account/AuthorizedKeysTest.java
@@ -17,12 +17,13 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.testing.GerritBaseTests;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
 import org.junit.Test;
 
-public class AuthorizedKeysTest {
+public class AuthorizedKeysTest extends GerritBaseTests {
   private static final String KEY1 =
       "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCgug5VyMXQGnem2H1KVC4/HcRcD4zzBqS"
           + "uJBRWVonSSoz3RoAZ7bWXCVVGwchtXwUURD689wFYdiPecOrWOUgeeyRq754YWRhU+W28"
@@ -140,7 +141,7 @@
   }
 
   private static String toWindowsLineEndings(String s) {
-    return s.replaceAll("\n", "\r\n");
+    return s.replace("\n", "\r\n");
   }
 
   private static void assertSerialization(
diff --git a/javatests/com/google/gerrit/server/account/DestinationListTest.java b/javatests/com/google/gerrit/server/account/DestinationListTest.java
index 1f6ed60..e51b041 100644
--- a/javatests/com/google/gerrit/server/account/DestinationListTest.java
+++ b/javatests/com/google/gerrit/server/account/DestinationListTest.java
@@ -15,19 +15,19 @@
 package com.google.gerrit.server.account;
 
 import static com.google.common.truth.Truth.assertThat;
-import static org.easymock.EasyMock.createNiceMock;
-import static org.easymock.EasyMock.replay;
 
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.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;
+import java.util.List;
 import java.util.Set;
-import junit.framework.TestCase;
 import org.junit.Test;
 
-public class DestinationListTest extends TestCase {
+public class DestinationListTest extends GerritBaseTests {
   public static final String R_FOO = "refs/heads/foo";
   public static final String R_BAR = "refs/heads/bar";
 
@@ -128,11 +128,12 @@
     assertThat(branches).contains(B_COMPLEX);
   }
 
-  @Test(expected = IOException.class)
+  @Test
   public void testParseBad() throws IOException {
-    ValidationError.Sink sink = createNiceMock(ValidationError.Sink.class);
-    replay(sink);
-    new DestinationList().parseLabel(LABEL, L_BAD, sink);
+    List<ValidationError> errors = new ArrayList<>();
+    new DestinationList().parseLabel(LABEL, L_BAD, errors::add);
+    assertThat(errors)
+        .containsExactly(new ValidationError("destinationslabel", 1, "missing tab delimiter"));
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/account/GroupUUIDTest.java b/javatests/com/google/gerrit/server/account/GroupUUIDTest.java
index 3b72b08..70887e6 100644
--- a/javatests/com/google/gerrit/server/account/GroupUUIDTest.java
+++ b/javatests/com/google/gerrit/server/account/GroupUUIDTest.java
@@ -17,10 +17,11 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.testing.GerritBaseTests;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.junit.Test;
 
-public class GroupUUIDTest {
+public class GroupUUIDTest extends GerritBaseTests {
   @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 4955c06..9a0c9cb9 100644
--- a/javatests/com/google/gerrit/server/account/HashedPasswordTest.java
+++ b/javatests/com/google/gerrit/server/account/HashedPasswordTest.java
@@ -17,10 +17,11 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.testing.GerritBaseTests;
 import org.apache.commons.codec.DecoderException;
 import org.junit.Test;
 
-public class HashedPasswordTest {
+public class HashedPasswordTest extends GerritBaseTests {
 
   @Test
   public void encodeOneLine() throws Exception {
diff --git a/javatests/com/google/gerrit/server/account/QueryListTest.java b/javatests/com/google/gerrit/server/account/QueryListTest.java
index 2792de8..a0876e1 100644
--- a/javatests/com/google/gerrit/server/account/QueryListTest.java
+++ b/javatests/com/google/gerrit/server/account/QueryListTest.java
@@ -15,15 +15,14 @@
 package com.google.gerrit.server.account;
 
 import static com.google.common.truth.Truth.assertThat;
-import static org.easymock.EasyMock.createNiceMock;
-import static org.easymock.EasyMock.replay;
 
 import com.google.gerrit.server.git.ValidationError;
-import java.io.IOException;
-import junit.framework.TestCase;
+import com.google.gerrit.testing.GerritBaseTests;
+import java.util.ArrayList;
+import java.util.List;
 import org.junit.Test;
 
-public class QueryListTest extends TestCase {
+public class QueryListTest extends GerritBaseTests {
   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'";
@@ -99,11 +98,11 @@
     assertThat(ql.getQuery(N_FOO)).isEqualTo(Q_COMPLEX);
   }
 
-  @Test(expected = IOException.class)
+  @Test
   public void testParseBad() throws Exception {
-    ValidationError.Sink sink = createNiceMock(ValidationError.Sink.class);
-    replay(sink);
-    QueryList.parse(L_BAD, sink);
+    List<ValidationError> errors = new ArrayList<>();
+    assertThat(QueryList.parse(L_BAD, errors::add).asText()).isNull();
+    assertThat(errors).containsExactly(new ValidationError("queries", 1, "missing tab delimiter"));
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/account/UniversalGroupBackendTest.java b/javatests/com/google/gerrit/server/account/UniversalGroupBackendTest.java
index f69bafa..334c627 100644
--- a/javatests/com/google/gerrit/server/account/UniversalGroupBackendTest.java
+++ b/javatests/com/google/gerrit/server/account/UniversalGroupBackendTest.java
@@ -39,7 +39,6 @@
 import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.gerrit.testing.GerritBaseTests;
 import java.util.Set;
-import org.easymock.IAnswer;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Before;
 import org.junit.Test;
@@ -113,16 +112,13 @@
     expect(backend.handles(not(eq(handled)))).andStubReturn(false);
     expect(backend.membershipsOf(anyObject(IdentifiedUser.class)))
         .andStubAnswer(
-            new IAnswer<GroupMembership>() {
-              @Override
-              public GroupMembership answer() throws Throwable {
-                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;
-              }
+            () -> {
+              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;
             });
     replay(member, notMember, backend);
 
diff --git a/javatests/com/google/gerrit/server/account/externalids/AllExternalIdsTest.java b/javatests/com/google/gerrit/server/account/externalids/AllExternalIdsTest.java
index f29ff1f..d757f71 100644
--- a/javatests/com/google/gerrit/server/account/externalids/AllExternalIdsTest.java
+++ b/javatests/com/google/gerrit/server/account/externalids/AllExternalIdsTest.java
@@ -16,8 +16,8 @@
 
 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 static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.byteString;
-import static com.google.gerrit.server.cache.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSetMultimap;
@@ -25,12 +25,13 @@
 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 {
+public class AllExternalIdsTest extends GerritBaseTests {
   @Test
   public void serializeEmptyExternalIds() throws Exception {
     assertRoundTrip(allExternalIds(), AllExternalIdsProto.getDefaultInstance());
diff --git a/javatests/com/google/gerrit/server/auth/oauth/OAuthTokenCacheTest.java b/javatests/com/google/gerrit/server/auth/oauth/OAuthTokenCacheTest.java
index 81fd6d7..3c7e492 100644
--- a/javatests/com/google/gerrit/server/auth/oauth/OAuthTokenCacheTest.java
+++ b/javatests/com/google/gerrit/server/auth/oauth/OAuthTokenCacheTest.java
@@ -1,11 +1,26 @@
+// 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.auth.oauth;
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
-import static com.google.gerrit.server.cache.testing.SerializedClassSubject.assertThatSerializedClass;
+import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.extensions.auth.oauth.OAuthToken;
+import com.google.gerrit.proto.testing.SerializedClassSubject;
 import com.google.gerrit.server.cache.proto.Cache.OAuthTokenProto;
 import com.google.gerrit.server.cache.serialize.CacheSerializer;
 import java.lang.reflect.Type;
@@ -56,10 +71,7 @@
     assertThat(s.deserialize(serializedWithEmptyString)).isEqualTo(tokenWithNull);
   }
 
-  /**
-   * See {@link com.google.gerrit.server.cache.testing.SerializedClassSubject} for background and
-   * what to do if this test fails.
-   */
+  /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
   @Test
   public void oAuthTokenFields() throws Exception {
     assertThatSerializedClass(OAuthToken.class)
diff --git a/javatests/com/google/gerrit/server/cache/BUILD b/javatests/com/google/gerrit/server/cache/BUILD
index 4950266..c255e61 100644
--- a/javatests/com/google/gerrit/server/cache/BUILD
+++ b/javatests/com/google/gerrit/server/cache/BUILD
@@ -5,6 +5,7 @@
     srcs = glob(["*.java"]),
     deps = [
         "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:junit",
         "//lib/truth",
     ],
diff --git a/javatests/com/google/gerrit/server/cache/PerThreadCacheTest.java b/javatests/com/google/gerrit/server/cache/PerThreadCacheTest.java
index cfb5f3f..6a42577 100644
--- a/javatests/com/google/gerrit/server/cache/PerThreadCacheTest.java
+++ b/javatests/com/google/gerrit/server/cache/PerThreadCacheTest.java
@@ -16,14 +16,11 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.gerrit.testing.GerritBaseTests;
 import java.util.function.Supplier;
-import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 
-public class PerThreadCacheTest {
-  @Rule public ExpectedException exception = ExpectedException.none();
-
+public class PerThreadCacheTest extends GerritBaseTests {
   @Test
   public void key_respectsClass() {
     assertThat(PerThreadCache.Key.create(String.class))
diff --git a/javatests/com/google/gerrit/server/cache/serialize/BUILD b/javatests/com/google/gerrit/server/cache/serialize/BUILD
index 35d8527..a0d5ea6 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/BUILD
+++ b/javatests/com/google/gerrit/server/cache/serialize/BUILD
@@ -6,8 +6,9 @@
     deps = [
         "//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:gwtorm",
         "//lib:junit",
         "//lib:protobuf",
         "//lib/auto:auto-value",
@@ -16,5 +17,6 @@
         "//lib/truth",
         "//lib/truth:truth-proto-extension",
         "//proto:cache_java_proto",
+        "//proto/testing:test_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 7504850..c634a78 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/BooleanCacheSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/BooleanCacheSerializerTest.java
@@ -18,10 +18,11 @@
 import static com.google.common.truth.Truth.assert_;
 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 {
+public class BooleanCacheSerializerTest extends GerritBaseTests {
   @Test
   public void serialize() throws Exception {
     assertThat(BooleanCacheSerializer.INSTANCE.serialize(true))
diff --git a/javatests/com/google/gerrit/server/cache/serialize/EnumCacheSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/EnumCacheSerializerTest.java
index 0b80fc7..c6efc21 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/EnumCacheSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/EnumCacheSerializerTest.java
@@ -18,9 +18,10 @@
 import static com.google.common.truth.Truth.assert_;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class EnumCacheSerializerTest {
+public class EnumCacheSerializerTest extends GerritBaseTests {
   @Test
   public void serialize() throws Exception {
     assertRoundTrip(MyEnum.FOO);
diff --git a/javatests/com/google/gerrit/server/cache/serialize/IntKeyCacheSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/IntKeyCacheSerializerTest.java
index 987a62a..56dd6ad 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/IntKeyCacheSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/IntKeyCacheSerializerTest.java
@@ -17,11 +17,12 @@
 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 {
+public class IntKeyCacheSerializerTest extends GerritBaseTests {
 
   private static class MyIntKey extends IntKey<Key<?>> {
     private static final long serialVersionUID = 1L;
diff --git a/javatests/com/google/gerrit/server/cache/serialize/IntegerCacheSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/IntegerCacheSerializerTest.java
index c2db808..1d54010 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/IntegerCacheSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/IntegerCacheSerializerTest.java
@@ -19,10 +19,11 @@
 
 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 {
+public class IntegerCacheSerializerTest extends GerritBaseTests {
   @Test
   public void serialize() throws Exception {
     for (int i :
diff --git a/javatests/com/google/gerrit/server/cache/serialize/JavaCacheSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/JavaCacheSerializerTest.java
index 6596730..9fcb8a4 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/JavaCacheSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/JavaCacheSerializerTest.java
@@ -17,10 +17,11 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.auto.value.AutoValue;
+import com.google.gerrit.testing.GerritBaseTests;
 import java.io.Serializable;
 import org.junit.Test;
 
-public class JavaCacheSerializerTest {
+public class JavaCacheSerializerTest extends GerritBaseTests {
   @Test
   public void builtInTypes() throws Exception {
     assertRoundTrip("foo");
diff --git a/javatests/com/google/gerrit/server/cache/serialize/ObjectIdCacheSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/ObjectIdCacheSerializerTest.java
index c56f8f8..257be54 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/ObjectIdCacheSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/ObjectIdCacheSerializerTest.java
@@ -18,10 +18,11 @@
 import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.byteArray;
 
+import com.google.gerrit.testing.GerritBaseTests;
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Test;
 
-public class ObjectIdCacheSerializerTest {
+public class ObjectIdCacheSerializerTest extends GerritBaseTests {
   @Test
   public void serialize() {
     ObjectId id = ObjectId.fromString("aabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
diff --git a/javatests/com/google/gerrit/server/cache/serialize/ObjectIdConverterTest.java b/javatests/com/google/gerrit/server/cache/serialize/ObjectIdConverterTest.java
new file mode 100644
index 0000000..c5ea2ea
--- /dev/null
+++ b/javatests/com/google/gerrit/server/cache/serialize/ObjectIdConverterTest.java
@@ -0,0 +1,72 @@
+// 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 static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.byteString;
+
+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 {
+  @Test
+  public void objectIdFromByteString() {
+    ObjectIdConverter idConverter = ObjectIdConverter.create();
+    assertThat(
+            idConverter.fromByteString(
+                byteString(
+                    0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
+                    0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa)))
+        .isEqualTo(ObjectId.fromString("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"));
+    assertThat(
+            idConverter.fromByteString(
+                byteString(
+                    0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb,
+                    0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb)))
+        .isEqualTo(ObjectId.fromString("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"));
+  }
+
+  @Test
+  public void objectIdFromByteStringWrongSize() {
+    try {
+      ObjectIdConverter.create().fromByteString(ByteString.copyFromUtf8("foo"));
+      assert_().fail("expected IllegalArgumentException");
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
+  }
+
+  @Test
+  public void objectIdToByteString() {
+    ObjectIdConverter idConverter = ObjectIdConverter.create();
+    assertThat(
+            idConverter.toByteString(
+                ObjectId.fromString("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")))
+        .isEqualTo(
+            byteString(
+                0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
+                0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa));
+    assertThat(
+            idConverter.toByteString(
+                ObjectId.fromString("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")))
+        .isEqualTo(
+            byteString(
+                0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb,
+                0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb));
+  }
+}
diff --git a/javatests/com/google/gerrit/server/cache/serialize/ProtoCacheSerializersTest.java b/javatests/com/google/gerrit/server/cache/serialize/ProtoCacheSerializersTest.java
deleted file mode 100644
index 69694fe..0000000
--- a/javatests/com/google/gerrit/server/cache/serialize/ProtoCacheSerializersTest.java
+++ /dev/null
@@ -1,116 +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 static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
-import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.byteString;
-
-import com.google.gerrit.server.cache.proto.Cache.ChangeNotesKeyProto;
-import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto;
-import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers.ObjectIdConverter;
-import com.google.protobuf.ByteString;
-import org.eclipse.jgit.lib.ObjectId;
-import org.junit.Test;
-
-public class ProtoCacheSerializersTest {
-  @Test
-  public void objectIdFromByteString() {
-    ObjectIdConverter idConverter = ObjectIdConverter.create();
-    assertThat(
-            idConverter.fromByteString(
-                byteString(
-                    0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
-                    0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa)))
-        .isEqualTo(ObjectId.fromString("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"));
-    assertThat(
-            idConverter.fromByteString(
-                byteString(
-                    0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb,
-                    0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb)))
-        .isEqualTo(ObjectId.fromString("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"));
-  }
-
-  @Test
-  public void objectIdFromByteStringWrongSize() {
-    try {
-      ObjectIdConverter.create().fromByteString(ByteString.copyFromUtf8("foo"));
-      assert_().fail("expected IllegalArgumentException");
-    } catch (IllegalArgumentException e) {
-      // Expected.
-    }
-  }
-
-  @Test
-  public void objectIdToByteString() {
-    ObjectIdConverter idConverter = ObjectIdConverter.create();
-    assertThat(
-            idConverter.toByteString(
-                ObjectId.fromString("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")))
-        .isEqualTo(
-            byteString(
-                0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
-                0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa));
-    assertThat(
-            idConverter.toByteString(
-                ObjectId.fromString("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")))
-        .isEqualTo(
-            byteString(
-                0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb,
-                0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb));
-  }
-
-  @Test
-  public void parseUncheckedWrongProtoType() {
-    ChangeNotesKeyProto proto =
-        ChangeNotesKeyProto.newBuilder()
-            .setProject("project")
-            .setChangeId(1234)
-            .setId(ByteString.copyFromUtf8("foo"))
-            .build();
-    byte[] bytes = ProtoCacheSerializers.toByteArray(proto);
-    try {
-      ProtoCacheSerializers.parseUnchecked(ChangeNotesStateProto.parser(), bytes);
-      assert_().fail("expected IllegalArgumentException");
-    } catch (IllegalArgumentException e) {
-      // Expected.
-    }
-  }
-
-  @Test
-  public void parseUncheckedInvalidData() {
-    byte[] bytes = new byte[] {0x00};
-    try {
-      ProtoCacheSerializers.parseUnchecked(ChangeNotesStateProto.parser(), bytes);
-      assert_().fail("expected IllegalArgumentException");
-    } catch (IllegalArgumentException e) {
-      // Expected.
-    }
-  }
-
-  @Test
-  public void parseUnchecked() {
-    ChangeNotesKeyProto proto =
-        ChangeNotesKeyProto.newBuilder()
-            .setProject("project")
-            .setChangeId(1234)
-            .setId(ByteString.copyFromUtf8("foo"))
-            .build();
-    byte[] bytes = ProtoCacheSerializers.toByteArray(proto);
-    assertThat(ProtoCacheSerializers.parseUnchecked(ChangeNotesKeyProto.parser(), bytes))
-        .isEqualTo(proto);
-  }
-}
diff --git a/javatests/com/google/gerrit/server/cache/serialize/ProtobufSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/ProtobufSerializerTest.java
new file mode 100644
index 0000000..845da9b
--- /dev/null
+++ b/javatests/com/google/gerrit/server/cache/serialize/ProtobufSerializerTest.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.cache.serialize;
+
+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 {
+  @Test
+  public void requiredAndOptionalTypes() {
+    assertRoundTrip(SerializableProto.newBuilder().setId(123));
+    assertRoundTrip(SerializableProto.newBuilder().setId(123).setText("foo bar"));
+  }
+
+  @Test
+  public void exactByteSequence() {
+    ProtobufSerializer<SerializableProto> s = new ProtobufSerializer<>(SerializableProto.parser());
+    SerializableProto proto = SerializableProto.newBuilder().setId(123).setText("foo bar").build();
+    byte[] serialized = s.serialize(proto);
+    // Hard-code byte sequence to detect library changes
+    assertThat(serialized).isEqualTo(new byte[] {8, 123, 18, 7, 102, 111, 111, 32, 98, 97, 114});
+  }
+
+  private static void assertRoundTrip(SerializableProto.Builder input) {
+    ProtobufSerializer<SerializableProto> s = new ProtobufSerializer<>(SerializableProto.parser());
+    assertThat(s.deserialize(s.serialize(input.build()))).isEqualTo(input.build());
+  }
+}
diff --git a/javatests/com/google/gerrit/server/cache/serialize/StringCacheSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/StringCacheSerializerTest.java
index fa3b7d7..ff0cf9a 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/StringCacheSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/StringCacheSerializerTest.java
@@ -17,11 +17,12 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assert_;
 
+import com.google.gerrit.testing.GerritBaseTests;
 import java.nio.charset.CharacterCodingException;
 import java.nio.charset.StandardCharsets;
 import org.junit.Test;
 
-public class StringCacheSerializerTest {
+public class StringCacheSerializerTest extends GerritBaseTests {
   @Test
   public void serialize() {
     assertThat(StringCacheSerializer.INSTANCE.serialize("")).isEmpty();
diff --git a/javatests/com/google/gerrit/server/change/ChangeKindCacheImplTest.java b/javatests/com/google/gerrit/server/change/ChangeKindCacheImplTest.java
index b847ed7..fffb1da 100644
--- a/javatests/com/google/gerrit/server/change/ChangeKindCacheImplTest.java
+++ b/javatests/com/google/gerrit/server/change/ChangeKindCacheImplTest.java
@@ -16,17 +16,19 @@
 
 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 static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.byteString;
-import static com.google.gerrit.server.cache.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.proto.testing.SerializedClassSubject;
 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 {
+public class ChangeKindCacheImplTest extends GerritBaseTests {
   @Test
   public void keySerializer() throws Exception {
     ChangeKindCacheImpl.Key key =
@@ -49,10 +51,7 @@
     assertThat(s.deserialize(serialized)).isEqualTo(key);
   }
 
-  /**
-   * See {@link com.google.gerrit.server.cache.testing.SerializedClassSubject} for background and
-   * what to do if this test fails.
-   */
+  /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
   @Test
   public void keyFields() throws Exception {
     assertThatSerializedClass(ChangeKindCacheImpl.Key.class)
diff --git a/javatests/com/google/gerrit/server/change/HashtagsTest.java b/javatests/com/google/gerrit/server/change/HashtagsTest.java
index 780ac71..49d2952 100644
--- a/javatests/com/google/gerrit/server/change/HashtagsTest.java
+++ b/javatests/com/google/gerrit/server/change/HashtagsTest.java
@@ -17,9 +17,10 @@
 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 {
+public class HashtagsTest extends GerritBaseTests {
   @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 dca2dcb..0cfe483 100644
--- a/javatests/com/google/gerrit/server/change/IncludedInResolverTest.java
+++ b/javatests/com/google/gerrit/server/change/IncludedInResolverTest.java
@@ -15,22 +15,19 @@
 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 java.io.IOException;
-import org.eclipse.jgit.api.Git;
-import org.eclipse.jgit.api.MergeCommand.FastForwardMode;
-import org.eclipse.jgit.junit.RepositoryTestCase;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Ref;
+import com.google.gerrit.testing.GerritBaseTests;
+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.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevTag;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
-public class IncludedInResolverTest extends RepositoryTestCase {
-
+public class IncludedInResolverTest extends GerritBaseTests {
   // Branch names
   private static final String BRANCH_MASTER = "master";
   private static final String BRANCH_1_0 = "rel-1.0";
@@ -53,12 +50,11 @@
   private RevCommit commit_v1_3;
   private RevCommit commit_v2_5;
 
-  private RevWalk revWalk;
+  private TestRepository<?> tr;
 
-  @Override
   @Before
   public void setUp() throws Exception {
-    super.setUp();
+    tr = new TestRepository<>(new InMemoryRepository(new DfsRepositoryDescription("repo")));
 
     /*- The following graph will be created.
 
@@ -78,55 +74,37 @@
 
     */
 
-    // TODO(dborowitz): Use try/finally when this doesn't double-close the repo.
-    @SuppressWarnings("resource")
-    Git git = new Git(db);
-    revWalk = new RevWalk(db);
     // Version 1.0
-    commit_initial = git.commit().setMessage("c1").call();
-    git.commit().setMessage("c2").call();
-    RevCommit commit_v1_0 = git.commit().setMessage("version 1.0").call();
-    git.tag().setName(TAG_1_0).setObjectId(commit_v1_0).call();
-    RevCommit c3 = git.commit().setMessage("c3").call();
+    commit_initial = tr.branch(BRANCH_MASTER).commit().message("c1").create();
+    tr.branch(BRANCH_MASTER).commit().message("c2").create();
+    RevCommit commit_v1_0 = tr.branch(BRANCH_MASTER).commit().message("version 1.0").create();
+    tag(TAG_1_0, commit_v1_0);
+    RevCommit c3 = tr.branch(BRANCH_MASTER).commit().message("c3").create();
+
     // Version 1.01
-    createAndCheckoutBranch(commit_v1_0, BRANCH_1_0);
-    RevCommit commit_v1_0_1 = git.commit().setMessage("verREFS_HEADS_RELsion 1.0.1").call();
-    git.tag().setName(TAG_1_0_1).setObjectId(commit_v1_0_1).call();
+    tr.branch(BRANCH_1_0).update(commit_v1_0);
+    RevCommit commit_v1_0_1 = tr.branch(BRANCH_1_0).commit().message("version 1.0.1").create();
+    tag(TAG_1_0_1, commit_v1_0_1);
+
     // Version 1.3
-    createAndCheckoutBranch(c3, BRANCH_1_3);
-    commit_v1_3 = git.commit().setMessage("version 1.3").call();
-    git.tag().setName(TAG_1_3).setObjectId(commit_v1_3).call();
+    tr.branch(BRANCH_1_3).update(c3);
+    commit_v1_3 = tr.branch(BRANCH_1_3).commit().message("version 1.3").create();
+    tag(TAG_1_3, commit_v1_3);
+
     // Version 2.0
-    createAndCheckoutBranch(c3, BRANCH_2_0);
-    RevCommit commit_v2_0 = git.commit().setMessage("version 2.0").call();
-    git.tag().setName(TAG_2_0).setObjectId(commit_v2_0).call();
-    RevCommit commit_v2_0_1 = git.commit().setMessage("version 2.0.1").call();
-    git.tag().setName(TAG_2_0_1).setObjectId(commit_v2_0_1).call();
+    tr.branch(BRANCH_2_0).update(c3);
+    RevCommit commit_v2_0 = tr.branch(BRANCH_2_0).commit().message("version 2.0").create();
+    tag(TAG_2_0, commit_v2_0);
+    RevCommit commit_v2_0_1 = tr.branch(BRANCH_2_0).commit().message("version 2.0.1").create();
+    tag(TAG_2_0_1, commit_v2_0_1);
 
     // Version 2.5
-    createAndCheckoutBranch(commit_v1_3, BRANCH_2_5);
-    git.merge()
-        .include(commit_v2_0_1)
-        .setCommit(false)
-        .setFastForward(FastForwardMode.NO_FF)
-        .call();
-    commit_v2_5 = git.commit().setMessage("version 2.5").call();
-    git.tag().setName(TAG_2_5).setObjectId(commit_v2_5).setAnnotated(false).call();
-    Ref ref_tag_2_5_annotated =
-        git.tag().setName(TAG_2_5_ANNOTATED).setObjectId(commit_v2_5).setAnnotated(true).call();
-    RevTag tag_2_5_annotated = revWalk.parseTag(ref_tag_2_5_annotated.getObjectId());
-    git.tag()
-        .setName(TAG_2_5_ANNOTATED_TWICE)
-        .setObjectId(tag_2_5_annotated)
-        .setAnnotated(true)
-        .call();
-  }
-
-  @Override
-  @After
-  public void tearDown() throws Exception {
-    revWalk.close();
-    super.tearDown();
+    tr.branch(BRANCH_2_5).update(commit_v1_3);
+    tr.branch(BRANCH_2_5).commit().parent(commit_v2_0_1).create(); // Merge v2.0.1
+    commit_v2_5 = tr.branch(BRANCH_2_5).commit().message("version 2.5").create();
+    tr.update(REFS_TAGS + TAG_2_5, commit_v2_5);
+    RevTag tag_2_5_annotated = tag(TAG_2_5_ANNOTATED, commit_v2_5);
+    tag(TAG_2_5_ANNOTATED_TWICE, tag_2_5_annotated);
   }
 
   @Test
@@ -171,12 +149,10 @@
   }
 
   private IncludedInResolver.Result resolve(RevCommit commit) throws Exception {
-    return IncludedInResolver.resolve(db, revWalk, commit);
+    return IncludedInResolver.resolve(tr.getRepository(), tr.getRevWalk(), commit);
   }
 
-  private void createAndCheckoutBranch(ObjectId objectId, String branchName) throws IOException {
-    String fullBranchName = "refs/heads/" + branchName;
-    super.createBranch(objectId, fullBranchName);
-    super.checkoutBranch(fullBranchName);
+  private RevTag tag(String name, RevObject dest) throws Exception {
+    return tr.update(REFS_TAGS + name, tr.tag(name, dest));
   }
 }
diff --git a/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java b/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java
index 5a067f1..6e02d61 100644
--- a/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java
+++ b/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java
@@ -24,16 +24,14 @@
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.LabelType;
+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.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.reviewdb.client.PatchSetInfo;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AuthRequest;
@@ -45,16 +43,13 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.schema.SchemaCreator;
-import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gerrit.testing.InMemoryDatabase;
+import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.testing.InMemoryModule;
 import com.google.inject.Guice;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
-import com.google.inject.Provider;
-import com.google.inject.util.Providers;
 import java.util.List;
 import org.eclipse.jgit.lib.Repository;
 import org.junit.After;
@@ -62,21 +57,21 @@
 import org.junit.Test;
 
 /** Unit tests for {@link LabelNormalizer}. */
-public class LabelNormalizerTest {
+public class LabelNormalizerTest extends GerritBaseTests {
   @Inject private AccountManager accountManager;
   @Inject private AllProjectsName allProjects;
   @Inject private GitRepositoryManager repoManager;
   @Inject private IdentifiedUser.GenericFactory userFactory;
-  @Inject private InMemoryDatabase schemaFactory;
   @Inject private LabelNormalizer norm;
   @Inject private MetaDataUpdate.User metaDataUpdateFactory;
   @Inject private ProjectCache projectCache;
   @Inject private SchemaCreator schemaCreator;
   @Inject protected ThreadLocalRequestContext requestContext;
   @Inject private ChangeNotes.Factory changeNotesFactory;
+  @Inject private ProjectConfig.Factory projectConfigFactory;
+  @Inject private GerritApi gApi;
 
   private LifecycleManager lifecycle;
-  private ReviewDb db;
   private Account.Id userId;
   private IdentifiedUser user;
   private Change change;
@@ -90,23 +85,11 @@
     lifecycle.add(injector);
     lifecycle.start();
 
-    db = schemaFactory.open();
-    schemaCreator.create(db);
+    schemaCreator.create();
     userId = accountManager.authenticate(AuthRequest.forUser("user")).getAccountId();
     user = userFactory.create(userId);
 
-    requestContext.setContext(
-        new RequestContext() {
-          @Override
-          public CurrentUser getUser() {
-            return user;
-          }
-
-          @Override
-          public Provider<ReviewDb> getReviewDbProvider() {
-            return Providers.of(db);
-          }
-        });
+    requestContext.setContext(() -> user);
 
     configureProject();
     setUpChange();
@@ -126,18 +109,14 @@
   }
 
   private void setUpChange() throws Exception {
-    change =
-        new Change(
-            new Change.Key("Iabcd1234abcd1234abcd1234abcd1234abcd1234"),
-            new Change.Id(1),
-            userId,
-            new Branch.NameKey(allProjects, "refs/heads/master"),
-            TimeUtil.nowTs());
-    PatchSetInfo ps = new PatchSetInfo(new PatchSet.Id(change.getId(), 1));
-    ps.setSubject("Test change");
-    change.setCurrentPatchSet(ps);
-    db.changes().insert(ImmutableList.of(change));
-    notes = changeNotesFactory.createChecked(db, change);
+    ChangeInput input = new ChangeInput();
+    input.project = allProjects.get();
+    input.branch = "master";
+    input.newBranch = true;
+    input.subject = "Test change";
+    ChangeInfo info = gApi.changes().create(input).get();
+    notes = changeNotesFactory.createChecked(allProjects, new Change.Id(info._number));
+    change = notes.getChange();
   }
 
   @After
@@ -146,10 +125,6 @@
       lifecycle.stop();
     }
     requestContext.setContext(null);
-    if (db != null) {
-      db.close();
-    }
-    InMemoryDatabase.drop(schemaFactory);
   }
 
   @Test
@@ -198,7 +173,7 @@
 
   private ProjectConfig loadAllProjects() throws Exception {
     try (Repository repo = repoManager.openRepository(allProjects)) {
-      ProjectConfig pc = new ProjectConfig(allProjects);
+      ProjectConfig pc = projectConfigFactory.create(allProjects);
       pc.load(repo);
       return pc;
     }
@@ -225,6 +200,6 @@
   }
 
   private static List<PatchSetApproval> list(PatchSetApproval... psas) {
-    return ImmutableList.<PatchSetApproval>copyOf(psas);
+    return ImmutableList.copyOf(psas);
   }
 }
diff --git a/javatests/com/google/gerrit/server/change/MergeabilityCacheImplTest.java b/javatests/com/google/gerrit/server/change/MergeabilityCacheImplTest.java
index e10a236..46ddbc2 100644
--- a/javatests/com/google/gerrit/server/change/MergeabilityCacheImplTest.java
+++ b/javatests/com/google/gerrit/server/change/MergeabilityCacheImplTest.java
@@ -16,16 +16,18 @@
 
 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 static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.byteString;
-import static com.google.gerrit.server.cache.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableMap;
 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 {
+public class MergeabilityCacheImplTest extends GerritBaseTests {
   @Test
   public void keySerializer() throws Exception {
     MergeabilityCacheImpl.EntryKey key =
@@ -53,10 +55,7 @@
         .isEqualTo(key);
   }
 
-  /**
-   * See {@link com.google.gerrit.server.cache.testing.SerializedClassSubject} for background and
-   * what to do if this test fails.
-   */
+  /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
   @Test
   public void keyFields() throws Exception {
     assertThatSerializedClass(MergeabilityCacheImpl.EntryKey.class)
diff --git a/javatests/com/google/gerrit/server/config/ConfigUtilTest.java b/javatests/com/google/gerrit/server/config/ConfigUtilTest.java
index 231b584..b1378ad 100644
--- a/javatests/com/google/gerrit/server/config/ConfigUtilTest.java
+++ b/javatests/com/google/gerrit/server/config/ConfigUtilTest.java
@@ -21,14 +21,14 @@
 import static java.util.concurrent.TimeUnit.MINUTES;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
-import com.google.gerrit.extensions.client.Theme;
+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 {
+public class ConfigUtilTest extends GerritBaseTests {
   private static final String SECT = "foo";
   private static final String SUB = "bar";
 
@@ -47,8 +47,6 @@
     public String s;
     public String sd;
     public String nd;
-    public Theme t;
-    public Theme td;
     public List<String> list;
     public Map<String, String> map;
 
@@ -66,8 +64,6 @@
       i.s = "foo";
       i.sd = "bar";
       // i.nd = null; // Don't need to explicitly set it; it's null by default
-      i.t = Theme.DEFAULT;
-      i.td = Theme.DEFAULT;
       return i;
     }
   }
@@ -85,7 +81,6 @@
     in.bb = true;
     in.bd = false;
     in.s = "baz";
-    in.t = Theme.MIDNIGHT;
 
     Config cfg = new Config();
     ConfigUtil.storeSection(cfg, SECT, SUB, in, d);
@@ -116,8 +111,6 @@
     assertThat(out.s).isEqualTo(in.s);
     assertThat(out.sd).isEqualTo(d.sd);
     assertThat(out.nd).isNull();
-    assertThat(out.t).isEqualTo(in.t);
-    assertThat(out.td).isEqualTo(d.td);
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/config/GitwebConfigTest.java b/javatests/com/google/gerrit/server/config/GitwebConfigTest.java
index cb6de34..bf7e4fd 100644
--- a/javatests/com/google/gerrit/server/config/GitwebConfigTest.java
+++ b/javatests/com/google/gerrit/server/config/GitwebConfigTest.java
@@ -16,9 +16,10 @@
 
 import static com.google.common.truth.Truth.assertWithMessage;
 
+import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class GitwebConfigTest {
+public class GitwebConfigTest extends GerritBaseTests {
   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 fd9c925..30fabdc 100644
--- a/javatests/com/google/gerrit/server/config/ListCapabilitiesTest.java
+++ b/javatests/com/google/gerrit/server/config/ListCapabilitiesTest.java
@@ -16,15 +16,18 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.data.GlobalCapability;
 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.Id;
+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;
@@ -33,7 +36,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class ListCapabilitiesTest {
+public class ListCapabilitiesTest extends GerritBaseTests {
   private Injector injector;
 
   @Before
@@ -43,8 +46,18 @@
           @Override
           protected void configure() {
             DynamicMap.mapOf(binder(), CapabilityDefinition.class);
+            DynamicMap.mapOf(binder(), PluginProjectPermissionDefinition.class);
             bind(CapabilityDefinition.class)
-                .annotatedWith(Exports.named("printHello"))
+                .annotatedWith(Exports.named("foo"))
+                .toInstance(
+                    new CapabilityDefinition() {
+                      @Override
+                      public String getDescription() {
+                        return "Print Hello";
+                      }
+                    });
+            bind(CapabilityDefinition.class)
+                .annotatedWith(Exports.named("bar"))
                 .toInstance(
                     new CapabilityDefinition() {
                       @Override
@@ -68,10 +81,11 @@
       assertThat(m.get(id).name).isNotNull();
     }
 
-    String pluginCapability = "gerrit-printHello";
-    assertThat(m).containsKey(pluginCapability);
-    assertThat(m.get(pluginCapability).id).isEqualTo(pluginCapability);
-    assertThat(m.get(pluginCapability).name).isEqualTo("Print Hello");
+    for (String pluginCapability : ImmutableSet.of("gerrit-foo", "gerrit-bar")) {
+      assertThat(m).containsKey(pluginCapability);
+      assertThat(m.get(pluginCapability).id).isEqualTo(pluginCapability);
+      assertThat(m.get(pluginCapability).name).isEqualTo("Print Hello");
+    }
   }
 
   @Singleton
@@ -87,7 +101,7 @@
     }
 
     @Override
-    public WithUser absentUser(Id id) {
+    public WithUser absentUser(Account.Id id) {
       throw new UnsupportedOperationException();
     }
 
diff --git a/javatests/com/google/gerrit/server/config/RepositoryConfigTest.java b/javatests/com/google/gerrit/server/config/RepositoryConfigTest.java
index 2edcf7c..2a473f4 100644
--- a/javatests/com/google/gerrit/server/config/RepositoryConfigTest.java
+++ b/javatests/com/google/gerrit/server/config/RepositoryConfigTest.java
@@ -19,7 +19,8 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.extensions.client.SubmitType;
-import com.google.gerrit.reviewdb.client.Project.NameKey;
+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;
@@ -27,7 +28,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class RepositoryConfigTest {
+public class RepositoryConfigTest extends GerritBaseTests {
 
   private Config cfg;
   private RepositoryConfig repoCfg;
@@ -41,35 +42,35 @@
   @Test
   public void defaultSubmitTypeWhenNotConfigured() {
     // Check expected value explicitly rather than depending on constant.
-    assertThat(repoCfg.getDefaultSubmitType(new NameKey("someProject")))
+    assertThat(repoCfg.getDefaultSubmitType(new Project.NameKey("someProject")))
         .isEqualTo(SubmitType.INHERIT);
   }
 
   @Test
   public void defaultSubmitTypeForStarFilter() {
     configureDefaultSubmitType("*", SubmitType.CHERRY_PICK);
-    assertThat(repoCfg.getDefaultSubmitType(new NameKey("someProject")))
+    assertThat(repoCfg.getDefaultSubmitType(new Project.NameKey("someProject")))
         .isEqualTo(SubmitType.CHERRY_PICK);
 
     configureDefaultSubmitType("*", SubmitType.FAST_FORWARD_ONLY);
-    assertThat(repoCfg.getDefaultSubmitType(new NameKey("someProject")))
+    assertThat(repoCfg.getDefaultSubmitType(new Project.NameKey("someProject")))
         .isEqualTo(SubmitType.FAST_FORWARD_ONLY);
 
     configureDefaultSubmitType("*", SubmitType.REBASE_IF_NECESSARY);
-    assertThat(repoCfg.getDefaultSubmitType(new NameKey("someProject")))
+    assertThat(repoCfg.getDefaultSubmitType(new Project.NameKey("someProject")))
         .isEqualTo(SubmitType.REBASE_IF_NECESSARY);
 
     configureDefaultSubmitType("*", SubmitType.REBASE_ALWAYS);
-    assertThat(repoCfg.getDefaultSubmitType(new NameKey("someProject")))
+    assertThat(repoCfg.getDefaultSubmitType(new Project.NameKey("someProject")))
         .isEqualTo(SubmitType.REBASE_ALWAYS);
   }
 
   @Test
   public void defaultSubmitTypeForSpecificFilter() {
     configureDefaultSubmitType("someProject", SubmitType.CHERRY_PICK);
-    assertThat(repoCfg.getDefaultSubmitType(new NameKey("someOtherProject")))
+    assertThat(repoCfg.getDefaultSubmitType(new Project.NameKey("someOtherProject")))
         .isEqualTo(RepositoryConfig.DEFAULT_SUBMIT_TYPE);
-    assertThat(repoCfg.getDefaultSubmitType(new NameKey("someProject")))
+    assertThat(repoCfg.getDefaultSubmitType(new Project.NameKey("someProject")))
         .isEqualTo(SubmitType.CHERRY_PICK);
   }
 
@@ -79,13 +80,13 @@
     configureDefaultSubmitType("somePath/*", SubmitType.CHERRY_PICK);
     configureDefaultSubmitType("*", SubmitType.MERGE_ALWAYS);
 
-    assertThat(repoCfg.getDefaultSubmitType(new NameKey("someProject")))
+    assertThat(repoCfg.getDefaultSubmitType(new Project.NameKey("someProject")))
         .isEqualTo(SubmitType.MERGE_ALWAYS);
 
-    assertThat(repoCfg.getDefaultSubmitType(new NameKey("somePath/someProject")))
+    assertThat(repoCfg.getDefaultSubmitType(new Project.NameKey("somePath/someProject")))
         .isEqualTo(SubmitType.CHERRY_PICK);
 
-    assertThat(repoCfg.getDefaultSubmitType(new NameKey("somePath/somePath/someProject")))
+    assertThat(repoCfg.getDefaultSubmitType(new Project.NameKey("somePath/somePath/someProject")))
         .isEqualTo(SubmitType.REBASE_IF_NECESSARY);
   }
 
@@ -99,14 +100,14 @@
 
   @Test
   public void ownerGroupsWhenNotConfigured() {
-    assertThat(repoCfg.getOwnerGroups(new NameKey("someProject"))).isEmpty();
+    assertThat(repoCfg.getOwnerGroups(new Project.NameKey("someProject"))).isEmpty();
   }
 
   @Test
   public void ownerGroupsForStarFilter() {
     ImmutableList<String> ownerGroups = ImmutableList.of("group1", "group2");
     configureOwnerGroups("*", ownerGroups);
-    assertThat(repoCfg.getOwnerGroups(new NameKey("someProject")))
+    assertThat(repoCfg.getOwnerGroups(new Project.NameKey("someProject")))
         .containsExactlyElementsIn(ownerGroups);
   }
 
@@ -114,8 +115,8 @@
   public void ownerGroupsForSpecificFilter() {
     ImmutableList<String> ownerGroups = ImmutableList.of("group1", "group2");
     configureOwnerGroups("someProject", ownerGroups);
-    assertThat(repoCfg.getOwnerGroups(new NameKey("someOtherProject"))).isEmpty();
-    assertThat(repoCfg.getOwnerGroups(new NameKey("someProject")))
+    assertThat(repoCfg.getOwnerGroups(new Project.NameKey("someOtherProject"))).isEmpty();
+    assertThat(repoCfg.getOwnerGroups(new Project.NameKey("someProject")))
         .containsExactlyElementsIn(ownerGroups);
   }
 
@@ -129,13 +130,13 @@
     configureOwnerGroups("somePath/*", ownerGroups2);
     configureOwnerGroups("somePath/somePath/*", ownerGroups3);
 
-    assertThat(repoCfg.getOwnerGroups(new NameKey("someProject")))
+    assertThat(repoCfg.getOwnerGroups(new Project.NameKey("someProject")))
         .containsExactlyElementsIn(ownerGroups1);
 
-    assertThat(repoCfg.getOwnerGroups(new NameKey("somePath/someProject")))
+    assertThat(repoCfg.getOwnerGroups(new Project.NameKey("somePath/someProject")))
         .containsExactlyElementsIn(ownerGroups2);
 
-    assertThat(repoCfg.getOwnerGroups(new NameKey("somePath/somePath/someProject")))
+    assertThat(repoCfg.getOwnerGroups(new Project.NameKey("somePath/somePath/someProject")))
         .containsExactlyElementsIn(ownerGroups3);
   }
 
@@ -149,22 +150,24 @@
 
   @Test
   public void basePathWhenNotConfigured() {
-    assertThat(repoCfg.getBasePath(new NameKey("someProject"))).isNull();
+    assertThat(repoCfg.getBasePath(new Project.NameKey("someProject"))).isNull();
   }
 
   @Test
   public void basePathForStarFilter() {
     String basePath = "/someAbsolutePath/someDirectory";
     configureBasePath("*", basePath);
-    assertThat(repoCfg.getBasePath(new NameKey("someProject")).toString()).isEqualTo(basePath);
+    assertThat(repoCfg.getBasePath(new Project.NameKey("someProject")).toString())
+        .isEqualTo(basePath);
   }
 
   @Test
   public void basePathForSpecificFilter() {
     String basePath = "/someAbsolutePath/someDirectory";
     configureBasePath("someProject", basePath);
-    assertThat(repoCfg.getBasePath(new NameKey("someOtherProject"))).isNull();
-    assertThat(repoCfg.getBasePath(new NameKey("someProject")).toString()).isEqualTo(basePath);
+    assertThat(repoCfg.getBasePath(new Project.NameKey("someOtherProject"))).isNull();
+    assertThat(repoCfg.getBasePath(new Project.NameKey("someProject")).toString())
+        .isEqualTo(basePath);
   }
 
   @Test
@@ -179,12 +182,14 @@
     configureBasePath("project/*", basePath3);
     configureBasePath("*", basePath4);
 
-    assertThat(repoCfg.getBasePath(new NameKey("project1")).toString()).isEqualTo(basePath1);
-    assertThat(repoCfg.getBasePath(new NameKey("project/project/someProject")).toString())
+    assertThat(repoCfg.getBasePath(new Project.NameKey("project1")).toString())
+        .isEqualTo(basePath1);
+    assertThat(repoCfg.getBasePath(new Project.NameKey("project/project/someProject")).toString())
         .isEqualTo(basePath2);
-    assertThat(repoCfg.getBasePath(new NameKey("project/someProject")).toString())
+    assertThat(repoCfg.getBasePath(new Project.NameKey("project/someProject")).toString())
         .isEqualTo(basePath3);
-    assertThat(repoCfg.getBasePath(new NameKey("someProject")).toString()).isEqualTo(basePath4);
+    assertThat(repoCfg.getBasePath(new 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 55f0374..6926052 100644
--- a/javatests/com/google/gerrit/server/config/ScheduleConfigTest.java
+++ b/javatests/com/google/gerrit/server/config/ScheduleConfigTest.java
@@ -22,6 +22,7 @@
 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;
@@ -31,7 +32,7 @@
 import org.eclipse.jgit.lib.Config;
 import org.junit.Test;
 
-public class ScheduleConfigTest {
+public class ScheduleConfigTest extends GerritBaseTests {
 
   // Friday June 13, 2014 10:00 UTC
   private static final ZonedDateTime NOW =
diff --git a/javatests/com/google/gerrit/server/edit/ChangeEditTest.java b/javatests/com/google/gerrit/server/edit/ChangeEditTest.java
index 6509c4b..4c0b5a1 100644
--- a/javatests/com/google/gerrit/server/edit/ChangeEditTest.java
+++ b/javatests/com/google/gerrit/server/edit/ChangeEditTest.java
@@ -20,9 +20,10 @@
 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 org.junit.Test;
 
-public class ChangeEditTest {
+public class ChangeEditTest extends GerritBaseTests {
   @Test
   public void changeEditRef() throws Exception {
     Account.Id accountId = new Account.Id(1000042);
diff --git a/javatests/com/google/gerrit/server/edit/tree/ChangeFileContentModificationSubject.java b/javatests/com/google/gerrit/server/edit/tree/ChangeFileContentModificationSubject.java
index 574c795..265b24e 100644
--- a/javatests/com/google/gerrit/server/edit/tree/ChangeFileContentModificationSubject.java
+++ b/javatests/com/google/gerrit/server/edit/tree/ChangeFileContentModificationSubject.java
@@ -20,7 +20,6 @@
 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.restapi.RawInput;
 import java.io.IOException;
 import java.io.InputStreamReader;
@@ -41,16 +40,16 @@
 
   public StringSubject filePath() {
     isNotNull();
-    return Truth.assertThat(actual().getFilePath()).named("filePath");
+    return check("filePath()").that(actual().getFilePath());
   }
 
   public StringSubject newContent() throws IOException {
     isNotNull();
     RawInput newContent = actual().getNewContent();
-    Truth.assertThat(newContent).named("newContent").isNotNull();
+    check("newContent()").that(newContent).isNotNull();
     String contentString =
         CharStreams.toString(
             new InputStreamReader(newContent.getInputStream(), StandardCharsets.UTF_8));
-    return Truth.assertThat(contentString).named("newContent");
+    return check("newContent()").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 59ee2b7..bd9d4df 100644
--- a/javatests/com/google/gerrit/server/edit/tree/TreeModificationSubject.java
+++ b/javatests/com/google/gerrit/server/edit/tree/TreeModificationSubject.java
@@ -24,12 +24,17 @@
 public class TreeModificationSubject extends Subject<TreeModificationSubject, TreeModification> {
 
   public static TreeModificationSubject assertThat(TreeModification treeModification) {
-    return assertAbout(TreeModificationSubject::new).that(treeModification);
+    return assertAbout(treeModifications()).that(treeModification);
+  }
+
+  private static Factory<TreeModificationSubject, TreeModification> treeModifications() {
+    return TreeModificationSubject::new;
   }
 
   public static ListSubject<TreeModificationSubject, TreeModification> assertThatList(
       List<TreeModification> treeModifications) {
-    return ListSubject.assertThat(treeModifications, TreeModificationSubject::assertThat)
+    return assertAbout(ListSubject.elements())
+        .thatCustom(treeModifications, treeModifications())
         .named("treeModifications");
   }
 
diff --git a/javatests/com/google/gerrit/server/events/EventDeserializerTest.java b/javatests/com/google/gerrit/server/events/EventDeserializerTest.java
index 997fda9..eac8d0d 100644
--- a/javatests/com/google/gerrit/server/events/EventDeserializerTest.java
+++ b/javatests/com/google/gerrit/server/events/EventDeserializerTest.java
@@ -20,11 +20,12 @@
 import com.google.common.base.Suppliers;
 import com.google.gerrit.server.data.AccountAttribute;
 import com.google.gerrit.server.data.RefUpdateAttribute;
+import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
 import org.junit.Test;
 
-public class EventDeserializerTest {
+public class EventDeserializerTest extends GerritBaseTests {
 
   @Test
   public void refUpdatedEvent() {
@@ -58,12 +59,6 @@
   }
 
   private <T> Supplier<T> createSupplier(T value) {
-    return Suppliers.memoize(
-        new Supplier<T>() {
-          @Override
-          public T get() {
-            return value;
-          }
-        });
+    return Suppliers.memoize(() -> value);
   }
 }
diff --git a/javatests/com/google/gerrit/server/events/EventJsonTest.java b/javatests/com/google/gerrit/server/events/EventJsonTest.java
index fb7ffd4..b59641d 100644
--- a/javatests/com/google/gerrit/server/events/EventJsonTest.java
+++ b/javatests/com/google/gerrit/server/events/EventJsonTest.java
@@ -577,10 +577,10 @@
 
   private Change newChange() {
     return new Change(
-        new Change.Key(CHANGE_ID),
-        new Change.Id(CHANGE_NUM),
-        new Account.Id(9999),
-        new Branch.NameKey(new Project.NameKey(PROJECT), BRANCH),
+        Change.key(CHANGE_ID),
+        Change.id(CHANGE_NUM),
+        Account.id(9999),
+        Branch.nameKey(Project.nameKey(PROJECT), BRANCH),
         TimeUtil.nowTs());
   }
 
diff --git a/javatests/com/google/gerrit/server/events/EventTypesTest.java b/javatests/com/google/gerrit/server/events/EventTypesTest.java
index c822d6c..dd5c7f9 100644
--- a/javatests/com/google/gerrit/server/events/EventTypesTest.java
+++ b/javatests/com/google/gerrit/server/events/EventTypesTest.java
@@ -16,9 +16,10 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class EventTypesTest {
+public class EventTypesTest extends GerritBaseTests {
   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 08d7082..4a1f47c 100644
--- a/javatests/com/google/gerrit/server/extensions/webui/UiActionsTest.java
+++ b/javatests/com/google/gerrit/server/extensions/webui/UiActionsTest.java
@@ -18,6 +18,8 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+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;
@@ -30,6 +32,7 @@
 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;
@@ -38,7 +41,7 @@
 import org.eclipse.jgit.lib.Repository;
 import org.junit.Test;
 
-public class UiActionsTest {
+public class UiActionsTest extends GerritBaseTests {
 
   private static class FakeForProject extends ForProject {
     private boolean allowValueQueries = true;
@@ -54,19 +57,29 @@
     }
 
     @Override
-    public void check(ProjectPermission perm) throws AuthException, PermissionBackendException {
+    public void check(CoreOrPluginProjectPermission perm)
+        throws AuthException, PermissionBackendException {
       throw new UnsupportedOperationException("not implemented");
     }
 
     @Override
-    public Set<ProjectPermission> test(Collection<ProjectPermission> permSet)
+    public <T extends CoreOrPluginProjectPermission> Set<T> test(Collection<T> permSet)
         throws PermissionBackendException {
       assertThat(allowValueQueries).isTrue();
-      return ImmutableSet.of(ProjectPermission.READ);
+      Set<T> ok = Sets.newHashSetWithExpectedSize(permSet.size());
+      for (T perm : permSet) {
+        // Allow ProjectPermission.READ, if it was requested in the input permSet. This implies
+        // that permSet has type Collection<ProjectPermission>, otherwise no permission would
+        // compare equal to READ.
+        if (perm.equals(ProjectPermission.READ)) {
+          ok.add(perm);
+        }
+      }
+      return ok;
     }
 
     @Override
-    public BooleanCondition testCond(ProjectPermission perm) {
+    public BooleanCondition testCond(CoreOrPluginProjectPermission perm) {
       return new PermissionBackendCondition.ForProject(this, perm, fakeUser());
     }
 
diff --git a/javatests/com/google/gerrit/server/fixes/FixReplacementInterpreterTest.java b/javatests/com/google/gerrit/server/fixes/FixReplacementInterpreterTest.java
index c1a65bb..cc648bf 100644
--- a/javatests/com/google/gerrit/server/fixes/FixReplacementInterpreterTest.java
+++ b/javatests/com/google/gerrit/server/fixes/FixReplacementInterpreterTest.java
@@ -26,6 +26,7 @@
 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;
@@ -33,14 +34,9 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 
-public class FixReplacementInterpreterTest {
-
-  @Rule public ExpectedException expectedException = ExpectedException.none();
-
+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);
@@ -261,7 +257,7 @@
 
     replay(fileContentUtil);
 
-    expectedException.expect(ResourceConflictException.class);
+    exception.expect(ResourceConflictException.class);
     toTreeModifications(fixReplacement);
   }
 
@@ -273,7 +269,7 @@
 
     replay(fileContentUtil);
 
-    expectedException.expect(ResourceConflictException.class);
+    exception.expect(ResourceConflictException.class);
     toTreeModifications(fixReplacement);
   }
 
@@ -285,7 +281,7 @@
 
     replay(fileContentUtil);
 
-    expectedException.expect(ResourceConflictException.class);
+    exception.expect(ResourceConflictException.class);
     toTreeModifications(fixReplacement);
   }
 
@@ -297,7 +293,7 @@
 
     replay(fileContentUtil);
 
-    expectedException.expect(ResourceConflictException.class);
+    exception.expect(ResourceConflictException.class);
     toTreeModifications(fixReplacement);
   }
 
@@ -309,7 +305,7 @@
 
     replay(fileContentUtil);
 
-    expectedException.expect(ResourceConflictException.class);
+    exception.expect(ResourceConflictException.class);
     toTreeModifications(fixReplacement);
   }
 
diff --git a/javatests/com/google/gerrit/server/fixes/LineIdentifierTest.java b/javatests/com/google/gerrit/server/fixes/LineIdentifierTest.java
index f638346..309f726 100644
--- a/javatests/com/google/gerrit/server/fixes/LineIdentifierTest.java
+++ b/javatests/com/google/gerrit/server/fixes/LineIdentifierTest.java
@@ -16,27 +16,23 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import org.junit.Rule;
+import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 
-public class LineIdentifierTest {
-
-  @Rule public ExpectedException expectedException = ExpectedException.none();
-
+public class LineIdentifierTest extends GerritBaseTests {
   @Test
   public void lineNumberMustBePositive() {
     LineIdentifier lineIdentifier = new LineIdentifier("First line\nSecond line");
-    expectedException.expect(StringIndexOutOfBoundsException.class);
-    expectedException.expectMessage("positive");
+    exception.expect(StringIndexOutOfBoundsException.class);
+    exception.expectMessage("positive");
     lineIdentifier.getStartIndexOfLine(0);
   }
 
   @Test
   public void lineNumberMustIndicateAnAvailableLine() {
     LineIdentifier lineIdentifier = new LineIdentifier("First line\nSecond line");
-    expectedException.expect(StringIndexOutOfBoundsException.class);
-    expectedException.expectMessage("Line 3 isn't available");
+    exception.expect(StringIndexOutOfBoundsException.class);
+    exception.expectMessage("Line 3 isn't available");
     lineIdentifier.getStartIndexOfLine(3);
   }
 
diff --git a/javatests/com/google/gerrit/server/fixes/StringModifierTest.java b/javatests/com/google/gerrit/server/fixes/StringModifierTest.java
index d23e928..185b58c 100644
--- a/javatests/com/google/gerrit/server/fixes/StringModifierTest.java
+++ b/javatests/com/google/gerrit/server/fixes/StringModifierTest.java
@@ -16,15 +16,11 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 
-public class StringModifierTest {
-
-  @Rule public ExpectedException expectedException = ExpectedException.none();
-
+public class StringModifierTest extends GerritBaseTests {
   private final String originalString = "This is the original, unmodified string.";
   private StringModifier stringModifier;
 
@@ -67,19 +63,19 @@
   @Test
   public void replacedPartsMustNotOverlap() {
     stringModifier.replace(0, 9, "");
-    expectedException.expect(StringIndexOutOfBoundsException.class);
+    exception.expect(StringIndexOutOfBoundsException.class);
     stringModifier.replace(8, 32, "The modified");
   }
 
   @Test
   public void startIndexMustNotBeGreaterThanEndIndex() {
-    expectedException.expect(StringIndexOutOfBoundsException.class);
+    exception.expect(StringIndexOutOfBoundsException.class);
     stringModifier.replace(10, 9, "something");
   }
 
   @Test
   public void startIndexMustNotBeNegative() {
-    expectedException.expect(StringIndexOutOfBoundsException.class);
+    exception.expect(StringIndexOutOfBoundsException.class);
     stringModifier.replace(-1, 9, "something");
   }
 
@@ -94,13 +90,13 @@
 
   @Test
   public void startIndexMustNotBeGreaterThanLengthOfString() {
-    expectedException.expect(StringIndexOutOfBoundsException.class);
+    exception.expect(StringIndexOutOfBoundsException.class);
     stringModifier.replace(originalString.length() + 1, originalString.length() + 1, "something");
   }
 
   @Test
   public void endIndexMustNotBeGreaterThanLengthOfString() {
-    expectedException.expect(StringIndexOutOfBoundsException.class);
+    exception.expect(StringIndexOutOfBoundsException.class);
     stringModifier.replace(8, originalString.length() + 1, "something");
   }
 }
diff --git a/javatests/com/google/gerrit/server/git/GroupCollectorTest.java b/javatests/com/google/gerrit/server/git/GroupCollectorTest.java
index e5e8e26..f694299 100644
--- a/javatests/com/google/gerrit/server/git/GroupCollectorTest.java
+++ b/javatests/com/google/gerrit/server/git/GroupCollectorTest.java
@@ -21,6 +21,7 @@
 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 org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
@@ -31,7 +32,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class GroupCollectorTest {
+public class GroupCollectorTest extends GerritBaseTests {
   private TestRepository<?> tr;
 
   @Before
diff --git a/javatests/com/google/gerrit/server/git/LocalDiskRepositoryManagerTest.java b/javatests/com/google/gerrit/server/git/LocalDiskRepositoryManagerTest.java
index 4e0cb0c..821a6e6b 100644
--- a/javatests/com/google/gerrit/server/git/LocalDiskRepositoryManagerTest.java
+++ b/javatests/com/google/gerrit/server/git/LocalDiskRepositoryManagerTest.java
@@ -20,13 +20,10 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.ioutil.HostPlatform;
-import com.google.gerrit.testing.TempFileUtil;
-import com.google.gwtorm.client.KeyUtil;
-import com.google.gwtorm.server.StandardKeyEncoder;
+import com.google.gerrit.testing.GerritBaseTests;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import org.easymock.EasyMockSupport;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
@@ -35,13 +32,12 @@
 import org.eclipse.jgit.lib.RepositoryCache.FileKey;
 import org.eclipse.jgit.util.FS;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
 
-public class LocalDiskRepositoryManagerTest extends EasyMockSupport {
-
-  static {
-    KeyUtil.setEncoderImpl(new StandardKeyEncoder());
-  }
+public class LocalDiskRepositoryManagerTest extends GerritBaseTests {
+  @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
 
   private Config cfg;
   private SitePaths site;
@@ -49,7 +45,7 @@
 
   @Before
   public void setUp() throws Exception {
-    site = new SitePaths(TempFileUtil.createTempDirectory().toPath());
+    site = new SitePaths(temporaryFolder.newFolder().toPath());
     site.resolve("git").toFile().mkdir();
     cfg = new Config();
     cfg.setString("gerrit", null, "basePath", "git");
diff --git a/javatests/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java b/javatests/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java
index e848fa3..fc79a6d 100644
--- a/javatests/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java
+++ b/javatests/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java
@@ -25,7 +25,6 @@
 import com.google.gerrit.server.config.RepositoryConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.testing.GerritBaseTests;
-import com.google.gerrit.testing.TempFileUtil;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -37,11 +36,14 @@
 import org.eclipse.jgit.lib.RepositoryCache;
 import org.eclipse.jgit.lib.RepositoryCache.FileKey;
 import org.eclipse.jgit.util.FS;
-import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
 
 public class MultiBaseLocalDiskRepositoryManagerTest extends GerritBaseTests {
+  @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
   private Config cfg;
   private SitePaths site;
   private MultiBaseLocalDiskRepositoryManager repoManager;
@@ -49,7 +51,7 @@
 
   @Before
   public void setUp() throws IOException {
-    site = new SitePaths(TempFileUtil.createTempDirectory().toPath());
+    site = new SitePaths(temporaryFolder.newFolder().toPath());
     site.resolve("git").toFile().mkdir();
     cfg = new Config();
     cfg.setString("gerrit", null, "basePath", "git");
@@ -59,11 +61,6 @@
     repoManager = new MultiBaseLocalDiskRepositoryManager(site, cfg, configMock);
   }
 
-  @After
-  public void tearDown() throws IOException {
-    TempFileUtil.cleanup();
-  }
-
   @Test
   public void defaultRepositoryLocation()
       throws RepositoryCaseMismatchException, RepositoryNotFoundException, IOException {
@@ -91,7 +88,7 @@
 
   @Test
   public void alternateRepositoryLocation() throws IOException {
-    Path alternateBasePath = TempFileUtil.createTempDirectory().toPath();
+    Path alternateBasePath = temporaryFolder.newFolder().toPath();
     Project.NameKey someProjectKey = new Project.NameKey("someProject");
     reset(configMock);
     expect(configMock.getBasePath(someProjectKey)).andReturn(alternateBasePath).anyTimes();
@@ -124,7 +121,7 @@
     Project.NameKey misplacedProject1 = new Project.NameKey("misplacedProject1");
     Project.NameKey misplacedProject2 = new Project.NameKey("misplacedProject2");
 
-    Path alternateBasePath = TempFileUtil.createTempDirectory().toPath();
+    Path alternateBasePath = temporaryFolder.newFolder().toPath();
 
     reset(configMock);
     expect(configMock.getBasePath(altPathProject)).andReturn(alternateBasePath).anyTimes();
diff --git a/javatests/com/google/gerrit/server/git/PureRevertCacheKeyTest.java b/javatests/com/google/gerrit/server/git/PureRevertCacheKeyTest.java
new file mode 100644
index 0000000..8c17075
--- /dev/null
+++ b/javatests/com/google/gerrit/server/git/PureRevertCacheKeyTest.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.server.git;
+
+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.server.cache.proto.Cache;
+import com.google.protobuf.ByteString;
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Test;
+
+public class PureRevertCacheKeyTest {
+  @Test
+  public void serialization() {
+    ObjectId revert = ObjectId.zeroId();
+    ObjectId original = ObjectId.fromString("aabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
+
+    byte[] serializedRevert =
+        new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    byte[] serializedOriginal =
+        byteArray(
+            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);
+    assertThat(key)
+        .isEqualTo(
+            Cache.PureRevertKeyProto.newBuilder()
+                .setProject("test")
+                .setClaimedRevert(ByteString.copyFrom(serializedRevert))
+                .setClaimedOriginal(ByteString.copyFrom(serializedOriginal))
+                .build());
+  }
+}
diff --git a/javatests/com/google/gerrit/server/git/TagSetHolderTest.java b/javatests/com/google/gerrit/server/git/TagSetHolderTest.java
index 3d3e734..87ddc75 100644
--- a/javatests/com/google/gerrit/server/git/TagSetHolderTest.java
+++ b/javatests/com/google/gerrit/server/git/TagSetHolderTest.java
@@ -16,14 +16,15 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
-import static com.google.gerrit.server.cache.testing.SerializedClassSubject.assertThatSerializedClass;
+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.server.cache.proto.Cache.TagSetHolderProto;
+import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class TagSetHolderTest {
+public class TagSetHolderTest extends GerritBaseTests {
   @Test
   public void serializerWithTagSet() throws Exception {
     TagSetHolder holder = new TagSetHolder(new Project.NameKey("project"));
diff --git a/javatests/com/google/gerrit/server/git/TagSetTest.java b/javatests/com/google/gerrit/server/git/TagSetTest.java
index 1eebe75..3ac72be 100644
--- a/javatests/com/google/gerrit/server/git/TagSetTest.java
+++ b/javatests/com/google/gerrit/server/git/TagSetTest.java
@@ -17,8 +17,8 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
 import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.byteString;
-import static com.google.gerrit.server.cache.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSortedSet;
@@ -30,6 +30,7 @@
 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;
@@ -42,7 +43,7 @@
 import org.eclipse.jgit.lib.ObjectIdOwnerMap;
 import org.junit.Test;
 
-public class TagSetTest {
+public class TagSetTest extends GerritBaseTests {
   @Test
   public void roundTripToProto() {
     HashMap<String, CachedRef> refs = new HashMap<>();
diff --git a/javatests/com/google/gerrit/server/git/meta/VersionedMetaDataTest.java b/javatests/com/google/gerrit/server/git/meta/VersionedMetaDataTest.java
index 7e12439..dedccc2 100644
--- a/javatests/com/google/gerrit/server/git/meta/VersionedMetaDataTest.java
+++ b/javatests/com/google/gerrit/server/git/meta/VersionedMetaDataTest.java
@@ -22,11 +22,12 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Streams;
 import com.google.gerrit.common.Nullable;
+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.update.RefUpdateUtil;
 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;
@@ -50,7 +51,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class VersionedMetaDataTest {
+public class VersionedMetaDataTest extends GerritBaseTests {
   // 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.
diff --git a/javatests/com/google/gerrit/server/group/db/BUILD b/javatests/com/google/gerrit/server/group/db/BUILD
index f3dd5d6..b4652c9 100644
--- a/javatests/com/google/gerrit/server/group/db/BUILD
+++ b/javatests/com/google/gerrit/server/group/db/BUILD
@@ -8,8 +8,10 @@
         "//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/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",
@@ -18,7 +20,6 @@
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//java/com/google/gerrit/truth",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/jgit/org.eclipse.jgit.junit: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 a5744d1..c9ba72e 100644
--- a/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java
+++ b/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java
@@ -16,6 +16,7 @@
 
 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.truth.OptionalSubject.assertThat;
 import static org.hamcrest.CoreMatchers.instanceOf;
 
@@ -32,9 +33,8 @@
 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 com.google.gwtorm.client.KeyUtil;
-import com.google.gwtorm.server.StandardKeyEncoder;
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.time.LocalDate;
@@ -53,18 +53,9 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 
-public class GroupConfigTest {
-  static {
-    // Necessary so that toString() methods of ReviewDb entities work correctly.
-    KeyUtil.setEncoderImpl(new StandardKeyEncoder());
-  }
-
-  @Rule public ExpectedException expectedException = ExpectedException.none();
-
+public class GroupConfigTest extends GerritBaseTests {
   private Project.NameKey projectName;
   private Repository repository;
   private TestRepository<?> testRepository;
@@ -122,8 +113,8 @@
     GroupConfig groupConfig = GroupConfig.createForNewGroup(projectName, repository, groupCreation);
 
     try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
-      expectedException.expectCause(instanceOf(ConfigInvalidException.class));
-      expectedException.expectMessage("Name of the group " + groupUuid);
+      exception.expectCause(instanceOf(ConfigInvalidException.class));
+      exception.expectMessage("Name of the group " + groupUuid);
       groupConfig.commit(metaDataUpdate);
     }
   }
@@ -135,8 +126,8 @@
     GroupConfig groupConfig = GroupConfig.createForNewGroup(projectName, repository, groupCreation);
 
     try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
-      expectedException.expectCause(instanceOf(ConfigInvalidException.class));
-      expectedException.expectMessage("Name of the group " + groupUuid);
+      exception.expectCause(instanceOf(ConfigInvalidException.class));
+      exception.expectMessage("Name of the group " + groupUuid);
       groupConfig.commit(metaDataUpdate);
     }
   }
@@ -157,8 +148,8 @@
     GroupConfig groupConfig = GroupConfig.createForNewGroup(projectName, repository, groupCreation);
 
     try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
-      expectedException.expectCause(instanceOf(ConfigInvalidException.class));
-      expectedException.expectMessage("ID of the group " + groupUuid);
+      exception.expectCause(instanceOf(ConfigInvalidException.class));
+      exception.expectMessage("ID of the group " + groupUuid);
       groupConfig.commit(metaDataUpdate);
     }
   }
@@ -236,8 +227,8 @@
     groupConfig.setGroupUpdate(groupUpdate, auditLogFormatter);
 
     try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
-      expectedException.expectCause(instanceOf(ConfigInvalidException.class));
-      expectedException.expectMessage("Owner UUID of the group " + groupUuid);
+      exception.expectCause(instanceOf(ConfigInvalidException.class));
+      exception.expectMessage("Owner UUID of the group " + groupUuid);
       groupConfig.commit(metaDataUpdate);
     }
   }
@@ -251,8 +242,8 @@
     groupConfig.setGroupUpdate(groupUpdate, auditLogFormatter);
 
     try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
-      expectedException.expectCause(instanceOf(ConfigInvalidException.class));
-      expectedException.expectMessage("Owner UUID of the group " + groupUuid);
+      exception.expectCause(instanceOf(ConfigInvalidException.class));
+      exception.expectMessage("Owner UUID of the group " + groupUuid);
       groupConfig.commit(metaDataUpdate);
     }
   }
@@ -362,8 +353,8 @@
   public void idInConfigMustBeDefined() throws Exception {
     populateGroupConfig(groupUuid, "[group]\n\tname = users\n\townerGroupUuid = owners\n");
 
-    expectedException.expect(ConfigInvalidException.class);
-    expectedException.expectMessage("ID of the group " + groupUuid);
+    exception.expect(ConfigInvalidException.class);
+    exception.expectMessage("ID of the group " + groupUuid);
     GroupConfig.loadForGroup(projectName, repository, groupUuid);
   }
 
@@ -372,8 +363,8 @@
     populateGroupConfig(
         groupUuid, "[group]\n\tname = users\n\tid = -5\n\townerGroupUuid = owners\n");
 
-    expectedException.expect(ConfigInvalidException.class);
-    expectedException.expectMessage("ID of the group " + groupUuid);
+    exception.expect(ConfigInvalidException.class);
+    exception.expectMessage("ID of the group " + groupUuid);
     GroupConfig.loadForGroup(projectName, repository, groupUuid);
   }
 
@@ -398,8 +389,8 @@
   public void ownerGroupUuidInConfigMustBeDefined() throws Exception {
     populateGroupConfig(groupUuid, "[group]\n\tname = users\n\tid = 42\n");
 
-    expectedException.expect(ConfigInvalidException.class);
-    expectedException.expectMessage("Owner UUID of the group " + groupUuid);
+    exception.expect(ConfigInvalidException.class);
+    exception.expectMessage("Owner UUID of the group " + groupUuid);
     GroupConfig.loadForGroup(projectName, repository, groupUuid);
   }
 
@@ -452,8 +443,8 @@
     populateGroupConfig(groupUuid, "[group]\n\tname=users\n\tid = 42\n\townerGroupUuid = owners\n");
     populateMembersFile(groupUuid, "One");
 
-    expectedException.expect(ConfigInvalidException.class);
-    expectedException.expectMessage("Invalid file members");
+    exception.expect(ConfigInvalidException.class);
+    exception.expectMessage("Invalid file members");
     loadGroup(groupUuid);
   }
 
@@ -462,8 +453,8 @@
     populateGroupConfig(groupUuid, "[group]\n\tname=users\n\tid = 42\n\townerGroupUuid = owners\n");
     populateMembersFile(groupUuid, "1\t2");
 
-    expectedException.expect(ConfigInvalidException.class);
-    expectedException.expectMessage("Invalid file members");
+    exception.expect(ConfigInvalidException.class);
+    exception.expectMessage("Invalid file members");
     loadGroup(groupUuid);
   }
 
@@ -554,8 +545,8 @@
     groupConfig.setGroupUpdate(groupUpdate, auditLogFormatter);
 
     try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
-      expectedException.expectCause(instanceOf(ConfigInvalidException.class));
-      expectedException.expectMessage("Name of the group " + groupUuid);
+      exception.expectCause(instanceOf(ConfigInvalidException.class));
+      exception.expectMessage("Name of the group " + groupUuid);
       groupConfig.commit(metaDataUpdate);
     }
   }
@@ -570,8 +561,8 @@
     groupConfig.setGroupUpdate(groupUpdate, auditLogFormatter);
 
     try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
-      expectedException.expectCause(instanceOf(ConfigInvalidException.class));
-      expectedException.expectMessage("Name of the group " + groupUuid);
+      exception.expectCause(instanceOf(ConfigInvalidException.class));
+      exception.expectMessage("Name of the group " + groupUuid);
       groupConfig.commit(metaDataUpdate);
     }
   }
@@ -637,8 +628,8 @@
     groupConfig.setGroupUpdate(groupUpdate, auditLogFormatter);
 
     try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
-      expectedException.expectCause(instanceOf(ConfigInvalidException.class));
-      expectedException.expectMessage("Owner UUID of the group " + groupUuid);
+      exception.expectCause(instanceOf(ConfigInvalidException.class));
+      exception.expectMessage("Owner UUID of the group " + groupUuid);
       groupConfig.commit(metaDataUpdate);
     }
   }
@@ -653,8 +644,8 @@
     groupConfig.setGroupUpdate(groupUpdate, auditLogFormatter);
 
     try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
-      expectedException.expectCause(instanceOf(ConfigInvalidException.class));
-      expectedException.expectMessage("Owner UUID of the group " + groupUuid);
+      exception.expectCause(instanceOf(ConfigInvalidException.class));
+      exception.expectMessage("Owner UUID of the group " + groupUuid);
       groupConfig.commit(metaDataUpdate);
     }
   }
@@ -1683,6 +1674,6 @@
 
   private static OptionalSubject<InternalGroupSubject, InternalGroup> assertThatGroup(
       Optional<InternalGroup> loadedGroup) {
-    return assertThat(loadedGroup, InternalGroupSubject::assertThat);
+    return assertThat(loadedGroup, internalGroups());
   }
 }
diff --git a/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java b/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
index 42d01e2..2a8d551 100644
--- a/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
+++ b/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
@@ -16,7 +16,9 @@
 
 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.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.truth.OptionalSubject.assertThat;
 import static java.nio.charset.StandardCharsets.UTF_8;
@@ -25,8 +27,10 @@
 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.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;
@@ -34,15 +38,12 @@
 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.update.RefUpdateUtil;
 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;
 import com.google.gerrit.truth.OptionalSubject;
-import com.google.gwtorm.client.KeyUtil;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
-import com.google.gwtorm.server.StandardKeyEncoder;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.List;
@@ -66,21 +67,13 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 
-public class GroupNameNotesTest {
-  static {
-    KeyUtil.setEncoderImpl(new StandardKeyEncoder());
-  }
-
+public class GroupNameNotesTest extends GerritBaseTests {
   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");
 
-  @Rule public ExpectedException expectedException = ExpectedException.none();
-
   private final AccountGroup.UUID groupUuid = new AccountGroup.UUID("users-XYZ");
   private final AccountGroup.NameKey groupName = new AccountGroup.NameKey("users");
 
@@ -112,13 +105,13 @@
 
   @Test
   public void uuidOfNewGroupMustNotBeNull() throws Exception {
-    expectedException.expect(NullPointerException.class);
+    exception.expect(NullPointerException.class);
     GroupNameNotes.forNewGroup(allUsersName, repo, null, groupName);
   }
 
   @Test
   public void nameOfNewGroupMustNotBeNull() throws Exception {
-    expectedException.expect(NullPointerException.class);
+    exception.expect(NullPointerException.class);
     GroupNameNotes.forNewGroup(allUsersName, repo, groupUuid, null);
   }
 
@@ -136,8 +129,8 @@
     createGroup(groupUuid, groupName);
 
     AccountGroup.UUID anotherGroupUuid = new AccountGroup.UUID("AnotherGroup");
-    expectedException.expect(OrmDuplicateKeyException.class);
-    expectedException.expectMessage(groupName.get());
+    exception.expect(DuplicateKeyException.class);
+    exception.expectMessage(groupName.get());
     GroupNameNotes.forNewGroup(allUsersName, repo, anotherGroupUuid, groupName);
   }
 
@@ -181,7 +174,7 @@
   public void groupCannotBeRenamedToNull() throws Exception {
     createGroup(groupUuid, groupName);
 
-    expectedException.expect(NullPointerException.class);
+    exception.expect(NullPointerException.class);
     GroupNameNotes.forRename(allUsersName, repo, groupUuid, groupName, null);
   }
 
@@ -190,7 +183,7 @@
     createGroup(groupUuid, groupName);
 
     AccountGroup.NameKey anotherName = new AccountGroup.NameKey("admins");
-    expectedException.expect(NullPointerException.class);
+    exception.expect(NullPointerException.class);
     GroupNameNotes.forRename(allUsersName, repo, groupUuid, null, anotherName);
   }
 
@@ -200,8 +193,8 @@
 
     AccountGroup.NameKey anotherOldName = new AccountGroup.NameKey("contributors");
     AccountGroup.NameKey anotherName = new AccountGroup.NameKey("admins");
-    expectedException.expect(ConfigInvalidException.class);
-    expectedException.expectMessage(anotherOldName.get());
+    exception.expect(ConfigInvalidException.class);
+    exception.expectMessage(anotherOldName.get());
     GroupNameNotes.forRename(allUsersName, repo, groupUuid, anotherOldName, anotherName);
   }
 
@@ -212,8 +205,8 @@
     AccountGroup.NameKey anotherGroupName = new AccountGroup.NameKey("admins");
     createGroup(anotherGroupUuid, anotherGroupName);
 
-    expectedException.expect(OrmDuplicateKeyException.class);
-    expectedException.expectMessage(anotherGroupName.get());
+    exception.expect(DuplicateKeyException.class);
+    exception.expectMessage(anotherGroupName.get());
     GroupNameNotes.forRename(allUsersName, repo, groupUuid, groupName, anotherGroupName);
   }
 
@@ -222,7 +215,7 @@
     createGroup(groupUuid, groupName);
 
     AccountGroup.NameKey anotherName = new AccountGroup.NameKey("admins");
-    expectedException.expect(NullPointerException.class);
+    exception.expect(NullPointerException.class);
     GroupNameNotes.forRename(allUsersName, repo, null, groupName, anotherName);
   }
 
@@ -232,8 +225,8 @@
 
     AccountGroup.UUID anotherGroupUuid = new AccountGroup.UUID("admins-ABC");
     AccountGroup.NameKey anotherName = new AccountGroup.NameKey("admins");
-    expectedException.expect(ConfigInvalidException.class);
-    expectedException.expectMessage(groupUuid.get());
+    exception.expect(ConfigInvalidException.class);
+    exception.expectMessage(groupUuid.get());
     GroupNameNotes.forRename(allUsersName, repo, anotherGroupUuid, groupName, anotherName);
   }
 
@@ -260,7 +253,7 @@
     createGroup(anotherGroupUuid, anotherName);
 
     ImmutableList<CommitInfo> commitsAfterFurtherGroup = log();
-    assertThatCommits(commitsAfterFurtherGroup).containsAllIn(commitsAfterCreation);
+    assertThatCommits(commitsAfterFurtherGroup).containsAtLeastElementsIn(commitsAfterCreation);
     assertThatCommits(commitsAfterFurtherGroup).lastElement().isNotIn(commitsAfterCreation);
   }
 
@@ -273,7 +266,7 @@
     renameGroup(groupUuid, groupName, anotherName);
 
     ImmutableList<CommitInfo> commitsAfterRename = log();
-    assertThatCommits(commitsAfterRename).containsAllIn(commitsAfterCreation);
+    assertThatCommits(commitsAfterRename).containsAtLeastElementsIn(commitsAfterCreation);
     assertThatCommits(commitsAfterRename).lastElement().isNotIn(commitsAfterCreation);
   }
 
@@ -431,35 +424,36 @@
     GroupReference g1 = newGroup("a");
     GroupReference g2 = newGroup("b");
 
-    TestRepository<?> tr = new TestRepository<>(repo);
-    ObjectId k1 = getNoteKey(g1);
-    ObjectId k2 = getNoteKey(g2);
-    ObjectId k3 = GroupNameNotes.getNoteKey(new AccountGroup.NameKey("c"));
-    PersonIdent ident = newPersonIdent();
-    ObjectId origCommitId =
-        tr.branch(REFS_GROUPNAMES)
-            .commit()
-            .message("Prepopulate group name")
-            .author(ident)
-            .committer(ident)
-            .add(k1.name(), "[group]\n\tuuid = a-1\n\tname = a\nanotherKey = foo\n")
-            .add(k2.name(), "[group]\n\tuuid = a-1\n\tname = b\n")
-            .add(k3.name(), "[group]\n\tuuid = c-3\n\tname = c\n")
-            .create()
-            .copy();
+    try (TestRepository<?> tr = new TestRepository<>(repo)) {
+      ObjectId k1 = getNoteKey(g1);
+      ObjectId k2 = getNoteKey(g2);
+      ObjectId k3 = GroupNameNotes.getNoteKey(new AccountGroup.NameKey("c"));
+      PersonIdent ident = newPersonIdent();
+      ObjectId origCommitId =
+          tr.branch(REFS_GROUPNAMES)
+              .commit()
+              .message("Prepopulate group name")
+              .author(ident)
+              .committer(ident)
+              .add(k1.name(), "[group]\n\tuuid = a-1\n\tname = a\nanotherKey = foo\n")
+              .add(k2.name(), "[group]\n\tuuid = a-1\n\tname = b\n")
+              .add(k3.name(), "[group]\n\tuuid = c-3\n\tname = c\n")
+              .create()
+              .copy();
 
-    ident = newPersonIdent();
-    updateAllGroups(ident, g1, g2);
+      ident = newPersonIdent();
+      updateAllGroups(ident, g1, g2);
 
-    assertThat(GroupNameNotes.loadAllGroups(repo)).containsExactly(g1, g2);
+      assertThat(GroupNameNotes.loadAllGroups(repo)).containsExactly(g1, g2);
 
-    ImmutableList<CommitInfo> log = log();
-    assertThat(log).hasSize(2);
-    assertThat(log.get(0)).commit().isEqualTo(origCommitId.name());
+      ImmutableList<CommitInfo> log = log();
+      assertThat(log).hasSize(2);
+      assertThat(log.get(0)).commit().isEqualTo(origCommitId.name());
 
-    assertThat(log.get(1)).message().isEqualTo("Store 2 group names");
-    assertThat(log.get(1)).author().matches(ident);
-    assertThat(log.get(1)).committer().matches(ident);
+      assertThat(log.get(1)).message().isEqualTo("Store 2 group names");
+      assertThat(log.get(1)).author().matches(ident);
+      assertThat(log.get(1)).committer().matches(ident);
+    }
 
     // Old note content was overwritten.
     assertThat(readNameNote(g1)).isEqualTo("[group]\n\tuuid = a-1\n\tname = a\n");
@@ -593,11 +587,11 @@
 
   private static OptionalSubject<GroupReferenceSubject, GroupReference> assertThatGroup(
       Optional<GroupReference> group) {
-    return assertThat(group, GroupReferenceSubject::assertThat);
+    return assertThat(group, groupReferences());
   }
 
   private static ListSubject<CommitInfoSubject, CommitInfo> assertThatCommits(
       List<CommitInfo> commits) {
-    return ListSubject.assertThat(commits, CommitInfoSubject::assertThat);
+    return ListSubject.assertThat(commits, commits());
   }
 }
diff --git a/javatests/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java b/javatests/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java
index 53994a6..fd23da3 100644
--- a/javatests/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java
+++ b/javatests/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java
@@ -68,7 +68,7 @@
   public void nonIndexPredicate() throws Exception {
     Predicate<ChangeData> in = parse("foo:a");
     Predicate<ChangeData> out = rewrite(in);
-    assertThat(AndChangeSource.class).isSameAs(out.getClass());
+    assertThat(AndChangeSource.class).isSameInstanceAs(out.getClass());
     assertThat(out.getChildren())
         .containsExactly(
             query(Predicate.or(ChangeStatusPredicate.open(), ChangeStatusPredicate.closed())), in)
@@ -85,7 +85,7 @@
   public void nonIndexPredicates() throws Exception {
     Predicate<ChangeData> in = parse("foo:a OR foo:b");
     Predicate<ChangeData> out = rewrite(in);
-    assertThat(AndChangeSource.class).isSameAs(out.getClass());
+    assertThat(AndChangeSource.class).isSameInstanceAs(out.getClass());
     assertThat(out.getChildren())
         .containsExactly(
             query(Predicate.or(ChangeStatusPredicate.open(), ChangeStatusPredicate.closed())), in)
@@ -96,7 +96,7 @@
   public void oneIndexPredicate() throws Exception {
     Predicate<ChangeData> in = parse("foo:a file:b");
     Predicate<ChangeData> out = rewrite(in);
-    assertThat(AndChangeSource.class).isSameAs(out.getClass());
+    assertThat(AndChangeSource.class).isSameInstanceAs(out.getClass());
     assertThat(out.getChildren()).containsExactly(query(in.getChild(1)), in.getChild(0)).inOrder();
   }
 
@@ -110,7 +110,7 @@
   public void threeLevelTreeWithSomeIndexPredicates() throws Exception {
     Predicate<ChangeData> in = parse("-foo:a (file:b OR file:c)");
     Predicate<ChangeData> out = rewrite(in);
-    assertThat(out.getClass()).isSameAs(AndChangeSource.class);
+    assertThat(out.getClass()).isSameInstanceAs(AndChangeSource.class);
     assertThat(out.getChildren()).containsExactly(query(in.getChild(1)), in.getChild(0)).inOrder();
   }
 
@@ -118,7 +118,7 @@
   public void multipleIndexPredicates() throws Exception {
     Predicate<ChangeData> in = parse("file:a OR foo:b OR file:c OR foo:d");
     Predicate<ChangeData> out = rewrite(in);
-    assertThat(out.getClass()).isSameAs(OrSource.class);
+    assertThat(out.getClass()).isSameInstanceAs(OrSource.class);
     assertThat(out.getChildren())
         .containsExactly(query(or(in.getChild(0), in.getChild(2))), in.getChild(1), in.getChild(3))
         .inOrder();
@@ -128,7 +128,7 @@
   public void indexAndNonIndexPredicates() throws Exception {
     Predicate<ChangeData> in = parse("status:new bar:p file:a");
     Predicate<ChangeData> out = rewrite(in);
-    assertThat(AndChangeSource.class).isSameAs(out.getClass());
+    assertThat(AndChangeSource.class).isSameInstanceAs(out.getClass());
     assertThat(out.getChildren())
         .containsExactly(query(and(in.getChild(0), in.getChild(2))), in.getChild(1))
         .inOrder();
@@ -255,7 +255,7 @@
   }
 
   private static QueryOptions options(int start, int limit) {
-    return IndexedChangeQuery.createOptions(CONFIG, start, limit, ImmutableSet.<String>of());
+    return IndexedChangeQuery.createOptions(CONFIG, start, limit, ImmutableSet.of());
   }
 
   private Set<Change.Status> status(String query) throws QueryParseException {
diff --git a/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java b/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java
index d4ecb6d..34c5717 100644
--- a/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java
+++ b/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java
@@ -15,23 +15,20 @@
 package com.google.gerrit.server.index.change;
 
 import com.google.common.collect.ImmutableList;
-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;
 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 com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
 import org.junit.Ignore;
 
 @Ignore
 public class FakeChangeIndex implements ChangeIndex {
-  static final Schema<ChangeData> V1 =
-      new Schema<>(1, ImmutableList.<FieldDef<ChangeData, ?>>of(ChangeField.STATUS));
+  static final Schema<ChangeData> V1 = new Schema<>(1, ImmutableList.of(ChangeField.STATUS));
 
   static final Schema<ChangeData> V2 =
       new Schema<>(2, ImmutableList.of(ChangeField.STATUS, ChangeField.PATH, ChangeField.UPDATED));
@@ -54,12 +51,12 @@
     }
 
     @Override
-    public ResultSet<ChangeData> read() throws OrmException {
+    public ResultSet<ChangeData> read() {
       throw new UnsupportedOperationException();
     }
 
     @Override
-    public ResultSet<FieldBundle> readRaw() throws OrmException {
+    public ResultSet<FieldBundle> readRaw() {
       throw new UnsupportedOperationException("not implemented");
     }
 
diff --git a/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java b/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
index b525504..0753127 100644
--- a/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
+++ b/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
@@ -24,10 +24,10 @@
 public class FakeQueryBuilder extends ChangeQueryBuilder {
   FakeQueryBuilder(ChangeIndexCollection indexes) {
     super(
-        new FakeQueryBuilder.Definition<>(FakeQueryBuilder.class),
+        new ChangeQueryBuilder.Definition<>(FakeQueryBuilder.class),
         new ChangeQueryBuilder.Arguments(
             null, null, null, null, null, null, null, null, null, null, null, null, null, null,
-            null, null, null, null, null, indexes, null, null, null, null, null, null, null, null));
+            null, null, null, null, indexes, null, null, null, null, null, null, null));
   }
 
   @Operator
diff --git a/javatests/com/google/gerrit/server/index/change/StalenessCheckerTest.java b/javatests/com/google/gerrit/server/index/change/StalenessCheckerTest.java
index 51bda66..a38eabe 100644
--- a/javatests/com/google/gerrit/server/index/change/StalenessCheckerTest.java
+++ b/javatests/com/google/gerrit/server/index/change/StalenessCheckerTest.java
@@ -17,7 +17,6 @@
 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.TestChanges.newChange;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.stream.Collectors.toList;
 
@@ -25,16 +24,12 @@
 import com.google.common.collect.ImmutableSetMultimap;
 import com.google.common.collect.ListMultimap;
 import com.google.gerrit.index.RefState;
-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.git.GitRepositoryManager;
 import com.google.gerrit.server.index.change.StalenessChecker.RefStatePattern;
-import com.google.gerrit.server.notedb.NoteDbChangeState;
 import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.testing.InMemoryRepositoryManager;
-import com.google.gwtorm.protobuf.CodecFactory;
-import com.google.gwtorm.protobuf.ProtobufCodec;
 import java.util.stream.Stream;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.ObjectId;
@@ -51,8 +46,6 @@
 
   private static final Change.Id C = new Change.Id(1234);
 
-  private static final ProtobufCodec<Change> CHANGE_CODEC = CodecFactory.encoder(Change.class);
-
   private GitRepositoryManager repoManager;
   private Repository r1;
   private Repository r2;
@@ -316,29 +309,7 @@
         .isFalse();
   }
 
-  @Test
-  public void reviewDbChangeIsStale() throws Exception {
-    Change indexChange = newChange(P1, new Account.Id(1));
-    indexChange.setNoteDbState(SHA1);
-
-    // Change is missing from ReviewDb but present in index.
-    assertThat(StalenessChecker.reviewDbChangeIsStale(indexChange, null)).isTrue();
-
-    // Change differs only in primary storage.
-    Change noteDbPrimary = clone(indexChange);
-    noteDbPrimary.setNoteDbState(NoteDbChangeState.NOTE_DB_PRIMARY_STATE);
-    assertThat(StalenessChecker.reviewDbChangeIsStale(indexChange, noteDbPrimary)).isTrue();
-
-    assertThat(StalenessChecker.reviewDbChangeIsStale(indexChange, clone(indexChange))).isFalse();
-
-    // Can't easily change row version to check true case.
-  }
-
   private static Iterable<byte[]> byteArrays(String... strs) {
     return Stream.of(strs).map(s -> s != null ? s.getBytes(UTF_8) : null).collect(toList());
   }
-
-  private static Change clone(Change change) {
-    return CHANGE_CODEC.decode(CHANGE_CODEC.encodeToByteArray(change));
-  }
 }
diff --git a/javatests/com/google/gerrit/server/ioutil/BUILD b/javatests/com/google/gerrit/server/ioutil/BUILD
index ac9530f..ef02243 100644
--- a/javatests/com/google/gerrit/server/ioutil/BUILD
+++ b/javatests/com/google/gerrit/server/ioutil/BUILD
@@ -10,6 +10,7 @@
     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 fae8559..c4f32c7 100644
--- a/javatests/com/google/gerrit/server/ioutil/BasicSerializationTest.java
+++ b/javatests/com/google/gerrit/server/ioutil/BasicSerializationTest.java
@@ -23,13 +23,14 @@
 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 {
+public class BasicSerializationTest extends GerritBaseTests {
   @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 fe642ba..9f5e60a 100644
--- a/javatests/com/google/gerrit/server/ioutil/ColumnFormatterTest.java
+++ b/javatests/com/google/gerrit/server/ioutil/ColumnFormatterTest.java
@@ -14,12 +14,13 @@
 
 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 {
+public class ColumnFormatterTest extends GerritBaseTests {
   /**
    * 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 9bb6951..40fd71f 100644
--- a/javatests/com/google/gerrit/server/ioutil/HexFormatTest.java
+++ b/javatests/com/google/gerrit/server/ioutil/HexFormatTest.java
@@ -16,9 +16,10 @@
 
 import static org.junit.Assert.assertEquals;
 
+import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class HexFormatTest {
+public class HexFormatTest extends GerritBaseTests {
 
   @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 3043985..33b1c4f 100644
--- a/javatests/com/google/gerrit/server/ioutil/RegexListSearcherTest.java
+++ b/javatests/com/google/gerrit/server/ioutil/RegexListSearcherTest.java
@@ -18,10 +18,11 @@
 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 {
+public class RegexListSearcherTest extends GerritBaseTests {
   private static final ImmutableList<String> EMPTY = ImmutableList.of();
 
   @Test
diff --git a/javatests/com/google/gerrit/server/ioutil/StringUtilTest.java b/javatests/com/google/gerrit/server/ioutil/StringUtilTest.java
index 04f806d..817b317 100644
--- a/javatests/com/google/gerrit/server/ioutil/StringUtilTest.java
+++ b/javatests/com/google/gerrit/server/ioutil/StringUtilTest.java
@@ -16,9 +16,10 @@
 
 import static org.junit.Assert.assertEquals;
 
+import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class StringUtilTest {
+public class StringUtilTest extends GerritBaseTests {
   /** 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 5117c01..463decf 100644
--- a/javatests/com/google/gerrit/server/logging/LoggingContextAwareExecutorServiceTest.java
+++ b/javatests/com/google/gerrit/server/logging/LoggingContextAwareExecutorServiceTest.java
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.truth.Expect;
+import com.google.gerrit.testing.GerritBaseTests;
 import java.util.SortedMap;
 import java.util.SortedSet;
 import java.util.concurrent.ExecutorService;
@@ -24,7 +25,7 @@
 import org.junit.Rule;
 import org.junit.Test;
 
-public class LoggingContextAwareExecutorServiceTest {
+public class LoggingContextAwareExecutorServiceTest extends GerritBaseTests {
   @Rule public final Expect expect = Expect.create();
 
   @Test
diff --git a/javatests/com/google/gerrit/server/logging/MutableTagsTest.java b/javatests/com/google/gerrit/server/logging/MutableTagsTest.java
index 4fadbb4..113f26c 100644
--- a/javatests/com/google/gerrit/server/logging/MutableTagsTest.java
+++ b/javatests/com/google/gerrit/server/logging/MutableTagsTest.java
@@ -20,13 +20,14 @@
 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 {
+public class MutableTagsTest extends GerritBaseTests {
   private MutableTags tags;
 
   @Before
diff --git a/javatests/com/google/gerrit/server/logging/TraceContextTest.java b/javatests/com/google/gerrit/server/logging/TraceContextTest.java
index 044d237..19b2eeb 100644
--- a/javatests/com/google/gerrit/server/logging/TraceContextTest.java
+++ b/javatests/com/google/gerrit/server/logging/TraceContextTest.java
@@ -19,13 +19,14 @@
 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 {
+public class TraceContextTest extends GerritBaseTests {
   @After
   public void cleanup() {
     LoggingContext.getInstance().clearTags();
diff --git a/javatests/com/google/gerrit/server/mail/send/CommentFormatterTest.java b/javatests/com/google/gerrit/server/mail/send/CommentFormatterTest.java
index f4fbc78..78116ed 100644
--- a/javatests/com/google/gerrit/server/mail/send/CommentFormatterTest.java
+++ b/javatests/com/google/gerrit/server/mail/send/CommentFormatterTest.java
@@ -20,10 +20,11 @@
 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 {
+public class CommentFormatterTest extends GerritBaseTests {
   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 6b6632c..0682bb3 100644
--- a/javatests/com/google/gerrit/server/mail/send/CommentSenderTest.java
+++ b/javatests/com/google/gerrit/server/mail/send/CommentSenderTest.java
@@ -16,13 +16,13 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.testing.GerritBaseTests;
 import java.util.Collections;
 import org.junit.Test;
 
-public class CommentSenderTest {
+public class CommentSenderTest extends GerritBaseTests {
   private static class TestSender extends CommentSender {
-    TestSender() throws OrmException {
+    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 75923b4..537ebff 100644
--- a/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
+++ b/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
@@ -28,6 +28,7 @@
 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;
@@ -36,7 +37,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class FromAddressGeneratorProviderTest {
+public class FromAddressGeneratorProviderTest extends GerritBaseTests {
   private Config config;
   private PersonIdent ident;
   private AccountCache accountCache;
diff --git a/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
index d00f96b..a1f318f 100644
--- a/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
@@ -28,7 +28,6 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
@@ -43,7 +42,7 @@
 import com.google.gerrit.server.config.AnonymousCowardNameProvider;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.DefaultUrlFormatter;
-import com.google.gerrit.server.config.DisableReverseDnsLookup;
+import com.google.gerrit.server.config.EnableReverseDnsLookup;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.GerritServerId;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
@@ -58,12 +57,9 @@
 import com.google.gerrit.testing.InMemoryRepositoryManager;
 import com.google.gerrit.testing.TestChanges;
 import com.google.gerrit.testing.TestTimeUtil;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Guice;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
-import com.google.inject.TypeLiteral;
 import com.google.inject.util.Providers;
 import java.sql.Timestamp;
 import java.util.TimeZone;
@@ -137,11 +133,11 @@
                 install(new GitModule());
 
                 install(new DefaultUrlFormatter.Module());
-                install(NoteDbModule.forTest(testConfig));
+                install(NoteDbModule.forTest());
                 bind(AllUsersName.class).toProvider(AllUsersNameProvider.class);
                 bind(String.class).annotatedWith(GerritServerId.class).toInstance("gerrit");
                 bind(GitRepositoryManager.class).toInstance(repoManager);
-                bind(ProjectCache.class).toProvider(Providers.<ProjectCache>of(null));
+                bind(ProjectCache.class).toProvider(Providers.of(null));
                 bind(Config.class).annotatedWith(GerritServerConfig.class).toInstance(testConfig);
                 bind(String.class)
                     .annotatedWith(AnonymousCowardName.class)
@@ -150,8 +146,8 @@
                     .annotatedWith(CanonicalWebUrl.class)
                     .toInstance("http://localhost:8080/");
                 bind(Boolean.class)
-                    .annotatedWith(DisableReverseDnsLookup.class)
-                    .toInstance(Boolean.FALSE);
+                    .annotatedWith(EnableReverseDnsLookup.class)
+                    .toInstance(Boolean.TRUE);
                 bind(Realm.class).to(FakeRealm.class);
                 bind(GroupBackend.class).to(SystemGroupBackend.class).in(SINGLETON);
                 bind(AccountCache.class).toInstance(accountCache);
@@ -160,23 +156,6 @@
                     .toInstance(serverIdent);
                 bind(GitReferenceUpdated.class).toInstance(GitReferenceUpdated.DISABLED);
                 bind(MetricMaker.class).to(DisabledMetricMaker.class);
-                bind(ReviewDb.class).toProvider(Providers.<ReviewDb>of(null));
-                MutableNotesMigration migration = MutableNotesMigration.newDisabled();
-                migration.setFrom(NotesMigrationState.FINAL);
-                bind(MutableNotesMigration.class).toInstance(migration);
-                bind(NotesMigration.class).to(MutableNotesMigration.class);
-
-                // Tests don't support ReviewDb at all, but bindings are required via NoteDbModule.
-                bind(new TypeLiteral<SchemaFactory<ReviewDb>>() {})
-                    .toInstance(
-                        () -> {
-                          throw new UnsupportedOperationException();
-                        });
-                bind(ChangeBundleReader.class)
-                    .toInstance(
-                        (db, id) -> {
-                          throw new UnsupportedOperationException();
-                        });
               }
             });
 
@@ -201,7 +180,7 @@
 
   protected Change newChange(boolean workInProgress) throws Exception {
     Change c = TestChanges.newChange(project, changeOwner.getAccountId());
-    ChangeUpdate u = newUpdate(c, changeOwner);
+    ChangeUpdate u = newUpdateForNewChange(c, changeOwner);
     u.setChangeId(c.getKey().get());
     u.setBranch(c.getDest().get());
     u.setWorkInProgress(workInProgress);
@@ -217,15 +196,24 @@
     return newChange(false);
   }
 
+  protected ChangeUpdate newUpdateForNewChange(Change c, CurrentUser user) throws Exception {
+    return newUpdate(c, user, false);
+  }
+
   protected ChangeUpdate newUpdate(Change c, CurrentUser user) throws Exception {
-    ChangeUpdate update = TestChanges.newUpdate(injector, c, user);
+    return newUpdate(c, user, true);
+  }
+
+  protected ChangeUpdate newUpdate(Change c, CurrentUser user, boolean shouldExist)
+      throws Exception {
+    ChangeUpdate update = TestChanges.newUpdate(injector, c, user, shouldExist);
     update.setPatchSetId(c.currentPatchSetId());
     update.setAllowWriteToNewRef(true);
     return update;
   }
 
-  protected ChangeNotes newNotes(Change c) throws OrmException {
-    return new ChangeNotes(args, c).load();
+  protected ChangeNotes newNotes(Change c) {
+    return new ChangeNotes(args, c, true, null).load();
   }
 
   protected static SubmitRecord submitRecord(
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeBundleTest.java b/javatests/com/google/gerrit/server/notedb/ChangeBundleTest.java
deleted file mode 100644
index fc2a272..0000000
--- a/javatests/com/google/gerrit/server/notedb/ChangeBundleTest.java
+++ /dev/null
@@ -1,1976 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.server.notedb.ChangeBundle.Source.NOTE_DB;
-import static com.google.gerrit.server.notedb.ChangeBundle.Source.REVIEW_DB;
-import static com.google.gerrit.server.notedb.ReviewerStateInternal.CC;
-import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
-import static com.google.gerrit.server.util.time.TimeUtil.truncateToSecond;
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
-import static java.util.concurrent.TimeUnit.SECONDS;
-
-import com.google.common.collect.HashBasedTable;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Table;
-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.LabelId;
-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.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.server.ReviewerSet;
-import com.google.gerrit.server.notedb.rebuild.ChangeRebuilderImpl;
-import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gerrit.testing.GerritBaseTests;
-import com.google.gerrit.testing.TestChanges;
-import com.google.gerrit.testing.TestTimeUtil;
-import com.google.gwtorm.protobuf.CodecFactory;
-import com.google.gwtorm.protobuf.ProtobufCodec;
-import java.sql.Timestamp;
-import java.time.LocalDate;
-import java.time.Month;
-import java.time.ZoneId;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.TimeZone;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-public class ChangeBundleTest extends GerritBaseTests {
-  private static final ProtobufCodec<Change> CHANGE_CODEC = CodecFactory.encoder(Change.class);
-  private static final ProtobufCodec<ChangeMessage> CHANGE_MESSAGE_CODEC =
-      CodecFactory.encoder(ChangeMessage.class);
-  private static final ProtobufCodec<PatchSet> PATCH_SET_CODEC =
-      CodecFactory.encoder(PatchSet.class);
-  private static final ProtobufCodec<PatchSetApproval> PATCH_SET_APPROVAL_CODEC =
-      CodecFactory.encoder(PatchSetApproval.class);
-  private static final ProtobufCodec<PatchLineComment> PATCH_LINE_COMMENT_CODEC =
-      CodecFactory.encoder(PatchLineComment.class);
-  private static final String TIMEZONE_ID = "US/Eastern";
-
-  private String systemTimeZoneProperty;
-  private TimeZone systemTimeZone;
-
-  private Project.NameKey project;
-  private Account.Id accountId;
-
-  @Before
-  public void setUp() {
-    systemTimeZoneProperty = System.setProperty("user.timezone", TIMEZONE_ID);
-    systemTimeZone = TimeZone.getDefault();
-    TimeZone.setDefault(TimeZone.getTimeZone(TIMEZONE_ID));
-    long maxMs = ChangeRebuilderImpl.MAX_WINDOW_MS;
-    assertThat(maxMs).isGreaterThan(1000L);
-    TestTimeUtil.resetWithClockStep(maxMs * 2, MILLISECONDS);
-    project = new Project.NameKey("project");
-    accountId = new Account.Id(100);
-  }
-
-  @After
-  public void tearDown() {
-    TestTimeUtil.useSystemTime();
-    System.setProperty("user.timezone", systemTimeZoneProperty);
-    TimeZone.setDefault(systemTimeZone);
-  }
-
-  private void superWindowResolution() {
-    TestTimeUtil.setClockStep(ChangeRebuilderImpl.MAX_WINDOW_MS * 2, MILLISECONDS);
-    TimeUtil.nowTs();
-  }
-
-  private void subWindowResolution() {
-    TestTimeUtil.setClockStep(1, SECONDS);
-    TimeUtil.nowTs();
-  }
-
-  @Test
-  public void diffChangesDifferentIds() throws Exception {
-    Change c1 = TestChanges.newChange(project, accountId);
-    int id1 = c1.getId().get();
-    Change c2 = TestChanges.newChange(project, accountId);
-    int id2 = c2.getId().get();
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-
-    assertDiffs(
-        b1,
-        b2,
-        "changeId differs for Changes: {" + id1 + "} != {" + id2 + "}",
-        "createdOn differs for Changes: {2009-09-30 17:00:00.0} != {2009-09-30 17:00:06.0}",
-        "effective last updated time differs for Changes:"
-            + " {2009-09-30 17:00:00.0} != {2009-09-30 17:00:06.0}");
-  }
-
-  @Test
-  public void diffChangesSameId() throws Exception {
-    Change c1 = TestChanges.newChange(new Project.NameKey("project"), new Account.Id(100));
-    Change c2 = clone(c1);
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-
-    assertNoDiffs(b1, b2);
-
-    c2.setTopic("topic");
-    assertDiffs(b1, b2, "topic differs for Change.Id " + c1.getId() + ": {null} != {topic}");
-  }
-
-  @Test
-  public void diffChangesMixedSourcesAllowsSlop() throws Exception {
-    subWindowResolution();
-    Change c1 = TestChanges.newChange(new Project.NameKey("project"), new Account.Id(100));
-    Change c2 = clone(c1);
-    c2.setCreatedOn(TimeUtil.nowTs());
-    c2.setLastUpdatedOn(TimeUtil.nowTs());
-
-    // Both are ReviewDb, exact timestamp match is required.
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    assertDiffs(
-        b1,
-        b2,
-        "createdOn differs for Change.Id "
-            + c1.getId()
-            + ":"
-            + " {2009-09-30 17:00:01.0} != {2009-09-30 17:00:02.0}",
-        "effective last updated time differs for Change.Id "
-            + c1.getId()
-            + ":"
-            + " {2009-09-30 17:00:01.0} != {2009-09-30 17:00:03.0}");
-
-    // One NoteDb, slop is allowed.
-    b1 =
-        new ChangeBundle(
-            c1, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
-    b2 =
-        new ChangeBundle(
-            c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    assertNoDiffs(b1, b2);
-    assertNoDiffs(b2, b1);
-
-    // But not too much slop.
-    superWindowResolution();
-    Change c3 = clone(c1);
-    c3.setLastUpdatedOn(TimeUtil.nowTs());
-    b1 =
-        new ChangeBundle(
-            c1, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
-    ChangeBundle b3 =
-        new ChangeBundle(
-            c3, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    String msg =
-        "effective last updated time differs for Change.Id "
-            + c1.getId()
-            + " in NoteDb vs. ReviewDb:"
-            + " {2009-09-30 17:00:01.0} != {2009-09-30 17:00:10.0}";
-    assertDiffs(b1, b3, msg);
-    assertDiffs(b3, b1, msg);
-  }
-
-  @Test
-  public void diffChangesIgnoresOriginalSubjectInReviewDb() throws Exception {
-    Change c1 = TestChanges.newChange(new Project.NameKey("project"), new Account.Id(100));
-    c1.setCurrentPatchSet(c1.currentPatchSetId(), "Subject", "Original A");
-    Change c2 = clone(c1);
-    c2.setCurrentPatchSet(c2.currentPatchSetId(), c1.getSubject(), "Original B");
-
-    // Both ReviewDb, exact match required.
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    assertDiffs(
-        b1,
-        b2,
-        "originalSubject differs for Change.Id "
-            + c1.getId()
-            + ":"
-            + " {Original A} != {Original B}");
-
-    // Both NoteDb, exact match required.
-    b1 =
-        new ChangeBundle(
-            c1, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
-    b2 =
-        new ChangeBundle(
-            c2, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
-    assertDiffs(
-        b1,
-        b2,
-        "originalSubject differs for Change.Id "
-            + c1.getId()
-            + ":"
-            + " {Original A} != {Original B}");
-
-    // One ReviewDb, one NoteDb, original subject is ignored.
-    b1 =
-        new ChangeBundle(
-            c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    b2 =
-        new ChangeBundle(
-            c2, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
-    assertNoDiffs(b1, b2);
-    assertNoDiffs(b2, b1);
-  }
-
-  @Test
-  public void diffChangesSanitizesSubjectsBeforeComparison() throws Exception {
-    Change c1 = TestChanges.newChange(new Project.NameKey("project"), new Account.Id(100));
-    c1.setCurrentPatchSet(c1.currentPatchSetId(), "Subject\r\rbody", "Original");
-    Change c2 = clone(c1);
-    c2.setCurrentPatchSet(c2.currentPatchSetId(), "Subject  body", "Original");
-
-    // Both ReviewDb, exact match required
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    assertDiffs(
-        b1,
-        b2,
-        "subject differs for Change.Id "
-            + c1.getId()
-            + ":"
-            + " {Subject\r\rbody} != {Subject  body}");
-
-    // Both NoteDb, exact match required (although it should be impossible to
-    // create a NoteDb change with '\r' in the subject).
-    b1 =
-        new ChangeBundle(
-            c1, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
-    b2 =
-        new ChangeBundle(
-            c2, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
-    assertDiffs(
-        b1,
-        b2,
-        "subject differs for Change.Id "
-            + c1.getId()
-            + ":"
-            + " {Subject\r\rbody} != {Subject  body}");
-
-    // One ReviewDb, one NoteDb, '\r' is normalized to ' '.
-    b1 =
-        new ChangeBundle(
-            c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    b2 =
-        new ChangeBundle(
-            c2, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
-    assertNoDiffs(b1, b2);
-    assertNoDiffs(b2, b1);
-  }
-
-  @Test
-  public void diffChangesConsidersEmptyReviewDbTopicEquivalentToNullInNoteDb() throws Exception {
-    Change c1 = TestChanges.newChange(new Project.NameKey("project"), new Account.Id(100));
-    c1.setTopic("");
-    Change c2 = clone(c1);
-    c2.setTopic(null);
-
-    // Both ReviewDb, exact match required.
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    assertDiffs(b1, b2, "topic differs for Change.Id " + c1.getId() + ": {} != {null}");
-
-    // Topic ignored if ReviewDb is empty and NoteDb is null.
-    b1 =
-        new ChangeBundle(
-            c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    b2 =
-        new ChangeBundle(
-            c2, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
-    assertNoDiffs(b1, b2);
-
-    // Exact match still required if NoteDb has empty value (not realistic).
-    b1 =
-        new ChangeBundle(
-            c1, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
-    b2 =
-        new ChangeBundle(
-            c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    assertDiffs(b1, b2, "topic differs for Change.Id " + c1.getId() + ": {} != {null}");
-
-    // Null is not equal to a non-empty string.
-    Change c3 = clone(c1);
-    c3.setTopic("topic");
-    b1 =
-        new ChangeBundle(
-            c3, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    b2 =
-        new ChangeBundle(
-            c2, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
-    assertDiffs(b1, b2, "topic differs for Change.Id " + c1.getId() + ": {topic} != {null}");
-
-    // Null is equal to a string that is all whitespace.
-    Change c4 = clone(c1);
-    c4.setTopic("  ");
-    b1 =
-        new ChangeBundle(
-            c4, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    b2 =
-        new ChangeBundle(
-            c2, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
-    assertNoDiffs(b1, b2);
-    assertNoDiffs(b2, b1);
-  }
-
-  @Test
-  public void diffChangesIgnoresLeadingAndTrailingWhitespaceInReviewDbTopics() throws Exception {
-    Change c1 = TestChanges.newChange(new Project.NameKey("project"), new Account.Id(100));
-    c1.setTopic(" abc ");
-    Change c2 = clone(c1);
-    c2.setTopic("abc");
-
-    // Both ReviewDb, exact match required.
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    assertDiffs(b1, b2, "topic differs for Change.Id " + c1.getId() + ": { abc } != {abc}");
-
-    // Leading whitespace in ReviewDb topic is ignored.
-    b1 =
-        new ChangeBundle(
-            c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    b2 =
-        new ChangeBundle(
-            c2, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
-    assertNoDiffs(b1, b2);
-    assertNoDiffs(b2, b1);
-
-    // Must match except for the leading/trailing whitespace.
-    Change c3 = clone(c1);
-    c3.setTopic("cba");
-    b1 =
-        new ChangeBundle(
-            c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    b2 =
-        new ChangeBundle(
-            c3, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
-    assertDiffs(b1, b2, "topic differs for Change.Id " + c1.getId() + ": { abc } != {cba}");
-  }
-
-  @Test
-  public void diffChangesTakesMaxEntityTimestampFromReviewDb() throws Exception {
-    Change c1 = TestChanges.newChange(new Project.NameKey("project"), new Account.Id(100));
-    PatchSet ps = new PatchSet(c1.currentPatchSetId());
-    ps.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
-    ps.setUploader(accountId);
-    ps.setCreatedOn(TimeUtil.nowTs());
-    PatchSetApproval a =
-        new PatchSetApproval(
-            new PatchSetApproval.Key(c1.currentPatchSetId(), accountId, new LabelId("Code-Review")),
-            (short) 1,
-            TimeUtil.nowTs());
-
-    Change c2 = clone(c1);
-    c2.setLastUpdatedOn(a.getGranted());
-
-    // Both ReviewDb, exact match required.
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c1, messages(), patchSets(ps), approvals(a), comments(), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c2, messages(), patchSets(ps), approvals(a), comments(), reviewers(), REVIEW_DB);
-    assertDiffs(
-        b1,
-        b2,
-        "effective last updated time differs for Change.Id "
-            + c1.getId()
-            + ":"
-            + " {2009-09-30 17:00:00.0} != {2009-09-30 17:00:12.0}");
-
-    // NoteDb allows latest timestamp from all entities in bundle.
-    b2 =
-        new ChangeBundle(
-            c2, messages(), patchSets(ps), approvals(a), comments(), reviewers(), NOTE_DB);
-    assertNoDiffs(b1, b2);
-  }
-
-  @Test
-  public void diffChangesIgnoresChangeTimestampIfAnyOtherEntitiesExist() {
-    Change c1 = TestChanges.newChange(new Project.NameKey("project"), new Account.Id(100));
-    PatchSet ps = new PatchSet(c1.currentPatchSetId());
-    ps.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
-    ps.setUploader(accountId);
-    ps.setCreatedOn(TimeUtil.nowTs());
-    PatchSetApproval a =
-        new PatchSetApproval(
-            new PatchSetApproval.Key(c1.currentPatchSetId(), accountId, new LabelId("Code-Review")),
-            (short) 1,
-            TimeUtil.nowTs());
-    c1.setLastUpdatedOn(a.getGranted());
-
-    Change c2 = clone(c1);
-    c2.setLastUpdatedOn(TimeUtil.nowTs());
-
-    // ReviewDb has later lastUpdatedOn timestamp than NoteDb, allowed since
-    // NoteDb matches the latest timestamp of a non-Change entity.
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c2, messages(), patchSets(ps), approvals(a), comments(), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c1, messages(), patchSets(ps), approvals(a), comments(), reviewers(), NOTE_DB);
-    assertThat(b1.getChange().getLastUpdatedOn()).isGreaterThan(b2.getChange().getLastUpdatedOn());
-    assertNoDiffs(b1, b2);
-
-    // Timestamps must actually match if Change is the only entity.
-    b1 =
-        new ChangeBundle(
-            c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    b2 =
-        new ChangeBundle(
-            c1, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
-    assertDiffs(
-        b1,
-        b2,
-        "effective last updated time differs for Change.Id "
-            + c1.getId()
-            + " in NoteDb vs. ReviewDb:"
-            + " {2009-09-30 17:00:12.0} != {2009-09-30 17:00:18.0}");
-  }
-
-  @Test
-  public void diffChangesAllowsReviewDbSubjectToBePrefixOfNoteDbSubject() throws Exception {
-    Change c1 = TestChanges.newChange(new Project.NameKey("project"), new Account.Id(100));
-    Change c2 = clone(c1);
-    c2.setCurrentPatchSet(
-        c1.currentPatchSetId(), c1.getSubject().substring(0, 10), c1.getOriginalSubject());
-    assertThat(c2.getSubject()).isNotEqualTo(c1.getSubject());
-
-    // Both ReviewDb, exact match required.
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    assertDiffs(
-        b1,
-        b2,
-        "subject differs for Change.Id " + c1.getId() + ": {Change subject} != {Change sub}");
-
-    // ReviewDb has shorter subject, allowed.
-    b1 =
-        new ChangeBundle(
-            c1, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
-    b2 =
-        new ChangeBundle(
-            c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    assertNoDiffs(b1, b2);
-
-    // NoteDb has shorter subject, not allowed.
-    b1 =
-        new ChangeBundle(
-            c1, messages(), latest(c1), approvals(), comments(), reviewers(), REVIEW_DB);
-    b2 =
-        new ChangeBundle(c2, messages(), latest(c2), approvals(), comments(), reviewers(), NOTE_DB);
-    assertDiffs(
-        b1,
-        b2,
-        "subject differs for Change.Id " + c1.getId() + ": {Change subject} != {Change sub}");
-  }
-
-  @Test
-  public void diffChangesTrimsLeadingSpacesFromReviewDbComparingToNoteDb() throws Exception {
-    Change c1 = TestChanges.newChange(new Project.NameKey("project"), new Account.Id(100));
-    Change c2 = clone(c1);
-    c2.setCurrentPatchSet(c1.currentPatchSetId(), "   " + c1.getSubject(), c1.getOriginalSubject());
-
-    // Both ReviewDb, exact match required.
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    assertDiffs(
-        b1,
-        b2,
-        "subject differs for Change.Id "
-            + c1.getId()
-            + ":"
-            + " {Change subject} != {   Change subject}");
-
-    // ReviewDb is missing leading spaces, allowed.
-    b1 =
-        new ChangeBundle(
-            c1, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
-    b2 =
-        new ChangeBundle(
-            c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    assertNoDiffs(b1, b2);
-    assertNoDiffs(b2, b1);
-  }
-
-  @Test
-  public void diffChangesDoesntTrimLeadingNonSpaceWhitespaceFromSubject() throws Exception {
-    Change c1 = TestChanges.newChange(new Project.NameKey("project"), new Account.Id(100));
-    Change c2 = clone(c1);
-    c2.setCurrentPatchSet(c1.currentPatchSetId(), "\t" + c1.getSubject(), c1.getOriginalSubject());
-
-    // Both ReviewDb.
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    assertDiffs(
-        b1,
-        b2,
-        "subject differs for Change.Id "
-            + c1.getId()
-            + ":"
-            + " {Change subject} != {\tChange subject}");
-
-    // One NoteDb.
-    b1 =
-        new ChangeBundle(c1, messages(), latest(c1), approvals(), comments(), reviewers(), NOTE_DB);
-    b2 =
-        new ChangeBundle(
-            c2, messages(), latest(c2), approvals(), comments(), reviewers(), REVIEW_DB);
-    assertDiffs(
-        b1,
-        b2,
-        "subject differs for Change.Id "
-            + c1.getId()
-            + ":"
-            + " {Change subject} != {\tChange subject}");
-    assertDiffs(
-        b2,
-        b1,
-        "subject differs for Change.Id "
-            + c1.getId()
-            + ":"
-            + " {\tChange subject} != {Change subject}");
-  }
-
-  @Test
-  public void diffChangesHandlesBuggyJGitSubjectExtraction() throws Exception {
-    Change c1 = TestChanges.newChange(project, accountId);
-    String buggySubject = "Subject\r \r Rest of message.";
-    c1.setCurrentPatchSet(c1.currentPatchSetId(), buggySubject, buggySubject);
-    Change c2 = clone(c1);
-    c2.setCurrentPatchSet(c2.currentPatchSetId(), "Subject", "Subject");
-
-    // Both ReviewDb.
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    assertDiffs(
-        b1,
-        b2,
-        "originalSubject differs for Change.Id "
-            + c1.getId()
-            + ":"
-            + " {Subject\r \r Rest of message.} != {Subject}",
-        "subject differs for Change.Id "
-            + c1.getId()
-            + ":"
-            + " {Subject\r \r Rest of message.} != {Subject}");
-
-    // NoteDb has correct subject without "\r ".
-    b1 =
-        new ChangeBundle(
-            c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    b2 =
-        new ChangeBundle(
-            c2, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
-    assertNoDiffs(b1, b2);
-    assertNoDiffs(b2, b1);
-  }
-
-  @Test
-  public void diffChangesIgnoresInvalidCurrentPatchSetIdInReviewDb() throws Exception {
-    Change c1 = TestChanges.newChange(new Project.NameKey("project"), new Account.Id(100));
-    Change c2 = clone(c1);
-    c2.setCurrentPatchSet(
-        new PatchSet.Id(c2.getId(), 0), "Unrelated subject", c2.getOriginalSubject());
-
-    // Both ReviewDb.
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    assertDiffs(
-        b1,
-        b2,
-        "currentPatchSetId differs for Change.Id " + c1.getId() + ": {1} != {0}",
-        "subject differs for Change.Id "
-            + c1.getId()
-            + ":"
-            + " {Change subject} != {Unrelated subject}");
-
-    // One NoteDb.
-    //
-    // This is based on a real corrupt change where all patch sets were deleted
-    // but the Change entity stuck around, resulting in a currentPatchSetId of 0
-    // after converting to NoteDb.
-    b1 =
-        new ChangeBundle(
-            c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    b2 =
-        new ChangeBundle(
-            c2, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
-    assertNoDiffs(b1, b2);
-    assertNoDiffs(b2, b1);
-  }
-
-  @Test
-  public void diffChangesAllowsCreatedToMatchLastUpdated() throws Exception {
-    Change c1 = TestChanges.newChange(new Project.NameKey("project"), new Account.Id(100));
-    c1.setCreatedOn(TimeUtil.nowTs());
-    assertThat(c1.getCreatedOn()).isGreaterThan(c1.getLastUpdatedOn());
-    Change c2 = clone(c1);
-    c2.setCreatedOn(c2.getLastUpdatedOn());
-
-    // Both ReviewDb.
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    assertDiffs(
-        b1,
-        b2,
-        "createdOn differs for Change.Id "
-            + c1.getId()
-            + ": {2009-09-30 17:00:06.0} != {2009-09-30 17:00:00.0}");
-
-    // One NoteDb.
-    b1 =
-        new ChangeBundle(
-            c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    b2 =
-        new ChangeBundle(
-            c2, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
-    assertNoDiffs(b1, b2);
-    assertNoDiffs(b2, b1);
-  }
-
-  @Test
-  public void diffChangeMessageKeySets() throws Exception {
-    Change c = TestChanges.newChange(project, accountId);
-    int id = c.getId().get();
-    ChangeMessage cm1 =
-        new ChangeMessage(
-            new ChangeMessage.Key(c.getId(), "uuid1"),
-            accountId,
-            TimeUtil.nowTs(),
-            c.currentPatchSetId());
-    ChangeMessage cm2 =
-        new ChangeMessage(
-            new ChangeMessage.Key(c.getId(), "uuid2"),
-            accountId,
-            TimeUtil.nowTs(),
-            c.currentPatchSetId());
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c, messages(cm1), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c, messages(cm2), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
-
-    assertDiffs(
-        b1,
-        b2,
-        "ChangeMessage.Key sets differ:"
-            + " ["
-            + id
-            + ",uuid1] only in A; ["
-            + id
-            + ",uuid2] only in B");
-  }
-
-  @Test
-  public void diffChangeMessages() throws Exception {
-    Change c = TestChanges.newChange(project, accountId);
-    ChangeMessage cm1 =
-        new ChangeMessage(
-            new ChangeMessage.Key(c.getId(), "uuid"),
-            accountId,
-            TimeUtil.nowTs(),
-            c.currentPatchSetId());
-    cm1.setMessage("message 1");
-    ChangeMessage cm2 = clone(cm1);
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c, messages(cm1), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c, messages(cm2), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
-
-    assertNoDiffs(b1, b2);
-
-    cm2.setMessage("message 2");
-    assertDiffs(
-        b1,
-        b2,
-        "message differs for ChangeMessage.Key "
-            + c.getId()
-            + ",uuid:"
-            + " {message 1} != {message 2}");
-  }
-
-  @Test
-  public void diffChangeMessagesIgnoresUuids() throws Exception {
-    Change c = TestChanges.newChange(project, accountId);
-    int id = c.getId().get();
-    ChangeMessage cm1 =
-        new ChangeMessage(
-            new ChangeMessage.Key(c.getId(), "uuid1"),
-            accountId,
-            TimeUtil.nowTs(),
-            c.currentPatchSetId());
-    cm1.setMessage("message 1");
-    ChangeMessage cm2 = clone(cm1);
-    cm2.getKey().set("uuid2");
-
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c, messages(cm1), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c, messages(cm2), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
-    // Both are ReviewDb, exact UUID match is required.
-    assertDiffs(
-        b1,
-        b2,
-        "ChangeMessage.Key sets differ:"
-            + " ["
-            + id
-            + ",uuid1] only in A; ["
-            + id
-            + ",uuid2] only in B");
-
-    // One NoteDb, UUIDs are ignored.
-    b1 =
-        new ChangeBundle(
-            c, messages(cm1), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
-    b2 =
-        new ChangeBundle(
-            c, messages(cm2), latest(c), approvals(), comments(), reviewers(), NOTE_DB);
-    assertNoDiffs(b1, b2);
-  }
-
-  @Test
-  public void diffChangeMessagesWithDifferentCounts() throws Exception {
-    Change c = TestChanges.newChange(project, accountId);
-    int id = c.getId().get();
-    ChangeMessage cm1 =
-        new ChangeMessage(
-            new ChangeMessage.Key(c.getId(), "uuid1"),
-            accountId,
-            TimeUtil.nowTs(),
-            c.currentPatchSetId());
-    cm1.setMessage("message 1");
-    ChangeMessage cm2 =
-        new ChangeMessage(
-            new ChangeMessage.Key(c.getId(), "uuid2"),
-            accountId,
-            TimeUtil.nowTs(),
-            c.currentPatchSetId());
-    cm1.setMessage("message 2");
-
-    // Both ReviewDb: Uses same keySet diff as other types.
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c, messages(cm1, cm2), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c, messages(cm1), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
-    assertDiffs(
-        b1, b2, "ChangeMessage.Key sets differ: [" + id + ",uuid2] only in A; [] only in B");
-
-    // One NoteDb: UUIDs in keys can't be used for comparison, just diff counts.
-    b1 =
-        new ChangeBundle(
-            c, messages(cm1, cm2), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
-    b2 =
-        new ChangeBundle(
-            c, messages(cm1), latest(c), approvals(), comments(), reviewers(), NOTE_DB);
-    assertDiffs(b1, b2, "ChangeMessages differ for Change.Id " + id + "\nOnly in A:\n  " + cm2);
-    assertDiffs(b2, b1, "ChangeMessages differ for Change.Id " + id + "\nOnly in B:\n  " + cm2);
-  }
-
-  @Test
-  public void diffChangeMessagesMixedSourcesWithDifferences() throws Exception {
-    Change c = TestChanges.newChange(project, accountId);
-    int id = c.getId().get();
-    ChangeMessage cm1 =
-        new ChangeMessage(
-            new ChangeMessage.Key(c.getId(), "uuid1"),
-            accountId,
-            TimeUtil.nowTs(),
-            c.currentPatchSetId());
-    cm1.setMessage("message 1");
-    ChangeMessage cm2 = clone(cm1);
-    cm2.setMessage("message 2");
-    ChangeMessage cm3 = clone(cm1);
-    cm3.getKey().set("uuid2"); // Differs only in UUID.
-
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c, messages(cm1, cm3), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c, messages(cm2, cm3), latest(c), approvals(), comments(), reviewers(), NOTE_DB);
-    // Implementation happens to pair up cm1 in b1 with cm3 in b2 because it
-    // depends on iteration order and doesn't care about UUIDs. The important
-    // thing is that there's some diff.
-    assertDiffs(
-        b1,
-        b2,
-        "ChangeMessages differ for Change.Id "
-            + id
-            + "\n"
-            + "Only in A:\n  "
-            + cm3
-            + "\n"
-            + "Only in B:\n  "
-            + cm2);
-    assertDiffs(
-        b2,
-        b1,
-        "ChangeMessages differ for Change.Id "
-            + id
-            + "\n"
-            + "Only in A:\n  "
-            + cm2
-            + "\n"
-            + "Only in B:\n  "
-            + cm3);
-  }
-
-  @Test
-  public void diffChangeMessagesMixedSourcesAllowsSlop() throws Exception {
-    subWindowResolution();
-    Change c = TestChanges.newChange(project, accountId);
-    ChangeMessage cm1 =
-        new ChangeMessage(
-            new ChangeMessage.Key(c.getId(), "uuid1"),
-            accountId,
-            TimeUtil.nowTs(),
-            c.currentPatchSetId());
-    ChangeMessage cm2 = clone(cm1);
-    cm2.setWrittenOn(TimeUtil.nowTs());
-
-    // Both are ReviewDb, exact timestamp match is required.
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c, messages(cm1), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c, messages(cm2), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
-    assertDiffs(
-        b1,
-        b2,
-        "writtenOn differs for ChangeMessage.Key "
-            + c.getId()
-            + ",uuid1:"
-            + " {2009-09-30 17:00:02.0} != {2009-09-30 17:00:03.0}");
-
-    // One NoteDb, slop is allowed.
-    b1 =
-        new ChangeBundle(
-            c, messages(cm1), latest(c), approvals(), comments(), reviewers(), NOTE_DB);
-    b2 =
-        new ChangeBundle(
-            c, messages(cm2), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
-    assertNoDiffs(b1, b2);
-    assertNoDiffs(b2, b1);
-
-    // But not too much slop.
-    superWindowResolution();
-    ChangeMessage cm3 = clone(cm1);
-    cm3.setWrittenOn(TimeUtil.nowTs());
-    b1 =
-        new ChangeBundle(
-            c, messages(cm1), latest(c), approvals(), comments(), reviewers(), NOTE_DB);
-    ChangeBundle b3 =
-        new ChangeBundle(
-            c, messages(cm3), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
-    int id = c.getId().get();
-    assertDiffs(
-        b1,
-        b3,
-        "ChangeMessages differ for Change.Id "
-            + id
-            + "\n"
-            + "Only in A:\n  "
-            + cm1
-            + "\n"
-            + "Only in B:\n  "
-            + cm3);
-    assertDiffs(
-        b3,
-        b1,
-        "ChangeMessages differ for Change.Id "
-            + id
-            + "\n"
-            + "Only in A:\n  "
-            + cm3
-            + "\n"
-            + "Only in B:\n  "
-            + cm1);
-  }
-
-  @Test
-  public void diffChangeMessagesAllowsNullPatchSetIdFromReviewDb() throws Exception {
-    Change c = TestChanges.newChange(project, accountId);
-    int id = c.getId().get();
-    ChangeMessage cm1 =
-        new ChangeMessage(
-            new ChangeMessage.Key(c.getId(), "uuid"),
-            accountId,
-            TimeUtil.nowTs(),
-            c.currentPatchSetId());
-    cm1.setMessage("message 1");
-    ChangeMessage cm2 = clone(cm1);
-    cm2.setPatchSetId(null);
-
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c, messages(cm1), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c, messages(cm2), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
-
-    // Both are ReviewDb, exact patch set ID match is required.
-    assertDiffs(
-        b1,
-        b2,
-        "patchset differs for ChangeMessage.Key "
-            + c.getId()
-            + ",uuid:"
-            + " {"
-            + id
-            + ",1} != {null}");
-
-    // Null patch set ID on ReviewDb is ignored.
-    b1 =
-        new ChangeBundle(
-            c, messages(cm1), latest(c), approvals(), comments(), reviewers(), NOTE_DB);
-    b2 =
-        new ChangeBundle(
-            c, messages(cm2), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
-    assertNoDiffs(b1, b2);
-
-    // Null patch set ID on NoteDb is not ignored (but is not realistic).
-    b1 =
-        new ChangeBundle(
-            c, messages(cm1), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
-    b2 =
-        new ChangeBundle(
-            c, messages(cm2), latest(c), approvals(), comments(), reviewers(), NOTE_DB);
-    assertDiffs(
-        b1,
-        b2,
-        "ChangeMessages differ for Change.Id "
-            + id
-            + "\n"
-            + "Only in A:\n  "
-            + cm1
-            + "\n"
-            + "Only in B:\n  "
-            + cm2);
-    assertDiffs(
-        b2,
-        b1,
-        "ChangeMessages differ for Change.Id "
-            + id
-            + "\n"
-            + "Only in A:\n  "
-            + cm2
-            + "\n"
-            + "Only in B:\n  "
-            + cm1);
-  }
-
-  @Test
-  public void diffPatchSetIdSets() throws Exception {
-    Change c = TestChanges.newChange(project, accountId);
-    TestChanges.incrementPatchSet(c);
-
-    PatchSet ps1 = new PatchSet(new PatchSet.Id(c.getId(), 1));
-    ps1.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
-    ps1.setUploader(accountId);
-    ps1.setCreatedOn(TimeUtil.nowTs());
-    PatchSet ps2 = new PatchSet(new PatchSet.Id(c.getId(), 2));
-    ps2.setRevision(new RevId("badc0feebadc0feebadc0feebadc0feebadc0fee"));
-    ps2.setUploader(accountId);
-    ps2.setCreatedOn(TimeUtil.nowTs());
-
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c, messages(), patchSets(ps2), approvals(), comments(), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c, messages(), patchSets(ps1, ps2), approvals(), comments(), reviewers(), REVIEW_DB);
-
-    assertDiffs(b1, b2, "PatchSet.Id sets differ: [] only in A; [" + c.getId() + ",1] only in B");
-  }
-
-  @Test
-  public void diffPatchSets() throws Exception {
-    Change c = TestChanges.newChange(project, accountId);
-    PatchSet ps1 = new PatchSet(c.currentPatchSetId());
-    ps1.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
-    ps1.setUploader(accountId);
-    ps1.setCreatedOn(TimeUtil.nowTs());
-    PatchSet ps2 = clone(ps1);
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c, messages(), patchSets(ps1), approvals(), comments(), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c, messages(), patchSets(ps2), approvals(), comments(), reviewers(), REVIEW_DB);
-
-    assertNoDiffs(b1, b2);
-
-    ps2.setRevision(new RevId("badc0feebadc0feebadc0feebadc0feebadc0fee"));
-    assertDiffs(
-        b1,
-        b2,
-        "revision differs for PatchSet.Id "
-            + c.getId()
-            + ",1:"
-            + " {RevId{deadbeefdeadbeefdeadbeefdeadbeefdeadbeef}}"
-            + " != {RevId{badc0feebadc0feebadc0feebadc0feebadc0fee}}");
-  }
-
-  @Test
-  public void diffPatchSetsMixedSourcesAllowsSlop() throws Exception {
-    subWindowResolution();
-    Change c = TestChanges.newChange(project, accountId);
-    PatchSet ps1 = new PatchSet(c.currentPatchSetId());
-    ps1.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
-    ps1.setUploader(accountId);
-    ps1.setCreatedOn(truncateToSecond(TimeUtil.nowTs()));
-    PatchSet ps2 = clone(ps1);
-    ps2.setCreatedOn(TimeUtil.nowTs());
-
-    // Both are ReviewDb, exact timestamp match is required.
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c, messages(), patchSets(ps1), approvals(), comments(), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c, messages(), patchSets(ps2), approvals(), comments(), reviewers(), REVIEW_DB);
-    assertDiffs(
-        b1,
-        b2,
-        "createdOn differs for PatchSet.Id "
-            + c.getId()
-            + ",1:"
-            + " {2009-09-30 17:00:02.0} != {2009-09-30 17:00:03.0}");
-
-    // One NoteDb, slop is allowed.
-    b1 =
-        new ChangeBundle(
-            c, messages(), patchSets(ps1), approvals(), comments(), reviewers(), NOTE_DB);
-    b2 =
-        new ChangeBundle(
-            c, messages(), patchSets(ps2), approvals(), comments(), reviewers(), REVIEW_DB);
-    assertNoDiffs(b1, b2);
-
-    // But not too much slop.
-    superWindowResolution();
-    PatchSet ps3 = clone(ps1);
-    ps3.setCreatedOn(TimeUtil.nowTs());
-    b1 =
-        new ChangeBundle(
-            c, messages(), patchSets(ps1), approvals(), comments(), reviewers(), NOTE_DB);
-    ChangeBundle b3 =
-        new ChangeBundle(
-            c, messages(), patchSets(ps3), approvals(), comments(), reviewers(), REVIEW_DB);
-    String msg =
-        "createdOn differs for PatchSet.Id "
-            + c.getId()
-            + ",1 in NoteDb vs. ReviewDb:"
-            + " {2009-09-30 17:00:02.0} != {2009-09-30 17:00:10.0}";
-    assertDiffs(b1, b3, msg);
-    assertDiffs(b3, b1, msg);
-  }
-
-  @Test
-  public void diffPatchSetsIgnoresTrailingNewlinesInPushCertificate() throws Exception {
-    subWindowResolution();
-    Change c = TestChanges.newChange(project, accountId);
-    PatchSet ps1 = new PatchSet(c.currentPatchSetId());
-    ps1.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
-    ps1.setUploader(accountId);
-    ps1.setCreatedOn(truncateToSecond(TimeUtil.nowTs()));
-    ps1.setPushCertificate("some cert");
-    PatchSet ps2 = clone(ps1);
-    ps2.setPushCertificate(ps2.getPushCertificate() + "\n\n");
-
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c, messages(), patchSets(ps1), approvals(), comments(), reviewers(), NOTE_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c, messages(), patchSets(ps2), approvals(), comments(), reviewers(), REVIEW_DB);
-    assertNoDiffs(b1, b2);
-    assertNoDiffs(b2, b1);
-
-    b1 =
-        new ChangeBundle(
-            c, messages(), patchSets(ps1), approvals(), comments(), reviewers(), REVIEW_DB);
-    b2 =
-        new ChangeBundle(
-            c, messages(), patchSets(ps2), approvals(), comments(), reviewers(), NOTE_DB);
-    assertNoDiffs(b1, b2);
-    assertNoDiffs(b2, b1);
-  }
-
-  @Test
-  public void diffPatchSetsGreaterThanCurrent() throws Exception {
-    Change c = TestChanges.newChange(project, accountId);
-
-    PatchSet ps1 = new PatchSet(new PatchSet.Id(c.getId(), 1));
-    ps1.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
-    ps1.setUploader(accountId);
-    ps1.setCreatedOn(TimeUtil.nowTs());
-    PatchSet ps2 = new PatchSet(new PatchSet.Id(c.getId(), 2));
-    ps2.setRevision(new RevId("badc0feebadc0feebadc0feebadc0feebadc0fee"));
-    ps2.setUploader(accountId);
-    ps2.setCreatedOn(TimeUtil.nowTs());
-    assertThat(ps2.getId().get()).isGreaterThan(c.currentPatchSetId().get());
-
-    ChangeMessage cm1 =
-        new ChangeMessage(
-            new ChangeMessage.Key(c.getId(), "uuid1"),
-            accountId,
-            TimeUtil.nowTs(),
-            c.currentPatchSetId());
-    ChangeMessage cm2 =
-        new ChangeMessage(
-            new ChangeMessage.Key(c.getId(), "uuid2"),
-            accountId,
-            TimeUtil.nowTs(),
-            c.currentPatchSetId());
-
-    PatchSetApproval a1 =
-        new PatchSetApproval(
-            new PatchSetApproval.Key(ps1.getId(), accountId, new LabelId("Code-Review")),
-            (short) 1,
-            TimeUtil.nowTs());
-    PatchSetApproval a2 =
-        new PatchSetApproval(
-            new PatchSetApproval.Key(ps2.getId(), accountId, new LabelId("Code-Review")),
-            (short) 1,
-            TimeUtil.nowTs());
-
-    // Both ReviewDb.
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c, messages(cm1), patchSets(ps1), approvals(a1), comments(), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c,
-            messages(cm1, cm2),
-            patchSets(ps1, ps2),
-            approvals(a1, a2),
-            comments(),
-            reviewers(),
-            REVIEW_DB);
-    assertDiffs(
-        b1,
-        b2,
-        "ChangeMessage.Key sets differ: [] only in A; [" + cm2.getKey() + "] only in B",
-        "PatchSet.Id sets differ: [] only in A; [" + ps2.getId() + "] only in B",
-        "PatchSetApproval.Key sets differ: [] only in A; [" + a2.getKey() + "] only in B");
-
-    // One NoteDb.
-    b1 =
-        new ChangeBundle(
-            c, messages(cm1), patchSets(ps1), approvals(a1), comments(), reviewers(), NOTE_DB);
-    b2 =
-        new ChangeBundle(
-            c,
-            messages(cm1, cm2),
-            patchSets(ps1, ps2),
-            approvals(a1, a2),
-            comments(),
-            reviewers(),
-            REVIEW_DB);
-    assertDiffs(
-        b1,
-        b2,
-        "ChangeMessages differ for Change.Id " + c.getId() + "\nOnly in B:\n  " + cm2,
-        "PatchSet.Id sets differ: [] only in A; [" + ps2.getId() + "] only in B",
-        "PatchSetApproval.Key sets differ: [] only in A; [" + a2.getKey() + "] only in B");
-
-    // Both NoteDb.
-    b1 =
-        new ChangeBundle(
-            c, messages(cm1), patchSets(ps1), approvals(a1), comments(), reviewers(), NOTE_DB);
-    b2 =
-        new ChangeBundle(
-            c,
-            messages(cm1, cm2),
-            patchSets(ps1, ps2),
-            approvals(a1, a2),
-            comments(),
-            reviewers(),
-            NOTE_DB);
-    assertDiffs(
-        b1,
-        b2,
-        "ChangeMessages differ for Change.Id " + c.getId() + "\nOnly in B:\n  " + cm2,
-        "PatchSet.Id sets differ: [] only in A; [" + ps2.getId() + "] only in B",
-        "PatchSetApproval.Key sets differ: [] only in A; [" + a2.getKey() + "] only in B");
-  }
-
-  @Test
-  public void diffPatchSetsIgnoresLeadingAndTrailingWhitespaceInReviewDbDescriptions()
-      throws Exception {
-    Change c = TestChanges.newChange(project, accountId);
-
-    PatchSet ps1 = new PatchSet(new PatchSet.Id(c.getId(), 1));
-    ps1.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
-    ps1.setUploader(accountId);
-    ps1.setCreatedOn(TimeUtil.nowTs());
-    ps1.setDescription(" abc ");
-    PatchSet ps2 = clone(ps1);
-    ps2.setDescription("abc");
-
-    // Both ReviewDb, exact match required.
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c, messages(), patchSets(ps1), approvals(), comments(), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c, messages(), patchSets(ps2), approvals(), comments(), reviewers(), REVIEW_DB);
-    assertDiffs(
-        b1, b2, "description differs for PatchSet.Id " + ps1.getId() + ": { abc } != {abc}");
-
-    // Whitespace in ReviewDb description is ignored.
-    b1 =
-        new ChangeBundle(
-            c, messages(), patchSets(ps1), approvals(), comments(), reviewers(), REVIEW_DB);
-    b2 =
-        new ChangeBundle(
-            c, messages(), patchSets(ps2), approvals(), comments(), reviewers(), NOTE_DB);
-    assertNoDiffs(b1, b2);
-    assertNoDiffs(b2, b1);
-
-    // Must match except for the leading/trailing whitespace.
-    PatchSet ps3 = clone(ps1);
-    ps3.setDescription("cba");
-    b1 =
-        new ChangeBundle(
-            c, messages(), patchSets(ps1), approvals(), comments(), reviewers(), REVIEW_DB);
-    b2 =
-        new ChangeBundle(
-            c, messages(), patchSets(ps3), approvals(), comments(), reviewers(), NOTE_DB);
-    assertDiffs(
-        b1, b2, "description differs for PatchSet.Id " + ps1.getId() + ": { abc } != {cba}");
-  }
-
-  @Test
-  public void diffPatchSetsIgnoresCreatedOnWhenReviewDbIsNonMonotonic() throws Exception {
-    Change c = TestChanges.newChange(project, accountId);
-
-    Timestamp beforePs1 = TimeUtil.nowTs();
-
-    PatchSet goodPs1 = new PatchSet(new PatchSet.Id(c.getId(), 1));
-    goodPs1.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
-    goodPs1.setUploader(accountId);
-    goodPs1.setCreatedOn(TimeUtil.nowTs());
-    PatchSet goodPs2 = new PatchSet(new PatchSet.Id(c.getId(), 2));
-    goodPs2.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
-    goodPs2.setUploader(accountId);
-    goodPs2.setCreatedOn(TimeUtil.nowTs());
-    assertThat(goodPs2.getCreatedOn()).isGreaterThan(goodPs1.getCreatedOn());
-
-    PatchSet badPs2 = clone(goodPs2);
-    badPs2.setCreatedOn(beforePs1);
-    assertThat(badPs2.getCreatedOn()).isLessThan(goodPs1.getCreatedOn());
-
-    // Both ReviewDb, exact match required.
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c,
-            messages(),
-            patchSets(goodPs1, goodPs2),
-            approvals(),
-            comments(),
-            reviewers(),
-            REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c,
-            messages(),
-            patchSets(goodPs1, badPs2),
-            approvals(),
-            comments(),
-            reviewers(),
-            REVIEW_DB);
-    assertDiffs(
-        b1,
-        b2,
-        "createdOn differs for PatchSet.Id "
-            + badPs2.getId()
-            + ":"
-            + " {2009-09-30 17:00:18.0} != {2009-09-30 17:00:06.0}");
-
-    // Non-monotonic in ReviewDb but monotonic in NoteDb, timestamps are
-    // ignored, including for ps1.
-    PatchSet badPs1 = clone(goodPs1);
-    badPs1.setCreatedOn(TimeUtil.nowTs());
-    b1 =
-        new ChangeBundle(
-            c,
-            messages(),
-            patchSets(badPs1, badPs2),
-            approvals(),
-            comments(),
-            reviewers(),
-            REVIEW_DB);
-    b2 =
-        new ChangeBundle(
-            c,
-            messages(),
-            patchSets(goodPs1, goodPs2),
-            approvals(),
-            comments(),
-            reviewers(),
-            NOTE_DB);
-    assertNoDiffs(b1, b2);
-    assertNoDiffs(b2, b1);
-
-    // Non-monotonic in NoteDb but monotonic in ReviewDb, timestamps are not
-    // ignored.
-    b1 =
-        new ChangeBundle(
-            c,
-            messages(),
-            patchSets(goodPs1, goodPs2),
-            approvals(),
-            comments(),
-            reviewers(),
-            REVIEW_DB);
-    b2 =
-        new ChangeBundle(
-            c,
-            messages(),
-            patchSets(badPs1, badPs2),
-            approvals(),
-            comments(),
-            reviewers(),
-            NOTE_DB);
-    assertDiffs(
-        b1,
-        b2,
-        "createdOn differs for PatchSet.Id "
-            + badPs1.getId()
-            + " in NoteDb vs. ReviewDb:"
-            + " {2009-09-30 17:00:24.0} != {2009-09-30 17:00:12.0}",
-        "createdOn differs for PatchSet.Id "
-            + badPs2.getId()
-            + " in NoteDb vs. ReviewDb:"
-            + " {2009-09-30 17:00:06.0} != {2009-09-30 17:00:18.0}");
-  }
-
-  @Test
-  public void diffPatchSetsAllowsFirstPatchSetCreatedOnToMatchChangeCreatedOn() {
-    Change c = TestChanges.newChange(project, accountId);
-    c.setLastUpdatedOn(TimeUtil.nowTs());
-
-    PatchSet goodPs1 = new PatchSet(new PatchSet.Id(c.getId(), 1));
-    goodPs1.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
-    goodPs1.setUploader(accountId);
-    goodPs1.setCreatedOn(TimeUtil.nowTs());
-    assertThat(goodPs1.getCreatedOn()).isGreaterThan(c.getCreatedOn());
-
-    PatchSet ps1AtCreatedOn = clone(goodPs1);
-    ps1AtCreatedOn.setCreatedOn(c.getCreatedOn());
-
-    PatchSet goodPs2 = new PatchSet(new PatchSet.Id(c.getId(), 2));
-    goodPs2.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
-    goodPs2.setUploader(accountId);
-    goodPs2.setCreatedOn(TimeUtil.nowTs());
-
-    PatchSet ps2AtCreatedOn = clone(goodPs2);
-    ps2AtCreatedOn.setCreatedOn(c.getCreatedOn());
-
-    // Both ReviewDb, exact match required.
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c,
-            messages(),
-            patchSets(goodPs1, goodPs2),
-            approvals(),
-            comments(),
-            reviewers(),
-            REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c,
-            messages(),
-            patchSets(ps1AtCreatedOn, ps2AtCreatedOn),
-            approvals(),
-            comments(),
-            reviewers(),
-            REVIEW_DB);
-    assertDiffs(
-        b1,
-        b2,
-        "createdOn differs for PatchSet.Id "
-            + c.getId()
-            + ",1: {2009-09-30 17:00:12.0} != {2009-09-30 17:00:00.0}",
-        "createdOn differs for PatchSet.Id "
-            + c.getId()
-            + ",2: {2009-09-30 17:00:18.0} != {2009-09-30 17:00:00.0}");
-
-    // One ReviewDb, PS1 is allowed to match change createdOn, but PS2 isn't.
-    b1 =
-        new ChangeBundle(
-            c,
-            messages(),
-            patchSets(goodPs1, goodPs2),
-            approvals(),
-            comments(),
-            reviewers(),
-            REVIEW_DB);
-    b2 =
-        new ChangeBundle(
-            c,
-            messages(),
-            patchSets(ps1AtCreatedOn, ps2AtCreatedOn),
-            approvals(),
-            comments(),
-            reviewers(),
-            NOTE_DB);
-    assertDiffs(
-        b1,
-        b2,
-        "createdOn differs for PatchSet.Id "
-            + c.getId()
-            + ",2 in NoteDb vs. ReviewDb: {2009-09-30 17:00:00.0} != {2009-09-30 17:00:18.0}");
-    assertDiffs(
-        b2,
-        b1,
-        "createdOn differs for PatchSet.Id "
-            + c.getId()
-            + ",2 in NoteDb vs. ReviewDb: {2009-09-30 17:00:00.0} != {2009-09-30 17:00:18.0}");
-  }
-
-  @Test
-  public void diffPatchSetApprovalKeySets() throws Exception {
-    Change c = TestChanges.newChange(project, accountId);
-    int id = c.getId().get();
-    PatchSetApproval a1 =
-        new PatchSetApproval(
-            new PatchSetApproval.Key(c.currentPatchSetId(), accountId, new LabelId("Code-Review")),
-            (short) 1,
-            TimeUtil.nowTs());
-    PatchSetApproval a2 =
-        new PatchSetApproval(
-            new PatchSetApproval.Key(c.currentPatchSetId(), accountId, new LabelId("Verified")),
-            (short) 1,
-            TimeUtil.nowTs());
-
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c, messages(), latest(c), approvals(a1), comments(), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c, messages(), latest(c), approvals(a2), comments(), reviewers(), REVIEW_DB);
-
-    assertDiffs(
-        b1,
-        b2,
-        "PatchSetApproval.Key sets differ:"
-            + " ["
-            + id
-            + "%2C1,100,Code-Review] only in A;"
-            + " ["
-            + id
-            + "%2C1,100,Verified] only in B");
-  }
-
-  @Test
-  public void diffPatchSetApprovals() throws Exception {
-    Change c = TestChanges.newChange(project, accountId);
-    PatchSetApproval a1 =
-        new PatchSetApproval(
-            new PatchSetApproval.Key(c.currentPatchSetId(), accountId, new LabelId("Code-Review")),
-            (short) 1,
-            TimeUtil.nowTs());
-    PatchSetApproval a2 = clone(a1);
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c, messages(), latest(c), approvals(a1), comments(), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c, messages(), latest(c), approvals(a2), comments(), reviewers(), REVIEW_DB);
-
-    assertNoDiffs(b1, b2);
-
-    a2.setValue((short) -1);
-    assertDiffs(
-        b1,
-        b2,
-        "value differs for PatchSetApproval.Key "
-            + c.getId()
-            + "%2C1,100,Code-Review: {1} != {-1}");
-  }
-
-  @Test
-  public void diffPatchSetApprovalsMixedSourcesAllowsSlop() throws Exception {
-    Change c = TestChanges.newChange(project, accountId);
-    subWindowResolution();
-    PatchSetApproval a1 =
-        new PatchSetApproval(
-            new PatchSetApproval.Key(c.currentPatchSetId(), accountId, new LabelId("Code-Review")),
-            (short) 1,
-            truncateToSecond(TimeUtil.nowTs()));
-    PatchSetApproval a2 = clone(a1);
-    a2.setGranted(TimeUtil.nowTs());
-
-    // Both are ReviewDb, exact timestamp match is required.
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c, messages(), latest(c), approvals(a1), comments(), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c, messages(), latest(c), approvals(a2), comments(), reviewers(), REVIEW_DB);
-    assertDiffs(
-        b1,
-        b2,
-        "granted differs for PatchSetApproval.Key "
-            + c.getId()
-            + "%2C1,100,Code-Review:"
-            + " {2009-09-30 17:00:07.0} != {2009-09-30 17:00:08.0}");
-
-    // One NoteDb, slop is allowed.
-    b1 =
-        new ChangeBundle(c, messages(), latest(c), approvals(a1), comments(), reviewers(), NOTE_DB);
-    b2 =
-        new ChangeBundle(
-            c, messages(), latest(c), approvals(a2), comments(), reviewers(), REVIEW_DB);
-    assertNoDiffs(b1, b2);
-
-    // But not too much slop.
-    superWindowResolution();
-    PatchSetApproval a3 = clone(a1);
-    a3.setGranted(TimeUtil.nowTs());
-    b1 =
-        new ChangeBundle(c, messages(), latest(c), approvals(a1), comments(), reviewers(), NOTE_DB);
-    ChangeBundle b3 =
-        new ChangeBundle(
-            c, messages(), latest(c), approvals(a3), comments(), reviewers(), REVIEW_DB);
-    String msg =
-        "granted differs for PatchSetApproval.Key "
-            + c.getId()
-            + "%2C1,100,Code-Review in NoteDb vs. ReviewDb:"
-            + " {2009-09-30 17:00:07.0} != {2009-09-30 17:00:15.0}";
-    assertDiffs(b1, b3, msg);
-    assertDiffs(b3, b1, msg);
-  }
-
-  @Test
-  public void diffPatchSetApprovalsAllowsTruncatedTimestampInNoteDb() throws Exception {
-    Change c = TestChanges.newChange(project, accountId);
-    PatchSetApproval a1 =
-        new PatchSetApproval(
-            new PatchSetApproval.Key(c.currentPatchSetId(), accountId, new LabelId("Code-Review")),
-            (short) 1,
-            c.getCreatedOn());
-    PatchSetApproval a2 = clone(a1);
-    a2.setGranted(
-        new Timestamp(
-            LocalDate.of(1900, Month.JANUARY, 1)
-                .atStartOfDay()
-                .atZone(ZoneId.of(TIMEZONE_ID))
-                .toInstant()
-                .toEpochMilli()));
-
-    // Both are ReviewDb, exact match is required.
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c, messages(), latest(c), approvals(a1), comments(), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c, messages(), latest(c), approvals(a2), comments(), reviewers(), REVIEW_DB);
-    assertDiffs(
-        b1,
-        b2,
-        "granted differs for PatchSetApproval.Key "
-            + c.getId()
-            + "%2C1,100,Code-Review:"
-            + " {2009-09-30 17:00:00.0} != {1900-01-01 00:00:00.0}");
-
-    // Truncating NoteDb timestamp is allowed.
-    b1 =
-        new ChangeBundle(c, messages(), latest(c), approvals(a1), comments(), reviewers(), NOTE_DB);
-    b2 =
-        new ChangeBundle(
-            c, messages(), latest(c), approvals(a2), comments(), reviewers(), REVIEW_DB);
-    assertNoDiffs(b1, b2);
-    assertNoDiffs(b2, b1);
-  }
-
-  @Test
-  public void diffPatchSetApprovalsIgnoresPostSubmitBitOnZeroVote() throws Exception {
-    Change c = TestChanges.newChange(project, accountId);
-    c.setStatus(Change.Status.MERGED);
-    PatchSetApproval a1 =
-        new PatchSetApproval(
-            new PatchSetApproval.Key(c.currentPatchSetId(), accountId, new LabelId("Code-Review")),
-            (short) 0,
-            TimeUtil.nowTs());
-    a1.setPostSubmit(false);
-    PatchSetApproval a2 = clone(a1);
-    a2.setPostSubmit(true);
-
-    // Both are ReviewDb, exact match is required.
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c, messages(), latest(c), approvals(a1), comments(), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c, messages(), latest(c), approvals(a2), comments(), reviewers(), REVIEW_DB);
-    assertDiffs(
-        b1,
-        b2,
-        "postSubmit differs for PatchSetApproval.Key "
-            + c.getId()
-            + "%2C1,100,Code-Review:"
-            + " {false} != {true}");
-
-    // One NoteDb, postSubmit is ignored.
-    b1 =
-        new ChangeBundle(
-            c, messages(), latest(c), approvals(a1), comments(), reviewers(), REVIEW_DB);
-    b2 =
-        new ChangeBundle(c, messages(), latest(c), approvals(a2), comments(), reviewers(), NOTE_DB);
-    assertNoDiffs(b1, b2);
-    assertNoDiffs(b2, b1);
-
-    // postSubmit is not ignored if vote isn't 0.
-    a1.setValue((short) 1);
-    a2.setValue((short) 1);
-    assertDiffs(
-        b1,
-        b2,
-        "postSubmit differs for PatchSetApproval.Key "
-            + c.getId()
-            + "%2C1,100,Code-Review:"
-            + " {false} != {true}");
-    assertDiffs(
-        b2,
-        b1,
-        "postSubmit differs for PatchSetApproval.Key "
-            + c.getId()
-            + "%2C1,100,Code-Review:"
-            + " {true} != {false}");
-  }
-
-  @Test
-  public void diffReviewers() throws Exception {
-    Change c = TestChanges.newChange(project, accountId);
-    Timestamp now = TimeUtil.nowTs();
-    ReviewerSet r1 = reviewers(REVIEWER, new Account.Id(1), now);
-    ReviewerSet r2 = reviewers(REVIEWER, new Account.Id(2), now);
-
-    ChangeBundle b1 =
-        new ChangeBundle(c, messages(), latest(c), approvals(), comments(), r1, REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(c, messages(), latest(c), approvals(), comments(), r2, REVIEW_DB);
-    assertNoDiffs(b1, b1);
-    assertNoDiffs(b2, b2);
-    assertDiffs(b1, b2, "reviewer sets differ: [1] only in A; [2] only in B");
-  }
-
-  @Test
-  public void diffReviewersIgnoresStateAndTimestamp() throws Exception {
-    Change c = TestChanges.newChange(project, accountId);
-    ReviewerSet r1 = reviewers(REVIEWER, new Account.Id(1), TimeUtil.nowTs());
-    ReviewerSet r2 = reviewers(CC, new Account.Id(1), TimeUtil.nowTs());
-
-    ChangeBundle b1 =
-        new ChangeBundle(c, messages(), latest(c), approvals(), comments(), r1, REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(c, messages(), latest(c), approvals(), comments(), r2, REVIEW_DB);
-    assertNoDiffs(b1, b1);
-    assertNoDiffs(b2, b2);
-  }
-
-  @Test
-  public void diffPatchLineCommentKeySets() throws Exception {
-    Change c = TestChanges.newChange(project, accountId);
-    int id = c.getId().get();
-    PatchLineComment c1 =
-        new PatchLineComment(
-            new PatchLineComment.Key(new Patch.Key(c.currentPatchSetId(), "filename1"), "uuid1"),
-            5,
-            accountId,
-            null,
-            TimeUtil.nowTs());
-    PatchLineComment c2 =
-        new PatchLineComment(
-            new PatchLineComment.Key(new Patch.Key(c.currentPatchSetId(), "filename2"), "uuid2"),
-            5,
-            accountId,
-            null,
-            TimeUtil.nowTs());
-
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c, messages(), latest(c), approvals(), comments(c1), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c, messages(), latest(c), approvals(), comments(c2), reviewers(), REVIEW_DB);
-
-    assertDiffs(
-        b1,
-        b2,
-        "PatchLineComment.Key sets differ:"
-            + " ["
-            + id
-            + ",1,filename1,uuid1] only in A;"
-            + " ["
-            + id
-            + ",1,filename2,uuid2] only in B");
-  }
-
-  @Test
-  public void diffPatchLineComments() throws Exception {
-    Change c = TestChanges.newChange(project, accountId);
-    PatchLineComment c1 =
-        new PatchLineComment(
-            new PatchLineComment.Key(new Patch.Key(c.currentPatchSetId(), "filename"), "uuid"),
-            5,
-            accountId,
-            null,
-            TimeUtil.nowTs());
-    PatchLineComment c2 = clone(c1);
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c, messages(), latest(c), approvals(), comments(c1), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c, messages(), latest(c), approvals(), comments(c2), reviewers(), REVIEW_DB);
-
-    assertNoDiffs(b1, b2);
-
-    c2.setStatus(PatchLineComment.Status.PUBLISHED);
-    assertDiffs(
-        b1,
-        b2,
-        "status differs for PatchLineComment.Key " + c.getId() + ",1,filename,uuid: {d} != {P}");
-  }
-
-  @Test
-  public void diffPatchLineCommentsMixedSourcesAllowsSlop() throws Exception {
-    subWindowResolution();
-    Change c = TestChanges.newChange(project, accountId);
-    PatchLineComment c1 =
-        new PatchLineComment(
-            new PatchLineComment.Key(new Patch.Key(c.currentPatchSetId(), "filename"), "uuid"),
-            5,
-            accountId,
-            null,
-            truncateToSecond(TimeUtil.nowTs()));
-    PatchLineComment c2 = clone(c1);
-    c2.setWrittenOn(TimeUtil.nowTs());
-
-    // Both are ReviewDb, exact timestamp match is required.
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c, messages(), latest(c), approvals(), comments(c1), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c, messages(), latest(c), approvals(), comments(c2), reviewers(), REVIEW_DB);
-    assertDiffs(
-        b1,
-        b2,
-        "writtenOn differs for PatchLineComment.Key "
-            + c.getId()
-            + ",1,filename,uuid:"
-            + " {2009-09-30 17:00:02.0} != {2009-09-30 17:00:03.0}");
-
-    // One NoteDb, slop is allowed.
-    b1 =
-        new ChangeBundle(c, messages(), latest(c), approvals(), comments(c1), reviewers(), NOTE_DB);
-    b2 =
-        new ChangeBundle(
-            c, messages(), latest(c), approvals(), comments(c2), reviewers(), REVIEW_DB);
-    assertNoDiffs(b1, b2);
-
-    // But not too much slop.
-    superWindowResolution();
-    PatchLineComment c3 = clone(c1);
-    c3.setWrittenOn(TimeUtil.nowTs());
-    b1 =
-        new ChangeBundle(c, messages(), latest(c), approvals(), comments(c1), reviewers(), NOTE_DB);
-    ChangeBundle b3 =
-        new ChangeBundle(
-            c, messages(), latest(c), approvals(), comments(c3), reviewers(), REVIEW_DB);
-    String msg =
-        "writtenOn differs for PatchLineComment.Key "
-            + c.getId()
-            + ",1,filename,uuid in NoteDb vs. ReviewDb:"
-            + " {2009-09-30 17:00:02.0} != {2009-09-30 17:00:10.0}";
-    assertDiffs(b1, b3, msg);
-    assertDiffs(b3, b1, msg);
-  }
-
-  @Test
-  public void diffPatchLineCommentsIgnoresCommentsOnInvalidPatchSet() throws Exception {
-    Change c = TestChanges.newChange(project, accountId);
-    PatchLineComment c1 =
-        new PatchLineComment(
-            new PatchLineComment.Key(new Patch.Key(c.currentPatchSetId(), "filename1"), "uuid1"),
-            5,
-            accountId,
-            null,
-            TimeUtil.nowTs());
-    PatchLineComment c2 =
-        new PatchLineComment(
-            new PatchLineComment.Key(
-                new Patch.Key(new PatchSet.Id(c.getId(), 0), "filename2"), "uuid2"),
-            5,
-            accountId,
-            null,
-            TimeUtil.nowTs());
-
-    ChangeBundle b1 =
-        new ChangeBundle(
-            c, messages(), latest(c), approvals(), comments(c1, c2), reviewers(), REVIEW_DB);
-    ChangeBundle b2 =
-        new ChangeBundle(
-            c, messages(), latest(c), approvals(), comments(c1), reviewers(), REVIEW_DB);
-    assertNoDiffs(b1, b2);
-  }
-
-  private static void assertNoDiffs(ChangeBundle a, ChangeBundle b) {
-    assertThat(a.differencesFrom(b)).isEmpty();
-    assertThat(b.differencesFrom(a)).isEmpty();
-  }
-
-  private static void assertDiffs(ChangeBundle a, ChangeBundle b, String first, String... rest) {
-    List<String> actual = a.differencesFrom(b);
-    if (actual.size() == 1 && rest.length == 0) {
-      // This error message is much easier to read.
-      assertThat(actual.get(0)).isEqualTo(first);
-    } else {
-      List<String> expected = new ArrayList<>(1 + rest.length);
-      expected.add(first);
-      Collections.addAll(expected, rest);
-      assertThat(actual).containsExactlyElementsIn(expected).inOrder();
-    }
-    assertThat(a).isNotEqualTo(b);
-  }
-
-  private static List<ChangeMessage> messages(ChangeMessage... ents) {
-    return Arrays.asList(ents);
-  }
-
-  private static List<PatchSet> patchSets(PatchSet... ents) {
-    return Arrays.asList(ents);
-  }
-
-  private static List<PatchSet> latest(Change c) {
-    PatchSet ps = new PatchSet(c.currentPatchSetId());
-    ps.setCreatedOn(c.getLastUpdatedOn());
-    return ImmutableList.of(ps);
-  }
-
-  private static List<PatchSetApproval> approvals(PatchSetApproval... ents) {
-    return Arrays.asList(ents);
-  }
-
-  private static ReviewerSet reviewers(Object... ents) {
-    checkArgument(ents.length % 3 == 0);
-    Table<ReviewerStateInternal, Account.Id, Timestamp> t = HashBasedTable.create();
-    for (int i = 0; i < ents.length; i += 3) {
-      t.put((ReviewerStateInternal) ents[i], (Account.Id) ents[i + 1], (Timestamp) ents[i + 2]);
-    }
-    return ReviewerSet.fromTable(t);
-  }
-
-  private static List<PatchLineComment> comments(PatchLineComment... ents) {
-    return Arrays.asList(ents);
-  }
-
-  private static Change clone(Change ent) {
-    return clone(CHANGE_CODEC, ent);
-  }
-
-  private static ChangeMessage clone(ChangeMessage ent) {
-    return clone(CHANGE_MESSAGE_CODEC, ent);
-  }
-
-  private static PatchSet clone(PatchSet ent) {
-    return clone(PATCH_SET_CODEC, ent);
-  }
-
-  private static PatchSetApproval clone(PatchSetApproval ent) {
-    return clone(PATCH_SET_APPROVAL_CODEC, ent);
-  }
-
-  private static PatchLineComment clone(PatchLineComment ent) {
-    return clone(PATCH_LINE_COMMENT_CODEC, ent);
-  }
-
-  private static <T> T clone(ProtobufCodec<T> codec, T obj) {
-    return codec.decode(codec.encodeToByteArray(obj));
-  }
-}
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesCacheTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesCacheTest.java
index 7b140b7..b4d9738 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesCacheTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesCacheTest.java
@@ -16,8 +16,8 @@
 
 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 static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.byteString;
-import static com.google.gerrit.server.cache.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.reviewdb.client.Change;
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
index 7b41ba3..2931b17 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
@@ -16,11 +16,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
-import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.APPROVAL_CODEC;
-import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.MESSAGE_CODEC;
-import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.PATCH_SET_CODEC;
-import static com.google.gerrit.server.cache.serialize.ProtoCacheSerializers.toByteString;
-import static com.google.gerrit.server.cache.testing.SerializedClassSubject.assertThatSerializedClass;
+import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
+import static com.google.gerrit.server.notedb.ChangeNotesState.Serializer.toByteString;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
@@ -39,6 +36,9 @@
 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.ReviewerByEmailSet;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.ReviewerStatusUpdate;
@@ -47,11 +47,10 @@
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerByEmailSetEntryProto;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerSetEntryProto;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerStatusUpdateProto;
-import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers.ObjectIdConverter;
+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.gwtorm.client.KeyUtil;
-import com.google.gwtorm.server.StandardKeyEncoder;
+import com.google.gerrit.testing.GerritBaseTests;
 import com.google.inject.TypeLiteral;
 import com.google.protobuf.ByteString;
 import java.lang.reflect.Type;
@@ -62,11 +61,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class ChangeNotesStateTest {
-  static {
-    KeyUtil.setEncoderImpl(new StandardKeyEncoder());
-  }
-
+public class ChangeNotesStateTest extends GerritBaseTests {
   private static final Change.Id ID = new Change.Id(123);
   private static final ObjectId SHA =
       ObjectId.fromString("1234567812345678123456781234567812345678");
@@ -345,14 +340,14 @@
     ps1.setUploader(new Account.Id(2000));
     ps1.setRevision(new RevId("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"));
     ps1.setCreatedOn(cols.createdOn());
-    ByteString ps1Bytes = toByteString(ps1, PATCH_SET_CODEC);
+    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());
-    ByteString ps2Bytes = toByteString(ps2, PATCH_SET_CODEC);
+    ByteString ps2Bytes = toByteString(ps2, PatchSetProtoConverter.INSTANCE);
     assertThat(ps2Bytes.size()).isEqualTo(66);
     assertThat(ps2Bytes).isNotEqualTo(ps1Bytes);
 
@@ -377,7 +372,7 @@
                 new PatchSet.Id(ID, 1), new Account.Id(2001), new LabelId("Code-Review")),
             (short) 1,
             new Timestamp(1212L));
-    ByteString a1Bytes = toByteString(a1, APPROVAL_CODEC);
+    ByteString a1Bytes = toByteString(a1, PatchSetApprovalProtoConverter.INSTANCE);
     assertThat(a1Bytes.size()).isEqualTo(43);
 
     PatchSetApproval a2 =
@@ -386,7 +381,7 @@
                 new PatchSet.Id(ID, 1), new Account.Id(2002), new LabelId("Verified")),
             (short) -1,
             new Timestamp(3434L));
-    ByteString a2Bytes = toByteString(a2, APPROVAL_CODEC);
+    ByteString a2Bytes = toByteString(a2, PatchSetApprovalProtoConverter.INSTANCE);
     assertThat(a2Bytes.size()).isEqualTo(49);
     assertThat(a2Bytes).isNotEqualTo(a1Bytes);
 
@@ -644,7 +639,7 @@
             new Account.Id(1000),
             new Timestamp(1212L),
             new PatchSet.Id(ID, 1));
-    ByteString m1Bytes = toByteString(m1, MESSAGE_CODEC);
+    ByteString m1Bytes = toByteString(m1, ChangeMessageProtoConverter.INSTANCE);
     assertThat(m1Bytes.size()).isEqualTo(35);
 
     ChangeMessage m2 =
@@ -653,7 +648,7 @@
             new Account.Id(2000),
             new Timestamp(3434L),
             new PatchSet.Id(ID, 2));
-    ByteString m2Bytes = toByteString(m2, MESSAGE_CODEC);
+    ByteString m2Bytes = toByteString(m2, ChangeMessageProtoConverter.INSTANCE);
     assertThat(m2Bytes.size()).isEqualTo(35);
     assertThat(m2Bytes).isNotEqualTo(m1Bytes);
 
@@ -709,19 +704,6 @@
   }
 
   @Test
-  public void serializeReadOnlyUntil() throws Exception {
-    assertRoundTrip(
-        newBuilder().readOnlyUntil(new Timestamp(1212L)).build(),
-        ChangeNotesStateProto.newBuilder()
-            .setMetaId(SHA_BYTES)
-            .setChangeId(ID.get())
-            .setColumns(colsProto)
-            .setReadOnlyUntil(1212L)
-            .setHasReadOnlyUntil(true)
-            .build());
-  }
-
-  @Test
   public void changeNotesStateMethods() throws Exception {
     assertThatSerializedClass(ChangeNotesState.class)
         .hasAutoValueMethods(
@@ -751,7 +733,6 @@
                 .put(
                     "publishedComments",
                     new TypeLiteral<ImmutableListMultimap<RevId, Comment>>() {}.getType())
-                .put("readOnlyUntil", Timestamp.class)
                 .build());
   }
 
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
index 549f5db..a6c0224 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -14,14 +14,15 @@
 
 package com.google.gerrit.server.notedb;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
 import static com.google.gerrit.reviewdb.client.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 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;
 
@@ -34,6 +35,7 @@
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.SubmitRecord;
+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;
@@ -45,7 +47,6 @@
 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.server.ReviewDbUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.ReviewerSet;
@@ -54,14 +55,11 @@
 import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.testing.TestChanges;
-import com.google.gerrit.testing.TestTimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.sql.Timestamp;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
@@ -408,10 +406,10 @@
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    List<PatchSetApproval> approvals =
-        ReviewDbUtil.intKeyOrdering()
-            .onResultOf(PatchSetApproval::getAccountId)
-            .sortedCopy(notes.getApprovals().get(c.currentPatchSetId()));
+    ImmutableList<PatchSetApproval> approvals =
+        notes.getApprovals().get(c.currentPatchSetId()).stream()
+            .sorted(comparing(a -> a.getAccountId().get()))
+            .collect(toImmutableList());
     assertThat(approvals).hasSize(2);
 
     assertThat(approvals.get(0).getAccountId()).isEqualTo(changeOwner.getAccountId());
@@ -956,7 +954,7 @@
     Change c = TestChanges.newChange(project, changeOwner.getAccountId());
     String trimmedSubj = c.getSubject();
     c.setCurrentPatchSet(c.currentPatchSetId(), "  " + trimmedSubj, c.getOriginalSubject());
-    ChangeUpdate update = newUpdate(c, changeOwner);
+    ChangeUpdate update = newUpdateForNewChange(c, changeOwner);
     update.setChangeId(c.getKey().get());
     update.setBranch(c.getDest().get());
     update.commit();
@@ -968,7 +966,7 @@
 
     c = TestChanges.newChange(project, changeOwner.getAccountId());
     c.setCurrentPatchSet(c.currentPatchSetId(), tabSubj, c.getOriginalSubject());
-    update = newUpdate(c, changeOwner);
+    update = newUpdateForNewChange(c, changeOwner);
     update.setChangeId(c.getKey().get());
     update.setBranch(c.getDest().get());
     update.commit();
@@ -995,7 +993,7 @@
     try {
       newNotes(c);
       fail("Expected IOException");
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       assertCause(
           e,
           ConfigInvalidException.class,
@@ -2605,7 +2603,7 @@
     }
 
     // Looking at drafts directly shows the zombie comment.
-    DraftCommentNotes draftNotes = draftNotesFactory.create(c, otherUserId);
+    DraftCommentNotes draftNotes = draftNotesFactory.create(c.getId(), otherUserId);
     assertThat(draftNotes.load().getComments().get(rev1)).containsExactly(comment1, comment2);
 
     // Zombie comment is filtered out of drafts via ChangeNotes.
@@ -2760,76 +2758,6 @@
   }
 
   @Test
-  public void readOnlyUntilExpires() throws Exception {
-    Change c = newChange();
-    ChangeUpdate update = newUpdate(c, changeOwner);
-    Timestamp until = new Timestamp(TimeUtil.nowMs() + 10000);
-    update.setReadOnlyUntil(until);
-    update.commit();
-
-    update = newUpdate(c, changeOwner);
-    update.setTopic("failing-topic");
-    try {
-      update.commit();
-      assert_().fail("expected OrmException");
-    } catch (OrmException e) {
-      assertThat(e.getMessage()).contains("read-only until");
-    }
-
-    ChangeNotes notes = newNotes(c);
-    assertThat(notes.getChange().getTopic()).isNotEqualTo("failing-topic");
-    assertThat(notes.getReadOnlyUntil()).isEqualTo(until);
-
-    TestTimeUtil.incrementClock(30, TimeUnit.SECONDS);
-    update = newUpdate(c, changeOwner);
-    update.setTopic("succeeding-topic");
-    update.commit();
-
-    // Write succeeded; lease still exists, even though it's expired.
-    notes = newNotes(c);
-    assertThat(notes.getChange().getTopic()).isEqualTo("succeeding-topic");
-    assertThat(notes.getReadOnlyUntil()).isEqualTo(until);
-
-    // New lease takes precedence.
-    update = newUpdate(c, changeOwner);
-    until = new Timestamp(TimeUtil.nowMs() + 10000);
-    update.setReadOnlyUntil(until);
-    update.commit();
-    assertThat(newNotes(c).getReadOnlyUntil()).isEqualTo(until);
-  }
-
-  @Test
-  public void readOnlyUntilCleared() throws Exception {
-    Change c = newChange();
-    ChangeUpdate update = newUpdate(c, changeOwner);
-    Timestamp until = new Timestamp(TimeUtil.nowMs() + TimeUnit.DAYS.toMillis(30));
-    update.setReadOnlyUntil(until);
-    update.commit();
-
-    update = newUpdate(c, changeOwner);
-    update.setTopic("failing-topic");
-    try {
-      update.commit();
-      assert_().fail("expected OrmException");
-    } catch (OrmException e) {
-      assertThat(e.getMessage()).contains("read-only until");
-    }
-
-    // Sentinel timestamp of 0 can be written to clear lease.
-    update = newUpdate(c, changeOwner);
-    update.setReadOnlyUntil(new Timestamp(0));
-    update.commit();
-
-    update = newUpdate(c, changeOwner);
-    update.setTopic("succeeding-topic");
-    update.commit();
-
-    ChangeNotes notes = newNotes(c);
-    assertThat(notes.getChange().getTopic()).isEqualTo("succeeding-topic");
-    assertThat(notes.getReadOnlyUntil()).isEqualTo(new Timestamp(0));
-  }
-
-  @Test
   public void privateDefault() throws Exception {
     Change c = newChange();
     ChangeNotes notes = newNotes(c);
@@ -3067,7 +2995,7 @@
   public void setRevertOfPersistsValue() throws Exception {
     Change changeToRevert = newChange();
     Change c = TestChanges.newChange(project, changeOwner.getAccountId());
-    ChangeUpdate update = newUpdate(c, changeOwner);
+    ChangeUpdate update = newUpdateForNewChange(c, changeOwner);
     update.setChangeId(c.getKey().get());
     update.setRevertOf(changeToRevert.getId().get());
     update.commit();
@@ -3088,7 +3016,7 @@
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
     update.setRevertOf(newChange().getId().get());
-    exception.expect(OrmException.class);
+    exception.expect(StorageException.class);
     exception.expectMessage("Given ChangeUpdate is only allowed on initial commit");
     update.commit();
   }
diff --git a/javatests/com/google/gerrit/server/notedb/CommentJsonMigratorTest.java b/javatests/com/google/gerrit/server/notedb/CommentJsonMigratorTest.java
deleted file mode 100644
index dee6181..0000000
--- a/javatests/com/google/gerrit/server/notedb/CommentJsonMigratorTest.java
+++ /dev/null
@@ -1,527 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.collect.ImmutableMap.toImmutableMap;
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ListMultimap;
-import com.google.common.collect.Multimaps;
-import com.google.gerrit.reviewdb.client.Change;
-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.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.server.CommentsUtil;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.notedb.CommentJsonMigrator.ProjectMigrationResult;
-import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gerrit.testing.TestChanges;
-import com.google.inject.Inject;
-import java.io.ByteArrayOutputStream;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicInteger;
-import org.eclipse.jgit.junit.TestRepository;
-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.notes.NoteMap;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevSort;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.junit.Before;
-import org.junit.Test;
-
-public class CommentJsonMigratorTest extends AbstractChangeNotesTest {
-  private CommentJsonMigrator migrator;
-  @Inject private ChangeNoteUtil noteUtil;
-  @Inject private CommentsUtil commentsUtil;
-  @Inject private LegacyChangeNoteWrite legacyChangeNoteWrite;
-  @Inject private AllUsersName allUsersName;
-
-  private AtomicInteger uuidCounter;
-
-  @Before
-  public void setUpCounter() {
-    uuidCounter = new AtomicInteger();
-    migrator = new CommentJsonMigrator(new ChangeNoteJson(), "gerrit", allUsersName);
-  }
-
-  @Test
-  public void noOpIfAllCommentsAreJson() throws Exception {
-    Change c = newChange();
-    incrementPatchSet(c);
-
-    ChangeNotes notes = newNotes(c);
-    ChangeUpdate update = newUpdate(c, changeOwner);
-    Comment ps1Comment = newComment(notes, 1, "comment on ps1");
-    update.putComment(Status.PUBLISHED, ps1Comment);
-    update.commit();
-
-    notes = newNotes(c);
-    update = newUpdate(c, changeOwner);
-    Comment ps2Comment = newComment(notes, 2, "comment on ps2");
-    update.putComment(Status.PUBLISHED, ps2Comment);
-    update.commit();
-
-    notes = newNotes(c);
-    assertThat(getToStringRepresentations(notes.getComments()))
-        .containsExactly(
-            getRevId(notes, 1), ps1Comment.toString(),
-            getRevId(notes, 2), ps2Comment.toString());
-
-    ChangeNotes oldNotes = notes;
-    checkMigrate(project, ImmutableList.of());
-    assertNoDifferences(notes, oldNotes);
-    assertThat(notes.getMetaId()).isEqualTo(oldNotes.getMetaId());
-  }
-
-  @Test
-  public void migratePublishedComments() throws Exception {
-    Change c = newChange();
-    incrementPatchSet(c);
-
-    ChangeNotes notes = newNotes(c);
-
-    Comment ps1Comment1 = newComment(notes, 1, "first comment on ps1");
-    Comment ps2Comment1 = newComment(notes, 2, "first comment on ps2");
-    Comment ps1Comment2 = newComment(notes, 1, "second comment on ps1");
-
-    // Construct legacy format 'by hand'.
-    ByteArrayOutputStream out1 = new ByteArrayOutputStream(0);
-    legacyChangeNoteWrite.buildNote(
-        ImmutableListMultimap.<Integer, Comment>builder().put(1, ps1Comment1).build(), out1);
-
-    ByteArrayOutputStream out2 = new ByteArrayOutputStream(0);
-    legacyChangeNoteWrite.buildNote(
-        ImmutableListMultimap.<Integer, Comment>builder().put(2, ps2Comment1).build(), out2);
-
-    ByteArrayOutputStream out3 = new ByteArrayOutputStream(0);
-    legacyChangeNoteWrite.buildNote(
-        ImmutableListMultimap.<Integer, Comment>builder()
-            .put(1, ps1Comment2)
-            .put(1, ps1Comment1)
-            .build(),
-        out3);
-
-    TestRepository<Repository> testRepository = new TestRepository<>(repo, rw);
-
-    String metaRefName = RefNames.changeMetaRef(c.getId());
-    testRepository
-        .branch(metaRefName)
-        .commit()
-        .message("Review ps 1\n\nPatch-set: 1")
-        .add(ps1Comment1.revId, out1.toString())
-        .author(serverIdent)
-        .committer(serverIdent)
-        .create();
-
-    testRepository
-        .branch(metaRefName)
-        .commit()
-        .message("Review ps 2\n\nPatch-set: 2")
-        .add(ps2Comment1.revId, out2.toString())
-        .add(ps1Comment1.revId, out3.toString())
-        .author(serverIdent)
-        .committer(serverIdent)
-        .create();
-
-    notes = newNotes(c);
-    assertThat(getToStringRepresentations(notes.getComments()))
-        .containsExactly(
-            getRevId(notes, 1), ps1Comment1.toString(),
-            getRevId(notes, 1), ps1Comment2.toString(),
-            getRevId(notes, 2), ps2Comment1.toString());
-
-    // Comments at each commit all have legacy format.
-    ImmutableList<RevCommit> oldLog = log(project, RefNames.changeMetaRef(c.getId()));
-    assertThat(oldLog).hasSize(4);
-    assertThat(getLegacyFormatMapForPublishedComments(notes, oldLog.get(0))).isEmpty();
-    assertThat(getLegacyFormatMapForPublishedComments(notes, oldLog.get(1))).isEmpty();
-    assertThat(getLegacyFormatMapForPublishedComments(notes, oldLog.get(2)))
-        .containsExactly(ps1Comment1.key, true);
-    assertThat(getLegacyFormatMapForPublishedComments(notes, oldLog.get(3)))
-        .containsExactly(ps1Comment1.key, true, ps1Comment2.key, true, ps2Comment1.key, true);
-
-    // Check that dryRun doesn't touch anything.
-    String refName = RefNames.changeMetaRef(c.getId());
-    ObjectId before = repo.getRefDatabase().getRef(refName).getObjectId();
-    ProjectMigrationResult dryRunResult = migrator.migrateProject(project, repo, true);
-    ObjectId after = repo.getRefDatabase().getRef(refName).getObjectId();
-    assertThat(before).isEqualTo(after);
-    assertThat(dryRunResult.refsUpdated).isEqualTo(ImmutableList.of(refName));
-
-    ChangeNotes oldNotes = notes;
-    checkMigrate(project, ImmutableList.of(refName));
-
-    // Comment content is the same.
-    notes = newNotes(c);
-    assertNoDifferences(notes, oldNotes);
-    assertThat(getToStringRepresentations(notes.getComments()))
-        .containsExactly(
-            getRevId(notes, 1), ps1Comment1.toString(),
-            getRevId(notes, 1), ps1Comment2.toString(),
-            getRevId(notes, 2), ps2Comment1.toString());
-
-    // Comments at each commit all have JSON format.
-    ImmutableList<RevCommit> newLog = log(project, RefNames.changeMetaRef(c.getId()));
-    assertLogEqualExceptTrees(newLog, oldLog);
-    assertThat(getLegacyFormatMapForPublishedComments(notes, newLog.get(0))).isEmpty();
-    assertThat(getLegacyFormatMapForPublishedComments(notes, newLog.get(1))).isEmpty();
-    assertThat(getLegacyFormatMapForPublishedComments(notes, newLog.get(2)))
-        .containsExactly(ps1Comment1.key, false);
-    assertThat(getLegacyFormatMapForPublishedComments(notes, newLog.get(3)))
-        .containsExactly(ps1Comment1.key, false, ps1Comment2.key, false, ps2Comment1.key, false);
-  }
-
-  @Test
-  public void migrateDraftComments() throws Exception {
-    Change c = newChange();
-    incrementPatchSet(c);
-
-    ChangeNotes notes = newNotes(c);
-    ObjectId origMetaId = notes.getMetaId();
-
-    Comment ownerCommentPs1 = newComment(notes, 1, "owner comment on ps1", changeOwner);
-    Comment ownerCommentPs2 = newComment(notes, 2, "owner comment on ps2", changeOwner);
-    Comment otherCommentPs1 = newComment(notes, 1, "other user comment on ps1", otherUser);
-
-    ByteArrayOutputStream out1 = new ByteArrayOutputStream(0);
-    legacyChangeNoteWrite.buildNote(
-        ImmutableListMultimap.<Integer, Comment>builder().put(1, ownerCommentPs1).build(), out1);
-
-    ByteArrayOutputStream out2 = new ByteArrayOutputStream(0);
-    legacyChangeNoteWrite.buildNote(
-        ImmutableListMultimap.<Integer, Comment>builder().put(2, ownerCommentPs2).build(), out2);
-
-    ByteArrayOutputStream out3 = new ByteArrayOutputStream(0);
-    legacyChangeNoteWrite.buildNote(
-        ImmutableListMultimap.<Integer, Comment>builder().put(1, otherCommentPs1).build(), out3);
-
-    try (Repository allUsersRepo = repoManager.openRepository(allUsers);
-        RevWalk allUsersRw = new RevWalk(allUsersRepo)) {
-      TestRepository<Repository> testRepository = new TestRepository<>(allUsersRepo, allUsersRw);
-
-      testRepository
-          .branch(RefNames.refsDraftComments(c.getId(), changeOwner.getAccountId()))
-          .commit()
-          .message("Review ps 1\n\nPatch-set: 1")
-          .add(ownerCommentPs1.revId, out1.toString())
-          .author(serverIdent)
-          .committer(serverIdent)
-          .create();
-
-      testRepository
-          .branch(RefNames.refsDraftComments(c.getId(), changeOwner.getAccountId()))
-          .commit()
-          .message("Review ps 1\n\nPatch-set: 2")
-          .add(ownerCommentPs2.revId, out2.toString())
-          .author(serverIdent)
-          .committer(serverIdent)
-          .create();
-
-      testRepository
-          .branch(RefNames.refsDraftComments(c.getId(), otherUser.getAccountId()))
-          .commit()
-          .message("Review ps 2\n\nPatch-set: 2")
-          .add(otherCommentPs1.revId, out3.toString())
-          .author(serverIdent)
-          .committer(serverIdent)
-          .create();
-    }
-
-    notes = newNotes(c);
-    assertThat(getToStringRepresentations(notes.getDraftComments(changeOwner.getAccountId())))
-        .containsExactly(
-            getRevId(notes, 1), ownerCommentPs1.toString(),
-            getRevId(notes, 2), ownerCommentPs2.toString());
-    assertThat(getToStringRepresentations(notes.getDraftComments(otherUser.getAccountId())))
-        .containsExactly(getRevId(notes, 1), otherCommentPs1.toString());
-
-    // Comments at each commit all have legacy format.
-    ImmutableList<RevCommit> oldOwnerLog =
-        log(allUsers, RefNames.refsDraftComments(c.getId(), changeOwner.getAccountId()));
-    assertThat(oldOwnerLog).hasSize(2);
-    assertThat(getLegacyFormatMapForDraftComments(notes, oldOwnerLog.get(0)))
-        .containsExactly(ownerCommentPs1.key, true);
-    assertThat(getLegacyFormatMapForDraftComments(notes, oldOwnerLog.get(1)))
-        .containsExactly(ownerCommentPs1.key, true, ownerCommentPs2.key, true);
-
-    ImmutableList<RevCommit> oldOtherLog =
-        log(allUsers, RefNames.refsDraftComments(c.getId(), otherUser.getAccountId()));
-    assertThat(oldOtherLog).hasSize(1);
-    assertThat(getLegacyFormatMapForDraftComments(notes, oldOtherLog.get(0)))
-        .containsExactly(otherCommentPs1.key, true);
-
-    ChangeNotes oldNotes = notes;
-    checkMigrate(
-        allUsers,
-        ImmutableList.of(
-            RefNames.refsDraftComments(c.getId(), changeOwner.getAccountId()),
-            RefNames.refsDraftComments(c.getId(), otherUser.getAccountId())));
-    assertNoDifferences(notes, oldNotes);
-
-    // Migration doesn't touch change ref.
-    assertThat(repo.exactRef(RefNames.changeMetaRef(c.getId())).getObjectId())
-        .isEqualTo(origMetaId);
-
-    // Comment content is the same.
-    notes = newNotes(c);
-    assertThat(getToStringRepresentations(notes.getDraftComments(changeOwner.getAccountId())))
-        .containsExactly(
-            getRevId(notes, 1), ownerCommentPs1.toString(),
-            getRevId(notes, 2), ownerCommentPs2.toString());
-    assertThat(getToStringRepresentations(notes.getDraftComments(otherUser.getAccountId())))
-        .containsExactly(getRevId(notes, 1), otherCommentPs1.toString());
-
-    // Comments at each commit all have JSON format.
-    ImmutableList<RevCommit> newOwnerLog =
-        log(allUsers, RefNames.refsDraftComments(c.getId(), changeOwner.getAccountId()));
-    assertLogEqualExceptTrees(newOwnerLog, oldOwnerLog);
-    assertThat(getLegacyFormatMapForDraftComments(notes, newOwnerLog.get(0)))
-        .containsExactly(ownerCommentPs1.key, false);
-    assertThat(getLegacyFormatMapForDraftComments(notes, newOwnerLog.get(1)))
-        .containsExactly(ownerCommentPs1.key, false, ownerCommentPs2.key, false);
-
-    ImmutableList<RevCommit> newOtherLog =
-        log(allUsers, RefNames.refsDraftComments(c.getId(), otherUser.getAccountId()));
-    assertLogEqualExceptTrees(newOtherLog, oldOtherLog);
-    assertThat(getLegacyFormatMapForDraftComments(notes, newOtherLog.get(0)))
-        .containsExactly(otherCommentPs1.key, false);
-  }
-
-  @Test
-  public void migrateMixOfJsonAndLegacyComments() throws Exception {
-    // 3 comments: legacy, JSON, legacy. Because adding a comment necessarily rewrites the entire
-    // note, these comments need to be on separate patch sets.
-    Change c = newChange();
-    incrementPatchSet(c);
-    incrementPatchSet(c);
-
-    ChangeNotes notes = newNotes(c);
-
-    Comment ps1Comment = newComment(notes, 1, "comment on ps1 (legacy)");
-
-    ByteArrayOutputStream out1 = new ByteArrayOutputStream(0);
-    legacyChangeNoteWrite.buildNote(
-        ImmutableListMultimap.<Integer, Comment>builder().put(1, ps1Comment).build(), out1);
-
-    TestRepository<Repository> testRepository = new TestRepository<>(repo, rw);
-
-    String metaRefName = RefNames.changeMetaRef(c.getId());
-    testRepository
-        .branch(metaRefName)
-        .commit()
-        .message("Review ps 1\n\nPatch-set: 1")
-        .add(ps1Comment.revId, out1.toString())
-        .author(serverIdent)
-        .committer(serverIdent)
-        .create();
-
-    notes = newNotes(c);
-    ChangeUpdate update = newUpdate(c, changeOwner);
-    Comment ps2Comment = newComment(notes, 2, "comment on ps2 (JSON)");
-    update.putComment(Status.PUBLISHED, ps2Comment);
-    update.commit();
-
-    Comment ps3Comment = newComment(notes, 3, "comment on ps3 (legacy)");
-    ByteArrayOutputStream out3 = new ByteArrayOutputStream(0);
-    legacyChangeNoteWrite.buildNote(
-        ImmutableListMultimap.<Integer, Comment>builder().put(3, ps3Comment).build(), out3);
-
-    testRepository
-        .branch(metaRefName)
-        .commit()
-        .message("Review ps 3\n\nPatch-set: 3")
-        .add(ps3Comment.revId, out3.toString())
-        .author(serverIdent)
-        .committer(serverIdent)
-        .create();
-
-    notes = newNotes(c);
-    assertThat(getToStringRepresentations(notes.getComments()))
-        .containsExactly(
-            getRevId(notes, 1), ps1Comment.toString(),
-            getRevId(notes, 2), ps2Comment.toString(),
-            getRevId(notes, 3), ps3Comment.toString());
-
-    // Comments at each commit match expected format.
-    ImmutableList<RevCommit> oldLog = log(project, RefNames.changeMetaRef(c.getId()));
-    assertThat(oldLog).hasSize(6);
-    assertThat(getLegacyFormatMapForPublishedComments(notes, oldLog.get(0))).isEmpty();
-    assertThat(getLegacyFormatMapForPublishedComments(notes, oldLog.get(1))).isEmpty();
-    assertThat(getLegacyFormatMapForPublishedComments(notes, oldLog.get(2))).isEmpty();
-    assertThat(getLegacyFormatMapForPublishedComments(notes, oldLog.get(3)))
-        .containsExactly(ps1Comment.key, true);
-    assertThat(getLegacyFormatMapForPublishedComments(notes, oldLog.get(4)))
-        .containsExactly(ps1Comment.key, true, ps2Comment.key, false);
-    assertThat(getLegacyFormatMapForPublishedComments(notes, oldLog.get(5)))
-        .containsExactly(ps1Comment.key, true, ps2Comment.key, false, ps3Comment.key, true);
-
-    ChangeNotes oldNotes = notes;
-    checkMigrate(project, ImmutableList.of(RefNames.changeMetaRef(c.getId())));
-    assertNoDifferences(notes, oldNotes);
-
-    // Comment content is the same.
-    notes = newNotes(c);
-    assertThat(getToStringRepresentations(notes.getComments()))
-        .containsExactly(
-            getRevId(notes, 1), ps1Comment.toString(),
-            getRevId(notes, 2), ps2Comment.toString(),
-            getRevId(notes, 3), ps3Comment.toString());
-
-    // Comments at each commit all have JSON format.
-    ImmutableList<RevCommit> newLog = log(project, RefNames.changeMetaRef(c.getId()));
-    assertLogEqualExceptTrees(newLog, oldLog);
-    assertThat(getLegacyFormatMapForPublishedComments(notes, newLog.get(0))).isEmpty();
-    assertThat(getLegacyFormatMapForPublishedComments(notes, newLog.get(1))).isEmpty();
-    assertThat(getLegacyFormatMapForPublishedComments(notes, newLog.get(2))).isEmpty();
-    assertThat(getLegacyFormatMapForPublishedComments(notes, newLog.get(3)))
-        .containsExactly(ps1Comment.key, false);
-    assertThat(getLegacyFormatMapForPublishedComments(notes, newLog.get(4)))
-        .containsExactly(ps1Comment.key, false, ps2Comment.key, false);
-    assertThat(getLegacyFormatMapForPublishedComments(notes, newLog.get(5)))
-        .containsExactly(ps1Comment.key, false, ps2Comment.key, false, ps3Comment.key, false);
-  }
-
-  private void checkMigrate(Project.NameKey project, List<String> expectedRefs) throws Exception {
-    try (Repository repo = repoManager.openRepository(project)) {
-      ProjectMigrationResult progress = migrator.migrateProject(project, repo, false);
-
-      assertThat(progress.ok).isTrue();
-      assertThat(progress.refsUpdated).isEqualTo(expectedRefs);
-    }
-  }
-
-  private Comment newComment(ChangeNotes notes, int psNum, String message) {
-    return newComment(notes, psNum, message, changeOwner);
-  }
-
-  private Comment newComment(
-      ChangeNotes notes, int psNum, String message, IdentifiedUser commenter) {
-    return newComment(
-        new PatchSet.Id(notes.getChangeId(), psNum),
-        "filename",
-        "uuid-" + uuidCounter.getAndIncrement(),
-        null,
-        0,
-        commenter,
-        null,
-        TimeUtil.nowTs(),
-        message,
-        (short) 1,
-        getRevId(notes, psNum).get(),
-        false);
-  }
-
-  private void incrementPatchSet(Change c) throws Exception {
-    TestChanges.incrementPatchSet(c);
-    RevCommit commit = tr.commit().message("PS" + c.currentPatchSetId().get()).create();
-    ChangeUpdate update = newUpdate(c, changeOwner);
-    update.setCommit(rw, commit);
-    update.commit();
-  }
-
-  private static RevId getRevId(ChangeNotes notes, int psNum) {
-    PatchSet.Id psId = new PatchSet.Id(notes.getChangeId(), psNum);
-    PatchSet ps = notes.getPatchSets().get(psId);
-    checkArgument(ps != null, "no patch set %s: %s", psNum, notes.getPatchSets());
-    return ps.getRevision();
-  }
-
-  private static ListMultimap<RevId, String> getToStringRepresentations(
-      ListMultimap<RevId, Comment> comments) {
-    // Use string representation for equality comparison in this test, because Comment#equals only
-    // compares keys.
-    return Multimaps.transformValues(comments, Comment::toString);
-  }
-
-  private ImmutableMap<Comment.Key, Boolean> getLegacyFormatMapForPublishedComments(
-      ChangeNotes notes, ObjectId metaId) throws Exception {
-    return getLegacyFormatMap(project, notes.getChangeId(), metaId, Status.PUBLISHED);
-  }
-
-  private ImmutableMap<Comment.Key, Boolean> getLegacyFormatMapForDraftComments(
-      ChangeNotes notes, ObjectId metaId) throws Exception {
-    return getLegacyFormatMap(allUsers, notes.getChangeId(), metaId, Status.DRAFT);
-  }
-
-  private ImmutableMap<Comment.Key, Boolean> getLegacyFormatMap(
-      Project.NameKey project, Change.Id changeId, ObjectId metaId, Status status)
-      throws Exception {
-    try (Repository repo = repoManager.openRepository(project);
-        ObjectReader reader = repo.newObjectReader();
-        RevWalk rw = new RevWalk(reader)) {
-      NoteMap noteMap = NoteMap.read(reader, rw.parseCommit(metaId));
-      RevisionNoteMap<ChangeRevisionNote> revNoteMap =
-          RevisionNoteMap.parse(
-              noteUtil.getChangeNoteJson(),
-              noteUtil.getLegacyChangeNoteRead(),
-              changeId,
-              reader,
-              noteMap,
-              status);
-      return revNoteMap.revisionNotes.values().stream()
-          .flatMap(crn -> crn.getComments().stream())
-          .collect(toImmutableMap(c -> c.key, c -> c.legacyFormat));
-    }
-  }
-
-  private ImmutableList<RevCommit> log(Project.NameKey project, String refName) throws Exception {
-    try (Repository repo = repoManager.openRepository(project);
-        RevWalk rw = new RevWalk(repo)) {
-      rw.sort(RevSort.TOPO);
-      rw.sort(RevSort.REVERSE);
-      Ref ref = repo.exactRef(refName);
-      checkArgument(ref != null, "missing ref: %s", refName);
-      rw.markStart(rw.parseCommit(ref.getObjectId()));
-      return ImmutableList.copyOf(rw);
-    }
-  }
-
-  private static void assertLogEqualExceptTrees(
-      ImmutableList<RevCommit> actualLog, ImmutableList<RevCommit> expectedLog) {
-    assertThat(actualLog).hasSize(expectedLog.size());
-    for (int i = 0; i < expectedLog.size(); i++) {
-      RevCommit actual = actualLog.get(i);
-      RevCommit expected = expectedLog.get(i);
-      assertThat(actual.getAuthorIdent())
-          .named("author of entry %s", i)
-          .isEqualTo(expected.getAuthorIdent());
-      assertThat(actual.getCommitterIdent())
-          .named("committer of entry %s", i)
-          .isEqualTo(expected.getCommitterIdent());
-      assertThat(actual.getFullMessage()).named("message of entry %s", i).isNotNull();
-      assertThat(actual.getFullMessage())
-          .named("message of entry %s", i)
-          .isEqualTo(expected.getFullMessage());
-    }
-  }
-
-  private void assertNoDifferences(ChangeNotes actual, ChangeNotes expected) throws Exception {
-    assertThat(
-            ChangeBundle.fromNotes(commentsUtil, actual)
-                .differencesFrom(ChangeBundle.fromNotes(commentsUtil, expected)))
-        .isEmpty();
-  }
-}
diff --git a/javatests/com/google/gerrit/server/notedb/CommentTimestampAdapterTest.java b/javatests/com/google/gerrit/server/notedb/CommentTimestampAdapterTest.java
index e7d8956..4dd2005 100644
--- a/javatests/com/google/gerrit/server/notedb/CommentTimestampAdapterTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommentTimestampAdapterTest.java
@@ -18,6 +18,7 @@
 
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
 import java.sql.Timestamp;
@@ -28,7 +29,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class CommentTimestampAdapterTest {
+public class CommentTimestampAdapterTest extends GerritBaseTests {
   /** Arbitrary time outside of a DST transition, as an ISO instant. */
   private static final String NON_DST_STR = "2017-02-07T10:20:30.123Z";
 
diff --git a/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java b/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
index 1b50431..5552572 100644
--- a/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
@@ -41,7 +41,7 @@
   @Test
   public void approvalsCommitFormatSimple() throws Exception {
     Change c = TestChanges.newChange(project, changeOwner.getAccountId(), 1);
-    ChangeUpdate update = newUpdate(c, changeOwner);
+    ChangeUpdate update = newUpdateForNewChange(c, changeOwner);
     update.putApproval("Verified", (short) 1);
     update.putApproval("Code-Review", (short) -1);
     update.putReviewer(changeOwner.getAccount().getId(), REVIEWER);
@@ -84,7 +84,7 @@
   @Test
   public void changeMessageCommitFormatSimple() throws Exception {
     Change c = TestChanges.newChange(project, changeOwner.getAccountId(), 1);
-    ChangeUpdate update = newUpdate(c, changeOwner);
+    ChangeUpdate update = newUpdateForNewChange(c, changeOwner);
     update.setChangeMessage("Just a little code change.\nHow about a new line");
     update.commit();
     assertThat(update.getRefName()).isEqualTo("refs/changes/01/1/meta");
@@ -110,7 +110,7 @@
   @Test
   public void changeWithRevision() throws Exception {
     Change c = TestChanges.newChange(project, changeOwner.getAccountId(), 1);
-    ChangeUpdate update = newUpdate(c, changeOwner);
+    ChangeUpdate update = newUpdateForNewChange(c, changeOwner);
     update.setChangeMessage("Foo");
     RevCommit commit = tr.commit().message("Subject").create();
     update.setCommit(rw, commit);
@@ -309,7 +309,7 @@
   public void leadingWhitespace() throws Exception {
     Change c = TestChanges.newChange(project, changeOwner.getAccountId());
     c.setCurrentPatchSet(c.currentPatchSetId(), "  " + c.getSubject(), c.getOriginalSubject());
-    ChangeUpdate update = newUpdate(c, changeOwner);
+    ChangeUpdate update = newUpdateForNewChange(c, changeOwner);
     update.setChangeId(c.getKey().get());
     update.setBranch(c.getDest().get());
     update.commit();
@@ -330,7 +330,7 @@
 
     c = TestChanges.newChange(project, changeOwner.getAccountId());
     c.setCurrentPatchSet(c.currentPatchSetId(), "\t\t" + c.getSubject(), c.getOriginalSubject());
-    update = newUpdate(c, changeOwner);
+    update = newUpdateForNewChange(c, changeOwner);
     update.setChangeId(c.getKey().get());
     update.setBranch(c.getDest().get());
     update.commit();
diff --git a/javatests/com/google/gerrit/server/notedb/IntBlobTest.java b/javatests/com/google/gerrit/server/notedb/IntBlobTest.java
new file mode 100644
index 0000000..1cbe61d
--- /dev/null
+++ b/javatests/com/google/gerrit/server/notedb/IntBlobTest.java
@@ -0,0 +1,202 @@
+// 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.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.truth.OptionalSubject.assertThat;
+
+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;
+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.ObjectId;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.junit.Before;
+import org.junit.Test;
+
+public class IntBlobTest {
+  // Note: Can't easily test GitRefUpdated behavior, since binding GitRefUpdated requires a thick
+  // stack of dependencies, and it's not just a simple interface or abstract class.
+
+  private Project.NameKey projectName;
+  private InMemoryRepository repo;
+  private TestRepository<InMemoryRepository> tr;
+  private RevWalk rw;
+
+  @Before
+  public void setUp() throws Exception {
+    projectName = new Project.NameKey("repo");
+    repo = new InMemoryRepository(new DfsRepositoryDescription(projectName.get()));
+    tr = new TestRepository<>(repo);
+    rw = tr.getRevWalk();
+  }
+
+  @Test
+  public void parseNoRef() throws Exception {
+    assertThat(IntBlob.parse(repo, "refs/nothing")).isEmpty();
+  }
+
+  @Test
+  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.
+    }
+  }
+
+  @Test
+  public void parseValid() throws Exception {
+    String refName = "refs/foo";
+    ObjectId id = tr.update(refName, tr.blob("123"));
+    assertThat(IntBlob.parse(repo, refName)).value().isEqualTo(IntBlob.create(id, 123));
+  }
+
+  @Test
+  public void parseWithWhitespace() throws Exception {
+    String refName = "refs/foo";
+    ObjectId id = tr.update(refName, tr.blob(" 123 "));
+    assertThat(IntBlob.parse(repo, refName)).value().isEqualTo(IntBlob.create(id, 123));
+  }
+
+  @Test
+  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());
+    }
+  }
+
+  @Test
+  public void tryStoreNoOldId() throws Exception {
+    String refName = "refs/foo";
+    RefUpdate ru =
+        IntBlob.tryStore(repo, rw, projectName, refName, null, 123, GitReferenceUpdated.DISABLED);
+    assertThat(ru.getResult()).isEqualTo(RefUpdate.Result.NEW);
+    assertThat(ru.getName()).isEqualTo(refName);
+    assertThat(IntBlob.parse(repo, refName))
+        .value()
+        .isEqualTo(IntBlob.create(ru.getNewObjectId(), 123));
+  }
+
+  @Test
+  public void tryStoreOldIdZero() throws Exception {
+    String refName = "refs/foo";
+    RefUpdate ru =
+        IntBlob.tryStore(
+            repo, rw, projectName, refName, ObjectId.zeroId(), 123, GitReferenceUpdated.DISABLED);
+    assertThat(ru.getResult()).isEqualTo(RefUpdate.Result.NEW);
+    assertThat(ru.getName()).isEqualTo(refName);
+    assertThat(IntBlob.parse(repo, refName))
+        .value()
+        .isEqualTo(IntBlob.create(ru.getNewObjectId(), 123));
+  }
+
+  @Test
+  public void tryStoreCorrectOldId() throws Exception {
+    String refName = "refs/foo";
+    ObjectId id = tr.update(refName, tr.blob("123"));
+    RefUpdate ru =
+        IntBlob.tryStore(repo, rw, projectName, refName, id, 456, GitReferenceUpdated.DISABLED);
+    assertThat(ru.getResult()).isEqualTo(RefUpdate.Result.FORCED);
+    assertThat(ru.getName()).isEqualTo(refName);
+    assertThat(IntBlob.parse(repo, refName))
+        .value()
+        .isEqualTo(IntBlob.create(ru.getNewObjectId(), 456));
+  }
+
+  @Test
+  public void tryStoreWrongOldId() throws Exception {
+    String refName = "refs/foo";
+    RefUpdate ru =
+        IntBlob.tryStore(
+            repo,
+            rw,
+            projectName,
+            refName,
+            ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"),
+            123,
+            GitReferenceUpdated.DISABLED);
+    assertThat(ru.getResult()).isEqualTo(RefUpdate.Result.LOCK_FAILURE);
+    assertThat(ru.getName()).isEqualTo(refName);
+    assertThat(IntBlob.parse(repo, refName)).isEmpty();
+  }
+
+  @Test
+  public void storeNoOldId() throws Exception {
+    String refName = "refs/foo";
+    IntBlob.store(repo, rw, projectName, refName, null, 123, GitReferenceUpdated.DISABLED);
+    assertThat(IntBlob.parse(repo, refName))
+        .value()
+        .isEqualTo(IntBlob.create(getRef(refName), 123));
+  }
+
+  @Test
+  public void storeOldIdZero() throws Exception {
+    String refName = "refs/foo";
+    IntBlob.store(
+        repo, rw, projectName, refName, ObjectId.zeroId(), 123, GitReferenceUpdated.DISABLED);
+    assertThat(IntBlob.parse(repo, refName))
+        .value()
+        .isEqualTo(IntBlob.create(getRef(refName), 123));
+  }
+
+  @Test
+  public void storeCorrectOldId() throws Exception {
+    String refName = "refs/foo";
+    ObjectId id = tr.update(refName, tr.blob("123"));
+    IntBlob.store(repo, rw, projectName, refName, id, 456, GitReferenceUpdated.DISABLED);
+    assertThat(IntBlob.parse(repo, refName))
+        .value()
+        .isEqualTo(IntBlob.create(getRef(refName), 456));
+  }
+
+  @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");
+    }
+    assertThat(IntBlob.parse(repo, refName)).isEmpty();
+  }
+
+  private ObjectId getRef(String refName) throws IOException {
+    return repo.exactRef(refName).getObjectId();
+  }
+}
diff --git a/javatests/com/google/gerrit/server/notedb/NoteDbChangeStateTest.java b/javatests/com/google/gerrit/server/notedb/NoteDbChangeStateTest.java
deleted file mode 100644
index c82135e..0000000
--- a/javatests/com/google/gerrit/server/notedb/NoteDbChangeStateTest.java
+++ /dev/null
@@ -1,241 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
-import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.NOTE_DB;
-import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.REVIEW_DB;
-import static com.google.gerrit.server.notedb.NoteDbChangeState.applyDelta;
-import static com.google.gerrit.server.notedb.NoteDbChangeState.parse;
-import static com.google.gerrit.server.util.time.TimeUtil.nowTs;
-import static org.eclipse.jgit.lib.ObjectId.zeroId;
-
-import com.google.common.collect.ImmutableMap;
-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.notedb.NoteDbChangeState.Delta;
-import com.google.gerrit.server.notedb.NoteDbChangeState.RefState;
-import com.google.gerrit.testing.GerritBaseTests;
-import com.google.gerrit.testing.TestChanges;
-import com.google.gerrit.testing.TestTimeUtil;
-import java.sql.Timestamp;
-import java.util.Optional;
-import java.util.concurrent.TimeUnit;
-import org.eclipse.jgit.lib.ObjectId;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-/** Unit tests for {@link NoteDbChangeState}. */
-public class NoteDbChangeStateTest extends GerritBaseTests {
-  ObjectId SHA1 = ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
-  ObjectId SHA2 = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
-  ObjectId SHA3 = ObjectId.fromString("badc0feebadc0feebadc0feebadc0feebadc0fee");
-
-  @Before
-  public void setUp() {
-    TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS);
-  }
-
-  @After
-  public void tearDown() {
-    TestTimeUtil.useSystemTime();
-  }
-
-  @Test
-  public void parseReviewDbWithoutDrafts() {
-    NoteDbChangeState state = parse(new Change.Id(1), SHA1.name());
-    assertThat(state.getPrimaryStorage()).isEqualTo(REVIEW_DB);
-    assertThat(state.getChangeId()).isEqualTo(new Change.Id(1));
-    assertThat(state.getChangeMetaId()).isEqualTo(SHA1);
-    assertThat(state.getDraftIds()).isEmpty();
-    assertThat(state.getReadOnlyUntil()).isEmpty();
-    assertThat(state.toString()).isEqualTo(SHA1.name());
-
-    state = parse(new Change.Id(1), "R," + SHA1.name());
-    assertThat(state.getPrimaryStorage()).isEqualTo(REVIEW_DB);
-    assertThat(state.getChangeId()).isEqualTo(new Change.Id(1));
-    assertThat(state.getChangeMetaId()).isEqualTo(SHA1);
-    assertThat(state.getDraftIds()).isEmpty();
-    assertThat(state.getReadOnlyUntil()).isEmpty();
-    assertThat(state.toString()).isEqualTo(SHA1.name());
-  }
-
-  @Test
-  public void parseReviewDbWithDrafts() {
-    String str = SHA1.name() + ",2003=" + SHA2.name() + ",1001=" + SHA3.name();
-    String expected = SHA1.name() + ",1001=" + SHA3.name() + ",2003=" + SHA2.name();
-    NoteDbChangeState state = parse(new Change.Id(1), str);
-    assertThat(state.getPrimaryStorage()).isEqualTo(REVIEW_DB);
-    assertThat(state.getChangeId()).isEqualTo(new Change.Id(1));
-    assertThat(state.getChangeMetaId()).isEqualTo(SHA1);
-    assertThat(state.getDraftIds())
-        .containsExactly(
-            new Account.Id(1001), SHA3,
-            new Account.Id(2003), SHA2);
-    assertThat(state.getReadOnlyUntil()).isEmpty();
-    assertThat(state.toString()).isEqualTo(expected);
-
-    state = parse(new Change.Id(1), "R," + str);
-    assertThat(state.getPrimaryStorage()).isEqualTo(REVIEW_DB);
-    assertThat(state.getChangeId()).isEqualTo(new Change.Id(1));
-    assertThat(state.getChangeMetaId()).isEqualTo(SHA1);
-    assertThat(state.getDraftIds())
-        .containsExactly(
-            new Account.Id(1001), SHA3,
-            new Account.Id(2003), SHA2);
-    assertThat(state.getReadOnlyUntil()).isEmpty();
-    assertThat(state.toString()).isEqualTo(expected);
-  }
-
-  @Test
-  public void parseReadOnlyUntil() {
-    Timestamp ts = new Timestamp(12345);
-    String str = "R=12345," + SHA1.name();
-    NoteDbChangeState state = parse(new Change.Id(1), str);
-    assertThat(state.getPrimaryStorage()).isEqualTo(REVIEW_DB);
-    assertThat(state.getChangeId()).isEqualTo(new Change.Id(1));
-    assertThat(state.getChangeMetaId()).isEqualTo(SHA1);
-    assertThat(state.getReadOnlyUntil().get()).isEqualTo(ts);
-    assertThat(state.toString()).isEqualTo(str);
-
-    str = "N=12345";
-    state = parse(new Change.Id(1), str);
-    assertThat(state.getPrimaryStorage()).isEqualTo(NOTE_DB);
-    assertThat(state.getChangeId()).isEqualTo(new Change.Id(1));
-    assertThat(state.getRefState()).isEmpty();
-    assertThat(state.getReadOnlyUntil().get()).isEqualTo(ts);
-    assertThat(state.toString()).isEqualTo(str);
-  }
-
-  @Test
-  public void applyDeltaToNullWithNoNewMetaId() throws Exception {
-    Change c = newChange();
-    assertThat(c.getNoteDbState()).isNull();
-    applyDelta(c, Delta.create(c.getId(), noMetaId(), noDrafts()));
-    assertThat(c.getNoteDbState()).isNull();
-
-    applyDelta(c, Delta.create(c.getId(), noMetaId(), drafts(new Account.Id(1001), zeroId())));
-    assertThat(c.getNoteDbState()).isNull();
-  }
-
-  @Test
-  public void applyDeltaToMetaId() throws Exception {
-    Change c = newChange();
-    applyDelta(c, Delta.create(c.getId(), metaId(SHA1), noDrafts()));
-    assertThat(c.getNoteDbState()).isEqualTo(SHA1.name());
-
-    applyDelta(c, Delta.create(c.getId(), metaId(SHA2), noDrafts()));
-    assertThat(c.getNoteDbState()).isEqualTo(SHA2.name());
-
-    // No-op delta.
-    applyDelta(c, Delta.create(c.getId(), noMetaId(), noDrafts()));
-    assertThat(c.getNoteDbState()).isEqualTo(SHA2.name());
-
-    // Set to zero clears the field.
-    applyDelta(c, Delta.create(c.getId(), metaId(zeroId()), noDrafts()));
-    assertThat(c.getNoteDbState()).isNull();
-  }
-
-  @Test
-  public void applyDeltaToDrafts() throws Exception {
-    Change c = newChange();
-    applyDelta(c, Delta.create(c.getId(), metaId(SHA1), drafts(new Account.Id(1001), SHA2)));
-    assertThat(c.getNoteDbState()).isEqualTo(SHA1.name() + ",1001=" + SHA2.name());
-
-    applyDelta(c, Delta.create(c.getId(), noMetaId(), drafts(new Account.Id(2003), SHA3)));
-    assertThat(c.getNoteDbState())
-        .isEqualTo(SHA1.name() + ",1001=" + SHA2.name() + ",2003=" + SHA3.name());
-
-    applyDelta(c, Delta.create(c.getId(), noMetaId(), drafts(new Account.Id(2003), zeroId())));
-    assertThat(c.getNoteDbState()).isEqualTo(SHA1.name() + ",1001=" + SHA2.name());
-
-    applyDelta(c, Delta.create(c.getId(), metaId(SHA3), noDrafts()));
-    assertThat(c.getNoteDbState()).isEqualTo(SHA3.name() + ",1001=" + SHA2.name());
-  }
-
-  @Test
-  public void applyDeltaToReadOnly() throws Exception {
-    Timestamp ts = nowTs();
-    Change c = newChange();
-    NoteDbChangeState state =
-        new NoteDbChangeState(
-            c.getId(),
-            REVIEW_DB,
-            Optional.of(RefState.create(SHA1, ImmutableMap.of())),
-            Optional.of(new Timestamp(ts.getTime() + 10000)));
-    c.setNoteDbState(state.toString());
-    Delta delta = Delta.create(c.getId(), metaId(SHA2), noDrafts());
-    applyDelta(c, delta);
-    assertThat(NoteDbChangeState.parse(c))
-        .isEqualTo(
-            new NoteDbChangeState(
-                state.getChangeId(),
-                state.getPrimaryStorage(),
-                Optional.of(RefState.create(SHA2, ImmutableMap.of())),
-                state.getReadOnlyUntil()));
-  }
-
-  @Test
-  public void parseNoteDbPrimary() {
-    NoteDbChangeState state = parse(new Change.Id(1), "N");
-    assertThat(state.getPrimaryStorage()).isEqualTo(NOTE_DB);
-    assertThat(state.getRefState()).isEmpty();
-    assertThat(state.getReadOnlyUntil()).isEmpty();
-  }
-
-  @Test(expected = IllegalArgumentException.class)
-  public void parseInvalidPrimaryStorage() {
-    parse(new Change.Id(1), "X");
-  }
-
-  @Test
-  public void applyDeltaToNoteDbPrimaryIsNoOp() throws Exception {
-    Change c = newChange();
-    c.setNoteDbState("N");
-    applyDelta(c, Delta.create(c.getId(), metaId(SHA1), drafts(new Account.Id(1001), SHA2)));
-    assertThat(c.getNoteDbState()).isEqualTo("N");
-  }
-
-  private static Change newChange() {
-    return TestChanges.newChange(new Project.NameKey("project"), new Account.Id(12345));
-  }
-
-  // Static factory methods to avoid type arguments when using as method args.
-
-  private static Optional<ObjectId> noMetaId() {
-    return Optional.empty();
-  }
-
-  private static Optional<ObjectId> metaId(ObjectId id) {
-    return Optional.of(id);
-  }
-
-  private static ImmutableMap<Account.Id, ObjectId> noDrafts() {
-    return ImmutableMap.of();
-  }
-
-  private static ImmutableMap<Account.Id, ObjectId> drafts(Object... args) {
-    checkArgument(args.length % 2 == 0);
-    ImmutableMap.Builder<Account.Id, ObjectId> b = ImmutableMap.builder();
-    for (int i = 0; i < args.length / 2; i++) {
-      b.put((Account.Id) args[2 * i], (ObjectId) args[2 * i + 1]);
-    }
-    return b.build();
-  }
-}
diff --git a/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java b/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java
index a21f5ba..74cc507 100644
--- a/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java
+++ b/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java
@@ -23,11 +23,12 @@
 import com.github.rholder.retry.RetryerBuilder;
 import com.github.rholder.retry.StopStrategies;
 import com.google.common.util.concurrent.Runnables;
+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 com.google.gwtorm.server.OrmException;
 import java.io.IOException;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -40,17 +41,13 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 
-public class RepoSequenceTest {
+public class RepoSequenceTest extends GerritBaseTests {
   // Don't sleep in tests.
-  private static final Retryer<RefUpdate.Result> RETRYER =
+  private static final Retryer<RefUpdate> RETRYER =
       RepoSequence.retryerBuilder().withBlockStrategy(t -> {}).build();
 
-  @Rule public ExpectedException exception = ExpectedException.none();
-
   private InMemoryRepositoryManager repoManager;
   private Project.NameKey project;
 
@@ -70,7 +67,7 @@
       for (int i = 1; i <= max; i++) {
         try {
           assertThat(s.next()).named("i=" + i + " for " + name).isEqualTo(i);
-        } catch (OrmException e) {
+        } catch (StorageException e) {
           throw new AssertionError("failed batchSize=" + batchSize + ", i=" + i, e);
         }
       }
@@ -171,20 +168,20 @@
   @Test
   public void failOnInvalidValue() throws Exception {
     ObjectId id = writeBlob("id", "not a number");
-    exception.expect(OrmException.class);
+    exception.expect(StorageException.class);
     exception.expectMessage("invalid value in refs/sequences/id blob at " + id.name());
     newSequence("id", 1, 3).next();
   }
 
   @Test
   public void failOnWrongType() throws Exception {
-    try (Repository repo = repoManager.openRepository(project)) {
-      TestRepository<Repository> tr = new TestRepository<>(repo);
+    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 (OrmException e) {
+      } catch (StorageException e) {
         assertThat(e.getCause()).isInstanceOf(ExecutionException.class);
         assertThat(e.getCause().getCause()).isInstanceOf(IncorrectObjectTypeException.class);
       }
@@ -200,11 +197,11 @@
             1,
             10,
             () -> writeBlob("id", Integer.toString(bgCounter.getAndAdd(1000))),
-            RetryerBuilder.<RefUpdate.Result>newBuilder()
+            RetryerBuilder.<RefUpdate>newBuilder()
                 .withStopStrategy(StopStrategies.stopAfterAttempt(3))
                 .build());
-    exception.expect(OrmException.class);
-    exception.expectMessage("failed to update refs/sequences/id: LOCK_FAILURE");
+    exception.expect(StorageException.class);
+    exception.expectMessage("Failed to update refs/sequences/id: LOCK_FAILURE");
     s.next();
   }
 
@@ -254,95 +251,6 @@
     assertThat(s2.acquireCount).isEqualTo(1);
   }
 
-  @Test
-  public void increaseTo() throws Exception {
-    // Seed existing ref value.
-    writeBlob("id", "1");
-
-    RepoSequence s = newSequence("id", 1, 10);
-
-    s.increaseTo(2);
-    assertThat(s.next()).isEqualTo(2);
-  }
-
-  @Test
-  public void increaseToLowerValueIsIgnored() throws Exception {
-    // Seed existing ref value.
-    writeBlob("id", "2");
-
-    RepoSequence s = newSequence("id", 1, 10);
-
-    s.increaseTo(1);
-    assertThat(s.next()).isEqualTo(2);
-  }
-
-  @Test
-  public void increaseToRetryOnLockFailureV1() throws Exception {
-    // Seed existing ref value.
-    writeBlob("id", "1");
-
-    AtomicBoolean doneBgUpdate = new AtomicBoolean(false);
-    Runnable bgUpdate =
-        () -> {
-          if (!doneBgUpdate.getAndSet(true)) {
-            writeBlob("id", "2");
-          }
-        };
-
-    RepoSequence s = newSequence("id", 1, 10, bgUpdate, RETRYER);
-    assertThat(doneBgUpdate.get()).isFalse();
-
-    // Increase the value to 3. The background thread increases the value to 2, which makes the
-    // increase to value 3 fail once with LockFailure. The increase to 3 is then retried and is
-    // expected to succeed.
-    s.increaseTo(3);
-    assertThat(s.next()).isEqualTo(3);
-
-    assertThat(doneBgUpdate.get()).isTrue();
-  }
-
-  @Test
-  public void increaseToRetryOnLockFailureV2() throws Exception {
-    // Seed existing ref value.
-    writeBlob("id", "1");
-
-    AtomicBoolean doneBgUpdate = new AtomicBoolean(false);
-    Runnable bgUpdate =
-        () -> {
-          if (!doneBgUpdate.getAndSet(true)) {
-            writeBlob("id", "3");
-          }
-        };
-
-    RepoSequence s = newSequence("id", 1, 10, bgUpdate, RETRYER);
-    assertThat(doneBgUpdate.get()).isFalse();
-
-    // Increase the value to 2. The background thread increases the value to 3, which makes the
-    // increase to value 2 fail with LockFailure. The increase to 2 is then not retried because the
-    // current value is already higher and it should be preserved.
-    s.increaseTo(2);
-    assertThat(s.next()).isEqualTo(3);
-
-    assertThat(doneBgUpdate.get()).isTrue();
-  }
-
-  @Test
-  public void increaseToFailAfterRetryerGivesUp() throws Exception {
-    AtomicInteger bgCounter = new AtomicInteger(1234);
-    RepoSequence s =
-        newSequence(
-            "id",
-            1,
-            10,
-            () -> writeBlob("id", Integer.toString(bgCounter.getAndAdd(1000))),
-            RetryerBuilder.<RefUpdate.Result>newBuilder()
-                .withStopStrategy(StopStrategies.stopAfterAttempt(3))
-                .build());
-    exception.expect(OrmException.class);
-    exception.expectMessage("failed to update refs/sequences/id: LOCK_FAILURE");
-    s.increaseTo(2);
-  }
-
   private RepoSequence newSequence(String name, int start, int batchSize) {
     return newSequence(name, start, batchSize, Runnables.doNothing(), RETRYER);
   }
@@ -352,7 +260,7 @@
       final int start,
       int batchSize,
       Runnable afterReadRef,
-      Retryer<RefUpdate.Result> retryer) {
+      Retryer<RefUpdate> retryer) {
     return new RepoSequence(
         repoManager,
         GitReferenceUpdated.DISABLED,
diff --git a/javatests/com/google/gerrit/server/notedb/rebuild/EventSorterTest.java b/javatests/com/google/gerrit/server/notedb/rebuild/EventSorterTest.java
deleted file mode 100644
index c81a176..0000000
--- a/javatests/com/google/gerrit/server/notedb/rebuild/EventSorterTest.java
+++ /dev/null
@@ -1,231 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb.rebuild;
-
-import static com.google.common.truth.Truth.assertThat;
-import static java.util.stream.Collectors.toList;
-import static org.junit.Assert.fail;
-
-import com.google.common.collect.Collections2;
-import com.google.common.collect.Lists;
-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.notedb.ChangeUpdate;
-import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gerrit.testing.TestTimeUtil;
-import java.sql.Timestamp;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-import java.util.stream.Stream;
-import org.junit.Before;
-import org.junit.Test;
-
-public class EventSorterTest {
-  private class TestEvent extends Event {
-    protected TestEvent(Timestamp when) {
-      super(
-          new PatchSet.Id(new Change.Id(1), 1),
-          new Account.Id(1000),
-          new Account.Id(1000),
-          when,
-          changeCreatedOn,
-          null);
-    }
-
-    @Override
-    boolean uniquePerUpdate() {
-      return false;
-    }
-
-    @Override
-    void apply(ChangeUpdate update) {
-      throw new UnsupportedOperationException();
-    }
-
-    @SuppressWarnings("deprecation")
-    @Override
-    public String toString() {
-      return "E{" + when.getSeconds() + '}';
-    }
-  }
-
-  private Timestamp changeCreatedOn;
-
-  @Before
-  public void setUp() {
-    TestTimeUtil.resetWithClockStep(10, TimeUnit.SECONDS);
-    changeCreatedOn = TimeUtil.nowTs();
-  }
-
-  @Test
-  public void naturalSort() {
-    Event e1 = new TestEvent(TimeUtil.nowTs());
-    Event e2 = new TestEvent(TimeUtil.nowTs());
-    Event e3 = new TestEvent(TimeUtil.nowTs());
-
-    for (List<Event> events : Collections2.permutations(events(e1, e2, e3))) {
-      assertSorted(events, events(e1, e2, e3));
-    }
-  }
-
-  @Test
-  public void topoSortOneDep() {
-    List<Event> es;
-
-    // Input list is 0,1,2
-
-    // 0 depends on 1 => 1,0,2
-    es = threeEventsOneDep(0, 1);
-    assertSorted(es, events(es, 1, 0, 2));
-
-    // 1 depends on 0 => 0,1,2
-    es = threeEventsOneDep(1, 0);
-    assertSorted(es, events(es, 0, 1, 2));
-
-    // 0 depends on 2 => 1,2,0
-    es = threeEventsOneDep(0, 2);
-    assertSorted(es, events(es, 1, 2, 0));
-
-    // 2 depends on 0 => 0,1,2
-    es = threeEventsOneDep(2, 0);
-    assertSorted(es, events(es, 0, 1, 2));
-
-    // 1 depends on 2 => 0,2,1
-    es = threeEventsOneDep(1, 2);
-    assertSorted(es, events(es, 0, 2, 1));
-
-    // 2 depends on 1 => 0,1,2
-    es = threeEventsOneDep(2, 1);
-    assertSorted(es, events(es, 0, 1, 2));
-  }
-
-  private List<Event> threeEventsOneDep(int depFromIdx, int depOnIdx) {
-    List<Event> events =
-        Lists.newArrayList(
-            new TestEvent(TimeUtil.nowTs()),
-            new TestEvent(TimeUtil.nowTs()),
-            new TestEvent(TimeUtil.nowTs()));
-    events.get(depFromIdx).addDep(events.get(depOnIdx));
-    return events;
-  }
-
-  @Test
-  public void lastEventDependsOnFirstEvent() {
-    List<Event> events = new ArrayList<>();
-    for (int i = 0; i < 20; i++) {
-      events.add(new TestEvent(TimeUtil.nowTs()));
-    }
-    events.get(events.size() - 1).addDep(events.get(0));
-    assertSorted(events, events);
-  }
-
-  @Test
-  public void firstEventDependsOnLastEvent() {
-    List<Event> events = new ArrayList<>();
-    for (int i = 0; i < 20; i++) {
-      events.add(new TestEvent(TimeUtil.nowTs()));
-    }
-    events.get(0).addDep(events.get(events.size() - 1));
-
-    List<Event> expected = new ArrayList<>();
-    expected.addAll(events.subList(1, events.size()));
-    expected.add(events.get(0));
-    assertSorted(events, expected);
-  }
-
-  @Test
-  public void topoSortChainOfDeps() {
-    Event e1 = new TestEvent(TimeUtil.nowTs());
-    Event e2 = new TestEvent(TimeUtil.nowTs());
-    Event e3 = new TestEvent(TimeUtil.nowTs());
-    Event e4 = new TestEvent(TimeUtil.nowTs());
-    e1.addDep(e2);
-    e2.addDep(e3);
-    e3.addDep(e4);
-
-    assertSorted(events(e1, e2, e3, e4), events(e4, e3, e2, e1));
-  }
-
-  @Test
-  public void topoSortMultipleDeps() {
-    Event e1 = new TestEvent(TimeUtil.nowTs());
-    Event e2 = new TestEvent(TimeUtil.nowTs());
-    Event e3 = new TestEvent(TimeUtil.nowTs());
-    Event e4 = new TestEvent(TimeUtil.nowTs());
-    e1.addDep(e2);
-    e1.addDep(e4);
-    e2.addDep(e3);
-
-    // Processing 3 pops 2, processing 4 pops 1.
-    assertSorted(events(e2, e3, e1, e4), events(e3, e2, e4, e1));
-  }
-
-  @Test
-  public void topoSortMultipleDepsPreservesNaturalOrder() {
-    Event e1 = new TestEvent(TimeUtil.nowTs());
-    Event e2 = new TestEvent(TimeUtil.nowTs());
-    Event e3 = new TestEvent(TimeUtil.nowTs());
-    Event e4 = new TestEvent(TimeUtil.nowTs());
-    e1.addDep(e4);
-    e2.addDep(e4);
-    e3.addDep(e4);
-
-    // Processing 4 pops 1, 2, 3 in natural order.
-    assertSorted(events(e4, e3, e2, e1), events(e4, e1, e2, e3));
-  }
-
-  @Test
-  public void topoSortCycle() {
-    Event e1 = new TestEvent(TimeUtil.nowTs());
-    Event e2 = new TestEvent(TimeUtil.nowTs());
-
-    // Implementation is not really defined, but infinite looping would be bad.
-    // According to current implementation details, 2 pops 1, 1 pops 2 which was
-    // already seen.
-    assertSorted(events(e2, e1), events(e1, e2));
-  }
-
-  @Test
-  public void topoSortDepNotInInputList() {
-    Event e1 = new TestEvent(TimeUtil.nowTs());
-    Event e2 = new TestEvent(TimeUtil.nowTs());
-    Event e3 = new TestEvent(TimeUtil.nowTs());
-    e1.addDep(e3);
-
-    List<Event> events = events(e2, e1);
-    try {
-      new EventSorter(events).sort();
-      fail("expected IllegalArgumentException");
-    } catch (IllegalArgumentException e) {
-      // Expected.
-    }
-  }
-
-  private static List<Event> events(Event... es) {
-    return Lists.newArrayList(es);
-  }
-
-  private static List<Event> events(List<Event> in, Integer... indexes) {
-    return Stream.of(indexes).map(in::get).collect(toList());
-  }
-
-  private static void assertSorted(List<Event> unsorted, List<Event> expected) {
-    List<Event> actual = new ArrayList<>(unsorted);
-    new EventSorter(actual).sort();
-    assertThat(actual).named("sorted" + unsorted).isEqualTo(expected);
-  }
-}
diff --git a/javatests/com/google/gerrit/server/patch/IntraLineLoaderTest.java b/javatests/com/google/gerrit/server/patch/IntraLineLoaderTest.java
index ef80d7e..6c63c5f 100644
--- a/javatests/com/google/gerrit/server/patch/IntraLineLoaderTest.java
+++ b/javatests/com/google/gerrit/server/patch/IntraLineLoaderTest.java
@@ -19,13 +19,14 @@
 
 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.eclipse.jgit.diff.ReplaceEdit;
 import org.junit.Test;
 
-public class IntraLineLoaderTest {
+public class IntraLineLoaderTest extends GerritBaseTests {
 
   @Test
   public void rewriteAtStartOfLineIsRecognized() throws Exception {
diff --git a/javatests/com/google/gerrit/server/patch/PatchListEntryTest.java b/javatests/com/google/gerrit/server/patch/PatchListEntryTest.java
index 81f03af..4ed5f4b 100644
--- a/javatests/com/google/gerrit/server/patch/PatchListEntryTest.java
+++ b/javatests/com/google/gerrit/server/patch/PatchListEntryTest.java
@@ -20,9 +20,10 @@
 import static org.junit.Assert.assertTrue;
 
 import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class PatchListEntryTest {
+public class PatchListEntryTest extends GerritBaseTests {
   @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 6fbafb6..ccdd040 100644
--- a/javatests/com/google/gerrit/server/patch/PatchListTest.java
+++ b/javatests/com/google/gerrit/server/patch/PatchListTest.java
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.testing.GerritBaseTests;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.InputStream;
@@ -25,7 +26,7 @@
 import java.util.Arrays;
 import org.junit.Test;
 
-public class PatchListTest {
+public class PatchListTest extends GerritBaseTests {
   @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 305e81b..ff9ac41 100644
--- a/javatests/com/google/gerrit/server/permissions/DefaultPermissionsMappingTest.java
+++ b/javatests/com/google/gerrit/server/permissions/DefaultPermissionsMappingTest.java
@@ -18,9 +18,10 @@
 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 {
+public class DefaultPermissionsMappingTest extends GerritBaseTests {
   @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
new file mode 100644
index 0000000..f40c3bc
--- /dev/null
+++ b/javatests/com/google/gerrit/server/permissions/PluginPermissionsUtilTest.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.server.permissions;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.permissions.PluginPermissionsUtil.isValidPluginPermission;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.Test;
+
+/** Small tests for {@link PluginPermissionsUtil}. */
+public final class PluginPermissionsUtilTest {
+
+  @Test
+  public void isPluginPermissionReturnsTrueForValidName() {
+    // "-" is allowed for a plugin name. Here "foo-a" should be the name of the plugin.
+    ImmutableList<String> validPluginPermissions =
+        ImmutableList.of("plugin-foo-a", "plugin-foo-a-b");
+
+    for (String permission : validPluginPermissions) {
+      assertThat(isValidPluginPermission(permission))
+          .named("valid plugin permission: %s", permission)
+          .isTrue();
+    }
+  }
+
+  @Test
+  public void isPluginPermissionReturnsFalseForInvalidName() {
+    ImmutableList<String> invalidPluginPermissions =
+        ImmutableList.of(
+            "create",
+            "label-Code-Review",
+            "plugin-foo",
+            "plugin-foo",
+            "plugin-foo-a-",
+            "plugin-foo-a1");
+
+    for (String permission : invalidPluginPermissions) {
+      assertThat(isValidPluginPermission(permission))
+          .named("invalid plugin permission: %s", permission)
+          .isFalse();
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/server/permissions/RefControlTest.java b/javatests/com/google/gerrit/server/permissions/RefControlTest.java
index 7de50dc..ce24897 100644
--- a/javatests/com/google/gerrit/server/permissions/RefControlTest.java
+++ b/javatests/com/google/gerrit/server/permissions/RefControlTest.java
@@ -41,12 +41,11 @@
 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.common.errors.InvalidNameException;
+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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.CapabilityCollection;
 import com.google.gerrit.server.account.GroupMembership;
@@ -55,7 +54,6 @@
 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.config.SitePaths;
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.index.SingleVersionModule.SingleVersionListener;
 import com.google.gerrit.server.project.ProjectCache;
@@ -64,16 +62,13 @@
 import com.google.gerrit.server.project.RefPattern;
 import com.google.gerrit.server.project.testing.Util;
 import com.google.gerrit.server.schema.SchemaCreator;
-import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
-import com.google.gerrit.testing.InMemoryDatabase;
+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 com.google.inject.Provider;
-import com.google.inject.util.Providers;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -89,7 +84,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class RefControlTest {
+public class RefControlTest extends GerritBaseTests {
   private void assertAdminsAreOwnersAndDevsAreNot() {
     ProjectControl uBlah = user(local, DEVS);
     ProjectControl uAdmin = user(local, DEVS, ADMIN);
@@ -195,17 +190,16 @@
   private ProjectCache projectCache;
   private PermissionCollection.Factory sectionSorter;
   private ChangeControl.Factory changeControlFactory;
-  private ReviewDb db;
 
   @Inject private PermissionBackend permissionBackend;
   @Inject private CapabilityCollection.Factory capabilityCollectionFactory;
   @Inject private SchemaCreator schemaCreator;
   @Inject private SingleVersionListener singleVersionListener;
-  @Inject private InMemoryDatabase schemaFactory;
   @Inject private ThreadLocalRequestContext requestContext;
   @Inject private DefaultRefFilter.Factory refFilterFactory;
   @Inject private TransferConfig transferConfig;
   @Inject private MetricMaker metricMaker;
+  @Inject private ProjectConfig.Factory projectConfigFactory;
 
   @Before
   public void setUp() throws Exception {
@@ -274,7 +268,8 @@
 
     try {
       Repository repo = repoManager.createRepository(allProjectsName);
-      ProjectConfig allProjects = new ProjectConfig(new Project.NameKey(allProjectsName.get()));
+      ProjectConfig allProjects =
+          projectConfigFactory.create(new Project.NameKey(allProjectsName.get()));
       allProjects.load(repo);
       LabelType cr = Util.codeReview();
       allProjects.getLabelSections().put(cr.getName(), cr);
@@ -283,10 +278,9 @@
       throw new RuntimeException(e);
     }
 
-    db = schemaFactory.open();
     singleVersionListener.start();
     try {
-      schemaCreator.create(db);
+      schemaCreator.create();
     } finally {
       singleVersionListener.stop();
     }
@@ -295,27 +289,16 @@
         CacheBuilder.newBuilder().build();
     sectionSorter = new PermissionCollection.Factory(new SectionSortCache(c), metricMaker);
 
-    parent = new ProjectConfig(parentKey);
+    parent = projectConfigFactory.create(parentKey);
     parent.load(newRepository(parentKey));
     add(parent);
 
-    local = new ProjectConfig(localKey);
+    local = projectConfigFactory.create(localKey);
     local.load(newRepository(localKey));
     add(local);
     local.getProject().setParentName(parentKey);
 
-    requestContext.setContext(
-        new RequestContext() {
-          @Override
-          public CurrentUser getUser() {
-            return null;
-          }
-
-          @Override
-          public Provider<ReviewDb> getReviewDbProvider() {
-            return Providers.of(db);
-          }
-        });
+    requestContext.setContext(() -> null);
 
     changeControlFactory = injector.getInstance(ChangeControl.Factory.class);
   }
@@ -323,10 +306,6 @@
   @After
   public void tearDown() {
     requestContext.setContext(null);
-    if (db != null) {
-      db.close();
-    }
-    InMemoryDatabase.drop(schemaFactory);
   }
 
   @Test
@@ -455,7 +434,7 @@
     allow(local, READ, DEVS, "refs/heads/*");
     assertCanAccess(user(local, "a", ADMIN));
 
-    local = new ProjectConfig(localKey);
+    local = projectConfigFactory.create(localKey);
     local.load(newRepository(localKey));
     local.getProject().setParentName(parentKey);
     allow(local, READ, DEVS, "refs/*");
@@ -961,7 +940,6 @@
   }
 
   private InMemoryRepository add(ProjectConfig pc) {
-    SitePaths sitePaths = null;
     List<CommentLinkInfo> commentLinks = null;
 
     InMemoryRepository repo;
@@ -976,7 +954,6 @@
     all.put(
         pc.getName(),
         new ProjectState(
-            sitePaths,
             projectCache,
             allProjectsName,
             allUsersName,
@@ -996,8 +973,8 @@
   private ProjectControl user(
       ProjectConfig local, @Nullable String name, AccountGroup.UUID... memberOf) {
     return new ProjectControl(
-        Collections.<AccountGroup.UUID>emptySet(),
-        Collections.<AccountGroup.UUID>emptySet(),
+        Collections.emptySet(),
+        Collections.emptySet(),
         sectionSorter,
         changeControlFactory,
         permissionBackend,
diff --git a/javatests/com/google/gerrit/server/project/CommitsCollectionTest.java b/javatests/com/google/gerrit/server/project/CommitsCollectionTest.java
index 07f64a8..cf6d50f 100644
--- a/javatests/com/google/gerrit/server/project/CommitsCollectionTest.java
+++ b/javatests/com/google/gerrit/server/project/CommitsCollectionTest.java
@@ -34,6 +34,7 @@
 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;
@@ -48,7 +49,7 @@
 import org.junit.Test;
 
 /** Unit tests for {@link CommitsCollection}. */
-public class CommitsCollectionTest {
+public class CommitsCollectionTest extends GerritBaseTests {
   @Rule public InMemoryTestEnvironment testEnvironment = new InMemoryTestEnvironment();
 
   @Inject private AccountManager accountManager;
@@ -57,6 +58,7 @@
   @Inject protected MetaDataUpdate.Server metaDataUpdateFactory;
   @Inject protected AllProjectsName allProjects;
   @Inject private CommitsCollection commits;
+  @Inject private ProjectConfig.Factory projectConfigFactory;
 
   private TestRepository<InMemoryRepository> repo;
   private ProjectConfig project;
@@ -70,7 +72,7 @@
 
     Project.NameKey name = new Project.NameKey("project");
     InMemoryRepository inMemoryRepo = repoManager.createRepository(name);
-    project = new ProjectConfig(name);
+    project = projectConfigFactory.create(name);
     project.load(inMemoryRepo);
     repo = new TestRepository<>(inMemoryRepo);
   }
diff --git a/javatests/com/google/gerrit/server/project/GroupListTest.java b/javatests/com/google/gerrit/server/project/GroupListTest.java
index 2249a16..08aca9f 100644
--- a/javatests/com/google/gerrit/server/project/GroupListTest.java
+++ b/javatests/com/google/gerrit/server/project/GroupListTest.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.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;
@@ -36,7 +37,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class GroupListTest {
+public class GroupListTest extends GerritBaseTests {
   private static final Project.NameKey PROJECT = new Project.NameKey("project");
   private static final String TEXT =
       "# UUID                                  \tGroup Name\n"
diff --git a/javatests/com/google/gerrit/server/project/ProjectConfigTest.java b/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
index 0e4ba10..3436153 100644
--- a/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
+++ b/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
@@ -16,7 +16,9 @@
 
 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 com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.ContributorAgreement;
@@ -24,16 +26,21 @@
 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.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 java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Map;
@@ -51,7 +58,9 @@
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.util.RawParseUtils;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
 
 public class ProjectConfigTest extends GerritBaseTests {
   private static final String LABEL_SCORES_CONFIG =
@@ -74,15 +83,24 @@
           + !LabelType.DEF_COPY_ALL_SCORES_IF_NO_CHANGE
           + "\n";
 
+  private static final AllProjectsName ALL_PROJECTS = new AllProjectsName("All-The-Projects");
+
+  @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");
 
+  private SitePaths sitePaths;
+  private ProjectConfig.Factory factory;
   private Repository db;
   private TestRepository<?> tr;
 
   @Before
   public void setUp() throws Exception {
+    sitePaths = new SitePaths(temporaryFolder.newFolder().toPath());
+    Files.createDirectories(sitePaths.etc_dir);
+    factory = new ProjectConfig.Factory(sitePaths, ALL_PROJECTS);
     db = new InMemoryRepository(new DfsRepositoryDescription("repo"));
     tr = new TestRepository<>(db);
   }
@@ -104,6 +122,13 @@
                     + "  sameGroupVisibility = block group Staff\n"
                     + "[contributor-agreement \"Individual\"]\n"
                     + "  description = A simple description\n"
+                    + "  matchProjects = ^/ourproject\n"
+                    + "  matchProjects = ^/ourotherproject\n"
+                    + "  matchProjects = ^/someotherroot/ourproject\n"
+                    + "  excludeProjects = ^/theirproject\n"
+                    + "  excludeProjects = ^/theirotherproject\n"
+                    + "  excludeProjects = ^/someotherroot/theirproject\n"
+                    + "  excludeProjects = ^/someotherroot/theirotherproject\n"
                     + "  accepted = group Developers\n"
                     + "  accepted = group Staff\n"
                     + "  autoVerify = group Developers\n"
@@ -115,6 +140,14 @@
     ContributorAgreement ca = cfg.getContributorAgreement("Individual");
     assertThat(ca.getName()).isEqualTo("Individual");
     assertThat(ca.getDescription()).isEqualTo("A simple description");
+    assertThat(ca.getMatchProjectsRegexes())
+        .containsExactly("^/ourproject", "^/ourotherproject", "^/someotherroot/ourproject");
+    assertThat(ca.getExcludeProjectsRegexes())
+        .containsExactly(
+            "^/theirproject",
+            "^/theirotherproject",
+            "^/someotherroot/theirproject",
+            "^/someotherroot/theirotherproject");
     assertThat(ca.getAgreementUrl()).isEqualTo("http://www.example.com/agree");
     assertThat(ca.getAccepted()).hasSize(2);
     assertThat(ca.getAccepted().get(0).getGroup()).isEqualTo(developers);
@@ -256,6 +289,7 @@
                     + "  sameGroupVisibility = block group Staff\n"
                     + "[contributor-agreement \"Individual\"]\n"
                     + "  description = A simple description\n"
+                    + "  matchProjects = ^/ourproject\n"
                     + "  accepted = group Developers\n"
                     + "  autoVerify = group Developers\n"
                     + "  agreementUrl = http://www.example.com/agree\n"
@@ -273,6 +307,8 @@
     ContributorAgreement ca = cfg.getContributorAgreement("Individual");
     ca.setAccepted(Collections.singletonList(new PermissionRule(cfg.resolve(staff))));
     ca.setAutoVerify(null);
+    ca.setMatchProjectsRegexes(null);
+    ca.setExcludeProjectsRegexes(Collections.singletonList("^/theirproject"));
     ca.setDescription("A new description");
     rev = commit(cfg);
     assertThat(text(rev, "project.config"))
@@ -289,6 +325,7 @@
                 + "  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
@@ -385,7 +422,7 @@
 
   @Test
   public void readUnexistingPluginConfig() throws Exception {
-    ProjectConfig cfg = new ProjectConfig(new Project.NameKey("test"));
+    ProjectConfig cfg = factory.create(new Project.NameKey("test"));
     cfg.load(db);
     PluginConfig pluginCfg = cfg.getPluginConfig("somePlugin");
     assertThat(pluginCfg.getNames()).isEmpty();
@@ -572,8 +609,46 @@
                     + "commentlink.bugzilla must have either link or html"));
   }
 
+  @Test
+  public void readAllProjectsBaseConfigFromSitePaths() throws Exception {
+    ProjectConfig cfg = factory.create(ALL_PROJECTS);
+    cfg.load(db);
+    assertThat(cfg.getProject().getBooleanConfig(REQUIRE_CHANGE_ID))
+        .isEqualTo(InheritableBoolean.INHERIT);
+
+    writeDefaultAllProjectsConfig("[receive]", "requireChangeId = false");
+
+    cfg.load(db);
+    assertThat(cfg.getProject().getBooleanConfig(REQUIRE_CHANGE_ID))
+        .isEqualTo(InheritableBoolean.FALSE);
+  }
+
+  @Test
+  public void readOtherProjectIgnoresAllProjectsBaseConfig() throws Exception {
+    ProjectConfig cfg = factory.create(new Project.NameKey("test"));
+    cfg.load(db);
+    assertThat(cfg.getProject().getBooleanConfig(REQUIRE_CHANGE_ID))
+        .isEqualTo(InheritableBoolean.INHERIT);
+
+    writeDefaultAllProjectsConfig("[receive]", "requireChangeId = false");
+
+    cfg.load(db);
+    // If we went through ProjectState, then this would return FALSE, since project.config for
+    // All-Projects would inherit from all_projects.config, and this project would inherit from
+    // All-Projects. But in ProjectConfig itself, there is no inheritance from All-Projects, so this
+    // continues to return the default.
+    assertThat(cfg.getProject().getBooleanConfig(REQUIRE_CHANGE_ID))
+        .isEqualTo(InheritableBoolean.INHERIT);
+  }
+
+  private Path writeDefaultAllProjectsConfig(String... lines) throws IOException {
+    Path dir = sitePaths.etc_dir.resolve(ALL_PROJECTS.get());
+    Files.createDirectories(dir);
+    return Files.write(dir.resolve("project.config"), ImmutableList.copyOf(lines));
+  }
+
   private ProjectConfig read(RevCommit rev) throws IOException, ConfigInvalidException {
-    ProjectConfig cfg = new ProjectConfig(new Project.NameKey("test"));
+    ProjectConfig cfg = factory.create(new 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 d292e19..a2b2866 100644
--- a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
+++ b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
@@ -49,7 +49,6 @@
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
@@ -80,11 +79,9 @@
 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.InMemoryDatabase;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.Provider;
-import com.google.inject.util.Providers;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -118,8 +115,6 @@
 
   @Inject private Provider<AnonymousUser> anonymousUser;
 
-  @Inject protected InMemoryDatabase schemaFactory;
-
   @Inject protected SchemaCreator schemaCreator;
 
   @Inject protected ThreadLocalRequestContext requestContext;
@@ -140,7 +135,6 @@
 
   protected LifecycleManager lifecycle;
   protected Injector injector;
-  protected ReviewDb db;
   protected AccountInfo currentUserInfo;
   protected CurrentUser admin;
 
@@ -160,12 +154,10 @@
   @After
   public void cleanUp() {
     lifecycle.stop();
-    db.close();
   }
 
   protected void setUpDatabase() throws Exception {
-    db = schemaFactory.open();
-    schemaCreator.create(db);
+    schemaCreator.create();
 
     Account.Id adminId = createAccount("admin", "Administrator", "admin@example.com", true);
     admin = userFactory.create(adminId);
@@ -177,32 +169,11 @@
 
   protected RequestContext newRequestContext(Account.Id requestUserId) {
     final CurrentUser requestUser = userFactory.create(requestUserId);
-    return new RequestContext() {
-      @Override
-      public CurrentUser getUser() {
-        return requestUser;
-      }
-
-      @Override
-      public Provider<ReviewDb> getReviewDbProvider() {
-        return Providers.of(db);
-      }
-    };
+    return () -> requestUser;
   }
 
   protected void setAnonymous() {
-    requestContext.setContext(
-        new RequestContext() {
-          @Override
-          public CurrentUser getUser() {
-            return anonymousUser.get();
-          }
-
-          @Override
-          public Provider<ReviewDb> getReviewDbProvider() {
-            return Providers.of(db);
-          }
-        });
+    requestContext.setContext(anonymousUser::get);
   }
 
   @After
@@ -211,10 +182,6 @@
       lifecycle.stop();
     }
     requestContext.setContext(null);
-    if (db != null) {
-      db.close();
-    }
-    InMemoryDatabase.drop(schemaFactory);
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/query/account/LuceneQueryAccountsTest.java b/javatests/com/google/gerrit/server/query/account/LuceneQueryAccountsTest.java
index 660c1d8..e36b79e 100644
--- a/javatests/com/google/gerrit/server/query/account/LuceneQueryAccountsTest.java
+++ b/javatests/com/google/gerrit/server/query/account/LuceneQueryAccountsTest.java
@@ -44,6 +44,6 @@
   protected Injector createInjector() {
     Config luceneConfig = new Config(config);
     InMemoryModule.setDefaults(luceneConfig);
-    return Guice.createInjector(new InMemoryModule(luceneConfig, notesMigration));
+    return Guice.createInjector(new InMemoryModule(luceneConfig));
   }
 }
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index c01bf04..04428ff 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.query.change;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
@@ -49,10 +50,8 @@
 import com.google.gerrit.extensions.api.changes.Changes.QueryRequest;
 import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.api.changes.HashtagsInput;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
-import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
 import com.google.gerrit.extensions.api.changes.StarsInput;
 import com.google.gerrit.extensions.api.groups.GroupInput;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
@@ -65,27 +64,25 @@
 import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
 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.RestApiException;
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.IndexConfig;
-import com.google.gerrit.index.QueryOptions;
 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.Account.Id;
 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.reviewdb.server.ReviewDb;
-import com.google.gerrit.reviewdb.server.ReviewDbUtil;
+import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
-import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.account.AccountCache;
@@ -96,17 +93,15 @@
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.change.ChangeTriplet;
+import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.change.PatchSetInserter;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.index.change.ChangeIndexCollection;
 import com.google.gerrit.server.index.change.ChangeIndexer;
-import com.google.gerrit.server.index.change.IndexedChangeQuery;
-import com.google.gerrit.server.index.change.StalenessChecker;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.notedb.NoteDbChangeState;
-import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
+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.schema.SchemaCreator;
@@ -116,28 +111,22 @@
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gerrit.testing.DisabledReviewDb;
 import com.google.gerrit.testing.GerritServerTests;
-import com.google.gerrit.testing.InMemoryDatabase;
 import com.google.gerrit.testing.InMemoryRepositoryManager;
 import com.google.gerrit.testing.InMemoryRepositoryManager.Repo;
 import com.google.gerrit.testing.TestTimeUtil;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.Provider;
-import com.google.inject.util.Providers;
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Optional;
 import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.ObjectId;
@@ -170,6 +159,7 @@
   @Inject protected ChangeIndexer indexer;
   @Inject protected IndexConfig indexConfig;
   @Inject protected InMemoryRepositoryManager repoManager;
+  @Inject protected Provider<AnonymousUser> anonymousUserProvider;
   @Inject protected Provider<InternalChangeQuery> queryProvider;
   @Inject protected ChangeNotes.Factory notesFactory;
   @Inject protected OneOffRequestContext oneOffRequestContext;
@@ -178,19 +168,14 @@
   @Inject protected ChangeNotes.Factory changeNotesFactory;
   @Inject protected Provider<ChangeQueryProcessor> queryProcessorProvider;
   @Inject protected SchemaCreator schemaCreator;
-  @Inject protected SchemaFactory<ReviewDb> schemaFactory;
   @Inject protected Sequences seq;
   @Inject protected ThreadLocalRequestContext requestContext;
   @Inject protected ProjectCache projectCache;
   @Inject protected MetaDataUpdate.Server metaDataUpdateFactory;
   @Inject protected IdentifiedUser.GenericFactory identifiedUserFactory;
 
-  // Only for use in setting up/tearing down injector; other users should use schemaFactory.
-  @Inject private InMemoryDatabase inMemoryDatabase;
-
   protected Injector injector;
   protected LifecycleManager lifecycle;
-  protected ReviewDb db;
   protected Account.Id userId;
   protected CurrentUser user;
 
@@ -228,16 +213,12 @@
   @After
   public void cleanUp() {
     lifecycle.stop();
-    db.close();
   }
 
   protected void initAfterLifecycleStart() throws Exception {}
 
   protected void setUpDatabase() throws Exception {
-    try (ReviewDb underlyingDb = inMemoryDatabase.getDatabase().open()) {
-      schemaCreator.create(underlyingDb);
-    }
-    db = schemaFactory.open();
+    schemaCreator.create();
 
     userId = accountManager.authenticate(AuthRequest.forUser("user")).getAccountId();
     String email = "user@example.com";
@@ -252,17 +233,7 @@
 
   protected RequestContext newRequestContext(Account.Id requestUserId) {
     final CurrentUser requestUser = userFactory.create(requestUserId);
-    return new RequestContext() {
-      @Override
-      public CurrentUser getUser() {
-        return requestUser;
-      }
-
-      @Override
-      public Provider<ReviewDb> getReviewDbProvider() {
-        return Providers.of(db);
-      }
-    };
+    return () -> requestUser;
   }
 
   protected void resetUser() {
@@ -276,10 +247,6 @@
       lifecycle.stop();
     }
     requestContext.setContext(null);
-    if (db != null) {
-      db.close();
-    }
-    InMemoryDatabase.drop(inMemoryDatabase);
   }
 
   @Before
@@ -576,7 +543,6 @@
   @Test
   public void restorePendingReviewers() throws Exception {
     assume().that(getSchemaVersion()).isAtLeast(44);
-    assume().that(notesMigration.readChanges()).isTrue();
 
     Project.NameKey project = new Project.NameKey("repo");
     TestRepository<Repo> repo = createProject(project.get());
@@ -1320,7 +1286,7 @@
     assertQuery("status:new", change2, change1);
 
     gApi.changes().id(change1.getId().get()).topic("new-topic");
-    change1 = notesFactory.create(db, change1.getProject(), change1.getId()).getChange();
+    change1 = notesFactory.create(change1.getProject(), change1.getId()).getChange();
 
     assertThat(lastUpdatedMs(change1)).isGreaterThan(lastUpdatedMs(change2));
     assertThat(lastUpdatedMs(change1) - lastUpdatedMs(change2))
@@ -1360,14 +1326,7 @@
   @Test
   public void byFileExact() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
-    RevCommit commit =
-        repo.parseBody(
-            repo.commit()
-                .message("one")
-                .add("dir/file1", "contents1")
-                .add("dir/file2", "contents2")
-                .create());
-    Change change = insert(repo, newChangeForCommit(repo, commit));
+    Change change = insert(repo, newChangeWithFiles(repo, "dir/file1", "dir/file2"));
 
     assertQuery("file:file");
     assertQuery("file:dir", change);
@@ -1380,14 +1339,7 @@
   @Test
   public void byFileRegex() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
-    RevCommit commit =
-        repo.parseBody(
-            repo.commit()
-                .message("one")
-                .add("dir/file1", "contents1")
-                .add("dir/file2", "contents2")
-                .create());
-    Change change = insert(repo, newChangeForCommit(repo, commit));
+    Change change = insert(repo, newChangeWithFiles(repo, "dir/file1", "dir/file2"));
 
     assertQuery("file:.*file.*");
     assertQuery("file:^file.*"); // Whole path only.
@@ -1397,14 +1349,7 @@
   @Test
   public void byPathExact() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
-    RevCommit commit =
-        repo.parseBody(
-            repo.commit()
-                .message("one")
-                .add("dir/file1", "contents1")
-                .add("dir/file2", "contents2")
-                .create());
-    Change change = insert(repo, newChangeForCommit(repo, commit));
+    Change change = insert(repo, newChangeWithFiles(repo, "dir/file1", "dir/file2"));
 
     assertQuery("path:file");
     assertQuery("path:dir");
@@ -1417,20 +1362,248 @@
   @Test
   public void byPathRegex() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
-    RevCommit commit =
-        repo.parseBody(
-            repo.commit()
-                .message("one")
-                .add("dir/file1", "contents1")
-                .add("dir/file2", "contents2")
-                .create());
-    Change change = insert(repo, newChangeForCommit(repo, commit));
+    Change change = insert(repo, newChangeWithFiles(repo, "dir/file1", "dir/file2"));
 
     assertQuery("path:.*file.*");
     assertQuery("path:^dir.file.*", change);
   }
 
   @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"));
+    Change change3 = insert(repo, newChangeWithFiles(repo, "dir/baz.h", "dir/baz.cc"));
+    Change change4 = insert(repo, newChangeWithFiles(repo, "Quux.java", "foo"));
+    Change change5 = insert(repo, newChangeWithFiles(repo, "foo"));
+
+    assertQuery("extension:java", change4);
+    assertQuery("ext:java", change4);
+    assertQuery("ext:.java", change4);
+    assertQuery("ext:jAvA", change4);
+    assertQuery("ext:.jAvA", change4);
+    assertQuery("ext:cc", change3, change2, change1);
+
+    if (getSchemaVersion() >= 56) {
+      // matching changes with files that have no extension is possible
+      assertQuery("ext:\"\"", change5, change4);
+      assertFailingQuery("ext:");
+    }
+  }
+
+  @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"));
+    Change change3 = insert(repo, newChangeWithFiles(repo, "foo.CC", "bar.cc"));
+    Change change4 = insert(repo, newChangeWithFiles(repo, "dir/baz.h", "dir/baz.cc"));
+    Change change5 = insert(repo, newChangeWithFiles(repo, "Quux.java"));
+    Change change6 = insert(repo, newChangeWithFiles(repo, "foo.txt", "foo"));
+    Change change7 = insert(repo, newChangeWithFiles(repo, "foo"));
+
+    // case doesn't matter
+    assertQuery("onlyextensions:cc,h", change4, change2, change1);
+    assertQuery("onlyextensions:CC,H", change4, change2, change1);
+    assertQuery("onlyextensions:cc,H", change4, change2, change1);
+    assertQuery("onlyextensions:cC,h", change4, change2, change1);
+    assertQuery("onlyextensions:cc", change3);
+    assertQuery("onlyextensions:CC", change3);
+    assertQuery("onlyexts:java", change5);
+    assertQuery("onlyexts:jAvA", change5);
+    assertQuery("onlyexts:.jAvA", change5);
+
+    // order doesn't matter
+    assertQuery("onlyextensions:h,cc", change4, change2, change1);
+    assertQuery("onlyextensions:H,CC", change4, change2, change1);
+
+    // specifying extension with '.' is okay
+    assertQuery("onlyextensions:.cc,.h", change4, change2, change1);
+    assertQuery("onlyextensions:cc,.h", change4, change2, change1);
+    assertQuery("onlyextensions:.cc,h", change4, change2, change1);
+    assertQuery("onlyexts:.java", change5);
+
+    // matching changes without extension is possible
+    assertQuery("onlyexts:txt");
+    assertQuery("onlyexts:txt,", change6);
+    assertQuery("onlyexts:,txt", change6);
+    assertQuery("onlyextensions:\"\"", change7);
+    assertQuery("onlyexts:\"\"", change7);
+    assertQuery("onlyextensions:,", change7);
+    assertQuery("onlyexts:,", change7);
+    assertFailingQuery("onlyextensions:");
+    assertFailingQuery("onlyexts:");
+
+    // inverse queries
+    assertQuery("-onlyextensions:cc,h", change7, change6, change5, change3);
+  }
+
+  @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));
+    RevCommit commit2 = repo.parseBody(repo.commit().message("Test\n\nfoo: baz").create());
+    Change change2 = insert(repo, newChangeForCommit(repo, commit2));
+    RevCommit commit3 = repo.parseBody(repo.commit().message("Test\n\nfoo: bar\nfoo:baz").create());
+    Change change3 = insert(repo, newChangeForCommit(repo, commit3));
+    RevCommit commit4 = repo.parseBody(repo.commit().message("Test\n\nfoo: bar=baz").create());
+    Change change4 = insert(repo, newChangeForCommit(repo, commit4));
+
+    // create a changes with lines that look like footers, but which are not
+    RevCommit commit5 =
+        repo.parseBody(
+            repo.commit().message("Test\n\nfoo: bar\n\nfoo=bar").insertChangeId().create());
+    Change change5 = insert(repo, newChangeForCommit(repo, commit5));
+    RevCommit commit6 = repo.parseBody(repo.commit().message("Test\n\na=b: c").create());
+    insert(repo, newChangeForCommit(repo, commit6));
+
+    // matching by 'key=value' works
+    assertQuery("footer:foo=bar", change3, change1);
+    assertQuery("footer:foo=baz", change3, change2);
+    assertQuery("footer:Change-Id=" + change5.getKey(), change5);
+    assertQuery("footer:foo=bar=baz", change4);
+
+    // case doesn't matter
+    assertQuery("footer:foo=BAR", change3, change1);
+    assertQuery("footer:FOO=bar", change3, change1);
+    assertQuery("footer:fOo=BaZ", change3, change2);
+
+    // verbatim matching of footers works
+    assertQuery("footer:\"foo: bar\"", change3, change1);
+    assertQuery("footer:\"foo: baz\"", change3, change2);
+    assertQuery("footer:\"Change-Id: " + change5.getKey() + "\"", change5);
+    assertQuery("footer:\"foo: bar=baz\"", change4);
+
+    // expect no match because 'a=b: c' of commit6 is not a valid footer (footer key cannot contain
+    // '=')
+    assertQuery("footer:a=b=c");
+    assertQuery("footer:\"a=b: c\"");
+
+    // expect empty result for invalid footers
+    assertQuery("footer:foo");
+    assertQuery("footer:foo=");
+    assertQuery("footer:=foo");
+    assertQuery("footer:=");
+  }
+
+  @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"));
+    Change change3 =
+        insert(repo, newChangeWithFiles(repo, "documentation/training/slides/README.txt"));
+    Change change4 = insert(repo, newChangeWithFiles(repo, "a.txt"));
+    Change change5 = insert(repo, newChangeWithFiles(repo, "a/b/c/d/e/foo.txt"));
+
+    // matching by directory prefix works
+    assertQuery("directory:src", change2, change1);
+    assertQuery("directory:src/java", change2);
+    assertQuery("directory:src/js", change2);
+    assertQuery("directory:documentation/", change3);
+    assertQuery("directory:documentation/training", change3);
+    assertQuery("directory:documentation/training/slides", change3);
+
+    // 'dir' alias works
+    assertQuery("dir:src", change2, change1);
+    assertQuery("dir:src/java", change2);
+
+    // case doesn't matter
+    assertQuery("directory:Documentation/TrAiNiNg/SLIDES", change3);
+
+    // leading and trailing '/' doesn't matter
+    assertQuery("directory:/documentation/training/slides", change3);
+    assertQuery("directory:documentation/training/slides/", change3);
+    assertQuery("directory:/documentation/training/slides/", change3);
+
+    // files do not match as directory
+    assertQuery("directory:src/foo.h");
+    assertQuery("directory:documentation/training/slides/README.txt");
+
+    // root directory matches all changes
+    assertQuery("directory:/", change5, change4, change3, change2, change1);
+    assertQuery("directory:\"\"", change5, change4, change3, change2, change1);
+    assertFailingQuery("directory:");
+
+    // matching single directory segments works
+    assertQuery("directory:java", change2);
+    assertQuery("directory:slides", change3);
+
+    // files do not match as directory segment
+    assertQuery("directory:foo.h");
+
+    // matching any combination of intermediate directory segments works
+    assertQuery("directory:training/slides", change3);
+    assertQuery("directory:b/c", change5);
+    assertQuery("directory:b/c/d", change5);
+    assertQuery("directory:b/c/d/e", change5);
+    assertQuery("directory:c/d", change5);
+    assertQuery("directory:c/d/e", change5);
+    assertQuery("directory:d/e", change5);
+
+    // files do not match as directory segments
+    assertQuery("directory:d/e/foo.txt");
+    assertQuery("directory:e/foo.txt");
+
+    // matching any combination of intermediate directory segments works with leading and trailing
+    // '/'
+    assertQuery("directory:/b/c", change5);
+    assertQuery("directory:/b/c/", change5);
+    assertQuery("directory:b/c/", change5);
+  }
+
+  @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 =
+        insert(repo, newChangeWithFiles(repo, "documentation/training/slides/README.txt"));
+
+    // match by regexp
+    assertQuery("directory:^.*va.*", change1);
+    assertQuery("directory:^documentation/.*/slides", change2);
+    assertQuery("directory:^train.*", change2);
+  }
+
+  @Test
   public void byComment() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
     ChangeInserter ins = newChange(repo);
@@ -1441,9 +1614,7 @@
     ReviewInput.CommentInput commentInput = new ReviewInput.CommentInput();
     commentInput.line = 1;
     commentInput.message = "inline";
-    input.comments =
-        ImmutableMap.<String, List<ReviewInput.CommentInput>>of(
-            Patch.COMMIT_MSG, ImmutableList.<ReviewInput.CommentInput>of(commentInput));
+    input.comments = ImmutableMap.of(Patch.COMMIT_MSG, ImmutableList.of(commentInput));
     gApi.changes().id(change.getId().get()).current().review(input);
 
     Map<String, List<CommentInfo>> comments =
@@ -1603,8 +1774,7 @@
   }
 
   @Test
-  public void byHashtagWithNoteDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
+  public void byHashtag() throws Exception {
     List<Change> changes = setUpHashtagChanges();
     assertQuery("hashtag:foo", changes.get(1), changes.get(0));
     assertQuery("hashtag:bar", changes.get(1));
@@ -1618,35 +1788,6 @@
   }
 
   @Test
-  public void byHashtagWithoutNoteDb() throws Exception {
-    assume().that(notesMigration.readChanges()).isFalse();
-
-    notesMigration.setWriteChanges(true);
-    notesMigration.setReadChanges(true);
-    db.close();
-    db = schemaFactory.open();
-    List<Change> changes;
-    try {
-      changes = setUpHashtagChanges();
-      notesMigration.setWriteChanges(false);
-      notesMigration.setReadChanges(false);
-    } finally {
-      db.close();
-    }
-    db = schemaFactory.open();
-    for (Change c : changes) {
-      indexer.index(db, c); // Reindex without hashtag field.
-    }
-    assertQuery("hashtag:foo");
-    assertQuery("hashtag:bar");
-    assertQuery("hashtag:\" bar \"");
-    assertQuery("hashtag:\"a tag\"");
-    assertQuery("hashtag:\" a tag \"");
-    assertQuery("hashtag:#foo");
-    assertQuery("hashtag:\"# #foo\"");
-  }
-
-  @Test
   public void byDefault() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
 
@@ -1662,7 +1803,7 @@
     Change change4 = insert(repo, ins4);
     ReviewInput ri4 = new ReviewInput();
     ri4.message = "toplevel";
-    ri4.labels = ImmutableMap.<String, Short>of("Code-Review", (short) 1);
+    ri4.labels = ImmutableMap.of("Code-Review", (short) 1);
     gApi.changes().id(change4.getId().get()).current().review(ri4);
 
     ChangeInserter ins5 = newChangeWithTopic(repo, "feature5");
@@ -1683,6 +1824,8 @@
     Change[] expected = new Change[] {change6, change5, change4, change3, change2, change1};
     assertQuery("user@example.com", expected);
     assertQuery("repo", expected);
+
+    assertQuery("Code-Review:+1", change4);
   }
 
   @Test
@@ -1721,6 +1864,24 @@
   }
 
   @Test
+  public void visibleToSelf() throws Exception {
+    TestRepository<Repo> repo = createProject("repo");
+    Change change1 = insert(repo, newChange(repo));
+    Change change2 = insert(repo, newChange(repo));
+
+    gApi.changes().id(change2.getChangeId()).setPrivate(true, "private");
+
+    String q = "project:repo";
+    assertQuery(q + " visibleto:self", change2, change1);
+    assertQuery(q + " visibleto:me", change2, change1);
+
+    // Anonymous user cannot see first user's private change.
+    requestContext.setContext(anonymousUserProvider::get);
+    assertQuery(q + " visibleto:self", change1);
+    assertQuery(q + " visibleto:me", change1);
+  }
+
+  @Test
   public void byCommentBy() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
     Change change1 = insert(repo, newChange(repo));
@@ -1734,9 +1895,7 @@
     ReviewInput.CommentInput comment = new ReviewInput.CommentInput();
     comment.line = 1;
     comment.message = "inline";
-    input.comments =
-        ImmutableMap.<String, List<ReviewInput.CommentInput>>of(
-            Patch.COMMIT_MSG, ImmutableList.<ReviewInput.CommentInput>of(comment));
+    input.comments = ImmutableMap.of(Patch.COMMIT_MSG, ImmutableList.of(comment));
     gApi.changes().id(change1.getId().get()).current().review(input);
 
     input = new ReviewInput();
@@ -1777,8 +1936,6 @@
 
   @Test
   public void byDraftByExcludesZombieDrafts() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     Project.NameKey project = new Project.NameKey("repo");
     TestRepository<Repo> repo = createProject(project.get());
     Change change = insert(repo, newChange(repo));
@@ -1793,36 +1950,25 @@
     assertQuery("draftby:" + userId, change);
     assertQuery("commentby:" + userId);
 
-    TestRepository<Repo> allUsers = new TestRepository<>(repoManager.openRepository(allUsersName));
+    try (TestRepository<Repo> allUsers =
+        new TestRepository<>(repoManager.openRepository(allUsersName))) {
+      Ref draftsRef = allUsers.getRepository().exactRef(RefNames.refsDraftComments(id, userId));
+      assertThat(draftsRef).isNotNull();
 
-    Ref draftsRef = allUsers.getRepository().exactRef(RefNames.refsDraftComments(id, userId));
-    assertThat(draftsRef).isNotNull();
+      ReviewInput rin = ReviewInput.dislike();
+      rin.drafts = DraftHandling.PUBLISH_ALL_REVISIONS;
+      gApi.changes().id(id.get()).current().review(rin);
 
-    ReviewInput rin = ReviewInput.dislike();
-    rin.drafts = DraftHandling.PUBLISH_ALL_REVISIONS;
-    gApi.changes().id(id.get()).current().review(rin);
+      assertQuery("draftby:" + userId);
+      assertQuery("commentby:" + userId, change);
+      assertThat(allUsers.getRepository().exactRef(draftsRef.getName())).isNull();
 
-    assertQuery("draftby:" + userId);
-    assertQuery("commentby:" + userId, change);
-    assertThat(allUsers.getRepository().exactRef(draftsRef.getName())).isNull();
-
-    // Re-add drafts ref and ensure it gets filtered out during indexing.
-    allUsers.update(draftsRef.getName(), draftsRef.getObjectId());
-    assertThat(allUsers.getRepository().exactRef(draftsRef.getName())).isNotNull();
-
-    if (PrimaryStorage.of(change) == PrimaryStorage.REVIEW_DB
-        && !notesMigration.disableChangeReviewDb()) {
-      // Record draft ref in noteDbState as well.
-      ReviewDb db = ReviewDbUtil.unwrapDb(this.db);
-      change = db.changes().get(id);
-      NoteDbChangeState.applyDelta(
-          change,
-          NoteDbChangeState.Delta.create(
-              id, Optional.empty(), ImmutableMap.of(userId, draftsRef.getObjectId())));
-      db.changes().update(Collections.singleton(change));
+      // Re-add drafts ref and ensure it gets filtered out during indexing.
+      allUsers.update(draftsRef.getName(), draftsRef.getObjectId());
+      assertThat(allUsers.getRepository().exactRef(draftsRef.getName())).isNotNull();
     }
 
-    indexer.index(db, project, id);
+    indexer.index(project, id);
     assertQuery("draftby:" + userId);
   }
 
@@ -1915,9 +2061,7 @@
     ReviewInput.CommentInput comment = new ReviewInput.CommentInput();
     comment.line = 1;
     comment.message = "inline";
-    input.comments =
-        ImmutableMap.<String, List<ReviewInput.CommentInput>>of(
-            Patch.COMMIT_MSG, ImmutableList.<ReviewInput.CommentInput>of(comment));
+    input.comments = ImmutableMap.of(Patch.COMMIT_MSG, ImmutableList.of(comment));
     gApi.changes().id(change2.getId().get()).current().review(input);
 
     assertQuery("from:" + userId.get(), change2, change1);
@@ -2036,17 +2180,10 @@
     assertQuery("reviewer:self", change3);
 
     requestContext.setContext(newRequestContext(user1));
-    if (notesMigration.readChanges()) {
-      assertQuery("reviewer:" + user1, change1);
-      assertQuery("cc:" + user1, change2);
-      assertQuery("is:cc", change2);
-      assertQuery("cc:self", change2);
-    } else {
-      assertQuery("reviewer:" + user1, change2, change1);
-      assertQuery("cc:" + user1);
-      assertQuery("is:cc");
-      assertQuery("cc:self");
-    }
+    assertQuery("reviewer:" + user1, change1);
+    assertQuery("cc:" + user1, change2);
+    assertQuery("is:cc", change2);
+    assertQuery("cc:self", change2);
   }
 
   @Test
@@ -2106,36 +2243,19 @@
             .collect(toList());
     assertThat(members).contains(user2.toString());
 
-    if (notesMigration.readChanges()) {
-      // CC and REVIEWER are separate in NoteDB
-      assertQuery("reviewerin:\"Registered Users\"", change2, change1);
-      assertQuery("reviewerin:" + group, change2);
-    } else {
-      // CC and REVIEWER are the same in ReviewDb
-      assertQuery("reviewerin:\"Registered Users\"", change3, change2, change1);
-      assertQuery("reviewerin:" + group, change3, change2);
-    }
+    assertQuery("reviewerin:\"Registered Users\"", change2, change1);
+    assertQuery("reviewerin:" + group, change2);
 
     gApi.changes().id(change2.getId().get()).current().review(ReviewInput.approve());
     gApi.changes().id(change2.getId().get()).current().submit();
 
-    if (notesMigration.readChanges()) {
-      // CC and REVIEWER are separate in NoteDB
-      assertQuery("reviewerin:" + group, change2);
-      assertQuery("project:repo reviewerin:" + group, change2);
-      assertQuery("status:merged reviewerin:" + group, change2);
-    } else {
-      // CC and REVIEWER are the same in ReviewDb
-      assertQuery("reviewerin:" + group, change2, change3);
-      assertQuery("project:repo reviewerin:" + group, change2, change3);
-      assertQuery("status:merged reviewerin:" + group, change2);
-    }
+    assertQuery("reviewerin:" + group, change2);
+    assertQuery("project:repo reviewerin:" + group, change2);
+    assertQuery("status:merged reviewerin:" + group, change2);
   }
 
   @Test
   public void reviewerAndCcByEmail() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     Project.NameKey project = new Project.NameKey("repo");
     TestRepository<Repo> repo = createProject(project.get());
     ConfigInput conf = new ConfigInput();
@@ -2182,8 +2302,6 @@
 
   @Test
   public void reviewerAndCcByEmailWithQueryForDifferentUser() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
-
     Project.NameKey project = new Project.NameKey("repo");
     TestRepository<Repo> repo = createProject(project.get());
     ConfigInput conf = new ConfigInput();
@@ -2347,7 +2465,7 @@
 
     for (int i = 1; i <= 11; i++) {
       Iterable<ChangeData> cds =
-          queryProvider.get().byCommitsOnBranchNotMerged(repo.getRepository(), db, dest, shas, i);
+          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);
@@ -2356,73 +2474,14 @@
   }
 
   @Test
-  public void prepopulatedFields() throws Exception {
-    assume().that(notesMigration.readChanges()).isFalse();
-    TestRepository<Repo> repo = createProject("repo");
-    Change change = insert(repo, newChange(repo));
-
-    db = new DisabledReviewDb();
-    requestContext.setContext(newRequestContext(userId));
-    // Use QueryProcessor directly instead of API so we get ChangeDatas back.
-    List<ChangeData> cds =
-        queryProcessorProvider
-            .get()
-            .query(queryBuilder.parse(change.getId().toString()))
-            .entities();
-    assertThat(cds).hasSize(1);
-
-    ChangeData cd = cds.get(0);
-    cd.change();
-    cd.patchSets();
-    cd.currentApprovals();
-    cd.changedLines();
-    cd.reviewedBy();
-    cd.reviewers();
-    cd.unresolvedCommentCount();
-
-    // TODO(dborowitz): Swap out GitRepositoryManager somehow? Will probably be
-    // necessary for NoteDb anyway.
-    cd.isMergeable();
-
-    exception.expect(DisabledReviewDb.Disabled.class);
-    cd.messages();
-  }
-
-  @Test
-  public void prepopulateOnlyRequestedFields() throws Exception {
-    assume().that(notesMigration.readChanges()).isFalse();
-    TestRepository<Repo> repo = createProject("repo");
-    Change change = insert(repo, newChange(repo));
-
-    db = new DisabledReviewDb();
-    requestContext.setContext(newRequestContext(userId));
-    // Use QueryProcessor directly instead of API so we get ChangeDatas back.
-    List<ChangeData> cds =
-        queryProcessorProvider
-            .get()
-            .setRequestedFields(
-                ImmutableSet.of(ChangeField.PATCH_SET.getName(), ChangeField.CHANGE.getName()))
-            .query(queryBuilder.parse(change.getId().toString()))
-            .entities();
-    assertThat(cds).hasSize(1);
-
-    ChangeData cd = cds.get(0);
-    cd.change();
-    cd.patchSets();
-
-    exception.expect(DisabledReviewDb.Disabled.class);
-    cd.currentApprovals();
-  }
-
-  @Test
   public void reindexIfStale() throws Exception {
     Account.Id user = createAccount("user");
     Project.NameKey project = new Project.NameKey("repo");
     TestRepository<Repo> repo = createProject(project.get());
     Change change = insert(repo, newChange(repo));
     String changeId = change.getKey().get();
-    ChangeNotes notes = notesFactory.create(db, change.getProject(), change.getId());
-    PatchSet ps = psUtil.get(db, notes, change.currentPatchSetId());
+    ChangeNotes notes = notesFactory.create(change.getProject(), change.getId());
+    PatchSet ps = psUtil.get(notes, change.currentPatchSetId());
 
     requestContext.setContext(newRequestContext(user));
     gApi.changes().id(changeId).edit().create();
@@ -2442,92 +2501,6 @@
   }
 
   @Test
-  public void refStateFields() throws Exception {
-    // This test method manages primary storage manually.
-    assume().that(notesMigration.changePrimaryStorage()).isEqualTo(PrimaryStorage.REVIEW_DB);
-    Account.Id user = createAccount("user");
-    Project.NameKey project = new Project.NameKey("repo");
-    TestRepository<Repo> repo = createProject(project.get());
-    String path = "file";
-    RevCommit commit = repo.parseBody(repo.commit().message("one").add(path, "contents").create());
-    Change change = insert(repo, newChangeForCommit(repo, commit));
-    Change.Id id = change.getId();
-    int c = id.get();
-    String changeId = change.getKey().get();
-    requestContext.setContext(newRequestContext(user));
-
-    // Ensure one of each type of supported ref is present for the change. If
-    // any more refs are added, update this test to reflect them.
-
-    // Edit
-    gApi.changes().id(changeId).edit().create();
-
-    // Star
-    gApi.accounts().self().starChange(change.getId().toString());
-
-    if (notesMigration.readChanges()) {
-      // Robot comment.
-      ReviewInput rin = new ReviewInput();
-      RobotCommentInput rcin = new RobotCommentInput();
-      rcin.robotId = "happyRobot";
-      rcin.robotRunId = "1";
-      rcin.line = 1;
-      rcin.message = "nit: trailing whitespace";
-      rcin.path = path;
-      rin.robotComments = ImmutableMap.of(path, ImmutableList.of(rcin));
-      gApi.changes().id(c).current().review(rin);
-    }
-
-    // Draft.
-    DraftInput din = new DraftInput();
-    din.path = path;
-    din.line = 1;
-    din.message = "draft";
-    gApi.changes().id(c).current().createDraft(din);
-
-    if (notesMigration.readChanges()) {
-      // Force NoteDb primary.
-      change = ReviewDbUtil.unwrapDb(db).changes().get(id);
-      change.setNoteDbState(NoteDbChangeState.NOTE_DB_PRIMARY_STATE);
-      ReviewDbUtil.unwrapDb(db).changes().update(Collections.singleton(change));
-      indexer.index(db, change);
-    }
-
-    QueryOptions opts =
-        IndexedChangeQuery.createOptions(indexConfig, 0, 1, StalenessChecker.FIELDS);
-    ChangeData cd = indexes.getSearchIndex().get(id, opts).get();
-
-    String cs = RefNames.shard(c);
-    int u = user.get();
-    String us = RefNames.shard(u);
-
-    List<String> expectedStates =
-        Lists.newArrayList(
-            "repo:refs/users/" + us + "/edit-" + c + "/1",
-            "All-Users:refs/starred-changes/" + cs + "/" + u);
-    if (notesMigration.readChanges()) {
-      expectedStates.add("repo:refs/changes/" + cs + "/meta");
-      expectedStates.add("repo:refs/changes/" + cs + "/robot-comments");
-      expectedStates.add("All-Users:refs/draft-comments/" + cs + "/" + u);
-    }
-    assertThat(
-            cd.getRefStates().stream()
-                .map(String::new)
-                // Omit SHA-1, we're just concerned with the project/ref names.
-                .map(s -> s.substring(0, s.lastIndexOf(':')))
-                .collect(toList()))
-        .containsExactlyElementsIn(expectedStates);
-
-    List<String> expectedPatterns = Lists.newArrayList("repo:refs/users/*/edit-" + c + "/*");
-    expectedPatterns.add("All-Users:refs/starred-changes/" + cs + "/*");
-    if (notesMigration.readChanges()) {
-      expectedPatterns.add("All-Users:refs/draft-comments/" + cs + "/*");
-    }
-    assertThat(cd.getRefStatePatterns().stream().map(String::new).collect(toList()))
-        .containsExactlyElementsIn(expectedPatterns);
-  }
-
-  @Test
   public void watched() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
     ChangeInserter ins1 = newChangeWithStatus(repo, Change.Status.NEW);
@@ -2688,7 +2661,7 @@
       return this;
     }
 
-    DashboardChangeState draftAndDeleteCommentBy(Id commenterId) {
+    DashboardChangeState draftAndDeleteCommentBy(Account.Id commenterId) {
       deleteDraftCommentBy.add(commenterId);
       return this;
     }
@@ -3050,16 +3023,18 @@
     String destination4 = "refs/heads/master\trepo3";
     String destination5 = "refs/heads/other\trepo1";
 
-    TestRepository<Repo> allUsers = new TestRepository<>(repoManager.openRepository(allUsersName));
-    String refsUsers = RefNames.refsUsers(userId);
-    allUsers.branch(refsUsers).commit().add("destinations/destination1", destination1).create();
-    allUsers.branch(refsUsers).commit().add("destinations/destination2", destination2).create();
-    allUsers.branch(refsUsers).commit().add("destinations/destination3", destination3).create();
-    allUsers.branch(refsUsers).commit().add("destinations/destination4", destination4).create();
-    allUsers.branch(refsUsers).commit().add("destinations/destination5", destination5).create();
+    try (TestRepository<Repo> allUsers =
+        new TestRepository<>(repoManager.openRepository(allUsersName))) {
+      String refsUsers = RefNames.refsUsers(userId);
+      allUsers.branch(refsUsers).commit().add("destinations/destination1", destination1).create();
+      allUsers.branch(refsUsers).commit().add("destinations/destination2", destination2).create();
+      allUsers.branch(refsUsers).commit().add("destinations/destination3", destination3).create();
+      allUsers.branch(refsUsers).commit().add("destinations/destination4", destination4).create();
+      allUsers.branch(refsUsers).commit().add("destinations/destination5", destination5).create();
 
-    Ref userRef = allUsers.getRepository().exactRef(refsUsers);
-    assertThat(userRef).isNotNull();
+      Ref userRef = allUsers.getRepository().exactRef(refsUsers);
+      assertThat(userRef).isNotNull();
+    }
 
     assertQuery("destination:destination1", change1);
     assertQuery("destination:destination2", change2);
@@ -3080,12 +3055,14 @@
             + "query3\tproject:repo branch:stable\n"
             + "query4\tproject:repo branch:other";
 
-    TestRepository<Repo> allUsers = new TestRepository<>(repoManager.openRepository(allUsersName));
-    String refsUsers = RefNames.refsUsers(userId);
-    allUsers.branch(refsUsers).commit().add("queries", queries).create();
+    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();
+      Ref userRef = allUsers.getRepository().exactRef(refsUsers);
+      assertThat(userRef).isNotNull();
+    }
 
     assertThatQueryException("query:foo").hasMessageThat().isEqualTo("Unknown named query: foo");
 
@@ -3125,6 +3102,64 @@
     assertQuery("project:repo+foo", change);
   }
 
+  @Test
+  public void selfFailsForAnonymousUser() throws Exception {
+    for (String query : ImmutableList.of("assignee:self", "starredby:self", "is:starred")) {
+      assertQuery(query);
+      RequestContext oldContext = requestContext.setContext(anonymousUserProvider::get);
+
+      try {
+        requestContext.setContext(anonymousUserProvider::get);
+        assertThatAuthException(query)
+            .hasMessageThat()
+            .isEqualTo("Must be signed-in to use this operator");
+      } finally {
+        requestContext.setContext(oldContext);
+      }
+    }
+  }
+
+  @Test
+  public void selfSucceedsForInactiveAccount() throws Exception {
+    Account.Id user2 =
+        accountManager.authenticate(AuthRequest.forUser("anotheruser")).getAccountId();
+
+    TestRepository<Repo> repo = createProject("repo");
+    Change change = insert(repo, newChange(repo));
+    AssigneeInput ain = new AssigneeInput();
+    ain.assignee = user2.toString();
+    gApi.changes().id(change.getId().get()).setAssignee(ain);
+
+    RequestContext adminContext = requestContext.setContext(newRequestContext(user2));
+    assertQuery("assignee:self", change);
+
+    requestContext.setContext(adminContext);
+    gApi.accounts().id(user2.get()).setActive(false);
+
+    requestContext.setContext(newRequestContext(user2));
+    assertQuery("assignee:self", change);
+  }
+
+  @Test
+  public void none() throws Exception {
+    TestRepository<Repo> repo = createProject("repo");
+    Change change = insert(repo, newChange(repo));
+
+    assertQuery(ChangeIndexPredicate.none());
+
+    for (Predicate<ChangeData> matchingOneChange :
+        ImmutableList.of(
+            // One index query, one post-filtering query.
+            queryBuilder.parse(change.getId().toString()),
+            queryBuilder.parse("ownerin:Administrators"))) {
+      assertQuery(matchingOneChange, change);
+      assertQuery(Predicate.or(ChangeIndexPredicate.none(), matchingOneChange), change);
+      assertQuery(Predicate.and(ChangeIndexPredicate.none(), matchingOneChange));
+      assertQuery(
+          Predicate.and(Predicate.not(ChangeIndexPredicate.none()), matchingOneChange), change);
+    }
+  }
+
   protected ChangeInserter newChange(TestRepository<Repo> repo) throws Exception {
     return newChange(repo, null, null, null, null, false);
   }
@@ -3134,6 +3169,15 @@
     return newChange(repo, commit, null, null, null, false);
   }
 
+  protected ChangeInserter newChangeWithFiles(TestRepository<Repo> repo, String... paths)
+      throws Exception {
+    TestRepository<?>.CommitBuilder b = repo.commit().message("Change with files");
+    for (String path : paths) {
+      b.add(path, "contents of " + path);
+    }
+    return newChangeForCommit(repo, repo.parseBody(b.create()));
+  }
+
   protected ChangeInserter newChangeForBranch(TestRepository<Repo> repo, String branch)
       throws Exception {
     return newChange(repo, null, branch, null, null, false);
@@ -3200,7 +3244,7 @@
         new Project.NameKey(repo.getRepository().getDescription().getRepositoryName());
     Account.Id ownerId = owner != null ? owner : userId;
     IdentifiedUser user = userFactory.create(ownerId);
-    try (BatchUpdate bu = updateFactory.create(db, project, user, createdOn)) {
+    try (BatchUpdate bu = updateFactory.create(project, user, createdOn)) {
       bu.insertChange(ins);
       bu.execute();
       return ins.getChange();
@@ -3216,15 +3260,15 @@
 
     PatchSetInserter inserter =
         patchSetFactory
-            .create(changeNotesFactory.createChecked(db, c), new PatchSet.Id(c.getId(), n), commit)
-            .setNotify(NotifyHandling.NONE)
+            .create(changeNotesFactory.createChecked(c), new PatchSet.Id(c.getId(), n), commit)
             .setFireRevisionCreated(false)
             .setValidate(false);
-    try (BatchUpdate bu = updateFactory.create(db, c.getProject(), user, TimeUtil.nowTs());
+    try (BatchUpdate bu = updateFactory.create(c.getProject(), user, TimeUtil.nowTs());
         ObjectInserter oi = repo.getRepository().newObjectInserter();
         ObjectReader reader = oi.newReader();
         RevWalk rw = new RevWalk(reader)) {
       bu.setRepository(repo.getRepository(), rw, oi);
+      bu.setNotify(NotifyResolver.Result.none());
       bu.addOp(c.getId(), inserter);
       bu.execute();
     }
@@ -3245,6 +3289,15 @@
     }
   }
 
+  protected ThrowableSubject assertThatAuthException(Object query) throws Exception {
+    try {
+      newQuery(query).get();
+      throw new AssertionError("expected AuthException for query: " + query);
+    } catch (AuthException e) {
+      return assertThat(e);
+    }
+  }
+
   protected TestRepository<Repo> createProject(String name) throws Exception {
     gApi.projects().create(name).get();
     return new TestRepository<>(repoManager.openRepository(new Project.NameKey(name)));
@@ -3280,21 +3333,32 @@
     List<ChangeInfo> result = query.get();
     Iterable<Change.Id> ids = ids(result);
     assertThat(ids)
-        .named(format(query, ids, changes))
+        .named(format(query.getQuery(), ids, changes))
         .containsExactlyElementsIn(Arrays.asList(changes))
         .inOrder();
     return result;
   }
 
-  private String format(
-      QueryRequest query, Iterable<Change.Id> actualIds, Change.Id... expectedChanges)
+  protected void assertQuery(Predicate<ChangeData> predicate, Change... changes) throws Exception {
+    ImmutableList<Change.Id> actualIds =
+        queryProvider.get().query(predicate).stream()
+            .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))
+        .containsExactlyElementsIn(expectedIds)
+        .inOrder();
+  }
+
+  private String format(String query, Iterable<Change.Id> actualIds, Change.Id... expectedChanges)
       throws RestApiException {
-    StringBuilder b = new StringBuilder();
-    b.append("query '").append(query.getQuery()).append("' with expected changes ");
-    b.append(format(Arrays.asList(expectedChanges)));
-    b.append(" and result ");
-    b.append(format(actualIds));
-    return b.toString();
+    return "query '"
+        + query
+        + "' with expected changes "
+        + format(Arrays.asList(expectedChanges))
+        + " and result "
+        + format(actualIds);
   }
 
   private String format(Iterable<Change.Id> changeIds) throws RestApiException {
@@ -3347,9 +3411,7 @@
     comment.line = 1;
     comment.message = message;
     comment.unresolved = unresolved;
-    input.comments =
-        ImmutableMap.<String, List<ReviewInput.CommentInput>>of(
-            Patch.COMMIT_MSG, ImmutableList.<ReviewInput.CommentInput>of(comment));
+    input.comments = ImmutableMap.of(Patch.COMMIT_MSG, ImmutableList.of(comment));
     gApi.changes().id(changeId).current().review(input);
   }
 
@@ -3378,12 +3440,19 @@
         .isFalse();
   }
 
-  protected void assertFailingQuery(String query, String expectedMessage) throws Exception {
+  protected void assertFailingQuery(String query) throws Exception {
+    assertFailingQuery(query, null);
+  }
+
+  protected void assertFailingQuery(String query, @Nullable String expectedMessage)
+      throws Exception {
     try {
       assertQuery(query);
       fail("expected BadRequestException for query '" + query + "'");
     } catch (BadRequestException e) {
-      assertThat(e.getMessage()).isEqualTo(expectedMessage);
+      if (expectedMessage != null) {
+        assertThat(e.getMessage()).isEqualTo(expectedMessage);
+      }
     }
   }
 
diff --git a/javatests/com/google/gerrit/server/query/change/BUILD b/javatests/com/google/gerrit/server/query/change/BUILD
index 511bab6..4a3f508 100644
--- a/javatests/com/google/gerrit/server/query/change/BUILD
+++ b/javatests/com/google/gerrit/server/query/change/BUILD
@@ -22,7 +22,6 @@
         "//java/com/google/gerrit/server/util/time",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib/guice",
         "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/jgit/org.eclipse.jgit.junit:junit",
@@ -44,7 +43,6 @@
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib/guice",
         "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/jgit/org.eclipse.jgit.junit:junit",
@@ -61,13 +59,14 @@
     ),
     visibility = ["//visibility:public"],
     deps = [
+        "//java/com/google/gerrit/exceptions",
         "//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/testing:gerrit-test-util",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/truth",
         "//lib/truth:truth-proto-extension",
diff --git a/javatests/com/google/gerrit/server/query/change/ChangeDataTest.java b/javatests/com/google/gerrit/server/query/change/ChangeDataTest.java
index c8637cd..aba0018 100644
--- a/javatests/com/google/gerrit/server/query/change/ChangeDataTest.java
+++ b/javatests/com/google/gerrit/server/query/change/ChangeDataTest.java
@@ -21,10 +21,11 @@
 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.testing.TestChanges;
 import org.junit.Test;
 
-public class ChangeDataTest {
+public class ChangeDataTest extends GerritBaseTests {
   @Test
   public void setPatchSetsClearsCurrentPatchSet() throws Exception {
     Project.NameKey project = new Project.NameKey("project");
@@ -36,6 +37,6 @@
     PatchSet ps2 = new PatchSet(new PatchSet.Id(cd.getId(), currId + 2));
     cd.setPatchSets(ImmutableList.of(ps1, ps2));
     PatchSet curr2 = cd.currentPatchSet();
-    assertThat(curr2).isNotSameAs(curr1);
+    assertThat(curr2).isNotSameInstanceAs(curr1);
   }
 }
diff --git a/javatests/com/google/gerrit/server/query/change/ConflictKeyTest.java b/javatests/com/google/gerrit/server/query/change/ConflictKeyTest.java
index de2acf0..e550f8e 100644
--- a/javatests/com/google/gerrit/server/query/change/ConflictKeyTest.java
+++ b/javatests/com/google/gerrit/server/query/change/ConflictKeyTest.java
@@ -18,16 +18,18 @@
 import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
 import static com.google.gerrit.extensions.client.SubmitType.FAST_FORWARD_ONLY;
 import static com.google.gerrit.extensions.client.SubmitType.MERGE_IF_NECESSARY;
+import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
 import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.byteString;
-import static com.google.gerrit.server.cache.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableMap;
 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 {
+public class ConflictKeyTest extends GerritBaseTests {
   @Test
   public void ffOnlyPreservesInputOrder() {
     ObjectId id1 = ObjectId.fromString("badc0feebadc0feebadc0feebadc0feebadc0fee");
@@ -81,10 +83,7 @@
     assertThat(ConflictKey.Serializer.INSTANCE.deserialize(serialized)).isEqualTo(key);
   }
 
-  /**
-   * See {@link com.google.gerrit.server.cache.testing.SerializedClassSubject} for background and
-   * what to do if this test fails.
-   */
+  /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
   @Test
   public void methods() throws Exception {
     assertThatSerializedClass(ConflictKey.class)
diff --git a/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
index 1dfe7df..2ea198f 100644
--- a/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
@@ -49,7 +49,7 @@
   protected Injector createInjector() {
     Config luceneConfig = new Config(config);
     InMemoryModule.setDefaults(luceneConfig);
-    return Guice.createInjector(new InMemoryModule(luceneConfig, notesMigration));
+    return Guice.createInjector(new InMemoryModule(luceneConfig));
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/query/change/RegexPathPredicateTest.java b/javatests/com/google/gerrit/server/query/change/RegexPathPredicateTest.java
index a13a8f7..135e9c26 100644
--- a/javatests/com/google/gerrit/server/query/change/RegexPathPredicateTest.java
+++ b/javatests/com/google/gerrit/server/query/change/RegexPathPredicateTest.java
@@ -19,13 +19,13 @@
 
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.testing.GerritBaseTests;
 import java.util.Arrays;
 import org.junit.Test;
 
-public class RegexPathPredicateTest {
+public class RegexPathPredicateTest extends GerritBaseTests {
   @Test
-  public void prefixOnlyOptimization() throws OrmException {
+  public void prefixOnlyOptimization() {
     RegexPathPredicate p = predicate("^a/b/.*");
     assertTrue(p.match(change("a/b/source.c")));
     assertFalse(p.match(change("source.c")));
@@ -35,7 +35,7 @@
   }
 
   @Test
-  public void prefixReducesSearchSpace() throws OrmException {
+  public void prefixReducesSearchSpace() {
     RegexPathPredicate p = predicate("^a/b/.*\\.[ch]");
     assertTrue(p.match(change("a/b/source.c")));
     assertFalse(p.match(change("a/b/source.res")));
@@ -45,7 +45,7 @@
   }
 
   @Test
-  public void fileExtension_Constant() throws OrmException {
+  public void fileExtension_Constant() {
     RegexPathPredicate p = predicate("^.*\\.res");
     assertTrue(p.match(change("test.res")));
     assertTrue(p.match(change("foo/bar/test.res")));
@@ -53,7 +53,7 @@
   }
 
   @Test
-  public void fileExtension_CharacterGroup() throws OrmException {
+  public void fileExtension_CharacterGroup() {
     RegexPathPredicate p = predicate("^.*\\.[ch]");
     assertTrue(p.match(change("test.c")));
     assertTrue(p.match(change("test.h")));
@@ -61,7 +61,7 @@
   }
 
   @Test
-  public void endOfString() throws OrmException {
+  public void endOfString() {
     assertTrue(predicate("^a$").match(change("a")));
     assertFalse(predicate("^a$").match(change("a$")));
 
@@ -70,7 +70,7 @@
   }
 
   @Test
-  public void exactMatch() throws OrmException {
+  public void exactMatch() {
     RegexPathPredicate p = predicate("^foo.c");
     assertTrue(p.match(change("foo.c")));
     assertFalse(p.match(change("foo.cc")));
@@ -81,7 +81,7 @@
     return new RegexPathPredicate(pattern);
   }
 
-  private static ChangeData change(String... files) throws OrmException {
+  private static ChangeData change(String... files) {
     Arrays.sort(files);
     ChangeData cd = ChangeData.createForTest(new Project.NameKey("project"), new Change.Id(1), 1);
     cd.setCurrentFilePaths(Arrays.asList(files));
diff --git a/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java b/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
index fd65f83..4a3c755 100644
--- a/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
+++ b/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
@@ -34,7 +34,6 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -58,11 +57,9 @@
 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.InMemoryDatabase;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.Provider;
-import com.google.inject.util.Providers;
 import java.util.Arrays;
 import java.util.Iterator;
 import java.util.List;
@@ -89,8 +86,6 @@
 
   @Inject private Provider<AnonymousUser> anonymousUser;
 
-  @Inject protected InMemoryDatabase schemaFactory;
-
   @Inject protected SchemaCreator schemaCreator;
 
   @Inject protected ThreadLocalRequestContext requestContext;
@@ -109,7 +104,6 @@
 
   protected LifecycleManager lifecycle;
   protected Injector injector;
-  protected ReviewDb db;
   protected AccountInfo currentUserInfo;
   protected CurrentUser user;
 
@@ -129,12 +123,10 @@
   @After
   public void cleanUp() {
     lifecycle.stop();
-    db.close();
   }
 
   protected void setUpDatabase() throws Exception {
-    db = schemaFactory.open();
-    schemaCreator.create(db);
+    schemaCreator.create();
 
     Account.Id userId =
         createAccountOutsideRequestContext("user", "User", "user@example.com", true);
@@ -147,32 +139,11 @@
 
   protected RequestContext newRequestContext(Account.Id requestUserId) {
     final CurrentUser requestUser = userFactory.create(requestUserId);
-    return new RequestContext() {
-      @Override
-      public CurrentUser getUser() {
-        return requestUser;
-      }
-
-      @Override
-      public Provider<ReviewDb> getReviewDbProvider() {
-        return Providers.of(db);
-      }
-    };
+    return () -> requestUser;
   }
 
   protected void setAnonymous() {
-    requestContext.setContext(
-        new RequestContext() {
-          @Override
-          public CurrentUser getUser() {
-            return anonymousUser.get();
-          }
-
-          @Override
-          public Provider<ReviewDb> getReviewDbProvider() {
-            return Providers.of(db);
-          }
-        });
+    requestContext.setContext(anonymousUser::get);
   }
 
   @After
@@ -183,10 +154,6 @@
     if (requestContext != null) {
       requestContext.setContext(null);
     }
-    if (db != null) {
-      db.close();
-    }
-    InMemoryDatabase.drop(schemaFactory);
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/query/group/LuceneQueryGroupsTest.java b/javatests/com/google/gerrit/server/query/group/LuceneQueryGroupsTest.java
index 83835c1..2a453a0 100644
--- a/javatests/com/google/gerrit/server/query/group/LuceneQueryGroupsTest.java
+++ b/javatests/com/google/gerrit/server/query/group/LuceneQueryGroupsTest.java
@@ -43,6 +43,6 @@
   protected Injector createInjector() {
     Config luceneConfig = new Config(config);
     InMemoryModule.setDefaults(luceneConfig);
-    return Guice.createInjector(new InMemoryModule(luceneConfig, notesMigration));
+    return Guice.createInjector(new InMemoryModule(luceneConfig));
   }
 }
diff --git a/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java b/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
index 7872b44..08ef2b0 100644
--- a/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
+++ b/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
@@ -39,7 +39,6 @@
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -58,11 +57,9 @@
 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.InMemoryDatabase;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.Provider;
-import com.google.inject.util.Providers;
 import java.util.Arrays;
 import java.util.Iterator;
 import java.util.List;
@@ -89,8 +86,6 @@
 
   @Inject private Provider<AnonymousUser> anonymousUser;
 
-  @Inject protected InMemoryDatabase schemaFactory;
-
   @Inject protected SchemaCreator schemaCreator;
 
   @Inject protected ThreadLocalRequestContext requestContext;
@@ -105,7 +100,6 @@
 
   protected LifecycleManager lifecycle;
   protected Injector injector;
-  protected ReviewDb db;
   protected AccountInfo currentUserInfo;
   protected CurrentUser user;
 
@@ -125,12 +119,10 @@
   @After
   public void cleanUp() {
     lifecycle.stop();
-    db.close();
   }
 
   protected void setUpDatabase() throws Exception {
-    db = schemaFactory.open();
-    schemaCreator.create(db);
+    schemaCreator.create();
 
     Account.Id userId = createAccount("user", "User", "user@example.com", true);
     user = userFactory.create(userId);
@@ -142,32 +134,11 @@
 
   protected RequestContext newRequestContext(Account.Id requestUserId) {
     final CurrentUser requestUser = userFactory.create(requestUserId);
-    return new RequestContext() {
-      @Override
-      public CurrentUser getUser() {
-        return requestUser;
-      }
-
-      @Override
-      public Provider<ReviewDb> getReviewDbProvider() {
-        return Providers.of(db);
-      }
-    };
+    return () -> requestUser;
   }
 
   protected void setAnonymous() {
-    requestContext.setContext(
-        new RequestContext() {
-          @Override
-          public CurrentUser getUser() {
-            return anonymousUser.get();
-          }
-
-          @Override
-          public Provider<ReviewDb> getReviewDbProvider() {
-            return Providers.of(db);
-          }
-        });
+    requestContext.setContext(anonymousUser::get);
   }
 
   @After
@@ -176,10 +147,6 @@
       lifecycle.stop();
     }
     requestContext.setContext(null);
-    if (db != null) {
-      db.close();
-    }
-    InMemoryDatabase.drop(schemaFactory);
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/query/project/LuceneQueryProjectsTest.java b/javatests/com/google/gerrit/server/query/project/LuceneQueryProjectsTest.java
index 42964fa..77a56ed 100644
--- a/javatests/com/google/gerrit/server/query/project/LuceneQueryProjectsTest.java
+++ b/javatests/com/google/gerrit/server/query/project/LuceneQueryProjectsTest.java
@@ -45,6 +45,6 @@
   protected Injector createInjector() {
     Config luceneConfig = new Config(config);
     InMemoryModule.setDefaults(luceneConfig);
-    return Guice.createInjector(new InMemoryModule(luceneConfig, notesMigration));
+    return Guice.createInjector(new InMemoryModule(luceneConfig));
   }
 }
diff --git a/javatests/com/google/gerrit/server/rules/GerritCommonTest.java b/javatests/com/google/gerrit/server/rules/GerritCommonTest.java
index 086dd65..180c16b 100644
--- a/javatests/com/google/gerrit/server/rules/GerritCommonTest.java
+++ b/javatests/com/google/gerrit/server/rules/GerritCommonTest.java
@@ -49,7 +49,7 @@
             bind(PrologEnvironment.Args.class)
                 .toInstance(
                     new PrologEnvironment.Args(
-                        null, null, null, null, null, null, null, cfg, null, null));
+                        null, null, null, null, null, null, null, null, cfg, null, null));
           }
         });
   }
diff --git a/javatests/com/google/gerrit/server/rules/IgnoreSelfApprovalRuleTest.java b/javatests/com/google/gerrit/server/rules/IgnoreSelfApprovalRuleTest.java
index 27f4423..14124fa 100644
--- a/javatests/com/google/gerrit/server/rules/IgnoreSelfApprovalRuleTest.java
+++ b/javatests/com/google/gerrit/server/rules/IgnoreSelfApprovalRuleTest.java
@@ -24,6 +24,7 @@
 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.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -31,7 +32,7 @@
 import java.util.List;
 import org.junit.Test;
 
-public class IgnoreSelfApprovalRuleTest {
+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);
   private static final LabelType VERIFIED = makeLabel("Verified");
diff --git a/javatests/com/google/gerrit/server/rules/PrologRuleEvaluatorTest.java b/javatests/com/google/gerrit/server/rules/PrologRuleEvaluatorTest.java
index 8622b32..6eb0747 100644
--- a/javatests/com/google/gerrit/server/rules/PrologRuleEvaluatorTest.java
+++ b/javatests/com/google/gerrit/server/rules/PrologRuleEvaluatorTest.java
@@ -16,9 +16,10 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class PrologRuleEvaluatorTest {
+public class PrologRuleEvaluatorTest extends GerritBaseTests {
 
   @Test
   public void validLabelNamesAreKept() {
diff --git a/javatests/com/google/gerrit/server/schema/AllProjectsCreatorTest.java b/javatests/com/google/gerrit/server/schema/AllProjectsCreatorTest.java
new file mode 100644
index 0000000..e5890c9
--- /dev/null
+++ b/javatests/com/google/gerrit/server/schema/AllProjectsCreatorTest.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.schema;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.schema.AllProjectsInput.getDefaultCodeReviewLabel;
+import static com.google.gerrit.server.schema.testing.AllProjectsCreatorTestUtil.assertSectionEquivalent;
+import static com.google.gerrit.server.schema.testing.AllProjectsCreatorTestUtil.assertTwoConfigsEquivalent;
+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 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.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;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Before;
+import org.junit.Test;
+
+public class AllProjectsCreatorTest extends GerritBaseTests {
+  private static final LabelType TEST_LABEL =
+      new LabelType(
+          "Test-Label",
+          ImmutableList.of(
+              new LabelValue((short) 2, "Two"),
+              new LabelValue((short) 0, "Zero"),
+              new LabelValue((short) 1, "One")));
+
+  private static final String TEST_LABEL_STRING =
+      String.join(
+          "\n",
+          ImmutableList.of(
+              "[label \"Test-Label\"]",
+              "\tfunction = MaxWithBlock",
+              "\tdefaultValue = 0",
+              "\tvalue = 0 Zero",
+              "\tvalue = +1 One",
+              "\tvalue = +2 Two"));
+
+  @Inject private AllProjectsName allProjectsName;
+
+  @Inject @GerritPersonIdent private PersonIdent serverUser;
+
+  @Inject private AllProjectsCreator allProjectsCreator;
+
+  @Inject private GitRepositoryManager repoManager;
+
+  @Before
+  public void setUp() throws Exception {
+    InMemoryModule inMemoryModule = new InMemoryModule();
+    inMemoryModule.inject(this);
+
+    // Creates an empty All-Projects.
+    try (Repository repo = repoManager.createRepository(allProjectsName)) {
+      // Intentionally empty.
+    }
+  }
+
+  @Test
+  public void createDefaultAllProjectsConfig() throws Exception {
+    // Loads the expected configs.
+    Config expectedConfig = new Config();
+    expectedConfig.fromText(getDefaultAllProjectsWithAllDefaultSections());
+
+    GroupReference adminsGroup = createGroupReference("Administrators");
+    GroupReference batchUsersGroup = createGroupReference("Non-Interactive Users");
+    AllProjectsInput allProjectsInput =
+        AllProjectsInput.builder()
+            .administratorsGroup(adminsGroup)
+            .batchUsersGroup(batchUsersGroup)
+            .build();
+    allProjectsCreator.create(allProjectsInput);
+
+    Config config = readAllProjectsConfig(repoManager, allProjectsName);
+    assertTwoConfigsEquivalent(config, expectedConfig);
+  }
+
+  private GroupReference createGroupReference(String name) {
+    AccountGroup.UUID groupUuid = GroupUUID.make(name, serverUser);
+    return new GroupReference(groupUuid, name);
+  }
+
+  @Test
+  public void createAllProjectsWithNewCodeReviewLabel() throws Exception {
+    Config expectedLabelConfig = new Config();
+    expectedLabelConfig.fromText(TEST_LABEL_STRING);
+
+    AllProjectsInput allProjectsInput =
+        AllProjectsInput.builder().codeReviewLabel(TEST_LABEL).build();
+    allProjectsCreator.create(allProjectsInput);
+
+    Config config = readAllProjectsConfig(repoManager, allProjectsName);
+    assertSectionEquivalent(config, expectedLabelConfig, "label");
+  }
+
+  @Test
+  public void createAllProjectsWithProjectDescription() throws Exception {
+    String testDescription = "test description";
+    AllProjectsInput allProjectsInput =
+        AllProjectsInput.builder().projectDescription(testDescription).build();
+    allProjectsCreator.create(allProjectsInput);
+
+    Config config = readAllProjectsConfig(repoManager, allProjectsName);
+    assertThat(config.getString("project", null, "description")).isEqualTo(testDescription);
+  }
+
+  @Test
+  public void createAllProjectsWithBooleanConfigs() throws Exception {
+    AllProjectsInput allProjectsInput =
+        AllProjectsInput.builderWithNoDefault()
+            .codeReviewLabel(getDefaultCodeReviewLabel())
+            .firstChangeIdForNoteDb(Sequences.FIRST_CHANGE_ID)
+            .addBooleanProjectConfig(
+                BooleanProjectConfig.REJECT_EMPTY_COMMIT, InheritableBoolean.TRUE)
+            .initDefaultAcls(true)
+            .build();
+    allProjectsCreator.create(allProjectsInput);
+
+    Config config = readAllProjectsConfig(repoManager, allProjectsName);
+    assertThat(config.getBoolean("submit", null, "rejectEmptyCommit", false)).isTrue();
+  }
+
+  @Test
+  public void createAllProjectsWithoutInitializingDefaultACLs() throws Exception {
+    AllProjectsInput allProjectsInput = AllProjectsInput.builder().initDefaultAcls(false).build();
+    allProjectsCreator.create(allProjectsInput);
+
+    Config expectedConfig = new Config();
+    expectedConfig.fromText(getAllProjectsWithoutDefaultAcls());
+    Config config = readAllProjectsConfig(repoManager, allProjectsName);
+    assertTwoConfigsEquivalent(config, expectedConfig);
+  }
+
+  @Test
+  public void createAllProjectsOnlyInitializingProjectDescription() throws Exception {
+    String description = "a project.config with just a project description";
+    AllProjectsInput allProjectsInput =
+        AllProjectsInput.builderWithNoDefault()
+            .firstChangeIdForNoteDb(Sequences.FIRST_CHANGE_ID)
+            .projectDescription(description)
+            .initDefaultAcls(false)
+            .build();
+    allProjectsCreator.create(allProjectsInput);
+
+    Config expectedConfig = new Config();
+    expectedConfig.setString("project", null, "description", description);
+    Config config = readAllProjectsConfig(repoManager, allProjectsName);
+    assertTwoConfigsEquivalent(config, expectedConfig);
+  }
+}
diff --git a/javatests/com/google/gerrit/server/schema/GroupBundleTest.java b/javatests/com/google/gerrit/server/schema/GroupBundleTest.java
deleted file mode 100644
index c1de3a3..0000000
--- a/javatests/com/google/gerrit/server/schema/GroupBundleTest.java
+++ /dev/null
@@ -1,146 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupById;
-import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
-import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
-import com.google.gerrit.server.schema.GroupBundle.Source;
-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.TimeZone;
-import java.util.concurrent.TimeUnit;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-public class GroupBundleTest extends GerritBaseTests {
-  // This class just contains sanity checks that GroupBundle#compare correctly compares all parts of
-  // the bundle. Most other test coverage should come via the slightly more realistic
-  // GroupRebuilderTest.
-
-  private static final String TIMEZONE_ID = "US/Eastern";
-
-  private String systemTimeZoneProperty;
-  private TimeZone systemTimeZone;
-  private Timestamp ts;
-
-  @Before
-  public void setUp() {
-    systemTimeZoneProperty = System.setProperty("user.timezone", TIMEZONE_ID);
-    systemTimeZone = TimeZone.getDefault();
-    TimeZone.setDefault(TimeZone.getTimeZone(TIMEZONE_ID));
-    TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS);
-    ts = TimeUtil.nowTs();
-  }
-
-  @After
-  public void tearDown() {
-    TestTimeUtil.useSystemTime();
-    System.setProperty("user.timezone", systemTimeZoneProperty);
-    TimeZone.setDefault(systemTimeZone);
-  }
-
-  @Test
-  public void compareNonEqual() throws Exception {
-    GroupBundle reviewDbBundle = newBundle().source(Source.REVIEW_DB).build();
-    AccountGroup g2 = new AccountGroup(reviewDbBundle.group());
-    g2.setDescription("Hello!");
-    GroupBundle noteDbBundle = GroupBundle.builder().source(Source.NOTE_DB).group(g2).build();
-    assertThat(GroupBundle.compareWithAudits(reviewDbBundle, noteDbBundle))
-        .containsExactly(
-            "AccountGroups differ\n"
-                + ("ReviewDb: AccountGroup{name=group, groupId=1, description=null,"
-                    + " visibleToAll=false, groupUUID=group-1, ownerGroupUUID=group-1,"
-                    + " createdOn=2009-09-30 17:00:00.0}\n")
-                + ("NoteDb  : AccountGroup{name=group, groupId=1, description=Hello!,"
-                    + " visibleToAll=false, groupUUID=group-1, ownerGroupUUID=group-1,"
-                    + " createdOn=2009-09-30 17:00:00.0}"),
-            "AccountGroupMembers differ\n"
-                + "ReviewDb: [AccountGroupMember{key=1000,1}]\n"
-                + "NoteDb  : []",
-            "AccountGroupMemberAudits differ\n"
-                + ("ReviewDb: [AccountGroupMemberAudit{key=Key{groupId=1, accountId=1000,"
-                    + " addedOn=2009-09-30 17:00:00.0}, addedBy=2000, removedBy=null,"
-                    + " removedOn=null}]\n")
-                + "NoteDb  : []",
-            "AccountGroupByIds differ\n"
-                + "ReviewDb: [AccountGroupById{key=1,subgroup}]\n"
-                + "NoteDb  : []",
-            "AccountGroupByIdAudits differ\n"
-                + ("ReviewDb: [AccountGroupByIdAud{key=Key{groupId=1, includeUUID=subgroup,"
-                    + " addedOn=2009-09-30 17:00:00.0}, addedBy=3000, removedBy=null,"
-                    + " removedOn=null}]\n")
-                + "NoteDb  : []");
-  }
-
-  @Test
-  public void compareIgnoreAudits() throws Exception {
-    GroupBundle reviewDbBundle = newBundle().source(Source.REVIEW_DB).build();
-    AccountGroup group = new AccountGroup(reviewDbBundle.group());
-
-    AccountGroupMember member =
-        new AccountGroupMember(new AccountGroupMember.Key(new Account.Id(1), group.getId()));
-    AccountGroupMemberAudit memberAudit =
-        new AccountGroupMemberAudit(member, new Account.Id(2), ts);
-    AccountGroupById byId =
-        new AccountGroupById(
-            new AccountGroupById.Key(group.getId(), new AccountGroup.UUID("subgroup-2")));
-    AccountGroupByIdAud byIdAudit = new AccountGroupByIdAud(byId, new Account.Id(3), ts);
-
-    GroupBundle noteDbBundle =
-        newBundle().source(Source.NOTE_DB).memberAudit(memberAudit).byIdAudit(byIdAudit).build();
-
-    assertThat(GroupBundle.compareWithAudits(reviewDbBundle, noteDbBundle)).isNotEmpty();
-    assertThat(GroupBundle.compareWithoutAudits(reviewDbBundle, noteDbBundle)).isEmpty();
-  }
-
-  @Test
-  public void compareEqual() throws Exception {
-    GroupBundle reviewDbBundle = newBundle().source(Source.REVIEW_DB).build();
-    GroupBundle noteDbBundle = newBundle().source(Source.NOTE_DB).build();
-    assertThat(GroupBundle.compareWithAudits(reviewDbBundle, noteDbBundle)).isEmpty();
-  }
-
-  private GroupBundle.Builder newBundle() {
-    AccountGroup group =
-        new AccountGroup(
-            new AccountGroup.NameKey("group"),
-            new AccountGroup.Id(1),
-            new AccountGroup.UUID("group-1"),
-            ts);
-    AccountGroupMember member =
-        new AccountGroupMember(new AccountGroupMember.Key(new Account.Id(1000), group.getId()));
-    AccountGroupMemberAudit memberAudit =
-        new AccountGroupMemberAudit(member, new Account.Id(2000), ts);
-    AccountGroupById byId =
-        new AccountGroupById(
-            new AccountGroupById.Key(group.getId(), new AccountGroup.UUID("subgroup")));
-    AccountGroupByIdAud byIdAudit = new AccountGroupByIdAud(byId, new Account.Id(3000), ts);
-    return GroupBundle.builder()
-        .group(group)
-        .members(member)
-        .memberAudit(memberAudit)
-        .byId(byId)
-        .byIdAudit(byIdAudit);
-  }
-}
diff --git a/javatests/com/google/gerrit/server/schema/GroupRebuilderTest.java b/javatests/com/google/gerrit/server/schema/GroupRebuilderTest.java
deleted file mode 100644
index bf6953a..0000000
--- a/javatests/com/google/gerrit/server/schema/GroupRebuilderTest.java
+++ /dev/null
@@ -1,747 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import static com.google.common.collect.ImmutableList.toImmutableList;
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
-import static com.google.gerrit.extensions.common.testing.CommitInfoSubject.assertThat;
-import static com.google.gerrit.reviewdb.client.RefNames.REFS_GROUPNAMES;
-
-import com.google.common.collect.ImmutableList;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupReference;
-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.AccountGroupById;
-import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
-import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
-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.group.db.AuditLogFormatter;
-import com.google.gerrit.server.group.db.AuditLogReader;
-import com.google.gerrit.server.group.db.GroupNameNotes;
-import com.google.gerrit.server.update.RefUpdateUtil;
-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.InMemoryRepositoryManager;
-import com.google.gerrit.testing.TestTimeUtil;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
-import java.sql.Timestamp;
-import java.util.Optional;
-import java.util.TimeZone;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.stream.IntStream;
-import org.eclipse.jgit.lib.BatchRefUpdate;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-public class GroupRebuilderTest extends GerritBaseTests {
-  private static final TimeZone TZ = TimeZone.getTimeZone("America/Los_Angeles");
-  private static final String SERVER_ID = "server-id";
-  private static final String SERVER_NAME = "Gerrit Server";
-  private static final String SERVER_EMAIL = "noreply@gerritcodereview.com";
-
-  private AtomicInteger idCounter;
-  private AllUsersName allUsersName;
-  private Repository repo;
-  private GroupRebuilder rebuilder;
-  private GroupBundle.Factory bundleFactory;
-
-  @Before
-  public void setUp() throws Exception {
-    TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS);
-    idCounter = new AtomicInteger();
-    allUsersName = new AllUsersName(AllUsersNameProvider.DEFAULT);
-    repo = new InMemoryRepositoryManager().createRepository(allUsersName);
-    rebuilder =
-        new GroupRebuilder(
-            GroupRebuilderTest.newPersonIdent(),
-            allUsersName,
-            // Note that the expected name/email values in tests are not necessarily realistic,
-            // since they use these trivial name/email functions.
-            getAuditLogFormatter());
-    bundleFactory = new GroupBundle.Factory(new AuditLogReader(SERVER_ID, allUsersName));
-  }
-
-  @After
-  public void tearDown() {
-    TestTimeUtil.useSystemTime();
-  }
-
-  @Test
-  public void minimalGroupFields() throws Exception {
-    AccountGroup g = newGroup("a");
-    GroupBundle b = builder().group(g).build();
-
-    rebuilder.rebuild(repo, b, null);
-
-    assertMigratedCleanly(reload(g), b);
-    ImmutableList<CommitInfo> log = log(g);
-    assertThat(log).hasSize(1);
-    assertCommit(log.get(0), "Create group", SERVER_NAME, SERVER_EMAIL);
-    assertThat(logGroupNames()).isEmpty();
-  }
-
-  @Test
-  public void allGroupFields() throws Exception {
-    AccountGroup g = newGroup("a");
-    g.setDescription("Description");
-    g.setOwnerGroupUUID(new AccountGroup.UUID("owner"));
-    g.setVisibleToAll(true);
-    GroupBundle b = builder().group(g).build();
-
-    rebuilder.rebuild(repo, b, null);
-
-    assertMigratedCleanly(reload(g), b);
-    ImmutableList<CommitInfo> log = log(g);
-    assertThat(log).hasSize(1);
-    assertServerCommit(log.get(0), "Create group");
-  }
-
-  @Test
-  public void emptyGroupName() throws Exception {
-    AccountGroup g = newGroup("");
-    GroupBundle b = builder().group(g).build();
-
-    rebuilder.rebuild(repo, b, null);
-
-    GroupBundle noteDbBundle = reload(g);
-    assertMigratedCleanly(noteDbBundle, b);
-    assertThat(noteDbBundle.group().getName()).isEmpty();
-  }
-
-  @Test
-  public void nullGroupDescription() throws Exception {
-    AccountGroup g = newGroup("a");
-    g.setDescription(null);
-    assertThat(g.getDescription()).isNull();
-    GroupBundle b = builder().group(g).build();
-
-    rebuilder.rebuild(repo, b, null);
-
-    GroupBundle noteDbBundle = reload(g);
-    assertMigratedCleanly(noteDbBundle, b);
-    assertThat(noteDbBundle.group().getDescription()).isNull();
-  }
-
-  @Test
-  public void emptyGroupDescription() throws Exception {
-    AccountGroup g = newGroup("a");
-    g.setDescription("");
-    assertThat(g.getDescription()).isEmpty();
-    GroupBundle b = builder().group(g).build();
-
-    rebuilder.rebuild(repo, b, null);
-
-    GroupBundle noteDbBundle = reload(g);
-    assertMigratedCleanly(noteDbBundle, b);
-    assertThat(noteDbBundle.group().getDescription()).isNull();
-  }
-
-  @Test
-  public void membersAndSubgroups() throws Exception {
-    AccountGroup g = newGroup("a");
-    GroupBundle b =
-        builder()
-            .group(g)
-            .members(member(g, 1), member(g, 2))
-            .byId(byId(g, "x"), byId(g, "y"))
-            .build();
-
-    rebuilder.rebuild(repo, b, null);
-
-    assertMigratedCleanly(reload(g), b);
-    ImmutableList<CommitInfo> log = log(g);
-    assertThat(log).hasSize(2);
-    assertServerCommit(log.get(0), "Create group");
-    assertServerCommit(
-        log.get(1),
-        "Update group\n"
-            + "\n"
-            + "Add-group: Group x <x>\n"
-            + "Add-group: Group y <y>\n"
-            + "Add: Account 1 <1@server-id>\n"
-            + "Add: Account 2 <2@server-id>");
-  }
-
-  @Test
-  public void memberAudit() throws Exception {
-    AccountGroup g = newGroup("a");
-    Timestamp t1 = TimeUtil.nowTs();
-    Timestamp t2 = TimeUtil.nowTs();
-    Timestamp t3 = TimeUtil.nowTs();
-    GroupBundle b =
-        builder()
-            .group(g)
-            .members(member(g, 1))
-            .memberAudit(addMember(g, 1, 8, t2), addAndRemoveMember(g, 2, 8, t1, 9, t3))
-            .build();
-
-    rebuilder.rebuild(repo, b, null);
-
-    assertMigratedCleanly(reload(g), b);
-    ImmutableList<CommitInfo> log = log(g);
-    assertThat(log).hasSize(4);
-    assertServerCommit(log.get(0), "Create group");
-    assertCommit(
-        log.get(1), "Update group\n\nAdd: Account 2 <2@server-id>", "Account 8", "8@server-id");
-    assertCommit(
-        log.get(2), "Update group\n\nAdd: Account 1 <1@server-id>", "Account 8", "8@server-id");
-    assertCommit(
-        log.get(3), "Update group\n\nRemove: Account 2 <2@server-id>", "Account 9", "9@server-id");
-  }
-
-  @Test
-  public void memberAuditLegacyRemoved() throws Exception {
-    AccountGroup g = newGroup("a");
-    GroupBundle b =
-        builder()
-            .group(g)
-            .members(member(g, 2))
-            .memberAudit(
-                addAndLegacyRemoveMember(g, 1, 8, TimeUtil.nowTs()),
-                addMember(g, 2, 8, TimeUtil.nowTs()))
-            .build();
-
-    rebuilder.rebuild(repo, b, null);
-
-    assertMigratedCleanly(reload(g), b);
-    ImmutableList<CommitInfo> log = log(g);
-    assertThat(log).hasSize(4);
-    assertServerCommit(log.get(0), "Create group");
-    assertCommit(
-        log.get(1), "Update group\n\nAdd: Account 1 <1@server-id>", "Account 8", "8@server-id");
-    assertCommit(
-        log.get(2), "Update group\n\nRemove: Account 1 <1@server-id>", "Account 8", "8@server-id");
-    assertCommit(
-        log.get(3), "Update group\n\nAdd: Account 2 <2@server-id>", "Account 8", "8@server-id");
-  }
-
-  @Test
-  public void unauditedMembershipsAddedAtEnd() throws Exception {
-    AccountGroup g = newGroup("a");
-    GroupBundle b =
-        builder()
-            .group(g)
-            .members(member(g, 1), member(g, 2), member(g, 3))
-            .memberAudit(addMember(g, 1, 8, TimeUtil.nowTs()))
-            .build();
-
-    rebuilder.rebuild(repo, b, null);
-
-    assertMigratedCleanly(reload(g), b);
-    ImmutableList<CommitInfo> log = log(g);
-    assertThat(log).hasSize(3);
-    assertServerCommit(log.get(0), "Create group");
-    assertCommit(
-        log.get(1), "Update group\n\nAdd: Account 1 <1@server-id>", "Account 8", "8@server-id");
-    assertServerCommit(
-        log.get(2), "Update group\n\nAdd: Account 2 <2@server-id>\nAdd: Account 3 <3@server-id>");
-  }
-
-  @Test
-  public void byIdAudit() throws Exception {
-    AccountGroup g = newGroup("a");
-    Timestamp t1 = TimeUtil.nowTs();
-    Timestamp t2 = TimeUtil.nowTs();
-    Timestamp t3 = TimeUtil.nowTs();
-    GroupBundle b =
-        builder()
-            .group(g)
-            .byId(byId(g, "x"))
-            .byIdAudit(addById(g, "x", 8, t2), addAndRemoveById(g, "y", 8, t1, 9, t3))
-            .build();
-
-    rebuilder.rebuild(repo, b, null);
-
-    assertMigratedCleanly(reload(g), b);
-    ImmutableList<CommitInfo> log = log(g);
-    assertThat(log).hasSize(4);
-    assertServerCommit(log.get(0), "Create group");
-    assertCommit(log.get(1), "Update group\n\nAdd-group: Group y <y>", "Account 8", "8@server-id");
-    assertCommit(log.get(2), "Update group\n\nAdd-group: Group x <x>", "Account 8", "8@server-id");
-    assertCommit(
-        log.get(3), "Update group\n\nRemove-group: Group y <y>", "Account 9", "9@server-id");
-  }
-
-  @Test
-  public void unauditedByIdAddedAtEnd() throws Exception {
-    AccountGroup g = newGroup("a");
-    GroupBundle b =
-        builder()
-            .group(g)
-            .byId(byId(g, "x"), byId(g, "y"), byId(g, "z"))
-            .byIdAudit(addById(g, "x", 8, TimeUtil.nowTs()))
-            .build();
-
-    rebuilder.rebuild(repo, b, null);
-
-    assertMigratedCleanly(reload(g), b);
-    ImmutableList<CommitInfo> log = log(g);
-    assertThat(log).hasSize(3);
-    assertServerCommit(log.get(0), "Create group");
-    assertCommit(log.get(1), "Update group\n\nAdd-group: Group x <x>", "Account 8", "8@server-id");
-    assertServerCommit(
-        log.get(2), "Update group\n\nAdd-group: Group y <y>\nAdd-group: Group z <z>");
-  }
-
-  @Test
-  public void auditsAtSameTimestampBrokenDownByType() throws Exception {
-    AccountGroup g = newGroup("a");
-    Timestamp ts = TimeUtil.nowTs();
-    int user = 8;
-    GroupBundle b =
-        builder()
-            .group(g)
-            .members(member(g, 1), member(g, 2))
-            .memberAudit(
-                addMember(g, 1, user, ts),
-                addMember(g, 2, user, ts),
-                addAndRemoveMember(g, 3, user, ts, user, ts))
-            .byId(byId(g, "x"), byId(g, "y"))
-            .byIdAudit(
-                addById(g, "x", user, ts),
-                addById(g, "y", user, ts),
-                addAndRemoveById(g, "z", user, ts, user, ts))
-            .build();
-
-    rebuilder.rebuild(repo, b, null);
-
-    assertMigratedCleanly(reload(g), b);
-    ImmutableList<CommitInfo> log = log(g);
-    assertThat(log).hasSize(5);
-    assertServerCommit(log.get(0), "Create group");
-    assertCommit(
-        log.get(1),
-        "Update group\n"
-            + "\n"
-            + "Add: Account 1 <1@server-id>\n"
-            + "Add: Account 2 <2@server-id>\n"
-            + "Add: Account 3 <3@server-id>",
-        "Account 8",
-        "8@server-id");
-    assertCommit(
-        log.get(2), "Update group\n\nRemove: Account 3 <3@server-id>", "Account 8", "8@server-id");
-    assertCommit(
-        log.get(3),
-        "Update group\n"
-            + "\n"
-            + "Add-group: Group x <x>\n"
-            + "Add-group: Group y <y>\n"
-            + "Add-group: Group z <z>",
-        "Account 8",
-        "8@server-id");
-    assertCommit(
-        log.get(4), "Update group\n\nRemove-group: Group z <z>", "Account 8", "8@server-id");
-  }
-
-  @Test
-  public void auditsAtSameTimestampBrokenDownByUserAndType() throws Exception {
-    AccountGroup g = newGroup("a");
-    Timestamp ts = TimeUtil.nowTs();
-    int user1 = 8;
-    int user2 = 9;
-
-    GroupBundle b =
-        builder()
-            .group(g)
-            .members(member(g, 1), member(g, 2), member(g, 3))
-            .memberAudit(
-                addMember(g, 1, user1, ts), addMember(g, 2, user2, ts), addMember(g, 3, user1, ts))
-            .byId(byId(g, "x"), byId(g, "y"), byId(g, "z"))
-            .byIdAudit(
-                addById(g, "x", user1, ts), addById(g, "y", user2, ts), addById(g, "z", user1, ts))
-            .build();
-
-    rebuilder.rebuild(repo, b, null);
-
-    assertMigratedCleanly(reload(g), b);
-    ImmutableList<CommitInfo> log = log(g);
-    assertThat(log).hasSize(5);
-    assertServerCommit(log.get(0), "Create group");
-    assertCommit(
-        log.get(1),
-        "Update group\n" + "\n" + "Add: Account 1 <1@server-id>\n" + "Add: Account 3 <3@server-id>",
-        "Account 8",
-        "8@server-id");
-    assertCommit(
-        log.get(2),
-        "Update group\n\nAdd-group: Group x <x>\nAdd-group: Group z <z>",
-        "Account 8",
-        "8@server-id");
-    assertCommit(
-        log.get(3), "Update group\n\nAdd: Account 2 <2@server-id>", "Account 9", "9@server-id");
-    assertCommit(log.get(4), "Update group\n\nAdd-group: Group y <y>", "Account 9", "9@server-id");
-  }
-
-  @Test
-  public void fixupCommitPostDatesAllAuditEventsEvenIfAuditEventsAreInTheFuture() throws Exception {
-    AccountGroup g = newGroup("a");
-    IntStream.range(0, 20).forEach(i -> TimeUtil.nowTs());
-    Timestamp future = TimeUtil.nowTs();
-    TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS);
-
-    GroupBundle b =
-        builder()
-            .group(g)
-            .byId(byId(g, "x"), byId(g, "y"), byId(g, "z"))
-            .byIdAudit(addById(g, "x", 8, future))
-            .build();
-
-    rebuilder.rebuild(repo, b, null);
-
-    assertMigratedCleanly(reload(g), b);
-    ImmutableList<CommitInfo> log = log(g);
-    assertThat(log).hasSize(3);
-    assertServerCommit(log.get(0), "Create group");
-    assertCommit(log.get(1), "Update group\n\nAdd-group: Group x <x>", "Account 8", "8@server-id");
-    assertServerCommit(
-        log.get(2), "Update group\n\nAdd-group: Group y <y>\nAdd-group: Group z <z>");
-
-    assertThat(log.stream().map(c -> c.committer.date).collect(toImmutableList()))
-        .named("%s", log)
-        .isOrdered();
-    assertThat(TimeUtil.nowTs()).isLessThan(future);
-  }
-
-  @Test
-  public void redundantMemberAuditsAreIgnored() throws Exception {
-    AccountGroup g = newGroup("a");
-    Timestamp t1 = TimeUtil.nowTs();
-    Timestamp t2 = TimeUtil.nowTs();
-    Timestamp t3 = TimeUtil.nowTs();
-    Timestamp t4 = TimeUtil.nowTs();
-    Timestamp t5 = TimeUtil.nowTs();
-    GroupBundle b =
-        builder()
-            .group(g)
-            .members(member(g, 2))
-            .memberAudit(
-                addMember(g, 1, 8, t1),
-                addMember(g, 1, 8, t1),
-                addMember(g, 1, 8, t3),
-                addMember(g, 1, 9, t4),
-                addAndRemoveMember(g, 1, 8, t2, 9, t5),
-                addAndLegacyRemoveMember(g, 2, 9, t3),
-                addMember(g, 2, 8, t1),
-                addMember(g, 2, 9, t4),
-                addMember(g, 1, 8, t5))
-            .build();
-
-    rebuilder.rebuild(repo, b, null);
-
-    assertMigratedCleanly(reload(g), b);
-    ImmutableList<CommitInfo> log = log(g);
-    assertThat(log).hasSize(5);
-    assertServerCommit(log.get(0), "Create group");
-    assertCommit(
-        log.get(1),
-        "Update group\n\nAdd: Account 1 <1@server-id>\nAdd: Account 2 <2@server-id>",
-        "Account 8",
-        "8@server-id");
-    assertCommit(
-        log.get(2), "Update group\n\nRemove: Account 2 <2@server-id>", "Account 9", "9@server-id");
-    assertCommit(
-        log.get(3), "Update group\n\nAdd: Account 2 <2@server-id>", "Account 9", "9@server-id");
-    assertCommit(
-        log.get(4), "Update group\n\nRemove: Account 1 <1@server-id>", "Account 9", "9@server-id");
-  }
-
-  @Test
-  public void additionsAndRemovalsWithinSameSecondCanBeMigrated() throws Exception {
-    TestTimeUtil.resetWithClockStep(1, TimeUnit.MILLISECONDS);
-    AccountGroup g = newGroup("a");
-    Timestamp t1 = TimeUtil.nowTs();
-    Timestamp t2 = TimeUtil.nowTs();
-    Timestamp t3 = TimeUtil.nowTs();
-    Timestamp t4 = TimeUtil.nowTs();
-    Timestamp t5 = TimeUtil.nowTs();
-    GroupBundle b =
-        builder()
-            .group(g)
-            .members(member(g, 1))
-            .memberAudit(
-                addAndLegacyRemoveMember(g, 1, 8, t1),
-                addMember(g, 1, 10, t2),
-                addAndRemoveMember(g, 1, 8, t3, 9, t4),
-                addMember(g, 1, 8, t5))
-            .build();
-
-    rebuilder.rebuild(repo, b, null);
-
-    assertMigratedCleanly(reload(g), b);
-    ImmutableList<CommitInfo> log = log(g);
-    assertThat(log).hasSize(6);
-    assertServerCommit(log.get(0), "Create group");
-    assertCommit(
-        log.get(1), "Update group\n\nAdd: Account 1 <1@server-id>", "Account 8", "8@server-id");
-    assertCommit(
-        log.get(2), "Update group\n\nRemove: Account 1 <1@server-id>", "Account 8", "8@server-id");
-    assertCommit(
-        log.get(3), "Update group\n\nAdd: Account 1 <1@server-id>", "Account 10", "10@server-id");
-    assertCommit(
-        log.get(4), "Update group\n\nRemove: Account 1 <1@server-id>", "Account 9", "9@server-id");
-    assertCommit(
-        log.get(5), "Update group\n\nAdd: Account 1 <1@server-id>", "Account 8", "8@server-id");
-  }
-
-  @Test
-  public void redundantByIdAuditsAreIgnored() throws Exception {
-    AccountGroup g = newGroup("a");
-    Timestamp t1 = TimeUtil.nowTs();
-    Timestamp t2 = TimeUtil.nowTs();
-    Timestamp t3 = TimeUtil.nowTs();
-    Timestamp t4 = TimeUtil.nowTs();
-    Timestamp t5 = TimeUtil.nowTs();
-    GroupBundle b =
-        builder()
-            .group(g)
-            .byId()
-            .byIdAudit(
-                addById(g, "x", 8, t1),
-                addById(g, "x", 8, t3),
-                addById(g, "x", 9, t4),
-                addAndRemoveById(g, "x", 8, t2, 9, t5))
-            .build();
-
-    rebuilder.rebuild(repo, b, null);
-
-    assertMigratedCleanly(reload(g), b);
-    ImmutableList<CommitInfo> log = log(g);
-    assertThat(log).hasSize(3);
-    assertServerCommit(log.get(0), "Create group");
-    assertCommit(log.get(1), "Update group\n\nAdd-group: Group x <x>", "Account 8", "8@server-id");
-    assertCommit(
-        log.get(2), "Update group\n\nRemove-group: Group x <x>", "Account 9", "9@server-id");
-  }
-
-  @Test
-  public void combineWithBatchGroupNameNotes() throws Exception {
-    AccountGroup g1 = newGroup("a");
-    AccountGroup g2 = newGroup("b");
-    GroupReference gr1 = new GroupReference(g1.getGroupUUID(), g1.getName());
-    GroupReference gr2 = new GroupReference(g2.getGroupUUID(), g2.getName());
-
-    GroupBundle b1 = builder().group(g1).build();
-    GroupBundle b2 = builder().group(g2).build();
-
-    BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
-
-    rebuilder.rebuild(repo, b1, bru);
-    rebuilder.rebuild(repo, b2, bru);
-    try (ObjectInserter inserter = repo.newObjectInserter()) {
-      ImmutableList<GroupReference> refs = ImmutableList.of(gr1, gr2);
-      GroupNameNotes.updateAllGroups(repo, inserter, bru, refs, newPersonIdent());
-      inserter.flush();
-    }
-
-    assertThat(log(g1)).isEmpty();
-    assertThat(log(g2)).isEmpty();
-    assertThat(logGroupNames()).isEmpty();
-
-    RefUpdateUtil.executeChecked(bru, repo);
-
-    assertThat(log(g1)).hasSize(1);
-    assertThat(log(g2)).hasSize(1);
-    assertThat(logGroupNames()).hasSize(1);
-    assertMigratedCleanly(reload(g1), b1);
-    assertMigratedCleanly(reload(g2), b2);
-
-    assertThat(GroupNameNotes.loadAllGroups(repo)).containsExactly(gr1, gr2);
-  }
-
-  @Test
-  public void groupNamesWithLeadingAndTrailingWhitespace() throws Exception {
-    for (String leading : ImmutableList.of("", " ", "  ")) {
-      for (String trailing : ImmutableList.of("", " ", "  ")) {
-        AccountGroup g = newGroup(leading + "a" + trailing);
-        GroupBundle b = builder().group(g).build();
-        rebuilder.rebuild(repo, b, null);
-        assertMigratedCleanly(reload(g), b);
-      }
-    }
-  }
-
-  @Test
-  public void disallowExisting() throws Exception {
-    AccountGroup g = newGroup("a");
-    GroupBundle b = builder().group(g).build();
-
-    rebuilder.rebuild(repo, b, null);
-    assertMigratedCleanly(reload(g), b);
-    String refName = RefNames.refsGroups(g.getGroupUUID());
-    ObjectId oldId = repo.exactRef(refName).getObjectId();
-
-    try {
-      rebuilder.rebuild(repo, b, null);
-      assert_().fail("expected OrmDuplicateKeyException");
-    } catch (OrmDuplicateKeyException e) {
-      // Expected.
-    }
-
-    assertThat(repo.exactRef(refName).getObjectId()).isEqualTo(oldId);
-  }
-
-  private GroupBundle reload(AccountGroup g) throws Exception {
-    return bundleFactory.fromNoteDb(allUsersName, repo, g.getGroupUUID());
-  }
-
-  private void assertMigratedCleanly(GroupBundle noteDbBundle, GroupBundle expectedReviewDbBundle) {
-    assertThat(GroupBundle.compareWithAudits(expectedReviewDbBundle, noteDbBundle)).isEmpty();
-  }
-
-  private AccountGroup newGroup(String name) {
-    int id = idCounter.incrementAndGet();
-    return new AccountGroup(
-        new AccountGroup.NameKey(name),
-        new AccountGroup.Id(id),
-        new AccountGroup.UUID(name.trim() + "-" + id),
-        TimeUtil.nowTs());
-  }
-
-  private AccountGroupMember member(AccountGroup g, int accountId) {
-    return new AccountGroupMember(new AccountGroupMember.Key(new Account.Id(accountId), g.getId()));
-  }
-
-  private AccountGroupMemberAudit addMember(
-      AccountGroup g, int accountId, int adder, Timestamp addedOn) {
-    return new AccountGroupMemberAudit(member(g, accountId), new Account.Id(adder), addedOn);
-  }
-
-  private AccountGroupMemberAudit addAndLegacyRemoveMember(
-      AccountGroup g, int accountId, int adder, Timestamp addedOn) {
-    AccountGroupMemberAudit a = addMember(g, accountId, adder, addedOn);
-    a.removedLegacy();
-    return a;
-  }
-
-  private AccountGroupMemberAudit addAndRemoveMember(
-      AccountGroup g,
-      int accountId,
-      int adder,
-      Timestamp addedOn,
-      int removedBy,
-      Timestamp removedOn) {
-    AccountGroupMemberAudit a = addMember(g, accountId, adder, addedOn);
-    a.removed(new Account.Id(removedBy), removedOn);
-    return a;
-  }
-
-  private AccountGroupByIdAud addById(
-      AccountGroup g, String subgroupUuid, int adder, Timestamp addedOn) {
-    return new AccountGroupByIdAud(byId(g, subgroupUuid), new Account.Id(adder), addedOn);
-  }
-
-  private AccountGroupByIdAud addAndRemoveById(
-      AccountGroup g,
-      String subgroupUuid,
-      int adder,
-      Timestamp addedOn,
-      int removedBy,
-      Timestamp removedOn) {
-    AccountGroupByIdAud a = addById(g, subgroupUuid, adder, addedOn);
-    a.removed(new Account.Id(removedBy), removedOn);
-    return a;
-  }
-
-  private AccountGroupById byId(AccountGroup g, String subgroupUuid) {
-    return new AccountGroupById(
-        new AccountGroupById.Key(g.getId(), new AccountGroup.UUID(subgroupUuid)));
-  }
-
-  private ImmutableList<CommitInfo> log(AccountGroup g) throws Exception {
-    return GitTestUtil.log(repo, RefNames.refsGroups(g.getGroupUUID()));
-  }
-
-  private ImmutableList<CommitInfo> logGroupNames() throws Exception {
-    return GitTestUtil.log(repo, REFS_GROUPNAMES);
-  }
-
-  private static GroupBundle.Builder builder() {
-    return GroupBundle.builder().source(GroupBundle.Source.REVIEW_DB);
-  }
-
-  private static PersonIdent newPersonIdent() {
-    return new PersonIdent(SERVER_NAME, SERVER_EMAIL, TimeUtil.nowTs(), TZ);
-  }
-
-  private static void assertServerCommit(CommitInfo commitInfo, String expectedMessage) {
-    assertCommit(commitInfo, expectedMessage, SERVER_NAME, SERVER_EMAIL);
-  }
-
-  private static void assertCommit(
-      CommitInfo commitInfo, String expectedMessage, String expectedName, String expectedEmail) {
-    assertThat(commitInfo).message().isEqualTo(expectedMessage);
-    assertThat(commitInfo).author().name().isEqualTo(expectedName);
-    assertThat(commitInfo).author().email().isEqualTo(expectedEmail);
-
-    // Committer should always be the server, regardless of author.
-    assertThat(commitInfo).committer().name().isEqualTo(SERVER_NAME);
-    assertThat(commitInfo).committer().email().isEqualTo(SERVER_EMAIL);
-    assertThat(commitInfo).committer().date().isEqualTo(commitInfo.author.date);
-    assertThat(commitInfo).committer().tz().isEqualTo(commitInfo.author.tz);
-  }
-
-  private static AuditLogFormatter getAuditLogFormatter() {
-    return AuditLogFormatter.create(
-        GroupRebuilderTest::getAccount, GroupRebuilderTest::getGroup, SERVER_ID);
-  }
-
-  private static Optional<Account> getAccount(Account.Id id) {
-    Account account = new Account(id, TimeUtil.nowTs());
-    account.setFullName("Account " + id);
-    return Optional.of(account);
-  }
-
-  private static Optional<GroupDescription.Basic> getGroup(AccountGroup.UUID uuid) {
-    GroupDescription.Basic group =
-        new GroupDescription.Basic() {
-          @Override
-          public AccountGroup.UUID getGroupUUID() {
-            return uuid;
-          }
-
-          @Override
-          public String getName() {
-            return "Group " + uuid;
-          }
-
-          @Nullable
-          @Override
-          public String getEmailAddress() {
-            return null;
-          }
-
-          @Nullable
-          @Override
-          public String getUrl() {
-            return null;
-          }
-        };
-    return Optional.of(group);
-  }
-}
diff --git a/javatests/com/google/gerrit/server/schema/HANATest.java b/javatests/com/google/gerrit/server/schema/HANATest.java
deleted file mode 100644
index ac58134..0000000
--- a/javatests/com/google/gerrit/server/schema/HANATest.java
+++ /dev/null
@@ -1,44 +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
-
-package com.google.gerrit.server.schema;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import org.eclipse.jgit.lib.Config;
-import org.junit.Before;
-import org.junit.Test;
-
-public class HANATest {
-
-  private HANA hana;
-  private Config config;
-
-  @Before
-  public void setup() {
-    config = new Config();
-    config.setString("database", null, "hostname", "my.host");
-    hana = new HANA(config);
-  }
-
-  @Test
-  public void getUrl() throws Exception {
-    config.setString("database", null, "port", "4242");
-    assertThat(hana.getUrl()).isEqualTo("jdbc:sap://my.host:4242");
-  }
-
-  @Test
-  public void getIndexScript() throws Exception {
-    assertThat(hana.getIndexScript()).isSameAs(ScriptRunner.NOOP);
-  }
-}
diff --git a/javatests/com/google/gerrit/server/schema/NoteDbSchemaUpdaterTest.java b/javatests/com/google/gerrit/server/schema/NoteDbSchemaUpdaterTest.java
new file mode 100644
index 0000000..5c1b201
--- /dev/null
+++ b/javatests/com/google/gerrit/server/schema/NoteDbSchemaUpdaterTest.java
@@ -0,0 +1,297 @@
+// 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.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 com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSortedMap;
+import com.google.common.collect.ImmutableSortedSet;
+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;
+import com.google.gerrit.server.git.GitRepositoryManager;
+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;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Test;
+
+public class NoteDbSchemaUpdaterTest extends GerritBaseTests {
+  @Test
+  public void requiredUpgradesFromNoVersion() throws Exception {
+    assertThat(requiredUpgrades(0, versions(10))).containsExactly(10).inOrder();
+    assertThat(requiredUpgrades(0, versions(10, 11, 12))).containsExactly(10, 11, 12).inOrder();
+  }
+
+  @Test
+  public void requiredUpgradesFromExistingVersion() throws Exception {
+    ImmutableSortedSet<Integer> versions = versions(10, 11, 12, 13);
+    assertThat(requiredUpgrades(10, versions)).containsExactly(11, 12, 13).inOrder();
+    assertThat(requiredUpgrades(11, versions)).containsExactly(12, 13).inOrder();
+    assertThat(requiredUpgrades(12, versions)).containsExactly(13).inOrder();
+    assertThat(requiredUpgrades(13, versions)).isEmpty();
+  }
+
+  @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");
+    }
+  }
+
+  @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");
+    }
+  }
+
+  private static class TestUpdate {
+    protected final Config cfg;
+    protected final AllProjectsName allProjectsName;
+    protected final AllUsersName allUsersName;
+    protected final NoteDbSchemaUpdater updater;
+    protected final GitRepositoryManager repoManager;
+    protected final NoteDbSchemaVersion.Arguments args;
+    private final List<String> messages;
+
+    TestUpdate(Optional<Integer> initialVersion) {
+      cfg = new Config();
+      allProjectsName = new AllProjectsName("The-Projects");
+      allUsersName = new AllUsersName("The-Users");
+      repoManager = new InMemoryRepositoryManager();
+
+      args = new NoteDbSchemaVersion.Arguments(repoManager, allProjectsName, allUsersName);
+      NoteDbSchemaVersionManager versionManager =
+          new NoteDbSchemaVersionManager(allProjectsName, repoManager);
+      updater =
+          new NoteDbSchemaUpdater(
+              cfg,
+              allUsersName,
+              repoManager,
+              new TestSchemaCreator(initialVersion),
+              versionManager,
+              args,
+              ImmutableSortedMap.of(10, TestSchema_10.class, 11, TestSchema_11.class));
+      messages = new ArrayList<>();
+    }
+
+    private class TestSchemaCreator implements SchemaCreator {
+      private final Optional<Integer> initialVersion;
+
+      TestSchemaCreator(Optional<Integer> initialVersion) {
+        this.initialVersion = initialVersion;
+      }
+
+      @Override
+      public void create() throws IOException {
+        try (Repository repo = repoManager.createRepository(allProjectsName);
+            TestRepository<Repository> tr = new TestRepository<>(repo)) {
+          if (initialVersion.isPresent()) {
+            tr.update(RefNames.REFS_VERSION, tr.blob(initialVersion.get().toString()));
+          }
+        } catch (Exception e) {
+          throw new StorageException(e);
+        }
+        repoManager.createRepository(allUsersName).close();
+        setUp();
+      }
+
+      @Override
+      public void ensureCreated() throws IOException {
+        try {
+          repoManager.openRepository(allProjectsName).close();
+        } catch (RepositoryNotFoundException e) {
+          create();
+        }
+      }
+    }
+
+    protected void setNotesMigrationConfig() {
+      cfg.setString("noteDb", "changes", "write", "true");
+      cfg.setString("noteDb", "changes", "read", "true");
+      cfg.setString("noteDb", "changes", "primaryStorage", "NOTE_DB");
+      cfg.setString("noteDb", "changes", "disableReviewDb", "true");
+    }
+
+    protected void seedGroupSequenceRef() {
+      new RepoSequence(
+              repoManager,
+              GitReferenceUpdated.DISABLED,
+              allUsersName,
+              Sequences.NAME_GROUPS,
+              () -> 1,
+              1)
+          .next();
+    }
+
+    /** Test-specific setup. */
+    protected void setUp() {}
+
+    ImmutableList<String> update() throws Exception {
+      updater.update(
+          new TestUpdateUI() {
+            @Override
+            public void message(String m) {
+              messages.add(m);
+            }
+          });
+      return getMessages();
+    }
+
+    ImmutableList<String> getMessages() {
+      return ImmutableList.copyOf(messages);
+    }
+
+    Optional<Integer> readVersion() throws Exception {
+      try (Repository repo = repoManager.openRepository(allProjectsName)) {
+        return IntBlob.parse(repo, RefNames.REFS_VERSION).map(IntBlob::value);
+      }
+    }
+
+    static class TestSchema_10 implements NoteDbSchemaVersion {
+      @Override
+      public void upgrade(Arguments args, UpdateUI ui) {
+        ui.message("body of 10");
+      }
+    }
+
+    static class TestSchema_11 implements NoteDbSchemaVersion {
+      @Override
+      public void upgrade(Arguments args, UpdateUI ui) {
+        ui.message("BODY OF 11");
+      }
+    }
+  }
+
+  @Test
+  public void bootstrapUpdateWith216Prerequisites() throws Exception {
+    TestUpdate u =
+        new TestUpdate(Optional.empty()) {
+          @Override
+          public void setUp() {
+            setNotesMigrationConfig();
+            seedGroupSequenceRef();
+          }
+        };
+    assertThat(u.update())
+        .containsExactly(
+            "Migrating data to schema 10 ...",
+            "body of 10",
+            "Migrating data to schema 11 ...",
+            "BODY OF 11")
+        .inOrder();
+    assertThat(u.readVersion()).hasValue(11);
+  }
+
+  @Test
+  public void bootstrapUpdateFailsWithoutNotesMigrationConfig() throws Exception {
+    TestUpdate u =
+        new TestUpdate(Optional.empty()) {
+          @Override
+          public void setUp() {
+            seedGroupSequenceRef();
+          }
+        };
+    try {
+      u.update();
+      assert_().fail("expected StorageException");
+    } catch (StorageException e) {
+      assertThat(e).hasMessageThat().contains("NoteDb change migration was not completed");
+    }
+    assertThat(u.getMessages()).isEmpty();
+    assertThat(u.readVersion()).isEmpty();
+  }
+
+  @Test
+  public void bootstrapUpdateFailsWithoutGroupSequenceRef() throws Exception {
+    TestUpdate u =
+        new TestUpdate(Optional.empty()) {
+          @Override
+          public void setUp() {
+            setNotesMigrationConfig();
+          }
+        };
+    try {
+      u.update();
+      assert_().fail("expected StorageException");
+    } catch (StorageException e) {
+      assertThat(e).hasMessageThat().contains("upgrade to 2.16.x first");
+    }
+    assertThat(u.getMessages()).isEmpty();
+    assertThat(u.readVersion()).isEmpty();
+  }
+
+  @Test
+  public void updateTwoVersions() throws Exception {
+    TestUpdate u = new TestUpdate(Optional.of(9));
+    assertThat(u.update())
+        .containsExactly(
+            "Migrating data to schema 10 ...",
+            "body of 10",
+            "Migrating data to schema 11 ...",
+            "BODY OF 11")
+        .inOrder();
+    assertThat(u.readVersion()).hasValue(11);
+  }
+
+  @Test
+  public void updateOneVersion() throws Exception {
+    TestUpdate u = new TestUpdate(Optional.of(10));
+    assertThat(u.update())
+        .containsExactly("Migrating data to schema 11 ...", "BODY OF 11")
+        .inOrder();
+    assertThat(u.readVersion()).hasValue(11);
+  }
+
+  @Test
+  public void updateNoOp() throws Exception {
+    // This test covers the state when running the updater after initializing a new 3.x site, which
+    // seeds the schema version ref with the latest version.
+    TestUpdate u = new TestUpdate(Optional.of(11));
+    assertThat(u.update()).isEmpty();
+    assertThat(u.readVersion()).hasValue(11);
+  }
+
+  private static ImmutableSortedSet<Integer> versions(Integer... versions) {
+    return ImmutableSortedSet.copyOf(versions);
+  }
+}
diff --git a/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionManagerTest.java b/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionManagerTest.java
new file mode 100644
index 0000000..9c62d7f
--- /dev/null
+++ b/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionManagerTest.java
@@ -0,0 +1,93 @@
+// 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.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 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 {
+  private NoteDbSchemaVersionManager manager;
+  private TestRepository<?> tr;
+
+  @Before
+  public void setUp() throws Exception {
+    AllProjectsName allProjectsName = new AllProjectsName("The-Projects");
+    GitRepositoryManager repoManager = new InMemoryRepositoryManager();
+    tr = new TestRepository<>(repoManager.createRepository(allProjectsName));
+    manager = new NoteDbSchemaVersionManager(allProjectsName, repoManager);
+  }
+
+  @Test
+  public void readMissing() throws Exception {
+    assertThat(manager.read()).isEqualTo(0);
+  }
+
+  @Test
+  public void read() throws Exception {
+    tr.update(REFS_VERSION, tr.blob("123"));
+    assertThat(manager.read()).isEqualTo(123);
+  }
+
+  @Test
+  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());
+    }
+  }
+
+  @Test
+  public void incrementFromMissing() throws Exception {
+    manager.increment(123);
+    assertThat(manager.read()).isEqualTo(124);
+  }
+
+  @Test
+  public void increment() throws Exception {
+    tr.update(REFS_VERSION, tr.blob("123"));
+    manager.increment(123);
+    assertThat(manager.read()).isEqualTo(124);
+  }
+
+  @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");
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionsTest.java b/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionsTest.java
new file mode 100644
index 0000000..7bc3848
--- /dev/null
+++ b/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionsTest.java
@@ -0,0 +1,74 @@
+// 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.schema;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
+import static com.google.gerrit.server.schema.NoteDbSchemaVersions.guessVersion;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSortedSet;
+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 {
+  @Test
+  public void testGuessVersion() {
+    assertThat(guessVersion(getClass())).isEmpty();
+    assertThat(guessVersion(Schema_180.class)).hasValue(180);
+  }
+
+  @Test
+  public void contiguousVersions() {
+    ImmutableSortedSet<Integer> keys = NoteDbSchemaVersions.ALL.keySet();
+    ImmutableList<Integer> expected =
+        IntStream.rangeClosed(keys.first(), keys.last()).boxed().collect(toImmutableList());
+    assertThat(keys).containsExactlyElementsIn(expected).inOrder();
+  }
+
+  @Test
+  public void exceedsReviewDbVersion() {
+    assertThat(NoteDbSchemaVersions.ALL.firstKey()).isGreaterThan(170);
+  }
+
+  @Test
+  public void containsAllNoteDbSchemas() throws Exception {
+    int minNoteDbVersion = 180;
+    ImmutableList<Integer> allSchemaVersions =
+        ClassPath.from(getClass().getClassLoader())
+            .getTopLevelClasses(getClass().getPackage().getName()).stream()
+            .map(ClassInfo::load)
+            .map(NoteDbSchemaVersions::guessVersion)
+            .flatMap(Streams::stream)
+            .filter(v -> v >= minNoteDbVersion)
+            .sorted()
+            .collect(toImmutableList());
+    assertThat(NoteDbSchemaVersions.ALL.keySet())
+        .containsExactlyElementsIn(allSchemaVersions)
+        .inOrder();
+  }
+
+  @Test
+  public void schemaConstructors() throws Exception {
+    for (int version : NoteDbSchemaVersions.ALL.keySet()) {
+      NoteDbSchemaVersions.get(NoteDbSchemaVersions.ALL, version);
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/server/schema/ProjectConfigSchemaUpdateTest.java b/javatests/com/google/gerrit/server/schema/ProjectConfigSchemaUpdateTest.java
new file mode 100644
index 0000000..c5c956c
--- /dev/null
+++ b/javatests/com/google/gerrit/server/schema/ProjectConfigSchemaUpdateTest.java
@@ -0,0 +1,110 @@
+// 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.schema;
+
+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.server.config.AllProjectsName;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+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.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class ProjectConfigSchemaUpdateTest {
+  private static final String ALL_PROJECTS = "All-The-Projects";
+
+  @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+  private SitePaths sitePaths;
+  private ProjectConfigSchemaUpdate.Factory factory;
+  private File allProjectsRepoFile;
+
+  @Before
+  public void setUp() throws Exception {
+    sitePaths = new SitePaths(temporaryFolder.newFolder().toPath());
+    Files.createDirectories(sitePaths.etc_dir);
+
+    Path gitPath = sitePaths.resolve("git");
+
+    StoredConfig gerritConfig = new FileBasedConfig(sitePaths.gerrit_config.toFile(), FS.DETECTED);
+    gerritConfig.load();
+    gerritConfig.setString("gerrit", null, "basePath", gitPath.toAbsolutePath().toString());
+    gerritConfig.setString("gerrit", null, "allProjects", ALL_PROJECTS);
+    gerritConfig.save();
+
+    Files.createDirectories(sitePaths.resolve("git"));
+    allProjectsRepoFile = gitPath.resolve("All-The-Projects.git").toFile();
+    try (Repository repo = new FileRepository(allProjectsRepoFile)) {
+      repo.create(true);
+    }
+
+    factory = new ProjectConfigSchemaUpdate.Factory(sitePaths, new AllProjectsName(ALL_PROJECTS));
+  }
+
+  @Test
+  public void noBaseConfig() throws Exception {
+    assertThat(getConfig().getString("foo", null, "bar")).isNull();
+
+    try (Repository repo = new FileRepository(allProjectsRepoFile);
+        TestRepository<Repository> tr = new TestRepository<>(repo)) {
+      tr.branch("refs/meta/config").commit().add("project.config", "[foo]\nbar = baz").create();
+    }
+
+    assertThat(getConfig().getString("foo", null, "bar")).isEqualTo("baz");
+  }
+
+  @Test
+  public void baseConfig() throws Exception {
+    assertThat(getConfig().getString("foo", null, "bar")).isNull();
+
+    Path baseConfigPath = sitePaths.etc_dir.resolve(ALL_PROJECTS).resolve("project.config");
+    Files.createDirectories(baseConfigPath.getParent());
+    Files.write(baseConfigPath, ImmutableList.of("[foo]", "bar = base"));
+
+    assertThat(getConfig().getString("foo", null, "bar")).isEqualTo("base");
+
+    try (Repository repo = new FileRepository(allProjectsRepoFile);
+        TestRepository<Repository> tr = new TestRepository<>(repo)) {
+      tr.branch("refs/meta/config").commit().add("project.config", "[foo]\nbar = baz").create();
+    }
+
+    assertThat(getConfig().getString("foo", null, "bar")).isEqualTo("baz");
+  }
+
+  private Config getConfig() throws Exception {
+    try (Repository repo = new FileRepository(allProjectsRepoFile)) {
+      return factory
+          .read(
+              new MetaDataUpdate(
+                  GitReferenceUpdated.DISABLED, new 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
new file mode 100644
index 0000000..35f580c
--- /dev/null
+++ b/javatests/com/google/gerrit/server/schema/SchemaCreatorImplTest.java
@@ -0,0 +1,97 @@
+// 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.server.schema;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.data.LabelFunction;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelTypes;
+import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.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;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Before;
+import org.junit.Test;
+
+public class SchemaCreatorImplTest extends GerritBaseTests {
+  @Inject private AllProjectsName allProjects;
+
+  @Inject private GitRepositoryManager repoManager;
+
+  @Inject private SchemaCreator schemaCreator;
+
+  @Inject private ProjectConfig.Factory projectConfigFactory;
+
+  @Before
+  public void setUp() throws Exception {
+    new InMemoryModule().inject(this);
+    schemaCreator.create();
+  }
+
+  private LabelTypes getLabelTypes() throws Exception {
+    ProjectConfig c = projectConfigFactory.create(allProjects);
+    try (Repository repo = repoManager.openRepository(allProjects)) {
+      c.load(repo);
+      return new LabelTypes(ImmutableList.copyOf(c.getLabelSections().values()));
+    }
+  }
+
+  @Test
+  public void createSchema_LabelTypes() throws Exception {
+    List<String> labels = new ArrayList<>();
+    for (LabelType label : getLabelTypes().getLabelTypes()) {
+      labels.add(label.getName());
+    }
+    assertThat(labels).containsExactly("Code-Review");
+  }
+
+  @Test
+  public void createSchema_Label_CodeReview() throws Exception {
+    LabelType codeReview = getLabelTypes().byLabel("Code-Review");
+    assertThat(codeReview).isNotNull();
+    assertThat(codeReview.getName()).isEqualTo("Code-Review");
+    assertThat(codeReview.getDefaultValue()).isEqualTo(0);
+    assertThat(codeReview.getFunction()).isEqualTo(LabelFunction.MAX_WITH_BLOCK);
+    assertThat(codeReview.isCopyMinScore()).isTrue();
+    assertValueRange(codeReview, -2, -1, 0, 1, 2);
+  }
+
+  private void assertValueRange(LabelType label, Integer... range) {
+    List<Integer> rangeList = Arrays.asList(range);
+    assertThat(rangeList).isNotEmpty();
+    assertThat(rangeList).isStrictlyOrdered();
+
+    assertThat(label.getValues().stream().map(v -> (int) v.getValue()))
+        .containsExactlyElementsIn(rangeList)
+        .inOrder();
+    assertThat(label.getMax().getValue()).isEqualTo(Collections.max(rangeList));
+    assertThat(label.getMin().getValue()).isEqualTo(Collections.min(rangeList));
+    for (LabelValue v : label.getValues()) {
+      assertThat(v.getText()).isNotNull();
+      assertThat(v.getText()).isNotEmpty();
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/server/schema/SchemaCreatorTest.java b/javatests/com/google/gerrit/server/schema/SchemaCreatorTest.java
deleted file mode 100644
index 9569745..0000000
--- a/javatests/com/google/gerrit/server/schema/SchemaCreatorTest.java
+++ /dev/null
@@ -1,128 +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.server.schema;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
-
-import com.google.common.collect.ImmutableList;
-import com.google.gerrit.common.data.LabelFunction;
-import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.LabelTypes;
-import com.google.gerrit.common.data.LabelValue;
-import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gerrit.testing.InMemoryDatabase;
-import com.google.gerrit.testing.InMemoryModule;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import java.io.File;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import org.eclipse.jgit.lib.Repository;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-public class SchemaCreatorTest {
-  @Inject private AllProjectsName allProjects;
-
-  @Inject private GitRepositoryManager repoManager;
-
-  @Inject private InMemoryDatabase db;
-
-  @Before
-  public void setUp() throws Exception {
-    new InMemoryModule().inject(this);
-  }
-
-  @After
-  public void tearDown() throws Exception {
-    InMemoryDatabase.drop(db);
-  }
-
-  @Test
-  public void getCauses_CreateSchema() throws OrmException, SQLException {
-    // Initially the schema should be empty.
-    String[] types = {"TABLE", "VIEW"};
-    try (JdbcSchema d = (JdbcSchema) db.open();
-        ResultSet rs = d.getConnection().getMetaData().getTables(null, null, null, types)) {
-      assertThat(rs.next()).isFalse();
-    }
-
-    // Create the schema using the current schema version.
-    //
-    db.create();
-    db.assertSchemaVersion();
-
-    // By default sitePath is set to the current working directory.
-    //
-    File sitePath = new File(".").getAbsoluteFile();
-    if (sitePath.getName().equals(".")) {
-      sitePath = sitePath.getParentFile();
-    }
-  }
-
-  private LabelTypes getLabelTypes() throws Exception {
-    db.create();
-    ProjectConfig c = new ProjectConfig(allProjects);
-    try (Repository repo = repoManager.openRepository(allProjects)) {
-      c.load(repo);
-      return new LabelTypes(ImmutableList.copyOf(c.getLabelSections().values()));
-    }
-  }
-
-  @Test
-  public void createSchema_LabelTypes() throws Exception {
-    List<String> labels = new ArrayList<>();
-    for (LabelType label : getLabelTypes().getLabelTypes()) {
-      labels.add(label.getName());
-    }
-    assertThat(labels).containsExactly("Code-Review");
-  }
-
-  @Test
-  public void createSchema_Label_CodeReview() throws Exception {
-    LabelType codeReview = getLabelTypes().byLabel("Code-Review");
-    assertThat(codeReview).isNotNull();
-    assertThat(codeReview.getName()).isEqualTo("Code-Review");
-    assertThat(codeReview.getDefaultValue()).isEqualTo(0);
-    assertThat(codeReview.getFunction()).isEqualTo(LabelFunction.MAX_WITH_BLOCK);
-    assertThat(codeReview.isCopyMinScore()).isTrue();
-    assertValueRange(codeReview, -2, -1, 0, 1, 2);
-  }
-
-  private void assertValueRange(LabelType label, Integer... range) {
-    List<Integer> rangeList = Arrays.asList(range);
-    assertThat(rangeList).isNotEmpty();
-    assertThat(rangeList).isStrictlyOrdered();
-
-    assertThat(label.getValues().stream().map(v -> (int) v.getValue()))
-        .containsExactlyElementsIn(rangeList)
-        .inOrder();
-    assertThat(label.getMax().getValue()).isEqualTo(Collections.max(rangeList));
-    assertThat(label.getMin().getValue()).isEqualTo(Collections.min(rangeList));
-    for (LabelValue v : label.getValues()) {
-      assertThat(v.getText()).isNotNull();
-      assertThat(v.getText()).isNotEmpty();
-    }
-  }
-}
diff --git a/javatests/com/google/gerrit/server/schema/SchemaUpdaterTest.java b/javatests/com/google/gerrit/server/schema/SchemaUpdaterTest.java
deleted file mode 100644
index 7ea4d93..0000000
--- a/javatests/com/google/gerrit/server/schema/SchemaUpdaterTest.java
+++ /dev/null
@@ -1,149 +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.server.schema;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.gerrit.extensions.config.FactoryModule;
-import com.google.gerrit.lifecycle.LifecycleManager;
-import com.google.gerrit.metrics.DisabledMetricMaker;
-import com.google.gerrit.metrics.MetricMaker;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.GerritPersonIdentProvider;
-import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.config.AnonymousCowardName;
-import com.google.gerrit.server.config.AnonymousCowardNameProvider;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.config.GerritServerId;
-import com.google.gerrit.server.config.GerritServerIdProvider;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.notedb.NotesMigration;
-import com.google.gerrit.testing.InMemoryDatabase;
-import com.google.gerrit.testing.InMemoryH2Type;
-import com.google.gerrit.testing.InMemoryRepositoryManager;
-import com.google.gerrit.testing.TestUpdateUI;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Guice;
-import com.google.inject.Key;
-import com.google.inject.ProvisionException;
-import com.google.inject.TypeLiteral;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.UUID;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-public class SchemaUpdaterTest {
-  private LifecycleManager lifecycle;
-  private InMemoryDatabase db;
-
-  @Before
-  public void setUp() throws Exception {
-    lifecycle = new LifecycleManager();
-    db = InMemoryDatabase.newDatabase(lifecycle);
-    lifecycle.start();
-  }
-
-  @After
-  public void tearDown() throws Exception {
-    if (lifecycle != null) {
-      lifecycle.stop();
-    }
-    InMemoryDatabase.drop(db);
-  }
-
-  @Test
-  public void update() throws OrmException, FileNotFoundException, IOException {
-    db.create();
-
-    final Path site = Paths.get(UUID.randomUUID().toString());
-    final SitePaths paths = new SitePaths(site);
-    SchemaUpdater u =
-        Guice.createInjector(
-                new FactoryModule() {
-                  @Override
-                  protected void configure() {
-                    TypeLiteral<SchemaFactory<ReviewDb>> schemaFactory =
-                        new TypeLiteral<SchemaFactory<ReviewDb>>() {};
-                    bind(schemaFactory).to(NotesMigrationSchemaFactory.class);
-                    bind(Key.get(schemaFactory, ReviewDbFactory.class)).toInstance(db);
-                    bind(SitePaths.class).toInstance(paths);
-
-                    Config cfg = new Config();
-                    cfg.setString("user", null, "name", "Gerrit Code Review");
-                    cfg.setString("user", null, "email", "gerrit@localhost");
-                    cfg.setString(
-                        GerritServerIdProvider.SECTION,
-                        null,
-                        GerritServerIdProvider.KEY,
-                        "1234567");
-                    bind(Config.class) //
-                        .annotatedWith(GerritServerConfig.class) //
-                        .toInstance(cfg);
-
-                    bind(PersonIdent.class) //
-                        .annotatedWith(GerritPersonIdent.class) //
-                        .toProvider(GerritPersonIdentProvider.class);
-
-                    bind(String.class).annotatedWith(GerritServerId.class).toInstance("gerrit");
-
-                    bind(AllProjectsName.class).toInstance(new AllProjectsName("All-Projects"));
-                    bind(AllUsersName.class).toInstance(new AllUsersName("All-Users"));
-
-                    bind(GitRepositoryManager.class).toInstance(new InMemoryRepositoryManager());
-
-                    bind(String.class) //
-                        .annotatedWith(AnonymousCowardName.class) //
-                        .toProvider(AnonymousCowardNameProvider.class);
-
-                    bind(DataSourceType.class).to(InMemoryH2Type.class);
-
-                    bind(SystemGroupBackend.class);
-                    install(new NotesMigration.Module());
-                    bind(MetricMaker.class).to(DisabledMetricMaker.class);
-                  }
-                })
-            .getInstance(SchemaUpdater.class);
-
-    for (SchemaVersion s = u.getLatestSchemaVersion(); s.getVersionNbr() > 1; s = s.getPrior()) {
-      try {
-        assertThat(s.getPrior().getVersionNbr())
-            .named(
-                "schema %s has prior version %s. Not true that",
-                s.getVersionNbr(), s.getPrior().getVersionNbr())
-            .isEqualTo(s.getVersionNbr() - 1);
-      } catch (ProvisionException e) {
-        // Ignored
-        // The oldest supported schema version doesn't have a prior schema
-        // version.
-        break;
-      }
-    }
-
-    u.update(new TestUpdateUI());
-
-    db.assertSchemaVersion();
-  }
-}
diff --git a/javatests/com/google/gerrit/server/schema/Schema_150_to_151_Test.java b/javatests/com/google/gerrit/server/schema/Schema_150_to_151_Test.java
deleted file mode 100644
index 4d5db6d..0000000
--- a/javatests/com/google/gerrit/server/schema/Schema_150_to_151_Test.java
+++ /dev/null
@@ -1,253 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.TruthJUnit.assume;
-
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroup.Id;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.reviewdb.server.ReviewDbWrapper;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.Sequences;
-import com.google.gerrit.server.account.GroupUUID;
-import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gerrit.testing.InMemoryTestEnvironment;
-import com.google.gerrit.testing.TestUpdateUI;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.sql.Connection;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
-import java.sql.Statement;
-import java.sql.Timestamp;
-import java.time.Instant;
-import java.time.LocalDateTime;
-import java.time.Month;
-import java.time.ZoneOffset;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-
-public class Schema_150_to_151_Test {
-
-  @Rule public InMemoryTestEnvironment testEnv = new InMemoryTestEnvironment();
-
-  @Inject private Schema_151 schema151;
-  @Inject private ReviewDb db;
-  @Inject private IdentifiedUser currentUser;
-  @Inject private @GerritPersonIdent Provider<PersonIdent> serverIdent;
-  @Inject private Sequences seq;
-
-  private Connection connection;
-  private PreparedStatement createdOnRetrieval;
-  private PreparedStatement createdOnUpdate;
-  private PreparedStatement auditEntryDeletion;
-  private JdbcSchema jdbcSchema;
-
-  @Before
-  public void unwrapDb() {
-    jdbcSchema = ReviewDbWrapper.unwrapJbdcSchema(db);
-  }
-
-  @Before
-  public void setUp() throws Exception {
-    assume().that(db instanceof JdbcSchema).isTrue();
-
-    connection = ((JdbcSchema) db).getConnection();
-
-    try (Statement stmt = connection.createStatement()) {
-      stmt.execute(
-          "CREATE TABLE account_groups ("
-              + " group_uuid varchar(255) DEFAULT '' NOT NULL,"
-              + " group_id INTEGER DEFAULT 0 NOT NULL,"
-              + " name varchar(255) DEFAULT '' NOT NULL,"
-              + " created_on TIMESTAMP,"
-              + " description CLOB,"
-              + " owner_group_uuid varchar(255) DEFAULT '' NOT NULL,"
-              + " visible_to_all CHAR(1) DEFAULT 'N' NOT NULL"
-              + ")");
-
-      stmt.execute(
-          "CREATE TABLE account_group_members ("
-              + " group_id INTEGER DEFAULT 0 NOT NULL,"
-              + " account_id INTEGER DEFAULT 0 NOT NULL"
-              + ")");
-
-      stmt.execute(
-          "CREATE TABLE account_group_members_audit ("
-              + " group_id INTEGER DEFAULT 0 NOT NULL,"
-              + " account_id INTEGER DEFAULT 0 NOT NULL,"
-              + " added_by INTEGER DEFAULT 0 NOT NULL,"
-              + " added_on TIMESTAMP,"
-              + " removed_by INTEGER,"
-              + " removed_on TIMESTAMP"
-              + ")");
-    }
-
-    createdOnRetrieval =
-        connection.prepareStatement("SELECT created_on FROM account_groups WHERE group_id = ?");
-    createdOnUpdate =
-        connection.prepareStatement("UPDATE account_groups SET created_on = ? WHERE group_id = ?");
-    auditEntryDeletion =
-        connection.prepareStatement("DELETE FROM account_group_members_audit WHERE group_id = ?");
-  }
-
-  @After
-  public void tearDown() throws Exception {
-    if (auditEntryDeletion != null) {
-      auditEntryDeletion.close();
-    }
-    if (createdOnUpdate != null) {
-      createdOnUpdate.close();
-    }
-    if (createdOnRetrieval != null) {
-      createdOnRetrieval.close();
-    }
-    if (connection != null) {
-      connection.close();
-    }
-  }
-
-  @Test
-  public void createdOnIsPopulatedForGroupsCreatedAfterAudit() throws Exception {
-    Timestamp testStartTime = TimeUtil.nowTs();
-    AccountGroup.Id groupId = createGroupInReviewDb("Group for schema migration");
-    setCreatedOnToVeryOldTimestamp(groupId);
-
-    schema151.migrateData(db, new TestUpdateUI());
-
-    Timestamp createdOn = getCreatedOn(groupId);
-    assertThat(createdOn).isAtLeast(testStartTime);
-  }
-
-  @Test
-  public void createdOnIsPopulatedForGroupsCreatedBeforeAudit() throws Exception {
-    AccountGroup.Id groupId = createGroupInReviewDb("Ancient group for schema migration");
-    setCreatedOnToVeryOldTimestamp(groupId);
-    removeAuditEntriesFor(groupId);
-
-    schema151.migrateData(db, new TestUpdateUI());
-
-    Timestamp createdOn = getCreatedOn(groupId);
-    assertThat(createdOn).isEqualTo(AccountGroup.auditCreationInstantTs());
-  }
-
-  private AccountGroup.Id createGroupInReviewDb(String name) throws Exception {
-    AccountGroup group =
-        new AccountGroup(
-            new AccountGroup.NameKey(name),
-            new AccountGroup.Id(seq.nextGroupId()),
-            GroupUUID.make(name, serverIdent.get()),
-            TimeUtil.nowTs());
-    storeInReviewDb(group);
-    addMembersInReviewDb(group.getId(), currentUser.getAccountId());
-    return group.getId();
-  }
-
-  private Timestamp getCreatedOn(Id groupId) throws Exception {
-    createdOnRetrieval.setInt(1, groupId.get());
-    try (ResultSet results = createdOnRetrieval.executeQuery()) {
-      if (results.first()) {
-        return results.getTimestamp(1);
-      }
-    }
-    return null;
-  }
-
-  private void setCreatedOnToVeryOldTimestamp(Id groupId) throws Exception {
-    createdOnUpdate.setInt(1, groupId.get());
-    Instant instant = LocalDateTime.of(1800, Month.JANUARY, 1, 0, 0).toInstant(ZoneOffset.UTC);
-    createdOnUpdate.setTimestamp(1, Timestamp.from(instant));
-    createdOnUpdate.setInt(2, groupId.get());
-    createdOnUpdate.executeUpdate();
-  }
-
-  private void removeAuditEntriesFor(AccountGroup.Id groupId) throws Exception {
-    auditEntryDeletion.setInt(1, groupId.get());
-    auditEntryDeletion.executeUpdate();
-  }
-
-  private void storeInReviewDb(AccountGroup... groups) throws Exception {
-    try (PreparedStatement stmt =
-        jdbcSchema
-            .getConnection()
-            .prepareStatement(
-                "INSERT INTO account_groups"
-                    + " (group_uuid,"
-                    + " group_id,"
-                    + " name,"
-                    + " description,"
-                    + " created_on,"
-                    + " owner_group_uuid,"
-                    + " visible_to_all) VALUES (?, ?, ?, ?, ?, ?, ?)")) {
-      for (AccountGroup group : groups) {
-        stmt.setString(1, group.getGroupUUID().get());
-        stmt.setInt(2, group.getId().get());
-        stmt.setString(3, group.getName());
-        stmt.setString(4, group.getDescription());
-        stmt.setTimestamp(5, group.getCreatedOn());
-        stmt.setString(6, group.getOwnerGroupUUID().get());
-        stmt.setString(7, group.isVisibleToAll() ? "Y" : "N");
-        stmt.addBatch();
-      }
-      stmt.executeBatch();
-    }
-  }
-
-  private void addMembersInReviewDb(AccountGroup.Id groupId, Account.Id... memberIds)
-      throws Exception {
-    try (PreparedStatement addMemberStmt =
-            jdbcSchema
-                .getConnection()
-                .prepareStatement(
-                    "INSERT INTO account_group_members"
-                        + " (group_id,"
-                        + " account_id) VALUES ("
-                        + groupId.get()
-                        + ", ?)");
-        PreparedStatement addMemberAuditStmt =
-            jdbcSchema
-                .getConnection()
-                .prepareStatement(
-                    "INSERT INTO account_group_members_audit"
-                        + " (group_id,"
-                        + " account_id,"
-                        + " added_by,"
-                        + " added_on) VALUES ("
-                        + groupId.get()
-                        + ", ?, "
-                        + currentUser.getAccountId().get()
-                        + ", ?)")) {
-      Timestamp addedOn = TimeUtil.nowTs();
-      for (Account.Id memberId : memberIds) {
-        addMemberStmt.setInt(1, memberId.get());
-        addMemberStmt.addBatch();
-
-        addMemberAuditStmt.setInt(1, memberId.get());
-        addMemberAuditStmt.setTimestamp(2, addedOn);
-        addMemberAuditStmt.addBatch();
-      }
-      addMemberStmt.executeBatch();
-      addMemberAuditStmt.executeBatch();
-    }
-  }
-}
diff --git a/javatests/com/google/gerrit/server/schema/Schema_159_to_160_Test.java b/javatests/com/google/gerrit/server/schema/Schema_159_to_160_Test.java
deleted file mode 100644
index 9bd2084..0000000
--- a/javatests/com/google/gerrit/server/schema/Schema_159_to_160_Test.java
+++ /dev/null
@@ -1,222 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import static com.google.common.base.MoreObjects.firstNonNull;
-import static com.google.common.collect.ImmutableMap.toImmutableMap;
-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_USERS_DEFAULT;
-import static com.google.gerrit.server.git.UserConfigSections.KEY_URL;
-import static com.google.gerrit.server.git.UserConfigSections.MY;
-import static com.google.gerrit.server.schema.Schema_160.DEFAULT_DRAFT_ITEMS;
-import static com.google.gerrit.server.schema.VersionedAccountPreferences.PREFERENCES;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
-import com.google.gerrit.extensions.api.GerritApi;
-import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
-import com.google.gerrit.extensions.client.MenuItem;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.index.account.AccountIndexer;
-import com.google.gerrit.testing.InMemoryTestEnvironment;
-import com.google.gerrit.testing.TestUpdateUI;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.util.List;
-import java.util.Optional;
-import java.util.function.Supplier;
-import org.eclipse.jgit.junit.TestRepository;
-import org.eclipse.jgit.lib.BlobBasedConfig;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-
-public class Schema_159_to_160_Test {
-  @Rule public InMemoryTestEnvironment testEnv = new InMemoryTestEnvironment();
-
-  @Inject private AccountCache accountCache;
-  @Inject private AccountIndexer accountIndexer;
-  @Inject private AllUsersName allUsersName;
-  @Inject private GerritApi gApi;
-  @Inject private GitRepositoryManager repoManager;
-  @Inject private Provider<IdentifiedUser> userProvider;
-  @Inject private ReviewDb db;
-  @Inject private Schema_160 schema160;
-
-  private Account.Id accountId;
-
-  @Before
-  public void setUp() throws Exception {
-    accountId = userProvider.get().getAccountId();
-  }
-
-  @Test
-  public void skipUnmodified() throws Exception {
-    ObjectId oldMetaId = metaRef(accountId);
-    ImmutableSet<String> fromNoteDb = myMenusFromNoteDb(accountId);
-    ImmutableSet<String> fromApi = myMenusFromApi(accountId);
-    for (String item : DEFAULT_DRAFT_ITEMS) {
-      assertThat(fromNoteDb).doesNotContain(item);
-      assertThat(fromApi).doesNotContain(item);
-    }
-
-    schema160.migrateData(db, new TestUpdateUI());
-
-    assertThat(metaRef(accountId)).isEqualTo(oldMetaId);
-  }
-
-  @Test
-  public void deleteItems() throws Exception {
-    ObjectId oldMetaId = metaRef(accountId);
-    ImmutableSet<String> defaultNames = myMenusFromApi(accountId);
-
-    GeneralPreferencesInfo prefs = gApi.accounts().id(accountId.get()).getPreferences();
-    prefs.my.add(0, new MenuItem("Something else", DEFAULT_DRAFT_ITEMS.get(0) + "+is:mergeable"));
-    for (int i = 0; i < DEFAULT_DRAFT_ITEMS.size(); i++) {
-      prefs.my.add(new MenuItem("Draft entry " + i, DEFAULT_DRAFT_ITEMS.get(i)));
-    }
-    gApi.accounts().id(accountId.get()).setPreferences(prefs);
-
-    List<String> oldNames =
-        ImmutableList.<String>builder()
-            .add("Something else")
-            .addAll(defaultNames)
-            .add("Draft entry 0")
-            .add("Draft entry 1")
-            .add("Draft entry 2")
-            .add("Draft entry 3")
-            .build();
-    assertThat(myMenusFromApi(accountId)).containsExactlyElementsIn(oldNames).inOrder();
-
-    schema160.migrateData(db, new TestUpdateUI());
-    accountCache.evict(accountId);
-    accountIndexer.index(accountId);
-    testEnv.setApiUser(accountId);
-
-    assertThat(metaRef(accountId)).isNotEqualTo(oldMetaId);
-
-    List<String> newNames =
-        ImmutableList.<String>builder().add("Something else").addAll(defaultNames).build();
-    assertThat(myMenusFromNoteDb(accountId)).containsExactlyElementsIn(newNames).inOrder();
-    assertThat(myMenusFromApi(accountId)).containsExactlyElementsIn(newNames).inOrder();
-  }
-
-  @Test
-  public void skipNonExistentRefsUsersDefault() throws Exception {
-    assertThat(readRef(REFS_USERS_DEFAULT)).isEmpty();
-    schema160.migrateData(db, new TestUpdateUI());
-    assertThat(readRef(REFS_USERS_DEFAULT)).isEmpty();
-  }
-
-  @Test
-  public void deleteDefaultItem() throws Exception {
-    assertThat(readRef(REFS_USERS_DEFAULT)).isEmpty();
-    ImmutableSet<String> defaultNames = defaultMenusFromApi();
-
-    // Setting *any* preference causes preferences.config to contain the full set of "my" sections.
-    // This mimics real-world behavior prior to the 2.15 upgrade; see Issue 8439 for details.
-    GeneralPreferencesInfo prefs = gApi.config().server().getDefaultPreferences();
-    prefs.signedOffBy = !firstNonNull(prefs.signedOffBy, false);
-    gApi.config().server().setDefaultPreferences(prefs);
-
-    try (Repository repo = repoManager.openRepository(allUsersName)) {
-      Config cfg = new BlobBasedConfig(null, repo, readRef(REFS_USERS_DEFAULT).get(), PREFERENCES);
-      assertThat(cfg.getSubsections("my")).containsExactlyElementsIn(defaultNames).inOrder();
-
-      // Add more defaults directly in git, the SetPreferences endpoint doesn't respect the "my"
-      // field in the input in 2.15 and earlier.
-      cfg.setString("my", "Drafts", "url", "#/q/owner:self+is:draft");
-      cfg.setString("my", "Something else", "url", "#/q/owner:self+is:draft+is:mergeable");
-      cfg.setString("my", "Totally not drafts", "url", "#/q/owner:self+is:draft");
-      new TestRepository<>(repo)
-          .branch(REFS_USERS_DEFAULT)
-          .commit()
-          .add(PREFERENCES, cfg.toText())
-          .create();
-    }
-
-    List<String> oldNames =
-        ImmutableList.<String>builder()
-            .addAll(defaultNames)
-            .add("Drafts")
-            .add("Something else")
-            .add("Totally not drafts")
-            .build();
-    assertThat(defaultMenusFromApi()).containsExactlyElementsIn(oldNames).inOrder();
-
-    schema160.migrateData(db, new TestUpdateUI());
-
-    assertThat(readRef(REFS_USERS_DEFAULT)).isPresent();
-
-    List<String> newNames =
-        ImmutableList.<String>builder().addAll(defaultNames).add("Something else").build();
-    assertThat(myMenusFromNoteDb(VersionedAccountPreferences::forDefault).keySet())
-        .containsExactlyElementsIn(newNames)
-        .inOrder();
-    assertThat(defaultMenusFromApi()).containsExactlyElementsIn(newNames).inOrder();
-  }
-
-  private ImmutableSet<String> myMenusFromNoteDb(Account.Id id) throws Exception {
-    return myMenusFromNoteDb(() -> VersionedAccountPreferences.forUser(id)).keySet();
-  }
-
-  // Raw config values, bypassing the defaults set by PreferencesConfig.
-  private ImmutableMap<String, String> myMenusFromNoteDb(
-      Supplier<VersionedAccountPreferences> prefsSupplier) throws Exception {
-    try (Repository repo = repoManager.openRepository(allUsersName)) {
-      VersionedAccountPreferences prefs = prefsSupplier.get();
-      prefs.load(allUsersName, repo);
-      Config cfg = prefs.getConfig();
-      return cfg.getSubsections(MY).stream()
-          .collect(toImmutableMap(i -> i, i -> cfg.getString(MY, i, KEY_URL)));
-    }
-  }
-
-  private ImmutableSet<String> myMenusFromApi(Account.Id id) throws Exception {
-    return myMenus(gApi.accounts().id(id.get()).getPreferences()).keySet();
-  }
-
-  private ImmutableSet<String> defaultMenusFromApi() throws Exception {
-    return myMenus(gApi.config().server().getDefaultPreferences()).keySet();
-  }
-
-  private static ImmutableMap<String, String> myMenus(GeneralPreferencesInfo prefs) {
-
-    return prefs.my.stream().collect(toImmutableMap(i -> i.name, i -> i.url));
-  }
-
-  private ObjectId metaRef(Account.Id id) throws Exception {
-    return readRef(RefNames.refsUsers(id))
-        .orElseThrow(() -> new AssertionError("missing ref for account " + id));
-  }
-
-  private Optional<ObjectId> readRef(String ref) throws Exception {
-    try (Repository repo = repoManager.openRepository(allUsersName)) {
-      return Optional.ofNullable(repo.exactRef(ref)).map(Ref::getObjectId);
-    }
-  }
-}
diff --git a/javatests/com/google/gerrit/server/schema/Schema_161_to_162_Test.java b/javatests/com/google/gerrit/server/schema/Schema_161_to_162_Test.java
deleted file mode 100644
index 67d071d..0000000
--- a/javatests/com/google/gerrit/server/schema/Schema_161_to_162_Test.java
+++ /dev/null
@@ -1,96 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.gerrit.extensions.api.GerritApi;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.config.AllProjectsName;
-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.git.meta.MetaDataUpdate;
-import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gerrit.testing.InMemoryTestEnvironment;
-import com.google.gerrit.testing.TestUpdateUI;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import java.io.IOException;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
-import org.junit.Rule;
-import org.junit.Test;
-
-public class Schema_161_to_162_Test {
-  @Rule public InMemoryTestEnvironment testEnv = new InMemoryTestEnvironment();
-
-  @Inject private AllProjectsName allProjectsName;
-  @Inject private AllUsersName allUsersName;
-  @Inject private GerritApi gApi;
-  @Inject private GitRepositoryManager repoManager;
-  @Inject private Schema_162 schema162;
-  @Inject private ReviewDb db;
-  @Inject @GerritPersonIdent private PersonIdent serverUser;
-
-  @Test
-  public void skipCorrectInheritance() throws Exception {
-    assertThatAllUsersInheritsFrom(allProjectsName.get());
-    ObjectId oldHead;
-    try (Repository git = repoManager.openRepository(allUsersName)) {
-      oldHead = git.findRef(RefNames.REFS_CONFIG).getObjectId();
-    }
-
-    schema162.migrateData(db, new TestUpdateUI());
-
-    // Check that the parent remained unchanged and that no commit was made
-    assertThatAllUsersInheritsFrom(allProjectsName.get());
-    try (Repository git = repoManager.openRepository(allUsersName)) {
-      assertThat(oldHead).isEqualTo(git.findRef(RefNames.REFS_CONFIG).getObjectId());
-    }
-  }
-
-  @Test
-  public void fixIncorrectInheritance() throws Exception {
-    String testProject = gApi.projects().create("test").get().name;
-    assertThatAllUsersInheritsFrom(allProjectsName.get());
-
-    try (Repository git = repoManager.openRepository(allUsersName);
-        MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, allUsersName, git)) {
-      ProjectConfig cfg = ProjectConfig.read(md);
-      cfg.getProject().setParentName(testProject);
-      md.getCommitBuilder().setCommitter(serverUser);
-      md.getCommitBuilder().setAuthor(serverUser);
-      md.setMessage("Test");
-      cfg.commit(md);
-    } catch (ConfigInvalidException | IOException ex) {
-      throw new OrmException(ex);
-    }
-    assertThatAllUsersInheritsFrom(testProject);
-
-    schema162.migrateData(db, new TestUpdateUI());
-
-    assertThatAllUsersInheritsFrom(allProjectsName.get());
-  }
-
-  private void assertThatAllUsersInheritsFrom(String parent) throws Exception {
-    assertThat(gApi.projects().name(allUsersName.get()).access().inheritsFrom.name)
-        .isEqualTo(parent);
-  }
-}
diff --git a/javatests/com/google/gerrit/server/schema/Schema_166_to_167_WithGroupsInNoteDbTest.java b/javatests/com/google/gerrit/server/schema/Schema_166_to_167_WithGroupsInNoteDbTest.java
deleted file mode 100644
index 57689b3..0000000
--- a/javatests/com/google/gerrit/server/schema/Schema_166_to_167_WithGroupsInNoteDbTest.java
+++ /dev/null
@@ -1,227 +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.schema;
-
-import static com.google.common.collect.ImmutableList.toImmutableList;
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.server.notedb.NoteDbTable.GROUPS;
-import static com.google.gerrit.server.notedb.NotesMigration.DISABLE_REVIEW_DB;
-import static com.google.gerrit.server.notedb.NotesMigration.SECTION_NOTE_DB;
-
-import com.google.common.collect.ImmutableList;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.reviewdb.server.ReviewDbWrapper;
-import com.google.gerrit.server.Sequences;
-import com.google.gerrit.server.ServerInitiated;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.config.GerritServerIdProvider;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.group.InternalGroup;
-import com.google.gerrit.server.group.db.GroupNameNotes;
-import com.google.gerrit.server.group.db.GroupsUpdate;
-import com.google.gerrit.server.group.db.InternalGroupCreation;
-import com.google.gerrit.server.group.db.InternalGroupUpdate;
-import com.google.gerrit.testing.InMemoryTestEnvironment;
-import com.google.gerrit.testing.TestUpdateUI;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.inject.Inject;
-import java.io.IOException;
-import java.sql.PreparedStatement;
-import java.sql.Statement;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-
-public class Schema_166_to_167_WithGroupsInNoteDbTest {
-  private static Config createConfig() {
-    Config config = new Config();
-    config.setString(GerritServerIdProvider.SECTION, null, GerritServerIdProvider.KEY, "1234567");
-
-    // Disable groups in ReviewDb. This means the primary storage for groups is NoteDb.
-    config.setBoolean(SECTION_NOTE_DB, GROUPS.key(), DISABLE_REVIEW_DB, true);
-
-    return config;
-  }
-
-  @Rule
-  public InMemoryTestEnvironment testEnv =
-      new InMemoryTestEnvironment(Schema_166_to_167_WithGroupsInNoteDbTest::createConfig);
-
-  @Inject private Schema_167 schema167;
-  @Inject private ReviewDb db;
-  @Inject private GitRepositoryManager gitRepoManager;
-  @Inject private AllUsersName allUsersName;
-  @Inject private @ServerInitiated GroupsUpdate groupsUpdate;
-  @Inject private Sequences seq;
-
-  private JdbcSchema jdbcSchema;
-
-  @Before
-  public void initDb() throws Exception {
-    jdbcSchema = ReviewDbWrapper.unwrapJbdcSchema(db);
-
-    try (Statement stmt = jdbcSchema.getConnection().createStatement()) {
-      stmt.execute(
-          "CREATE TABLE account_groups ("
-              + " group_uuid varchar(255) DEFAULT '' NOT NULL,"
-              + " group_id INTEGER DEFAULT 0 NOT NULL,"
-              + " name varchar(255) DEFAULT '' NOT NULL,"
-              + " created_on TIMESTAMP,"
-              + " description CLOB,"
-              + " owner_group_uuid varchar(255) DEFAULT '' NOT NULL,"
-              + " visible_to_all CHAR(1) DEFAULT 'N' NOT NULL"
-              + ")");
-    }
-  }
-
-  @Test
-  public void migrationIsSkipped() throws Exception {
-    // Create a group in NoteDb (doesn't create the group in ReviewDb since
-    // disableReviewDb == true)
-    InternalGroup internalGroup =
-        groupsUpdate.createGroup(
-            InternalGroupCreation.builder()
-                .setNameKey(new AccountGroup.NameKey("users"))
-                .setGroupUUID(new AccountGroup.UUID("users"))
-                .setId(new AccountGroup.Id(seq.nextGroupId()))
-                .build(),
-            InternalGroupUpdate.builder().setDescription("description").build());
-
-    // Insert the group into ReviewDb
-    AccountGroup group1 =
-        newGroup()
-            .setName(internalGroup.getName())
-            .setGroupUuid(internalGroup.getGroupUUID())
-            .setId(internalGroup.getId())
-            .setCreatedOn(internalGroup.getCreatedOn())
-            .setDescription(internalGroup.getDescription())
-            .setGroupUuid(internalGroup.getGroupUUID())
-            .setVisibleToAll(internalGroup.isVisibleToAll())
-            .build();
-    storeInReviewDb(group1);
-
-    // Update the group description in ReviewDb so that the group state differs between ReviewDb and
-    // NoteDb
-    group1.setDescription("outdated");
-    updateInReviewDb(group1);
-
-    // Create a group that only exists in ReviewDb
-    AccountGroup group2 = newGroup().setName("reviewDbOnlyGroup").build();
-    storeInReviewDb(group2);
-
-    // Remember the SHA1 of the group ref in NoteDb
-    ObjectId groupSha1 = getGroupSha1(group1.getGroupUUID());
-
-    executeSchemaMigration(schema167);
-
-    // Verify the groups in NoteDb: "users" should still exist, "reviewDbOnlyGroup" should not have
-    // been created
-    ImmutableList<GroupReference> groupReferences = getAllGroupsFromNoteDb();
-    ImmutableList<String> groupNames =
-        groupReferences.stream().map(GroupReference::getName).collect(toImmutableList());
-    assertThat(groupNames).contains("users");
-    assertThat(groupNames).doesNotContain("reviewDbOnlyGroup");
-
-    // Verify that the group refs in NoteDb were not touched.
-    assertThat(getGroupSha1(group1.getGroupUUID())).isEqualTo(groupSha1);
-    assertThat(getGroupSha1(group2.getGroupUUID())).isNull();
-  }
-
-  private static TestGroup.Builder newGroup() {
-    return TestGroup.builder();
-  }
-
-  private void storeInReviewDb(AccountGroup... groups) throws Exception {
-    try (PreparedStatement stmt =
-        jdbcSchema
-            .getConnection()
-            .prepareStatement(
-                "INSERT INTO account_groups"
-                    + " (group_uuid,"
-                    + " group_id,"
-                    + " name,"
-                    + " description,"
-                    + " created_on,"
-                    + " owner_group_uuid,"
-                    + " visible_to_all) VALUES (?, ?, ?, ?, ?, ?, ?)")) {
-      for (AccountGroup group : groups) {
-        stmt.setString(1, group.getGroupUUID().get());
-        stmt.setInt(2, group.getId().get());
-        stmt.setString(3, group.getName());
-        stmt.setString(4, group.getDescription());
-        stmt.setTimestamp(5, group.getCreatedOn());
-        stmt.setString(6, group.getOwnerGroupUUID().get());
-        stmt.setString(7, group.isVisibleToAll() ? "Y" : "N");
-        stmt.addBatch();
-      }
-      stmt.executeBatch();
-    }
-  }
-
-  private void updateInReviewDb(AccountGroup... groups) throws Exception {
-    try (PreparedStatement stmt =
-        jdbcSchema
-            .getConnection()
-            .prepareStatement(
-                "UPDATE account_groups SET"
-                    + " group_uuid = ?,"
-                    + " name = ?,"
-                    + " description = ?,"
-                    + " created_on = ?,"
-                    + " owner_group_uuid = ?,"
-                    + " visible_to_all = ?"
-                    + " WHERE group_id = ?")) {
-      for (AccountGroup group : groups) {
-        stmt.setString(1, group.getGroupUUID().get());
-        stmt.setString(2, group.getName());
-        stmt.setString(3, group.getDescription());
-        stmt.setTimestamp(4, group.getCreatedOn());
-        stmt.setString(5, group.getOwnerGroupUUID().get());
-        stmt.setString(6, group.isVisibleToAll() ? "Y" : "N");
-        stmt.setInt(7, group.getId().get());
-        stmt.addBatch();
-      }
-      stmt.executeBatch();
-    }
-  }
-
-  private void executeSchemaMigration(SchemaVersion schema) throws Exception {
-    schema.migrateData(db, new TestUpdateUI());
-  }
-
-  private ImmutableList<GroupReference> getAllGroupsFromNoteDb()
-      throws IOException, ConfigInvalidException {
-    try (Repository allUsersRepo = gitRepoManager.openRepository(allUsersName)) {
-      return GroupNameNotes.loadAllGroups(allUsersRepo);
-    }
-  }
-
-  @Nullable
-  private ObjectId getGroupSha1(AccountGroup.UUID groupUuid) throws IOException {
-    try (Repository allUsersRepo = gitRepoManager.openRepository(allUsersName)) {
-      Ref ref = allUsersRepo.exactRef(RefNames.refsGroups(groupUuid));
-      return ref != null ? ref.getObjectId() : null;
-    }
-  }
-}
diff --git a/javatests/com/google/gerrit/server/schema/Schema_166_to_167_WithGroupsInReviewDbTest.java b/javatests/com/google/gerrit/server/schema/Schema_166_to_167_WithGroupsInReviewDbTest.java
deleted file mode 100644
index 78fef39..0000000
--- a/javatests/com/google/gerrit/server/schema/Schema_166_to_167_WithGroupsInReviewDbTest.java
+++ /dev/null
@@ -1,1125 +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.schema;
-
-import static com.google.common.collect.ImmutableList.toImmutableList;
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.extensions.common.testing.CommitInfoSubject.assertThat;
-import static com.google.gerrit.server.notedb.NoteDbTable.GROUPS;
-import static com.google.gerrit.server.notedb.NotesMigration.DISABLE_REVIEW_DB;
-import static com.google.gerrit.server.notedb.NotesMigration.SECTION_NOTE_DB;
-import static com.google.gerrit.truth.OptionalSubject.assertThat;
-
-import com.google.common.collect.ImmutableList;
-import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.extensions.api.GerritApi;
-import com.google.gerrit.extensions.api.accounts.AccountInput;
-import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo;
-import com.google.gerrit.extensions.api.groups.GroupInput;
-import com.google.gerrit.extensions.common.AccountInfo;
-import com.google.gerrit.extensions.common.CommitInfo;
-import com.google.gerrit.extensions.common.GroupAuditEventInfo;
-import com.google.gerrit.extensions.common.GroupAuditEventInfo.GroupMemberAuditEventInfo;
-import com.google.gerrit.extensions.common.GroupAuditEventInfo.Type;
-import com.google.gerrit.extensions.common.GroupAuditEventInfo.UserMemberAuditEventInfo;
-import com.google.gerrit.extensions.common.GroupInfo;
-import com.google.gerrit.extensions.common.GroupOptionsInfo;
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.extensions.restapi.IdString;
-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.RefNames;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.reviewdb.server.ReviewDbWrapper;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.Sequences;
-import com.google.gerrit.server.account.GroupBackend;
-import com.google.gerrit.server.account.GroupUUID;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.config.GerritServerId;
-import com.google.gerrit.server.config.GerritServerIdProvider;
-import com.google.gerrit.server.git.CommitUtil;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.group.InternalGroup;
-import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.group.db.GroupConfig;
-import com.google.gerrit.server.group.db.GroupNameNotes;
-import com.google.gerrit.server.group.db.GroupsConsistencyChecker;
-import com.google.gerrit.server.group.testing.InternalGroupSubject;
-import com.google.gerrit.server.group.testing.TestGroupBackend;
-import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gerrit.testing.InMemoryTestEnvironment;
-import com.google.gerrit.testing.TestTimeUtil;
-import com.google.gerrit.testing.TestTimeUtil.TempClockStep;
-import com.google.gerrit.testing.TestUpdateUI;
-import com.google.gerrit.truth.OptionalSubject;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import java.io.IOException;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
-import java.sql.Statement;
-import java.sql.Timestamp;
-import java.time.LocalDate;
-import java.time.Month;
-import java.time.ZoneOffset;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Date;
-import java.util.List;
-import java.util.Optional;
-import java.util.concurrent.TimeUnit;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevSort;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-
-public class Schema_166_to_167_WithGroupsInReviewDbTest {
-  private static Config createConfig() {
-    Config config = new Config();
-    config.setString(GerritServerIdProvider.SECTION, null, GerritServerIdProvider.KEY, "1234567");
-
-    // Enable groups in ReviewDb. This means the primary storage for groups is ReviewDb.
-    config.setBoolean(SECTION_NOTE_DB, GROUPS.key(), DISABLE_REVIEW_DB, false);
-
-    return config;
-  }
-
-  @Rule
-  public InMemoryTestEnvironment testEnv =
-      new InMemoryTestEnvironment(Schema_166_to_167_WithGroupsInReviewDbTest::createConfig);
-
-  @Inject private GerritApi gApi;
-  @Inject private Schema_167 schema167;
-  @Inject private ReviewDb db;
-  @Inject private GitRepositoryManager gitRepoManager;
-  @Inject private AllUsersName allUsersName;
-  @Inject private GroupsConsistencyChecker consistencyChecker;
-  @Inject private IdentifiedUser currentUser;
-  @Inject private @GerritServerId String serverId;
-  @Inject private @GerritPersonIdent PersonIdent serverIdent;
-  @Inject private GroupBundle.Factory groupBundleFactory;
-  @Inject private GroupBackend groupBackend;
-  @Inject private DynamicSet<GroupBackend> backends;
-  @Inject private Sequences seq;
-
-  private JdbcSchema jdbcSchema;
-
-  @Before
-  public void initDb() throws Exception {
-    jdbcSchema = ReviewDbWrapper.unwrapJbdcSchema(db);
-
-    try (Statement stmt = jdbcSchema.getConnection().createStatement()) {
-      stmt.execute(
-          "CREATE TABLE account_groups ("
-              + " group_uuid varchar(255) DEFAULT '' NOT NULL,"
-              + " group_id INTEGER DEFAULT 0 NOT NULL,"
-              + " name varchar(255) DEFAULT '' NOT NULL,"
-              + " created_on TIMESTAMP,"
-              + " description CLOB,"
-              + " owner_group_uuid varchar(255) DEFAULT '' NOT NULL,"
-              + " visible_to_all CHAR(1) DEFAULT 'N' NOT NULL"
-              + ")");
-
-      stmt.execute(
-          "CREATE TABLE account_group_members ("
-              + " group_id INTEGER DEFAULT 0 NOT NULL,"
-              + " account_id INTEGER DEFAULT 0 NOT NULL"
-              + ")");
-
-      stmt.execute(
-          "CREATE TABLE account_group_members_audit ("
-              + " group_id INTEGER DEFAULT 0 NOT NULL,"
-              + " account_id INTEGER DEFAULT 0 NOT NULL,"
-              + " added_by INTEGER DEFAULT 0 NOT NULL,"
-              + " added_on TIMESTAMP,"
-              + " removed_by INTEGER,"
-              + " removed_on TIMESTAMP"
-              + ")");
-
-      stmt.execute(
-          "CREATE TABLE account_group_by_id ("
-              + " group_id INTEGER DEFAULT 0 NOT NULL,"
-              + " include_uuid VARCHAR(255) DEFAULT '' NOT NULL"
-              + ")");
-
-      stmt.execute(
-          "CREATE TABLE account_group_by_id_aud ("
-              + " group_id INTEGER DEFAULT 0 NOT NULL,"
-              + " include_uuid VARCHAR(255) DEFAULT '' NOT NULL,"
-              + " added_by INTEGER DEFAULT 0 NOT NULL,"
-              + " added_on TIMESTAMP,"
-              + " removed_by INTEGER,"
-              + " removed_on TIMESTAMP"
-              + ")");
-    }
-  }
-
-  @Before
-  public void setTimeForTesting() {
-    TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS);
-  }
-
-  @After
-  public void resetTime() {
-    TestTimeUtil.useSystemTime();
-  }
-
-  @Test
-  public void reviewDbOnlyGroupsAreMigratedToNoteDb() throws Exception {
-    // Create groups only in ReviewDb
-    AccountGroup group1 = newGroup().setName("verifiers").build();
-    AccountGroup group2 = newGroup().setName("contributors").build();
-    storeInReviewDb(group1, group2);
-
-    executeSchemaMigration(schema167, group1, group2);
-
-    ImmutableList<GroupReference> groups = getAllGroupsFromNoteDb();
-    ImmutableList<String> groupNames =
-        groups.stream().map(GroupReference::getName).collect(toImmutableList());
-    assertThat(groupNames).containsAllOf("verifiers", "contributors");
-  }
-
-  @Test
-  public void alreadyExistingGroupsAreMigratedToNoteDb() throws Exception {
-    // Create group in NoteDb and ReviewDb
-    GroupInput groupInput = new GroupInput();
-    groupInput.name = "verifiers";
-    groupInput.description = "old";
-    GroupInfo group1 = gApi.groups().create(groupInput).get();
-    storeInReviewDb(group1);
-
-    // Update group only in ReviewDb
-    AccountGroup group1InReviewDb = getFromReviewDb(new AccountGroup.Id(group1.groupId));
-    group1InReviewDb.setDescription("new");
-    updateInReviewDb(group1InReviewDb);
-
-    // Create a second group in NoteDb and ReviewDb
-    GroupInfo group2 = gApi.groups().create("contributors").get();
-    storeInReviewDb(group2);
-
-    executeSchemaMigration(schema167, group1, group2);
-
-    // Verify that both groups are present in NoteDb
-    ImmutableList<GroupReference> groups = getAllGroupsFromNoteDb();
-    ImmutableList<String> groupNames =
-        groups.stream().map(GroupReference::getName).collect(toImmutableList());
-    assertThat(groupNames).containsAllOf("verifiers", "contributors");
-
-    // Verify that group1 has the description from ReviewDb
-    Optional<InternalGroup> group1InNoteDb = getGroupFromNoteDb(new AccountGroup.UUID(group1.id));
-    assertThatGroup(group1InNoteDb).value().description().isEqualTo("new");
-  }
-
-  @Test
-  public void adminGroupIsMigratedToNoteDb() throws Exception {
-    // Administrators group is automatically created for all Gerrit servers (NoteDb only).
-    GroupInfo adminGroup = gApi.groups().id("Administrators").get();
-    storeInReviewDb(adminGroup);
-
-    executeSchemaMigration(schema167, adminGroup);
-
-    ImmutableList<GroupReference> groups = getAllGroupsFromNoteDb();
-    ImmutableList<String> groupNames =
-        groups.stream().map(GroupReference::getName).collect(toImmutableList());
-    assertThat(groupNames).contains("Administrators");
-  }
-
-  @Test
-  public void nonInteractiveUsersGroupIsMigratedToNoteDb() throws Exception {
-    // 'Non-Interactive Users' group is automatically created for all Gerrit servers (NoteDb only).
-    GroupInfo nonInteractiveUsersGroup = gApi.groups().id("Non-Interactive Users").get();
-    storeInReviewDb(nonInteractiveUsersGroup);
-
-    executeSchemaMigration(schema167, nonInteractiveUsersGroup);
-
-    ImmutableList<GroupReference> groups = getAllGroupsFromNoteDb();
-    ImmutableList<String> groupNames =
-        groups.stream().map(GroupReference::getName).collect(toImmutableList());
-    assertThat(groupNames).contains("Non-Interactive Users");
-  }
-
-  @Test
-  public void groupsAreConsistentAfterMigrationToNoteDb() throws Exception {
-    // Administrators group are automatically created for all Gerrit servers (NoteDb only).
-    GroupInfo adminGroup = gApi.groups().id("Administrators").get();
-    GroupInfo nonInteractiveUsersGroup = gApi.groups().id("Non-Interactive Users").get();
-    storeInReviewDb(adminGroup, nonInteractiveUsersGroup);
-
-    AccountGroup group1 = newGroup().setName("verifiers").build();
-    AccountGroup group2 = newGroup().setName("contributors").build();
-    storeInReviewDb(group1, group2);
-
-    executeSchemaMigration(schema167, group1, group2);
-
-    List<ConsistencyCheckInfo.ConsistencyProblemInfo> consistencyProblems =
-        consistencyChecker.check();
-    assertThat(consistencyProblems).isEmpty();
-  }
-
-  @Test
-  public void nameIsKeptDuringMigrationToNoteDb() throws Exception {
-    AccountGroup group = newGroup().setName("verifiers").build();
-    storeInReviewDb(group);
-
-    executeSchemaMigration(schema167, group);
-
-    Optional<InternalGroup> groupInNoteDb = getGroupFromNoteDb(group.getGroupUUID());
-    assertThatGroup(groupInNoteDb).value().name().isEqualTo("verifiers");
-  }
-
-  @Test
-  public void emptyNameIsKeptDuringMigrationToNoteDb() throws Exception {
-    AccountGroup group = newGroup().setName("").build();
-    storeInReviewDb(group);
-
-    executeSchemaMigration(schema167, group);
-
-    Optional<InternalGroup> groupInNoteDb = getGroupFromNoteDb(group.getGroupUUID());
-    assertThatGroup(groupInNoteDb).value().name().isEqualTo("");
-  }
-
-  @Test
-  public void uuidIsKeptDuringMigrationToNoteDb() throws Exception {
-    AccountGroup.UUID groupUuid = new AccountGroup.UUID("ABCDEF");
-    AccountGroup group = newGroup().setGroupUuid(groupUuid).build();
-    storeInReviewDb(group);
-
-    executeSchemaMigration(schema167, group);
-
-    Optional<InternalGroup> groupInNoteDb = getGroupFromNoteDb(groupUuid);
-    assertThatGroup(groupInNoteDb).value().groupUuid().isEqualTo(groupUuid);
-  }
-
-  @Test
-  public void idIsKeptDuringMigrationToNoteDb() throws Exception {
-    AccountGroup.Id id = new AccountGroup.Id(12345);
-    AccountGroup group = newGroup().setId(id).build();
-    storeInReviewDb(group);
-
-    executeSchemaMigration(schema167, group);
-
-    Optional<InternalGroup> groupInNoteDb = getGroupFromNoteDb(group.getGroupUUID());
-    assertThatGroup(groupInNoteDb).value().id().isEqualTo(id);
-  }
-
-  @Test
-  public void createdOnIsKeptDuringMigrationToNoteDb() throws Exception {
-    Timestamp createdOn =
-        Timestamp.from(
-            LocalDate.of(2018, Month.FEBRUARY, 20)
-                .atTime(18, 2, 56)
-                .atZone(ZoneOffset.UTC)
-                .toInstant());
-    AccountGroup group = newGroup().setCreatedOn(createdOn).build();
-    storeInReviewDb(group);
-
-    executeSchemaMigration(schema167, group);
-
-    Optional<InternalGroup> groupInNoteDb = getGroupFromNoteDb(group.getGroupUUID());
-    assertThatGroup(groupInNoteDb).value().createdOn().isEqualTo(createdOn);
-  }
-
-  @Test
-  public void ownerUuidIsKeptDuringMigrationToNoteDb() throws Exception {
-    AccountGroup.UUID ownerGroupUuid = new AccountGroup.UUID("UVWXYZ");
-    AccountGroup group = newGroup().setOwnerGroupUuid(ownerGroupUuid).build();
-    storeInReviewDb(group);
-
-    executeSchemaMigration(schema167, group);
-
-    Optional<InternalGroup> groupInNoteDb = getGroupFromNoteDb(group.getGroupUUID());
-    assertThatGroup(groupInNoteDb).value().ownerGroupUuid().isEqualTo(ownerGroupUuid);
-  }
-
-  @Test
-  public void descriptionIsKeptDuringMigrationToNoteDb() throws Exception {
-    AccountGroup group = newGroup().setDescription("A test group").build();
-    storeInReviewDb(group);
-
-    executeSchemaMigration(schema167, group);
-
-    Optional<InternalGroup> groupInNoteDb = getGroupFromNoteDb(group.getGroupUUID());
-    assertThatGroup(groupInNoteDb).value().description().isEqualTo("A test group");
-  }
-
-  @Test
-  public void absentDescriptionIsKeptDuringMigrationToNoteDb() throws Exception {
-    AccountGroup group = newGroup().build();
-    storeInReviewDb(group);
-
-    executeSchemaMigration(schema167, group);
-
-    Optional<InternalGroup> groupInNoteDb = getGroupFromNoteDb(group.getGroupUUID());
-    assertThatGroup(groupInNoteDb).value().description().isNull();
-  }
-
-  @Test
-  public void visibleToAllIsKeptDuringMigrationToNoteDb() throws Exception {
-    AccountGroup group = newGroup().setVisibleToAll(true).build();
-    storeInReviewDb(group);
-
-    executeSchemaMigration(schema167, group);
-
-    Optional<InternalGroup> groupInNoteDb = getGroupFromNoteDb(group.getGroupUUID());
-    assertThatGroup(groupInNoteDb).value().visibleToAll().isTrue();
-  }
-
-  @Test
-  public void membersAreKeptDuringMigrationToNoteDb() throws Exception {
-    AccountGroup group = newGroup().build();
-    storeInReviewDb(group);
-    Account.Id member1 = new Account.Id(23456);
-    Account.Id member2 = new Account.Id(93483);
-    addMembersInReviewDb(group.getId(), member1, member2);
-
-    executeSchemaMigration(schema167, group);
-
-    Optional<InternalGroup> groupInNoteDb = getGroupFromNoteDb(group.getGroupUUID());
-    assertThatGroup(groupInNoteDb).value().members().containsExactly(member1, member2);
-  }
-
-  @Test
-  public void subgroupsAreKeptDuringMigrationToNoteDb() throws Exception {
-    AccountGroup group = newGroup().build();
-    storeInReviewDb(group);
-    AccountGroup.UUID subgroup1 = new AccountGroup.UUID("FGHIKL");
-    AccountGroup.UUID subgroup2 = new AccountGroup.UUID("MNOPQR");
-    addSubgroupsInReviewDb(group.getId(), subgroup1, subgroup2);
-
-    executeSchemaMigration(schema167, group);
-
-    Optional<InternalGroup> groupInNoteDb = getGroupFromNoteDb(group.getGroupUUID());
-    assertThatGroup(groupInNoteDb).value().subgroups().containsExactly(subgroup1, subgroup2);
-  }
-
-  @Test
-  public void logFormatWithAccountsAndGerritGroups() throws Exception {
-    AccountInfo user1 = createAccount("user1");
-    AccountInfo user2 = createAccount("user2");
-
-    AccountGroup group1 = createInReviewDb("group1");
-    AccountGroup group2 = createInReviewDb("group2");
-    AccountGroup group3 = createInReviewDb("group3");
-
-    // Add some accounts
-    try (TempClockStep step = TestTimeUtil.freezeClock()) {
-      addMembersInReviewDb(
-          group1.getId(), new Account.Id(user1._accountId), new Account.Id(user2._accountId));
-    }
-    TimeUtil.nowTs();
-
-    // Add some Gerrit groups
-    try (TempClockStep step = TestTimeUtil.freezeClock()) {
-      addSubgroupsInReviewDb(group1.getId(), group2.getGroupUUID(), group3.getGroupUUID());
-    }
-
-    executeSchemaMigration(schema167, group1, group2, group3);
-
-    GroupBundle noteDbBundle = readGroupBundleFromNoteDb(group1.getGroupUUID());
-
-    ImmutableList<CommitInfo> log = log(group1);
-    assertThat(log).hasSize(4);
-
-    // Verify commit that created the group
-    assertThat(log.get(0)).message().isEqualTo("Create group");
-    assertThat(log.get(0)).author().name().isEqualTo(serverIdent.getName());
-    assertThat(log.get(0)).author().email().isEqualTo(serverIdent.getEmailAddress());
-    assertThat(log.get(0)).author().date().isEqualTo(noteDbBundle.group().getCreatedOn());
-    assertThat(log.get(0)).author().tz().isEqualTo(serverIdent.getTimeZoneOffset());
-    assertThat(log.get(0)).committer().isEqualTo(log.get(0).author);
-
-    // Verify commit that the group creator as member
-    assertThat(log.get(1))
-        .message()
-        .isEqualTo(
-            "Update group\n\nAdd: "
-                + currentUser.getName()
-                + " <"
-                + currentUser.getAccountId()
-                + "@"
-                + serverId
-                + ">");
-    assertThat(log.get(1)).author().name().isEqualTo(currentUser.getName());
-    assertThat(log.get(1)).author().email().isEqualTo(currentUser.getAccountId() + "@" + serverId);
-    assertThat(log.get(1)).committer().hasSameDateAs(log.get(1).author);
-
-    // Verify commit that added members
-    assertThat(log.get(2))
-        .message()
-        .isEqualTo(
-            "Update group\n"
-                + "\n"
-                + ("Add: user1 <" + user1._accountId + "@" + serverId + ">\n")
-                + ("Add: user2 <" + user2._accountId + "@" + serverId + ">"));
-    assertThat(log.get(2)).author().name().isEqualTo(currentUser.getName());
-    assertThat(log.get(2)).author().email().isEqualTo(currentUser.getAccountId() + "@" + serverId);
-    assertThat(log.get(2)).committer().hasSameDateAs(log.get(2).author);
-
-    // Verify commit that added Gerrit groups
-    assertThat(log.get(3))
-        .message()
-        .isEqualTo(
-            "Update group\n"
-                + "\n"
-                + ("Add-group: " + group2.getName() + " <" + group2.getGroupUUID().get() + ">\n")
-                + ("Add-group: " + group3.getName() + " <" + group3.getGroupUUID().get() + ">"));
-    assertThat(log.get(3)).author().name().isEqualTo(currentUser.getName());
-    assertThat(log.get(3)).author().email().isEqualTo(currentUser.getAccountId() + "@" + serverId);
-    assertThat(log.get(3)).committer().hasSameDateAs(log.get(3).author);
-
-    // Verify that audit log is correctly read by Gerrit
-    List<? extends GroupAuditEventInfo> auditEvents =
-        gApi.groups().id(group1.getGroupUUID().get()).auditLog();
-    assertThat(auditEvents).hasSize(5);
-    AccountInfo currentUserInfo = gApi.accounts().id(currentUser.getAccountId().get()).get();
-    assertMemberAuditEvent(
-        auditEvents.get(4), Type.ADD_USER, currentUser.getAccountId(), currentUserInfo);
-    assertMemberAuditEvents(
-        auditEvents.get(3),
-        auditEvents.get(2),
-        Type.ADD_USER,
-        currentUser.getAccountId(),
-        user1,
-        user2);
-    assertSubgroupAuditEvents(
-        auditEvents.get(1),
-        auditEvents.get(0),
-        Type.ADD_GROUP,
-        currentUser.getAccountId(),
-        toGroupInfo(group2),
-        toGroupInfo(group3));
-  }
-
-  @Test
-  public void logFormatWithSystemGroups() throws Exception {
-    AccountGroup group = createInReviewDb("group");
-
-    try (TempClockStep step = TestTimeUtil.freezeClock()) {
-      addSubgroupsInReviewDb(
-          group.getId(), SystemGroupBackend.ANONYMOUS_USERS, SystemGroupBackend.REGISTERED_USERS);
-    }
-
-    executeSchemaMigration(schema167, group);
-
-    GroupBundle noteDbBundle = readGroupBundleFromNoteDb(group.getGroupUUID());
-
-    ImmutableList<CommitInfo> log = log(group);
-    assertThat(log).hasSize(3);
-
-    // Verify commit that created the group
-    assertThat(log.get(0)).message().isEqualTo("Create group");
-    assertThat(log.get(0)).author().name().isEqualTo(serverIdent.getName());
-    assertThat(log.get(0)).author().email().isEqualTo(serverIdent.getEmailAddress());
-    assertThat(log.get(0)).author().date().isEqualTo(noteDbBundle.group().getCreatedOn());
-    assertThat(log.get(0)).author().tz().isEqualTo(serverIdent.getTimeZoneOffset());
-    assertThat(log.get(0)).committer().isEqualTo(log.get(0).author);
-
-    // Verify commit that the group creator as member
-    assertThat(log.get(1))
-        .message()
-        .isEqualTo(
-            "Update group\n\nAdd: "
-                + currentUser.getName()
-                + " <"
-                + currentUser.getAccountId()
-                + "@"
-                + serverId
-                + ">");
-    assertThat(log.get(1)).author().name().isEqualTo(currentUser.getName());
-    assertThat(log.get(1)).author().email().isEqualTo(currentUser.getAccountId() + "@" + serverId);
-    assertThat(log.get(1)).committer().hasSameDateAs(log.get(1).author);
-
-    // Verify commit that added system groups
-    assertThat(log.get(2))
-        .message()
-        .isEqualTo(
-            "Update group\n"
-                + "\n"
-                + "Add-group: Anonymous Users <global:Anonymous-Users>\n"
-                + "Add-group: Registered Users <global:Registered-Users>");
-    assertThat(log.get(2)).author().name().isEqualTo(currentUser.getName());
-    assertThat(log.get(2)).author().email().isEqualTo(currentUser.getAccountId() + "@" + serverId);
-    assertThat(log.get(2)).committer().hasSameDateAs(log.get(2).author);
-
-    // Verify that audit log is correctly read by Gerrit
-    List<? extends GroupAuditEventInfo> auditEvents =
-        gApi.groups().id(group.getGroupUUID().get()).auditLog();
-    assertThat(auditEvents).hasSize(3);
-    AccountInfo currentUserInfo = gApi.accounts().id(currentUser.getAccountId().get()).get();
-    assertMemberAuditEvent(
-        auditEvents.get(2), Type.ADD_USER, currentUser.getAccountId(), currentUserInfo);
-    assertSubgroupAuditEvents(
-        auditEvents.get(1),
-        auditEvents.get(0),
-        Type.ADD_GROUP,
-        currentUser.getAccountId(),
-        groupInfoForExternalGroup(SystemGroupBackend.ANONYMOUS_USERS),
-        groupInfoForExternalGroup(SystemGroupBackend.REGISTERED_USERS));
-  }
-
-  @Test
-  public void logFormatWithExternalGroup() throws Exception {
-    AccountGroup group = createInReviewDb("group");
-
-    TestGroupBackend testGroupBackend = new TestGroupBackend();
-    backends.add("gerrit", testGroupBackend);
-    AccountGroup.UUID subgroupUuid = testGroupBackend.create("test").getGroupUUID();
-    assertThat(groupBackend.handles(subgroupUuid)).isTrue();
-    addSubgroupsInReviewDb(group.getId(), subgroupUuid);
-
-    executeSchemaMigration(schema167, group);
-
-    GroupBundle noteDbBundle = readGroupBundleFromNoteDb(group.getGroupUUID());
-
-    ImmutableList<CommitInfo> log = log(group);
-    assertThat(log).hasSize(3);
-
-    // Verify commit that created the group
-    assertThat(log.get(0)).message().isEqualTo("Create group");
-    assertThat(log.get(0)).author().name().isEqualTo(serverIdent.getName());
-    assertThat(log.get(0)).author().email().isEqualTo(serverIdent.getEmailAddress());
-    assertThat(log.get(0)).author().date().isEqualTo(noteDbBundle.group().getCreatedOn());
-    assertThat(log.get(0)).author().tz().isEqualTo(serverIdent.getTimeZoneOffset());
-    assertThat(log.get(0)).committer().isEqualTo(log.get(0).author);
-
-    // Verify commit that the group creator as member
-    assertThat(log.get(1))
-        .message()
-        .isEqualTo(
-            "Update group\n\nAdd: "
-                + currentUser.getName()
-                + " <"
-                + currentUser.getAccountId()
-                + "@"
-                + serverId
-                + ">");
-    assertThat(log.get(1)).author().name().isEqualTo(currentUser.getName());
-    assertThat(log.get(1)).author().email().isEqualTo(currentUser.getAccountId() + "@" + serverId);
-    assertThat(log.get(1)).committer().hasSameDateAs(log.get(1).author);
-
-    // Verify commit that added system groups
-    // Note: The schema migration can only resolve names of Gerrit groups, not of external groups
-    // and system groups, hence the UUID shows up in commit messages where we would otherwise
-    // expect the group name.
-    assertThat(log.get(2))
-        .message()
-        .isEqualTo(
-            "Update group\n"
-                + "\n"
-                + "Add-group: "
-                + subgroupUuid.get()
-                + " <"
-                + subgroupUuid.get()
-                + ">");
-    assertThat(log.get(2)).author().name().isEqualTo(currentUser.getName());
-    assertThat(log.get(2)).author().email().isEqualTo(currentUser.getAccountId() + "@" + serverId);
-    assertThat(log.get(2)).committer().hasSameDateAs(log.get(2).author);
-
-    // Verify that audit log is correctly read by Gerrit
-    List<? extends GroupAuditEventInfo> auditEvents =
-        gApi.groups().id(group.getGroupUUID().get()).auditLog();
-    assertThat(auditEvents).hasSize(2);
-    AccountInfo currentUserInfo = gApi.accounts().id(currentUser.getAccountId().get()).get();
-    assertMemberAuditEvent(
-        auditEvents.get(1), Type.ADD_USER, currentUser.getAccountId(), currentUserInfo);
-    assertSubgroupAuditEvent(
-        auditEvents.get(0),
-        Type.ADD_GROUP,
-        currentUser.getAccountId(),
-        groupInfoForExternalGroup(subgroupUuid));
-  }
-
-  @Test
-  public void logFormatWithNonExistingExternalGroup() throws Exception {
-    AccountGroup group = createInReviewDb("group");
-
-    AccountGroup.UUID subgroupUuid = new AccountGroup.UUID("notExisting:foo");
-
-    assertThat(groupBackend.handles(subgroupUuid)).isFalse();
-    addSubgroupsInReviewDb(group.getId(), subgroupUuid);
-
-    executeSchemaMigration(schema167, group);
-
-    GroupBundle noteDbBundle = readGroupBundleFromNoteDb(group.getGroupUUID());
-
-    ImmutableList<CommitInfo> log = log(group);
-    assertThat(log).hasSize(3);
-
-    // Verify commit that created the group
-    assertThat(log.get(0)).message().isEqualTo("Create group");
-    assertThat(log.get(0)).author().name().isEqualTo(serverIdent.getName());
-    assertThat(log.get(0)).author().email().isEqualTo(serverIdent.getEmailAddress());
-    assertThat(log.get(0)).author().date().isEqualTo(noteDbBundle.group().getCreatedOn());
-    assertThat(log.get(0)).author().tz().isEqualTo(serverIdent.getTimeZoneOffset());
-    assertThat(log.get(0)).committer().isEqualTo(log.get(0).author);
-
-    // Verify commit that the group creator as member
-    assertThat(log.get(1))
-        .message()
-        .isEqualTo(
-            "Update group\n\nAdd: "
-                + currentUser.getName()
-                + " <"
-                + currentUser.getAccountId()
-                + "@"
-                + serverId
-                + ">");
-    assertThat(log.get(1)).author().name().isEqualTo(currentUser.getName());
-    assertThat(log.get(1)).author().email().isEqualTo(currentUser.getAccountId() + "@" + serverId);
-    assertThat(log.get(1)).committer().hasSameDateAs(log.get(1).author);
-
-    // Verify commit that added system groups
-    // Note: The schema migration can only resolve names of Gerrit groups, not of external groups
-    // and system groups, hence the UUID shows up in commit messages where we would otherwise
-    // expect the group name.
-    assertThat(log.get(2))
-        .message()
-        .isEqualTo("Update group\n" + "\n" + "Add-group: notExisting:foo <notExisting:foo>");
-    assertThat(log.get(2)).author().name().isEqualTo(currentUser.getName());
-    assertThat(log.get(2)).author().email().isEqualTo(currentUser.getAccountId() + "@" + serverId);
-    assertThat(log.get(2)).committer().hasSameDateAs(log.get(2).author);
-
-    // Verify that audit log is correctly read by Gerrit
-    List<? extends GroupAuditEventInfo> auditEvents =
-        gApi.groups().id(group.getGroupUUID().get()).auditLog();
-    assertThat(auditEvents).hasSize(2);
-    AccountInfo currentUserInfo = gApi.accounts().id(currentUser.getAccountId().get()).get();
-    assertMemberAuditEvent(
-        auditEvents.get(1), Type.ADD_USER, currentUser.getAccountId(), currentUserInfo);
-    assertSubgroupAuditEvent(
-        auditEvents.get(0),
-        Type.ADD_GROUP,
-        currentUser.getAccountId(),
-        groupInfoForExternalGroup(subgroupUuid));
-  }
-
-  private static TestGroup.Builder newGroup() {
-    return TestGroup.builder();
-  }
-
-  private AccountGroup createInReviewDb(String groupName) throws Exception {
-    AccountGroup group =
-        new AccountGroup(
-            new AccountGroup.NameKey(groupName),
-            new AccountGroup.Id(seq.nextGroupId()),
-            GroupUUID.make(groupName, serverIdent),
-            TimeUtil.nowTs());
-    storeInReviewDb(group);
-    addMembersInReviewDb(group.getId(), currentUser.getAccountId());
-    return group;
-  }
-
-  private void storeInReviewDb(GroupInfo... groups) throws Exception {
-    storeInReviewDb(
-        Arrays.stream(groups)
-            .map(Schema_166_to_167_WithGroupsInReviewDbTest::toAccountGroup)
-            .toArray(AccountGroup[]::new));
-  }
-
-  private void storeInReviewDb(AccountGroup... groups) throws Exception {
-    try (PreparedStatement stmt =
-        jdbcSchema
-            .getConnection()
-            .prepareStatement(
-                "INSERT INTO account_groups"
-                    + " (group_uuid,"
-                    + " group_id,"
-                    + " name,"
-                    + " description,"
-                    + " created_on,"
-                    + " owner_group_uuid,"
-                    + " visible_to_all) VALUES (?, ?, ?, ?, ?, ?, ?)")) {
-      for (AccountGroup group : groups) {
-        stmt.setString(1, group.getGroupUUID().get());
-        stmt.setInt(2, group.getId().get());
-        stmt.setString(3, group.getName());
-        stmt.setString(4, group.getDescription());
-        stmt.setTimestamp(5, group.getCreatedOn());
-        stmt.setString(6, group.getOwnerGroupUUID().get());
-        stmt.setString(7, group.isVisibleToAll() ? "Y" : "N");
-        stmt.addBatch();
-      }
-      stmt.executeBatch();
-    }
-  }
-
-  private void updateInReviewDb(AccountGroup... groups) throws Exception {
-    try (PreparedStatement stmt =
-        jdbcSchema
-            .getConnection()
-            .prepareStatement(
-                "UPDATE account_groups SET"
-                    + " group_uuid = ?,"
-                    + " name = ?,"
-                    + " description = ?,"
-                    + " created_on = ?,"
-                    + " owner_group_uuid = ?,"
-                    + " visible_to_all = ?"
-                    + " WHERE group_id = ?")) {
-      for (AccountGroup group : groups) {
-        stmt.setString(1, group.getGroupUUID().get());
-        stmt.setString(2, group.getName());
-        stmt.setString(3, group.getDescription());
-        stmt.setTimestamp(4, group.getCreatedOn());
-        stmt.setString(5, group.getOwnerGroupUUID().get());
-        stmt.setString(6, group.isVisibleToAll() ? "Y" : "N");
-        stmt.setInt(7, group.getId().get());
-        stmt.addBatch();
-      }
-      stmt.executeBatch();
-    }
-  }
-
-  private AccountGroup getFromReviewDb(AccountGroup.Id groupId) throws Exception {
-    try (Statement stmt = jdbcSchema.getConnection().createStatement();
-        ResultSet rs =
-            stmt.executeQuery(
-                "SELECT group_uuid,"
-                    + " name,"
-                    + " description,"
-                    + " created_on,"
-                    + " owner_group_uuid,"
-                    + " visible_to_all"
-                    + " FROM account_groups"
-                    + " WHERE group_id = "
-                    + groupId.get())) {
-      if (!rs.next()) {
-        throw new OrmException(String.format("Group %s not found", groupId.get()));
-      }
-
-      AccountGroup.UUID groupUuid = new AccountGroup.UUID(rs.getString(1));
-      AccountGroup.NameKey groupName = new AccountGroup.NameKey(rs.getString(2));
-      String description = rs.getString(3);
-      Timestamp createdOn = rs.getTimestamp(4);
-      AccountGroup.UUID ownerGroupUuid = new AccountGroup.UUID(rs.getString(5));
-      boolean visibleToAll = "Y".equals(rs.getString(6));
-
-      AccountGroup group = new AccountGroup(groupName, groupId, groupUuid, createdOn);
-      group.setDescription(description);
-      group.setOwnerGroupUUID(ownerGroupUuid);
-      group.setVisibleToAll(visibleToAll);
-
-      if (rs.next()) {
-        throw new OrmException(String.format("Group ID %s is ambiguous", groupId.get()));
-      }
-
-      return group;
-    }
-  }
-
-  private void addMembersInReviewDb(AccountGroup.Id groupId, Account.Id... memberIds)
-      throws Exception {
-    try (PreparedStatement addMemberStmt =
-            jdbcSchema
-                .getConnection()
-                .prepareStatement(
-                    "INSERT INTO account_group_members"
-                        + " (group_id,"
-                        + " account_id) VALUES ("
-                        + groupId.get()
-                        + ", ?)");
-        PreparedStatement addMemberAuditStmt =
-            jdbcSchema
-                .getConnection()
-                .prepareStatement(
-                    "INSERT INTO account_group_members_audit"
-                        + " (group_id,"
-                        + " account_id,"
-                        + " added_by,"
-                        + " added_on) VALUES ("
-                        + groupId.get()
-                        + ", ?, "
-                        + currentUser.getAccountId().get()
-                        + ", ?)")) {
-      Timestamp addedOn = TimeUtil.nowTs();
-      for (Account.Id memberId : memberIds) {
-        addMemberStmt.setInt(1, memberId.get());
-        addMemberStmt.addBatch();
-
-        addMemberAuditStmt.setInt(1, memberId.get());
-        addMemberAuditStmt.setTimestamp(2, addedOn);
-        addMemberAuditStmt.addBatch();
-      }
-      addMemberStmt.executeBatch();
-      addMemberAuditStmt.executeBatch();
-    }
-  }
-
-  private void addSubgroupsInReviewDb(AccountGroup.Id groupId, AccountGroup.UUID... subgroupUuids)
-      throws Exception {
-    try (PreparedStatement addSubGroupStmt =
-            jdbcSchema
-                .getConnection()
-                .prepareStatement(
-                    "INSERT INTO account_group_by_id"
-                        + " (group_id,"
-                        + " include_uuid) VALUES ("
-                        + groupId.get()
-                        + ", ?)");
-        PreparedStatement addSubGroupAuditStmt =
-            jdbcSchema
-                .getConnection()
-                .prepareStatement(
-                    "INSERT INTO account_group_by_id_aud"
-                        + " (group_id,"
-                        + " include_uuid,"
-                        + " added_by,"
-                        + " added_on) VALUES ("
-                        + groupId.get()
-                        + ", ?, "
-                        + currentUser.getAccountId().get()
-                        + ", ?)")) {
-      Timestamp addedOn = TimeUtil.nowTs();
-      for (AccountGroup.UUID subgroupUuid : subgroupUuids) {
-        addSubGroupStmt.setString(1, subgroupUuid.get());
-        addSubGroupStmt.addBatch();
-
-        addSubGroupAuditStmt.setString(1, subgroupUuid.get());
-        addSubGroupAuditStmt.setTimestamp(2, addedOn);
-        addSubGroupAuditStmt.addBatch();
-      }
-      addSubGroupStmt.executeBatch();
-      addSubGroupAuditStmt.executeBatch();
-    }
-  }
-
-  private AccountInfo createAccount(String name) throws RestApiException {
-    AccountInput accountInput = new AccountInput();
-    accountInput.username = name;
-    accountInput.name = name;
-    return gApi.accounts().create(accountInput).get();
-  }
-
-  private GroupBundle readGroupBundleFromNoteDb(AccountGroup.UUID groupUuid) throws Exception {
-    try (Repository allUsersRepo = gitRepoManager.openRepository(allUsersName)) {
-      return groupBundleFactory.fromNoteDb(allUsersName, allUsersRepo, groupUuid);
-    }
-  }
-
-  private void executeSchemaMigration(SchemaVersion schema, AccountGroup... groupsToVerify)
-      throws Exception {
-    executeSchemaMigration(
-        schema,
-        Arrays.stream(groupsToVerify)
-            .map(AccountGroup::getGroupUUID)
-            .toArray(AccountGroup.UUID[]::new));
-  }
-
-  private void executeSchemaMigration(SchemaVersion schema, GroupInfo... groupsToVerify)
-      throws Exception {
-    executeSchemaMigration(
-        schema,
-        Arrays.stream(groupsToVerify)
-            .map(i -> new AccountGroup.UUID(i.id))
-            .toArray(AccountGroup.UUID[]::new));
-  }
-
-  private void executeSchemaMigration(SchemaVersion schema, AccountGroup.UUID... groupsToVerify)
-      throws Exception {
-    List<GroupBundle> reviewDbBundles = new ArrayList<>();
-    for (AccountGroup.UUID groupUuid : groupsToVerify) {
-      reviewDbBundles.add(GroupBundle.Factory.fromReviewDb(db, groupUuid));
-    }
-
-    schema.migrateData(db, new TestUpdateUI());
-
-    for (GroupBundle reviewDbBundle : reviewDbBundles) {
-      assertMigratedCleanly(readGroupBundleFromNoteDb(reviewDbBundle.uuid()), reviewDbBundle);
-    }
-  }
-
-  private void assertMigratedCleanly(GroupBundle noteDbBundle, GroupBundle expectedReviewDbBundle) {
-    assertThat(GroupBundle.compareWithAudits(expectedReviewDbBundle, noteDbBundle)).isEmpty();
-  }
-
-  private ImmutableList<CommitInfo> log(AccountGroup group) throws Exception {
-    ImmutableList.Builder<CommitInfo> result = ImmutableList.builder();
-    List<Date> commitDates = new ArrayList<>();
-    try (Repository allUsersRepo = gitRepoManager.openRepository(allUsersName);
-        RevWalk rw = new RevWalk(allUsersRepo)) {
-      Ref ref = allUsersRepo.exactRef(RefNames.refsGroups(group.getGroupUUID()));
-      if (ref != null) {
-        rw.sort(RevSort.REVERSE);
-        rw.setRetainBody(true);
-        rw.markStart(rw.parseCommit(ref.getObjectId()));
-        for (RevCommit c : rw) {
-          result.add(CommitUtil.toCommitInfo(c));
-          commitDates.add(c.getCommitterIdent().getWhen());
-        }
-      }
-    }
-    assertThat(commitDates).named("commit timestamps for %s", result).isOrdered();
-    return result.build();
-  }
-
-  private ImmutableList<GroupReference> getAllGroupsFromNoteDb()
-      throws IOException, ConfigInvalidException {
-    try (Repository allUsersRepo = gitRepoManager.openRepository(allUsersName)) {
-      return GroupNameNotes.loadAllGroups(allUsersRepo);
-    }
-  }
-
-  private Optional<InternalGroup> getGroupFromNoteDb(AccountGroup.UUID groupUuid) throws Exception {
-    try (Repository allUsersRepo = gitRepoManager.openRepository(allUsersName)) {
-      return GroupConfig.loadForGroup(allUsersName, allUsersRepo, groupUuid).getLoadedGroup();
-    }
-  }
-
-  private static OptionalSubject<InternalGroupSubject, InternalGroup> assertThatGroup(
-      Optional<InternalGroup> group) {
-    return assertThat(group, InternalGroupSubject::assertThat).named("group");
-  }
-
-  private void assertMemberAuditEvent(
-      GroupAuditEventInfo info,
-      Type expectedType,
-      Account.Id expectedUser,
-      AccountInfo expectedMember) {
-    assertThat(info.user._accountId).isEqualTo(expectedUser.get());
-    assertThat(info.type).isEqualTo(expectedType);
-    assertThat(info).isInstanceOf(UserMemberAuditEventInfo.class);
-    assertAccount(((UserMemberAuditEventInfo) info).member, expectedMember);
-  }
-
-  private void assertMemberAuditEvents(
-      GroupAuditEventInfo info1,
-      GroupAuditEventInfo info2,
-      Type expectedType,
-      Account.Id expectedUser,
-      AccountInfo expectedMember1,
-      AccountInfo expectedMember2) {
-    assertThat(info1).isInstanceOf(UserMemberAuditEventInfo.class);
-    assertThat(info2).isInstanceOf(UserMemberAuditEventInfo.class);
-
-    UserMemberAuditEventInfo event1 = (UserMemberAuditEventInfo) info1;
-    UserMemberAuditEventInfo event2 = (UserMemberAuditEventInfo) info2;
-
-    assertThat(event1.member._accountId)
-        .isAnyOf(expectedMember1._accountId, expectedMember2._accountId);
-    assertThat(event2.member._accountId)
-        .isAnyOf(expectedMember1._accountId, expectedMember2._accountId);
-    assertThat(event1.member._accountId).isNotEqualTo(event2.member._accountId);
-
-    if (event1.member._accountId == expectedMember1._accountId) {
-      assertMemberAuditEvent(info1, expectedType, expectedUser, expectedMember1);
-      assertMemberAuditEvent(info2, expectedType, expectedUser, expectedMember2);
-    } else {
-      assertMemberAuditEvent(info1, expectedType, expectedUser, expectedMember2);
-      assertMemberAuditEvent(info2, expectedType, expectedUser, expectedMember1);
-    }
-  }
-
-  private void assertSubgroupAuditEvent(
-      GroupAuditEventInfo info,
-      Type expectedType,
-      Account.Id expectedUser,
-      GroupInfo expectedSubGroup) {
-    assertThat(info.user._accountId).isEqualTo(expectedUser.get());
-    assertThat(info.type).isEqualTo(expectedType);
-    assertThat(info).isInstanceOf(GroupMemberAuditEventInfo.class);
-    assertGroup(((GroupMemberAuditEventInfo) info).member, expectedSubGroup);
-  }
-
-  private void assertSubgroupAuditEvents(
-      GroupAuditEventInfo info1,
-      GroupAuditEventInfo info2,
-      Type expectedType,
-      Account.Id expectedUser,
-      GroupInfo expectedSubGroup1,
-      GroupInfo expectedSubGroup2) {
-    assertThat(info1).isInstanceOf(GroupMemberAuditEventInfo.class);
-    assertThat(info2).isInstanceOf(GroupMemberAuditEventInfo.class);
-
-    GroupMemberAuditEventInfo event1 = (GroupMemberAuditEventInfo) info1;
-    GroupMemberAuditEventInfo event2 = (GroupMemberAuditEventInfo) info2;
-
-    assertThat(event1.member.id).isAnyOf(expectedSubGroup1.id, expectedSubGroup2.id);
-    assertThat(event2.member.id).isAnyOf(expectedSubGroup1.id, expectedSubGroup2.id);
-    assertThat(event1.member.id).isNotEqualTo(event2.member.id);
-
-    if (event1.member.id.equals(expectedSubGroup1.id)) {
-      assertSubgroupAuditEvent(info1, expectedType, expectedUser, expectedSubGroup1);
-      assertSubgroupAuditEvent(info2, expectedType, expectedUser, expectedSubGroup2);
-    } else {
-      assertSubgroupAuditEvent(info1, expectedType, expectedUser, expectedSubGroup2);
-      assertSubgroupAuditEvent(info2, expectedType, expectedUser, expectedSubGroup1);
-    }
-  }
-
-  private void assertAccount(AccountInfo actual, AccountInfo expected) {
-    assertThat(actual._accountId).isEqualTo(expected._accountId);
-    assertThat(actual.name).isEqualTo(expected.name);
-    assertThat(actual.email).isEqualTo(expected.email);
-    assertThat(actual.username).isEqualTo(expected.username);
-  }
-
-  private void assertGroup(GroupInfo actual, GroupInfo expected) {
-    assertThat(actual.id).isEqualTo(expected.id);
-    assertThat(actual.name).isEqualTo(expected.name);
-    assertThat(actual.groupId).isEqualTo(expected.groupId);
-  }
-
-  private GroupInfo groupInfoForExternalGroup(AccountGroup.UUID groupUuid) {
-    GroupInfo groupInfo = new GroupInfo();
-    groupInfo.id = IdString.fromDecoded(groupUuid.get()).encoded();
-
-    if (groupBackend.handles(groupUuid)) {
-      groupInfo.name = groupBackend.get(groupUuid).getName();
-    }
-
-    return groupInfo;
-  }
-
-  private static AccountGroup toAccountGroup(GroupInfo info) {
-    AccountGroup group =
-        new AccountGroup(
-            new AccountGroup.NameKey(info.name),
-            new AccountGroup.Id(info.groupId),
-            new AccountGroup.UUID(info.id),
-            info.createdOn);
-    group.setDescription(info.description);
-    if (info.ownerId != null) {
-      group.setOwnerGroupUUID(new AccountGroup.UUID(info.ownerId));
-    }
-    group.setVisibleToAll(
-        info.options != null && info.options.visibleToAll != null && info.options.visibleToAll);
-    return group;
-  }
-
-  private static GroupInfo toGroupInfo(AccountGroup group) {
-    GroupInfo groupInfo = new GroupInfo();
-    groupInfo.id = group.getGroupUUID().get();
-    groupInfo.groupId = group.getId().get();
-    groupInfo.name = group.getName();
-    groupInfo.createdOn = group.getCreatedOn();
-    groupInfo.description = group.getDescription();
-    groupInfo.owner = group.getOwnerGroupUUID().get();
-    groupInfo.options = new GroupOptionsInfo();
-    groupInfo.options.visibleToAll = group.isVisibleToAll() ? true : null;
-    return groupInfo;
-  }
-}
diff --git a/javatests/com/google/gerrit/server/update/BUILD b/javatests/com/google/gerrit/server/update/BUILD
index cef3d63..efe24d5 100644
--- a/javatests/com/google/gerrit/server/update/BUILD
+++ b/javatests/com/google/gerrit/server/update/BUILD
@@ -1,30 +1,9 @@
 load("//tools/bzl:junit.bzl", "junit_tests")
 
-MEDIUM_TESTS = ["RefUpdateUtilRepoTest.java"]
-
-junit_tests(
-    name = "medium_tests",
-    size = "medium",
-    timeout = "short",
-    srcs = MEDIUM_TESTS,
-    tags = ["no_windows"],
-    deps = [
-        "//java/com/google/gerrit/server",
-        "//lib:guava",
-        "//lib:junit",
-        "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/jgit/org.eclipse.jgit.junit:junit",
-        "//lib/truth",
-    ],
-)
-
 junit_tests(
     name = "small_tests",
     size = "small",
-    srcs = glob(
-        ["*.java"],
-        exclude = MEDIUM_TESTS,
-    ),
+    srcs = glob(["*.java"]),
     deps = [
         "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/lifecycle",
@@ -34,7 +13,6 @@
         "//java/com/google/gerrit/server/util/time",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib/guice",
         "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/jgit/org.eclipse.jgit.junit:junit",
diff --git a/javatests/com/google/gerrit/server/update/BatchUpdateTest.java b/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
index 70c4383..cca844e 100644
--- a/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
+++ b/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.server.update;
 
-import static org.junit.Assert.assertEquals;
+import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.git.GitRepositoryManager;
 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;
@@ -31,12 +31,11 @@
 import org.junit.Rule;
 import org.junit.Test;
 
-public class BatchUpdateTest {
+public class BatchUpdateTest extends GerritBaseTests {
   @Rule public InMemoryTestEnvironment testEnvironment = new InMemoryTestEnvironment();
 
   @Inject private GitRepositoryManager repoManager;
   @Inject private BatchUpdate.Factory batchUpdateFactory;
-  @Inject private ReviewDb db;
   @Inject private Provider<CurrentUser> user;
 
   private Project.NameKey project;
@@ -52,10 +51,10 @@
 
   @Test
   public void addRefUpdateFromFastForwardCommit() throws Exception {
-    final RevCommit masterCommit = repo.branch("master").commit().create();
-    final RevCommit branchCommit = repo.branch("branch").commit().parent(masterCommit).create();
+    RevCommit masterCommit = repo.branch("master").commit().create();
+    RevCommit branchCommit = repo.branch("branch").commit().parent(masterCommit).create();
 
-    try (BatchUpdate bu = batchUpdateFactory.create(db, project, user.get(), TimeUtil.nowTs())) {
+    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
       bu.addRepoOnlyOp(
           new RepoOnlyOp() {
             @Override
@@ -66,7 +65,7 @@
       bu.execute();
     }
 
-    assertEquals(
-        repo.getRepository().exactRef("refs/heads/master").getObjectId(), branchCommit.getId());
+    assertThat(repo.getRepository().exactRef("refs/heads/master").getObjectId())
+        .isEqualTo(branchCommit.getId());
   }
 }
diff --git a/javatests/com/google/gerrit/server/update/RefUpdateUtilRepoTest.java b/javatests/com/google/gerrit/server/update/RefUpdateUtilRepoTest.java
deleted file mode 100644
index fe9d522..0000000
--- a/javatests/com/google/gerrit/server/update/RefUpdateUtilRepoTest.java
+++ /dev/null
@@ -1,117 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.update;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.io.MoreFiles;
-import com.google.common.io.RecursiveDeleteOption;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
-import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
-import org.eclipse.jgit.internal.storage.file.FileRepository;
-import org.eclipse.jgit.junit.TestRepository;
-import org.eclipse.jgit.lib.Repository;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameter;
-import org.junit.runners.Parameterized.Parameters;
-
-@RunWith(Parameterized.class)
-public class RefUpdateUtilRepoTest {
-  public enum RepoSetup {
-    LOCAL_DISK {
-      @Override
-      Repository setUpRepo() throws Exception {
-        Path p = Files.createTempDirectory("gerrit_repo_");
-        try {
-          Repository repo = new FileRepository(p.toFile());
-          repo.create(true);
-          return repo;
-        } catch (Exception e) {
-          delete(p);
-          throw e;
-        }
-      }
-
-      @Override
-      void tearDownRepo(Repository repo) throws Exception {
-        delete(repo.getDirectory().toPath());
-      }
-
-      private void delete(Path p) throws Exception {
-        MoreFiles.deleteRecursively(p, RecursiveDeleteOption.ALLOW_INSECURE);
-      }
-    },
-
-    IN_MEMORY {
-      @Override
-      Repository setUpRepo() {
-        return new InMemoryRepository(new DfsRepositoryDescription("repo"));
-      }
-
-      @Override
-      void tearDownRepo(Repository repo) {}
-    };
-
-    abstract Repository setUpRepo() throws Exception;
-
-    abstract void tearDownRepo(Repository repo) throws Exception;
-  }
-
-  @Parameters(name = "{0}")
-  public static ImmutableList<RepoSetup[]> data() {
-    return ImmutableList.copyOf(new RepoSetup[][] {{RepoSetup.LOCAL_DISK}, {RepoSetup.IN_MEMORY}});
-  }
-
-  @Parameter public RepoSetup repoSetup;
-
-  private Repository repo;
-
-  @Before
-  public void setUp() throws Exception {
-    repo = repoSetup.setUpRepo();
-  }
-
-  @After
-  public void tearDown() throws Exception {
-    if (repo != null) {
-      repoSetup.tearDownRepo(repo);
-      repo = null;
-    }
-  }
-
-  @Test
-  public void deleteRefNoOp() throws Exception {
-    String ref = "refs/heads/foo";
-    assertThat(repo.exactRef(ref)).isNull();
-    RefUpdateUtil.deleteChecked(repo, "refs/heads/foo");
-    assertThat(repo.exactRef(ref)).isNull();
-  }
-
-  @Test
-  public void deleteRef() throws Exception {
-    String ref = "refs/heads/foo";
-    new TestRepository<>(repo).branch(ref).commit().create();
-    assertThat(repo.exactRef(ref)).isNotNull();
-    RefUpdateUtil.deleteChecked(repo, "refs/heads/foo");
-    assertThat(repo.exactRef(ref)).isNull();
-  }
-}
diff --git a/javatests/com/google/gerrit/server/update/RefUpdateUtilTest.java b/javatests/com/google/gerrit/server/update/RefUpdateUtilTest.java
deleted file mode 100644
index fc8696a..0000000
--- a/javatests/com/google/gerrit/server/update/RefUpdateUtilTest.java
+++ /dev/null
@@ -1,119 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.update;
-
-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 com.google.common.collect.ImmutableList;
-import com.google.gerrit.server.git.LockFailureException;
-import java.io.IOException;
-import java.util.function.Consumer;
-import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
-import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
-import org.eclipse.jgit.lib.BatchRefUpdate;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.transport.ReceiveCommand;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-@RunWith(JUnit4.class)
-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);
-  private static final Consumer<ReceiveCommand> REJECTED =
-      c -> c.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON);
-  private static final Consumer<ReceiveCommand> ABORTED =
-      c -> {
-        c.setResult(ReceiveCommand.Result.NOT_ATTEMPTED);
-        ReceiveCommand.abort(ImmutableList.of(c));
-        checkState(
-            c.getResult() != ReceiveCommand.Result.NOT_ATTEMPTED
-                && c.getResult() != ReceiveCommand.Result.LOCK_FAILURE
-                && c.getResult() != ReceiveCommand.Result.OK,
-            "unexpected state after abort: %s",
-            c);
-      };
-
-  @Test
-  public void checkBatchRefUpdateResults() throws Exception {
-    checkResults();
-    checkResults(OK);
-    checkResults(OK, OK);
-
-    assertIoException(REJECTED);
-    assertIoException(OK, REJECTED);
-    assertIoException(LOCK_FAILURE, REJECTED);
-    assertIoException(LOCK_FAILURE, OK);
-    assertIoException(LOCK_FAILURE, REJECTED, OK);
-    assertIoException(LOCK_FAILURE, LOCK_FAILURE, REJECTED);
-    assertIoException(LOCK_FAILURE, ABORTED, REJECTED);
-    assertIoException(LOCK_FAILURE, ABORTED, OK);
-
-    assertLockFailureException(LOCK_FAILURE);
-    assertLockFailureException(LOCK_FAILURE, LOCK_FAILURE);
-    assertLockFailureException(LOCK_FAILURE, LOCK_FAILURE, ABORTED);
-    assertLockFailureException(LOCK_FAILURE, LOCK_FAILURE, ABORTED, ABORTED);
-    assertLockFailureException(ABORTED);
-    assertLockFailureException(ABORTED, ABORTED);
-  }
-
-  @SafeVarargs
-  private static void checkResults(Consumer<ReceiveCommand>... resultSetters) throws Exception {
-    RefUpdateUtil.checkResults(newBatchRefUpdate(resultSetters));
-  }
-
-  @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);
-    }
-  }
-
-  @SafeVarargs
-  private static void assertLockFailureException(Consumer<ReceiveCommand>... resultSetters)
-      throws Exception {
-    try {
-      RefUpdateUtil.checkResults(newBatchRefUpdate(resultSetters));
-      assert_().fail("expected LockFailureException");
-    } catch (LockFailureException e) {
-      // Expected.
-    }
-  }
-
-  @SafeVarargs
-  private static BatchRefUpdate newBatchRefUpdate(Consumer<ReceiveCommand>... resultSetters) {
-    try (Repository repo = new InMemoryRepository(new DfsRepositoryDescription("repo"))) {
-      BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
-      for (int i = 0; i < resultSetters.length; i++) {
-        ReceiveCommand cmd =
-            new ReceiveCommand(
-                ObjectId.fromString(String.format("%039x1", i)),
-                ObjectId.fromString(String.format("%039x2", i)),
-                "refs/heads/branch" + i);
-        bru.addCommand(cmd);
-        resultSetters[i].accept(cmd);
-      }
-      return bru;
-    }
-  }
-}
diff --git a/javatests/com/google/gerrit/server/update/RepoViewTest.java b/javatests/com/google/gerrit/server/update/RepoViewTest.java
index 9f7deee..b41c66c 100644
--- a/javatests/com/google/gerrit/server/update/RepoViewTest.java
+++ b/javatests/com/google/gerrit/server/update/RepoViewTest.java
@@ -19,6 +19,7 @@
 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.testing.InMemoryRepositoryManager;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
@@ -30,7 +31,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class RepoViewTest {
+public class RepoViewTest extends GerritBaseTests {
   private static final String MASTER = "refs/heads/master";
   private static final String BRANCH = "refs/heads/branch";
 
diff --git a/javatests/com/google/gerrit/server/util/IdGeneratorTest.java b/javatests/com/google/gerrit/server/util/IdGeneratorTest.java
index 808eca8..e702656 100644
--- a/javatests/com/google/gerrit/server/util/IdGeneratorTest.java
+++ b/javatests/com/google/gerrit/server/util/IdGeneratorTest.java
@@ -17,10 +17,11 @@
 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 {
+public class IdGeneratorTest extends GerritBaseTests {
   @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 9069928..3048b75 100644
--- a/javatests/com/google/gerrit/server/util/LabelVoteTest.java
+++ b/javatests/com/google/gerrit/server/util/LabelVoteTest.java
@@ -19,9 +19,10 @@
 import static com.google.gerrit.server.util.LabelVote.parse;
 import static com.google.gerrit.server.util.LabelVote.parseWithEquals;
 
+import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class LabelVoteTest {
+public class LabelVoteTest extends GerritBaseTests {
   @Test
   public void labelVoteParse() {
     assertLabelVoteEquals(parse("Code-Review-2"), "Code-Review", -2);
diff --git a/javatests/com/google/gerrit/server/util/MostSpecificComparatorTest.java b/javatests/com/google/gerrit/server/util/MostSpecificComparatorTest.java
index 025bf84..9ea17f3 100644
--- a/javatests/com/google/gerrit/server/util/MostSpecificComparatorTest.java
+++ b/javatests/com/google/gerrit/server/util/MostSpecificComparatorTest.java
@@ -16,9 +16,10 @@
 
 import static org.junit.Assert.assertTrue;
 
+import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class MostSpecificComparatorTest {
+public class MostSpecificComparatorTest extends GerritBaseTests {
 
   private MostSpecificComparator cmp;
 
diff --git a/javatests/com/google/gerrit/server/util/git/BUILD b/javatests/com/google/gerrit/server/util/git/BUILD
index 61a776fa..0cb7b8a 100644
--- a/javatests/com/google/gerrit/server/util/git/BUILD
+++ b/javatests/com/google/gerrit/server/util/git/BUILD
@@ -10,12 +10,11 @@
     deps = [
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server/util/git",
+        "//java/com/google/gerrit/testing:gerrit-test-util",
         "//java/com/google/gerrit/truth",
-        "//java/org/eclipse/jgit:server",
         "//lib:gson",
         "//lib:guava",
         "//lib:guava-retrying",
-        "//lib:gwtorm",
         "//lib:protobuf",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
diff --git a/javatests/com/google/gerrit/server/util/git/SubmoduleSectionParserTest.java b/javatests/com/google/gerrit/server/util/git/SubmoduleSectionParserTest.java
index 0ec9b38..c5e683f 100644
--- a/javatests/com/google/gerrit/server/util/git/SubmoduleSectionParserTest.java
+++ b/javatests/com/google/gerrit/server/util/git/SubmoduleSectionParserTest.java
@@ -20,11 +20,12 @@
 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 java.util.Set;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Test;
 
-public class SubmoduleSectionParserTest {
+public class SubmoduleSectionParserTest extends GerritBaseTests {
   private static final String THIS_SERVER = "http://localhost/";
 
   @Test
diff --git a/javatests/com/google/gerrit/sshd/BUILD b/javatests/com/google/gerrit/sshd/BUILD
index ad7d8a9..7a5e18e 100644
--- a/javatests/com/google/gerrit/sshd/BUILD
+++ b/javatests/com/google/gerrit/sshd/BUILD
@@ -7,6 +7,7 @@
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/server",
         "//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 777cb4f..b663849 100644
--- a/javatests/com/google/gerrit/sshd/commands/ProjectConfigParamParserTest.java
+++ b/javatests/com/google/gerrit/sshd/commands/ProjectConfigParamParserTest.java
@@ -17,12 +17,13 @@
 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 {
+public class ProjectConfigParamParserTest extends GerritBaseTests {
 
   private CreateProjectCommand cmd;
 
diff --git a/java/com/google/gerrit/testing/GerritJUnitTest.java b/javatests/com/google/gerrit/testing/GerritJUnitTest.java
similarity index 100%
rename from java/com/google/gerrit/testing/GerritJUnitTest.java
rename to javatests/com/google/gerrit/testing/GerritJUnitTest.java
diff --git a/javatests/com/google/gerrit/util/http/BUILD b/javatests/com/google/gerrit/util/http/BUILD
index 48b4339..2999fc0 100644
--- a/javatests/com/google/gerrit/util/http/BUILD
+++ b/javatests/com/google/gerrit/util/http/BUILD
@@ -4,6 +4,7 @@
     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",
diff --git a/javatests/com/google/gerrit/util/http/RequestUtilTest.java b/javatests/com/google/gerrit/util/http/RequestUtilTest.java
index 48b7b9c..adda5e7 100644
--- a/javatests/com/google/gerrit/util/http/RequestUtilTest.java
+++ b/javatests/com/google/gerrit/util/http/RequestUtilTest.java
@@ -15,42 +15,54 @@
 package com.google.gerrit.util.http;
 
 import static com.google.common.truth.Truth.assertThat;
+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 {
+public class RequestUtilTest extends GerritBaseTests {
   @Test
-  public void emptyContextPath() {
-    assertThat(RequestUtil.getEncodedPathInfo(fakeRequest("", "/s", "/foo/bar")))
-        .isEqualTo("/foo/bar");
-    assertThat(RequestUtil.getEncodedPathInfo(fakeRequest("", "/s", "/foo%2Fbar")))
-        .isEqualTo("/foo%2Fbar");
+  public void getEncodedPathInfo_emptyContextPath() {
+    assertThat(getEncodedPathInfo(fakeRequest("", "/s", "/foo/bar"))).isEqualTo("/foo/bar");
+    assertThat(getEncodedPathInfo(fakeRequest("", "/s", "/foo%2Fbar"))).isEqualTo("/foo%2Fbar");
   }
 
   @Test
-  public void emptyServletPath() {
-    assertThat(RequestUtil.getEncodedPathInfo(fakeRequest("", "/c", "/foo/bar")))
-        .isEqualTo("/foo/bar");
-    assertThat(RequestUtil.getEncodedPathInfo(fakeRequest("", "/c", "/foo%2Fbar")))
-        .isEqualTo("/foo%2Fbar");
+  public void getEncodedPathInfo_emptyServletPath() {
+    assertThat(getEncodedPathInfo(fakeRequest("", "/c", "/foo/bar"))).isEqualTo("/foo/bar");
+    assertThat(getEncodedPathInfo(fakeRequest("", "/c", "/foo%2Fbar"))).isEqualTo("/foo%2Fbar");
   }
 
   @Test
-  public void trailingSlashes() {
-    assertThat(RequestUtil.getEncodedPathInfo(fakeRequest("/c", "/s", "/foo/bar/")))
-        .isEqualTo("/foo/bar/");
-    assertThat(RequestUtil.getEncodedPathInfo(fakeRequest("/c", "/s", "/foo/bar///")))
-        .isEqualTo("/foo/bar/");
-    assertThat(RequestUtil.getEncodedPathInfo(fakeRequest("/c", "/s", "/foo%2Fbar/")))
-        .isEqualTo("/foo%2Fbar/");
-    assertThat(RequestUtil.getEncodedPathInfo(fakeRequest("/c", "/s", "/foo%2Fbar///")))
+  public void getEncodedPathInfo_trailingSlashes() {
+    assertThat(getEncodedPathInfo(fakeRequest("/c", "/s", "/foo/bar/"))).isEqualTo("/foo/bar/");
+    assertThat(getEncodedPathInfo(fakeRequest("/c", "/s", "/foo/bar///"))).isEqualTo("/foo/bar/");
+    assertThat(getEncodedPathInfo(fakeRequest("/c", "/s", "/foo%2Fbar/"))).isEqualTo("/foo%2Fbar/");
+    assertThat(getEncodedPathInfo(fakeRequest("/c", "/s", "/foo%2Fbar///")))
         .isEqualTo("/foo%2Fbar/");
   }
 
   @Test
   public void emptyPathInfo() {
-    assertThat(RequestUtil.getEncodedPathInfo(fakeRequest("/c", "/s", ""))).isNull();
+    assertThat(getEncodedPathInfo(fakeRequest("/c", "/s", ""))).isNull();
+  }
+
+  @Test
+  public void getRestPathWithoutIds_emptyContextPath() {
+    assertThat(getRestPathWithoutIds(fakeRequest("", "/a/accounts", "/123/test")))
+        .isEqualTo("/accounts/test");
+    assertThat(getRestPathWithoutIds(fakeRequest("", "/accounts", "/123/test")))
+        .isEqualTo("/accounts/test");
+  }
+
+  @Test
+  public void getRestPathWithoutIds_nonEmptyContextPath() {
+    assertThat(getRestPathWithoutIds(fakeRequest("/c", "/a/accounts", "/123/test")))
+        .isEqualTo("/accounts/test");
+    assertThat(getRestPathWithoutIds(fakeRequest("/c", "/accounts", "/123/test")))
+        .isEqualTo("/accounts/test");
   }
 
   private FakeHttpServletRequest fakeRequest(
diff --git a/javatests/com/google/gwtexpui/safehtml/BUILD b/javatests/com/google/gwtexpui/safehtml/BUILD
deleted file mode 100644
index 694f422..0000000
--- a/javatests/com/google/gwtexpui/safehtml/BUILD
+++ /dev/null
@@ -1,13 +0,0 @@
-load("//tools/bzl:junit.bzl", "junit_tests")
-
-junit_tests(
-    name = "safehtml_tests",
-    srcs = glob(["client/**/*.java"]),
-    deps = [
-        "//java/com/google/gwtexpui/safehtml",
-        "//lib:guava",
-        "//lib/gwt:dev",
-        "//lib/gwt:user",
-        "//lib/truth",
-    ],
-)
diff --git a/javatests/com/google/gwtexpui/safehtml/client/LinkFindReplaceTest.java b/javatests/com/google/gwtexpui/safehtml/client/LinkFindReplaceTest.java
deleted file mode 100644
index a77c5b4..0000000
--- a/javatests/com/google/gwtexpui/safehtml/client/LinkFindReplaceTest.java
+++ /dev/null
@@ -1,80 +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.gwtexpui.safehtml.client;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-
-public class LinkFindReplaceTest {
-  @Rule public ExpectedException exception = ExpectedException.none();
-
-  @Test
-  public void noEscaping() {
-    String find = "find";
-    String link = "link";
-    LinkFindReplace a = new LinkFindReplace(find, link);
-    assertThat(a.pattern().getSource()).isEqualTo(find);
-    assertThat(a.replace(find)).isEqualTo("<a href=\"link\">find</a>");
-    assertThat(a.toString()).isEqualTo("find = " + find + ", link = " + link);
-  }
-
-  @Test
-  public void backreference() {
-    LinkFindReplace l = new LinkFindReplace("(bug|issue)\\s*([0-9]+)", "/bug?id=$2");
-    assertThat(l.replace("issue 123")).isEqualTo("<a href=\"/bug?id=123\">issue 123</a>");
-  }
-
-  @Test
-  public void hasValidScheme() {
-    assertThat(LinkFindReplace.hasValidScheme("/absolute/path")).isTrue();
-    assertThat(LinkFindReplace.hasValidScheme("relative/path")).isTrue();
-    assertThat(LinkFindReplace.hasValidScheme("http://url/")).isTrue();
-    assertThat(LinkFindReplace.hasValidScheme("HTTP://url/")).isTrue();
-    assertThat(LinkFindReplace.hasValidScheme("https://url/")).isTrue();
-    assertThat(LinkFindReplace.hasValidScheme("mailto://url/")).isTrue();
-    assertThat(LinkFindReplace.hasValidScheme("ftp://url/")).isFalse();
-    assertThat(LinkFindReplace.hasValidScheme("data:evil")).isFalse();
-    assertThat(LinkFindReplace.hasValidScheme("javascript:alert(1)")).isFalse();
-  }
-
-  @Test
-  public void invalidSchemeInReplace() {
-    exception.expect(IllegalArgumentException.class);
-    new LinkFindReplace("find", "javascript:alert(1)").replace("find");
-  }
-
-  @Test
-  public void invalidSchemeWithBackreference() {
-    exception.expect(IllegalArgumentException.class);
-    new LinkFindReplace(".*(script:[^;]*)", "java$1").replace("Look at this script: alert(1);");
-  }
-
-  @Test
-  public void replaceEscaping() {
-    assertThat(new LinkFindReplace("find", "a\"&'<>b").replace("find"))
-        .isEqualTo("<a href=\"a&quot;&amp;&#39;&lt;&gt;b\">find</a>");
-  }
-
-  @Test
-  public void htmlInFind() {
-    String rawFind = "<b>&quot;bold&quot;</b>";
-    LinkFindReplace a = new LinkFindReplace(rawFind, "/bold");
-    assertThat(a.pattern().getSource()).isEqualTo(rawFind);
-    assertThat(a.replace(rawFind)).isEqualTo("<a href=\"/bold\">" + rawFind + "</a>");
-  }
-}
diff --git a/javatests/com/google/gwtexpui/safehtml/client/RawFindReplaceTest.java b/javatests/com/google/gwtexpui/safehtml/client/RawFindReplaceTest.java
deleted file mode 100644
index 3b5e769..0000000
--- a/javatests/com/google/gwtexpui/safehtml/client/RawFindReplaceTest.java
+++ /dev/null
@@ -1,31 +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.gwtexpui.safehtml.client;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import org.junit.Test;
-
-public class RawFindReplaceTest {
-  @Test
-  public void findReplace() {
-    final String find = "find";
-    final String replace = "replace";
-    final RawFindReplace a = new RawFindReplace(find, replace);
-    assertThat(a.pattern().getSource()).isEqualTo(find);
-    assertThat(a.replace(find)).isEqualTo(replace);
-    assertThat(a.toString()).isEqualTo("find = " + find + ", replace = " + replace);
-  }
-}
diff --git a/javatests/com/google/gwtexpui/safehtml/client/SafeHtmlBuilderTest.java b/javatests/com/google/gwtexpui/safehtml/client/SafeHtmlBuilderTest.java
deleted file mode 100644
index 9a2dbe3..0000000
--- a/javatests/com/google/gwtexpui/safehtml/client/SafeHtmlBuilderTest.java
+++ /dev/null
@@ -1,290 +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.gwtexpui.safehtml.client;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-
-public class SafeHtmlBuilderTest {
-  @Rule public ExpectedException exception = ExpectedException.none();
-
-  @Test
-  public void empty() {
-    final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertThat(b.isEmpty()).isTrue();
-    assertThat(b.hasContent()).isFalse();
-    assertThat(b.asString()).isEmpty();
-
-    b.append("a");
-    assertThat(b.hasContent()).isTrue();
-    assertThat(b.asString()).isEqualTo("a");
-  }
-
-  @Test
-  public void toSafeHtml() {
-    final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    b.append(1);
-
-    final SafeHtml h = b.toSafeHtml();
-    assertThat(h).isNotNull();
-    assertThat(h).isNotSameAs(b);
-    assertThat(h).isNotInstanceOf(SafeHtmlBuilder.class);
-    assertThat(h.asString()).isEqualTo("1");
-  }
-
-  @Test
-  public void append_boolean() {
-    final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertThat(b).isSameAs(b.append(true));
-    assertThat(b).isSameAs(b.append(false));
-    assertThat(b.asString()).isEqualTo("truefalse");
-  }
-
-  @Test
-  public void append_char() {
-    final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertThat(b).isSameAs(b.append('a'));
-    assertThat(b).isSameAs(b.append('b'));
-    assertThat(b.asString()).isEqualTo("ab");
-  }
-
-  @Test
-  public void append_int() {
-    final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertThat(b).isSameAs(b.append(4));
-    assertThat(b).isSameAs(b.append(2));
-    assertThat(b).isSameAs(b.append(-100));
-    assertThat(b.asString()).isEqualTo("42-100");
-  }
-
-  @Test
-  public void append_long() {
-    final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertThat(b).isSameAs(b.append(4L));
-    assertThat(b).isSameAs(b.append(2L));
-    assertThat(b.asString()).isEqualTo("42");
-  }
-
-  @Test
-  public void append_float() {
-    final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertThat(b).isSameAs(b.append(0.0f));
-    assertThat(b.asString()).isEqualTo("0.0");
-  }
-
-  @Test
-  public void append_double() {
-    final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertThat(b).isSameAs(b.append(0.0));
-    assertThat(b.asString()).isEqualTo("0.0");
-  }
-
-  @Test
-  public void append_String() {
-    final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertThat(b).isSameAs(b.append((String) null));
-    assertThat(b.asString()).isEmpty();
-    assertThat(b).isSameAs(b.append("foo"));
-    assertThat(b).isSameAs(b.append("bar"));
-    assertThat(b.asString()).isEqualTo("foobar");
-  }
-
-  @Test
-  public void append_StringBuilder() {
-    final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertThat(b).isSameAs(b.append((StringBuilder) null));
-    assertThat(b.asString()).isEmpty();
-    assertThat(b).isSameAs(b.append(new StringBuilder("foo")));
-    assertThat(b).isSameAs(b.append(new StringBuilder("bar")));
-    assertThat(b.asString()).isEqualTo("foobar");
-  }
-
-  @Test
-  public void append_StringBuffer() {
-    final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertThat(b).isSameAs(b.append((StringBuffer) null));
-    assertThat(b.asString()).isEmpty();
-    assertThat(b).isSameAs(b.append(new StringBuffer("foo")));
-    assertThat(b).isSameAs(b.append(new StringBuffer("bar")));
-    assertThat(b.asString()).isEqualTo("foobar");
-  }
-
-  @Test
-  public void append_Object() {
-    final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertThat(b).isSameAs(b.append((Object) null));
-    assertThat(b.asString()).isEmpty();
-    assertThat(b)
-        .isSameAs(
-            b.append(
-                new Object() {
-                  @Override
-                  public String toString() {
-                    return "foobar";
-                  }
-                }));
-    assertThat(b.asString()).isEqualTo("foobar");
-  }
-
-  @Test
-  public void append_CharSequence() {
-    final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertThat(b).isSameAs(b.append((CharSequence) null));
-    assertThat(b.asString()).isEmpty();
-    assertThat(b).isSameAs(b.append((CharSequence) "foo"));
-    assertThat(b).isSameAs(b.append((CharSequence) "bar"));
-    assertThat(b.asString()).isEqualTo("foobar");
-  }
-
-  @Test
-  public void append_SafeHtml() {
-    final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertThat(b).isSameAs(b.append((SafeHtml) null));
-    assertThat(b.asString()).isEmpty();
-    assertThat(b).isSameAs(b.append(new SafeHtmlString("foo")));
-    assertThat(b).isSameAs(b.append(new SafeHtmlBuilder().append("bar")));
-    assertThat(b.asString()).isEqualTo("foobar");
-  }
-
-  @Test
-  public void htmlSpecialCharacters() {
-    assertThat(escape("&")).isEqualTo("&amp;");
-    assertThat(escape("<")).isEqualTo("&lt;");
-    assertThat(escape(">")).isEqualTo("&gt;");
-    assertThat(escape("\"")).isEqualTo("&quot;");
-    assertThat(escape("'")).isEqualTo("&#39;");
-
-    assertThat(escape('&')).isEqualTo("&amp;");
-    assertThat(escape('<')).isEqualTo("&lt;");
-    assertThat(escape('>')).isEqualTo("&gt;");
-    assertThat(escape('"')).isEqualTo("&quot;");
-    assertThat(escape('\'')).isEqualTo("&#39;");
-
-    assertThat(escape("<b>")).isEqualTo("&lt;b&gt;");
-    assertThat(escape("&lt;b&gt;")).isEqualTo("&amp;lt;b&amp;gt;");
-  }
-
-  @Test
-  public void entityNbsp() {
-    final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertThat(b).isSameAs(b.nbsp());
-    assertThat(b.asString()).isEqualTo("&nbsp;");
-  }
-
-  @Test
-  public void tagBr() {
-    final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertThat(b).isSameAs(b.br());
-    assertThat(b.asString()).isEqualTo("<br />");
-  }
-
-  @Test
-  public void tagTableTrTd() {
-    final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertThat(b).isSameAs(b.openElement("table"));
-    assertThat(b).isSameAs(b.openTr());
-    assertThat(b).isSameAs(b.openTd());
-    assertThat(b).isSameAs(b.append("d<a>ta"));
-    assertThat(b).isSameAs(b.closeTd());
-    assertThat(b).isSameAs(b.closeTr());
-    assertThat(b).isSameAs(b.closeElement("table"));
-    assertThat(b.asString()).isEqualTo("<table><tr><td>d&lt;a&gt;ta</td></tr></table>");
-  }
-
-  @Test
-  public void tagDiv() {
-    final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertThat(b).isSameAs(b.openDiv());
-    assertThat(b).isSameAs(b.append("d<a>ta"));
-    assertThat(b).isSameAs(b.closeDiv());
-    assertThat(b.asString()).isEqualTo("<div>d&lt;a&gt;ta</div>");
-  }
-
-  @Test
-  public void tagAnchor() {
-    final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertThat(b).isSameAs(b.openAnchor());
-
-    assertThat(b.getAttribute("href")).isEmpty();
-    assertThat(b).isSameAs(b.setAttribute("href", "http://here"));
-    assertThat(b.getAttribute("href")).isEqualTo("http://here");
-    assertThat(b).isSameAs(b.setAttribute("href", "d<a>ta"));
-    assertThat(b.getAttribute("href")).isEqualTo("d<a>ta");
-
-    assertThat(b.getAttribute("target")).isEmpty();
-    assertThat(b).isSameAs(b.setAttribute("target", null));
-    assertThat(b.getAttribute("target")).isEmpty();
-
-    assertThat(b).isSameAs(b.append("go"));
-    assertThat(b).isSameAs(b.closeAnchor());
-    assertThat(b.asString()).isEqualTo("<a href=\"d&lt;a&gt;ta\">go</a>");
-  }
-
-  @Test
-  public void tagHeightWidth() {
-    final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertThat(b).isSameAs(b.openElement("img"));
-    assertThat(b).isSameAs(b.setHeight(100));
-    assertThat(b).isSameAs(b.setWidth(42));
-    assertThat(b).isSameAs(b.closeSelf());
-    assertThat(b.asString()).isEqualTo("<img height=\"100\" width=\"42\" />");
-  }
-
-  @Test
-  public void styleName() {
-    final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertThat(b).isSameAs(b.openSpan());
-    assertThat(b).isSameAs(b.setStyleName("foo"));
-    assertThat(b).isSameAs(b.addStyleName("bar"));
-    assertThat(b).isSameAs(b.append("d<a>ta"));
-    assertThat(b).isSameAs(b.closeSpan());
-    assertThat(b.asString()).isEqualTo("<span class=\"foo bar\">d&lt;a&gt;ta</span>");
-  }
-
-  @Test
-  public void rejectJavaScript_AnchorHref() {
-    final String href = "javascript:window.close();";
-    exception.expect(RuntimeException.class);
-    exception.expectMessage("javascript unsafe in href: " + href);
-    new SafeHtmlBuilder().openAnchor().setAttribute("href", href);
-  }
-
-  @Test
-  public void rejectJavaScript_ImgSrc() {
-    final String href = "javascript:window.close();";
-    exception.expect(RuntimeException.class);
-    exception.expectMessage("javascript unsafe in href: " + href);
-    new SafeHtmlBuilder().openElement("img").setAttribute("src", href);
-  }
-
-  @Test
-  public void rejectJavaScript_FormAction() {
-    final String href = "javascript:window.close();";
-    exception.expect(RuntimeException.class);
-    exception.expectMessage("javascript unsafe in href: " + href);
-    new SafeHtmlBuilder().openElement("form").setAttribute("action", href);
-  }
-
-  private static String escape(char c) {
-    return new SafeHtmlBuilder().append(c).asString();
-  }
-
-  private static String escape(String c) {
-    return new SafeHtmlBuilder().append(c).asString();
-  }
-}
diff --git a/javatests/com/google/gwtexpui/safehtml/client/SafeHtml_LinkifyTest.java b/javatests/com/google/gwtexpui/safehtml/client/SafeHtml_LinkifyTest.java
deleted file mode 100644
index b42878b..0000000
--- a/javatests/com/google/gwtexpui/safehtml/client/SafeHtml_LinkifyTest.java
+++ /dev/null
@@ -1,124 +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.gwtexpui.safehtml.client;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import org.junit.Test;
-
-public class SafeHtml_LinkifyTest {
-  @Test
-  public void linkify_SimpleHttp1() {
-    final SafeHtml o = html("A http://go.here/ B");
-    final SafeHtml n = o.linkify();
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString())
-        .isEqualTo(
-            "A <a href=\"http://go.here/\" target=\"_blank\" rel=\"nofollow\""
-                + ">http://go.here/</a> B");
-  }
-
-  @Test
-  public void linkify_SimpleHttps2() {
-    final SafeHtml o = html("A https://go.here/ B");
-    final SafeHtml n = o.linkify();
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString())
-        .isEqualTo(
-            "A <a href=\"https://go.here/\" target=\"_blank\" rel=\"nofollow\""
-                + ">https://go.here/</a> B");
-  }
-
-  @Test
-  public void linkify_Parens1() {
-    final SafeHtml o = html("A (http://go.here/) B");
-    final SafeHtml n = o.linkify();
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString())
-        .isEqualTo(
-            "A (<a href=\"http://go.here/\" target=\"_blank\" rel=\"nofollow\""
-                + ">http://go.here/</a>) B");
-  }
-
-  @Test
-  public void linkify_Parens() {
-    final SafeHtml o = html("A http://go.here/#m() B");
-    final SafeHtml n = o.linkify();
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString())
-        .isEqualTo(
-            "A <a href=\"http://go.here/#m()\" target=\"_blank\" rel=\"nofollow\""
-                + ">http://go.here/#m()</a> B");
-  }
-
-  @Test
-  public void linkify_AngleBrackets1() {
-    final SafeHtml o = html("A <http://go.here/> B");
-    final SafeHtml n = o.linkify();
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString())
-        .isEqualTo(
-            "A &lt;<a href=\"http://go.here/\" target=\"_blank\" rel=\"nofollow\""
-                + ">http://go.here/</a>&gt; B");
-  }
-
-  @Test
-  public void linkify_TrailingPlainLetter() {
-    final SafeHtml o = html("A http://go.here/foo B");
-    final SafeHtml n = o.linkify();
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString())
-        .isEqualTo(
-            "A <a href=\"http://go.here/foo\" target=\"_blank\" rel=\"nofollow\""
-                + ">http://go.here/foo</a> B");
-  }
-
-  @Test
-  public void linkify_TrailingDot() {
-    final SafeHtml o = html("A http://go.here/. B");
-    final SafeHtml n = o.linkify();
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString())
-        .isEqualTo(
-            "A <a href=\"http://go.here/\" target=\"_blank\" rel=\"nofollow\""
-                + ">http://go.here/</a>. B");
-  }
-
-  @Test
-  public void linkify_TrailingComma() {
-    final SafeHtml o = html("A http://go.here/, B");
-    final SafeHtml n = o.linkify();
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString())
-        .isEqualTo(
-            "A <a href=\"http://go.here/\" target=\"_blank\" rel=\"nofollow\""
-                + ">http://go.here/</a>, B");
-  }
-
-  @Test
-  public void linkify_TrailingDotDot() {
-    final SafeHtml o = html("A http://go.here/.. B");
-    final SafeHtml n = o.linkify();
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString())
-        .isEqualTo(
-            "A <a href=\"http://go.here/.\" target=\"_blank\" rel=\"nofollow\""
-                + ">http://go.here/.</a>. B");
-  }
-
-  private static SafeHtml html(String text) {
-    return new SafeHtmlBuilder().append(text).toSafeHtml();
-  }
-}
diff --git a/javatests/com/google/gwtexpui/safehtml/client/SafeHtml_ReplaceTest.java b/javatests/com/google/gwtexpui/safehtml/client/SafeHtml_ReplaceTest.java
deleted file mode 100644
index ac0f6fd6..0000000
--- a/javatests/com/google/gwtexpui/safehtml/client/SafeHtml_ReplaceTest.java
+++ /dev/null
@@ -1,127 +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.gwtexpui.safehtml.client;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import org.junit.Test;
-
-public class SafeHtml_ReplaceTest {
-  @Test
-  public void replaceEmpty() {
-    SafeHtml o = html("A\nissue42\nB");
-    assertThat(o.replaceAll(null)).isSameAs(o);
-    assertThat(o.replaceAll(Collections.<FindReplace>emptyList())).isSameAs(o);
-  }
-
-  @Test
-  public void replaceOneLink() {
-    SafeHtml o = html("A\nissue 42\nB");
-    SafeHtml n =
-        o.replaceAll(repls(new RawFindReplace("(issue\\s(\\d+))", "<a href=\"?$2\">$1</a>")));
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString()).isEqualTo("A\n<a href=\"?42\">issue 42</a>\nB");
-  }
-
-  @Test
-  public void replaceNoLeadingOrTrailingText() {
-    SafeHtml o = html("issue 42");
-    SafeHtml n =
-        o.replaceAll(repls(new RawFindReplace("(issue\\s(\\d+))", "<a href=\"?$2\">$1</a>")));
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString()).isEqualTo("<a href=\"?42\">issue 42</a>");
-  }
-
-  @Test
-  public void replaceTwoLinks() {
-    SafeHtml o = html("A\nissue 42\nissue 9918\nB");
-    SafeHtml n =
-        o.replaceAll(repls(new RawFindReplace("(issue\\s(\\d+))", "<a href=\"?$2\">$1</a>")));
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString())
-        .isEqualTo("A\n<a href=\"?42\">issue 42</a>\n<a href=\"?9918\">issue 9918</a>\nB");
-  }
-
-  @Test
-  public void replaceInOrder() {
-    SafeHtml o = html("A\nissue 42\nReally GWTEXPUI-9918 is better\nB");
-    SafeHtml n =
-        o.replaceAll(
-            repls(
-                new RawFindReplace("(GWTEXPUI-(\\d+))", "<a href=\"gwtexpui-bug?$2\">$1</a>"),
-                new RawFindReplace("(issue\\s+(\\d+))", "<a href=\"generic-bug?$2\">$1</a>")));
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString())
-        .isEqualTo(
-            "A\n"
-                + "<a href=\"generic-bug?42\">issue 42</a>\n"
-                + "Really <a href=\"gwtexpui-bug?9918\">GWTEXPUI-9918</a> is better\n"
-                + "B");
-  }
-
-  @Test
-  public void replaceOverlappingAfterFirstChar() {
-    SafeHtml o = html("abcd");
-    RawFindReplace ab = new RawFindReplace("ab", "AB");
-    RawFindReplace bc = new RawFindReplace("bc", "23");
-    RawFindReplace cd = new RawFindReplace("cd", "YZ");
-
-    assertThat(o.replaceAll(repls(ab, bc)).asString()).isEqualTo("ABcd");
-    assertThat(o.replaceAll(repls(bc, ab)).asString()).isEqualTo("ABcd");
-    assertThat(o.replaceAll(repls(ab, bc, cd)).asString()).isEqualTo("ABYZ");
-  }
-
-  @Test
-  public void replaceOverlappingAtFirstCharLongestMatch() {
-    SafeHtml o = html("abcd");
-    RawFindReplace ab = new RawFindReplace("ab", "AB");
-    RawFindReplace abc = new RawFindReplace("[^d][^d][^d]", "234");
-
-    assertThat(o.replaceAll(repls(ab, abc)).asString()).isEqualTo("ABcd");
-    assertThat(o.replaceAll(repls(abc, ab)).asString()).isEqualTo("234d");
-  }
-
-  @Test
-  public void replaceOverlappingAtFirstCharFirstMatch() {
-    SafeHtml o = html("abcd");
-    RawFindReplace ab1 = new RawFindReplace("ab", "AB");
-    RawFindReplace ab2 = new RawFindReplace("[^cd][^cd]", "12");
-
-    assertThat(o.replaceAll(repls(ab1, ab2)).asString()).isEqualTo("ABcd");
-    assertThat(o.replaceAll(repls(ab2, ab1)).asString()).isEqualTo("12cd");
-  }
-
-  @Test
-  public void failedSanitization() {
-    SafeHtml o = html("abcd");
-    LinkFindReplace evil = new LinkFindReplace("(b)", "javascript:alert('$1')");
-    LinkFindReplace ok = new LinkFindReplace("(b)", "/$1");
-    assertThat(o.replaceAll(repls(evil)).asString()).isEqualTo("abcd");
-    String linked = "a<a href=\"/b\">b</a>cd";
-    assertThat(o.replaceAll(repls(ok)).asString()).isEqualTo(linked);
-    assertThat(o.replaceAll(repls(evil, ok)).asString()).isEqualTo(linked);
-  }
-
-  private static SafeHtml html(String text) {
-    return new SafeHtmlBuilder().append(text).toSafeHtml();
-  }
-
-  private static List<FindReplace> repls(FindReplace... repls) {
-    return Arrays.asList(repls);
-  }
-}
diff --git a/javatests/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyListTest.java b/javatests/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyListTest.java
deleted file mode 100644
index d69b36c..0000000
--- a/javatests/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyListTest.java
+++ /dev/null
@@ -1,125 +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 "<p>AS IS" BASIS,
-// WITHOUT 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.gwtexpui.safehtml.client;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import org.junit.Test;
-
-public class SafeHtml_WikifyListTest {
-  private static final String BEGIN_LIST = "<ul class=\"wikiList\">";
-  private static final String END_LIST = "</ul>";
-
-  private static String item(String raw) {
-    return "<li>" + raw + "</li>";
-  }
-
-  @Test
-  public void bulletList1() {
-    final SafeHtml o = html("A\n\n* line 1\n* 2nd line");
-    final SafeHtml n = o.wikify();
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString())
-        .isEqualTo("<p>A</p>" + BEGIN_LIST + item("line 1") + item("2nd line") + END_LIST);
-  }
-
-  @Test
-  public void bulletList2() {
-    final SafeHtml o = html("A\n\n* line 1\n* 2nd line\n\nB");
-    final SafeHtml n = o.wikify();
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString())
-        .isEqualTo(
-            "<p>A</p>" + BEGIN_LIST + item("line 1") + item("2nd line") + END_LIST + "<p>B</p>");
-  }
-
-  @Test
-  public void bulletList3() {
-    final SafeHtml o = html("* line 1\n* 2nd line\n\nB");
-    final SafeHtml n = o.wikify();
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString())
-        .isEqualTo(BEGIN_LIST + item("line 1") + item("2nd line") + END_LIST + "<p>B</p>");
-  }
-
-  @Test
-  public void bulletList4() {
-    final SafeHtml o =
-        html(
-            "To see this bug, you have to:\n" //
-                + "* Be on IMAP or EAS (not on POP)\n" //
-                + "* Be very unlucky\n");
-    final SafeHtml n = o.wikify();
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString())
-        .isEqualTo(
-            "<p>To see this bug, you have to:</p>"
-                + BEGIN_LIST
-                + item("Be on IMAP or EAS (not on POP)")
-                + item("Be very unlucky")
-                + END_LIST);
-  }
-
-  @Test
-  public void bulletList5() {
-    final SafeHtml o =
-        html(
-            "To see this bug,\n" //
-                + "you have to:\n" //
-                + "* Be on IMAP or EAS (not on POP)\n" //
-                + "* Be very unlucky\n");
-    final SafeHtml n = o.wikify();
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString())
-        .isEqualTo(
-            "<p>To see this bug, you have to:</p>"
-                + BEGIN_LIST
-                + item("Be on IMAP or EAS (not on POP)")
-                + item("Be very unlucky")
-                + END_LIST);
-  }
-
-  @Test
-  public void dashList1() {
-    final SafeHtml o = html("A\n\n- line 1\n- 2nd line");
-    final SafeHtml n = o.wikify();
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString())
-        .isEqualTo("<p>A</p>" + BEGIN_LIST + item("line 1") + item("2nd line") + END_LIST);
-  }
-
-  @Test
-  public void dashList2() {
-    final SafeHtml o = html("A\n\n- line 1\n- 2nd line\n\nB");
-    final SafeHtml n = o.wikify();
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString())
-        .isEqualTo(
-            "<p>A</p>" + BEGIN_LIST + item("line 1") + item("2nd line") + END_LIST + "<p>B</p>");
-  }
-
-  @Test
-  public void dashList3() {
-    final SafeHtml o = html("- line 1\n- 2nd line\n\nB");
-    final SafeHtml n = o.wikify();
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString())
-        .isEqualTo(BEGIN_LIST + item("line 1") + item("2nd line") + END_LIST + "<p>B</p>");
-  }
-
-  private static SafeHtml html(String text) {
-    return new SafeHtmlBuilder().append(text).toSafeHtml();
-  }
-}
diff --git a/javatests/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyPreformatTest.java b/javatests/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyPreformatTest.java
deleted file mode 100644
index 1346cda..0000000
--- a/javatests/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyPreformatTest.java
+++ /dev/null
@@ -1,81 +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 "<p>AS IS" BASIS,
-// WITHOUT 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.gwtexpui.safehtml.client;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import org.junit.Test;
-
-public class SafeHtml_WikifyPreformatTest {
-  private static final String B = "<span class=\"wikiPreFormat\">";
-  private static final String E = "</span><br />";
-
-  private static String pre(String raw) {
-    return B + raw + E;
-  }
-
-  @Test
-  public void preformat1() {
-    final SafeHtml o = html("A\n\n  This is pre\n  formatted");
-    final SafeHtml n = o.wikify();
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString())
-        .isEqualTo("<p>A</p><p>" + pre("  This is pre") + pre("  formatted") + "</p>");
-  }
-
-  @Test
-  public void preformat2() {
-    final SafeHtml o = html("A\n\n  This is pre\n  formatted\n\nbut this is not");
-    final SafeHtml n = o.wikify();
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString())
-        .isEqualTo(
-            "<p>A</p>"
-                + "<p>"
-                + pre("  This is pre")
-                + pre("  formatted")
-                + "</p>"
-                + "<p>but this is not</p>");
-  }
-
-  @Test
-  public void preformat3() {
-    final SafeHtml o = html("A\n\n  Q\n    <R>\n  S\n\nB");
-    final SafeHtml n = o.wikify();
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString())
-        .isEqualTo(
-            "<p>A</p>"
-                + "<p>"
-                + pre("  Q")
-                + pre("    &lt;R&gt;")
-                + pre("  S")
-                + "</p>"
-                + "<p>B</p>");
-  }
-
-  @Test
-  public void preformat4() {
-    final SafeHtml o = html("  Q\n    <R>\n  S\n\nB");
-    final SafeHtml n = o.wikify();
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString())
-        .isEqualTo("<p>" + pre("  Q") + pre("    &lt;R&gt;") + pre("  S") + "</p><p>B</p>");
-  }
-
-  private static SafeHtml html(String text) {
-    return new SafeHtmlBuilder().append(text).toSafeHtml();
-  }
-}
diff --git a/javatests/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyQuoteTest.java b/javatests/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyQuoteTest.java
deleted file mode 100644
index 2008447..0000000
--- a/javatests/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyQuoteTest.java
+++ /dev/null
@@ -1,56 +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 "<p>AS IS" BASIS,
-// WITHOUT 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.gwtexpui.safehtml.client;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import org.junit.Test;
-
-public class SafeHtml_WikifyQuoteTest {
-  private static final String B = "<blockquote class=\"wikiQuote\">";
-  private static final String E = "</blockquote>";
-
-  private static String quote(String raw) {
-    return B + raw + E;
-  }
-
-  @Test
-  public void quote1() {
-    final SafeHtml o = html("> I'm happy\n > with quotes!\n\nSee above.");
-    final SafeHtml n = o.wikify();
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString()).isEqualTo(quote("I&#39;m happy\nwith quotes!") + "<p>See above.</p>");
-  }
-
-  @Test
-  public void quote2() {
-    final SafeHtml o = html("See this said:\n\n > a quoted\n > string block\n\nOK?");
-    final SafeHtml n = o.wikify();
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString())
-        .isEqualTo("<p>See this said:</p>" + quote("a quoted\nstring block") + "<p>OK?</p>");
-  }
-
-  @Test
-  public void nestedQuotes1() {
-    final SafeHtml o = html(" > > prior\n > \n > next\n");
-    final SafeHtml n = o.wikify();
-    assertThat(n.asString()).isEqualTo(quote(quote("prior") + "next\n"));
-  }
-
-  private static SafeHtml html(String text) {
-    return new SafeHtmlBuilder().append(text).toSafeHtml();
-  }
-}
diff --git a/javatests/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyTest.java b/javatests/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyTest.java
deleted file mode 100644
index 166af97..0000000
--- a/javatests/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyTest.java
+++ /dev/null
@@ -1,120 +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 "<p>AS IS" BASIS,
-// WITHOUT 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.gwtexpui.safehtml.client;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import org.junit.Test;
-
-public class SafeHtml_WikifyTest {
-  @Test
-  public void wikify_OneLine1() {
-    final SafeHtml o = html("A  B");
-    final SafeHtml n = o.wikify();
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString()).isEqualTo("<p>A  B</p>");
-  }
-
-  @Test
-  public void wikify_OneLine2() {
-    final SafeHtml o = html("A  B\n");
-    final SafeHtml n = o.wikify();
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString()).isEqualTo("<p>A  B\n</p>");
-  }
-
-  @Test
-  public void wikify_OneParagraph1() {
-    final SafeHtml o = html("A\nB");
-    final SafeHtml n = o.wikify();
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString()).isEqualTo("<p>A\nB</p>");
-  }
-
-  @Test
-  public void wikify_OneParagraph2() {
-    final SafeHtml o = html("A\nB\n");
-    final SafeHtml n = o.wikify();
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString()).isEqualTo("<p>A\nB\n</p>");
-  }
-
-  @Test
-  public void wikify_TwoParagraphs() {
-    final SafeHtml o = html("A\nB\n\nC\nD");
-    final SafeHtml n = o.wikify();
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString()).isEqualTo("<p>A\nB</p><p>C\nD</p>");
-  }
-
-  @Test
-  public void linkify_SimpleHttp1() {
-    final SafeHtml o = html("A http://go.here/ B");
-    final SafeHtml n = o.wikify();
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString())
-        .isEqualTo(
-            "<p>A <a href=\"http://go.here/\" target=\"_blank\" rel=\"nofollow\""
-                + ">http://go.here/</a> B</p>");
-  }
-
-  @Test
-  public void linkify_SimpleHttps2() {
-    final SafeHtml o = html("A https://go.here/ B");
-    final SafeHtml n = o.wikify();
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString())
-        .isEqualTo(
-            "<p>A <a href=\"https://go.here/\" target=\"_blank\" rel=\"nofollow\""
-                + ">https://go.here/</a> B</p>");
-  }
-
-  @Test
-  public void linkify_Parens1() {
-    final SafeHtml o = html("A (http://go.here/) B");
-    final SafeHtml n = o.wikify();
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString())
-        .isEqualTo(
-            "<p>A (<a href=\"http://go.here/\" target=\"_blank\" rel=\"nofollow\""
-                + ">http://go.here/</a>) B</p>");
-  }
-
-  @Test
-  public void linkify_Parens() {
-    final SafeHtml o = html("A http://go.here/#m() B");
-    final SafeHtml n = o.wikify();
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString())
-        .isEqualTo(
-            "<p>A <a href=\"http://go.here/#m()\" target=\"_blank\" rel=\"nofollow\""
-                + ">http://go.here/#m()</a> B</p>");
-  }
-
-  @Test
-  public void linkify_AngleBrackets1() {
-    final SafeHtml o = html("A <http://go.here/> B");
-    final SafeHtml n = o.wikify();
-    assertThat(o).isNotSameAs(n);
-    assertThat(n.asString())
-        .isEqualTo(
-            "<p>A &lt;<a href=\"http://go.here/\" target=\"_blank\" rel=\"nofollow\""
-                + ">http://go.here/</a>&gt; B</p>");
-  }
-
-  private static SafeHtml html(String text) {
-    return new SafeHtmlBuilder().append(text).toSafeHtml();
-  }
-}
diff --git a/javatests/org/eclipse/jgit/BUILD b/javatests/org/eclipse/jgit/BUILD
deleted file mode 100644
index 49ae496..0000000
--- a/javatests/org/eclipse/jgit/BUILD
+++ /dev/null
@@ -1,13 +0,0 @@
-load("@rules_java//java:defs.bzl", "java_test")
-
-java_test(
-    name = "jgit_patch_tests",
-    srcs = glob(["**/*.java"]),
-    test_class = "org.eclipse.jgit.diff.EditDeserializerTest",
-    visibility = ["//visibility:public"],
-    deps = [
-        "//java/org/eclipse/jgit:server",
-        "//lib:junit",
-        "//lib/jgit/org.eclipse.jgit:jgit",
-    ],
-)
diff --git a/javatests/org/eclipse/jgit/diff/EditDeserializerTest.java b/javatests/org/eclipse/jgit/diff/EditDeserializerTest.java
deleted file mode 100644
index c431715..0000000
--- a/javatests/org/eclipse/jgit/diff/EditDeserializerTest.java
+++ /dev/null
@@ -1,26 +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 org.eclipse.jgit.diff;
-
-import static org.junit.Assert.assertNotNull;
-
-import org.junit.Test;
-
-public class EditDeserializerTest {
-  @Test
-  public void diffDeserializer() {
-    assertNotNull("edit deserializer", new EditDeserializer());
-  }
-}
diff --git a/lib/BUILD b/lib/BUILD
index 77b2fcd..7271026 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -29,20 +29,6 @@
 )
 
 java_library(
-    name = "gwtjsonrpc",
-    data = ["//lib:LICENSE-Apache2.0"],
-    visibility = ["//visibility:public"],
-    exports = ["@gwtjsonrpc//jar"],
-)
-
-java_library(
-    name = "gwtjsonrpc_src",
-    data = ["//lib:LICENSE-Apache2.0"],
-    visibility = ["//visibility:public"],
-    exports = ["@gwtjsonrpc//jar:src"],
-)
-
-java_library(
     name = "gson",
     data = ["//lib:LICENSE-Apache2.0"],
     visibility = ["//visibility:public"],
@@ -50,20 +36,6 @@
 )
 
 java_library(
-    name = "gwtorm-client",
-    data = ["//lib:LICENSE-Apache2.0"],
-    visibility = ["//visibility:public"],
-    exports = ["@gwtorm-client//jar"],
-)
-
-java_library(
-    name = "gwtorm-client_src",
-    data = ["//lib:LICENSE-Apache2.0"],
-    visibility = ["//visibility:public"],
-    exports = ["@gwtorm-client//jar:src"],
-)
-
-java_library(
     name = "protobuf",
     data = ["//lib:LICENSE-protobuf"],
     visibility = ["//visibility:public"],
@@ -71,14 +43,10 @@
 )
 
 java_library(
-    name = "gwtorm",
+    name = "guava-failureaccess",
+    data = ["//lib:LICENSE-Apache2.0"],
     visibility = ["//visibility:public"],
-    exports = [":gwtorm-client"],
-    runtime_deps = [
-        ":protobuf",
-        "//lib/antlr:java-runtime",
-        "//lib/ow2:ow2-asm",
-    ],
+    exports = ["@guava-failureaccess//jar"],
 )
 
 java_library(
@@ -93,6 +61,7 @@
     data = ["//lib:LICENSE-Apache2.0"],
     visibility = ["//visibility:public"],
     exports = [
+        ":guava-failureaccess",
         ":j2objc",
         "@guava//jar",
     ],
@@ -480,13 +449,6 @@
 )
 
 java_library(
-    name = "derby",
-    data = ["//lib:LICENSE-Apache2.0"],
-    visibility = ["//visibility:public"],
-    exports = ["@derby//jar"],
-)
-
-java_library(
     name = "soy",
     data = ["//lib:LICENSE-Apache2.0"],
     visibility = ["//visibility:public"],
@@ -523,13 +485,6 @@
     exports = ["@icu4j//jar"],
 )
 
-java_library(
-    name = "postgresql",
-    data = ["//lib:LICENSE-postgresql"],
-    visibility = ["//visibility:public"],
-    exports = ["@postgresql//jar"],
-)
-
 sh_test(
     name = "nongoogle_test",
     srcs = ["nongoogle_test.sh"],
diff --git a/lib/LICENSE-clippy b/lib/LICENSE-clippy
deleted file mode 100644
index b0feeae..0000000
--- a/lib/LICENSE-clippy
+++ /dev/null
@@ -1,20 +0,0 @@
-Copyright (c) 2008 Tom Preston-Werner
-
-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.
diff --git a/lib/LICENSE-codemirror-original b/lib/LICENSE-codemirror-original
deleted file mode 100644
index 7661321..0000000
--- a/lib/LICENSE-codemirror-original
+++ /dev/null
@@ -1,19 +0,0 @@
-Copyright (C) 2016 by Marijn Haverbeke <marijnh@gmail.com> and others
-
-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.
diff --git a/lib/LICENSE-commonmark b/lib/LICENSE-commonmark
new file mode 100644
index 0000000..3fecb98
--- /dev/null
+++ b/lib/LICENSE-commonmark
@@ -0,0 +1,23 @@
+Copyright (c) 2015-2016, Atlassian Pty Ltd
+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.
+
+      THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+      AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+      IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+      DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+      FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+      DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+      SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+      CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+      OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+      OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/lib/LICENSE-diffy b/lib/LICENSE-diffy
deleted file mode 100644
index e837a1a..0000000
--- a/lib/LICENSE-diffy
+++ /dev/null
@@ -1 +0,0 @@
-link:http://creativecommons.org/licenses/by/3.0/[CC-BY 3.0] (c) Sara Owens, inkylabs.com
diff --git a/lib/LICENSE-mockito b/lib/LICENSE-mockito
new file mode 100644
index 0000000..5a311f7c
--- /dev/null
+++ b/lib/LICENSE-mockito
@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (c) 2007 Mockito contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/lib/LICENSE-postgresql b/lib/LICENSE-postgresql
deleted file mode 100644
index fd416d2..0000000
--- a/lib/LICENSE-postgresql
+++ /dev/null
@@ -1,26 +0,0 @@
-Copyright (c) 1997-2011, PostgreSQL Global Development Group
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
-1. Redistributions of source code must retain the above copyright notice,
-   this list of conditions and the following disclaimer.
-2. Redistributions in binary form must reproduce the above copyright notice,
-   this list of conditions and the following disclaimer in the documentation
-   and/or other materials provided with the distribution.
-3. Neither the name of the PostgreSQL Global Development Group 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/LICENSE-silk_icons b/lib/LICENSE-silk_icons
deleted file mode 100644
index b7417f6..0000000
--- a/lib/LICENSE-silk_icons
+++ /dev/null
@@ -1,338 +0,0 @@
-link:http://creativecommons.org/licenses/by/3.0/[CC-BY 3.0] (c) Mark James, link:http://famfamfam.com/lab/icons/silk/[SILK ICONS]
-
-As an author, I would appreciate a reference to my authorship of the Silk
-icon set contents within a readme file or equivalent documentation for
-the software which includes the set or a subset of the icons contained
-within.
-
-THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS
-CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE").  THE WORK IS
-PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW.  ANY USE OF THE
-WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS
-PROHIBITED.
-
-BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND
-AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE.  TO THE EXTENT THIS
-LICENSE MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU
-THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH
-TERMS AND CONDITIONS.
-
-1.  Definitions
-
-  a.  "Adaptation" means a work based upon the Work, or upon the Work
-      and other pre-existing works, such as a translation, adaptation,
-      derivative work, arrangement of music or other alterations of a
-      literary or artistic work, or phonogram or performance and
-      includes cinematographic adaptations or any other form in which
-      the Work may be recast, transformed, or adapted including in any
-      form recognizably derived from the original, except that a work
-      that constitutes a Collection will not be considered an
-      Adaptation for the purpose of this License.  For the avoidance
-      of doubt, where the Work is a musical work, performance or
-      phonogram, the synchronization of the Work in timed-relation
-      with a moving image ("synching") will be considered an
-      Adaptation for the purpose of this License.
-
-  b.  "Collection" means a collection of literary or artistic works,
-      such as encyclopedias and anthologies, or performances,
-      phonograms or broadcasts, or other works or subject matter other
-      than works listed in Section 1(f) below, which, by reason of the
-      selection and arrangement of their contents, constitute
-      intellectual creations, in which the Work is included in its
-      entirety in unmodified form along with one or more other
-      contributions, each constituting separate and independent works
-      in themselves, which together are assembled into a collective
-      whole.  A work that constitutes a Collection will not be
-      considered an Adaptation (as defined above) for the purposes of
-      this License.
-
-  c.  "Distribute" means to make available to the public the original
-      and copies of the Work or Adaptation, as appropriate, through
-      sale or other transfer of ownership.
-
-  d.  "Licensor" means the individual, individuals, entity or entities
-      that offer(s) the Work under the terms of this License.
-
-  e.  "Original Author" means, in the case of a literary or artistic
-      work, the individual, individuals, entity or entities who
-      created the Work or if no individual or entity can be
-      identified, the publisher; and in addition (i) in the case of a
-      performance the actors, singers, musicians, dancers, and other
-      persons who act, sing, deliver, declaim, play in, interpret or
-      otherwise perform literary or artistic works or expressions of
-      folklore; (ii) in the case of a phonogram the producer being the
-      person or legal entity who first fixes the sounds of a
-      performance or other sounds; and, (iii) in the case of
-      broadcasts, the organization that transmits the broadcast.
-
-  f.  "Work" means the literary and/or artistic work offered under the
-      terms of this License including without limitation any
-      production in the literary, scientific and artistic domain,
-      whatever may be the mode or form of its expression including
-      digital form, such as a book, pamphlet and other writing; a
-      lecture, address, sermon or other work of the same nature; a
-      dramatic or dramatico-musical work; a choreographic work or
-      entertainment in dumb show; a musical composition with or
-      without words; a cinematographic work to which are assimilated
-      works expressed by a process analogous to cinematography; a work
-      of drawing, painting, architecture, sculpture, engraving or
-      lithography; a photographic work to which are assimilated works
-      expressed by a process analogous to photography; a work of
-      applied art; an illustration, map, plan, sketch or
-      three-dimensional work relative to geography, topography,
-      architecture or science; a performance; a broadcast; a
-      phonogram; a compilation of data to the extent it is protected
-      as a copyrightable work; or a work performed by a variety or
-      circus performer to the extent it is not otherwise considered a
-      literary or artistic work.
-
-  g.  "You" means an individual or entity exercising rights under this
-      License who has not previously violated the terms of this
-      License with respect to the Work, or who has received express
-      permission from the Licensor to exercise rights under this
-      License despite a previous violation.
-
-  h.  "Publicly Perform" means to perform public recitations of the
-      Work and to communicate to the public those public recitations,
-      by any means or process, including by wire or wireless means or
-      public digital performances; to make available to the public
-      Works in such a way that members of the public may access these
-      Works from a place and at a place individually chosen by them;
-      to perform the Work to the public by any means or process and
-      the communication to the public of the performances of the Work,
-      including by public digital performance; to broadcast and
-      rebroadcast the Work by any means including signs, sounds or
-      images.
-
-  i.  "Reproduce" means to make copies of the Work by any means
-      including without limitation by sound or visual recordings and
-      the right of fixation and reproducing fixations of the Work,
-      including storage of a protected performance or phonogram in
-      digital form or other electronic medium.
-
-2.  Fair Dealing Rights.  Nothing in this License is intended to
-    reduce, limit, or restrict any uses free from copyright or rights
-    arising from limitations or exceptions that are provided for in
-    connection with the copyright protection under copyright law or
-    other applicable laws.
-
-3.  License Grant.  Subject to the terms and conditions of this
-    License, Licensor hereby grants You a worldwide, royalty-free,
-    non-exclusive, perpetual (for the duration of the applicable
-    copyright) license to exercise the rights in the Work as stated
-    below:
-
-  a.  to Reproduce the Work, to incorporate the Work into one or more
-      Collections, and to Reproduce the Work as incorporated in the
-      Collections;
-
-  b.  to create and Reproduce Adaptations provided that any such
-      Adaptation, including any translation in any medium, takes
-      reasonable steps to clearly label, demarcate or otherwise
-      identify that changes were made to the original Work.  For
-      example, a translation could be marked "The original work was
-      translated from English to Spanish," or a modification could
-      indicate "The original work has been modified.";
-
-  c.  to Distribute and Publicly Perform the Work including as
-      incorporated in Collections; and,
-
-  d.  to Distribute and Publicly Perform Adaptations.
-
-  e.  For the avoidance of doubt:
-
-    i.   Non-waivable Compulsory License Schemes.  In those
-	     jurisdictions in which the right to collect royalties
-	     through any statutory or compulsory licensing scheme
-	     cannot be waived, the Licensor reserves the exclusive
-	     right to collect such royalties for any exercise by You
-	     of the rights granted under this License;
-
-    ii.  Waivable Compulsory License Schemes.  In those jurisdictions
-	     in which the right to collect royalties through any
-	     statutory or compulsory licensing scheme can be waived,
-	     the Licensor waives the exclusive right to collect such
-	     royalties for any exercise by You of the rights granted
-	     under this License; and,
-
-    iii. Voluntary License Schemes.  The Licensor waives the right to
-	     collect royalties, whether individually or, in the event
-	     that the Licensor is a member of a collecting society
-	     that administers voluntary licensing schemes, via that
-	     society, from any exercise by You of the rights granted
-	     under this License.
-
-The above rights may be exercised in all media and formats whether now
-known or hereafter devised.  The above rights include the right to
-make such modifications as are technically necessary to exercise the
-rights in other media and formats.  Subject to Section 8(f), all
-rights not expressly granted by Licensor are hereby reserved.
-
-4.  Restrictions.  The license granted in Section 3 above is expressly
-    made subject to and limited by the following restrictions:
-
-  a.  You may Distribute or Publicly Perform the Work only under the
-      terms of this License.  You must include a copy of, or the
-      Uniform Resource Identifier (URI) for, this License with every
-      copy of the Work You Distribute or Publicly Perform.  You may
-      not offer or impose any terms on the Work that restrict the
-      terms of this License or the ability of the recipient of the
-      Work to exercise the rights granted to that recipient under the
-      terms of the License.  You may not sublicense the Work.  You
-      must keep intact all notices that refer to this License and to
-      the disclaimer of warranties with every copy of the Work You
-      Distribute or Publicly Perform.  When You Distribute or Publicly
-      Perform the Work, You may not impose any effective technological
-      measures on the Work that restrict the ability of a recipient of
-      the Work from You to exercise the rights granted to that
-      recipient under the terms of the License.  This Section 4(a)
-      applies to the Work as incorporated in a Collection, but this
-      does not require the Collection apart from the Work itself to be
-      made subject to the terms of this License.  If You create a
-      Collection, upon notice from any Licensor You must, to the
-      extent practicable, remove from the Collection any credit as
-      required by Section 4(b), as requested.  If You create an
-      Adaptation, upon notice from any Licensor You must, to the
-      extent practicable, remove from the Adaptation any credit as
-      required by Section 4(b), as requested.
-
-  b.  If You Distribute, or Publicly Perform the Work or any
-      Adaptations or Collections, You must, unless a request has been
-      made pursuant to Section 4(a), keep intact all copyright notices
-      for the Work and provide, reasonable to the medium or means You
-      are utilizing: (i) the name of the Original Author (or
-      pseudonym, if applicable) if supplied, and/or if the Original
-      Author and/or Licensor designate another party or parties (e.g.,
-      a sponsor institute, publishing entity, journal) for attribution
-      ("Attribution Parties") in Licensor's copyright notice, terms of
-      service or by other reasonable means, the name of such party or
-      parties; (ii) the title of the Work if supplied; (iii) to the
-      extent reasonably practicable, the URI, if any, that Licensor
-      specifies to be associated with the Work, unless such URI does
-      not refer to the copyright notice or licensing information for
-      the Work; and (iv) , consistent with Section 3(b), in the case
-      of an Adaptation, a credit identifying the use of the Work in
-      the Adaptation (e.g., "French translation of the Work by
-      Original Author," or "Screenplay based on original Work by
-      Original Author").  The credit required by this Section 4 (b)
-      may be implemented in any reasonable manner; provided, however,
-      that in the case of a Adaptation or Collection, at a minimum
-      such credit will appear, if a credit for all contributing
-      authors of the Adaptation or Collection appears, then as part of
-      these credits and in a manner at least as prominent as the
-      credits for the other contributing authors.  For the avoidance
-      of doubt, You may only use the credit required by this Section
-      for the purpose of attribution in the manner set out above and,
-      by exercising Your rights under this License, You may not
-      implicitly or explicitly assert or imply any connection with,
-      sponsorship or endorsement by the Original Author, Licensor
-      and/or Attribution Parties, as appropriate, of You or Your use
-      of the Work, without the separate, express prior written
-      permission of the Original Author, Licensor and/or Attribution
-      Parties.
-
-  c.  Except as otherwise agreed in writing by the Licensor or as may
-      be otherwise permitted by applicable law, if You Reproduce,
-      Distribute or Publicly Perform the Work either by itself or as
-      part of any Adaptations or Collections, You must not distort,
-      mutilate, modify or take other derogatory action in relation to
-      the Work which would be prejudicial to the Original Author's
-      honor or reputation.  Licensor agrees that in those
-      jurisdictions (e.g.  Japan), in which any exercise of the right
-      granted in Section 3(b) of this License (the right to make
-      Adaptations) would be deemed to be a distortion, mutilation,
-      modification or other derogatory action prejudicial to the
-      Original Author's honor and reputation, the Licensor will waive
-      or not assert, as appropriate, this Section, to the fullest
-      extent permitted by the applicable national law, to enable You
-      to reasonably exercise Your right under Section 3(b) of this
-      License (right to make Adaptations) but not otherwise.
-
-5.  Representations, Warranties and Disclaimer
-
-UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING,
-LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR
-WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED,
-STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF
-TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE,
-NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY,
-OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE.
-SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES,
-SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
-
-6.  Limitation on Liability.  EXCEPT TO THE EXTENT REQUIRED BY
-    APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY
-    LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE
-    OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE
-    WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
-    DAMAGES.
-
-7.  Termination
-
-  a.  This License and the rights granted hereunder will terminate
-      automatically upon any breach by You of the terms of this
-      License.  Individuals or entities who have received Adaptations
-      or Collections from You under this License, however, will not
-      have their licenses terminated provided such individuals or
-      entities remain in full compliance with those licenses.
-      Sections 1, 2, 5, 6, 7, and 8 will survive any termination of
-      this License.
-
-  b.  Subject to the above terms and conditions, the license granted
-      here is perpetual (for the duration of the applicable copyright
-      in the Work).  Notwithstanding the above, Licensor reserves the
-      right to release the Work under different license terms or to
-      stop distributing the Work at any time; provided, however that
-      any such election will not serve to withdraw this License (or
-      any other license that has been, or is required to be, granted
-      under the terms of this License), and this License will continue
-      in full force and effect unless terminated as stated above.
-
-8. Miscellaneous
-
-  a.  Each time You Distribute or Publicly Perform the Work or a
-      Collection, the Licensor offers to the recipient a license to
-      the Work on the same terms and conditions as the license granted
-      to You under this License.
-
-  b.  Each time You Distribute or Publicly Perform an Adaptation,
-      Licensor offers to the recipient a license to the original Work
-      on the same terms and conditions as the license granted to You
-      under this License.
-
-  c.  If any provision of this License is invalid or unenforceable
-      under applicable law, it shall not affect the validity or
-      enforceability of the remainder of the terms of this License,
-      and without further action by the parties to this agreement,
-      such provision shall be reformed to the minimum extent necessary
-      to make such provision valid and enforceable.
-
-  d.  No term or provision of this License shall be deemed waived and
-      no breach consented to unless such waiver or consent shall be in
-      writing and signed by the party to be charged with such waiver
-      or consent.
-
-  e.  This License constitutes the entire agreement between the
-      parties with respect to the Work licensed here.  There are no
-      understandings, agreements or representations with respect to
-      the Work not specified here.  Licensor shall not be bound by any
-      additional provisions that may appear in any communication from
-      You.  This License may not be modified without the mutual
-      written agreement of the Licensor and You.
-
-  f.  The rights granted under, and the subject matter referenced, in
-      this License were drafted utilizing the terminology of the Berne
-      Convention for the Protection of Literary and Artistic Works (as
-      amended on September 28, 1979), the Rome Convention of 1961, the
-      WIPO Copyright Treaty of 1996, the WIPO Performances and
-      Phonograms Treaty of 1996 and the Universal Copyright Convention
-      (as revised on July 24, 1971).  These rights and subject matter
-      take effect in the relevant jurisdiction in which the License
-      terms are sought to be enforced according to the corresponding
-      provisions of the implementation of those treaty provisions in
-      the applicable national law.  If the standard suite of rights
-      granted under applicable copyright law includes additional
-      rights not granted under this License, such additional rights
-      are deemed to be included in the License; this License is not
-      intended to restrict the license of any rights under applicable
-      law.
diff --git a/lib/codemirror/BUILD b/lib/codemirror/BUILD
deleted file mode 100644
index 05ca920..0000000
--- a/lib/codemirror/BUILD
+++ /dev/null
@@ -1,12 +0,0 @@
-load("@rules_java//java:defs.bzl", "java_library")
-load("//lib/codemirror:cm.bzl", "pkg_cm")
-
-# This library is only used to insert a license statement into
-# js_licenses.txt.
-java_library(
-    name = "diff-match-patch",
-    data = ["//lib:LICENSE-Apache2.0"],
-    runtime_deps = ["@diff-match-patch//jar"],
-)
-
-pkg_cm()
diff --git a/lib/codemirror/cm.bzl b/lib/codemirror/cm.bzl
deleted file mode 100644
index 3e1589a..0000000
--- a/lib/codemirror/cm.bzl
+++ /dev/null
@@ -1,376 +0,0 @@
-load("@rules_java//java:defs.bzl", "java_import")
-load("//tools/bzl:genrule2.bzl", "genrule2")
-
-CM_CSS = [
-    "lib/codemirror.css",
-    "addon/dialog/dialog.css",
-    "addon/merge/merge.css",
-    "addon/scroll/simplescrollbars.css",
-    "addon/search/matchesonscrollbar.css",
-    "addon/lint/lint.css",
-]
-
-CM_JS = [
-    "lib/codemirror.js",
-    "mode/meta.js",
-    "keymap/emacs.js",
-    "keymap/sublime.js",
-    "keymap/vim.js",
-]
-
-CM_ADDONS = [
-    "dialog/dialog.js",
-    "edit/closebrackets.js",
-    "edit/matchbrackets.js",
-    "edit/trailingspace.js",
-    "scroll/annotatescrollbar.js",
-    "scroll/simplescrollbars.js",
-    "search/jump-to-line.js",
-    "search/matchesonscrollbar.js",
-    "search/searchcursor.js",
-    "search/search.js",
-    "selection/mark-selection.js",
-    "mode/multiplex.js",
-    "mode/overlay.js",
-    "mode/simple.js",
-    "lint/lint.js",
-]
-
-# Available themes must be enumerated here,
-# in gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Theme.java,
-# in gerrit-gwtui/src/main/java/net/codemirror/theme/Themes.java
-CM_THEMES = [
-    "3024-day",
-    "3024-night",
-    "abcdef",
-    "ambiance",
-    "base16-dark",
-    "base16-light",
-    "bespin",
-    "blackboard",
-    "cobalt",
-    "colorforth",
-    "dracula",
-    "duotone-dark",
-    "duotone-light",
-    "eclipse",
-    "elegant",
-    "erlang-dark",
-    "gruvbox-dark",
-    "hopscotch",
-    "icecoder",
-    "idea",
-    "isotope",
-    "lesser-dark",
-    "liquibyte",
-    "lucario",
-    "material",
-    "mbo",
-    "mdn-like",
-    "midnight",
-    "monokai",
-    "neat",
-    "neo",
-    "night",
-    "paraiso-dark",
-    "paraiso-light",
-    "pastel-on-dark",
-    "railscasts",
-    "rubyblue",
-    "seti",
-    "solarized",
-    "ssms",
-    "the-matrix",
-    "tomorrow-night-bright",
-    "tomorrow-night-eighties",
-    "ttcn",
-    "twilight",
-    "vibrant-ink",
-    "xq-dark",
-    "xq-light",
-    "yeti",
-    "zenburn",
-]
-
-# Available modes must be enumerated here,
-# in gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java,
-# gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java,
-# and in CodeMirror's own mode/meta.js script.
-CM_MODES = [
-    "apl",
-    "asciiarmor",
-    "asn.1",
-    "asterisk",
-    "brainfuck",
-    "clike",
-    "clojure",
-    "cmake",
-    "cobol",
-    "coffeescript",
-    "commonlisp",
-    "crystal",
-    "css",
-    "cypher",
-    "d",
-    "dart",
-    "diff",
-    "django",
-    "dockerfile",
-    "dtd",
-    "dylan",
-    "ebnf",
-    "ecl",
-    "eiffel",
-    "elm",
-    "erlang",
-    "factor",
-    "fcl",
-    "forth",
-    "fortran",
-    "gas",
-    "gfm",
-    "gherkin",
-    "go",
-    "groovy",
-    "haml",
-    "handlebars",
-    "haskell-literate",
-    "haskell",
-    "haxe",
-    "htmlembedded",
-    "htmlmixed",
-    "http",
-    "idl",
-    "javascript",
-    "jinja2",
-    "jsx",
-    "julia",
-    "livescript",
-    "lua",
-    "markdown",
-    "mathematica",
-    "mbox",
-    "mirc",
-    "mllike",
-    "modelica",
-    "mscgen",
-    "mumps",
-    "nginx",
-    "nsis",
-    "ntriples",
-    "octave",
-    "oz",
-    "pascal",
-    "pegjs",
-    "perl",
-    "php",
-    "pig",
-    "powershell",
-    "properties",
-    "protobuf",
-    "pug",
-    "puppet",
-    "python",
-    "q",
-    "r",
-    "rpm",
-    "rst",
-    "ruby",
-    "rust",
-    "sas",
-    "sass",
-    "scheme",
-    "shell",
-    "sieve",
-    "slim",
-    "smalltalk",
-    "smarty",
-    "solr",
-    "soy",
-    "sparql",
-    "spreadsheet",
-    "sql",
-    "stex",
-    "stylus",
-    "swift",
-    "tcl",
-    "textile",
-    "tiddlywiki",
-    "tiki",
-    "toml",
-    "tornado",
-    "troff",
-    "ttcn-cfg",
-    "ttcn",
-    "turtle",
-    "twig",
-    "vb",
-    "vbscript",
-    "velocity",
-    "verilog",
-    "vhdl",
-    "vue",
-    "webidl",
-    "xml",
-    "xquery",
-    "yacas",
-    "yaml-frontmatter",
-    "yaml",
-    "z80",
-]
-
-CM_VERSION = "5.37.0"
-
-TOP = "META-INF/resources/webjars/codemirror/%s" % CM_VERSION
-
-TOP_MINIFIED = "META-INF/resources/webjars/codemirror-minified/%s" % CM_VERSION
-
-LICENSE = "//lib:LICENSE-codemirror-original"
-
-LICENSE_MINIFIED = "//lib:LICENSE-codemirror-minified"
-
-DIFF_MATCH_PATCH_VERSION = "20121119-1"
-
-DIFF_MATCH_PATCH_TOP = ("META-INF/resources/webjars/google-diff-match-patch/%s" %
-                        DIFF_MATCH_PATCH_VERSION)
-
-def pkg_cm():
-    for archive, suffix, top, license in [
-        ("@codemirror-original-gwt//jar", "", TOP, LICENSE),
-        ("@codemirror-minified-gwt//jar", "_r", TOP_MINIFIED, LICENSE_MINIFIED),
-    ]:
-        # Main JavaScript and addons
-        genrule2(
-            name = "cm" + suffix,
-            cmd = " && ".join(
-                [
-                    "echo '/** @license' >$@",
-                    "unzip -p $(location %s) %s/LICENSE >>$@" % (archive, top),
-                    "echo '*/' >>$@",
-                ] +
-                ["unzip -p $(location %s) %s/%s >>$@" % (archive, top, n) for n in CM_JS] +
-                [
-                    "unzip -p $(location %s) %s/addon/%s >>$@" % (archive, top, n)
-                    for n in CM_ADDONS
-                ],
-            ),
-            tools = [archive],
-            outs = ["cm%s.js" % suffix],
-        )
-
-        # Main CSS
-        genrule2(
-            name = "css" + suffix,
-            cmd = " && ".join(
-                [
-                    "echo '/** @license' >$@",
-                    "unzip -p $(location %s) %s/LICENSE >>$@" % (archive, top),
-                    "echo '*/' >>$@",
-                ] +
-                [
-                    "unzip -p $(location %s) %s/%s >>$@" % (archive, top, n)
-                    for n in CM_CSS
-                ],
-            ),
-            tools = [archive],
-            outs = ["cm%s.css" % suffix],
-        )
-
-        # Modes
-        for n in CM_MODES:
-            genrule2(
-                name = "mode_%s%s" % (n, suffix),
-                cmd = " && ".join(
-                    [
-                        "echo '/** @license' >$@",
-                        "unzip -p $(location %s) %s/LICENSE >>$@" % (archive, top),
-                        "echo '*/' >>$@",
-                        "unzip -p $(location %s) %s/mode/%s/%s.js >>$@" % (archive, top, n, n),
-                    ],
-                ),
-                tools = [archive],
-                outs = ["mode_%s%s.js" % (n, suffix)],
-            )
-
-        # Themes
-        for n in CM_THEMES:
-            genrule2(
-                name = "theme_%s%s" % (n, suffix),
-                cmd = " && ".join(
-                    [
-                        "echo '/** @license' >$@",
-                        "unzip -p $(location %s) %s/LICENSE >>$@" % (archive, top),
-                        "echo '*/' >>$@",
-                        "unzip -p $(location %s) %s/theme/%s.css >>$@" % (archive, top, n),
-                    ],
-                ),
-                tools = [archive],
-                outs = ["theme_%s%s.css" % (n, suffix)],
-            )
-
-        # Merge Addon bundled with diff-match-patch
-        genrule2(
-            name = "addon_merge_with_diff_match_patch%s" % suffix,
-            cmd = " && ".join(
-                [
-                    "echo '/** @license' >$@",
-                    "unzip -p $(location %s) %s/LICENSE >>$@" % (archive, top),
-                    "echo '*/\n' >>$@",
-                    "echo '// The google-diff-match-patch library is from https://repo1.maven.org/maven2/org/webjars/google-diff-match-patch/%s/google-diff-match-patch-%s.jar\n' >> $@" % (DIFF_MATCH_PATCH_VERSION, DIFF_MATCH_PATCH_VERSION),
-                    "echo '/** @license' >>$@",
-                    "echo 'LICENSE-Apache2.0' >>$@",
-                    "echo '*/' >>$@",
-                    "unzip -p $(location @diff-match-patch//jar) %s/diff_match_patch.js >>$@" % DIFF_MATCH_PATCH_TOP,
-                    "echo ';' >> $@",
-                    "unzip -p $(location %s) %s/addon/merge/merge.js >>$@" % (archive, top),
-                ],
-            ),
-            tools = [
-                "@diff-match-patch//jar",
-                # dependency just for license tracking.
-                ":diff-match-patch",
-                archive,
-                "//lib:LICENSE-Apache2.0",
-            ],
-            outs = ["addon_merge_with_diff_match_patch%s.js" % suffix],
-        )
-
-        # Jar packaging
-        genrule2(
-            name = "jar" + suffix,
-            cmd = " && ".join([
-                                  "cd $$TMP",
-                                  "mkdir -p net/codemirror/{addon,lib,mode,theme}",
-                                  "cp $$ROOT/$(location :css%s) net/codemirror/lib/cm.css" % suffix,
-                                  "cp $$ROOT/$(location :cm%s) net/codemirror/lib/cm.js" % suffix,
-                              ] +
-                              [
-                                  "cp $$ROOT/$(location :mode_%s%s) net/codemirror/mode/%s.js" % (n, suffix, n)
-                                  for n in CM_MODES
-                              ] +
-                              [
-                                  "cp $$ROOT/$(location :theme_%s%s) net/codemirror/theme/%s.css" % (n, suffix, n)
-                                  for n in CM_THEMES
-                              ] +
-                              ["cp $$ROOT/$(location :addon_merge_with_diff_match_patch%s) net/codemirror/addon/merge_bundled.js" % suffix] +
-                              ["zip -qr $$ROOT/$@ net/codemirror/{addon,lib,mode,theme}"]),
-            tools = [
-                ":addon_merge_with_diff_match_patch%s" % suffix,
-                ":cm%s" % suffix,
-                ":css%s" % suffix,
-            ] + [
-                ":mode_%s%s" % (n, suffix)
-                for n in CM_MODES
-            ] + [
-                ":theme_%s%s" % (n, suffix)
-                for n in CM_THEMES
-            ],
-            outs = ["codemirror%s.jar" % suffix],
-        )
-
-        java_import(
-            name = "codemirror" + suffix,
-            jars = [":jar%s" % suffix],
-            visibility = ["//visibility:public"],
-            data = [license],
-        )
diff --git a/lib/commons/BUILD b/lib/commons/BUILD
index 7cf2f1f..38b1b6d 100644
--- a/lib/commons/BUILD
+++ b/lib/commons/BUILD
@@ -46,6 +46,13 @@
 )
 
 java_library(
+    name = "text",
+    data = ["//lib:LICENSE-Apache2.0"],
+    visibility = ["//visibility:public"],
+    exports = ["@commons-text//jar"],
+)
+
+java_library(
     name = "validator",
     data = ["//lib:LICENSE-Apache2.0"],
     exports = ["@commons-validator//jar"],
diff --git a/lib/gitiles/BUILD b/lib/gitiles/BUILD
new file mode 100644
index 0000000..6e03801
--- /dev/null
+++ b/lib/gitiles/BUILD
@@ -0,0 +1,58 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
+java_library(
+    name = "gitiles",
+    visibility = ["//visibility:public"],
+    exports = [
+        ":cm-autolink",
+        ":commonmark",
+        ":gfm-strikethrough",
+        ":gfm-tables",
+        ":gitiles-servlet",
+        ":prettify",
+        "//lib/commons:lang3",
+        "//lib/commons:text",
+    ],
+)
+
+java_library(
+    name = "cm-autolink",
+    data = ["//lib:LICENSE-commonmark"],
+    visibility = ["//visibility:public"],
+    exports = ["@cm-autolink//jar"],
+)
+
+java_library(
+    name = "commonmark",
+    data = ["//lib:LICENSE-commonmark"],
+    visibility = ["//visibility:public"],
+    exports = ["@commonmark//jar"],
+)
+
+java_library(
+    name = "gfm-strikethrough",
+    data = ["//lib:LICENSE-Apache2.0"],
+    visibility = ["//visibility:public"],
+    exports = ["@gfm-strikethrough//jar"],
+)
+
+java_library(
+    name = "gfm-tables",
+    data = ["//lib:LICENSE-Apache2.0"],
+    visibility = ["//visibility:public"],
+    exports = ["@gfm-tables//jar"],
+)
+
+java_library(
+    name = "gitiles-servlet",
+    data = ["//lib:LICENSE-Apache2.0"],
+    visibility = ["//visibility:public"],
+    exports = ["@gitiles-servlet//jar"],
+)
+
+java_library(
+    name = "prettify",
+    data = ["//lib:LICENSE-Apache2.0"],
+    visibility = ["//visibility:public"],
+    exports = ["@prettify//jar"],
+)
diff --git a/lib/greenmail/BUILD b/lib/greenmail/BUILD
index 0587287..e8845e2 100644
--- a/lib/greenmail/BUILD
+++ b/lib/greenmail/BUILD
@@ -6,7 +6,7 @@
 
 java_library(
     name = "javax-activation",
-    testonly = 1,
+    testonly = True,
     data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"],
     exports = ["@javax-activation//jar"],
 )
diff --git a/lib/guava.bzl b/lib/guava.bzl
index 1add718..c36bf14 100644
--- a/lib/guava.bzl
+++ b/lib/guava.bzl
@@ -1,5 +1,5 @@
-GUAVA_VERSION = "26.0-jre"
+GUAVA_VERSION = "27.1-jre"
 
-GUAVA_BIN_SHA1 = "6a806eff209f36f635f943e16d97491f00f6bfab"
+GUAVA_BIN_SHA1 = "e47b59c893079b87743cdcfb6f17ca95c08c592c"
 
 GUAVA_DOC_URL = "https://google.github.io/guava/releases/" + GUAVA_VERSION + "/api/docs/"
diff --git a/lib/gwt/BUILD b/lib/gwt/BUILD
deleted file mode 100644
index 5606647..0000000
--- a/lib/gwt/BUILD
+++ /dev/null
@@ -1,47 +0,0 @@
-load("@rules_java//java:defs.bzl", "java_library")
-
-[java_library(
-    name = n,
-    data = ["//lib:LICENSE-Apache2.0"],
-    visibility = ["//visibility:public"],
-    exports = ["@%s//jar" % n],
-) for n in [
-    "ant",
-    "colt",
-    "dev",
-    "javax-validation",
-    "jsinterop-annotations",
-    "tapestry",
-    "user",
-    "w3c-css-sac",
-]]
-
-java_library(
-    name = "user-neverlink",
-    data = ["//lib:LICENSE-Apache2.0"],
-    neverlink = 1,
-    visibility = ["//visibility:public"],
-    exports = ["@user//jar"],
-)
-
-java_library(
-    name = "dev-neverlink",
-    data = ["//lib:LICENSE-Apache2.0"],
-    neverlink = 1,
-    visibility = ["//visibility:public"],
-    exports = ["@dev//jar"],
-)
-
-java_library(
-    name = "javax-validation_src",
-    data = ["//lib:LICENSE-Apache2.0"],
-    visibility = ["//visibility:public"],
-    exports = ["@javax-validation//jar:src"],
-)
-
-java_library(
-    name = "jsinterop-annotations_src",
-    data = ["//lib:LICENSE-Apache2.0"],
-    visibility = ["//visibility:public"],
-    exports = ["@jsinterop-annotations//jar:src"],
-)
diff --git a/lib/jetty/BUILD b/lib/jetty/BUILD
index 1664a5c..6417385 100644
--- a/lib/jetty/BUILD
+++ b/lib/jetty/BUILD
@@ -17,13 +17,6 @@
 )
 
 java_library(
-    name = "servlets",
-    data = ["//lib:LICENSE-Apache2.0"],
-    visibility = ["//visibility:public"],
-    exports = ["@jetty-servlets//jar"],
-)
-
-java_library(
     name = "server",
     data = ["//lib:LICENSE-Apache2.0"],
     visibility = ["//visibility:public"],
diff --git a/lib/jgit/jgit.bzl b/lib/jgit/jgit.bzl
index 22e3999..d296c19 100644
--- a/lib/jgit/jgit.bzl
+++ b/lib/jgit/jgit.bzl
@@ -1,6 +1,6 @@
 load("//tools/bzl:maven_jar.bzl", "MAVEN_CENTRAL", "maven_jar")
 
-_JGIT_VERS = "5.1.12.201910011832-r"
+_JGIT_VERS = "5.3.6.201910020505-r"
 
 _DOC_VERS = _JGIT_VERS  # Set to _JGIT_VERS unless using a snapshot
 
@@ -40,29 +40,25 @@
         name = "jgit-lib",
         artifact = "org.eclipse.jgit:org.eclipse.jgit:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "62c60aa985aa8dcfa6ad7308d130c319a1d01073",
-        src_sha1 = "6260138710f82d9084ab55e97aba8981e63c6ac2",
-        unsign = True,
+        sha1 = "0b9085bd16c630412bfa50bcc509dcedfbbe16d3",
     )
     maven_jar(
         name = "jgit-servlet",
         artifact = "org.eclipse.jgit:org.eclipse.jgit.http.server:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "7e32173b3d1958627950f1b675e5a3f1f35e9b53",
-        unsign = True,
+        sha1 = "d4e4d92f6faf44371f7cc3b53a6a97306ab3baaf",
     )
     maven_jar(
         name = "jgit-archive",
         artifact = "org.eclipse.jgit:org.eclipse.jgit.archive:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "c86ea9a5771bc966a3b0e4bba4c7113e5423c0f4",
+        sha1 = "168492930b95ba743e9c29354b9ff5f9f3dfa777",
     )
     maven_jar(
         name = "jgit-junit",
         artifact = "org.eclipse.jgit:org.eclipse.jgit.junit:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "2d957b5b515f9510792659277b5df049b6e795d8",
-        unsign = True,
+        sha1 = "5574bb089515d729cf66290553bfbc47eb37c213",
     )
 
 def jgit_dep(name):
@@ -70,7 +66,6 @@
         "@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-lib//jar:src": "@jgit//org.eclipse.jgit:libjgit-src.jar",
         "@jgit-servlet//jar": "@jgit//org.eclipse.jgit.http.server:jgit-servlet",
     }
 
diff --git a/lib/jgit/org.eclipse.jgit/BUILD b/lib/jgit/org.eclipse.jgit/BUILD
index d6e0c5d..c1f2607 100644
--- a/lib/jgit/org.eclipse.jgit/BUILD
+++ b/lib/jgit/org.eclipse.jgit/BUILD
@@ -12,12 +12,6 @@
     ],
 )
 
-alias(
-    name = "jgit-source",
-    actual = jgit_dep("@jgit-lib//jar:src"),
-    visibility = ["//visibility:public"],
-)
-
 java_library(
     name = "javaewah",
     data = ["//lib:LICENSE-Apache2.0"],
diff --git a/lib/js/bower_archives.bzl b/lib/js/bower_archives.bzl
index d84a2b6..60e857e 100644
--- a/lib/js/bower_archives.bzl
+++ b/lib/js/bower_archives.bzl
@@ -124,8 +124,8 @@
     bower_archive(
         name = "paper-icon-button",
         package = "PolymerElements/paper-icon-button",
-        version = "2.2.0",
-        sha1 = "9525e76ef433428bb9d6ec4fa65c4ef83156a803",
+        version = "2.2.1",
+        sha1 = "68f76af3a9379f256a3900a4b68d871898f1fe57",
     )
     bower_archive(
         name = "paper-ripple",
diff --git a/lib/mockito/BUILD b/lib/mockito/BUILD
new file mode 100644
index 0000000..cff36cd
--- /dev/null
+++ b/lib/mockito/BUILD
@@ -0,0 +1,37 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
+package(
+    default_testonly = True,
+    default_visibility = ["//visibility:private"],
+)
+
+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__"],
+    exports = ["@mockito//jar"],
+    runtime_deps = [
+        ":byte-buddy",
+        ":byte-buddy-agent",
+        ":objenesis",
+    ],
+)
+
+java_library(
+    name = "byte-buddy",
+    data = ["//lib:LICENSE-Apache2.0"],
+    exports = ["@byte-buddy//jar"],
+)
+
+java_library(
+    name = "byte-buddy-agent",
+    data = ["//lib:LICENSE-Apache2.0"],
+    exports = ["@byte-buddy-agent//jar"],
+)
+
+java_library(
+    name = "objenesis",
+    data = ["//lib:LICENSE-Apache2.0"],
+    exports = ["@objenesis//jar"],
+)
diff --git a/package.json b/package.json
index b200b9c..1e6a9c2 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "gerrit",
-  "version": "2.16.16-SNAPSHOT",
+  "version": "3.0.7-SNAPSHOT",
   "description": "Gerrit Code Review",
   "dependencies": {},
   "devDependencies": {
diff --git a/plugins/BUILD b/plugins/BUILD
index 345d50d..3e93768 100644
--- a/plugins/BUILD
+++ b/plugins/BUILD
@@ -32,15 +32,19 @@
     "//antlr3:query_parser",
     "//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/git",
     "//java/com/google/gerrit/index",
     "//java/com/google/gerrit/index/project",
     "//java/com/google/gerrit/index:query_exception",
+    "//java/com/google/gerrit/json",
     "//java/com/google/gerrit/lifecycle",
     "//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/server/api",
     "//java/com/google/gerrit/server/audit",
     "//java/com/google/gerrit/server/cache/mem",
     "//java/com/google/gerrit/server/cache/serialize",
@@ -49,6 +53,8 @@
     "//java/com/google/gerrit/server/util/time",
     "//java/com/google/gerrit/util/cli",
     "//java/com/google/gerrit/util/http",
+    "//lib/antlr:java-runtime",
+    "//lib/auto:auto-value-annotations",
     "//lib/commons:compress",
     "//lib/commons:dbcp",
     "//lib/commons:lang",
@@ -76,7 +82,6 @@
     "//lib:guava",
     "//lib:guava-retrying",
     "//lib:gson",
-    "//lib:gwtorm",
     "//lib:icu4j",
     "//lib:jsch",
     "//lib:mime-util",
diff --git a/plugins/delete-project b/plugins/delete-project
new file mode 160000
index 0000000..0ddc5bb
--- /dev/null
+++ b/plugins/delete-project
@@ -0,0 +1 @@
+Subproject commit 0ddc5bbe3bca127d9bba8a59ba7bf5a72094aed9
diff --git a/plugins/gitiles b/plugins/gitiles
new file mode 160000
index 0000000..4ba2cc4
--- /dev/null
+++ b/plugins/gitiles
@@ -0,0 +1 @@
+Subproject commit 4ba2cc4ea8d46e1e8050b46d62c654b8772b969a
diff --git a/plugins/hooks b/plugins/hooks
index 57c8e62..072ee21 160000
--- a/plugins/hooks
+++ b/plugins/hooks
@@ -1 +1 @@
-Subproject commit 57c8e6261016e1cc233ea41f19fa5bdf187df8a7
+Subproject commit 072ee210f38f4c1bdc55330e88dc844b4daad6ca
diff --git a/plugins/plugin-manager b/plugins/plugin-manager
new file mode 160000
index 0000000..9edc195
--- /dev/null
+++ b/plugins/plugin-manager
@@ -0,0 +1 @@
+Subproject commit 9edc1950d3c0a3717cefda1688a4c37e7fc652fb
diff --git a/plugins/replication b/plugins/replication
index ba4eabb..707a917 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit ba4eabb68bdaeca1b5c2adb0c1585a77a8a4fe84
+Subproject commit 707a917c1d985afaa1dfeb9c6381888d1ab0cf8a
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index 848e2d9..c87afe7 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit 848e2d91a5f9257bf6a92a133f2baf0d563b7462
+Subproject commit c87afe76b180f6554dd5ad3c12203456ed9aab3d
diff --git a/plugins/singleusergroup b/plugins/singleusergroup
index 2a38a84..8a3b6fa 160000
--- a/plugins/singleusergroup
+++ b/plugins/singleusergroup
@@ -1 +1 @@
-Subproject commit 2a38a8413c25fec910fb843932aa869647bdc1e7
+Subproject commit 8a3b6faeaebc0f9b7d1af19eb0022b32994e476a
diff --git a/plugins/webhooks b/plugins/webhooks
new file mode 160000
index 0000000..7d0e904
--- /dev/null
+++ b/plugins/webhooks
@@ -0,0 +1 @@
+Subproject commit 7d0e904ba6252777e6bc0ece1512da6ad5892461
diff --git a/polygerrit-ui/BUILD b/polygerrit-ui/BUILD
index 3988f95..0e9b4bb 100644
--- a/polygerrit-ui/BUILD
+++ b/polygerrit-ui/BUILD
@@ -57,11 +57,8 @@
     data = [
         ":fonts.zip",
         "//polygerrit-ui/app:test_components.zip",
-        "//resources/com/google/gerrit/httpd/raw",
     ],
     deps = [
-        "@com_github_robfig_soy//:go_default_library",
-        "@com_github_robfig_soy//soyhtml:go_default_library",
         "@org_golang_x_tools//godoc/vfs/httpfs:go_default_library",
         "@org_golang_x_tools//godoc/vfs/zipfs:go_default_library",
     ],
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index 04a13e5..4dbe146 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -62,9 +62,9 @@
 that serves PolyGerrit:
 
 ```sh
-bazel build polygerrit &&
+bazel build gerrit &&
   $(bazel info output_base)/external/local_jdk/bin/java -DsourceRoot=/path/to/my/checkout \
-  -jar bazel-bin/polygerrit.war daemon --polygerrit-dev \
+  -jar bazel-bin/gerrit.war daemon --polygerrit-dev \
   -d ../gerrit_testsite --console-log --show-stack-trace
 ```
 
diff --git a/polygerrit-ui/app/.eslintrc.json b/polygerrit-ui/app/.eslintrc.json
index c481fd0..fb1a5b3 100644
--- a/polygerrit-ui/app/.eslintrc.json
+++ b/polygerrit-ui/app/.eslintrc.json
@@ -85,7 +85,6 @@
     ],
     "prefer-arrow-callback": "error",
     "prefer-const": "error",
-    "prefer-promise-reject-errors": "off",
     "prefer-spread": "error",
     "quote-props": ["error", "consistent-as-needed"],
     "semi": [2, "always"],
diff --git a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.html b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.html
index 48bd10e..1748647 100644
--- a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.html
+++ b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.html
@@ -21,27 +21,12 @@
 
   window.Gerrit = window.Gerrit || {};
 
-  const PROJECT_DASHBOARD_PATTERN = /\/p\/(.+)\/\+\/dashboard\/(.*)/;
-  const REPO_URL_PATTERN = /^\/admin\/repos/;
-  const PROJECT_URL = '/admin/projects';
   /** @polymerBehavior Gerrit.BaseUrlBehavior */
   Gerrit.BaseUrlBehavior = {
     /** @return {string} */
     getBaseUrl() {
       return window.CANONICAL_PATH || '';
     },
-
-    computeGwtUrl(path) {
-      const base = this.getBaseUrl();
-      let clientPath = path.substring(base.length);
-      const match = clientPath.match(PROJECT_DASHBOARD_PATTERN);
-      if (match) {
-        clientPath = `/projects/${match[1]},dashboards/${match[2]}`;
-      }
-      // Replace any '/admin/project' links to '/admin/repo'
-      clientPath = clientPath.replace(REPO_URL_PATTERN, PROJECT_URL);
-      return base + '/?polygerrit=0#' + clientPath;
-    },
   };
 })(window);
 </script>
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 429abe1..c21e96f 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
@@ -52,6 +52,7 @@
       // Define a Polymer element that uses this behavior.
       Polymer({
         is: 'test-element',
+        _legacyUndefinedCheck: true,
         behaviors: [
           Gerrit.BaseUrlBehavior,
         ],
@@ -66,24 +67,5 @@
     test('getBaseUrl', () => {
       assert.deepEqual(element.getBaseUrl(), '/r');
     });
-
-    test('computeGwtUrl', () => {
-      assert.deepEqual(
-          element.computeGwtUrl('/r/c/1/'),
-          '/r/?polygerrit=0#/c/1/'
-      );
-    });
-
-    test('computeGwtUrl for project dashboard', () => {
-      assert.deepEqual(
-          element.computeGwtUrl('/r/p/gerrit/proj/+/dashboard/main:default'),
-          '/r/?polygerrit=0#/projects/gerrit/proj,dashboards/main:default');
-    });
-
-    test('computeGwtUrl for project access', () => {
-      assert.deepEqual(
-          element.computeGwtUrl('/r/admin/repos/demo-project,access'),
-          '/r/?polygerrit=0#/admin/projects/demo-project,access');
-    });
   });
 </script>
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 e92eb49..96d4a08 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
@@ -38,6 +38,7 @@
       // 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 640d902..e445a78 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
@@ -46,6 +46,7 @@
       // Define a Polymer element that uses this behavior.
       Polymer({
         is: 'test-element',
+        _legacyUndefinedCheck: true,
         behaviors: [Gerrit.DomUtilBehavior],
       });
     });
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 111662b..530c76c 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
@@ -120,6 +120,10 @@
             id: 'submitAs',
             name: 'Submit (On Behalf Of)',
           },
+          toggleWipState: {
+            id: 'toggleWipState',
+            name: 'Toggle Work In Progress State',
+          },
           viewPrivateChanges: {
             id: 'viewPrivateChanges',
             name: 'View Private Changes',
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 929834f..0b37a0d 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
@@ -38,6 +38,7 @@
       // 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.html b/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.html
index db11937..182d242 100644
--- a/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.html
@@ -18,8 +18,6 @@
 (function(window) {
   'use strict';
 
-  const ACCOUNT_CAPABILITIES = ['createProject', 'createGroup', 'viewPlugins'];
-
   const ADMIN_LINKS = [{
     name: 'Repositories',
     noBaseUrl: true,
@@ -65,7 +63,7 @@
         return Promise.resolve(this._filterLinks(link => link.viewableToAll,
             getAdminMenuLinks, opt_options));
       }
-      return getAccountCapabilities(ACCOUNT_CAPABILITIES)
+      return getAccountCapabilities()
           .then(capabilities => {
             return this._filterLinks(link => {
               return !link.capability ||
@@ -97,9 +95,10 @@
       links.push(...getAdminMenuLinks().map(link => ({
         url: link.url,
         name: link.text,
+        capability: link.capability || null,
         noBaseUrl: !isExernalLink(link),
         view: null,
-        viewableToAll: true,
+        viewableToAll: !link.capability,
         target: isExernalLink(link) ? '_blank' : null,
       })));
 
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 a1902cd..d217577 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
@@ -41,6 +41,7 @@
       // Define a Polymer element that uses this behavior.
       Polymer({
         is: 'test-element',
+        _legacyUndefinedCheck: true,
         behaviors: [
           Gerrit.AdminNavBehavior,
         ],
@@ -307,5 +308,61 @@
         testAdminLinks(account, options, expected, done);
       });
     });
+
+
+    suite('view plugin screen with plugin capability', () => {
+      const account = {
+        name: 'test-user',
+      };
+      let expected;
+
+      setup(() => {
+        capabilityStub.returns(Promise.resolve({pluginCapability: true}));
+        expected = {};
+      });
+
+      test('with plugin with capabilities', done => {
+        let options;
+        const generatedLinks = [
+          {text: 'without capability', url: '/without'},
+          {text: 'with capability', url: '/with', capability: 'pluginCapability'},
+        ];
+        menuLinkStub.returns(generatedLinks);
+        expected = Object.assign(expected, {
+          totalLength: 4,
+          pluginGeneratedLinks: generatedLinks,
+        });
+        testAdminLinks(account, options, expected, done);
+      });
+    });
+
+
+    suite('view plugin screen without plugin capability', () => {
+      const account = {
+        name: 'test-user',
+      };
+      let expected;
+
+      setup(() => {
+        capabilityStub.returns(Promise.resolve({}));
+        expected = {};
+      });
+
+      test('with plugin with capabilities', done => {
+        let options;
+        const generatedLinks = [
+          {text: 'without capability', url: '/without'},
+          {text: 'with capability',
+            url: '/with',
+            capability: 'pluginCapability'},
+        ];
+        menuLinkStub.returns(generatedLinks);
+        expected = Object.assign(expected, {
+          totalLength: 3,
+          pluginGeneratedLinks: [generatedLinks[0]],
+        });
+        testAdminLinks(account, options, expected, done);
+      });
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior_test.html b/polygerrit-ui/app/behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior_test.html
index 3ac94fe..820d6bc 100644
--- a/polygerrit-ui/app/behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior_test.html
@@ -44,6 +44,7 @@
       // Define a Polymer element that uses this behavior.
       Polymer({
         is: 'test-element-anon',
+        _legacyUndefinedCheck: true,
         behaviors: [
           Gerrit.AnonymousNameBehavior,
         ],
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 e8cbb09..b052d06 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
@@ -48,6 +48,7 @@
       // 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-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 54b979f..f6c765f 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
@@ -40,6 +40,7 @@
       // Define a Polymer element that uses this behavior.
       Polymer({
         is: 'test-element',
+        _legacyUndefinedCheck: true,
         behaviors: [Gerrit.ListViewBehavior],
       });
     });
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 31a51a9..a4c1e86 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
@@ -238,7 +238,8 @@
       return restAPI.getChangeDetail(change._number)
           .then(detail => {
             if (!detail) {
-              return Promise.reject('Unable to check for latest patchset.');
+              const error = new Error('Unable to check for latest patchset.');
+              return Promise.reject(error);
             }
             const actualLatest = Gerrit.PatchSetBehavior.computeLatestPatchNum(
                 Gerrit.PatchSetBehavior.computeAllPatchSets(detail));
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
new file mode 100644
index 0000000..2dc070d
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/gr-repo-plugin-config-behavior/gr-repo-plugin-config-behavior.html
@@ -0,0 +1,38 @@
+<!--
+@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 this */
+  Gerrit.RepoPluginConfig = {
+    // Should be kept in sync with
+    // gerrit/java/com/google/gerrit/extensions/api/projects/ProjectConfigEntryType.java.
+    ENTRY_TYPES: {
+      ARRAY: 'ARRAY',
+      BOOLEAN: 'BOOLEAN',
+      INT: 'INT',
+      LIST: 'LIST',
+      LONG: 'LONG',
+      STRING: 'STRING',
+    },
+    PLUGIN_CONFIG_CHANGED: 'plugin-config-changed',
+  };
+})(window);
+</script>
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 8bca339..943e000 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
@@ -51,6 +51,7 @@
       // 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 73dda1b..d909e86 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
@@ -40,6 +40,7 @@
       // 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 3e99d44..df7f3cf 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
@@ -91,7 +91,7 @@
 element. An example of this is in comment threads. A diff view supports actions
 on comment threads, but there may be zero or many comment threads attached at
 any given point. So the shortcut is declared as doc-only by the diff view and
-by gr-app, and actually implemented by gr-diff-comment-thread.
+by gr-app, and actually implemented by gr-comment-thread.
 
 NOTE: doc-only shortcuts will not be customizable in the same way that other
 shortcuts are.
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 15d31b7..b4451fe 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
@@ -50,6 +50,7 @@
       // 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_test.html b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html
index 49d90f0..6af43dc 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
@@ -54,6 +54,7 @@
       // 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 bc16b39..6e040a3 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
@@ -39,6 +39,7 @@
     suiteSetup(() => {
       Polymer({
         is: 'safe-types-element',
+        _legacyUndefinedCheck: true,
         behaviors: [Gerrit.SafeTypes],
       });
     });
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 409fab4..71f8d26 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,6 +39,7 @@
 
   Polymer({
     is: 'gr-access-section',
+    _legacyUndefinedCheck: true,
 
     properties: {
       capabilities: Object,
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 f1c27af..9db2e34 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-admin-group-list',
+    _legacyUndefinedCheck: true,
 
     properties: {
       /**
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 fc04c34..4d964d7 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,6 +22,7 @@
 
   Polymer({
     is: 'gr-admin-view',
+    _legacyUndefinedCheck: true,
 
     properties: {
       /** @type {?} */
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 56079e3..ff25377 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
@@ -158,6 +158,7 @@
       return element.reload().then(() => {
         assert.equal(element._filteredLinks.length, 3);
         assert.deepEqual(element._filteredLinks[1], {
+          capability: null,
           url: '/internal/link/url',
           name: 'internal link text',
           noBaseUrl: true,
@@ -166,6 +167,7 @@
           target: null,
         });
         assert.deepEqual(element._filteredLinks[2], {
+          capability: null,
           url: 'http://external/link/url',
           name: 'external link text',
           noBaseUrl: false,
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 b5dcf63..9c0e405 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,6 +25,7 @@
 
   Polymer({
     is: 'gr-confirm-delete-item-dialog',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the confirm button is pressed.
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 c6687dd..5a4c064 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,6 +22,7 @@
 
   Polymer({
     is: 'gr-create-change-dialog',
+    _legacyUndefinedCheck: true,
 
     properties: {
       repoName: String,
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 01aeb43..ec667ee 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-create-group-dialog',
+    _legacyUndefinedCheck: true,
 
     properties: {
       params: Object,
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 4e9da90..65bb46d 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,6 +24,7 @@
 
   Polymer({
     is: 'gr-create-pointer-dialog',
+    _legacyUndefinedCheck: true,
 
     properties: {
       detailType: String,
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 bb2b5f2..ef7edd4 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-create-repo-dialog',
+    _legacyUndefinedCheck: true,
 
     properties: {
       params: Object,
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 bc6a5d0..966f3c9 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,6 +21,7 @@
 
   Polymer({
     is: 'gr-group-audit-log',
+    _legacyUndefinedCheck: true,
 
     properties: {
       groupId: String,
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 b7917be..b2784e5 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,6 +25,7 @@
 
   Polymer({
     is: 'gr-group-members',
+    _legacyUndefinedCheck: true,
 
     properties: {
       groupId: Number,
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 ef22daf..45d5497 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
@@ -233,9 +233,10 @@
       const memberName = 'bad-name';
       const alertStub = sandbox.stub();
       element.addEventListener('show-alert', alertStub);
-
+      const error = new Error('error');
+      error.status = 404;
       sandbox.stub(element.$.restAPI, 'saveGroupMembers',
-          () => Promise.reject({status: 404}));
+          () => Promise.reject(error));
 
       element.$.groupMemberSearchInput.text = memberName;
       element.$.groupMemberSearchInput.value = 1234;
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 de16a7c..2d7f9cb 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
@@ -32,6 +32,7 @@
 
   Polymer({
     is: 'gr-group',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the group name changes.
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 e33c1ec..58a006d 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
@@ -38,6 +38,7 @@
 
   Polymer({
     is: 'gr-permission',
+    _legacyUndefinedCheck: true,
 
     properties: {
       labels: Object,
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
new file mode 100644
index 0000000..ca98c50
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.html
@@ -0,0 +1,100 @@
+<!--
+@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.
+-->
+
+<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="../../../styles/gr-form-styles.html">
+<link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../shared/gr-button/gr-button.html">
+
+<dom-module id="gr-plugin-config-array-editor">
+  <template>
+    <style include="shared-styles"></style>
+    <style include="gr-form-styles">
+      .wrapper {
+        width: 30em;
+      }
+      .existingItems {
+        background: var(--table-header-background-color);
+        border: 1px solid var(--border-color);
+        border-radius: 2px;
+      }
+      gr-button {
+        float: right;
+        margin-left: .5em;
+        width: 4.5em;
+      }
+      .row {
+        align-items: center;
+        display: flex;
+        justify-content: space-between;
+        padding: .5em 0;
+        width: 100%;
+      }
+      .existingItems .row {
+        padding: .5em;
+      }
+      .existingItems .row:not(:first-of-type) {
+        border-top: 1px solid var(--border-color);
+      }
+      input {
+        flex-grow: 1;
+      }
+      .hide {
+        display: none;
+      }
+      .placeholder {
+        color: var(--deemphasized-text-color);
+        padding-top: .75em;
+      }
+    </style>
+    <div class="wrapper gr-form-styles">
+      <template is="dom-if" if="[[pluginOption.info.values.length]]">
+        <div class="existingItems">
+          <template is="dom-repeat" items="[[pluginOption.info.values]]">
+            <div class="row">
+              <span>[[item]]</span>
+              <gr-button
+                  link
+                  disabled$="[[disabled]]"
+                  data-item="[[item]]"
+                  on-tap="_handleDelete">Delete</gr-button>
+            </div>
+          </template>
+        </div>
+      </template>
+      <template is="dom-if" if="[[!pluginOption.info.values.length]]">
+        <div class="row placeholder">None configured.</div>
+      </template>
+      <div class$="row [[_computeShowInputRow(disabled)]]">
+        <input
+            is="iron-input"
+            id="input"
+            on-keydown="_handleInputKeydown"
+            bind-value="{{_newValue}}"/>
+        <gr-button
+            id="addButton"
+            disabled$="[[!_newValue.length]]"
+            link
+            on-tap="_handleAddTap">Add</gr-button>
+      </div>
+    </div>
+  </template>
+  <script src="gr-plugin-config-array-editor.js"></script>
+</dom-module>
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
new file mode 100644
index 0000000..ab4d286
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.js
@@ -0,0 +1,90 @@
+/**
+ * @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.
+ */
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-plugin-config-array-editor',
+
+    /**
+     * Fired when the plugin config option changes.
+     *
+     * @event plugin-config-option-changed
+     */
+
+    properties: {
+      /** @type {?} */
+      pluginOption: Object,
+      /** @type {Boolean} */
+      disabled: {
+        type: Boolean,
+        computed: '_computeDisabled(pluginOption.*)',
+      },
+      /** @type {?} */
+      _newValue: {
+        type: String,
+        value: '',
+      },
+    },
+
+    _computeDisabled(record) {
+      return !(record && record.base && record.base.info &&
+          record.base.info.editable);
+    },
+
+    _handleAddTap(e) {
+      e.preventDefault();
+      this._handleAdd();
+    },
+
+    _handleInputKeydown(e) {
+      // Enter.
+      if (e.keyCode === 13) {
+        e.preventDefault();
+        this._handleAdd();
+      }
+    },
+
+    _handleAdd() {
+      if (!this._newValue.length) { return; }
+      this._dispatchChanged(
+          this.pluginOption.info.values.concat([this._newValue]));
+      this._newValue = '';
+    },
+
+    _handleDelete(e) {
+      const value = Polymer.dom(e).localTarget.dataItem;
+      this._dispatchChanged(
+          this.pluginOption.info.values.filter(str => str !== value));
+    },
+
+    _dispatchChanged(values) {
+      const {_key, info} = this.pluginOption;
+      const detail = {
+        _key,
+        info: Object.assign(info, {values}, {}),
+        notifyPath: `${_key}.values`,
+      };
+      this.dispatchEvent(
+          new CustomEvent('plugin-config-option-changed', {detail}));
+    },
+
+    _computeShowInputRow(disabled) {
+      return disabled ? 'hide' : '';
+    },
+  });
+})();
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
new file mode 100644
index 0000000..dc3f67e
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.html
@@ -0,0 +1,142 @@
+<!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-plugin-config-array-editor</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"/>
+<link rel="import" href="gr-plugin-config-array-editor.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-plugin-config-array-editor></gr-plugin-config-array-editor>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-plugin-config-array-editor tests', () => {
+    let element;
+    let sandbox;
+    let dispatchStub;
+
+    const getAll = str => Polymer.dom(element.root).querySelectorAll(str);
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+      element = fixture('basic');
+      element.pluginOption = {
+        _key: 'test-key',
+        info: {
+          values: [],
+        },
+      };
+    });
+
+    teardown(() => sandbox.restore());
+
+    test('_computeShowInputRow', () => {
+      assert.equal(element._computeShowInputRow(true), 'hide');
+      assert.equal(element._computeShowInputRow(false), '');
+    });
+
+    test('_computeDisabled', () => {
+      assert.isTrue(element._computeDisabled({}));
+      assert.isTrue(element._computeDisabled({base: {}}));
+      assert.isTrue(element._computeDisabled({base: {info: {}}}));
+      assert.isTrue(
+          element._computeDisabled({base: {info: {editable: false}}}));
+      assert.isFalse(
+          element._computeDisabled({base: {info: {editable: true}}}));
+    });
+
+    suite('adding', () => {
+      setup(() => {
+        dispatchStub = sandbox.stub(element, '_dispatchChanged');
+      });
+
+      test('with enter', () => {
+        element._newValue = '';
+        MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter
+        flushAsynchronousOperations();
+
+        assert.isFalse(dispatchStub.called);
+        element._newValue = 'test';
+        MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter
+        flushAsynchronousOperations();
+
+        assert.isTrue(dispatchStub.called);
+        assert.equal(dispatchStub.lastCall.args[0], 'test');
+        assert.equal(element._newValue, '');
+      });
+
+      test('with add btn', () => {
+        element._newValue = '';
+        MockInteractions.tap(element.$.addButton);
+        flushAsynchronousOperations();
+
+        assert.isFalse(dispatchStub.called);
+        element._newValue = 'test';
+        MockInteractions.tap(element.$.addButton);
+        flushAsynchronousOperations();
+
+        assert.isTrue(dispatchStub.called);
+        assert.equal(dispatchStub.lastCall.args[0], 'test');
+        assert.equal(element._newValue, '');
+      });
+    });
+
+    test('deleting', () => {
+      dispatchStub = sandbox.stub(element, '_dispatchChanged');
+      element.pluginOption = {info: {values: ['test', 'test2']}};
+      flushAsynchronousOperations();
+
+      const rows = getAll('.existingItems .row');
+      assert.equal(rows.length, 2);
+      const button = rows[0].querySelector('gr-button');
+
+      MockInteractions.tap(button);
+      flushAsynchronousOperations();
+
+      assert.isFalse(dispatchStub.called);
+      element.pluginOption.info.editable = true;
+      element.notifyPath('pluginOption.info.editable');
+      flushAsynchronousOperations();
+
+      MockInteractions.tap(button);
+      flushAsynchronousOperations();
+
+      assert.isTrue(dispatchStub.called);
+      assert.deepEqual(dispatchStub.lastCall.args[0], ['test2']);
+    });
+
+    test('_dispatchChanged', () => {
+      const eventStub = sandbox.stub(element, 'dispatchEvent');
+      element._dispatchChanged(['new-test-value']);
+
+      assert.isTrue(eventStub.called);
+      const {detail} = eventStub.lastCall.args[0];
+      assert.equal(detail._key, 'test-key');
+      assert.deepEqual(detail.info, {values: ['new-test-value']});
+      assert.equal(detail.notifyPath, 'test-key.values');
+    });
+  });
+</script>
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 7fd3e05..d6484d8 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-plugin-list',
+    _legacyUndefinedCheck: true,
 
     properties: {
       /**
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 f7ddb23..1b8b322 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,6 +70,7 @@
 
   Polymer({
     is: 'gr-repo-access',
+    _legacyUndefinedCheck: true,
 
     properties: {
       repo: {
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 bcdb7f6..e0becaf 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-repo-command',
+    _legacyUndefinedCheck: true,
 
     properties: {
       title: String,
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 17d89a8..09c9026 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,6 +28,7 @@
 
   Polymer({
     is: 'gr-repo-commands',
+    _legacyUndefinedCheck: true,
 
     properties: {
       params: Object,
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 a620ba1..f72e986 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-repo-dashboards',
+    _legacyUndefinedCheck: true,
 
     properties: {
       repo: {
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 c576d7e..e4000e0 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,6 +26,7 @@
 
   Polymer({
     is: 'gr-repo-detail-list',
+    _legacyUndefinedCheck: true,
 
     properties: {
       /**
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 0eaa496..d4eb29b 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-repo-list',
+    _legacyUndefinedCheck: true,
 
     properties: {
       /**
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
new file mode 100644
index 0000000..7f2cbe7
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.html
@@ -0,0 +1,109 @@
+<!--
+@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.
+-->
+
+<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">
+<link rel="import" href="../../../styles/gr-subpage-styles.html">
+<link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../shared/gr-icons/gr-icons.html">
+<link rel="import" href="../../shared/gr-select/gr-select.html">
+<link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html">
+<link rel="import" href="../gr-plugin-config-array-editor/gr-plugin-config-array-editor.html">
+
+<dom-module id="gr-repo-plugin-config">
+  <template>
+    <style include="shared-styles"></style>
+    <style include="gr-form-styles"></style>
+    <style include="gr-subpage-styles">
+      .inherited {
+        color: var(--deemphasized-text-color);
+        margin-left: .5em;
+      }
+      section.section:not(.ARRAY) .title {
+        align-items: center;
+        display: flex;
+      }
+      section.section.ARRAY .title {
+        padding-top: .75em;
+      }
+    </style>
+    <div class="gr-form-styles">
+      <fieldset>
+        <h4>[[pluginData.name]]</h4>
+        <template is="dom-repeat" items="[[_pluginConfigOptions]]" as="option">
+          <section class$="section [[option.info.type]]">
+            <span class="title">
+              <gr-tooltip-content
+                  has-tooltip="[[option.info.description]]"
+                  show-icon="[[option.info.description]]"
+                  title="[[option.info.description]]">
+                <span>[[option.info.display_name]]</span>
+              </gr-tooltip-content>
+            </span>
+            <span class="value">
+              <template is="dom-if" if="[[_isArray(option.info.type)]]">
+                <gr-plugin-config-array-editor
+                    on-plugin-config-option-changed="_handleArrayChange"
+                    plugin-option="[[option]]"></gr-plugin-config-array-editor>
+              </template>
+              <template is="dom-if" if="[[_isBoolean(option.info.type)]]">
+                <paper-toggle-button
+                    checked="[[_computeChecked(option.info.value)]]"
+                    on-change="_handleBooleanChange"
+                    data-option-key$="[[option._key]]"
+                    disabled$="[[_computeDisabled(option.info.editable)]]"></paper-toggle-button>
+              </template>
+              <template is="dom-if" if="[[_isList(option.info.type)]]">
+                <gr-select
+                    bind-value$="[[option.info.value]]"
+                    on-change="_handleListChange">
+                  <select
+                      data-option-key$="[[option._key]]"
+                      disabled$="[[_computeDisabled(option.info.editable)]]">
+                    <template is="dom-repeat"
+                        items="[[option.info.permitted_values]]"
+                        as="value">
+                      <option value$="[[value]]">[[value]]</option>
+                    </template>
+                  </select>
+                </gr-select>
+              </template>
+              <template is="dom-if" if="[[_isString(option.info.type)]]">
+                <input
+                    is="iron-input"
+                    value="[[option.info.value]]"
+                    on-input="_handleStringChange"
+                    data-option-key$="[[option._key]]"
+                    disabled$="[[_computeDisabled(option.info.editable)]]"></input>
+              </template>
+              <template is="dom-if" if="[[option.info.inherited_value]]">
+                <span class="inherited">
+                  (Inherited: [[option.info.inherited_value]])
+                </span>
+              </template>
+            </span>
+          </section>
+        </template>
+      </fieldset>
+    </div>
+  </template>
+  <script src="gr-repo-plugin-config.js"></script>
+</dom-module>
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
new file mode 100644
index 0000000..6d7677e
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.js
@@ -0,0 +1,130 @@
+/**
+ * @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.
+ */
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-repo-plugin-config',
+
+    /**
+     * Fired when the plugin config changes.
+     *
+     * @event plugin-config-changed
+     */
+
+    properties: {
+      /** @type {?} */
+      pluginData: Object,
+      /** @type {Array} */
+      _pluginConfigOptions: {
+        type: Array,
+        computed: '_computePluginConfigOptions(pluginData.*)',
+      },
+    },
+
+    behaviors: [
+      Gerrit.RepoPluginConfig,
+    ],
+
+    _computePluginConfigOptions(dataRecord) {
+      if (!dataRecord || !dataRecord.base || !dataRecord.base.config) {
+        return [];
+      }
+      const {config} = dataRecord.base;
+      return Object.keys(config).map(_key => ({_key, info: config[_key]}));
+    },
+
+    _isArray(type) {
+      return type === this.ENTRY_TYPES.ARRAY;
+    },
+
+    _isBoolean(type) {
+      return type === this.ENTRY_TYPES.BOOLEAN;
+    },
+
+    _isList(type) {
+      return type === this.ENTRY_TYPES.LIST;
+    },
+
+    _isString(type) {
+      // Treat numbers like strings for simplicity.
+      return type === this.ENTRY_TYPES.STRING ||
+          type === this.ENTRY_TYPES.INT ||
+          type === this.ENTRY_TYPES.LONG;
+    },
+
+    _computeDisabled(editable) {
+      return editable === 'false';
+    },
+
+    _computeChecked(value) {
+      return JSON.parse(value);
+    },
+
+    _handleStringChange(e) {
+      const el = Polymer.dom(e).localTarget;
+      const _key = el.getAttribute('data-option-key');
+      const configChangeInfo =
+          this._buildConfigChangeInfo(el.value, _key);
+      this._handleChange(configChangeInfo);
+    },
+
+    _handleListChange(e) {
+      const el = Polymer.dom(e).localTarget;
+      const _key = el.getAttribute('data-option-key');
+      const configChangeInfo =
+          this._buildConfigChangeInfo(el.value, _key);
+      this._handleChange(configChangeInfo);
+    },
+
+    _handleBooleanChange(e) {
+      const el = Polymer.dom(e).localTarget;
+      const _key = el.getAttribute('data-option-key');
+      const configChangeInfo =
+          this._buildConfigChangeInfo(JSON.stringify(el.checked), _key);
+      this._handleChange(configChangeInfo);
+    },
+
+    _buildConfigChangeInfo(value, _key) {
+      const info = this.pluginData.config[_key];
+      info.value = value;
+      return {
+        _key,
+        info,
+        notifyPath: `${_key}.value`,
+      };
+    },
+
+    _handleArrayChange({detail}) {
+      this._handleChange(detail);
+    },
+
+    _handleChange({_key, info, notifyPath}) {
+      const {name, config} = this.pluginData;
+
+      /** @type {Object} */
+      const detail = {
+        name,
+        config: Object.assign(config, {[_key]: info}, {}),
+        notifyPath: `${name}.${notifyPath}`,
+      };
+
+      this.dispatchEvent(new CustomEvent(this.PLUGIN_CONFIG_CHANGED,
+          {detail, bubbles: 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
new file mode 100644
index 0000000..ba0c876
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.html
@@ -0,0 +1,175 @@
+<!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-repo-plugin-config</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"/>
+<link rel="import" href="gr-repo-plugin-config.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-repo-plugin-config></gr-repo-plugin-config>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-repo-plugin-config tests', () => {
+    let element;
+    let sandbox;
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+      element = fixture('basic');
+    });
+
+    teardown(() => sandbox.restore());
+
+    test('_computePluginConfigOptions', () => {
+      assert.deepEqual(element._computePluginConfigOptions(), []);
+      assert.deepEqual(element._computePluginConfigOptions({}), []);
+      assert.deepEqual(element._computePluginConfigOptions({base: {}}), []);
+      assert.deepEqual(element._computePluginConfigOptions(
+          {base: {config: {}}}), []);
+      assert.deepEqual(element._computePluginConfigOptions(
+          {base: {config: {testKey: 'testInfo'}}}),
+      [{_key: 'testKey', info: 'testInfo'}]);
+    });
+
+    test('_computeDisabled', () => {
+      assert.isFalse(element._computeDisabled('true'));
+      assert.isTrue(element._computeDisabled('false'));
+    });
+
+    test('_handleChange', () => {
+      const eventStub = sandbox.stub(element, 'dispatchEvent');
+      element.pluginData = {
+        name: 'testName',
+        config: {plugin: {value: 'test'}},
+      };
+      element._handleChange({
+        _key: 'plugin',
+        info: {value: 'newTest'},
+        notifyPath: 'plugin.value',
+      });
+
+      assert.isTrue(eventStub.called);
+
+      const {detail} = eventStub.lastCall.args[0];
+      assert.equal(detail.name, 'testName');
+      assert.deepEqual(detail.config, {plugin: {value: 'newTest'}});
+      assert.equal(detail.notifyPath, 'testName.plugin.value');
+    });
+
+    suite('option types', () => {
+      let changeStub;
+      let buildStub;
+
+      setup(() => {
+        changeStub = sandbox.stub(element, '_handleChange');
+        buildStub = sandbox.stub(element, '_buildConfigChangeInfo');
+      });
+
+      test('ARRAY type option', () => {
+        element.pluginData = {
+          name: 'testName',
+          config: {plugin: {value: 'test', type: 'ARRAY'}},
+        };
+        flushAsynchronousOperations();
+
+        const editor = element.$$('gr-plugin-config-array-editor');
+        assert.ok(editor);
+        element._handleArrayChange({detail: 'test'});
+        assert.isTrue(changeStub.called);
+        assert.equal(changeStub.lastCall.args[0], 'test');
+      });
+
+      test('BOOLEAN type option', () => {
+        element.pluginData = {
+          name: 'testName',
+          config: {plugin: {value: 'true', type: 'BOOLEAN'}},
+        };
+        flushAsynchronousOperations();
+
+        const toggle = element.$$('paper-toggle-button');
+        assert.ok(toggle);
+        toggle.click();
+        flushAsynchronousOperations();
+
+        assert.isTrue(buildStub.called);
+        assert.deepEqual(buildStub.lastCall.args, ['false', 'plugin']);
+
+        assert.isTrue(changeStub.called);
+      });
+
+      test('INT/LONG/STRING type option', () => {
+        element.pluginData = {
+          name: 'testName',
+          config: {plugin: {value: 'test', type: 'STRING'}},
+        };
+        flushAsynchronousOperations();
+
+        const input = element.$$('input');
+        assert.ok(input);
+        input.value = 'newTest';
+        input.dispatchEvent(new Event('input'));
+        flushAsynchronousOperations();
+
+        assert.isTrue(buildStub.called);
+        assert.deepEqual(buildStub.lastCall.args, ['newTest', 'plugin']);
+
+        assert.isTrue(changeStub.called);
+      });
+
+      test('LIST type option', () => {
+        const permitted_values = ['test', 'newTest'];
+        element.pluginData = {
+          name: 'testName',
+          config: {plugin: {value: 'test', type: 'LIST', permitted_values}},
+        };
+        flushAsynchronousOperations();
+
+        const select = element.$$('select');
+        assert.ok(select);
+        select.value = 'newTest';
+        select.dispatchEvent(new Event('change', {bubbles: true}));
+        flushAsynchronousOperations();
+
+        assert.isTrue(buildStub.called);
+        assert.deepEqual(buildStub.lastCall.args, ['newTest', 'plugin']);
+
+        assert.isTrue(changeStub.called);
+      });
+    });
+
+    test('_buildConfigChangeInfo', () => {
+      element.pluginData = {
+        name: 'testName',
+        config: {plugin: {value: 'test'}},
+      };
+      const detail = element._buildConfigChangeInfo('newTest', 'plugin');
+      assert.equal(detail._key, 'plugin');
+      assert.deepEqual(detail.info, {value: 'newTest'});
+      assert.equal(detail.notifyPath, 'plugin.value');
+    });
+  });
+</script>
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 186740e..8604ad8 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
@@ -26,7 +26,7 @@
 <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">
-
+<link rel="import" href="../gr-repo-plugin-config/gr-repo-plugin-config.html">
 
 <dom-module id="gr-repo">
   <template>
@@ -37,7 +37,7 @@
         content: ' *';
       }
       .loading,
-      .hideDownload {
+      .hide {
         display: none;
       }
       #loading.loading {
@@ -46,10 +46,10 @@
       #loading:not(.loading) {
         display: none;
       }
-      .repositorySettings {
+      #options .repositorySettings {
         display: none;
       }
-      .repositorySettings.showConfig {
+      #options .repositorySettings.showConfig {
         display: block;
       }
     </style>
@@ -67,7 +67,7 @@
       </div>
       <div id="loading" class$="[[_computeLoadingClass(_loading)]]">Loading...</div>
       <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
-        <div id="downloadContent" class$="[[_computeDownloadClass(_schemes)]]">
+        <div id="downloadContent" class$="[[_computeHideClass(_schemes)]]">
           <h2 id="download">Download</h2>
           <fieldset>
             <gr-download-commands
@@ -219,7 +219,7 @@
                   </gr-select>
                 </span>
               </section>
-              <section id="noteDbSettings" class$="repositorySettings [[_computeRepositoriesClass(_noteDbEnabled)]]">
+              <section>
                 <span class="title">
                   Enable adding unregistered users as reviewers and CCs on changes</span>
                 <span class="value">
@@ -346,7 +346,15 @@
                 </span>
               </section>
             </fieldset>
-            <!-- TODO @beckysiegel add plugin config widgets -->
+            <div
+                class$="pluginConfig [[_computeHideClass(_pluginData)]]"
+                on-plugin-config-changed="_handlePluginConfigChanged">
+              <h3>Plugins</h3>
+              <template is="dom-repeat" items="[[_pluginData]]" as="data">
+                <gr-repo-plugin-config
+                    plugin-data="[[data]]"></gr-repo-plugin-config>
+              </template>
+            </div>
             <gr-button
                 on-tap="_handleSaveRepoConfig"
                 disabled$="[[_computeButtonDisabled(_readOnly, _configChanged)]]">Save changes</gr-button>
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 91a0ffb..ff630c4 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
@@ -53,6 +53,7 @@
 
   Polymer({
     is: 'gr-repo',
+    _legacyUndefinedCheck: true,
 
     properties: {
       params: Object,
@@ -73,6 +74,11 @@
       },
       /** @type {?} */
       _repoConfig: Object,
+      /** @type {?} */
+      _pluginData: {
+        type: Array,
+        computed: '_computePluginData(_repoConfig.plugin_config.*)',
+      },
       _readOnly: {
         type: Boolean,
         value: true,
@@ -101,10 +107,6 @@
       },
       _selectedScheme: String,
       _schemesObj: Object,
-      _noteDbEnabled: {
-        type: Boolean,
-        value: false,
-      },
     },
 
     observers: [
@@ -117,6 +119,15 @@
       this.fire('title-change', {title: this.repo});
     },
 
+    _computePluginData(configRecord) {
+      if (!configRecord ||
+          !configRecord.base) { return []; }
+
+      const pluginConfig = configRecord.base;
+      return Object.keys(pluginConfig)
+          .map(name => ({name, config: pluginConfig[name]}));
+    },
+
     _loadRepo() {
       if (!this.repo) { return Promise.resolve(); }
 
@@ -162,7 +173,6 @@
         if (!config) { return Promise.resolve(); }
 
         this._schemesObj = config.download.schemes;
-        this._noteDbEnabled = !!config.note_db_enabled;
       }));
 
       return Promise.all(promises);
@@ -172,8 +182,8 @@
       return loading ? 'loading' : '';
     },
 
-    _computeDownloadClass(schemes) {
-      return !schemes || !schemes.length ? 'hideDownload' : '';
+    _computeHideClass(arr) {
+      return !arr || !arr.length ? 'hide' : '';
     },
 
     _loggedInChanged(_loggedIn) {
@@ -245,20 +255,22 @@
       return this.$.restAPI.getLoggedIn();
     },
 
-    _formatRepoConfigForSave(p) {
+    _formatRepoConfigForSave(repoConfig) {
       const configInputObj = {};
-      for (const key in p) {
-        if (p.hasOwnProperty(key)) {
+      for (const key in repoConfig) {
+        if (repoConfig.hasOwnProperty(key)) {
           if (key === 'default_submit_type') {
             // default_submit_type is not in the input type, and the
             // configured value was already copied to submit_type by
             // _loadProject. Omit this property when saving.
             continue;
           }
-          if (typeof p[key] === 'object') {
-            configInputObj[key] = p[key].configured_value;
+          if (key === 'plugin_config') {
+            configInputObj.plugin_config_values = repoConfig[key];
+          } else if (typeof repoConfig[key] === 'object') {
+            configInputObj[key] = repoConfig[key].configured_value;
           } else {
-            configInputObj[key] = p[key];
+            configInputObj[key] = repoConfig[key];
           }
         }
       }
@@ -322,5 +334,10 @@
     _computeChangesUrl(name) {
       return Gerrit.Nav.getUrlForProjectChanges(name);
     },
+
+    _handlePluginConfigChanged({detail: {name, config, notifyPath}}) {
+      this._repoConfig.plugin_config[name] = config;
+      this.notifyPath('_repoConfig.plugin_config.' + notifyPath);
+    },
   });
 })();
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 d6d4366..60645bb 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
@@ -125,6 +125,29 @@
       sandbox.restore();
     });
 
+    test('_computePluginData', () => {
+      assert.deepEqual(element._computePluginData(), []);
+      assert.deepEqual(element._computePluginData({}), []);
+      assert.deepEqual(element._computePluginData({base: {}}), []);
+      assert.deepEqual(element._computePluginData({base: {plugin: 'data'}}),
+          [{name: 'plugin', config: 'data'}]);
+    });
+
+    test('_handlePluginConfigChanged', () => {
+      const notifyStub = sandbox.stub(element, 'notifyPath');
+      element._repoConfig = {plugin_config: {}};
+      element._handlePluginConfigChanged({detail: {
+        name: 'test',
+        config: 'data',
+        notifyPath: 'path',
+      }});
+      flushAsynchronousOperations();
+
+      assert.equal(element._repoConfig.plugin_config.test, 'data');
+      assert.equal(notifyStub.lastCall.args[0],
+          '_repoConfig.plugin_config.path');
+    });
+
     test('loading displays before repo config is loaded', () => {
       assert.isTrue(element.$.loading.classList.contains('loading'));
       assert.isFalse(getComputedStyle(element.$.loading).display === 'none');
@@ -136,14 +159,12 @@
     test('download commands visibility', () => {
       element._loading = false;
       flushAsynchronousOperations();
-      assert.isTrue(element.$.downloadContent.classList
-          .contains('hideDownload'));
+      assert.isTrue(element.$.downloadContent.classList.contains('hide'));
       assert.isTrue(getComputedStyle(element.$.downloadContent)
           .display == 'none');
       element._schemesObj = SCHEMES;
       flushAsynchronousOperations();
-      assert.isFalse(element.$.downloadContent.classList
-          .contains('hideDownload'));
+      assert.isFalse(element.$.downloadContent.classList.contains('hide'));
       assert.isFalse(getComputedStyle(element.$.downloadContent)
           .display == 'none');
     });
@@ -293,17 +314,6 @@
       });
 
       test('fields update and save correctly', () => {
-        // test notedb
-        element._noteDbEnabled = false;
-
-        assert.equal(
-            element._computeRepositoriesClass(element._noteDbEnabled), '');
-
-        element._noteDbEnabled = true;
-
-        assert.equal(element._computeRepositoriesClass(
-            element._noteDbEnabled), 'showConfig');
-
         const configInputObj = {
           description: 'new description',
           use_contributor_agreements: 'TRUE',
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 a5dc04e..84c6101 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,6 +66,7 @@
 
   Polymer({
     is: 'gr-rule-editor',
+    _legacyUndefinedCheck: true,
 
     properties: {
       hasRange: Boolean,
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 2374677..a6dde7a 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
@@ -223,6 +223,15 @@
         [[_computeLabelValue(change, labelName)]]
       </td>
     </template>
+    <template is="dom-repeat" items="[[_dynamicCellEndpoints]]"
+      as="pluginEndpointName">
+      <td class="cell endpoint">
+        <gr-endpoint-decorator name$="[[pluginEndpointName]]">
+          <gr-endpoint-param name="change" value="[[change]]">
+          </gr-endpoint-param>
+        </gr-endpoint-decorator>
+      </td>
+    </template>
   </template>
   <script src="gr-change-list-item.js"></script>
 </dom-module>
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 65fe9ea..ecc7532 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,6 +26,7 @@
 
   Polymer({
     is: 'gr-change-list-item',
+    _legacyUndefinedCheck: true,
 
     properties: {
       visibleChangeTableColumns: Array,
@@ -57,6 +58,9 @@
         type: String,
         computed: '_computeChangeSize(change)',
       },
+      _dynamicCellEndpoints: {
+        type: Array,
+      },
     },
 
     behaviors: [
@@ -67,6 +71,13 @@
       Gerrit.URLEncodingBehavior,
     ],
 
+    attached() {
+      Gerrit.awaitPluginsLoaded().then(() => {
+        this._dynamicCellEndpoints = Gerrit._endpoints.getDynamicEndpoints(
+            'change-list-item-cell');
+      });
+    },
+
     _computeItemNeedsReview(reviewed) {
       return !reviewed;
     },
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 64cedf3..6d05132 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
@@ -31,6 +31,7 @@
 
   Polymer({
     is: 'gr-change-list-view',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the title of the page should change.
@@ -244,9 +245,13 @@
       if (!changes || !changes.length) {
         return;
       }
-      if (USER_QUERY_PATTERN.test(this._query) && changes[0].owner.email) {
-        this._userId = changes[0].owner.email;
-        return;
+      if (USER_QUERY_PATTERN.test(this._query)) {
+        const owner = changes[0].owner;
+        const userId = owner._account_id ? owner._account_id : owner.email;
+        if (userId) {
+          this._userId = userId;
+          return;
+        }
       }
       if (REPO_QUERY_PATTERN.test(this._query)) {
         this._repo = changes[0].project;
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 372e6be..ef17baa 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
@@ -52,17 +52,24 @@
             [[_computeLabelShortcut(labelName)]]
           </th>
         </template>
+        <template is="dom-repeat" items="[[_dynamicHeaderEndpoints]]"
+          as="pluginHeader">
+          <th class="endpoint">
+            <gr-endpoint-decorator name$="[[pluginHeader]]">
+            </gr-endpoint-decorator>
+          </th>
+        </template>
       </tr>
       <template is="dom-repeat" items="[[sections]]" as="changeSection"
           index-as="sectionIndex">
-        <template is="dom-if" if="[[changeSection.sectionName]]">
+        <template is="dom-if" if="[[changeSection.name]]">
           <tr class="groupHeader">
             <td class="leftPadding"></td>
             <td class="star" hidden$="[[!showStar]]" hidden></td>
             <td class="cell"
                 colspan$="[[_computeColspan(changeTableColumns, labelNames)]]">
               <a href$="[[_sectionHref(changeSection.query)]]">
-                [[changeSection.sectionName]]
+                [[changeSection.name]]
               </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 e57a389..587719d 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,6 +24,7 @@
 
   Polymer({
     is: 'gr-change-list',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when next page key shortcut was pressed.
@@ -63,7 +64,7 @@
        * properties should not be used together.
        *
        * @type {!Array<{
-       *   sectionName: string,
+       *   name: string,
        *   query: string,
        *   results: !Array<!Object>
        * }>}
@@ -76,6 +77,9 @@
         type: Array,
         computed: '_computeLabelNames(sections)',
       },
+      _dynamicHeaderEndpoints: {
+        type: Array,
+      },
       selectedIndex: {
         type: Number,
         notify: true,
@@ -128,6 +132,13 @@
       };
     },
 
+    attached() {
+      Gerrit.awaitPluginsLoaded().then(() => {
+        this._dynamicHeaderEndpoints = Gerrit._endpoints.getDynamicEndpoints(
+            'change-list-header');
+      });
+    },
+
     /**
      * Iron-a11y-keys-behavior catches keyboard events globally. Some keyboard
      * events must be scoped to a component level (e.g. `enter`) in order to not
@@ -341,7 +352,9 @@
     },
 
     _getListItems() {
-      return Polymer.dom(this.root).querySelectorAll('gr-change-list-item');
+      // Polymer2: querySelectorAll returns NodeList instead of Array.
+      return Array.from(
+          Polymer.dom(this.root).querySelectorAll('gr-change-list-item'));
     },
 
     _sectionsChanged() {
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 e793f42..ecbd67e 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
@@ -58,6 +58,7 @@
         font-size: var(--font-size-large);
       }
       #help p {
+        margin-bottom: .6em;
         max-width: 35em;
       }
       @media only screen and (max-width: 50em) {
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 b5b02a7..42d7bd7 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-create-change-help',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the "Create change" button is tapped.
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 bf17b911..e6d123c 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
@@ -74,8 +74,8 @@
             <li>
               <p>
                 Close this dialog and you should be able to see your recently
-                created change in ``Outgoing changes'' section on
-                ``Your changes'' page.
+                created change in the 'Outgoing changes' section on the
+                'Your changes' page.
               </p>
             </li>
           </ol>
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 0e71f1c..e4958c5 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,6 +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-destination-dialog/gr-create-destination-dialog.js b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js
index 4d2802e..ed87e9e 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,6 +26,7 @@
 
   Polymer({
     is: 'gr-create-destination-dialog',
+    _legacyUndefinedCheck: true,
     properties: {
       _repo: String,
       _branch: String,
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 43fe0c2..5faef08 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,6 +21,7 @@
 
   Polymer({
     is: 'gr-dashboard-view',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the title of the page should change.
@@ -206,7 +207,7 @@
               this._showNewUserHelp = lastResultSet.length == 0;
             }
             this._results = changes.map((results, i) => ({
-              sectionName: res.sections[i].name,
+              name: res.sections[i].name,
               query: res.sections[i].query,
               results,
               isOutgoing: res.sections[i].isOutgoing,
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 6da56e6..54c8ea9 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
@@ -278,7 +278,7 @@
 
       return element._fetchDashboardChanges({sections}, false).then(() => {
         assert.equal(element._results.length, 1);
-        assert.equal(element._results[0].sectionName, 'test2');
+        assert.equal(element._results[0].name, 'test2');
       });
     });
 
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
new file mode 100644
index 0000000..2394e24
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.html
@@ -0,0 +1,41 @@
+<!--
+@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.
+-->
+
+<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">
+
+<dom-module id="gr-embed-dashboard">
+  <template>
+    <gr-change-list
+        show-star
+        account="[[account]]"
+        preferences="[[preferences]]"
+        sections="[[sections]]">
+      <div id="emptyOutgoing" slot="empty-outgoing">
+        <template is="dom-if" if="[[showNewUserHelp]]">
+          <gr-create-change-help></gr-create-change-help>
+        </template>
+        <template is="dom-if" if="[[!showNewUserHelp]]">
+          No changes
+        </template>
+      </div>
+    </gr-change-list>
+  </template>
+  <script src="gr-embed-dashboard.js"></script>
+</dom-module>
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
new file mode 100644
index 0000000..14d0cb0
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js
@@ -0,0 +1,30 @@
+/**
+ * @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.
+ */
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-embed-dashboard',
+    _legacyUndefinedCheck: true,
+    properties: {
+      account: Object,
+      sections: Array,
+      preferences: Object,
+      showNewUserHelp: Boolean,
+    },
+  });
+})();
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 cd6eb77..67fbd97 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-repo-header',
+    _legacyUndefinedCheck: true,
     properties: {
       /** @type {?String} */
       repo: {
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 d3d54f56..c7fda2c 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-user-header',
+    _legacyUndefinedCheck: true,
     properties: {
       /** @type {?String} */
       userId: {
@@ -79,8 +80,11 @@
     },
 
     _computeDashboardUrl(accountDetails) {
-      if (!accountDetails || !accountDetails.email) { return null; }
-      return Gerrit.Nav.getUrlForUserDashboard(accountDetails.email);
+      if (!accountDetails) { return null; }
+      const id = accountDetails._account_id;
+      const email = accountDetails.email;
+      if (!id && !email ) { return null; }
+      return Gerrit.Nav.getUrlForUserDashboard(id ? id : email);
     },
 
     _computeDashboardLinkClass(showDashboardLink, loggedIn) {
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
index 88b2858..c53c111 100644
--- 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
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-account-entry',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when an account is entered.
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js
index 458719a..7cdffc8 100644
--- a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js
+++ b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js
@@ -21,6 +21,7 @@
 
   Polymer({
     is: 'gr-account-list',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when user inputs an invalid email address.
@@ -89,7 +90,9 @@
     },
 
     get accountChips() {
-      return Polymer.dom(this.root).querySelectorAll('gr-account-chip');
+      // Polymer2: querySelectorAll returns NodeList instead of Array.
+      return Array.from(
+          Polymer.dom(this.root).querySelectorAll('gr-account-chip'));
     },
 
     get focusStart() {
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 60177d1..9182bbc 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
@@ -193,6 +193,7 @@
 
   Polymer({
     is: 'gr-change-actions',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the change should be reloaded.
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 670de5d..9803fab 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
@@ -64,7 +64,9 @@
           });
         },
         send(method, url, payload) {
-          if (method !== 'POST') { return Promise.reject('bad method'); }
+          if (method !== 'POST') {
+            return Promise.reject(new Error('bad method'));
+          }
 
           if (url === '/changes/test~42/revisions/2/submit') {
             return Promise.resolve({
@@ -78,7 +80,7 @@
             });
           }
 
-          return Promise.reject('bad url');
+          return Promise.reject(new Error('bad url'));
         },
         getProjectConfig() { return Promise.resolve({}); },
       });
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 d62aaee..f1d3cd3 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
@@ -83,9 +83,6 @@
         getLoggedIn() { return Promise.resolve(false); },
         deleteVote() { return Promise.resolve({ok: true}); },
       });
-      stub('gr-change-metadata', {
-        _computeShowReviewersByState() { return true; },
-      });
     });
 
     teardown(() => {
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 c07a775..a09b730 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
@@ -200,38 +200,26 @@
               allow-any-user></gr-account-list>
         </span>
       </section>
-      <template is="dom-if" if="[[_showReviewersByState]]">
-        <section>
-          <span class="title">Reviewers</span>
-          <span class="value">
-            <gr-reviewer-list
-                change="{{change}}"
-                mutable="[[_mutable]]"
-                reviewers-only
-                max-reviewers-displayed="3"></gr-reviewer-list>
-          </span>
-        </section>
-        <section>
-          <span class="title">CC</span>
-          <span class="value">
-            <gr-reviewer-list
-                change="{{change}}"
-                mutable="[[_mutable]]"
-                ccs-only
-                max-reviewers-displayed="3"></gr-reviewer-list>
-          </span>
-        </section>
-      </template>
-      <template is="dom-if" if="[[!_showReviewersByState]]">
-        <section>
-          <span class="title">Reviewers</span>
-          <span class="value">
-            <gr-reviewer-list
-                change="{{change}}"
-                mutable="[[_mutable]]"></gr-reviewer-list>
-          </span>
-        </section>
-      </template>
+      <section>
+        <span class="title">Reviewers</span>
+        <span class="value">
+          <gr-reviewer-list
+              change="{{change}}"
+              mutable="[[_mutable]]"
+              reviewers-only
+              max-reviewers-displayed="3"></gr-reviewer-list>
+        </span>
+      </section>
+      <section>
+        <span class="title">CC</span>
+        <span class="value">
+          <gr-reviewer-list
+              change="{{change}}"
+              mutable="[[_mutable]]"
+              ccs-only
+              max-reviewers-displayed="3"></gr-reviewer-list>
+        </span>
+      </section>
       <section>
         <span class="title">Repo</span>
         <span class="value">
@@ -299,31 +287,29 @@
         <span class="title">Strategy</span>
         <span class="value">[[_computeStrategy(change)]]</span>
       </section>
-      <template is="dom-if" if="[[serverConfig.note_db_enabled]]">
-        <section class="hashtag">
-          <span class="title">Hashtags</span>
-          <span class="value">
-            <template is="dom-repeat" items="[[change.hashtags]]">
-              <gr-linked-chip
-                  class="hashtagChip"
-                  text="[[item]]"
-                  href="[[_computeHashtagURL(item)]]"
-                  removable="[[!_hashtagReadOnly]]"
-                  on-remove="_handleHashtagRemoved">
-              </gr-linked-chip>
-            </template>
-            <template is="dom-if" if="[[!_hashtagReadOnly]]">
-              <gr-editable-label
-                  uppercase
-                  label-text="Add a hashtag"
-                  value="{{_newHashtag}}"
-                  placeholder="[[_computeHashtagPlaceholder(_hashtagReadOnly)]]"
-                  read-only="[[_hashtagReadOnly]]"
-                  on-changed="_handleHashtagChanged"></gr-editable-label>
-            </template>
-          </span>
-        </section>
-      </template>
+      <section class="hashtag">
+        <span class="title">Hashtags</span>
+        <span class="value">
+          <template is="dom-repeat" items="[[change.hashtags]]">
+            <gr-linked-chip
+                class="hashtagChip"
+                text="[[item]]"
+                href="[[_computeHashtagURL(item)]]"
+                removable="[[!_hashtagReadOnly]]"
+                on-remove="_handleHashtagRemoved">
+            </gr-linked-chip>
+          </template>
+          <template is="dom-if" if="[[!_hashtagReadOnly]]">
+            <gr-editable-label
+                uppercase
+                label-text="Add a hashtag"
+                value="{{_newHashtag}}"
+                placeholder="[[_computeHashtagPlaceholder(_hashtagReadOnly)]]"
+                read-only="[[_hashtagReadOnly]]"
+                on-changed="_handleHashtagChanged"></gr-editable-label>
+          </template>
+        </span>
+      </section>
       <div class="separatedSection">
         <gr-change-requirements
             change="{{change}}"
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 a540665..6233241 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
@@ -61,6 +61,7 @@
 
   Polymer({
     is: 'gr-change-metadata',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the change topic is changed.
@@ -83,9 +84,7 @@
         type: Boolean,
         computed: '_computeIsMutable(account)',
       },
-      /**
-       * @type {{ note_db_enabled: string }}
-       */
+      /** @type {?} */
       serverConfig: Object,
       parentIsCurrent: Boolean,
       _notCurrentMessage: {
@@ -101,10 +100,6 @@
         type: Boolean,
         computed: '_computeHashtagReadOnly(_mutable, change)',
       },
-      _showReviewersByState: {
-        type: Boolean,
-        computed: '_computeShowReviewersByState(serverConfig)',
-      },
       /**
        * @type {Defs.PushCertificateValidation}
        */
@@ -282,10 +277,6 @@
       return _hashtagReadOnly ? '' : HASHTAG_ADD_MESSAGE;
     },
 
-    _computeShowReviewersByState(serverConfig) {
-      return !!serverConfig.note_db_enabled;
-    },
-
     _computeShowRequirements(change) {
       if (change.status !== this.ChangeStatus.NEW) {
         // TODO(maximeg) change this to display the stored
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 fcbd3d5..2dd0e47 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
@@ -117,18 +117,6 @@
       assert.isTrue(element.$$('.strategy').hasAttribute('hidden'));
     });
 
-    test('show CC section when NoteDb enabled', () => {
-      function hasCc() {
-        return element._showReviewersByState;
-      }
-
-      element.serverConfig = {};
-      assert.isFalse(hasCc());
-
-      element.serverConfig = {note_db_enabled: true};
-      assert.isTrue(hasCc());
-    });
-
     test('weblinks use Gerrit.Nav interface', () => {
       const weblinksStub = sandbox.stub(Gerrit.Nav, '_generateWeblinks')
           .returns([{name: 'stubb', url: '#s'}]);
@@ -550,9 +538,6 @@
       });
 
       test('_computeHashtagReadOnly', () => {
-        element.serverConfig = {
-          note_db_enabled: true,
-        };
         flushAsynchronousOperations();
         let mutable = false;
         assert.isTrue(element._computeHashtagReadOnly(mutable, change));
@@ -565,9 +550,6 @@
       });
 
       test('hashtag read only hides delete button', () => {
-        element.serverConfig = {
-          note_db_enabled: true,
-        };
         flushAsynchronousOperations();
         element.account = {};
         element.change = change;
@@ -577,9 +559,6 @@
       });
 
       test('hashtag not read only does not hide delete button', () => {
-        element.serverConfig = {
-          note_db_enabled: true,
-        };
         flushAsynchronousOperations();
         element.account = {test: true};
         change.actions.hashtags.enabled = true;
@@ -701,9 +680,6 @@
       });
 
       test('changing hashtag', () => {
-        element.serverConfig = {
-          note_db_enabled: true,
-        };
         flushAsynchronousOperations();
         element._newHashtag = 'new hashtag';
         const newHashtag = ['new hashtag'];
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 dfdcd59..4717ff9 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-change-requirements',
+    _legacyUndefinedCheck: true,
 
     properties: {
       /** @type {?} */
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 07573d3..8579762 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
@@ -200,6 +200,7 @@
       }
       .collapseToggleContainer {
         display: flex;
+        margin-bottom: 8px;
       }
       #relatedChangesToggle {
         display: none;
@@ -234,6 +235,7 @@
         --paper-tabs-selection-bar-color: var(--link-color);
       }
       paper-tab {
+        box-sizing: border-box;
         max-width: 15rem;
         --paper-tab-ink: var(--link-color);
       }
@@ -355,6 +357,9 @@
           min-width: initial;
           width: 100vw;
         }
+        #replyOverlay {
+          z-index: var(--reply-overlay-z-index);
+        }
       }
     </style>
     <div class="container loading" hidden$="[[!_loading]]">Loading...</div>
@@ -518,8 +523,27 @@
           </div>
         </div>
       </section>
+
       <section class="patchInfo">
-        <gr-file-list-header
+        <template is="dom-if" if="[[_showPrimaryTabs]]">
+          <paper-tabs id="primaryTabs" on-selected-changed="_handleFileTabChange">
+            <paper-tab>Files</paper-tab>
+            <template is="dom-repeat" items="[[_dynamicTabHeaderEndpoints]]"
+              as="tabHeader">
+              <paper-tab>
+                  <gr-endpoint-decorator name$="[[tabHeader]]">
+                      <gr-endpoint-param name="change" value="[[_change]]">
+                      </gr-endpoint-param>
+                      <gr-endpoint-param name="revision" value="[[_selectedRevision]]">
+                      </gr-endpoint-param>
+                  </gr-endpoint-decorator>
+              </paper-tab>
+            </template>
+          </paper-tabs>
+        </template>
+
+        <div hidden$="[[!_showFileTabContent]]">
+          <gr-file-list-header
             id="fileListHeader"
             account="[[_account]]"
             all-patch-sets="[[_allPatchSets]]"
@@ -538,6 +562,8 @@
             patch-num="{{_patchRange.patchNum}}"
             base-patch-num="{{_patchRange.basePatchNum}}"
             files-expanded="[[_filesExpanded]]"
+            diff-prefs-disabled="[[_diffPrefsDisabled]]"
+            show-title="[[!_showPrimaryTabs]]"
             on-open-diff-prefs="_handleOpenDiffPrefs"
             on-open-download-dialog="_handleOpenDownloadDialog"
             on-open-upload-help-dialog="_handleOpenUploadHelpDialog"
@@ -565,7 +591,18 @@
             on-files-shown-changed="_setShownFiles"
             on-file-action-tap="_handleFileActionTap"
             on-reload-drafts="_reloadDraftsWithCallback"></gr-file-list>
+        </div>
+
+        <template is="dom-if" if="[[!_showFileTabContent]]">
+            <gr-endpoint-decorator name$="[[_selectedFilesTabPluginEndpoint]]">
+                <gr-endpoint-param name="change" value="[[_change]]">
+                </gr-endpoint-param>
+                <gr-endpoint-param name="revision" value="[[_selectedRevision]]">
+                </gr-endpoint-param>
+            </gr-endpoint-decorator>
+        </template>
       </section>
+
       <gr-endpoint-decorator name="change-view-integration">
         <gr-endpoint-param name="change" value="[[_change]]">
         </gr-endpoint-param>
@@ -574,7 +611,7 @@
       </gr-endpoint-decorator>
       <paper-tabs
           id="commentTabs"
-          on-selected-changed="_handleTabChange">
+          on-selected-changed="_handleCommentTabChange">
         <paper-tab class="changeLog">Change Log</paper-tab>
         <paper-tab
             class="commentThreads">
@@ -594,6 +631,7 @@
             change-comments="[[_changeComments]]"
             project-name="[[_change.project]]"
             show-reply-buttons="[[_loggedIn]]"
+            on-message-anchor-tap="_handleMessageAnchorTap"
             on-reply="_handleMessageReply"></gr-messages-list>
       </template>
       <template is="dom-if" if="[[!_showMessagesView]]">
@@ -635,7 +673,6 @@
           patch-num="[[computeLatestPatchNum(_allPatchSets)]]"
           permitted-labels="[[_change.permitted_labels]]"
           diff-drafts="[[_diffDrafts]]"
-          server-config="[[_serverConfig]]"
           project-config="[[_projectConfig]]"
           can-be-started="[[_canStartReview]]"
           on-send="_handleReplySent"
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 ea54239..59304aa 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
@@ -44,6 +44,8 @@
 
   const TRAILING_WHITESPACE_REGEX = /[ \t]+$/gm;
 
+  const MSG_PREFIX = '#message-';
+
   const ReloadToastMessage = {
     NEWER_REVISION: 'A newer patch set has been uploaded',
     RESTORED: 'This change has been restored',
@@ -62,6 +64,7 @@
 
   Polymer({
     is: 'gr-change-view',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the title of the page should change.
@@ -106,6 +109,14 @@
         type: Boolean,
         value: false,
       },
+      disableDiffPrefs: {
+        type: Boolean,
+        value: false,
+      },
+      _diffPrefsDisabled: {
+        type: Boolean,
+        computed: '_computeDiffPrefsDisabled(disableDiffPrefs, _loggedIn)',
+      },
       _commentThreads: Array,
       /** @type {?} */
       _serverConfig: {
@@ -248,6 +259,25 @@
         type: Boolean,
         value: true,
       },
+      _showFileTabContent: {
+        type: Boolean,
+        value: true,
+      },
+      /** @type {Array<string>} */
+      _dynamicTabHeaderEndpoints: {
+        type: Array,
+      },
+      _showPrimaryTabs: {
+        type: Boolean,
+        computed: '_computeShowPrimaryTabs(_dynamicTabHeaderEndpoints)',
+      },
+      /** @type {Array<string>} */
+      _dynamicTabContentEndpoints: {
+        type: Array,
+      },
+      _selectedFilesTabPluginEndpoint: {
+        type: String,
+      },
     },
 
     behaviors: [
@@ -306,6 +336,17 @@
         this._setDiffViewMode();
       });
 
+      Gerrit.awaitPluginsLoaded().then(() => {
+        this._dynamicTabHeaderEndpoints =
+            Gerrit._endpoints.getDynamicEndpoints('change-view-tab-header');
+        this._dynamicTabContentEndpoints =
+            Gerrit._endpoints.getDynamicEndpoints('change-view-tab-content');
+        if (this._dynamicTabContentEndpoints.length
+            !== this._dynamicTabHeaderEndpoints.length) {
+          console.warn('Different number of tab headers and tab content.');
+        }
+      });
+
       this.addEventListener('comment-save', this._handleCommentSave.bind(this));
       this.addEventListener('comment-refresh', this._reloadDrafts.bind(this));
       this.addEventListener('comment-discard',
@@ -364,10 +405,18 @@
       }
     },
 
-    _handleTabChange() {
+    _handleCommentTabChange() {
       this._showMessagesView = this.$.commentTabs.selected === 0;
     },
 
+    _handleFileTabChange() {
+      const selectedIndex = this.$$('#primaryTabs').selected;
+      this._showFileTabContent = selectedIndex === 0;
+      // Initial tab is the static files list.
+      this._selectedFilesTabPluginEndpoint =
+          this._dynamicTabContentEndpoints[selectedIndex - 1];
+    },
+
     _handleEditCommitMessage(e) {
       this._editingCommitMessage = true;
       this.$.commitMessageEditor.focusTextarea();
@@ -458,9 +507,9 @@
     },
 
     _handleCommentSave(e) {
-      if (!e.target.comment.__draft) { return; }
+      const draft = e.detail.comment;
+      if (!draft.__draft) { return; }
 
-      const draft = e.target.comment;
       draft.patch_set = draft.patch_set || this._patchRange.patchNum;
 
       // The use of path-based notification helpers (set, push) can’t be used
@@ -490,9 +539,9 @@
     },
 
     _handleCommentDiscard(e) {
-      if (!e.target.comment.__draft) { return; }
+      const draft = e.detail.comment;
+      if (!draft.__draft) { return; }
 
-      const draft = e.target.comment;
       if (!this._diffDrafts[draft.path]) {
         return;
       }
@@ -695,6 +744,8 @@
       // 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.async(() => {
         if (this.viewState.scrollTop) {
@@ -727,10 +778,17 @@
       this.viewState.numFilesShown = numFilesShown;
     },
 
+    _handleMessageAnchorTap(e) {
+      const hash = MSG_PREFIX + e.detail.id;
+      const url = Gerrit.Nav.getUrlForChange(this._change,
+          this._patchRange.patchNum, this._patchRange.basePatchNum,
+          this._editMode, hash);
+      history.replaceState(null, '', url);
+    },
+
     _maybeScrollToMessage(hash) {
-      const msgPrefix = '#message-';
-      if (hash.startsWith(msgPrefix)) {
-        this.messagesList.scrollToMessage(hash.substr(msgPrefix.length));
+      if (hash.startsWith(MSG_PREFIX)) {
+        this.messagesList.scrollToMessage(hash.substr(MSG_PREFIX.length));
       }
     },
 
@@ -840,6 +898,10 @@
       return 'PARENT';
     },
 
+    _computeShowPrimaryTabs(dynamicTabContentEndpoints) {
+      return dynamicTabContentEndpoints.length > 0;
+    },
+
     _computeChangeUrl(change) {
       return Gerrit.Nav.getUrlForChange(change);
     },
@@ -1014,6 +1076,8 @@
       if (this.shouldSuppressKeyboardShortcut(e) ||
           this.modifierPressed(e)) { return; }
 
+      if (this._diffPrefsDisabled) { return; }
+
       e.preventDefault();
       this.$.fileList.openDiffPrefs();
     },
@@ -1712,5 +1776,9 @@
     _computeCurrentRevision(currentRevision, revisions) {
       return revisions && revisions[currentRevision];
     },
+
+    _computeDiffPrefsDisabled(disableDiffPrefs, loggedIn) {
+      return 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 71ca338..f74160b 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
@@ -95,6 +95,20 @@
       return element.getComputedStyleValue(cssParam);
     };
 
+    test('_handleMessageAnchorTap', () => {
+      element._changeNum = '1';
+      element._patchRange = {
+        basePatchNum: 'PARENT',
+        patchNum: 1,
+      };
+      const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForChange');
+      const replaceStateStub = sandbox.stub(history, 'replaceState');
+      element._handleMessageAnchorTap({detail: {id: 'a12345'}});
+
+      assert.equal(getUrlStub.lastCall.args[4], '#message-a12345');
+      assert.isTrue(replaceStateStub.called);
+    });
+
     suite('keyboard shortcuts', () => {
       test('t to add topic', () => {
         const editStub = sandbox.stub(element.$.metadata, 'editTopic');
@@ -285,6 +299,16 @@
       test(', should open diff preferences', () => {
         const stub = sandbox.stub(
             element.$.fileList.$.diffPreferencesDialog, 'open');
+        element._loggedIn = false;
+        element.disableDiffPrefs = true;
+        MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
+        assert.isFalse(stub.called);
+
+        element._loggedIn = true;
+        MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
+        assert.isFalse(stub.called);
+
+        element.disableDiffPrefs = false;
         MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
         assert.isTrue(stub.called);
       });
@@ -643,12 +667,12 @@
         path: '/foo/bar.txt',
         text: 'hello',
       };
-      element._handleCommentSave({target: {comment: draft}});
+      element._handleCommentSave({detail: {comment: draft}});
       draft.patch_set = 2;
       assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft]});
       draft.patch_set = null;
       draft.text = 'hello, there';
-      element._handleCommentSave({target: {comment: draft}});
+      element._handleCommentSave({detail: {comment: draft}});
       draft.patch_set = 2;
       assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft]});
       const draft2 = {
@@ -657,14 +681,14 @@
         path: '/foo/bar.txt',
         text: 'hola',
       };
-      element._handleCommentSave({target: {comment: draft2}});
+      element._handleCommentSave({detail: {comment: draft2}});
       draft2.patch_set = 2;
       assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft, draft2]});
       draft.patch_set = null;
-      element._handleCommentDiscard({target: {comment: draft}});
+      element._handleCommentDiscard({detail: {comment: draft}});
       draft.patch_set = 2;
       assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft2]});
-      element._handleCommentDiscard({target: {comment: draft2}});
+      element._handleCommentDiscard({detail: {comment: draft2}});
       assert.deepEqual(element._diffDrafts, {});
     });
 
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 42cb976..b19ca62 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,6 +18,7 @@
   'use strict';
   Polymer({
     is: 'gr-comment-list',
+    _legacyUndefinedCheck: true,
 
     behaviors: [
       Gerrit.BaseUrlBehavior,
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 837de59..ddab319 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-commit-info',
+    _legacyUndefinedCheck: true,
 
     properties: {
       change: Object,
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 03509ce..a371e13 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-confirm-abandon-dialog',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the confirm button is pressed.
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 355aa78..2e8af0e 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-confirm-cherrypick-conflict-dialog',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the confirm button is pressed.
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 a63ef3c..bdc0bb2 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,6 +21,7 @@
 
   Polymer({
     is: 'gr-confirm-cherrypick-dialog',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the confirm button is pressed.
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 f198483..91600ff 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,6 +21,7 @@
 
   Polymer({
     is: 'gr-confirm-move-dialog',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the confirm button is pressed.
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 461f030..77bc798 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-confirm-rebase-dialog',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the confirm button is pressed.
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 1cae866..93e21c7 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,6 +22,7 @@
 
   Polymer({
     is: 'gr-confirm-revert-dialog',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the confirm button is pressed.
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 346bdd0..1036b7f 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
@@ -49,9 +49,10 @@
       </div>
       <div class="main" slot="main">
         <gr-endpoint-decorator name="confirm-submit-change">
-          <p>
-            Ready to submit &ldquo;<strong>[[change.subject]]</strong>&rdquo;?
-          </p>
+          <p>Ready to submit &ldquo;<strong>[[change.subject]]</strong>&rdquo;?</p>
+          <template is="dom-if" if="[[change.is_private]]">
+            <p><strong>Heads Up!</strong> Submitting this private change will also make it public.</p>
+          </template>
           <gr-endpoint-param name="change" value="[[change]]"></gr-endpoint-param>
           <gr-endpoint-param name="action" value="[[action]]"></gr-endpoint-param>
         </gr-endpoint-decorator>
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 bda79a1..93d38df 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-confirm-submit-dialog',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the confirm button is pressed.
@@ -35,6 +36,7 @@
     properties: {
       /**
        * @type {{
+       *    is_private: boolean,
        *    subject: string,
        *  }}
        */
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 0e6315a..b6c155e 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-download-dialog',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the user presses the close button.
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 22c476c..f7d90eb 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
@@ -153,7 +153,10 @@
     </style>
     <div class$="patchInfo-header [[_computeEditModeClass(editMode)]] [[_computePatchInfoClass(patchNum, allPatchSets)]]">
       <div class="patchInfo-left">
-        <h3 class="label">Files</h3>
+        <template is="dom-if"
+            if="[[showTitle]]">
+          <h3 class="label">Files</h3>
+        </template>
         <div class="patchInfoContent">
           <gr-patch-range-select
               id="rangeSelect"
@@ -248,10 +251,10 @@
           <gr-diff-mode-selector
               id="modeSelect"
               mode="{{diffViewMode}}"
-              save-on-change="[[loggedIn]]"></gr-diff-mode-selector>
+              save-on-change="[[!diffPrefsDisabled]]"></gr-diff-mode-selector>
           <span id="diffPrefsContainer"
               class="hideOnEdit"
-              hidden$="[[_computePrefsButtonHidden(diffPrefs, loggedIn)]]"
+              hidden$="[[_computePrefsButtonHidden(diffPrefs, diffPrefsDisabled)]]"
               hidden>
             <gr-button
                 link
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 da8384f..8321879 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,6 +23,7 @@
 
   Polymer({
     is: 'gr-file-list-header',
+    _legacyUndefinedCheck: true,
 
     /**
      * @event expand-diffs
@@ -62,6 +63,7 @@
       serverConfig: Object,
       shownFileCount: Number,
       diffPrefs: Object,
+      diffPrefsDisabled: Boolean,
       diffViewMode: {
         type: String,
         notify: true,
@@ -80,6 +82,10 @@
         type: String,
         value: '',
       },
+      showTitle: {
+        type: Boolean,
+        value: true,
+      },
       _descriptionReadOnly: {
         type: Boolean,
         computed: '_computeDescriptionReadOnly(loggedIn, change, account)',
@@ -184,11 +190,10 @@
           });
     },
 
-    _computePrefsButtonHidden(prefs, loggedIn) {
-      return !loggedIn || !prefs;
+    _computePrefsButtonHidden(prefs, diffPrefsDisabled) {
+      return diffPrefsDisabled || !prefs;
     },
 
-
     _fileListActionsVisible(shownFileCount, maxFilesForBulkActions) {
       return shownFileCount <= maxFilesForBulkActions;
     },
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 e2685e1..adfeeb4 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
@@ -62,21 +62,21 @@
       });
     });
 
-    test('Diff preferences hidden when no prefs or logged out', () => {
-      element.loggedIn = false;
+    test('Diff preferences hidden when no prefs or diffPrefsDisabled', () => {
+      element.diffPrefsDisabled = true;
       flushAsynchronousOperations();
       assert.isTrue(element.$.diffPrefsContainer.hidden);
 
-      element.loggedIn = true;
+      element.diffPrefsDisabled = false;
       flushAsynchronousOperations();
       assert.isTrue(element.$.diffPrefsContainer.hidden);
 
-      element.loggedIn = false;
+      element.diffPrefsDisabled = true;
       element.diffPrefs = {font_size: '12'};
       flushAsynchronousOperations();
       assert.isTrue(element.$.diffPrefsContainer.hidden);
 
-      element.loggedIn = true;
+      element.diffPrefsDisabled = false;
       flushAsynchronousOperations();
       assert.isFalse(element.$.diffPrefsContainer.hidden);
     });
@@ -265,7 +265,7 @@
 
     suite('editMode behavior', () => {
       setup(() => {
-        element.loggedIn = true;
+        element.diffPrefsDisabled = false;
         element.diffPrefs = {};
       });
 
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 84de48f..29a12df 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
@@ -401,7 +401,6 @@
                 path="[[file.__path]]"
                 prefs="[[diffPrefs]]"
                 project-name="[[change.project]]"
-                project-config="[[projectConfig]]"
                 on-line-selected="_onLineSelected"
                 no-render-on-prefs-change
                 view-mode="[[diffViewMode]]"></gr-diff-host>
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 0f2b37e..87ae0ba 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
@@ -63,6 +63,7 @@
 
   Polymer({
     is: 'gr-file-list',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when a draft refresh should get triggered
@@ -218,7 +219,7 @@
         [this.Shortcut.TOGGLE_FILE_REVIEWED]: '_handleToggleFileReviewed',
         [this.Shortcut.TOGGLE_LEFT_PANE]: '_handleToggleLeftPane',
 
-        // Final two are actually handled by gr-diff-comment-thread.
+        // Final two are actually handled by gr-comment-thread.
         [this.Shortcut.EXPAND_ALL_COMMENT_THREADS]: null,
         [this.Shortcut.COLLAPSE_ALL_COMMENT_THREADS]: null,
       };
@@ -291,7 +292,9 @@
     },
 
     get diffs() {
-      return Polymer.dom(this.root).querySelectorAll('gr-diff-host');
+      // Polymer2: querySelectorAll returns NodeList instead of Array.
+      return Array.from(
+          Polymer.dom(this.root).querySelectorAll('gr-diff-host'));
     },
 
     openDiffPrefs() {
@@ -859,7 +862,9 @@
 
     _filesChanged() {
       Polymer.dom.flush();
-      const files = Polymer.dom(this.root).querySelectorAll('.file-row');
+      // 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);
     },
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 3e4e763..5cb0d36 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
@@ -1372,7 +1372,7 @@
         id: '503008e2_0ab203ee',
         line: 10,
         updated: '2018-02-14 22:07:43.000000000',
-        message: 'response',
+        message: 'a comment',
         unresolved: true,
       },
       {
@@ -1381,7 +1381,7 @@
         line: 20,
         in_reply_to: 'ecf0b9fa_fe1a5f62',
         updated: '2018-02-13 22:07:43.000000000',
-        message: 'a comments',
+        message: 'response',
         unresolved: true,
       },
     ];
@@ -1416,6 +1416,7 @@
         ignore_whitespace: 'IGNORE_NONE',
       };
       diff.diff = mock.diffResponse;
+      diff.$.diff.flushDebouncer('renderDiffTable');
     };
 
     const renderAndGetNewDiffs = function(index) {
@@ -1718,11 +1719,43 @@
     });
 
     test('reloadCommentsForThreadWithRootId', () => {
+      // Expand the commit message diff
+      MockInteractions.keyUpOn(element, 73, 'shift', 'i');
+      const diffs = renderAndGetNewDiffs(0);
+      flushAsynchronousOperations();
+
+      // Two comment threads should be generated by renderAndGetNewDiffs
+      const threadEls = diffs[0].getThreadEls();
+      assert.equal(threadEls.length, 2);
+      const threadElsByRootId = new Map(
+          threadEls.map(threadEl => [threadEl.rootId, threadEl]));
+
+      const thread1 = threadElsByRootId.get('503008e2_0ab203ee');
+      assert.equal(thread1.comments.length, 1);
+      assert.equal(thread1.comments[0].message, 'a comment');
+      assert.equal(thread1.comments[0].line, 10);
+
+      const thread2 = threadElsByRootId.get('ecf0b9fa_fe1a5f62');
+      assert.equal(thread2.comments.length, 2);
+      assert.isTrue(thread2.comments[0].unresolved);
+      assert.equal(thread2.comments[0].message, 'another comment');
+      assert.equal(thread2.comments[0].line, 20);
+
       const commentStub =
           sandbox.stub(element.changeComments, 'getCommentsForThread');
       const commentStubRes1 = [
         {
           patch_set: 2,
+          id: '503008e2_0ab203ee',
+          line: 20,
+          updated: '2018-02-08 18:49:18.000000000',
+          message: 'edited text',
+          unresolved: false,
+        },
+      ];
+      const commentStubRes2 = [
+        {
+          patch_set: 2,
           id: 'ecf0b9fa_fe1a5f62',
           line: 20,
           updated: '2018-02-08 18:49:18.000000000',
@@ -1733,6 +1766,7 @@
           patch_set: 2,
           id: '503008e2_0ab203ee',
           line: 10,
+          in_reply_to: 'ecf0b9fa_fe1a5f62',
           updated: '2018-02-14 22:07:43.000000000',
           message: 'response',
           unresolved: true,
@@ -1741,57 +1775,35 @@
           patch_set: 2,
           id: '503008e2_0ab203ef',
           line: 20,
-          in_reply_to: 'ecf0b9fa_fe1a5f62',
+          in_reply_to: '503008e2_0ab203ee',
           updated: '2018-02-15 22:07:43.000000000',
           message: 'a third comment in the thread',
           unresolved: true,
         },
       ];
-      const commentStubRes2 = [
-        {
-          patch_set: 2,
-          id: 'ecf0b9fa_fe1a5f62',
-          line: 20,
-          updated: '2018-02-08 18:49:18.000000000',
-          message: 'edited text',
-          unresolved: false,
-        },
-      ];
-      commentStub.withArgs('cc788d2c_cb1d728c').returns(
+      commentStub.withArgs('503008e2_0ab203ee').returns(
           commentStubRes1);
       commentStub.withArgs('ecf0b9fa_fe1a5f62').returns(
           commentStubRes2);
-      // Expand the commit message diff
-      MockInteractions.keyUpOn(element, 73, 'shift', 'i');
-      const diffs = renderAndGetNewDiffs(0);
-      flushAsynchronousOperations();
-
-      // Two comment threads sould be generated
-      const commentThreadEls = diffs[0].getThreadEls();
-      assert(commentThreadEls[0].comments.length, 2);
-      assert(commentThreadEls[1].comments.length, 1);
-      assert.isTrue(commentThreadEls[1].comments[0].unresolved);
-      assert.equal(commentThreadEls[1].comments[0].message, 'another comment');
-
-      // Reload comments from the first comment thread, which should have a new
-      // reply.
-      element.reloadCommentsForThreadWithRootId('cc788d2c_cb1d728c',
-          '/COMMIT_MSG');
-      assert(commentThreadEls[0].comments.length, 3);
-
 
       // Reload comments from the first comment thread, which should have a
       // an updated message and a toggled resolve state.
+      element.reloadCommentsForThreadWithRootId('503008e2_0ab203ee',
+          '/COMMIT_MSG');
+      assert.equal(thread1.comments.length, 1);
+      assert.isFalse(thread1.comments[0].unresolved);
+      assert.equal(thread1.comments[0].message, 'edited text');
+
+      // Reload comments from the second comment thread, which should have a new
+      // reply.
       element.reloadCommentsForThreadWithRootId('ecf0b9fa_fe1a5f62',
           '/COMMIT_MSG');
-      assert(commentThreadEls[1].comments.length, 1);
-      assert.isFalse(commentThreadEls[1].comments[0].unresolved);
-      assert.equal(commentThreadEls[1].comments[0].message, 'edited text');
+      assert.equal(thread2.comments.length, 3);
 
       const commentStubCount = commentStub.callCount;
       const getThreadsSpy = sandbox.spy(diffs[0], 'getThreadEls');
 
-      // Should not be getting threadss when the file is not expanded.
+      // Should not be getting threads when the file is not expanded.
       element.reloadCommentsForThreadWithRootId('ecf0b9fa_fe1a5f62',
           'other/file');
       assert.isFalse(getThreadsSpy.called);
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 d2ff035..7755a60 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-included-in-dialog',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the user presses the close button.
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 0ac5019..27c5baa 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
@@ -61,7 +61,7 @@
           background-color: var(--button-background-color, var(--table-header-background-color));
           color: var(--primary-text-color);
           padding: .2em .85em;
-          @apply(--vote-chip-styles);
+          @apply --vote-chip-styles;
         }
       }
       gr-button.iron-selected.max {
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 aa5b61a..9b5847d 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-label-score-row',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when any label is changed.
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 8734da2..eaf39bc 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-label-scores',
+    _legacyUndefinedCheck: true,
     properties: {
       _labels: {
         type: Array,
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 be03b23..7b49d2a 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
@@ -133,9 +133,12 @@
         right: var(--default-horizontal-margin);
         top: 10px;
       }
-      .date {
+      span.date {
         color: var(--deemphasized-text-color);
       }
+      span.date:hover {
+        text-decoration: underline;
+      }
       .dateContainer iron-icon {
         cursor: pointer;
       }
@@ -228,12 +231,12 @@
             </span>
           </template>
           <template is="dom-if" if="[[message.id]]">
-            <a class="date" href$="[[_computeMessageHash(message)]]" on-tap="_handleLinkTap">
+            <span class="date" on-tap="_handleAnchorTap">
               <gr-date-formatter
                   has-tooltip
                   show-date-and-time
                   date-str="[[message.date]]"></gr-date-formatter>
-            </a>
+            </span>
           </template>
           <iron-icon
               id="expandToggle"
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 0590c73..837ffe0 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
@@ -22,12 +22,7 @@
 
   Polymer({
     is: 'gr-message',
-
-    /**
-     * Fired when this message's permalink is tapped.
-     *
-     * @event scroll-to
-     */
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when this message's reply link is tapped.
@@ -35,6 +30,12 @@
      * @event reply
      */
 
+    /**
+     * Fired when the message's timestamp is tapped.
+     *
+     * @event message-anchor-tap
+     */
+
     listeners: {
       tap: '_handleTap',
     },
@@ -223,22 +224,12 @@
       return classes.join(' ');
     },
 
-    _computeMessageHash(message) {
-      return '#message-' + message.id;
-    },
-
-    _handleLinkTap(e) {
+    _handleAnchorTap(e) {
       e.preventDefault();
-
-      this.fire('scroll-to', {message: this.message}, {bubbles: false});
-
-      const hash = this._computeMessageHash(this.message);
-      // Don't add the hash to the window history if it's already there.
-      // Otherwise you mess up expected back button behavior.
-      if (window.location.hash == hash) { return; }
-      // Change the URL but don’t trigger a nav event. Otherwise it will
-      // reload the page.
-      page.show(window.location.pathname + hash, null, false);
+      this.dispatchEvent(new CustomEvent('message-anchor-tap', {
+        bubbles: true,
+        detail: {id: this.message.id},
+      }));
     },
 
     _handleReplyTap(e) {
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 870f366..64a5b26 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
@@ -164,6 +164,24 @@
       });
     });
 
+    test('clicking on date link fires event', () => {
+      element.message = {
+        type: 'REVIEWER_UPDATE',
+        updated: '2016-01-12 20:24:49.448000000',
+        reviewer: {},
+        id: '47c43261_55aa2c41',
+      };
+      flushAsynchronousOperations();
+      const stub = sinon.stub();
+      element.addEventListener('message-anchor-tap', stub);
+      const dateEl = element.$$('.date');
+      assert.ok(dateEl);
+      MockInteractions.tap(dateEl);
+
+      assert.isTrue(stub.called);
+      assert.deepEqual(stub.lastCall.args[0].detail, {id: element.message.id});
+    });
+
     test('votes', () => {
       element.message = {
         author: {},
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 0a7dacc..80708a1 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
@@ -109,7 +109,7 @@
           hide-automated="[[_hideAutomated]]"
           project-name="[[projectName]]"
           show-reply-button="[[showReplyButtons]]"
-          on-scroll-to="_handleScrollTo"
+          on-message-anchor-tap="_handleAnchorTap"
           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 6d8bb45..bd23a40 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,6 +27,7 @@
 
   Polymer({
     is: 'gr-messages-list',
+    _legacyUndefinedCheck: true,
 
     properties: {
       changeNum: Number,
@@ -184,8 +185,8 @@
       this.handleExpandCollapse(!this._expanded);
     },
 
-    _handleScrollTo(e) {
-      this.scrollToMessage(e.detail.message.id);
+    _handleAnchorTap(e) {
+      this.scrollToMessage(e.detail.id);
     },
 
     _hasAutomatedMessages(messages) {
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 7f4d782..07f6e20 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-related-changes-list',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when a new section is loaded so that the change view can determine
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 eb06b0f..692cb93 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
@@ -86,7 +86,6 @@
           '+1',
         ],
       };
-      element.serverConfig = {note_db_enabled: true};
       sandbox.stub(element, 'fetchChangeUpdates')
           .returns(Promise.resolve({isLatest: true}));
     };
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 45b9c15..f8d43cb 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
@@ -171,21 +171,19 @@
               on-account-text-changed="_handleAccountTextEntry">
           </gr-account-list>
         </div>
-        <template is="dom-if" if="[[serverConfig.note_db_enabled]]">
-          <div class="peopleList">
-            <div class="peopleListLabel">CC</div>
-            <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">
-            </gr-account-list>
-          </div>
-        </template>
+        <div class="peopleList">
+          <div class="peopleListLabel">CC</div>
+          <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">
+          </gr-account-list>
+        </div>
         <gr-overlay
             id="reviewerConfirmationOverlay"
             on-iron-overlay-canceled="_cancelPendingReviewer">
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 40a0922..1ab961b 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
@@ -58,6 +58,7 @@
 
   Polymer({
     is: 'gr-reply-dialog',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when a reply is successfully sent.
@@ -141,10 +142,6 @@
       },
       permittedLabels: Object,
       /**
-       * @type {{ note_db_enabled: boolean }}
-       */
-      serverConfig: Object,
-      /**
        * @type {{ commentlinks: Array }}
        */
       projectConfig: Object,
@@ -194,10 +191,6 @@
         type: String,
         computed: '_computeSendButtonLabel(canBeStarted)',
       },
-      _ccsEnabled: {
-        type: Boolean,
-        computed: '_computeCCsEnabled(serverConfig)',
-      },
       _savingComments: Boolean,
       _reviewersMutated: {
         type: Boolean,
@@ -244,7 +237,7 @@
     },
 
     observers: [
-      '_changeUpdated(change.reviewers.*, change.owner, serverConfig)',
+      '_changeUpdated(change.reviewers.*, change.owner)',
       '_ccsChanged(_ccs.splices)',
       '_reviewersChanged(_reviewers.splices)',
     ],
@@ -628,14 +621,14 @@
         'Say something nice...';
     },
 
-    _changeUpdated(changeRecord, owner, serverConfig) {
-      this._rebuildReviewerArrays(changeRecord.base, owner, serverConfig);
+    _changeUpdated(changeRecord, owner) {
+      this._rebuildReviewerArrays(changeRecord.base, owner);
     },
 
-    _rebuildReviewerArrays(change, owner, serverConfig) {
+    _rebuildReviewerArrays(change, owner) {
       this._owner = owner;
 
-      let reviewers = [];
+      const reviewers = [];
       const ccs = [];
 
       for (const key in change) {
@@ -660,12 +653,7 @@
         }
       }
 
-      if (this._ccsEnabled) {
-        this._ccs = ccs;
-      } else {
-        this._ccs = [];
-        reviewers = reviewers.concat(ccs);
-      }
+      this._ccs = ccs;
       this._reviewers = reviewers;
     },
 
@@ -719,13 +707,12 @@
       this.fire('cancel', null, {bubbles: false});
       this.$.textarea.closeDropdown();
       this._purgeReviewersPendingRemove(true);
-      this._rebuildReviewerArrays(this.change.reviewers, this._owner,
-          this.serverConfig);
+      this._rebuildReviewerArrays(this.change.reviewers, this._owner);
     },
 
     _saveTapHandler(e) {
       e.preventDefault();
-      if (this._ccsEnabled && !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;
@@ -741,7 +728,7 @@
     },
 
     _submit() {
-      if (this._ccsEnabled && !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;
@@ -855,10 +842,6 @@
       return canBeStarted ? ButtonTooltips.START_REVIEW : ButtonTooltips.SEND;
     },
 
-    _computeCCsEnabled(serverConfig) {
-      return serverConfig && serverConfig.note_db_enabled;
-    },
-
     _computeSavingLabelClass(savingComments) {
       return savingComments ? 'saving' : '';
     },
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 733a490..5577c1b 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
@@ -97,7 +97,6 @@
           '+1',
         ],
       };
-      element.serverConfig = {};
 
       getDraftCommentStub = sandbox.stub(element.$.storage, 'getDraftComment');
       setDraftCommentStub = sandbox.stub(element.$.storage, 'setDraftComment');
@@ -296,7 +295,6 @@
       const noButton =
           element.$$('.reviewerConfirmationButtons gr-button:last-child');
 
-      element.serverConfig = {note_db_enabled: true};
       element._ccPendingConfirmation = null;
       element._reviewerPendingConfirmation = null;
       flushAsynchronousOperations();
@@ -409,7 +407,6 @@
     });
 
     test('_reviewersMutated when account-text-change is fired from ccs', () => {
-      element.serverConfig = {note_db_enabled: true};
       flushAsynchronousOperations();
       assert.isFalse(element._reviewersMutated);
       assert.isTrue(element.$$('#ccs').allowAnyInput);
@@ -496,19 +493,6 @@
       flush(() => { element.send(); });
     });
 
-    test('ccs are displayed if NoteDb is enabled', () => {
-      function hasCc() {
-        flushAsynchronousOperations();
-        return !!element.$$('#ccs');
-      }
-
-      element.serverConfig = {};
-      assert.isFalse(hasCc());
-
-      element.serverConfig = {note_db_enabled: true};
-      assert.isTrue(hasCc());
-    });
-
     test('filterReviewerSuggestion', () => {
       const owner = makeAccount();
       const reviewer1 = makeAccount();
@@ -540,7 +524,6 @@
 
     test('_focusOn', () => {
       sandbox.spy(element, '_chooseFocusTarget');
-      element.serverConfig = {note_db_enabled: true};
       flushAsynchronousOperations();
       const textareaStub = sandbox.stub(element.$.textarea, 'async');
       const reviewerEntryStub = sandbox.stub(element.$.reviewers.focusStart,
@@ -680,7 +663,6 @@
     });
 
     test('moving from cc to reviewer', () => {
-      element.serverConfig = {note_db_enabled: true};
       element._reviewersPendingRemove = {
         CC: [],
         REVIEWER: [],
@@ -714,7 +696,6 @@
     });
 
     test('migrate reviewers between states', done => {
-      element.serverConfig = {note_db_enabled: true};
       element._reviewersPendingRemove = {
         CC: [],
         REVIEWER: [],
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 ab67704..4f1b051 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-reviewer-list',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the "Add reviewer..." button is tapped.
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 2201a9a..4d8e5ae 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
@@ -18,7 +18,7 @@
 <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="../../diff/gr-diff-comment-thread/gr-diff-comment-thread.html">
+<link rel="import" href="../../shared/gr-comment-thread/gr-comment-thread.html">
 
 <dom-module id="gr-thread-list">
   <template>
@@ -28,7 +28,7 @@
         min-height: 20rem;
         padding: 1rem;
       }
-      gr-diff-comment-thread {
+      gr-comment-thread {
         display: block;
         margin-bottom: .5rem;
         max-width: 80ch;
@@ -54,9 +54,9 @@
         display: flex;
         margin-right: 1rem;
       }
-      .draftsOnly:not(.unresolvedOnly) gr-diff-comment-thread[has-draft],
-      .unresolvedOnly:not(.draftsOnly) gr-diff-comment-thread[unresolved],
-      .draftsOnly.unresolvedOnly gr-diff-comment-thread[has-draft][unresolved] {
+      .draftsOnly:not(.unresolvedOnly) gr-comment-thread[has-draft],
+      .unresolvedOnly:not(.draftsOnly) gr-comment-thread[unresolved],
+      .draftsOnly.unresolvedOnly gr-comment-thread[has-draft][unresolved] {
         display: block
       }
     </style>
@@ -82,7 +82,7 @@
           as="thread"
           initial-count="5"
           target-framerate="60">
-        <gr-diff-comment-thread
+        <gr-comment-thread
             show-file-path
             change-num="[[changeNum]]"
             comments="[[thread.comments]]"
@@ -94,7 +94,7 @@
             path="[[thread.path]]"
             root-id="{{thread.rootId}}"
             on-thread-changed="_handleCommentsChanged"
-            on-thread-discard="_handleThreadDiscard"></gr-diff-comment-thread>
+            on-thread-discard="_handleThreadDiscard"></gr-comment-thread>
       </template>
     </div>
   </template>
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 dd6db8c..317685b 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,6 +25,7 @@
 
   Polymer({
     is: 'gr-thread-list',
+    _legacyUndefinedCheck: true,
 
     properties: {
       /** @type {?} */
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 804446a..792644e 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
@@ -171,7 +171,7 @@
       ];
       flushAsynchronousOperations();
       threadElements = Polymer.dom(element.root)
-          .querySelectorAll('gr-diff-comment-thread');
+          .querySelectorAll('gr-comment-thread');
     });
 
     teardown(() => {
@@ -188,7 +188,7 @@
 
     test('there are five threads by default', () => {
       assert.equal(Polymer.dom(element.root)
-          .querySelectorAll('gr-diff-comment-thread').length, 5);
+          .querySelectorAll('gr-comment-thread').length, 5);
     });
 
     test('_computeSortedThreads', () => {
@@ -231,14 +231,14 @@
       MockInteractions.tap(element.$.unresolvedToggle);
       flushAsynchronousOperations();
       assert.equal(Polymer.dom(element.root)
-          .querySelectorAll('gr-diff-comment-thread').length, 3);
+          .querySelectorAll('gr-comment-thread').length, 3);
     });
 
     test('toggle drafts only shows threads with draft comments', () => {
       MockInteractions.tap(element.$.draftToggle);
       flushAsynchronousOperations();
       assert.equal(Polymer.dom(element.root)
-          .querySelectorAll('gr-diff-comment-thread').length, 2);
+          .querySelectorAll('gr-comment-thread').length, 2);
     });
 
     test('toggle drafts and unresolved only shows threads with drafts and ' +
@@ -247,7 +247,7 @@
       MockInteractions.tap(element.$.unresolvedToggle);
       flushAsynchronousOperations();
       assert.equal(Polymer.dom(element.root)
-          .querySelectorAll('gr-diff-comment-thread').length, 2);
+          .querySelectorAll('gr-comment-thread').length, 2);
     });
 
     test('modification events are consumed and displatched', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js
index 02d00cf..df96be2 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,6 +29,7 @@
 
   Polymer({
     is: 'gr-upload-help-dialog',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the user presses the close button.
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 72ec7fa..af4510d 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,6 +21,7 @@
 
   Polymer({
     is: 'gr-account-dropdown',
+    _legacyUndefinedCheck: true,
 
     properties: {
       account: Object,
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 8d3b58e..5679408 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-error-dialog',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the dismiss button is pressed.
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 3ec4bb5..4ca106e 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
@@ -18,6 +18,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="../../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">
 <link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
@@ -32,6 +33,7 @@
           confirm-on-enter></gr-error-dialog>
     </gr-overlay>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+    <gr-reporting id="reporting"></gr-reporting>
   </template>
   <script src="gr-error-manager.js"></script>
 </dom-module>
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 b254182..38dfecb 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,6 +27,7 @@
 
   Polymer({
     is: 'gr-error-manager',
+    _legacyUndefinedCheck: true,
 
     behaviors: [
       Gerrit.BaseUrlBehavior,
@@ -283,6 +284,7 @@
     },
 
     _showErrorDialog(message) {
+      this.$.reporting.reportErrorDialog(message);
       this.$.errorDialog.text = message;
       this.$.errorOverlay.open();
     },
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 f92feae..88e3efd 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
@@ -304,11 +304,14 @@
     test('show-error', () => {
       const openStub = sandbox.stub(element.$.errorOverlay, 'open');
       const closeStub = sandbox.stub(element.$.errorOverlay, 'close');
+      const reportStub = sandbox.stub(element.$.reporting, 'reportErrorDialog');
+
       const message = 'test message';
       element.fire('show-error', {message});
       flushAsynchronousOperations();
 
       assert.isTrue(openStub.called);
+      assert.isTrue(reportStub.called);
       assert.equal(element.$.errorDialog.text, message);
 
       element.$.errorDialog.fire('dismiss');
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 89d1091..e8c6479 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-key-binding-display',
+    _legacyUndefinedCheck: true,
 
     properties: {
       /** @type {Array<string>} */
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 5b29972..f206db1 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,6 +21,7 @@
 
   Polymer({
     is: 'gr-keyboard-shortcuts-dialog',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the user presses the close button.
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 e38b16e..e552cda 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
@@ -155,6 +155,9 @@
       gr-account-dropdown {
         color: var(--header-text-color);
       }
+      #mobileSearch {
+        display: none;
+      }
       @media screen and (max-width: 50em) {
         .bigTitle {
           font-size: var(--font-size-large);
@@ -166,6 +169,9 @@
         .links > li.hideOnMobile {
           display: none;
         }
+        #mobileSearch {
+          display: inline-flex;
+        }
         .accountContainer {
           margin-left: .5em !important;
         }
@@ -206,6 +212,7 @@
             class="hideOnMobile"
             name="header-browse-source"></gr-endpoint-decorator>
         <div class="accountContainer" id="accountContainer">
+          <iron-icon id="mobileSearch" icon="gr-icons:search" on-tap='_onMobileSearchTap'></iron-icon>
           <div class$="[[_computeIsInvisible(_registerURL)]]">
             <a
                 class="registerButton"
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 65757c7..69fc89f 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,6 +71,7 @@
 
   Polymer({
     is: 'gr-main-header',
+    _legacyUndefinedCheck: true,
 
     hostAttributes: {
       role: 'banner',
@@ -276,8 +277,7 @@
       if (!account) { return; }
 
       this.$.restAPI.getPreferences().then(prefs => {
-        this._userLinks =
-            prefs.my.map(this._fixCustomMenuItem).filter(this._isSupportedLink);
+        this._userLinks = prefs.my.map(this._fixCustomMenuItem);
       });
     },
 
@@ -312,13 +312,13 @@
       return linkObj;
     },
 
-    _isSupportedLink(linkObj) {
-      // Groups are not yet supported.
-      return !linkObj.url.startsWith('/groups');
-    },
-
     _generateSettingsLink() {
       return this.getBaseUrl() + '/settings/';
     },
+
+    _onMobileSearchTap(e) {
+      e.preventDefault();
+      this.fire('mobile-search', null, {bubbles: false});
+    },
   });
 })();
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 7732870..89b3908 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
@@ -91,17 +91,6 @@
       ]);
     });
 
-    test('filter unsupported urls', () => {
-      assert.deepEqual([
-        {url: '/c/331788/'},
-        {url: '/groups/self'},
-        {url: 'https://awesometown.com/#hashyhash'},
-      ].filter(element._isSupportedLink), [
-        {url: '/c/331788/'},
-        {url: 'https://awesometown.com/#hashyhash'},
-      ]);
-    });
-
     test('user links', () => {
       const defaultLinks = [{
         title: 'Faves',
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 3d0f6f2..0d620bd 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
@@ -33,6 +33,8 @@
     //        also be provided.
     //    - `edit`, optional, Boolean: whether or not to load the file list with
     //        edit controls.
+    //    - `messageHash`, optional, String: the hash of the change message to
+    //        scroll to.
     //
     // - Gerrit.Nav.View.SEARCH:
     //    - `query`, optional, String: the literal search query. If provided,
@@ -364,9 +366,11 @@
        * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
        *     used for none.
        * @param {boolean=} opt_isEdit
+       * @param {string=} opt_messageHash
        * @return {string}
        */
-      getUrlForChange(change, opt_patchNum, opt_basePatchNum, opt_isEdit) {
+      getUrlForChange(change, opt_patchNum, opt_basePatchNum, opt_isEdit,
+          opt_messageHash) {
         if (opt_basePatchNum === PARENT_PATCHNUM) {
           opt_basePatchNum = undefined;
         }
@@ -380,6 +384,7 @@
           basePatchNum: opt_basePatchNum,
           edit: opt_isEdit,
           host: change.internalHost || undefined,
+          messageHash: opt_messageHash,
         });
       },
 
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 9b4c611..0947d77 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
@@ -69,6 +69,11 @@
     CATEGORY: 'exception',
   };
 
+  const ERROR_DIALOG = {
+    TYPE: 'error',
+    CATEGORY: 'Error Dialog',
+  };
+
   const TIMER = {
     CHANGE_DISPLAYED: 'ChangeDisplayed',
     CHANGE_LOAD_FULL: 'ChangeFullyLoaded',
@@ -139,6 +144,7 @@
   // eslint-disable-next-line prefer-const
   let GrReporting = Polymer({
     is: 'gr-reporting',
+    _legacyUndefinedCheck: true,
 
     properties: {
       category: String,
@@ -192,7 +198,7 @@
       };
       document.dispatchEvent(new CustomEvent(type, {detail}));
       if (opt_noLog) { return; }
-      if (type === ERROR.TYPE) {
+      if (type === ERROR.TYPE && category === ERROR.CATEGORY) {
         console.error(eventValue.error || eventName);
       } else {
         console.log(eventName + (eventValue !== undefined ?
@@ -212,7 +218,7 @@
      *     logged to the JS console.
      */
     cachingReporter(type, category, eventName, eventValue, opt_noLog) {
-      if (type === ERROR.TYPE) {
+      if (type === ERROR.TYPE && category === ERROR.CATEGORY) {
         console.error(eventValue.error || eventName);
       }
       if (this._arePluginsLoaded()) {
@@ -231,10 +237,8 @@
      * User-perceived app start time, should be reported when the app is ready.
      */
     appStarted(hidden) {
-      const startTime =
-          new Date().getTime() - this.performanceTiming.navigationStart;
       this.reporter(TIMING.TYPE, TIMING.CATEGORY_UI_LATENCY,
-          TIMING.APP_STARTED, startTime);
+          TIMING.APP_STARTED, this.now());
       if (hidden) {
         this.reporter(PAGE_VISIBILITY.TYPE, PAGE_VISIBILITY.CATEGORY,
             PAGE_VISIBILITY.STARTED_HIDDEN);
@@ -458,6 +462,11 @@
       // Mark the time and reinitialize the timer.
       timer.end().reset();
     },
+
+    reportErrorDialog(message) {
+      this.reporter(ERROR_DIALOG.TYPE, ERROR_DIALOG.CATEGORY,
+          'ErrorDialog: ' + message);
+    },
   });
 
   window.GrReporting = GrReporting;
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 cd5e2e5..29e70ea 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
@@ -61,11 +61,11 @@
     });
 
     test('appStarted', () => {
+      sandbox.stub(element, 'now').returns(42);
       element.appStarted(true);
       assert.isTrue(
           element.reporter.calledWithExactly(
-              'timing-report', 'UI Latency', 'App Started',
-              NOW_TIME - fakePerformance.navigationStart
+              'timing-report', 'UI Latency', 'App Started', 42
           ));
       assert.isTrue(
           element.reporter.calledWithExactly(
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 2c9a732..8e21738 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -38,6 +38,9 @@
     // Matches /admin/groups/[uuid-]<group>
     GROUP: /^\/admin\/groups\/(?:uuid-)?([^,]+)$/,
 
+    // Redirects /groups/self to /settings/#Groups for GWT compatibility
+    GROUP_SELF: /^\/groups\/self/,
+
     // Matches /admin/groups/[uuid-]<group>,info (backwords compat with gwtui)
     // Redirects to /admin/groups/[uuid-]<group>
     GROUP_INFO: /^\/admin\/groups\/(?:uuid-)?(.+),info$/,
@@ -206,6 +209,7 @@
 
   Polymer({
     is: 'gr-router',
+    _legacyUndefinedCheck: true,
 
     properties: {
       _app: {
@@ -401,6 +405,9 @@
       } else if (params.edit) {
         suffix += ',edit';
       }
+      if (params.messageHash) {
+        suffix += params.messageHash;
+      }
       if (params.project) {
         const encodedProject = this.encodeURL(params.project, true);
         return `/c/${encodedProject}/+/${params.changeNum}${suffix}`;
@@ -584,16 +591,6 @@
         params.patchNum = params.basePatchNum;
         params.basePatchNum = null;
       }
-      // In GWTUI, edits are represented in URLs with either 0 or 'edit'.
-      // TODO(kaspern): Remove this normalization when GWT UI is gone.
-      if (this.patchNumEquals(params.basePatchNum, 0)) {
-        params.basePatchNum = this.EDIT_NAME;
-        needsRedirect = true;
-      }
-      if (this.patchNumEquals(params.patchNum, 0)) {
-        params.patchNum = this.EDIT_NAME;
-        needsRedirect = true;
-      }
       return needsRedirect;
     },
 
@@ -645,7 +642,7 @@
           return Promise.resolve();
         } else {
           this._redirectToLogin(data.canonicalPath);
-          return Promise.reject();
+          return Promise.reject(new Error());
         }
       });
     },
@@ -754,6 +751,9 @@
       this._mapRoute(RoutePattern.GROUP_LIST_FILTER,
           '_handleGroupListFilterRoute', true);
 
+      this._mapRoute(RoutePattern.GROUP_SELF, '_handleGroupSelfRedirectRoute',
+          true);
+
       this._mapRoute(RoutePattern.GROUP, '_handleGroupRoute', true);
 
       this._mapRoute(RoutePattern.PROJECT_OLD,
@@ -1047,6 +1047,10 @@
       this._redirect('/admin/groups/' + encodeURIComponent(data.params[0]));
     },
 
+    _handleGroupSelfRedirectRoute(data) {
+      this._redirect('/settings/#Groups');
+    },
+
     _handleGroupRoute(data) {
       this._setParams({
         view: Gerrit.Nav.View.GROUP,
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 a907db2..5dcddce 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
@@ -179,6 +179,7 @@
         '_handleGroupListOffsetRoute',
         '_handleGroupMembersRoute',
         '_handleGroupRoute',
+        '_handleGroupSelfRedirectRoute',
         '_handleNewAgreementsRoute',
         '_handlePluginListFilterOffsetRoute',
         '_handlePluginListFilterRoute',
@@ -327,6 +328,9 @@
         paramsWithQuery.basePatchNum = 5;
         assert.equal(element._generateUrl(paramsWithQuery),
             '/c/test/+/1234/5..10?revert&foo=bar');
+
+        params.messageHash = '#123';
+        assert.equal(element._generateUrl(params), '/c/test/+/1234/5..10#123');
       });
 
       test('change with repo name encoding', () => {
@@ -598,30 +602,6 @@
           assert.isNotOk(params.basePatchNum);
           assert.equal(params.patchNum, 4);
         });
-
-        test('range 0..n normalizes to edit..n', () => {
-          const params = {basePatchNum: 0, patchNum: 4};
-          const needsRedirect = element._normalizePatchRangeParams(params);
-          assert.isTrue(needsRedirect);
-          assert.equal(params.basePatchNum, 'edit');
-          assert.equal(params.patchNum, 4);
-        });
-
-        test('range n..0 normalizes to n..edit', () => {
-          const params = {basePatchNum: 4, patchNum: 0};
-          const needsRedirect = element._normalizePatchRangeParams(params);
-          assert.isTrue(needsRedirect);
-          assert.equal(params.basePatchNum, 4);
-          assert.equal(params.patchNum, 'edit');
-        });
-
-        test('range 0..0 normalizes to edit', () => {
-          const params = {basePatchNum: 0, patchNum: 0};
-          const needsRedirect = element._normalizePatchRangeParams(params);
-          assert.isTrue(needsRedirect);
-          assert.isNotOk(params.basePatchNum);
-          assert.equal(params.patchNum, 'edit');
-        });
       });
     });
 
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 9a2be96..c877ac4 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
@@ -36,7 +36,12 @@
     'conflicts:',
     'deleted:',
     'delta:',
+    'dir:',
+    'directory:',
+    'ext:',
+    'extension:',
     'file:',
+    'footer:',
     'from:',
     'has:',
     'has:draft',
@@ -65,6 +70,8 @@
     'is:wip',
     'label:',
     'message:',
+    'onlyexts:',
+    'onlyextensions:',
     'owner:',
     'ownerin:',
     'parentproject:',
@@ -99,6 +106,7 @@
 
   Polymer({
     is: 'gr-search-bar',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when a search is committed
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 f2f9a4c..65141aa 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,6 +23,7 @@
 
   Polymer({
     is: 'gr-smart-search',
+    _legacyUndefinedCheck: true,
 
     properties: {
       searchQuery: String,
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 b7994e6..7bf71f5 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,6 +19,7 @@
 
   Polymer({
     is: 'comment-api-mock',
+    _legacyUndefinedCheck: true,
 
     properties: {
       _changeComments: Object,
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 6c0d19a..37491d2 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
@@ -485,6 +485,7 @@
 
   Polymer({
     is: 'gr-comment-api',
+    _legacyUndefinedCheck: true,
 
     properties: {
       _changeComments: Object,
diff --git a/polygerrit-ui/app/elements/diff/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js b/polygerrit-ui/app/elements/diff/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js
deleted file mode 100644
index b86b72a..0000000
--- a/polygerrit-ui/app/elements/diff/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js
+++ /dev/null
@@ -1,53 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-(function() {
-  'use strict';
-
-  Polymer({
-    is: 'gr-confirm-delete-comment-dialog',
-
-    /**
-     * Fired when the confirm button is pressed.
-     *
-     * @event confirm
-     */
-
-    /**
-     * Fired when the cancel button is pressed.
-     *
-     * @event cancel
-     */
-
-    properties: {
-      message: String,
-    },
-
-    resetFocus() {
-      this.$.messageInput.textarea.focus();
-    },
-
-    _handleConfirmTap(e) {
-      e.preventDefault();
-      this.fire('confirm', {reason: this.message}, {bubbles: false});
-    },
-
-    _handleCancelTap(e) {
-      e.preventDefault();
-      this.fire('cancel', null, {bubbles: false});
-    },
-  });
-})();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-binary.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-binary.js
index b2fc64c..6f5a8d3 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-binary.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-binary.js
@@ -14,14 +14,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-(function(window, GrDiffBuilderSideBySide) {
+(function(window, GrDiffBuilder) {
   'use strict';
 
   // Prevent redefinition.
   if (window.GrDiffBuilderBinary) { return; }
 
-  function GrDiffBuilderBinary(diff, comments, prefs, outputEl) {
-    GrDiffBuilder.call(this, diff, comments, null, prefs, outputEl);
+  function GrDiffBuilderBinary(diff, prefs, outputEl) {
+    GrDiffBuilder.call(this, diff, prefs, outputEl);
   }
 
   GrDiffBuilderBinary.prototype = Object.create(GrDiffBuilder.prototype);
@@ -43,4 +43,4 @@
   };
 
   window.GrDiffBuilderBinary = GrDiffBuilderBinary;
-})(window, GrDiffBuilderSideBySide);
+})(window, GrDiffBuilder);
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 7eaca0d..11bea8c 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
@@ -24,10 +24,8 @@
   // arbitrary JavaScript.
   const IMAGE_MIME_PATTERN = /^image\/(bmp|gif|x-icon|jpeg|jpg|png|tiff|webp)$/;
 
-  function GrDiffBuilderImage(diff, comments, createThreadGroupFn, prefs,
-      outputEl, baseImage, revisionImage) {
-    GrDiffBuilderSideBySide.call(this, diff, comments, createThreadGroupFn,
-        prefs, outputEl, []);
+  function GrDiffBuilderImage(diff, prefs, outputEl, baseImage, revisionImage) {
+    GrDiffBuilderSideBySide.call(this, diff, prefs, outputEl, []);
     this._baseImage = baseImage;
     this._revisionImage = revisionImage;
   }
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 fafae63..1ef278f 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
@@ -20,10 +20,8 @@
   // Prevent redefinition.
   if (window.GrDiffBuilderSideBySide) { return; }
 
-  function GrDiffBuilderSideBySide(diff, comments, createThreadGroupFn, prefs,
-      outputEl, layers) {
-    GrDiffBuilder.call(this, diff, comments, createThreadGroupFn, prefs,
-        outputEl, layers);
+  function GrDiffBuilderSideBySide(diff, prefs, outputEl, layers) {
+    GrDiffBuilder.call(this, diff, prefs, outputEl, layers);
   }
   GrDiffBuilderSideBySide.prototype = Object.create(GrDiffBuilder.prototype);
   GrDiffBuilderSideBySide.prototype.constructor = GrDiffBuilderSideBySide;
@@ -91,18 +89,14 @@
 
   GrDiffBuilderSideBySide.prototype._appendPair = function(section, row, line,
       lineNumber, side) {
-    const lineEl = this._createLineEl(line, lineNumber, line.type, side);
-    lineEl.classList.add(side);
-    row.appendChild(lineEl);
+    const lineNumberEl = this._createLineEl(line, lineNumber, line.type, side);
+    lineNumberEl.classList.add(side);
+    row.appendChild(lineNumberEl);
     const action = this._createContextControl(section, line);
     if (action) {
       row.appendChild(action);
     } else {
-      const textEl = this._createTextEl(line, side);
-      const threadGroupEl = this._commentThreadGroupForLine(line, side);
-      if (threadGroupEl) {
-        textEl.appendChild(threadGroupEl);
-      }
+      const textEl = this._createTextEl(lineNumberEl, line, side);
       row.appendChild(textEl);
     }
   };
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 247ccf7..6be2097 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
@@ -20,10 +20,8 @@
   // Prevent redefinition.
   if (window.GrDiffBuilderUnified) { return; }
 
-  function GrDiffBuilderUnified(diff, comments, createThreadGroupFn, prefs,
-      outputEl, layers) {
-    GrDiffBuilder.call(this, diff, comments, createThreadGroupFn, prefs,
-        outputEl, layers);
+  function GrDiffBuilderUnified(diff, prefs, outputEl, layers) {
+    GrDiffBuilder.call(this, diff, prefs, outputEl, layers);
   }
   GrDiffBuilderUnified.prototype = Object.create(GrDiffBuilder.prototype);
   GrDiffBuilderUnified.prototype.constructor = GrDiffBuilderUnified;
@@ -70,28 +68,24 @@
 
   GrDiffBuilderUnified.prototype._createRow = function(section, line) {
     const row = this._createElement('tr', line.type);
-    row.appendChild(this._createBlameCell(line));
-
-    let lineEl = this._createLineEl(line, line.beforeNumber,
-        GrDiffLine.Type.REMOVE);
-    lineEl.classList.add('left');
-    row.appendChild(lineEl);
-    lineEl = this._createLineEl(line, line.afterNumber,
-        GrDiffLine.Type.ADD);
-    lineEl.classList.add('right');
-    row.appendChild(lineEl);
     row.classList.add('diff-row', 'unified');
     row.tabIndex = -1;
+    row.appendChild(this._createBlameCell(line));
+
+    let lineNumberEl = this._createLineEl(line, line.beforeNumber,
+        GrDiffLine.Type.REMOVE);
+    lineNumberEl.classList.add('left');
+    row.appendChild(lineNumberEl);
+    lineNumberEl = this._createLineEl(line, line.afterNumber,
+        GrDiffLine.Type.ADD);
+    lineNumberEl.classList.add('right');
+    row.appendChild(lineNumberEl);
 
     const action = this._createContextControl(section, line);
     if (action) {
       row.appendChild(action);
     } else {
-      const textEl = this._createTextEl(line);
-      const threadGroupEl = this._commentThreadGroupForLine(line);
-      if (threadGroupEl) {
-        textEl.appendChild(threadGroupEl);
-      }
+      const textEl = this._createTextEl(lineNumberEl, line);
       row.appendChild(textEl);
     }
     return row;
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 2a2b753..443d886 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
@@ -15,9 +15,7 @@
 limitations under the License.
 -->
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
-<link rel="import" href="../gr-diff-comment-thread/gr-diff-comment-thread.html">
-<link rel="import" href="../gr-diff-comment-thread-group/gr-diff-comment-thread-group.html">
+<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.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">
@@ -29,16 +27,16 @@
     </div>
     <gr-ranged-comment-layer
         id="rangeLayer"
-        comments="[[comments]]"></gr-ranged-comment-layer>
+        comment-ranges="[[commentRanges]]"></gr-ranged-comment-layer>
     <gr-syntax-layer
         id="syntaxLayer"
         diff="[[diff]]"></gr-syntax-layer>
     <gr-diff-processor
         id="processor"
         groups="{{_groups}}"></gr-diff-processor>
-    <gr-reporting id="reporting"></gr-reporting>
     <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>
   <script src="../gr-diff/gr-diff-group.js"></script>
   <script src="../gr-diff-highlight/gr-annotation.js"></script>
@@ -51,27 +49,11 @@
     (function() {
       'use strict';
 
-      const Defs = {};
-
-      /**
-       * @typedef {{
-       *  number: number,
-       *  leftSide: {boolean}
-       * }}
-       */
-      Defs.LineOfInterest;
-
       const DiffViewMode = {
         SIDE_BY_SIDE: 'SIDE_BY_SIDE',
         UNIFIED: 'UNIFIED_DIFF',
       };
 
-      const TimingLabel = {
-        TOTAL: 'Diff Total Render',
-        CONTENT: 'Diff Content Render',
-        SYNTAX: 'Diff Syntax Render',
-      };
-
       // 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;
@@ -83,6 +65,7 @@
 
       Polymer({
         is: 'gr-diff-builder',
+        _legacyUndefinedCheck: true,
 
         /**
          * Fired when the diff begins rendering.
@@ -91,16 +74,16 @@
          */
 
         /**
-         * Fired when the diff is rendered.
+         * Fired when the diff finishes rendering text content and starts
+         * syntax highlighting.
          *
-         * @event render
+         * @event render-content
          */
 
         /**
-         * Fired when the diff finishes rendering text content, but not
-         * necessarily syntax highlights.
+         * Fired when the diff finishes syntax highlighting.
          *
-         * @event render-content
+         * @event render-syntax
          */
 
         properties: {
@@ -109,26 +92,30 @@
           changeNum: String,
           patchNum: String,
           viewMode: String,
-          comments: Object,
           isImageDiff: Boolean,
           baseImage: Object,
           revisionImage: Object,
-          projectName: String,
           parentIndex: Number,
-          /**
-           * @type {Defs.LineOfInterest|null}
-           */
-          lineOfInterest: Object,
-
-          /**
-           * @type {function(number, booleam, !string)}
-           */
-          createCommentFn: Function,
+          path: String,
+          projectName: String,
 
           _builder: Object,
           _groups: Array,
           _layers: Array,
           _showTabs: Boolean,
+          /** @type {!Array<!Gerrit.HoveredRange>} */
+          commentRanges: {
+            type: Array,
+            value: () => [],
+          },
+          /**
+           * The promise last returned from `render()` while the asynchronous
+           * rendering is running - `null` otherwise. Provides a `cancel()`
+           * method that rejects it with `{isCancelled: true}`.
+           *
+           * @type {?Object}
+           */
+          _cancelableRenderPromise: Object,
         },
 
         get diffElement() {
@@ -139,8 +126,59 @@
           '_groupsChanged(_groups.splices)',
         ],
 
-        attached() {
-          // Setup annotation layers.
+        render(keyLocations, prefs) {
+          // Setting up annotation layers must happen after plugins are
+          // installed, and |render| satisfies the requirement, however,
+          // |attached| doesn't because in the diff view page, the element is
+          // 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).
+          this.cancel();
+
+          this._builder = this._getDiffBuilder(this.diff, prefs);
+
+          this.$.processor.context = prefs.context;
+          this.$.processor.keyLocations = keyLocations;
+
+          this._clearDiffContent();
+          this._builder.addColumns(this.diffElement, prefs.font_size);
+
+          const isBinary = !!(this.isImageDiff || this.diff.binary);
+
+          this.dispatchEvent(new CustomEvent('render-start', {bubbles: true}));
+          this._cancelableRenderPromise = util.makeCancelable(
+              this.$.processor.process(this.diff.content, isBinary)
+                  .then(() => {
+                    if (this.isImageDiff) {
+                      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}));
+                  }));
+          return this._cancelableRenderPromise
+              .finally(() => { this._cancelableRenderPromise = null; })
+              // Mocca testing does not like uncaught rejections, so we catch
+              // the cancels which are expected and should not throw errors in
+              // tests.
+              .catch(e => { if (!e.isCanceled) return Promise.reject(e); });
+        },
+
+        _setupAnnotationLayers() {
           const layers = [
             this._createTrailingWhitespaceLayer(),
             this.$.syntaxLayer,
@@ -156,56 +194,6 @@
           }
 
           this._layers = layers;
-
-          this.async(() => {
-            this._preRenderThread();
-          });
-        },
-
-        render(comments, prefs) {
-          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).
-          this.cancel();
-
-          this._builder = this._getDiffBuilder(this.diff, comments, prefs);
-
-          this.$.processor.context = prefs.context;
-          this.$.processor.keyLocations = this._getKeyLocations(comments,
-              this.lineOfInterest);
-
-          this._clearDiffContent();
-          this._builder.addColumns(this.diffElement, prefs.font_size);
-
-          const reporting = this.$.reporting;
-          const isBinary = !!(this.isImageDiff || this.diff.binary);
-
-          reporting.time(TimingLabel.TOTAL);
-          reporting.time(TimingLabel.CONTENT);
-          this.dispatchEvent(new CustomEvent('render-start', {bubbles: true}));
-          return this.$.processor.process(this.diff.content, isBinary)
-              .then(() => {
-                if (this.isImageDiff) {
-                  this._builder.renderDiff();
-                }
-                this.dispatchEvent(new CustomEvent('render-content',
-                    {bubbles: true}));
-
-                if (this._diffTooLargeForSyntax()) {
-                  this.$.syntaxLayer.enabled = false;
-                }
-
-                reporting.timeEnd(TimingLabel.CONTENT);
-                reporting.time(TimingLabel.SYNTAX);
-                return this.$.syntaxLayer.process().then(() => {
-                  reporting.timeEnd(TimingLabel.SYNTAX);
-                  reporting.timeEnd(TimingLabel.TOTAL);
-                  this.dispatchEvent(
-                      new CustomEvent('render', {bubbles: true}));
-                });
-              });
         },
 
         getLineElByChild(node) {
@@ -282,6 +270,10 @@
         cancel() {
           this.$.processor.cancel();
           this.$.syntaxLayer.cancel();
+          if (this._cancelableRenderPromise) {
+            this._cancelableRenderPromise.cancel();
+            this._cancelableRenderPromise = null;
+          }
         },
 
         _handlePreferenceError(pref) {
@@ -294,7 +286,7 @@
           throw Error(`Invalid preference value: ${pref}`);
         },
 
-        _getDiffBuilder(diff, comments, prefs) {
+        _getDiffBuilder(diff, prefs) {
           if (isNaN(prefs.tab_size) || prefs.tab_size <= 0) {
             this._handlePreferenceError('tab size');
             return;
@@ -306,20 +298,18 @@
           }
 
           let builder = null;
-          const createFn = this.createCommentFn;
           if (this.isImageDiff) {
-            builder = new GrDiffBuilderImage(diff, comments, createFn, prefs,
-                this.diffElement, this.baseImage, this.revisionImage);
+            builder = new GrDiffBuilderImage(diff, prefs, this.diffElement,
+                this.baseImage, this.revisionImage);
           } else if (diff.binary) {
             // If the diff is binary, but not an image.
-            return new GrDiffBuilderBinary(diff, comments, prefs,
-                this.diffElement);
+            return new GrDiffBuilderBinary(diff, prefs, this.diffElement);
           } else if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) {
-            builder = new GrDiffBuilderSideBySide(diff, comments, createFn,
-                prefs, this.diffElement, this._layers);
+            builder = new GrDiffBuilderSideBySide(diff, prefs, this.diffElement,
+                this._layers);
           } else if (this.viewMode === DiffViewMode.UNIFIED) {
-            builder = new GrDiffBuilderUnified(diff, comments, createFn, prefs,
-                this.diffElement, this._layers);
+            builder = new GrDiffBuilderUnified(diff, prefs, this.diffElement,
+                this._layers);
           }
           if (!builder) {
             throw Error('Unsupported diff view mode: ' + this.viewMode);
@@ -331,33 +321,6 @@
           this.diffElement.innerHTML = null;
         },
 
-        /**
-         * @param {!Object} comments
-         * @param {Defs.LineOfInterest|null} lineOfInterest
-         */
-        _getKeyLocations(comments, lineOfInterest) {
-          const result = {
-            left: {},
-            right: {},
-          };
-          for (const side in comments) {
-            if (side !== GrDiffBuilder.Side.LEFT &&
-                side !== GrDiffBuilder.Side.RIGHT) {
-              continue;
-            }
-            for (const c of comments[side]) {
-              result[side][c.line || GrDiffLine.FILE] = true;
-            }
-          }
-
-          if (lineOfInterest) {
-            const side = lineOfInterest.leftSide ? 'left' : 'right';
-            result[side][lineOfInterest.number] = true;
-          }
-
-          return result;
-        },
-
         _groupsChanged(changeRecord) {
           if (!changeRecord) { return; }
           for (const splice of changeRecord.indexSplices) {
@@ -375,7 +338,7 @@
             // Take a DIV.contentText element and a line object with intraline
             // differences to highlight and apply them to the element as
             // annotations.
-            annotate(el, line) {
+            annotate(contentEl, lineNumberEl, line) {
               const HL_CLASS = 'style-scope gr-diff intraline';
               for (const highlight of line.highlights) {
                 // The start and end indices could be the same if a highlight is
@@ -389,7 +352,7 @@
                   highlight.endIndex;
 
                 GrAnnotation.annotateElement(
-                    el,
+                    contentEl,
                     highlight.startIndex,
                     endIndex - highlight.startIndex,
                     HL_CLASS);
@@ -401,7 +364,7 @@
         _createTabIndicatorLayer() {
           const show = () => this._showTabs;
           return {
-            annotate(el, line) {
+            annotate(contentEl, lineNumberEl, line) {
               // If visible tabs are disabled, do nothing.
               if (!show()) { return; }
 
@@ -412,7 +375,7 @@
                 // Skip forward by the length of the content
                 pos += split[i].length;
 
-                GrAnnotation.annotateElement(el, pos, 1,
+                GrAnnotation.annotateElement(contentEl, pos, 1,
                     'style-scope gr-diff tab-indicator');
 
                 // Skip forward by one tab character.
@@ -428,7 +391,7 @@
           }.bind(this);
 
           return {
-            annotate(el, line) {
+            annotate(contentEl, lineNumberEl, line) {
               if (!show()) { return; }
 
               const match = line.text.match(TRAILING_WHITESPACE_PATTERN);
@@ -438,7 +401,7 @@
                 const index = GrAnnotation.getStringLength(
                     line.text.substr(0, match.index));
                 const length = GrAnnotation.getStringLength(match[0]);
-                GrAnnotation.annotateElement(el, index, length,
+                GrAnnotation.annotateElement(contentEl, index, length,
                     'style-scope gr-diff trailing-whitespace');
               }
             },
@@ -446,26 +409,6 @@
         },
 
         /**
-         * In pages with large diffs, creating the first comment thread can be
-         * slow because nested Polymer elements (particularly
-         * iron-autogrow-textarea) add style elements to the document head,
-         * which, in turn, triggers a reflow on the page. Create a hidden
-         * thread, attach it to the page, and remove it so the stylesheet will
-         * already exist and the user's comment will be quick to load.
-         *
-         * @see https://gerrit-review.googlesource.com/c/82213/
-         */
-        _preRenderThread() {
-          const thread = document.createElement('gr-diff-comment-thread');
-          thread.setAttribute('hidden', true);
-          thread.addDraft();
-          const parent = Polymer.dom(this.root);
-          parent.appendChild(thread);
-          Polymer.dom.flush();
-          parent.removeChild(thread);
-        },
-
-        /**
          * @return {boolean} whether any of the lines in _groups are longer
          * than SYNTAX_MAX_LINE_LENGTH.
          */
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 f37b5d8..65a56f0 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
@@ -42,11 +42,8 @@
    */
   const REGEX_TAB_OR_SURROGATE_PAIR = /\t|[\uD800-\uDBFF][\uDC00-\uDFFF]/;
 
-  function GrDiffBuilder(diff, comments, createThreadGroupFn, prefs, outputEl,
-      layers) {
+  function GrDiffBuilder(diff, prefs, outputEl, layers) {
     this._diff = diff;
-    this._comments = comments;
-    this._createThreadGroupFn = createThreadGroupFn;
     this._prefs = prefs;
     this._outputEl = outputEl;
     this.groups = [];
@@ -223,7 +220,9 @@
         // if lines are collapsed and not visible on the page yet.
         continue;
       }
-      el.parentElement.replaceChild(this._createTextEl(line, side).firstChild,
+      const lineNumberEl = this._getLineNumberEl(el, side);
+      el.parentElement.replaceChild(
+          this._createTextEl(lineNumberEl, line, side).firstChild,
           el);
     }
   };
@@ -321,152 +320,6 @@
     return button;
   };
 
-  GrDiffBuilder.prototype._getCommentsForLine = function(comments, line,
-      opt_side) {
-    function byLineNum(lineNum) {
-      return function(c) {
-        return (c.line === lineNum) ||
-               (c.line === undefined && lineNum === GrDiffLine.FILE);
-      };
-    }
-    const leftComments =
-        comments[GrDiffBuilder.Side.LEFT].filter(byLineNum(line.beforeNumber));
-    const rightComments =
-        comments[GrDiffBuilder.Side.RIGHT].filter(byLineNum(line.afterNumber));
-
-    leftComments.forEach(c => { c.__commentSide = 'left'; });
-    rightComments.forEach(c => { c.__commentSide = 'right'; });
-
-    let result;
-
-    switch (opt_side) {
-      case GrDiffBuilder.Side.LEFT:
-        result = leftComments;
-        break;
-      case GrDiffBuilder.Side.RIGHT:
-        result = rightComments;
-        break;
-      default:
-        result = leftComments.concat(rightComments);
-        break;
-    }
-
-    return result;
-  };
-
-  /**
-   * @param {Array<Object>} comments
-   * @param {string} patchForNewThreads
-   */
-  GrDiffBuilder.prototype._getThreads = function(comments, patchForNewThreads) {
-    const sortedComments = comments.slice(0).sort((a, b) => {
-      if (b.__draft && !a.__draft ) { return 0; }
-      if (a.__draft && !b.__draft ) { return 1; }
-      return util.parseDate(a.updated) - util.parseDate(b.updated);
-    });
-
-    const threads = [];
-    for (const comment of sortedComments) {
-      // If the comment is in reply to another comment, find that comment's
-      // thread and append to it.
-      if (comment.in_reply_to) {
-        const thread = threads.find(thread =>
-          thread.comments.some(c => c.id === comment.in_reply_to));
-        if (thread) {
-          thread.comments.push(comment);
-          continue;
-        }
-      }
-
-      // Otherwise, this comment starts its own thread.
-      const newThread = {
-        start_datetime: comment.updated,
-        comments: [comment],
-        commentSide: comment.__commentSide,
-        /**
-         * Determines what the patchNum of a thread should be. Use patchNum from
-         * comment if it exists, otherwise the property of the thread group.
-         * This is needed for switching between side-by-side and unified views
-         * when there are unsaved drafts.
-         */
-        patchNum: comment.patch_set || patchForNewThreads,
-        rootId: comment.id || comment.__draftID,
-      };
-      if (comment.range) {
-        newThread.range = Object.assign({}, comment.range);
-      }
-      threads.push(newThread);
-    }
-    return threads;
-  };
-
-  /**
-   * Returns the patch number that new comment threads should be attached to.
-   *
-   * @param {GrDiffLine} line The line new thread will be attached to.
-   * @param {string=} opt_side Set to LEFT to force adding it to the LEFT side -
-   *     will be ignored if the left is a parent or a merge parent
-   * @return {number} Patch set to attach the new thread to
-   */
-  GrDiffBuilder.prototype._determinePatchNumForNewThreads = function(
-      patchRange, line, opt_side) {
-    if ((line.type === GrDiffLine.Type.REMOVE ||
-         opt_side === GrDiffBuilder.Side.LEFT) &&
-        patchRange.basePatchNum !== 'PARENT' &&
-        !Gerrit.PatchSetBehavior.isMergeParent(patchRange.basePatchNum)) {
-      return patchRange.basePatchNum;
-    } else {
-      return patchRange.patchNum;
-    }
-  };
-
-  /**
-   * Returns whether the comments on the given line are on a (merge) parent.
-   *
-   * @param {string} firstCommentSide
-   * @param {{basePatchNum: number, patchNum: number}} patchRange
-   * @param {GrDiffLine} line The line the comments are on.
-   * @param {string=} opt_side
-   * @return {boolean} True iff the comments on the given line are on a (merge)
-   *    parent.
-   */
-  GrDiffBuilder.prototype._determineIsOnParent = function(
-      firstCommentSide, patchRange, line, opt_side) {
-    return ((line.type === GrDiffLine.Type.REMOVE ||
-             opt_side === GrDiffBuilder.Side.LEFT) &&
-            (patchRange.basePatchNum === 'PARENT' ||
-             Gerrit.PatchSetBehavior.isMergeParent(
-                 patchRange.basePatchNum))) ||
-          firstCommentSide === 'PARENT';
-  };
-
-  /**
-   * @param {GrDiffLine} line
-   * @param {string=} opt_side
-   * @return {!Object}
-   */
-  GrDiffBuilder.prototype._commentThreadGroupForLine = function(
-      line, opt_side) {
-    const comments =
-    this._getCommentsForLine(this._comments, line, opt_side);
-    if (!comments || comments.length === 0) {
-      return null;
-    }
-
-    const patchNum = this._determinePatchNumForNewThreads(
-        this._comments.meta.patchRange, line, opt_side);
-    const isOnParent = this._determineIsOnParent(
-        comments[0].side, this._comments.meta.patchRange, line, opt_side);
-
-    const threadGroupEl = this._createThreadGroupFn(patchNum, isOnParent,
-        opt_side);
-    threadGroupEl.threads = this._getThreads(comments, patchNum);
-    if (opt_side) {
-      threadGroupEl.setAttribute('data-side', opt_side);
-    }
-    return threadGroupEl;
-  };
-
   GrDiffBuilder.prototype._createLineEl = function(
       line, number, type, opt_class) {
     const td = this._createElement('td');
@@ -494,7 +347,8 @@
     return td;
   };
 
-  GrDiffBuilder.prototype._createTextEl = function(line, opt_side) {
+  GrDiffBuilder.prototype._createTextEl = function(
+      lineNumberEl, line, opt_side) {
     const td = this._createElement('td');
     if (line.type !== GrDiffLine.Type.BLANK) {
       td.classList.add('content');
@@ -511,7 +365,7 @@
     }
 
     for (const layer of this.layers) {
-      layer.annotate(contentText, line);
+      layer.annotate(contentText, lineNumberEl, line);
     }
 
     td.appendChild(contentText);
@@ -752,5 +606,18 @@
     return blameTd;
   };
 
+  /**
+   * Finds the line number element given the content element by walking up the
+   * DOM tree to the diff row and then querying for a .lineNum element on the
+   * requested side.
+   *
+   * TODO(brohlfs): Consolidate this with getLineEl... methods in html file.
+   */
+  GrDiffBuilder.prototype._getLineNumberEl = function(content, side) {
+    let row = content;
+    while (row && !row.classList.contains('diff-row')) row = row.parentElement;
+    return row ? row.querySelector('.lineNum.' + side) : null;
+  };
+
   window.GrDiffBuilder = GrDiffBuilder;
 })(window, GrDiffGroup, GrDiffLine);
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 9a88ac9..a8db47a 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
@@ -57,9 +57,9 @@
 
 <script>
   suite('gr-diff-builder tests', () => {
+    let prefs;
     let element;
     let builder;
-    let createThreadGroupFn;
     let sandbox;
     const LINE_FEED_HTML = '<span class="style-scope gr-diff br"></span>';
 
@@ -70,180 +70,16 @@
         getLoggedIn() { return Promise.resolve(false); },
         getProjectConfig() { return Promise.resolve({}); },
       });
-      const prefs = {
+      prefs = {
         line_length: 10,
         show_tabs: true,
         tab_size: 4,
       };
-      createThreadGroupFn = sinon.spy(() => ({
-        setAttribute: sinon.spy(),
-      }));
-      builder = new GrDiffBuilder(
-          {content: []}, {left: [], right: []}, createThreadGroupFn, prefs);
+      builder = new GrDiffBuilder({content: []}, prefs);
     });
 
     teardown(() => { sandbox.restore(); });
 
-    test('_getThreads', () => {
-      const patchForNewThreads = 3;
-      const comments = [
-        {
-          id: 'sallys_confession',
-          message: 'i like you, jack',
-          updated: '2015-12-23 15:00:20.396000000',
-          __commentSide: 'left',
-        }, {
-          id: 'jacks_reply',
-          message: 'i like you, too',
-          updated: '2015-12-24 15:01:20.396000000',
-          __commentSide: 'left',
-          in_reply_to: 'sallys_confession',
-        },
-        {
-          id: 'new_draft',
-          message: 'i do not like either of you',
-          __commentSide: 'left',
-          __draft: true,
-          updated: '2015-12-20 15:01:20.396000000',
-        },
-      ];
-
-      let expectedThreadGroups = [
-        {
-          start_datetime: '2015-12-23 15:00:20.396000000',
-          commentSide: 'left',
-          comments: [{
-            id: 'sallys_confession',
-            message: 'i like you, jack',
-            updated: '2015-12-23 15:00:20.396000000',
-            __commentSide: 'left',
-          }, {
-            id: 'jacks_reply',
-            message: 'i like you, too',
-            updated: '2015-12-24 15:01:20.396000000',
-            __commentSide: 'left',
-            in_reply_to: 'sallys_confession',
-          }],
-          rootId: 'sallys_confession',
-          patchNum: 3,
-        },
-        {
-          start_datetime: '2015-12-20 15:01:20.396000000',
-          commentSide: 'left',
-          comments: [
-            {
-              id: 'new_draft',
-              message: 'i do not like either of you',
-              __commentSide: 'left',
-              __draft: true,
-              updated: '2015-12-20 15:01:20.396000000',
-            },
-          ],
-          rootId: 'new_draft',
-          patchNum: 3,
-        },
-      ];
-
-      assert.deepEqual(
-          builder._getThreads(comments, patchForNewThreads),
-          expectedThreadGroups);
-
-      // Patch num should get inherited from comment rather
-      comments.push({
-        id: 'betsys_confession',
-        message: 'i like you, jack',
-        updated: '2015-12-24 15:00:10.396000000',
-        range: {
-          start_line: 1,
-          start_character: 1,
-          end_line: 1,
-          end_character: 2,
-        },
-        __commentSide: 'left',
-      });
-
-      expectedThreadGroups = [
-        {
-          start_datetime: '2015-12-23 15:00:20.396000000',
-          commentSide: 'left',
-          comments: [{
-            id: 'sallys_confession',
-            message: 'i like you, jack',
-            updated: '2015-12-23 15:00:20.396000000',
-            __commentSide: 'left',
-          }, {
-            id: 'jacks_reply',
-            in_reply_to: 'sallys_confession',
-            message: 'i like you, too',
-            updated: '2015-12-24 15:01:20.396000000',
-            __commentSide: 'left',
-          }],
-          patchNum: 3,
-          rootId: 'sallys_confession',
-        },
-        {
-          start_datetime: '2015-12-24 15:00:10.396000000',
-          commentSide: 'left',
-          comments: [{
-            id: 'betsys_confession',
-            message: 'i like you, jack',
-            updated: '2015-12-24 15:00:10.396000000',
-            range: {
-              start_line: 1,
-              start_character: 1,
-              end_line: 1,
-              end_character: 2,
-            },
-            __commentSide: 'left',
-          }],
-          patchNum: 3,
-          rootId: 'betsys_confession',
-          range: {
-            start_line: 1,
-            start_character: 1,
-            end_line: 1,
-            end_character: 2,
-          },
-        },
-        {
-          start_datetime: '2015-12-20 15:01:20.396000000',
-          commentSide: 'left',
-          comments: [
-            {
-              id: 'new_draft',
-              message: 'i do not like either of you',
-              __commentSide: 'left',
-              __draft: true,
-              updated: '2015-12-20 15:01:20.396000000',
-            },
-          ],
-          rootId: 'new_draft',
-          patchNum: 3,
-        },
-      ];
-
-      assert.deepEqual(
-          builder._getThreads(comments, patchForNewThreads),
-          expectedThreadGroups);
-    });
-
-    test('multiple comments at same location but not threaded', () => {
-      const comments = [
-        {
-          id: 'sallys_confession',
-          message: 'i like you, jack',
-          updated: '2015-12-23 15:00:20.396000000',
-          __commentSide: 'left',
-        }, {
-          id: 'jacks_reply',
-          message: 'i like you, too',
-          updated: '2015-12-24 15:01:20.396000000',
-          __commentSide: 'left',
-        },
-      ];
-      assert.equal(builder._getThreads(comments, '3').length, 2);
-    });
-
     test('_createElement classStr applies all classes', () => {
       const node = builder._createElement('div', 'test classes');
       assert.isTrue(node.classList.contains('gr-diff'));
@@ -327,7 +163,7 @@
       const text = 'a'.repeat(51);
 
       const line = {text, highlights: []};
-      const result = builder._createTextEl(line).firstChild.innerHTML;
+      const result = builder._createTextEl(undefined, line).firstChild.innerHTML;
       assert.equal(result, text);
     });
 
@@ -337,14 +173,14 @@
 
       const line = {text, highlights: []};
       const expected = 'a'.repeat(50) + LINE_FEED_HTML + 'a';
-      const result = builder._createTextEl(line).firstChild.innerHTML;
+      const result = builder._createTextEl(undefined, line).firstChild.innerHTML;
       assert.equal(result, expected);
     });
 
     test('_createTextEl linewrap with tabs', () => {
       const text = '\t'.repeat(7) + '!';
       const line = {text, highlights: []};
-      const el = builder._createTextEl(line);
+      const el = builder._createTextEl(undefined, line);
       assert.equal(el.innerText, text);
       // With line length 10 and tab size 2, there should be a line break
       // after every two tabs.
@@ -417,135 +253,10 @@
       }
     });
 
-    test('comments', () => {
-      const line = new GrDiffLine(GrDiffLine.Type.BOTH);
-      line.beforeNumber = 3;
-      line.afterNumber = 5;
-
-      let comments = {left: [], right: []};
-      assert.deepEqual(builder._getCommentsForLine(comments, line), []);
-      assert.deepEqual(builder._getCommentsForLine(comments, line,
-          GrDiffBuilder.Side.LEFT), []);
-      assert.deepEqual(builder._getCommentsForLine(comments, line,
-          GrDiffBuilder.Side.RIGHT), []);
-
-      comments = {
-        left: [
-          {id: 'l3', line: 3},
-          {id: 'l5', line: 5},
-        ],
-        right: [
-          {id: 'r3', line: 3},
-          {id: 'r5', line: 5},
-        ],
-      };
-      assert.deepEqual(builder._getCommentsForLine(comments, line),
-          [{id: 'l3', line: 3, __commentSide: 'left'},
-            {id: 'r5', line: 5, __commentSide: 'right'}]);
-      assert.deepEqual(builder._getCommentsForLine(comments, line,
-          GrDiffBuilder.Side.LEFT), [{id: 'l3', line: 3,
-        __commentSide: 'left'}]);
-      assert.deepEqual(builder._getCommentsForLine(comments, line,
-          GrDiffBuilder.Side.RIGHT), [{id: 'r5', line: 5,
-        __commentSide: 'right'}]);
-    });
-
-    test('comment thread group creation', () => {
-      const l3 = {id: 'l3', line: 3, updated: '2016-08-09 00:42:32.000000000',
-        __commentSide: 'left'};
-      const l5 = {id: 'l5', line: 5, updated: '2016-08-09 00:42:32.000000000',
-        __commentSide: 'left'};
-      const r5 = {id: 'r5', line: 5, updated: '2016-08-09 00:42:32.000000000',
-        __commentSide: 'right'};
-
-      builder._comments = {
-        meta: {
-          changeNum: '42',
-          patchRange: {
-            basePatchNum: 'PARENT',
-            patchNum: '3',
-          },
-          path: '/path/to/foo',
-          projectConfig: {foo: 'bar'},
-        },
-        left: [l3, l5],
-        right: [r5],
-      };
-
-      function threadForComment(c, patchNum) {
-        return {
-          commentSide: c.__commentSide,
-          comments: [c],
-          patchNum,
-          rootId: c.id,
-          start_datetime: c.updated,
-        };
-      }
-
-      function checkThreadGroupProps(threadGroupEl, patchNum, isOnParent,
-          comments) {
-        assert.equal(createThreadGroupFn.lastCall.args[0], patchNum);
-        assert.equal(createThreadGroupFn.lastCall.args[1], isOnParent);
-        assert.deepEqual(
-            threadGroupEl.threads,
-            comments.map(c => threadForComment(c, patchNum)));
-      }
-
-      let line = new GrDiffLine(GrDiffLine.Type.BOTH);
-      line.beforeNumber = 5;
-      line.afterNumber = 5;
-      let threadGroupEl = builder._commentThreadGroupForLine(line);
-      assert.isTrue(createThreadGroupFn.calledOnce);
-      checkThreadGroupProps(threadGroupEl, '3', false, [l5, r5]);
-
-      threadGroupEl =
-          builder._commentThreadGroupForLine(line, GrDiffBuilder.Side.RIGHT);
-      assert.isTrue(createThreadGroupFn.calledTwice);
-      checkThreadGroupProps(threadGroupEl, '3', false, [r5]);
-
-      threadGroupEl =
-          builder._commentThreadGroupForLine(line, GrDiffBuilder.Side.LEFT);
-      assert.isTrue(createThreadGroupFn.calledThrice);
-      checkThreadGroupProps(threadGroupEl, '3', true, [l5]);
-
-      builder._comments.meta.patchRange.basePatchNum = '1';
-
-      threadGroupEl = builder._commentThreadGroupForLine(line);
-      assert.equal(createThreadGroupFn.callCount, 4);
-      checkThreadGroupProps(threadGroupEl, '3', false, [l5, r5]);
-
-      threadEl =
-          builder._commentThreadGroupForLine(line, GrDiffBuilder.Side.LEFT);
-      assert.equal(createThreadGroupFn.callCount, 5);
-      checkThreadGroupProps(threadEl, '1', false, [l5]);
-
-      threadGroupEl =
-          builder._commentThreadGroupForLine(line, GrDiffBuilder.Side.RIGHT);
-      assert.equal(createThreadGroupFn.callCount, 6);
-      checkThreadGroupProps(threadGroupEl, '3', false, [r5]);
-
-      builder._comments.meta.patchRange.basePatchNum = 'PARENT';
-
-      line = new GrDiffLine(GrDiffLine.Type.REMOVE);
-      line.beforeNumber = 5;
-      line.afterNumber = 5;
-      threadGroupEl = builder._commentThreadGroupForLine(line);
-      assert.equal(createThreadGroupFn.callCount, 7);
-      checkThreadGroupProps(threadGroupEl, '3', true, [l5, r5]);
-
-      line = new GrDiffLine(GrDiffLine.Type.ADD);
-      line.beforeNumber = 3;
-      line.afterNumber = 5;
-      threadGroupEl = builder._commentThreadGroupForLine(line);
-      assert.equal(createThreadGroupFn.callCount, 8);
-      checkThreadGroupProps(threadGroupEl, '3', false, [l3, r5]);
-    });
-
-
     test('_handlePreferenceError called with invalid preference', () => {
       sandbox.stub(element, '_handlePreferenceError');
       const prefs = {tab_size: 0};
-      element._getDiffBuilder(element.diff, element.comments, prefs);
+      element._getDiffBuilder(element.diff, prefs);
       assert.isTrue(element._handlePreferenceError.lastCall
           .calledWithExactly('tab size'));
     });
@@ -559,31 +270,6 @@
         `Fix in diff preferences`);
     });
 
-    test('_getKeyLocations', () => {
-      assert.deepEqual(element._getKeyLocations({left: [], right: []}, null),
-          {left: {}, right: {}});
-      const comments = {
-        left: [{line: 123}, {}],
-        right: [{line: 456}],
-      };
-      assert.deepEqual(element._getKeyLocations(comments, null), {
-        left: {FILE: true, 123: true},
-        right: {456: true},
-      });
-
-      const lineOfInterest = {number: 789, leftSide: true};
-      assert.deepEqual(element._getKeyLocations(comments, lineOfInterest), {
-        left: {FILE: true, 123: true, 789: true},
-        right: {456: true},
-      });
-
-      delete lineOfInterest.leftSide;
-      assert.deepEqual(element._getKeyLocations(comments, lineOfInterest), {
-        left: {FILE: true, 123: true},
-        right: {456: true, 789: true},
-      });
-    });
-
     suite('_isTotal', () => {
       test('is total for add', () => {
         const group = new GrDiffGroup(GrDiffGroup.Type.DELTA);
@@ -620,6 +306,7 @@
       let str;
       let annotateElementSpy;
       let layer;
+      const lineNumberEl = document.createElement('td');
 
       function slice(str, start, end) {
         return Array.from(str).slice(start, end).join('');
@@ -639,7 +326,7 @@
           highlights: [],
         };
 
-        layer.annotate(el, line);
+        layer.annotate(el, lineNumberEl, line);
 
         // The content is unchanged.
         assert.isFalse(annotateElementSpy.called);
@@ -662,7 +349,7 @@
         const str3 = slice(str, 18, 22);
         const str4 = slice(str, 22);
 
-        layer.annotate(el, line);
+        layer.annotate(el, lineNumberEl, line);
 
         assert.isTrue(annotateElementSpy.called);
         assert.equal(el.childNodes.length, 5);
@@ -694,7 +381,7 @@
         const str0 = slice(str, 0, 28);
         const str1 = slice(str, 28);
 
-        layer.annotate(el, line);
+        layer.annotate(el, lineNumberEl, line);
 
         assert.isTrue(annotateElementSpy.called);
         assert.equal(el.childNodes.length, 2);
@@ -714,7 +401,7 @@
           ],
         };
 
-        layer.annotate(el, line);
+        layer.annotate(el, lineNumberEl, line);
 
         assert.isFalse(annotateElementSpy.called);
         assert.equal(el.childNodes.length, 1);
@@ -735,7 +422,7 @@
         const str1 = slice(str, 6, 12);
         const str2 = slice(str, 12);
 
-        layer.annotate(el, line);
+        layer.annotate(el, lineNumberEl, line);
 
         assert.isTrue(annotateElementSpy.called);
         assert.equal(el.childNodes.length, 3);
@@ -765,7 +452,7 @@
         const str0 = slice(str, 0, 6);
         const str1 = slice(str, 6);
 
-        layer.annotate(el, line);
+        layer.annotate(el, lineNumberEl, line);
 
         assert.isTrue(annotateElementSpy.called);
         assert.equal(el.childNodes.length, 2);
@@ -781,6 +468,7 @@
     suite('tab indicators', () => {
       let element;
       let layer;
+      const lineNumberEl = document.createElement('td');
 
       setup(() => {
         element = fixture('basic');
@@ -794,7 +482,7 @@
         const annotateElementStub =
             sandbox.stub(GrAnnotation, 'annotateElement');
 
-        layer.annotate(el, line);
+        layer.annotate(el, lineNumberEl, line);
 
         assert.isFalse(annotateElementStub.called);
       });
@@ -807,7 +495,7 @@
         const annotateElementStub =
             sandbox.stub(GrAnnotation, 'annotateElement');
 
-        layer.annotate(el, line);
+        layer.annotate(el, lineNumberEl, line);
 
         assert.isFalse(annotateElementStub.called);
       });
@@ -820,7 +508,7 @@
         const annotateElementStub =
             sandbox.stub(GrAnnotation, 'annotateElement');
 
-        layer.annotate(el, line);
+        layer.annotate(el, lineNumberEl, line);
 
         assert.equal(annotateElementStub.callCount, 1);
         const args = annotateElementStub.getCalls()[0].args;
@@ -840,7 +528,7 @@
         const annotateElementStub =
             sandbox.stub(GrAnnotation, 'annotateElement');
 
-        layer.annotate(el, line);
+        layer.annotate(el, lineNumberEl, line);
 
         assert.isFalse(annotateElementStub.called);
       });
@@ -853,7 +541,7 @@
         const annotateElementStub =
             sandbox.stub(GrAnnotation, 'annotateElement');
 
-        layer.annotate(el, line);
+        layer.annotate(el, lineNumberEl, line);
 
         assert.equal(annotateElementStub.callCount, 2);
 
@@ -878,7 +566,7 @@
         const annotateElementStub =
             sandbox.stub(GrAnnotation, 'annotateElement');
 
-        layer.annotate(el, line);
+        layer.annotate(el, lineNumberEl, line);
 
         assert.equal(annotateElementStub.callCount, 1);
         const args = annotateElementStub.getCalls()[0].args;
@@ -896,13 +584,14 @@
       setup(() => {
         element = fixture('basic');
         element._showTrailingWhitespace = true;
+        element._setupAnnotationLayers();
         initialLayersCount = element._layers.length;
       });
 
       test('no plugin layers', () => {
         const getDiffLayersStub = sinon.stub(element.$.jsAPI, 'getDiffLayers')
             .returns([]);
-        element.attached();
+        element._setupAnnotationLayers();
         assert.isTrue(getDiffLayersStub.called);
         assert.equal(element._layers.length, initialLayersCount);
       });
@@ -910,15 +599,16 @@
       test('with plugin layers', () => {
         const getDiffLayersStub = sinon.stub(element.$.jsAPI, 'getDiffLayers')
             .returns([{}, {}]);
-        element.attached();
+        element._setupAnnotationLayers();
         assert.isTrue(getDiffLayersStub.called);
-        assert.equal(element._layers.length, initialLayersCount+2);
+        assert.equal(element._layers.length, initialLayersCount + 2);
       });
     });
 
     suite('trailing whitespace', () => {
       let element;
       let layer;
+      const lineNumberEl = document.createElement('td');
 
       setup(() => {
         element = fixture('basic');
@@ -931,7 +621,7 @@
         const el = document.createElement('div');
         const annotateElementStub =
             sandbox.stub(GrAnnotation, 'annotateElement');
-        layer.annotate(el, line);
+        layer.annotate(el, lineNumberEl, line);
         assert.isFalse(annotateElementStub.called);
       });
 
@@ -942,7 +632,7 @@
         el.textContent = str;
         const annotateElementStub =
             sandbox.stub(GrAnnotation, 'annotateElement');
-        layer.annotate(el, line);
+        layer.annotate(el, lineNumberEl, line);
         assert.isFalse(annotateElementStub.called);
       });
 
@@ -953,7 +643,7 @@
         el.textContent = str;
         const annotateElementStub =
             sandbox.stub(GrAnnotation, 'annotateElement');
-        layer.annotate(el, line);
+        layer.annotate(el, lineNumberEl, line);
         assert.isTrue(annotateElementStub.called);
         assert.equal(annotateElementStub.lastCall.args[1], 11);
         assert.equal(annotateElementStub.lastCall.args[2], 3);
@@ -966,7 +656,7 @@
         el.textContent = str;
         const annotateElementStub =
             sandbox.stub(GrAnnotation, 'annotateElement');
-        layer.annotate(el, line);
+        layer.annotate(el, lineNumberEl, line);
         assert.isTrue(annotateElementStub.called);
         assert.equal(annotateElementStub.lastCall.args[1], 11);
         assert.equal(annotateElementStub.lastCall.args[2], 3);
@@ -979,7 +669,7 @@
         el.textContent = str;
         const annotateElementStub =
             sandbox.stub(GrAnnotation, 'annotateElement');
-        layer.annotate(el, line);
+        layer.annotate(el, lineNumberEl, line);
         assert.isTrue(annotateElementStub.called);
         assert.equal(annotateElementStub.lastCall.args[1], 11);
         assert.equal(annotateElementStub.lastCall.args[2], 3);
@@ -992,7 +682,7 @@
         el.textContent = str;
         const annotateElementStub =
             sandbox.stub(GrAnnotation, 'annotateElement');
-        layer.annotate(el, line);
+        layer.annotate(el, lineNumberEl, line);
         assert.isTrue(annotateElementStub.called);
         assert.equal(annotateElementStub.lastCall.args[1], 1);
         assert.equal(annotateElementStub.lastCall.args[2], 1);
@@ -1006,14 +696,14 @@
         el.textContent = str;
         const annotateElementStub =
             sandbox.stub(GrAnnotation, 'annotateElement');
-        layer.annotate(el, line);
+        layer.annotate(el, lineNumberEl, line);
         assert.isFalse(annotateElementStub.called);
       });
     });
 
     suite('rendering text, images and binary files', () => {
       let processStub;
-      let comments;
+      let keyLocations;
       let prefs;
       let content;
 
@@ -1023,7 +713,7 @@
         processStub = sandbox.stub(element.$.processor, 'process')
             .returns(Promise.resolve());
         sandbox.stub(element, '_anyLineTooLong').returns(true);
-        comments = {left: [], right: []};
+        keyLocations = {left: {}, right: {}};
         prefs = {
           line_length: 10,
           show_tabs: true,
@@ -1044,7 +734,7 @@
 
       test('text', () => {
         element.diff = {content};
-        return element.render(comments, prefs).then(() => {
+        return element.render(keyLocations, prefs).then(() => {
           assert.isTrue(processStub.calledOnce);
           assert.isFalse(processStub.lastCall.args[1]);
         });
@@ -1053,7 +743,7 @@
       test('image', () => {
         element.diff = {content, binary: true};
         element.isImageDiff = true;
-        return element.render(comments, prefs).then(() => {
+        return element.render(keyLocations, prefs).then(() => {
           assert.isTrue(processStub.calledOnce);
           assert.isTrue(processStub.lastCall.args[1]);
         });
@@ -1061,7 +751,7 @@
 
       test('binary', () => {
         element.diff = {content, binary: true};
-        return element.render(comments, prefs).then(() => {
+        return element.render(keyLocations, prefs).then(() => {
           assert.isTrue(processStub.calledOnce);
           assert.isTrue(processStub.lastCall.args[1]);
         });
@@ -1071,6 +761,7 @@
     suite('rendering', () => {
       let content;
       let outputEl;
+      let keyLocations;
 
       setup(done => {
         const prefs = {
@@ -1092,15 +783,11 @@
             ],
           },
         ];
-        stub('gr-reporting', {
-          time: sandbox.stub(),
-          timeEnd: sandbox.stub(),
-        });
         element = fixture('basic');
         outputEl = element.queryEffectiveChildren('#diffTable');
+        keyLocations = {left: {}, right: {}};
         sandbox.stub(element, '_getDiffBuilder', () => {
-          const builder = new GrDiffBuilder(
-              {content}, {left: [], right: []}, null, prefs, outputEl);
+          const builder = new GrDiffBuilder({content}, prefs, outputEl);
           sandbox.stub(builder, 'addColumns');
           builder.buildSectionElement = function(group) {
             const section = document.createElement('stub');
@@ -1112,19 +799,7 @@
           return builder;
         });
         element.diff = {content};
-        element.render({left: [], right: []}, prefs).then(done);
-      });
-
-      test('reporting', done => {
-        const timeStub = element.$.reporting.time;
-        const timeEndStub = element.$.reporting.timeEnd;
-        assert.isTrue(timeStub.calledWithExactly('Diff Total Render'));
-        assert.isTrue(timeStub.calledWithExactly('Diff Content Render'));
-        assert.isTrue(timeStub.calledWithExactly('Diff Syntax Render'));
-        assert.isTrue(timeEndStub.calledWithExactly('Diff Total Render'));
-        assert.isTrue(timeEndStub.calledWithExactly('Diff Content Render'));
-        assert.isTrue(timeEndStub.calledWithExactly('Diff Syntax Render'));
-        done();
+        element.render(keyLocations, prefs).then(done);
       });
 
       test('renderSection', () => {
@@ -1137,7 +812,7 @@
       });
 
       test('addColumns is called', done => {
-        element.render({left: [], right: []}, {}).then(done);
+        element.render(keyLocations, {}).then(done);
         assert.isTrue(element._builder.addColumns.called);
       });
 
@@ -1159,14 +834,14 @@
         assert.strictEqual(sections[1], section[1]);
       });
 
-      test('render-start and render are fired', done => {
+      test('render-start and render-content are fired', done => {
         const dispatchEventStub = sandbox.stub(element, 'dispatchEvent');
-        element.render({left: [], right: []}, {}).then(() => {
+        element.render(keyLocations, {}).then(() => {
           const firedEventTypes = dispatchEventStub.getCalls()
               .map(c => { return c.args[0].type; });
           assert.include(firedEventTypes, 'render-start');
           assert.include(firedEventTypes, 'render-content');
-          assert.include(firedEventTypes, 'render');
+          assert.include(firedEventTypes, 'render-syntax');
           done();
         });
       });
@@ -1178,10 +853,6 @@
       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('*')];
-        element.addEventListener('render', () => {
-          assert.isFalse(element.$.syntaxLayer.enabled);
-          done();
-        });
         const prefs = {
           line_length: 10,
           show_tabs: true,
@@ -1189,7 +860,10 @@
           context: -1,
           syntax_highlighting: true,
         };
-        element.render({left: [], right: []}, prefs);
+        element.render(keyLocations, prefs).then(() => {
+          assert.isFalse(element.$.syntaxLayer.enabled);
+          done();
+        });
       });
 
       test('cancel', () => {
@@ -1206,6 +880,7 @@
       let builder;
       let diff;
       let prefs;
+      let keyLocations;
 
       setup(done => {
         element = fixture('mock-diff');
@@ -1217,8 +892,9 @@
           show_tabs: true,
           tab_size: 4,
         };
+        keyLocations = {left: {}, right: {}};
 
-        element.render({left: [], right: []}, prefs).then(() => {
+        element.render(keyLocations, prefs).then(() => {
           builder = element._builder;
           done();
         });
@@ -1274,7 +950,7 @@
 
         assert.equal(spy.callCount, count);
         spy.getCalls().forEach((call, i) => {
-          assert.equal(call.args[0].beforeNumber, start + i);
+          assert.equal(call.args[1].beforeNumber, start + i);
         });
       });
 
@@ -1285,9 +961,11 @@
             (s, e, d, lines, elements) => {
               // Add a line and a corresponding element.
               lines.push(new GrDiffLine(GrDiffLine.Type.BOTH));
-              const parEl = document.createElement('div');
+              const tr = document.createElement('tr');
+              const td = document.createElement('td');
               const el = document.createElement('div');
-              parEl.appendChild(el);
+              tr.appendChild(td);
+              td.appendChild(el);
               elements.push(el);
 
               // Add 2 lines without corresponding elements.
@@ -1301,6 +979,52 @@
         assert.equal(spy.callCount, 1);
       });
 
+      test('_getLineNumberEl side-by-side left', () => {
+        const contentEl = builder.getContentByLine(5, 'left',
+            element.$.diffTable);
+        const lineNumberEl = builder._getLineNumberEl(contentEl, 'left');
+        assert.isTrue(lineNumberEl.classList.contains('lineNum'));
+        assert.isTrue(lineNumberEl.classList.contains('left'));
+      });
+
+      test('_getLineNumberEl side-by-side right', () => {
+        const contentEl = builder.getContentByLine(5, 'right',
+            element.$.diffTable);
+        const lineNumberEl = builder._getLineNumberEl(contentEl, 'right');
+        assert.isTrue(lineNumberEl.classList.contains('lineNum'));
+        assert.isTrue(lineNumberEl.classList.contains('right'));
+      });
+
+      test('_getLineNumberEl unified left', done => {
+        // Re-render as unified:
+        element.viewMode = 'UNIFIED_DIFF';
+        element.render(keyLocations, prefs).then(() => {
+          builder = element._builder;
+
+          const contentEl = builder.getContentByLine(5, 'left',
+              element.$.diffTable);
+          const lineNumberEl = builder._getLineNumberEl(contentEl, 'left');
+          assert.isTrue(lineNumberEl.classList.contains('lineNum'));
+          assert.isTrue(lineNumberEl.classList.contains('left'));
+          done();
+        });
+      });
+
+      test('_getLineNumberEl unified right', done => {
+        // Re-render as unified:
+        element.viewMode = 'UNIFIED_DIFF';
+        element.render(keyLocations, prefs).then(() => {
+          builder = element._builder;
+
+          const contentEl = builder.getContentByLine(5, 'right',
+              element.$.diffTable);
+          const lineNumberEl = builder._getLineNumberEl(contentEl, 'right');
+          assert.isTrue(lineNumberEl.classList.contains('lineNum'));
+          assert.isTrue(lineNumberEl.classList.contains('right'));
+          done();
+        });
+      });
+
       test('_getNextContentOnSide side-by-side left', () => {
         const startElem = builder.getContentByLine(5, 'left',
             element.$.diffTable);
@@ -1328,7 +1052,7 @@
       test('_getNextContentOnSide unified left', done => {
         // Re-render as unified:
         element.viewMode = 'UNIFIED_DIFF';
-        element.render({left: [], right: []}, prefs).then(() => {
+        element.render(keyLocations, prefs).then(() => {
           builder = element._builder;
 
           const startElem = builder.getContentByLine(5, 'left',
@@ -1348,7 +1072,7 @@
       test('_getNextContentOnSide unified right', done => {
         // Re-render as unified:
         element.viewMode = 'UNIFIED_DIFF';
-        element.render({left: [], right: []}, prefs).then(() => {
+        element.render(keyLocations, prefs).then(() => {
           builder = element._builder;
 
           const startElem = builder.getContentByLine(5, 'right',
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.html
deleted file mode 100644
index 58b7c32..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.html
+++ /dev/null
@@ -1,50 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../gr-diff-comment-thread/gr-diff-comment-thread.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-diff-comment-thread-group">
-  <template>
-    <style include="shared-styles">
-      :host {
-        display: block;
-        max-width: var(--content-width, 80ch);
-        white-space: normal;
-      }
-      gr-diff-comment-thread + gr-diff-comment-thread {
-        margin-top: .2em;
-      }
-    </style>
-    <template is="dom-repeat" items="[[threads]]" as="thread">
-      <gr-diff-comment-thread
-          comments="[[thread.comments]]"
-          comment-side="[[thread.commentSide]]"
-          is-on-parent="[[isOnParent]]"
-          parent-index="[[parentIndex]]"
-          change-num="[[changeNum]]"
-          patch-num="[[thread.patchNum]]"
-          root-id="{{thread.rootId}}"
-          path="[[path]]"
-          project-name="[[projectName]]"
-          range="[[thread.range]]"
-          on-thread-discard="_handleThreadDiscard"></gr-diff-comment-thread>
-    </template>
-  </template>
-  <script src="gr-diff-comment-thread-group.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.js b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.js
deleted file mode 100644
index ae45c93..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.js
+++ /dev/null
@@ -1,133 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-(function() {
-  'use strict';
-
-  Polymer({
-    is: 'gr-diff-comment-thread-group',
-
-    properties: {
-      changeNum: String,
-      projectName: String,
-      patchForNewThreads: String,
-      range: Object,
-      isOnParent: {
-        type: Boolean,
-        value: false,
-      },
-      parentIndex: {
-        type: Number,
-        value: null,
-      },
-      threads: {
-        type: Array,
-        value() { return []; },
-      },
-    },
-
-    get threadEls() {
-      return Polymer.dom(this.root).querySelectorAll('gr-diff-comment-thread');
-    },
-
-    /**
-     * Adds a new thread. Range is optional because a comment can be
-     * added to a line without a range selected.
-     *
-     * @param {!Object} opt_range
-     */
-    addNewThread(commentSide, opt_range) {
-      this.push('threads', {
-        comments: [],
-        commentSide,
-        patchNum: this.patchForNewThreads,
-        range: opt_range,
-      });
-    },
-
-    removeThread(rootId) {
-      for (let i = 0; i < this.threads.length; i++) {
-        if (this.threads[i].rootId === rootId) {
-          this.splice('threads', i, 1);
-          return;
-        }
-      }
-    },
-
-    /**
-     * Fetch the thread group at the given range, or the range-less thread
-     * on the line if no range is provided, lineNum, and side.
-     *
-     * @param {string} side
-     * @param {!Object=} opt_range
-     * @return {!Object|undefined}
-     */
-    getThread(side, opt_range) {
-      const threads = [].filter.call(this.threadEls,
-          thread => this._rangesEqual(thread.range, opt_range))
-          .filter(thread => thread.commentSide === side);
-      if (threads.length === 1) {
-        return threads[0];
-      }
-    },
-
-    _handleThreadDiscard(e) {
-      this.removeThread(e.detail.rootId);
-    },
-
-    /**
-     * 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
-     * values.
-     *
-     * @param {Object=} a range 1
-     * @param {Object=} b range 2
-     * @return {boolean}
-     */
-    _rangesEqual(a, b) {
-      if (!a && !b) { return true; }
-      if (!a || !b) { return false; }
-      return a.startLine === b.startLine &&
-          a.startChar === b.startChar &&
-          a.endLine === b.endLine &&
-          a.endChar === b.endChar;
-    },
-
-    _sortByDate(threadGroups) {
-      if (!threadGroups.length) { return; }
-      return threadGroups.sort((a, b) => {
-        // If a comment is a draft, it doesn't have a start_datetime yet.
-        // Assume it is newer than the comment it is being compared to.
-        if (!a.start_datetime) {
-          return 1;
-        }
-        if (!b.start_datetime) {
-          return -1;
-        }
-        return util.parseDate(a.start_datetime) -
-            util.parseDate(b.start_datetime);
-      });
-    },
-
-    _calculateLocationRange(range, comment) {
-      return 'range-' + range.start_line + '-' +
-          range.start_character + '-' +
-          range.end_line + '-' +
-          range.end_character + '-' +
-          comment.__commentSide;
-    },
-  });
-})();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group_test.html
deleted file mode 100644
index 1fb8136..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group_test.html
+++ /dev/null
@@ -1,232 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-diff-comment-thread-group</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-diff-comment-thread-group.html">
-
-<script>void(0);</script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-diff-comment-thread-group></gr-diff-comment-thread-group>
-  </template>
-</test-fixture>
-
-<script>
-  suite('gr-diff-comment-thread-group tests', () => {
-    let element;
-    let sandbox;
-
-    setup(() => {
-      sandbox = sinon.sandbox.create();
-      stub('gr-rest-api-interface', {
-        getLoggedIn() { return Promise.resolve(false); },
-      });
-      element = fixture('basic');
-    });
-
-    teardown(() => {
-      sandbox.restore();
-    });
-
-    test('getThread', () => {
-      const range = {
-        start_line: 1,
-        start_character: 1,
-        end_line: 1,
-        end_character: 2,
-      };
-      element.threads = [
-        {
-          rootId: 'sallys_confession',
-          commentSide: 'left',
-          comments: [
-            {
-              id: 'sallys_confession',
-              message: 'i like you, jack',
-              updated: '2015-12-23 15:00:20.396000000',
-              __commentSide: 'left',
-            }, {
-              id: 'jacks_reply',
-              message: 'i like you, too',
-              updated: '2015-12-24 15:01:20.396000000',
-              __commentSide: 'left',
-              in_reply_to: 'sallys_confession',
-            }, {
-              id: 'new_draft',
-              message: 'i do not like either of you',
-              __commentSide: 'left',
-              __draft: true,
-              in_reply_to: 'sallys_confession',
-              updated: '2015-12-20 15:01:20.396000000',
-            },
-          ],
-        },
-        {
-          rootId: 'right_side_comment',
-          commentSide: 'right',
-          comments: [
-            {
-              id: 'right_side_comment',
-              message: 'right side comment',
-              __commentSide: 'right',
-              __draft: true,
-              updated: '2015-12-20 15:01:20.396000000',
-            },
-          ],
-        }, {
-          rootId: 'betsys_confession',
-          commentSide: 'left',
-          range,
-          comments: [
-            {
-              id: 'betsys_confession',
-              message: 'i like you more, jack',
-              updated: '2015-12-24 15:00:10.396000000',
-              range,
-              __commentSide: 'left',
-            },
-          ],
-        },
-      ];
-
-      flushAsynchronousOperations();
-      assert.deepEqual(element.getThread('right').rootId, 'right_side_comment');
-      assert.deepEqual(element.getThread('right').comments.length, 1);
-      assert.deepEqual(element.getThread('left').rootId, 'sallys_confession');
-      assert.deepEqual(element.getThread('left').comments.length, 3);
-      assert.deepEqual(element.getThread('left', range).rootId,
-          'betsys_confession');
-      assert.deepEqual(element.getThread('left', range).comments.length, 1);
-    });
-
-    test('_sortByDate', () => {
-      let threadGroups = [
-        {
-          start_datetime: '2015-12-23 15:00:20.396000000',
-          comments: [],
-          locationRange: 'line',
-        },
-        {
-          start_datetime: '2015-12-22 15:00:10.396000000',
-          comments: [],
-          locationRange: 'range-1-1-1-2',
-        },
-      ];
-
-      let expectedResult = [
-        {
-          start_datetime: '2015-12-22 15:00:10.396000000',
-          comments: [],
-          locationRange: 'range-1-1-1-2',
-        }, {
-          start_datetime: '2015-12-23 15:00:20.396000000',
-          comments: [],
-          locationRange: 'line',
-        },
-      ];
-
-      assert.deepEqual(element._sortByDate(threadGroups), expectedResult);
-
-      // When a comment doesn't have a date, the one without the date should be
-      // last.
-      threadGroups = [
-        {
-          start_datetime: '2015-12-23 15:00:20.396000000',
-          comments: [],
-          locationRange: 'line',
-        },
-        {
-          comments: [],
-          locationRange: 'range-1-1-1-2',
-        },
-      ];
-
-      expectedResult = [
-        {
-          start_datetime: '2015-12-23 15:00:20.396000000',
-          comments: [],
-          locationRange: 'line',
-        },
-        {
-          comments: [],
-          locationRange: 'range-1-1-1-2',
-        },
-      ];
-
-      assert.deepEqual(element._sortByDate(threadGroups), expectedResult);
-    });
-
-    test('_calculateLocationRange', () => {
-      const comment = {__commentSide: 'left'};
-      const range = {
-        start_line: 1,
-        start_character: 2,
-        end_line: 3,
-        end_character: 4,
-      };
-      assert.equal(
-          element._calculateLocationRange(range, comment),
-          'range-1-2-3-4-left');
-    });
-
-    test('addNewThread', () => {
-      const locationRange = 'range-1-2-3-4';
-      element._threads = [{locationRange: 'line'}];
-      element.addNewThread(locationRange);
-      assert(element._threads.length, 2);
-    });
-
-    test('removeThread', () => {
-      const locationRange = 'range-1-2-3-4';
-      element._threads = [
-        {locationRange: 'range-1-2-3-4', comments: []},
-        {locationRange: 'line', comments: []},
-      ];
-      flushAsynchronousOperations();
-      element.removeThread(locationRange);
-      flushAsynchronousOperations();
-      assert(element._threads.length, 1);
-    });
-
-    test('_rangesEqual', () => {
-      const range1 =
-          {startLine: 123, startChar: 345, endLine: 234, endChar: 456};
-      const range2 =
-          {startLine: 1, startChar: 2, endLine: 3, endChar: 4};
-
-      assert.isTrue(element._rangesEqual(null, null));
-      assert.isTrue(element._rangesEqual(null, undefined));
-      assert.isTrue(element._rangesEqual(undefined, null));
-      assert.isTrue(element._rangesEqual(undefined, undefined));
-
-      assert.isFalse(element._rangesEqual(range1, null));
-      assert.isFalse(element._rangesEqual(null, range1));
-      assert.isFalse(element._rangesEqual(range1, range2));
-
-      assert.isTrue(element._rangesEqual(range1, Object.assign({}, range1)));
-    });
-  });
-</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html
deleted file mode 100644
index c3a1de4..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html
+++ /dev/null
@@ -1,126 +0,0 @@
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../../../bower_components/polymer/polymer.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">
-<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-storage/gr-storage.html">
-<link rel="import" href="../gr-diff-comment/gr-diff-comment.html">
-
-<dom-module id="gr-diff-comment-thread">
-  <template>
-    <style include="shared-styles">
-      gr-button {
-        margin-left: .5em;
-      }
-      #actions {
-        margin-left: auto;
-        padding: .5em .7em;
-      }
-      #container {
-        background-color: var(--comment-background-color);
-        border: 1px solid var(--border-color);
-        color: var(--comment-text-color);
-        display: block;
-        margin-bottom: 1px;
-        white-space: normal;
-      }
-      #container.unresolved {
-        background-color: var(--unresolved-comment-background-color);
-      }
-      #commentInfoContainer {
-        border-top: 1px dotted var(--border-color);
-        display: flex;
-      }
-      #unresolvedLabel {
-        font-family: var(--font-family);
-        margin: auto 0;
-        padding: .5em .7em;
-      }
-      .pathInfo {
-        display: flex;
-        align-items: baseline;
-      }
-      .descriptionText {
-        margin-left: .5rem;
-        font-style: italic;
-      }
-    </style>
-    <template is="dom-if" if="[[showFilePath]]">
-      <div class="pathInfo">
-        <a href$="[[_getDiffUrlForComment(projectName, changeNum, path, patchNum)]]">[[_computeDisplayPath(path)]]</a>
-        <span class="descriptionText">Patchset [[patchNum]]</span>
-      </div>
-    </template>
-    <div id="container" class$="[[_computeHostClass(unresolved)]]">
-      <template id="commentList" is="dom-repeat" items="[[_orderedComments]]"
-          as="comment">
-        <gr-diff-comment
-            comment="{{comment}}"
-            robot-button-disabled="[[_hideActions(_showActions, _lastComment)]]"
-            change-num="[[changeNum]]"
-            patch-num="[[patchNum]]"
-            draft="[[_isDraft(comment)]]"
-            show-actions="[[_showActions]]"
-            comment-side="[[comment.__commentSide]]"
-            side="[[comment.side]]"
-            root-id="[[rootId]]"
-            project-config="[[_projectConfig]]"
-            on-create-fix-comment="_handleCommentFix"
-            on-comment-discard="_handleCommentDiscard"
-            on-comment-save="_handleCommentSavedOrDiscarded"></gr-diff-comment>
-      </template>
-      <div id="commentInfoContainer"
-          hidden$="[[_hideActions(_showActions, _lastComment)]]">
-        <span id="unresolvedLabel" hidden$="[[!unresolved]]">Unresolved</span>
-        <div id="actions">
-          <gr-button
-              id="replyBtn"
-              link
-              secondary
-              class="action reply"
-              on-tap="_handleCommentReply">Reply</gr-button>
-          <gr-button
-              id="quoteBtn"
-              link
-              secondary
-              class="action quote"
-              on-tap="_handleCommentQuote">Quote</gr-button>
-          <gr-button
-              id="ackBtn"
-              link
-              secondary
-              class="action ack"
-              on-tap="_handleCommentAck">Ack</gr-button>
-          <gr-button
-              id="doneBtn"
-              link
-              secondary
-              class="action done"
-              on-tap="_handleCommentDone">Done</gr-button>
-        </div>
-      </div>
-    </div>
-    <gr-reporting id="reporting"></gr-reporting>
-    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
-    <gr-storage id="storage"></gr-storage>
-  </template>
-  <script src="gr-diff-comment-thread.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
deleted file mode 100644
index 4ef173a..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
+++ /dev/null
@@ -1,466 +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 UNRESOLVED_EXPAND_COUNT = 5;
-  const NEWLINE_PATTERN = /\n/g;
-
-  Polymer({
-    is: 'gr-diff-comment-thread',
-
-    /**
-     * Fired when the thread should be discarded.
-     *
-     * @event thread-discard
-     */
-
-    /**
-     * Fired when a comment in the thread is permanently modified.
-     *
-     * @event thread-changed
-     */
-
-    properties: {
-      changeNum: String,
-      comments: {
-        type: Array,
-        value() { return []; },
-      },
-      range: Object,
-      keyEventTarget: {
-        type: Object,
-        value() { return document.body; },
-      },
-      commentSide: String,
-      patchNum: String,
-      path: String,
-      projectName: {
-        type: String,
-        observer: '_projectNameChanged',
-      },
-      hasDraft: {
-        type: Boolean,
-        notify: true,
-        reflectToAttribute: true,
-      },
-      isOnParent: {
-        type: Boolean,
-        value: false,
-      },
-      parentIndex: {
-        type: Number,
-        value: null,
-      },
-      rootId: {
-        type: String,
-        notify: true,
-        computed: '_computeRootId(comments.*)',
-      },
-      /**
-       * If this is true, the comment thread also needs to have the change and
-       * line properties property set
-       */
-      showFilePath: {
-        type: Boolean,
-        value: false,
-      },
-      /** Necessary only if showFilePath is true */
-      lineNum: Number,
-      unresolved: {
-        type: Boolean,
-        notify: true,
-        reflectToAttribute: true,
-      },
-      _showActions: Boolean,
-      _lastComment: Object,
-      _orderedComments: Array,
-      _projectConfig: Object,
-    },
-
-    behaviors: [
-      Gerrit.KeyboardShortcutBehavior,
-      Gerrit.PathListBehavior,
-    ],
-
-    listeners: {
-      'comment-update': '_handleCommentUpdate',
-    },
-
-    observers: [
-      '_commentsChanged(comments.*)',
-    ],
-
-    keyBindings: {
-      'e shift+e': '_handleEKey',
-    },
-
-    attached() {
-      this._getLoggedIn().then(loggedIn => {
-        this._showActions = loggedIn;
-      });
-      this._setInitialExpandedState();
-    },
-
-    addOrEditDraft(opt_lineNum, opt_range) {
-      const lastComment = this.comments[this.comments.length - 1] || {};
-      if (lastComment.__draft) {
-        const commentEl = this._commentElWithDraftID(
-            lastComment.id || lastComment.__draftID);
-        commentEl.editing = true;
-
-        // If the comment was collapsed, re-open it to make it clear which
-        // actions are available.
-        commentEl.collapsed = false;
-      } else {
-        const range = opt_range ? opt_range :
-          lastComment ? lastComment.range : undefined;
-        const unresolved = lastComment ? lastComment.unresolved : undefined;
-        this.addDraft(opt_lineNum, range, unresolved);
-      }
-    },
-
-    addDraft(opt_lineNum, opt_range, opt_unresolved) {
-      const draft = this._newDraft(opt_lineNum, opt_range);
-      draft.__editing = true;
-      draft.unresolved = opt_unresolved === false ? opt_unresolved : true;
-      this.push('comments', draft);
-    },
-
-    fireRemoveSelf() {
-      this.dispatchEvent(new CustomEvent('thread-discard',
-          {detail: {rootId: this.rootId}, bubbles: false}));
-    },
-
-    _getDiffUrlForComment(projectName, changeNum, path, patchNum) {
-      return Gerrit.Nav.getUrlForDiffById(changeNum,
-          projectName, path, patchNum,
-          null, this.lineNum);
-    },
-
-    _computeDisplayPath(path) {
-      const lineString = this.lineNum ? `#${this.lineNum}` : '';
-      return this.computeDisplayPath(path) + lineString;
-    },
-
-    _getLoggedIn() {
-      return this.$.restAPI.getLoggedIn();
-    },
-
-    _commentsChanged() {
-      this._orderedComments = this._sortedComments(this.comments);
-      this.updateThreadProperties();
-    },
-
-    updateThreadProperties() {
-      if (this._orderedComments.length) {
-        this._lastComment = this._getLastComment();
-        this.unresolved = this._lastComment.unresolved;
-        this.hasDraft = this._lastComment.__draft;
-      }
-    },
-
-    _hideActions(_showActions, _lastComment) {
-      return !_showActions || !_lastComment || !!_lastComment.__draft;
-    },
-
-    _getLastComment() {
-      return this._orderedComments[this._orderedComments.length - 1] || {};
-    },
-
-    _handleEKey(e) {
-      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-
-      // Don’t preventDefault in this case because it will render the event
-      // useless for other handlers (other gr-diff-comment-thread elements).
-      if (e.detail.keyboardEvent.shiftKey) {
-        this._expandCollapseComments(true);
-      } else {
-        if (this.modifierPressed(e)) { return; }
-        this._expandCollapseComments(false);
-      }
-    },
-
-    _expandCollapseComments(actionIsCollapse) {
-      const comments =
-          Polymer.dom(this.root).querySelectorAll('gr-diff-comment');
-      for (const comment of comments) {
-        comment.collapsed = actionIsCollapse;
-      }
-    },
-
-    /**
-     * Sets the initial state of the comment thread.
-     * Expands the thread if one of the following is true:
-     * - last {UNRESOLVED_EXPAND_COUNT} comments expanded by default if the
-     * thread is unresolved,
-     * - it's a robot comment.
-     */
-    _setInitialExpandedState() {
-      if (this._orderedComments) {
-        for (let i = 0; i < this._orderedComments.length; i++) {
-          const comment = this._orderedComments[i];
-          const isRobotComment = !!comment.robot_id;
-          // False if it's an unresolved comment under UNRESOLVED_EXPAND_COUNT.
-          const resolvedThread = !this.unresolved ||
-                this._orderedComments.length - i - 1 >= UNRESOLVED_EXPAND_COUNT;
-          comment.collapsed = !isRobotComment && resolvedThread;
-        }
-      }
-    },
-
-    _sortedComments(comments) {
-      return comments.slice().sort((c1, c2) => {
-        const c1Date = c1.__date || util.parseDate(c1.updated);
-        const c2Date = c2.__date || util.parseDate(c2.updated);
-        const dateCompare = c1Date - c2Date;
-        // Ensure drafts are at the end. There should only be one but in edge
-        // cases could be more. In the unlikely event two drafts are being
-        // compared, use the typical date compare.
-        if (c2.__draft && !c1.__draft ) { return 0; }
-        if (c1.__draft && !c2.__draft ) { return 1; }
-        if (dateCompare === 0 && (!c1.id || !c1.id.localeCompare)) { return 0; }
-        // If same date, fall back to sorting by id.
-        return dateCompare ? dateCompare : c1.id.localeCompare(c2.id);
-      });
-    },
-
-    _createReplyComment(parent, content, opt_isEditing,
-        opt_unresolved) {
-      this.$.reporting.recordDraftInteraction();
-      const reply = this._newReply(
-          this._orderedComments[this._orderedComments.length - 1].id,
-          parent.line,
-          content,
-          opt_unresolved,
-          parent.range);
-
-      // If there is currently a comment in an editing state, add an attribute
-      // so that the gr-diff-comment knows not to populate the draft text.
-      for (let i = 0; i < this.comments.length; i++) {
-        if (this.comments[i].__editing) {
-          reply.__otherEditing = true;
-          break;
-        }
-      }
-
-      if (opt_isEditing) {
-        reply.__editing = true;
-      }
-
-      this.push('comments', reply);
-
-      if (!opt_isEditing) {
-        // Allow the reply to render in the dom-repeat.
-        this.async(() => {
-          const commentEl = this._commentElWithDraftID(reply.__draftID);
-          commentEl.save();
-        }, 1);
-      }
-    },
-
-    _isDraft(comment) {
-      return !!comment.__draft;
-    },
-
-    /**
-     * @param {boolean=} opt_quote
-     */
-    _processCommentReply(opt_quote) {
-      const comment = this._lastComment;
-      let quoteStr;
-      if (opt_quote) {
-        const msg = comment.message;
-        quoteStr = '> ' + msg.replace(NEWLINE_PATTERN, '\n> ') + '\n\n';
-      }
-      this._createReplyComment(comment, quoteStr, true, comment.unresolved);
-    },
-
-    _handleCommentReply(e) {
-      this._processCommentReply();
-    },
-
-    _handleCommentQuote(e) {
-      this._processCommentReply(true);
-    },
-
-    _handleCommentAck(e) {
-      const comment = this._lastComment;
-      this._createReplyComment(comment, 'Ack', false, false);
-    },
-
-    _handleCommentDone(e) {
-      const comment = this._lastComment;
-      this._createReplyComment(comment, 'Done', false, false);
-    },
-
-    _handleCommentFix(e) {
-      const comment = e.detail.comment;
-      const msg = comment.message;
-      const quoteStr = '> ' + msg.replace(NEWLINE_PATTERN, '\n> ') + '\n\n';
-      const response = quoteStr + 'Please Fix';
-      this._createReplyComment(comment, response, false, true);
-    },
-
-    _commentElWithDraftID(id) {
-      const els = Polymer.dom(this.root).querySelectorAll('gr-diff-comment');
-      for (const el of els) {
-        if (el.comment.id === id || el.comment.__draftID === id) {
-          return el;
-        }
-      }
-      return null;
-    },
-
-    _newReply(inReplyTo, opt_lineNum, opt_message, opt_unresolved,
-        opt_range) {
-      const d = this._newDraft(opt_lineNum);
-      d.in_reply_to = inReplyTo;
-      d.range = opt_range;
-      if (opt_message != null) {
-        d.message = opt_message;
-      }
-      if (opt_unresolved !== undefined) {
-        d.unresolved = opt_unresolved;
-      }
-      return d;
-    },
-
-    /**
-     * @param {number=} opt_lineNum
-     * @param {!Object=} opt_range
-     */
-    _newDraft(opt_lineNum, opt_range) {
-      const d = {
-        __draft: true,
-        __draftID: Math.random().toString(36),
-        __date: new Date(),
-        path: this.path,
-        patchNum: this.patchNum,
-        side: this._getSide(this.isOnParent),
-        __commentSide: this.commentSide,
-      };
-      if (opt_lineNum) {
-        d.line = opt_lineNum;
-      }
-      if (opt_range) {
-        d.range = {
-          start_line: opt_range.startLine,
-          start_character: opt_range.startChar,
-          end_line: opt_range.endLine,
-          end_character: opt_range.endChar,
-        };
-      }
-      if (this.parentIndex) {
-        d.parent = this.parentIndex;
-      }
-      return d;
-    },
-
-    _getSide(isOnParent) {
-      if (isOnParent) { return 'PARENT'; }
-      return 'REVISION';
-    },
-
-    _computeRootId(comments) {
-      // Keep the root ID even if the comment was removed, so that notification
-      // to sync will know which thread to remove.
-      if (!comments.base.length) { return this.rootId; }
-      const rootComment = comments.base[0];
-      return rootComment.id || rootComment.__draftID;
-    },
-
-    _handleCommentDiscard(e) {
-      const diffCommentEl = Polymer.dom(e).rootTarget;
-      const comment = diffCommentEl.comment;
-      const idx = this._indexOf(comment, this.comments);
-      if (idx == -1) {
-        throw Error('Cannot find comment ' +
-            JSON.stringify(diffCommentEl.comment));
-      }
-      this.splice('comments', idx, 1);
-      if (this.comments.length === 0) {
-        this.fireRemoveSelf();
-      }
-      this._handleCommentSavedOrDiscarded(e);
-
-      // Check to see if there are any other open comments getting edited and
-      // set the local storage value to its message value.
-      for (const changeComment of this.comments) {
-        if (changeComment.__editing) {
-          const commentLocation = {
-            changeNum: this.changeNum,
-            patchNum: this.patchNum,
-            path: changeComment.path,
-            line: changeComment.line,
-          };
-          return this.$.storage.setDraftComment(commentLocation,
-              changeComment.message);
-        }
-      }
-    },
-
-    _handleCommentSavedOrDiscarded(e) {
-      this.dispatchEvent(new CustomEvent('thread-changed',
-          {detail: {rootId: this.rootId, path: this.path},
-            bubbles: false}));
-    },
-
-    _handleCommentUpdate(e) {
-      const comment = e.detail.comment;
-      const index = this._indexOf(comment, this.comments);
-      if (index === -1) {
-        // This should never happen: comment belongs to another thread.
-        console.warn('Comment update for another comment thread.');
-        return;
-      }
-      this.set(['comments', index], comment);
-      // Because of the way we pass these comment objects around by-ref, in
-      // combination with the fact that Polymer does dirty checking in
-      // observers, the this.set() call above will not cause a thread update in
-      // some situations.
-      this.updateThreadProperties();
-    },
-
-    _indexOf(comment, arr) {
-      for (let i = 0; i < arr.length; i++) {
-        const c = arr[i];
-        if ((c.__draftID != null && c.__draftID == comment.__draftID) ||
-            (c.id != null && c.id == comment.id)) {
-          return i;
-        }
-      }
-      return -1;
-    },
-
-    _computeHostClass(unresolved) {
-      return unresolved ? 'unresolved' : '';
-    },
-
-    /**
-     * Load the project config when a project name has been provided.
-     *
-     * @param {string} name The project name.
-     */
-    _projectNameChanged(name) {
-      if (!name) { return; }
-      this.$.restAPI.getProjectConfig(name).then(config => {
-        this._projectConfig = config;
-      });
-    },
-  });
-})();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
deleted file mode 100644
index 868be90..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
+++ /dev/null
@@ -1,718 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-diff-comment-thread</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-diff-comment-thread.html">
-
-<script>void(0);</script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-diff-comment-thread></gr-diff-comment-thread>
-  </template>
-</test-fixture>
-
-<test-fixture id="withComment">
-  <template>
-    <gr-diff-comment-thread></gr-diff-comment-thread>
-  </template>
-</test-fixture>
-
-<script>
-  suite('gr-diff-comment-thread tests', () => {
-    let element;
-    let sandbox;
-
-    setup(() => {
-      sandbox = sinon.sandbox.create();
-      stub('gr-rest-api-interface', {
-        getLoggedIn() { return Promise.resolve(false); },
-      });
-      sandbox = sinon.sandbox.create();
-      element = fixture('basic');
-    });
-
-    teardown(() => {
-      sandbox.restore();
-    });
-
-    test('comments are sorted correctly', () => {
-      const comments = [
-        {
-          message: 'i like you, too',
-          in_reply_to: 'sallys_confession',
-          __date: new Date('2015-12-25'),
-        }, {
-          id: 'sallys_confession',
-          message: 'i like you, jack',
-          updated: '2015-12-24 15:00:20.396000000',
-        }, {
-          id: 'sally_to_dr_finklestein',
-          message: 'i’m running away',
-          updated: '2015-10-31 09:00:20.396000000',
-        }, {
-          id: 'sallys_defiance',
-          in_reply_to: 'sally_to_dr_finklestein',
-          message: 'i will poison you so i can get away',
-          updated: '2015-10-31 15:00:20.396000000',
-        }, {
-          id: 'dr_finklesteins_response',
-          in_reply_to: 'sally_to_dr_finklestein',
-          message: 'no i will pull a thread and your arm will fall off',
-          updated: '2015-10-31 11:00:20.396000000',
-        }, {
-          id: 'sallys_mission',
-          message: 'i have to find santa',
-          updated: '2015-12-24 15:00:20.396000000',
-        },
-      ];
-      const results = element._sortedComments(comments);
-      assert.deepEqual(results, [
-        {
-          id: 'sally_to_dr_finklestein',
-          message: 'i’m running away',
-          updated: '2015-10-31 09:00:20.396000000',
-        }, {
-          id: 'dr_finklesteins_response',
-          in_reply_to: 'sally_to_dr_finklestein',
-          message: 'no i will pull a thread and your arm will fall off',
-          updated: '2015-10-31 11:00:20.396000000',
-        }, {
-          id: 'sallys_defiance',
-          in_reply_to: 'sally_to_dr_finklestein',
-          message: 'i will poison you so i can get away',
-          updated: '2015-10-31 15:00:20.396000000',
-        }, {
-          id: 'sallys_confession',
-          message: 'i like you, jack',
-          updated: '2015-12-24 15:00:20.396000000',
-        }, {
-          id: 'sallys_mission',
-          message: 'i have to find santa',
-          updated: '2015-12-24 15:00:20.396000000',
-        }, {
-          message: 'i like you, too',
-          in_reply_to: 'sallys_confession',
-          __date: new Date('2015-12-25'),
-        },
-      ]);
-    });
-
-    test('addOrEditDraft w/ edit draft', () => {
-      element.comments = [{
-        id: 'jacks_reply',
-        message: 'i like you, too',
-        in_reply_to: 'sallys_confession',
-        updated: '2015-12-25 15:00:20.396000000',
-        __draft: true,
-      }];
-      const commentElStub = sandbox.stub(element, '_commentElWithDraftID',
-          () => { return {}; });
-      const addDraftStub = sandbox.stub(element, 'addDraft');
-
-      element.addOrEditDraft(123);
-
-      assert.isTrue(commentElStub.called);
-      assert.isFalse(addDraftStub.called);
-    });
-
-    test('addOrEditDraft w/o edit draft', () => {
-      element.comments = [];
-      const commentElStub = sandbox.stub(element, '_commentElWithDraftID',
-          () => { return {}; });
-      const addDraftStub = sandbox.stub(element, 'addDraft');
-
-      element.addOrEditDraft(123);
-
-      assert.isFalse(commentElStub.called);
-      assert.isTrue(addDraftStub.called);
-    });
-
-    test('_hideActions', () => {
-      let showActions = true;
-      const lastComment = {};
-      assert.equal(element._hideActions(showActions, lastComment), false);
-      showActions = false;
-      assert.equal(element._hideActions(showActions, lastComment), true);
-      showActions = true;
-      lastComment.__draft = true;
-      assert.equal(element._hideActions(showActions, lastComment), true);
-    });
-
-    test('setting project name loads the project config', done => {
-      const projectName = 'foo/bar/baz';
-      const getProjectStub = sandbox.stub(element.$.restAPI, 'getProjectConfig')
-          .returns(Promise.resolve({}));
-      element.projectName = projectName;
-      flush(() => {
-        assert.isTrue(getProjectStub.calledWithExactly(projectName));
-        done();
-      });
-    });
-
-    test('optionally show file path', () => {
-      // Path info doesn't exist when showFilePath is false. Because it's in a
-      // dom-if it is not yet in the dom.
-      assert.isNotOk(element.$$('.pathInfo'));
-
-      sandbox.stub(Gerrit.Nav, 'getUrlForDiffById');
-      element.changeNum = 123;
-      element.projectName = 'test project';
-      element.path = 'path/to/file';
-      element.patchNum = 3;
-      element.lineNum = 5;
-      element.showFilePath = true;
-      flushAsynchronousOperations();
-      assert.isOk(element.$$('.pathInfo'));
-      assert.notEqual(getComputedStyle(element.$$('.pathInfo')).display,
-          'none');
-      assert.isTrue(Gerrit.Nav.getUrlForDiffById.lastCall.calledWithExactly(
-          element.changeNum, element.projectName, element.path,
-          element.patchNum, null, element.lineNum));
-    });
-
-    test('_computeDisplayPath', () => {
-      const path = 'path/to/file';
-      assert.equal(element._computeDisplayPath(path), 'path/to/file');
-
-      element.lineNum = 5;
-      assert.equal(element._computeDisplayPath(path), 'path/to/file#5');
-    });
-  });
-
-  suite('comment action tests', () => {
-    let element;
-    let sandbox;
-
-    setup(() => {
-      sandbox = sinon.sandbox.create();
-      stub('gr-rest-api-interface', {
-        getLoggedIn() { return Promise.resolve(false); },
-        saveDiffDraft() {
-          return Promise.resolve({
-            ok: true,
-            text() {
-              return Promise.resolve(')]}\'\n' +
-                  JSON.stringify({
-                    id: '7afa4931_de3d65bd',
-                    path: '/path/to/file.txt',
-                    line: 5,
-                    in_reply_to: 'baf0414d_60047215',
-                    updated: '2015-12-21 02:01:10.850000000',
-                    message: 'Done',
-                  }));
-            },
-          });
-        },
-        deleteDiffDraft() { return Promise.resolve({ok: true}); },
-      });
-      element = fixture('withComment');
-      element.comments = [{
-        author: {
-          name: 'Mr. Peanutbutter',
-          email: 'tenn1sballchaser@aol.com',
-        },
-        id: 'baf0414d_60047215',
-        line: 5,
-        message: 'is this a crossover episode!?',
-        updated: '2015-12-08 19:48:33.843000000',
-        path: '/path/to/file.txt',
-      }];
-      flushAsynchronousOperations();
-    });
-
-    teardown(() => {
-      sandbox.restore();
-    });
-
-    test('reply', () => {
-      const commentEl = element.$$('gr-diff-comment');
-      const reportStub = sandbox.stub(element.$.reporting,
-          'recordDraftInteraction');
-      assert.ok(commentEl);
-
-      const replyBtn = element.$.replyBtn;
-      MockInteractions.tap(replyBtn);
-      flushAsynchronousOperations();
-
-      const drafts = element._orderedComments.filter(c => {
-        return c.__draft == true;
-      });
-      assert.equal(drafts.length, 1);
-      assert.notOk(drafts[0].message, 'message should be empty');
-      assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
-      assert.isTrue(reportStub.calledOnce);
-    });
-
-    test('quote reply', () => {
-      const commentEl = element.$$('gr-diff-comment');
-      const reportStub = sandbox.stub(element.$.reporting,
-          'recordDraftInteraction');
-      assert.ok(commentEl);
-
-      const quoteBtn = element.$.quoteBtn;
-      MockInteractions.tap(quoteBtn);
-      flushAsynchronousOperations();
-
-      const drafts = element._orderedComments.filter(c => {
-        return c.__draft == true;
-      });
-      assert.equal(drafts.length, 1);
-      assert.equal(drafts[0].message, '> is this a crossover episode!?\n\n');
-      assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
-      assert.isTrue(reportStub.calledOnce);
-    });
-
-    test('quote reply multiline', () => {
-      const reportStub = sandbox.stub(element.$.reporting,
-          'recordDraftInteraction');
-      element.comments = [{
-        author: {
-          name: 'Mr. Peanutbutter',
-          email: 'tenn1sballchaser@aol.com',
-        },
-        id: 'baf0414d_60047215',
-        line: 5,
-        message: 'is this a crossover episode!?\nIt might be!',
-        updated: '2015-12-08 19:48:33.843000000',
-      }];
-      flushAsynchronousOperations();
-
-      const commentEl = element.$$('gr-diff-comment');
-      assert.ok(commentEl);
-
-      const quoteBtn = element.$.quoteBtn;
-      MockInteractions.tap(quoteBtn);
-      flushAsynchronousOperations();
-
-      const drafts = element._orderedComments.filter(c => {
-        return c.__draft == true;
-      });
-      assert.equal(drafts.length, 1);
-      assert.equal(drafts[0].message,
-          '> is this a crossover episode!?\n> It might be!\n\n');
-      assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
-      assert.isTrue(reportStub.calledOnce);
-    });
-
-    test('ack', done => {
-      const reportStub = sandbox.stub(element.$.reporting,
-          'recordDraftInteraction');
-      element.changeNum = '42';
-      element.patchNum = '1';
-
-      const commentEl = element.$$('gr-diff-comment');
-      assert.ok(commentEl);
-
-      const ackBtn = element.$.ackBtn;
-      MockInteractions.tap(ackBtn);
-      flush(() => {
-        const drafts = element.comments.filter(c => {
-          return c.__draft == true;
-        });
-        assert.equal(drafts.length, 1);
-        assert.equal(drafts[0].message, 'Ack');
-        assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
-        assert.equal(drafts[0].unresolved, false);
-        assert.isTrue(reportStub.calledOnce);
-        done();
-      });
-    });
-
-    test('done', done => {
-      const reportStub = sandbox.stub(element.$.reporting,
-          'recordDraftInteraction');
-      element.changeNum = '42';
-      element.patchNum = '1';
-      const commentEl = element.$$('gr-diff-comment');
-      assert.ok(commentEl);
-
-      const doneBtn = element.$.doneBtn;
-      MockInteractions.tap(doneBtn);
-      flush(() => {
-        const drafts = element.comments.filter(c => {
-          return c.__draft == true;
-        });
-        assert.equal(drafts.length, 1);
-        assert.equal(drafts[0].message, 'Done');
-        assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
-        assert.isFalse(drafts[0].unresolved);
-        assert.isTrue(reportStub.calledOnce);
-        done();
-      });
-    });
-
-    test('save', done => {
-      element.changeNum = '42';
-      element.patchNum = '1';
-      element.path = '/path/to/file.txt';
-      const commentEl = element.$$('gr-diff-comment');
-      assert.ok(commentEl);
-
-      const saveOrDiscardStub = sandbox.stub();
-      element.addEventListener('thread-changed', saveOrDiscardStub);
-      element.$$('gr-diff-comment')._fireSave();
-
-      flush(() => {
-        assert.isTrue(saveOrDiscardStub.called);
-        assert.equal(saveOrDiscardStub.lastCall.args[0].detail.rootId,
-            'baf0414d_60047215');
-        assert.equal(element.rootId, 'baf0414d_60047215');
-        assert.equal(saveOrDiscardStub.lastCall.args[0].detail.path,
-            '/path/to/file.txt');
-        done();
-      });
-    });
-
-    test('please fix', done => {
-      element.changeNum = '42';
-      element.patchNum = '1';
-      const commentEl = element.$$('gr-diff-comment');
-      assert.ok(commentEl);
-      commentEl.addEventListener('create-fix-comment', () => {
-        const drafts = element._orderedComments.filter(c => {
-          return c.__draft == true;
-        });
-        assert.equal(drafts.length, 1);
-        assert.equal(
-            drafts[0].message, '> is this a crossover episode!?\n\nPlease Fix');
-        assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
-        assert.isTrue(drafts[0].unresolved);
-        done();
-      });
-      commentEl.fire('create-fix-comment', {comment: commentEl.comment},
-          {bubbles: false});
-    });
-
-    test('discard', done => {
-      element.changeNum = '42';
-      element.patchNum = '1';
-      element.path = '/path/to/file.txt';
-      element.push('comments', element._newReply(
-          element.comments[0].id,
-          element.comments[0].line,
-          element.comments[0].path,
-          'it’s pronouced jiff, not giff'));
-      flushAsynchronousOperations();
-
-      const saveOrDiscardStub = sandbox.stub();
-      element.addEventListener('thread-changed', saveOrDiscardStub);
-      const draftEl =
-          Polymer.dom(element.root).querySelectorAll('gr-diff-comment')[1];
-      assert.ok(draftEl);
-      draftEl.addEventListener('comment-discard', () => {
-        const drafts = element.comments.filter(c => {
-          return c.__draft == true;
-        });
-        assert.equal(drafts.length, 0);
-        assert.isTrue(saveOrDiscardStub.called);
-        assert.equal(saveOrDiscardStub.lastCall.args[0].detail.rootId,
-            element.rootId);
-        assert.equal(saveOrDiscardStub.lastCall.args[0].detail.path,
-            element.path);
-        done();
-      });
-      draftEl.fire('comment-discard', {comment: draftEl.comment},
-          {bubbles: false});
-    });
-
-    test('discard with a single comment still fires event with previous rootId',
-        done => {
-          element.changeNum = '42';
-          element.patchNum = '1';
-          element.path = '/path/to/file.txt';
-          element.comments = [];
-          element.addOrEditDraft('1');
-          flushAsynchronousOperations();
-          const rootId = element.rootId;
-          assert.isOk(rootId);
-
-          const saveOrDiscardStub = sandbox.stub();
-          element.addEventListener('thread-changed', saveOrDiscardStub);
-          const draftEl =
-          Polymer.dom(element.root).querySelectorAll('gr-diff-comment')[0];
-          assert.ok(draftEl);
-          draftEl.addEventListener('comment-discard', () => {
-            assert.equal(element.comments.length, 0);
-            assert.isTrue(saveOrDiscardStub.called);
-            assert.equal(saveOrDiscardStub.lastCall.args[0].detail.rootId,
-                rootId);
-            assert.equal(saveOrDiscardStub.lastCall.args[0].detail.path,
-                element.path);
-            done();
-          });
-          draftEl.fire('comment-discard', {comment: draftEl.comment},
-              {bubbles: false});
-        });
-
-    test('first editing comment does not add __otherEditing attribute', () => {
-      element.comments = [{
-        author: {
-          name: 'Mr. Peanutbutter',
-          email: 'tenn1sballchaser@aol.com',
-        },
-        id: 'baf0414d_60047215',
-        line: 5,
-        message: 'is this a crossover episode!?',
-        updated: '2015-12-08 19:48:33.843000000',
-        __draft: true,
-      }];
-
-      const replyBtn = element.$.replyBtn;
-      MockInteractions.tap(replyBtn);
-      flushAsynchronousOperations();
-
-      const editing = element._orderedComments.filter(c => {
-        return c.__editing == true;
-      });
-      assert.equal(editing.length, 1);
-      assert.equal(!!editing[0].__otherEditing, false);
-    });
-
-    test('When not editing other comments, local storage not set' +
-        ' after discard', done => {
-      element.changeNum = '42';
-      element.patchNum = '1';
-      element.comments = [{
-        author: {
-          name: 'Mr. Peanutbutter',
-          email: 'tenn1sballchaser@aol.com',
-        },
-        id: 'baf0414d_60047215',
-        line: 5,
-        message: 'is this a crossover episode!?',
-        updated: '2015-12-08 19:48:31.843000000',
-      },
-      {
-        author: {
-          name: 'Mr. Peanutbutter',
-          email: 'tenn1sballchaser@aol.com',
-        },
-        __draftID: '1',
-        in_reply_to: 'baf0414d_60047215',
-        line: 5,
-        message: 'yes',
-        updated: '2015-12-08 19:48:32.843000000',
-        __draft: true,
-        __editing: true,
-      },
-      {
-        author: {
-          name: 'Mr. Peanutbutter',
-          email: 'tenn1sballchaser@aol.com',
-        },
-        __draftID: '2',
-        in_reply_to: 'baf0414d_60047215',
-        line: 5,
-        message: 'no',
-        updated: '2015-12-08 19:48:33.843000000',
-        __draft: true,
-      }];
-      const storageStub = sinon.stub(element.$.storage, 'setDraftComment');
-      flushAsynchronousOperations();
-
-      const draftEl =
-      Polymer.dom(element.root).querySelectorAll('gr-diff-comment')[1];
-      assert.ok(draftEl);
-      draftEl.addEventListener('comment-discard', () => {
-        assert.isFalse(storageStub.called);
-        storageStub.restore();
-        done();
-      });
-      draftEl.fire('comment-discard', {comment: draftEl.comment},
-          {bubbles: false});
-    });
-
-    test('comment-update', () => {
-      const commentEl = element.$$('gr-diff-comment');
-      const updatedComment = {
-        id: element.comments[0].id,
-        foo: 'bar',
-      };
-      commentEl.fire('comment-update', {comment: updatedComment});
-      assert.strictEqual(element.comments[0], updatedComment);
-    });
-
-    suite('jack and sally comment data test consolidation', () => {
-      setup(() => {
-        element.comments = [
-          {
-            id: 'jacks_reply',
-            message: 'i like you, too',
-            in_reply_to: 'sallys_confession',
-            updated: '2015-12-25 15:00:20.396000000',
-            unresolved: false,
-          }, {
-            id: 'sallys_confession',
-            in_reply_to: 'nonexistent_comment',
-            message: 'i like you, jack',
-            updated: '2015-12-24 15:00:20.396000000',
-          }, {
-            id: 'sally_to_dr_finklestein',
-            in_reply_to: 'nonexistent_comment',
-            message: 'i’m running away',
-            updated: '2015-10-31 09:00:20.396000000',
-          }, {
-            id: 'sallys_defiance',
-            message: 'i will poison you so i can get away',
-            updated: '2015-10-31 15:00:20.396000000',
-          }];
-      });
-
-      test('orphan replies', () => {
-        assert.equal(4, element._orderedComments.length);
-      });
-
-      test('keyboard shortcuts', () => {
-        const expandCollapseStub =
-            sinon.stub(element, '_expandCollapseComments');
-        MockInteractions.pressAndReleaseKeyOn(element, 69, null, 'e');
-        assert.isTrue(expandCollapseStub.lastCall.calledWith(false));
-
-        MockInteractions.pressAndReleaseKeyOn(element, 69, 'shift', 'e');
-        assert.isTrue(expandCollapseStub.lastCall.calledWith(true));
-      });
-
-      test('comment in_reply_to is either null or most recent comment', () => {
-        element._createReplyComment(element.comments[3], 'dummy', true);
-        flushAsynchronousOperations();
-        assert.equal(element._orderedComments.length, 5);
-        assert.equal(element._orderedComments[4].in_reply_to, 'jacks_reply');
-      });
-
-      test('resolvable comments', () => {
-        assert.isFalse(element.unresolved);
-        element._createReplyComment(element.comments[3], 'dummy', true, true);
-        flushAsynchronousOperations();
-        assert.isTrue(element.unresolved);
-      });
-
-      test('_setInitialExpandedState', () => {
-        element.unresolved = true;
-        element._setInitialExpandedState();
-        for (let i = 0; i < element.comments.length; i++) {
-          assert.isFalse(element.comments[i].collapsed);
-        }
-        element.unresolved = false;
-        element._setInitialExpandedState();
-        for (let i = 0; i < element.comments.length; i++) {
-          assert.isTrue(element.comments[i].collapsed);
-        }
-        for (let i = 0; i < element.comments.length; i++) {
-          element.comments[i].robot_id = 123;
-        }
-        element._setInitialExpandedState();
-        for (let i = 0; i < element.comments.length; i++) {
-          assert.isFalse(element.comments[i].collapsed);
-        }
-      });
-    });
-
-    test('_computeHostClass', () => {
-      assert.equal(element._computeHostClass(true), 'unresolved');
-      assert.equal(element._computeHostClass(false), '');
-    });
-
-    test('addDraft sets unresolved state correctly', () => {
-      let unresolved = true;
-      element.comments = [];
-      element.addDraft(null, null, unresolved);
-      assert.equal(element.comments[0].unresolved, true);
-
-      unresolved = false; // comment should get added as actually resolved.
-      element.comments = [];
-      element.addDraft(null, null, unresolved);
-      assert.equal(element.comments[0].unresolved, false);
-
-      element.comments = [];
-      element.addDraft();
-      assert.equal(element.comments[0].unresolved, true);
-    });
-
-    test('_newDraft', () => {
-      element.commentSide = 'left';
-      element.patchNum = 3;
-      const draft = element._newDraft();
-      assert.equal(draft.__commentSide, 'left');
-      assert.equal(draft.patchNum, 3);
-    });
-
-    test('new comment gets created', () => {
-      element.comments = [];
-      element.addOrEditDraft(1);
-      assert.equal(element.comments.length, 1);
-      // Mock a submitted comment.
-      element.comments[0].id = element.comments[0].__draftID;
-      element.comments[0].__draft = false;
-      element.addOrEditDraft(1);
-      assert.equal(element.comments.length, 2);
-    });
-
-    test('unresolved label', () => {
-      element.unresolved = false;
-      assert.isTrue(element.$.unresolvedLabel.hasAttribute('hidden'));
-      element.unresolved = true;
-      assert.isFalse(element.$.unresolvedLabel.hasAttribute('hidden'));
-    });
-
-    test('draft comments are at the end of orderedComments', () => {
-      element.comments = [{
-        author: {
-          name: 'Mr. Peanutbutter',
-          email: 'tenn1sballchaser@aol.com',
-        },
-        id: 2,
-        line: 5,
-        message: 'Earlier draft',
-        updated: '2015-12-08 19:48:33.843000000',
-        __draft: true,
-      },
-      {
-        author: {
-          name: 'Mr. Peanutbutter2',
-          email: 'tenn1sballchaser@aol.com',
-        },
-        id: 1,
-        line: 5,
-        message: 'This comment was left last but is not a draft',
-        updated: '2015-12-10 19:48:33.843000000',
-      },
-      {
-        author: {
-          name: 'Mr. Peanutbutter2',
-          email: 'tenn1sballchaser@aol.com',
-        },
-        id: 3,
-        line: 5,
-        message: 'Later draft',
-        updated: '2015-12-09 19:48:33.843000000',
-        __draft: true,
-      }];
-      assert.equal(element._orderedComments[0].id, '1');
-      assert.equal(element._orderedComments[1].id, '2');
-      assert.equal(element._orderedComments[2].id, '3');
-    });
-  });
-</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
deleted file mode 100644
index 6ea0330..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
+++ /dev/null
@@ -1,389 +0,0 @@
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<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="../../../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">
-<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.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">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<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="../../shared/gr-textarea/gr-textarea.html">
-<link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html">
-<link rel="import" href="../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html">
-<script src="../../../scripts/rootElement.js"></script>
-
-<dom-module id="gr-diff-comment">
-  <template>
-    <style include="shared-styles">
-      :host {
-        display: block;
-        font-family: var(--font-family);
-        padding: .7em .7em;
-        --iron-autogrow-textarea: {
-          box-sizing: border-box;
-          padding: 2px;
-        };
-      }
-      :host([disabled]) {
-        pointer-events: none;
-      }
-      :host([disabled]) .actions,
-      :host([disabled]) .robotActions,
-      :host([disabled]) .date {
-        opacity: .5;
-      }
-      :host([discarding]) {
-        display: none;
-      }
-      .header {
-        align-items: baseline;
-        cursor: pointer;
-        display: flex;
-        font-family: 'Open Sans', sans-serif;
-        margin: -.7em -.7em 0 -.7em;
-        padding: .7em;
-      }
-      .container.collapsed .header {
-        margin-bottom: -.7em;
-      }
-      .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);
-        display: none;
-      }
-      .date {
-        justify-content: flex-end;
-        margin-left: 5px;
-        min-width: 4.5em;
-        text-align: right;
-        white-space: nowrap;
-      }
-      a.date:link,
-      a.date:visited {
-        color: var(--deemphasized-text-color);
-      }
-      .actions {
-        display: flex;
-        justify-content: flex-end;
-        padding-top: 0;
-      }
-      .action {
-        margin-left: 1em;
-      }
-      .robotActions {
-        display: flex;
-        justify-content: flex-start;
-        padding-top: 0;
-      }
-      .robotActions .action {
-        /* Keep button text lined up with output text */
-        margin-left: -.3rem;
-        margin-right: 1em;
-      }
-      .rightActions {
-        display: flex;
-        justify-content: flex-end;
-      }
-      .editMessage {
-        display: none;
-        margin: .5em 0;
-        width: 100%;
-      }
-      .container:not(.draft) .actions .hideOnPublished {
-        display: none;
-      }
-      .draft .reply,
-      .draft .quote,
-      .draft .ack,
-      .draft .done {
-        display: none;
-      }
-      .draft .draftLabel,
-      .draft .draftTooltip {
-        display: inline;
-      }
-      .draft:not(.editing) .save,
-      .draft:not(.editing) .cancel {
-        display: none;
-      }
-      .editing .message,
-      .editing .reply,
-      .editing .quote,
-      .editing .ack,
-      .editing .done,
-      .editing .edit,
-      .editing .discard,
-      .editing .unresolved {
-        display: none;
-      }
-      .editing .editMessage {
-        display: block;
-      }
-      .show-hide {
-        margin-left: .4em;
-      }
-      .robotId {
-        color: var(--deemphasized-text-color);
-        margin-bottom: .8em;
-        margin-top: -.4em;
-      }
-      .robotIcon {
-        margin-right: .2em;
-        /* 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;
-      }
-      .runIdInformation {
-        margin: .7em 0;
-      }
-      .robotRun {
-        margin-left: .5em;
-      }
-      .robotRunLink {
-        margin-left: .5em;
-      }
-      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;
-      }
-      #container .collapsedContent {
-        display: none;
-      }
-      #container.collapsed {
-        padding-bottom: 3px;
-      }
-      #container.collapsed .collapsedContent {
-        display: block;
-        overflow: hidden;
-        padding-left: 5px;
-        text-overflow: ellipsis;
-        white-space: nowrap;
-      }
-      #container.collapsed .actions,
-      #container.collapsed gr-formatted-text,
-      #container.collapsed gr-textarea {
-        display: none;
-      }
-      .resolve,
-      .unresolved {
-        align-items: center;
-        display: flex;
-        flex: 1;
-        margin: 0;
-      }
-      .resolve label {
-        color: var(--comment-text-color);
-      }
-      gr-dialog .main {
-        display: flex;
-        flex-direction: column;
-        width: 100%;
-      }
-      #deleteBtn {
-        display: none;
-        --gr-button: {
-          color: var(--deemphasized-text-color);
-          padding: 0;
-        }
-      }
-      #deleteBtn.showDeleteButtons {
-        display: block;
-      }
-    </style>
-    <div id="container"
-        class="container"
-        on-mouseenter="_handleMouseEnter"
-        on-mouseleave="_handleMouseLeave">
-      <div class="header" id="header" on-tap="_handleToggleCollapsed">
-        <div class="headerLeft">
-          <span class="authorName">[[comment.author.name]]</span>
-          <span class="draftLabel">DRAFT</span>
-          <gr-tooltip-content class="draftTooltip"
-              has-tooltip
-              title="This draft is only visible to you. To publish drafts, click the 'Reply' or 'Start review' button at the top of the change or press the 'A' key."
-              max-width="20em"
-              show-icon></gr-tooltip-content>
-        </div>
-        <div class="headerMiddle">
-          <span class="collapsedContent">[[comment.message]]</span>
-        </div>
-        <gr-button
-            id="deleteBtn"
-            link
-            secondary
-            class$="action delete [[_computeDeleteButtonClass(_isAdmin, draft)]]"
-            on-tap="_handleCommentDelete">
-          (Delete)
-        </gr-button>
-        <a class="date" href$="[[_computeLinkToComment(comment)]]" on-tap="_handleLinkTap">
-          <gr-date-formatter
-              has-tooltip
-              date-str="[[comment.updated]]"></gr-date-formatter>
-        </a>
-        <div class="show-hide">
-          <label class="show-hide">
-            <input type="checkbox" class="show-hide"
-               checked$="[[collapsed]]"
-               on-change="_handleToggleCollapsed">
-            [[_computeShowHideText(collapsed)]]
-          </label>
-        </div>
-      </div>
-      <div class="body">
-        <template is="dom-if" if="[[comment.robot_id]]">
-          <div class="robotId" hidden$="[[collapsed]]">
-            <iron-icon class="robotIcon" icon="gr-icons:robot"></iron-icon>
-            [[comment.robot_id]]
-          </div>
-        </template>
-        <template is="dom-if" if="[[editing]]">
-          <gr-textarea
-              id="editTextarea"
-              class="editMessage"
-              autocomplete="on"
-              monospace
-              disabled="{{disabled}}"
-              rows="4"
-              text="{{_messageText}}"></gr-textarea>
-        </template>
-        <!--The message class is needed to ensure selectability from
-        gr-diff-selection.-->
-        <gr-formatted-text class="message"
-            content="[[comment.message]]"
-            no-trailing-margin="[[!comment.__draft]]"
-            collapsed="[[collapsed]]"
-            config="[[projectConfig.commentlinks]]"></gr-formatted-text>
-        <div hidden$="[[!comment.robot_run_id]]" class="message">
-          <div class="runIdInformation" hidden$="[[collapsed]]">
-            Run ID:
-            <template is="dom-if" if="[[comment.url]]">
-              <a class="robotRunLink" href$="[[comment.url]]">
-                <span class="robotRun link">[[comment.robot_run_id]]</span>
-              </a>
-            </template>
-            <template is="dom-if" if="[[!comment.url]]">
-              <span class="robotRun text">[[comment.robot_run_id]]</span>
-            </template>
-          </div>
-        </div>
-        <div class="actions humanActions" hidden$="[[!_showHumanActions]]">
-          <div class="action resolve hideOnPublished">
-            <label>
-              <input type="checkbox"
-                  id="resolvedCheckbox"
-                  checked="[[resolved]]"
-                  on-change="_handleToggleResolved">
-              Resolved
-            </label>
-          </div>
-          <div class="rightActions">
-            <gr-button
-                link
-                secondary
-                class="action cancel hideOnPublished"
-                on-tap="_handleCancel">Cancel</gr-button>
-            <gr-button
-                link
-                secondary
-                class="action discard hideOnPublished"
-                on-tap="_handleDiscard">Discard</gr-button>
-            <gr-button
-                link
-                secondary
-                class="action edit hideOnPublished"
-                on-tap="_handleEdit">Edit</gr-button>
-            <gr-button
-                link
-                secondary
-                disabled$="[[_computeSaveDisabled(_messageText, comment, resolved)]]"
-                class="action save hideOnPublished"
-                on-tap="_handleSave">Save</gr-button>
-          </div>
-        </div>
-        <div class="robotActions" hidden$="[[!_showRobotActions]]">
-          <template is="dom-if" if="[[isRobotComment]]">
-            <gr-button
-                link
-                secondary
-                class="action fix"
-                on-tap="_handleFix"
-                disabled="[[robotButtonDisabled]]">
-              Please Fix
-            </gr-button>
-            <gr-endpoint-decorator name="robot-comment-controls">
-              <gr-endpoint-param name="comment" value="[[comment]]">
-              </gr-endpoint-param>
-            </gr-endpoint-decorator>
-          </template>
-        </div>
-      </div>
-    </div>
-    <template is="dom-if" if="[[_enableOverlay]]">
-      <gr-overlay id="confirmDeleteOverlay" with-backdrop>
-        <gr-confirm-delete-comment-dialog id="confirmDeleteComment"
-            on-confirm="_handleConfirmDeleteComment"
-            on-cancel="_handleCancelDeleteComment">
-        </gr-confirm-delete-comment-dialog>
-      </gr-overlay>
-      <gr-overlay id="confirmDiscardOverlay" with-backdrop>
-        <gr-dialog
-            id="confirmDiscardDialog"
-            confirm-label="Discard"
-            confirm-on-enter
-            on-confirm="_handleConfirmDiscard"
-            on-cancel="_closeConfirmDiscardOverlay">
-          <div class="header" slot="header">
-            Discard comment
-          </div>
-          <div class="main" slot="main">
-            Are you sure you want to discard this draft comment?
-          </div>
-        </gr-dialog>
-      </gr-overlay>
-    </template>
-    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
-    <gr-storage id="storage"></gr-storage>
-    <gr-reporting id="reporting"></gr-reporting>
-  </template>
-  <script src="gr-diff-comment.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
deleted file mode 100644
index 6de0746..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
+++ /dev/null
@@ -1,659 +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 STORAGE_DEBOUNCE_INTERVAL = 400;
-  const TOAST_DEBOUNCE_INTERVAL = 200;
-
-  const SAVING_MESSAGE = 'Saving';
-  const DRAFT_SINGULAR = 'draft...';
-  const DRAFT_PLURAL = 'drafts...';
-  const SAVED_MESSAGE = 'All changes saved';
-
-  const REPORT_CREATE_DRAFT = 'CreateDraftComment';
-  const REPORT_UPDATE_DRAFT = 'UpdateDraftComment';
-  const REPORT_DISCARD_DRAFT = 'DiscardDraftComment';
-
-  Polymer({
-    is: 'gr-diff-comment',
-
-    /**
-     * Fired when the create fix comment action is triggered.
-     *
-     * @event create-fix-comment
-     */
-
-    /**
-     * Fired when this comment is discarded.
-     *
-     * @event comment-discard
-     */
-
-    /**
-     * Fired when this comment is saved.
-     *
-     * @event comment-save
-     */
-
-    /**
-     * Fired when this comment is updated.
-     *
-     * @event comment-update
-     */
-
-    /**
-     * @event comment-mouse-over
-     */
-
-    /**
-     * @event comment-mouse-out
-     */
-
-    properties: {
-      changeNum: String,
-      /** @type {?} */
-      comment: {
-        type: Object,
-        notify: true,
-        observer: '_commentChanged',
-      },
-      isRobotComment: {
-        type: Boolean,
-        value: false,
-        reflectToAttribute: true,
-      },
-      disabled: {
-        type: Boolean,
-        value: false,
-        reflectToAttribute: true,
-      },
-      draft: {
-        type: Boolean,
-        value: false,
-        observer: '_draftChanged',
-      },
-      editing: {
-        type: Boolean,
-        value: false,
-        observer: '_editingChanged',
-      },
-      discarding: {
-        type: Boolean,
-        value: false,
-        reflectToAttribute: true,
-      },
-      hasChildren: Boolean,
-      patchNum: String,
-      showActions: Boolean,
-      _showHumanActions: Boolean,
-      _showRobotActions: Boolean,
-      collapsed: {
-        type: Boolean,
-        value: true,
-        observer: '_toggleCollapseClass',
-      },
-      /** @type {?} */
-      projectConfig: Object,
-      robotButtonDisabled: Boolean,
-      _isAdmin: {
-        type: Boolean,
-        value: false,
-      },
-
-      _xhrPromise: Object, // Used for testing.
-      _messageText: {
-        type: String,
-        value: '',
-        observer: '_messageTextChanged',
-      },
-      commentSide: String,
-
-      resolved: Boolean,
-
-      _numPendingDraftRequests: {
-        type: Object,
-        value: {number: 0}, // Intentional to share the object across instances.
-      },
-
-      _enableOverlay: {
-        type: Boolean,
-        value: false,
-      },
-
-      /**
-       * Property for storing references to overlay elements. When the overlays
-       * are moved to Gerrit.getRootElement() to be shown they are no-longer
-       * children, so they can't be queried along the tree, so they are stored
-       * here.
-       */
-      _overlays: {
-        type: Object,
-        value: () => ({}),
-      },
-    },
-
-    observers: [
-      '_commentMessageChanged(comment.message)',
-      '_loadLocalDraft(changeNum, patchNum, comment)',
-      '_isRobotComment(comment)',
-      '_calculateActionstoShow(showActions, isRobotComment)',
-    ],
-
-    behaviors: [
-      Gerrit.KeyboardShortcutBehavior,
-    ],
-
-    keyBindings: {
-      'ctrl+enter meta+enter ctrl+s meta+s': '_handleSaveKey',
-      'esc': '_handleEsc',
-    },
-
-    attached() {
-      if (this.editing) {
-        this.collapsed = false;
-      } else if (this.comment) {
-        this.collapsed = this.comment.collapsed;
-      }
-      this._getIsAdmin().then(isAdmin => {
-        this._isAdmin = isAdmin;
-      });
-    },
-
-    detached() {
-      this.cancelDebouncer('fire-update');
-      if (this.textarea) {
-        this.textarea.closeDropdown();
-      }
-    },
-
-    get textarea() {
-      return this.$$('#editTextarea');
-    },
-
-    get confirmDeleteOverlay() {
-      if (!this._overlays.confirmDelete) {
-        this._enableOverlay = true;
-        Polymer.dom.flush();
-        this._overlays.confirmDelete = this.$$('#confirmDeleteOverlay');
-      }
-      return this._overlays.confirmDelete;
-    },
-
-    get confirmDiscardOverlay() {
-      if (!this._overlays.confirmDiscard) {
-        this._enableOverlay = true;
-        Polymer.dom.flush();
-        this._overlays.confirmDiscard = this.$$('#confirmDiscardOverlay');
-      }
-      return this._overlays.confirmDiscard;
-    },
-
-    _computeShowHideText(collapsed) {
-      return collapsed ? '◀' : '▼';
-    },
-
-    _calculateActionstoShow(showActions, isRobotComment) {
-      this._showHumanActions = showActions && !isRobotComment;
-      this._showRobotActions = showActions && isRobotComment;
-    },
-
-    _isRobotComment(comment) {
-      this.isRobotComment = !!comment.robot_id;
-    },
-
-    isOnParent() {
-      return this.side === 'PARENT';
-    },
-
-    _getIsAdmin() {
-      return this.$.restAPI.getIsAdmin();
-    },
-
-    /**
-     * @param {*=} opt_comment
-     */
-    save(opt_comment) {
-      let comment = opt_comment;
-      if (!comment) { comment = this.comment; }
-
-      this.set('comment.message', this._messageText);
-      this.editing = false;
-      this.disabled = true;
-
-      if (!this._messageText) {
-        return this._discardDraft();
-      }
-
-      this._xhrPromise = this._saveDraft(comment).then(response => {
-        this.disabled = false;
-        if (!response.ok) { return response; }
-
-        this._eraseDraftComment();
-        return this.$.restAPI.getResponseObject(response).then(obj => {
-          const resComment = obj;
-          resComment.__draft = true;
-          // Maintain the ephemeral draft ID for identification by other
-          // elements.
-          if (this.comment.__draftID) {
-            resComment.__draftID = this.comment.__draftID;
-          }
-          resComment.__commentSide = this.commentSide;
-          this.comment = resComment;
-          this._fireSave();
-          return obj;
-        });
-      }).catch(err => {
-        this.disabled = false;
-        throw err;
-      });
-
-      return this._xhrPromise;
-    },
-
-    _eraseDraftComment() {
-      // Prevents a race condition in which removing the draft comment occurs
-      // prior to it being saved.
-      this.cancelDebouncer('store');
-
-      this.$.storage.eraseDraftComment({
-        changeNum: this.changeNum,
-        patchNum: this._getPatchNum(),
-        path: this.comment.path,
-        line: this.comment.line,
-        range: this.comment.range,
-      });
-    },
-
-    _commentChanged(comment) {
-      this.editing = !!comment.__editing;
-      this.resolved = !comment.unresolved;
-      if (this.editing) { // It's a new draft/reply, notify.
-        this._fireUpdate();
-      }
-    },
-
-    /**
-     * @param {!Object=} opt_mixin
-     *
-     * @return {!Object}
-     */
-    _getEventPayload(opt_mixin) {
-      return Object.assign({}, opt_mixin, {
-        comment: this.comment,
-        patchNum: this.patchNum,
-      });
-    },
-
-    _fireSave() {
-      this.fire('comment-save', this._getEventPayload());
-    },
-
-    _fireUpdate() {
-      this.debounce('fire-update', () => {
-        this.fire('comment-update', this._getEventPayload());
-      });
-    },
-
-    _draftChanged(draft) {
-      this.$.container.classList.toggle('draft', draft);
-    },
-
-    _editingChanged(editing, previousValue) {
-      this.$.container.classList.toggle('editing', editing);
-      if (this.comment && this.comment.id) {
-        this.$$('.cancel').hidden = !editing;
-      }
-      if (this.comment) {
-        this.comment.__editing = this.editing;
-      }
-      if (editing != !!previousValue) {
-        // To prevent event firing on comment creation.
-        this._fireUpdate();
-      }
-      if (editing) {
-        this.async(() => {
-          Polymer.dom.flush();
-          this.textarea.putCursorAtEnd();
-        }, 1);
-      }
-    },
-
-    _computeLinkToComment(comment) {
-      return '#' + comment.line;
-    },
-
-    _computeDeleteButtonClass(isAdmin, draft) {
-      return isAdmin && !draft ? 'showDeleteButtons' : '';
-    },
-
-    _computeSaveDisabled(draft, comment, resolved) {
-      // If resolved state has changed and a msg exists, save should be enabled.
-      if (comment.unresolved === resolved && draft) { return false; }
-      return !draft || draft.trim() === '';
-    },
-
-    _handleSaveKey(e) {
-      if (!this._computeSaveDisabled(this._messageText, this.comment,
-          this.resolved)) {
-        e.preventDefault();
-        this._handleSave(e);
-      }
-    },
-
-    _handleEsc(e) {
-      if (!this._messageText.length) {
-        e.preventDefault();
-        this._handleCancel(e);
-      }
-    },
-
-    _handleToggleCollapsed() {
-      this.collapsed = !this.collapsed;
-    },
-
-    _toggleCollapseClass(collapsed) {
-      if (collapsed) {
-        this.$.container.classList.add('collapsed');
-      } else {
-        this.$.container.classList.remove('collapsed');
-      }
-    },
-
-    _commentMessageChanged(message) {
-      this._messageText = message || '';
-    },
-
-    _messageTextChanged(newValue, oldValue) {
-      if (!this.comment || (this.comment && this.comment.id)) { return; }
-
-      this.debounce('store', () => {
-        const message = this._messageText;
-        const commentLocation = {
-          changeNum: this.changeNum,
-          patchNum: this._getPatchNum(),
-          path: this.comment.path,
-          line: this.comment.line,
-          range: this.comment.range,
-        };
-
-        if ((!this._messageText || !this._messageText.length) && oldValue) {
-          // If the draft has been modified to be empty, then erase the storage
-          // entry.
-          this.$.storage.eraseDraftComment(commentLocation);
-        } else {
-          this.$.storage.setDraftComment(commentLocation, message);
-        }
-      }, STORAGE_DEBOUNCE_INTERVAL);
-    },
-
-    _handleLinkTap(e) {
-      e.preventDefault();
-      const hash = this._computeLinkToComment(this.comment);
-      // Don't add the hash to the window history if it's already there.
-      // Otherwise you mess up expected back button behavior.
-      if (window.location.hash == hash) { return; }
-      // Change the URL but don’t trigger a nav event. Otherwise it will
-      // reload the page.
-      page.show(window.location.pathname + hash, null, false);
-    },
-
-    _handleEdit(e) {
-      e.preventDefault();
-      this._messageText = this.comment.message;
-      this.editing = true;
-      this.$.reporting.recordDraftInteraction();
-    },
-
-    _handleSave(e) {
-      e.preventDefault();
-
-      // Ignore saves started while already saving.
-      if (this.disabled) { return; }
-      const timingLabel = this.comment.id ?
-        REPORT_UPDATE_DRAFT : REPORT_CREATE_DRAFT;
-      const timer = this.$.reporting.getTimer(timingLabel);
-      this.set('comment.__editing', false);
-      return this.save().then(() => { timer.end(); });
-    },
-
-    _handleCancel(e) {
-      e.preventDefault();
-
-      if (!this.comment.message ||
-          this.comment.message.trim().length === 0 ||
-          !this.comment.id) {
-        this._fireDiscard();
-        return;
-      }
-      this._messageText = this.comment.message;
-      this.editing = false;
-    },
-
-    _fireDiscard() {
-      this.cancelDebouncer('fire-update');
-      this.fire('comment-discard', this._getEventPayload());
-    },
-
-    _handleFix() {
-      this.dispatchEvent(new CustomEvent('create-fix-comment', {
-        bubbles: true,
-        detail: this._getEventPayload(),
-      }));
-    },
-
-    _handleDiscard(e) {
-      e.preventDefault();
-      this.$.reporting.recordDraftInteraction();
-
-      if (!this._messageText) {
-        this._discardDraft();
-        return;
-      }
-
-      this._openOverlay(this.confirmDiscardOverlay).then(() => {
-        this.confirmDiscardOverlay.querySelector('#confirmDiscardDialog')
-            .resetFocus();
-      });
-    },
-
-    _handleConfirmDiscard(e) {
-      e.preventDefault();
-      const timer = this.$.reporting.getTimer(REPORT_DISCARD_DRAFT);
-      this._closeConfirmDiscardOverlay();
-      return this._discardDraft().then(() => { timer.end(); });
-    },
-
-    _discardDraft() {
-      if (!this.comment.__draft) {
-        throw Error('Cannot discard a non-draft comment.');
-      }
-      this.discarding = true;
-      this.editing = false;
-      this.disabled = true;
-      this._eraseDraftComment();
-
-      if (!this.comment.id) {
-        this.disabled = false;
-        this._fireDiscard();
-        return;
-      }
-
-      this._xhrPromise = this._deleteDraft(this.comment).then(response => {
-        this.disabled = false;
-        if (!response.ok) {
-          this.discarding = false;
-          return response;
-        }
-
-        this._fireDiscard();
-      }).catch(err => {
-        this.disabled = false;
-        throw err;
-      });
-
-      return this._xhrPromise;
-    },
-
-    _closeConfirmDiscardOverlay() {
-      this._closeOverlay(this.confirmDiscardOverlay);
-    },
-
-    _getSavingMessage(numPending) {
-      if (numPending === 0) { return SAVED_MESSAGE; }
-      return [
-        SAVING_MESSAGE,
-        numPending,
-        numPending === 1 ? DRAFT_SINGULAR : DRAFT_PLURAL,
-      ].join(' ');
-    },
-
-    _showStartRequest() {
-      const numPending = ++this._numPendingDraftRequests.number;
-      this._updateRequestToast(numPending);
-    },
-
-    _showEndRequest() {
-      const numPending = --this._numPendingDraftRequests.number;
-      this._updateRequestToast(numPending);
-    },
-
-    _handleFailedDraftRequest() {
-      this._numPendingDraftRequests.number--;
-
-      // Cancel the debouncer so that error toasts from the error-manager will
-      // not be overridden.
-      this.cancelDebouncer('draft-toast');
-    },
-
-    _updateRequestToast(numPending) {
-      const message = this._getSavingMessage(numPending);
-      this.debounce('draft-toast', () => {
-        // Note: the event is fired on the body rather than this element because
-        // this element may not be attached by the time this executes, in which
-        // case the event would not bubble.
-        document.body.dispatchEvent(new CustomEvent('show-alert',
-            {detail: {message}, bubbles: true}));
-      }, TOAST_DEBOUNCE_INTERVAL);
-    },
-
-    _saveDraft(draft) {
-      this._showStartRequest();
-      return this.$.restAPI.saveDiffDraft(this.changeNum, this.patchNum, draft)
-          .then(result => {
-            if (result.ok) {
-              this._showEndRequest();
-            } else {
-              this._handleFailedDraftRequest();
-            }
-            return result;
-          });
-    },
-
-    _deleteDraft(draft) {
-      this._showStartRequest();
-      return this.$.restAPI.deleteDiffDraft(this.changeNum, this.patchNum,
-          draft).then(result => {
-        if (result.ok) {
-          this._showEndRequest();
-        } else {
-          this._handleFailedDraftRequest();
-        }
-        return result;
-      });
-    },
-
-    _getPatchNum() {
-      return this.isOnParent() ? 'PARENT' : this.patchNum;
-    },
-
-    _loadLocalDraft(changeNum, patchNum, comment) {
-      // Only apply local drafts to comments that haven't been saved
-      // remotely, and haven't been given a default message already.
-      //
-      // Don't get local draft if there is another comment that is currently
-      // in an editing state.
-      if (!comment || comment.id || comment.message || comment.__otherEditing) {
-        delete comment.__otherEditing;
-        return;
-      }
-
-      const draft = this.$.storage.getDraftComment({
-        changeNum,
-        patchNum: this._getPatchNum(),
-        path: comment.path,
-        line: comment.line,
-        range: comment.range,
-      });
-
-      if (draft) {
-        this.set('comment.message', draft.message);
-      }
-    },
-
-    _handleMouseEnter(e) {
-      this.fire('comment-mouse-over', this._getEventPayload());
-    },
-
-    _handleMouseLeave(e) {
-      this.fire('comment-mouse-out', this._getEventPayload());
-    },
-
-    _handleToggleResolved() {
-      this.$.reporting.recordDraftInteraction();
-      this.resolved = !this.resolved;
-      // Modify payload instead of this.comment, as this.comment is passed from
-      // the parent by ref.
-      const payload = this._getEventPayload();
-      payload.comment.unresolved = !this.$.resolvedCheckbox.checked;
-      this.fire('comment-update', payload);
-      if (!this.editing) {
-        // Save the resolved state immediately.
-        this.save(payload.comment);
-      }
-    },
-
-    _handleCommentDelete() {
-      this._openOverlay(this.confirmDeleteOverlay);
-    },
-
-    _handleCancelDeleteComment() {
-      this._closeOverlay(this.confirmDeleteOverlay);
-    },
-
-    _openOverlay(overlay) {
-      Polymer.dom(Gerrit.getRootElement()).appendChild(overlay);
-      return overlay.open();
-    },
-
-    _closeOverlay(overlay) {
-      Polymer.dom(Gerrit.getRootElement()).removeChild(overlay);
-      overlay.close();
-    },
-
-    _handleConfirmDeleteComment() {
-      const dialog =
-          this.confirmDeleteOverlay.querySelector('#confirmDeleteComment');
-      this.$.restAPI.deleteComment(
-          this.changeNum, this.patchNum, this.comment.id, dialog.message)
-          .then(newComment => {
-            this._handleCancelDeleteComment();
-            this.comment = newComment;
-          });
-    },
-  });
-})();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
deleted file mode 100644
index ca85892..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
+++ /dev/null
@@ -1,856 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-diff-comment</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="../../../bower_components/page/page.js"></script>
-<script src="../../../scripts/util.js"></script>
-
-<link rel="import" href="gr-diff-comment.html">
-
-<script>void(0);</script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-diff-comment></gr-diff-comment>
-  </template>
-</test-fixture>
-
-<test-fixture id="draft">
-  <template>
-    <gr-diff-comment draft="true"></gr-diff-comment>
-  </template>
-</test-fixture>
-
-<script>
-
-  function isVisible(el) {
-    assert.ok(el);
-    return getComputedStyle(el).getPropertyValue('display') !== 'none';
-  }
-
-  suite('gr-diff-comment tests', () => {
-    let element;
-    let sandbox;
-    setup(() => {
-      stub('gr-rest-api-interface', {
-        getAccount() { return Promise.resolve(null); },
-      });
-      element = fixture('basic');
-      element.comment = {
-        author: {
-          name: 'Mr. Peanutbutter',
-          email: 'tenn1sballchaser@aol.com',
-        },
-        id: 'baf0414d_60047215',
-        line: 5,
-        message: 'is this a crossover episode!?',
-        updated: '2015-12-08 19:48:33.843000000',
-      };
-      sandbox = sinon.sandbox.create();
-    });
-
-    teardown(() => {
-      sandbox.restore();
-    });
-
-    test('collapsible comments', () => {
-      // When a comment (not draft) is loaded, it should be collapsed
-      assert.isTrue(element.collapsed);
-      assert.isFalse(isVisible(element.$$('gr-formatted-text')),
-          'gr-formatted-text is not visible');
-      assert.isFalse(isVisible(element.$$('.actions')),
-          'actions are not visible');
-      assert.isNotOk(element.textarea, 'textarea is not visible');
-
-      // The header middle content is only visible when comments are collapsed.
-      // It shows the message in a condensed way, and limits to a single line.
-      assert.isTrue(isVisible(element.$$('.collapsedContent')),
-          'header middle content is visible');
-
-      // When the header row is clicked, the comment should expand
-      MockInteractions.tap(element.$.header);
-      assert.isFalse(element.collapsed);
-      assert.isTrue(isVisible(element.$$('gr-formatted-text')),
-          'gr-formatted-text is visible');
-      assert.isTrue(isVisible(element.$$('.actions')),
-          'actions are visible');
-      assert.isNotOk(element.textarea, 'textarea is not visible');
-      assert.isFalse(isVisible(element.$$('.collapsedContent')),
-          'header middle content is not visible');
-    });
-
-    test('clicking on date link does not trigger nav', () => {
-      const showStub = sinon.stub(page, 'show');
-      const dateEl = element.$$('.date');
-      assert.ok(dateEl);
-      MockInteractions.tap(dateEl);
-      const dest = window.location.pathname + '#5';
-      assert(showStub.lastCall.calledWithExactly(dest, null, false),
-          'Should navigate to ' + dest + ' without triggering nav');
-      showStub.restore();
-    });
-
-    test('message is not retrieved from storage when other edits', done => {
-      const storageStub = sandbox.stub(element.$.storage, 'getDraftComment');
-      const loadSpy = sandbox.spy(element, '_loadLocalDraft');
-
-      element.changeNum = 1;
-      element.patchNum = 1;
-      element.comment = {
-        author: {
-          name: 'Mr. Peanutbutter',
-          email: 'tenn1sballchaser@aol.com',
-        },
-        line: 5,
-        __otherEditing: true,
-      };
-      flush(() => {
-        assert.isTrue(loadSpy.called);
-        assert.isFalse(storageStub.called);
-        done();
-      });
-    });
-
-    test('message is retrieved from storage when no other edits', done => {
-      const storageStub = sandbox.stub(element.$.storage, 'getDraftComment');
-      const loadSpy = sandbox.spy(element, '_loadLocalDraft');
-
-      element.changeNum = 1;
-      element.patchNum = 1;
-      element.comment = {
-        author: {
-          name: 'Mr. Peanutbutter',
-          email: 'tenn1sballchaser@aol.com',
-        },
-        line: 5,
-      };
-      flush(() => {
-        assert.isTrue(loadSpy.called);
-        assert.isTrue(storageStub.called);
-        done();
-      });
-    });
-
-    test('_getPatchNum', () => {
-      element.side = 'PARENT';
-      element.patchNum = 1;
-      assert.equal(element._getPatchNum(), 'PARENT');
-      element.side = 'REVISION';
-      assert.equal(element._getPatchNum(), 1);
-    });
-
-    test('comment expand and collapse', () => {
-      element.collapsed = true;
-      assert.isFalse(isVisible(element.$$('gr-formatted-text')),
-          'gr-formatted-text is not visible');
-      assert.isFalse(isVisible(element.$$('.actions')),
-          'actions are not visible');
-      assert.isNotOk(element.textarea, 'textarea is not visible');
-      assert.isTrue(isVisible(element.$$('.collapsedContent')),
-          'header middle content is visible');
-
-      element.collapsed = false;
-      assert.isFalse(element.collapsed);
-      assert.isTrue(isVisible(element.$$('gr-formatted-text')),
-          'gr-formatted-text is visible');
-      assert.isTrue(isVisible(element.$$('.actions')),
-          'actions are visible');
-      assert.isNotOk(element.textarea, 'textarea is not visible');
-      assert.isFalse(isVisible(element.$$('.collapsedContent')),
-          'header middle content is is not visible');
-    });
-
-    suite('while editing', () => {
-      setup(() => {
-        element.editing = true;
-        element._messageText = 'test';
-        sandbox.stub(element, '_handleCancel');
-        sandbox.stub(element, '_handleSave');
-        flushAsynchronousOperations();
-      });
-
-      suite('when text is empty', () => {
-        setup(() => {
-          element._messageText = '';
-          element.comment = {};
-        });
-
-        test('esc closes comment when text is empty', () => {
-          MockInteractions.pressAndReleaseKeyOn(
-              element.textarea, 27); // esc
-          assert.isTrue(element._handleCancel.called);
-        });
-
-        test('ctrl+enter does not save', () => {
-          MockInteractions.pressAndReleaseKeyOn(
-              element.textarea, 13, 'ctrl'); // ctrl + enter
-          assert.isFalse(element._handleSave.called);
-        });
-
-        test('meta+enter does not save', () => {
-          MockInteractions.pressAndReleaseKeyOn(
-              element.textarea, 13, 'meta'); // meta + enter
-          assert.isFalse(element._handleSave.called);
-        });
-
-        test('ctrl+s does not save', () => {
-          MockInteractions.pressAndReleaseKeyOn(
-              element.textarea, 83, 'ctrl'); // ctrl + s
-          assert.isFalse(element._handleSave.called);
-        });
-      });
-
-      test('esc does not close comment that has content', () => {
-        MockInteractions.pressAndReleaseKeyOn(
-            element.textarea, 27); // esc
-        assert.isFalse(element._handleCancel.called);
-      });
-
-      test('ctrl+enter saves', () => {
-        MockInteractions.pressAndReleaseKeyOn(
-            element.textarea, 13, 'ctrl'); // ctrl + enter
-        assert.isTrue(element._handleSave.called);
-      });
-
-      test('meta+enter saves', () => {
-        MockInteractions.pressAndReleaseKeyOn(
-            element.textarea, 13, 'meta'); // meta + enter
-        assert.isTrue(element._handleSave.called);
-      });
-
-      test('ctrl+s saves', () => {
-        MockInteractions.pressAndReleaseKeyOn(
-            element.textarea, 83, 'ctrl'); // ctrl + s
-        assert.isTrue(element._handleSave.called);
-      });
-    });
-    test('delete comment button for non-admins is hidden', () => {
-      element._isAdmin = false;
-      assert.isFalse(element.$$('.action.delete')
-          .classList.contains('showDeleteButtons'));
-    });
-
-    test('delete comment button for admins with draft is hidden', () => {
-      element._isAdmin = false;
-      element.draft = true;
-      assert.isFalse(element.$$('.action.delete')
-          .classList.contains('showDeleteButtons'));
-    });
-
-    test('delete comment', done => {
-      sandbox.stub(
-          element.$.restAPI, 'deleteComment').returns(Promise.resolve({}));
-      sandbox.spy(element.confirmDeleteOverlay, 'open');
-      element.changeNum = 42;
-      element.patchNum = 0xDEADBEEF;
-      element._isAdmin = true;
-      assert.isTrue(element.$$('.action.delete')
-          .classList.contains('showDeleteButtons'));
-      MockInteractions.tap(element.$$('.action.delete'));
-      flush(() => {
-        element.confirmDeleteOverlay.open.lastCall.returnValue.then(() => {
-          const dialog =
-              this.confirmDeleteOverlay.querySelector('#confirmDeleteComment');
-          dialog.message = 'removal reason';
-          element._handleConfirmDeleteComment();
-          assert.isTrue(element.$.restAPI.deleteComment.calledWith(
-              42, 0xDEADBEEF, 'baf0414d_60047215', 'removal reason'));
-          done();
-        });
-      });
-    });
-
-    suite('draft update reporting', () => {
-      let endStub;
-      let getTimerStub;
-      let mockEvent;
-
-      setup(() => {
-        mockEvent = {preventDefault() {}};
-        sandbox.stub(element, 'save')
-            .returns(Promise.resolve({}));
-        sandbox.stub(element, '_discardDraft')
-            .returns(Promise.resolve({}));
-        endStub = sinon.stub();
-        getTimerStub = sandbox.stub(element.$.reporting, 'getTimer')
-            .returns({end: endStub});
-      });
-
-      test('create', () => {
-        element.comment = {};
-        return element._handleSave(mockEvent).then(() => {
-          assert.isTrue(endStub.calledOnce);
-          assert.isTrue(getTimerStub.calledOnce);
-          assert.equal(getTimerStub.lastCall.args[0], 'CreateDraftComment');
-        });
-      });
-
-      test('update', () => {
-        element.comment = {id: 'abc_123'};
-        return element._handleSave(mockEvent).then(() => {
-          assert.isTrue(endStub.calledOnce);
-          assert.isTrue(getTimerStub.calledOnce);
-          assert.equal(getTimerStub.lastCall.args[0], 'UpdateDraftComment');
-        });
-      });
-
-      test('discard', () => {
-        element.comment = {id: 'abc_123'};
-        sandbox.stub(element, '_closeConfirmDiscardOverlay');
-        return element._handleConfirmDiscard(mockEvent).then(() => {
-          assert.isTrue(endStub.calledOnce);
-          assert.isTrue(getTimerStub.calledOnce);
-          assert.equal(getTimerStub.lastCall.args[0], 'DiscardDraftComment');
-        });
-      });
-    });
-
-    test('edit reports interaction', () => {
-      const reportStub = sandbox.stub(element.$.reporting,
-          'recordDraftInteraction');
-      MockInteractions.tap(element.$$('.edit'));
-      assert.isTrue(reportStub.calledOnce);
-    });
-
-    test('discard reports interaction', () => {
-      const reportStub = sandbox.stub(element.$.reporting,
-          'recordDraftInteraction');
-      element.draft = true;
-      MockInteractions.tap(element.$$('.discard'));
-      assert.isTrue(reportStub.calledOnce);
-    });
-  });
-
-  suite('gr-diff-comment draft tests', () => {
-    let element;
-    let sandbox;
-
-    setup(() => {
-      stub('gr-rest-api-interface', {
-        getAccount() { return Promise.resolve(null); },
-        saveDiffDraft() {
-          return Promise.resolve({
-            ok: true,
-            text() {
-              return Promise.resolve(
-                  ')]}\'\n{' +
-                  '"id": "baf0414d_40572e03",' +
-                  '"path": "/path/to/file",' +
-                  '"line": 5,' +
-                  '"updated": "2015-12-08 21:52:36.177000000",' +
-                  '"message": "saved!"' +
-                '}'
-              );
-            },
-          });
-        },
-        removeChangeReviewer() {
-          return Promise.resolve({ok: true});
-        },
-      });
-      stub('gr-storage', {
-        getDraftComment() { return null; },
-      });
-      element = fixture('draft');
-      element.changeNum = 42;
-      element.patchNum = 1;
-      element.editing = false;
-      element.comment = {
-        __commentSide: 'right',
-        __draft: true,
-        __draftID: 'temp_draft_id',
-        path: '/path/to/file',
-        line: 5,
-      };
-      element.commentSide = 'right';
-      sandbox = sinon.sandbox.create();
-    });
-
-    teardown(() => {
-      sandbox.restore();
-    });
-
-    test('button visibility states', () => {
-      element.showActions = false;
-      assert.isTrue(element.$$('.humanActions').hasAttribute('hidden'));
-      assert.isTrue(element.$$('.robotActions').hasAttribute('hidden'));
-
-      element.showActions = true;
-      assert.isFalse(element.$$('.humanActions').hasAttribute('hidden'));
-      assert.isTrue(element.$$('.robotActions').hasAttribute('hidden'));
-
-      element.draft = true;
-      assert.isTrue(isVisible(element.$$('.edit')), 'edit is visible');
-      assert.isTrue(isVisible(element.$$('.discard')), 'discard is visible');
-      assert.isFalse(isVisible(element.$$('.save')), 'save is not visible');
-      assert.isFalse(isVisible(element.$$('.cancel')), 'cancel is not visible');
-      assert.isTrue(isVisible(element.$$('.resolve')), 'resolve is visible');
-      assert.isFalse(element.$$('.humanActions').hasAttribute('hidden'));
-      assert.isTrue(element.$$('.robotActions').hasAttribute('hidden'));
-
-      element.editing = true;
-      flushAsynchronousOperations();
-      assert.isFalse(isVisible(element.$$('.edit')), 'edit is not visible');
-      assert.isFalse(isVisible(element.$$('.discard')), 'discard not visible');
-      assert.isTrue(isVisible(element.$$('.save')), 'save is visible');
-      assert.isTrue(isVisible(element.$$('.cancel')), 'cancel is visible');
-      assert.isTrue(isVisible(element.$$('.resolve')), 'resolve is visible');
-      assert.isFalse(element.$$('.humanActions').hasAttribute('hidden'));
-      assert.isTrue(element.$$('.robotActions').hasAttribute('hidden'));
-
-      element.draft = false;
-      element.editing = false;
-      flushAsynchronousOperations();
-      assert.isFalse(isVisible(element.$$('.edit')), 'edit is not visible');
-      assert.isFalse(isVisible(element.$$('.discard')),
-          'discard is not visible');
-      assert.isFalse(isVisible(element.$$('.save')), 'save is not visible');
-      assert.isFalse(isVisible(element.$$('.cancel')), 'cancel is not visible');
-      assert.isFalse(element.$$('.humanActions').hasAttribute('hidden'));
-      assert.isTrue(element.$$('.robotActions').hasAttribute('hidden'));
-
-      element.comment.id = 'foo';
-      element.draft = true;
-      element.editing = true;
-      flushAsynchronousOperations();
-      assert.isTrue(isVisible(element.$$('.cancel')), 'cancel is visible');
-      assert.isFalse(element.$$('.humanActions').hasAttribute('hidden'));
-      assert.isTrue(element.$$('.robotActions').hasAttribute('hidden'));
-
-      element.isRobotComment = true;
-      element.draft = true;
-      assert.isTrue(element.$$('.humanActions').hasAttribute('hidden'));
-      assert.isFalse(element.$$('.robotActions').hasAttribute('hidden'));
-
-      // It is not expected to see Robot comment drafts, but if they appear,
-      // they will behave the same as non-drafts.
-      element.draft = false;
-      assert.isTrue(element.$$('.humanActions').hasAttribute('hidden'));
-      assert.isFalse(element.$$('.robotActions').hasAttribute('hidden'));
-
-      // A robot comment with run ID should display plain text.
-      element.set(['comment', 'robot_run_id'], 'text');
-      element.editing = false;
-      element.collapsed = false;
-      flushAsynchronousOperations();
-      assert.isNotOk(element.$$('.robotRun.link'));
-      assert.notEqual(getComputedStyle(element.$$('.robotRun.text')).display,
-          'none');
-
-      // A robot comment with run ID and url should display a link.
-      element.set(['comment', 'url'], '/path/to/run');
-      flushAsynchronousOperations();
-      assert.notEqual(getComputedStyle(element.$$('.robotRun.link')).display,
-          'none');
-      assert.equal(getComputedStyle(element.$$('.robotRun.text')).display,
-          'none');
-    });
-
-    test('collapsible drafts', () => {
-      assert.isTrue(element.collapsed);
-      assert.isFalse(isVisible(element.$$('gr-formatted-text')),
-          'gr-formatted-text is not visible');
-      assert.isFalse(isVisible(element.$$('.actions')),
-          'actions are not visible');
-      assert.isNotOk(element.textarea, 'textarea is not visible');
-      assert.isTrue(isVisible(element.$$('.collapsedContent')),
-          'header middle content is visible');
-
-      MockInteractions.tap(element.$.header);
-      assert.isFalse(element.collapsed);
-      assert.isTrue(isVisible(element.$$('gr-formatted-text')),
-          'gr-formatted-text is visible');
-      assert.isTrue(isVisible(element.$$('.actions')),
-          'actions are visible');
-      assert.isNotOk(element.textarea, 'textarea is not visible');
-      assert.isFalse(isVisible(element.$$('.collapsedContent')),
-          'header middle content is is not visible');
-
-      // When the edit button is pressed, should still see the actions
-      // and also textarea
-      MockInteractions.tap(element.$$('.edit'));
-      flushAsynchronousOperations();
-      assert.isFalse(element.collapsed);
-      assert.isFalse(isVisible(element.$$('gr-formatted-text')),
-          'gr-formatted-text is not visible');
-      assert.isTrue(isVisible(element.$$('.actions')),
-          'actions are visible');
-      assert.isTrue(isVisible(element.textarea), 'textarea is visible');
-      assert.isFalse(isVisible(element.$$('.collapsedContent')),
-          'header middle content is not visible');
-
-      // When toggle again, everything should be hidden except for textarea
-      // and header middle content should be visible
-      MockInteractions.tap(element.$.header);
-      assert.isTrue(element.collapsed);
-      assert.isFalse(isVisible(element.$$('gr-formatted-text')),
-          'gr-formatted-text is not visible');
-      assert.isFalse(isVisible(element.$$('.actions')),
-          'actions are not visible');
-      assert.isFalse(isVisible(element.$$('gr-textarea')),
-          'textarea is not visible');
-      assert.isTrue(isVisible(element.$$('.collapsedContent')),
-          'header middle content is visible');
-
-      // When toggle again, textarea should remain open in the state it was
-      // before
-      MockInteractions.tap(element.$.header);
-      assert.isFalse(isVisible(element.$$('gr-formatted-text')),
-          'gr-formatted-text is not visible');
-      assert.isTrue(isVisible(element.$$('.actions')),
-          'actions are visible');
-      assert.isTrue(isVisible(element.textarea), 'textarea is visible');
-      assert.isFalse(isVisible(element.$$('.collapsedContent')),
-          'header middle content is not visible');
-    });
-
-    test('draft creation/cancellation', done => {
-      assert.isFalse(element.editing);
-      MockInteractions.tap(element.$$('.edit'));
-      assert.isTrue(element.editing);
-
-      element._messageText = '';
-      const eraseMessageDraftSpy = sandbox.spy(element, '_eraseDraftComment');
-
-      // Save should be disabled on an empty message.
-      let disabled = element.$$('.save').hasAttribute('disabled');
-      assert.isTrue(disabled, 'save button should be disabled.');
-      element._messageText = '     ';
-      disabled = element.$$('.save').hasAttribute('disabled');
-      assert.isTrue(disabled, 'save button should be disabled.');
-
-      const updateStub = sinon.stub();
-      element.addEventListener('comment-update', updateStub);
-
-      let numDiscardEvents = 0;
-      element.addEventListener('comment-discard', e => {
-        numDiscardEvents++;
-        assert.isFalse(eraseMessageDraftSpy.called);
-        if (numDiscardEvents === 2) {
-          assert.isFalse(updateStub.called);
-          done();
-        }
-      });
-      MockInteractions.tap(element.$$('.cancel'));
-      element.flushDebouncer('fire-update');
-      element._messageText = '';
-      flushAsynchronousOperations();
-      MockInteractions.pressAndReleaseKeyOn(element.textarea, 27); // esc
-    });
-
-    test('draft discard removes message from storage', done => {
-      element._messageText = '';
-      const eraseMessageDraftSpy = sandbox.spy(element, '_eraseDraftComment');
-      sandbox.stub(element, '_closeConfirmDiscardOverlay');
-
-      element.addEventListener('comment-discard', e => {
-        assert.isTrue(eraseMessageDraftSpy.called);
-        done();
-      });
-      element._handleConfirmDiscard({preventDefault: sinon.stub()});
-    });
-
-    test('storage is cleared only after save success', () => {
-      element._messageText = 'test';
-      const eraseStub = sandbox.stub(element, '_eraseDraftComment');
-      sandbox.stub(element.$.restAPI, 'getResponseObject')
-          .returns(Promise.resolve({}));
-
-      sandbox.stub(element, '_saveDraft').returns(Promise.resolve({ok: false}));
-
-      const savePromise = element.save();
-      assert.isFalse(eraseStub.called);
-      return savePromise.then(() => {
-        assert.isFalse(eraseStub.called);
-
-        element._saveDraft.restore();
-        sandbox.stub(element, '_saveDraft')
-            .returns(Promise.resolve({ok: true}));
-        return element.save().then(() => {
-          assert.isTrue(eraseStub.called);
-        });
-      });
-    });
-
-    test('_computeSaveDisabled', () => {
-      const comment = {unresolved: true};
-      const msgComment = {message: 'test', unresolved: true};
-      assert.equal(element._computeSaveDisabled('', comment, false), true);
-      assert.equal(element._computeSaveDisabled('test', comment, false), false);
-      assert.equal(element._computeSaveDisabled('', msgComment, false), true);
-      assert.equal(
-          element._computeSaveDisabled('test', msgComment, false), false);
-      assert.equal(
-          element._computeSaveDisabled('test2', msgComment, false), false);
-      assert.equal(element._computeSaveDisabled('test', comment, true), false);
-      assert.equal(element._computeSaveDisabled('', comment, true), true);
-      assert.equal(element._computeSaveDisabled('', comment, false), true);
-    });
-
-    suite('confirm discard', () => {
-      let discardStub;
-      let overlayStub;
-      let mockEvent;
-
-      setup(() => {
-        discardStub = sandbox.stub(element, '_discardDraft');
-        overlayStub = sandbox.stub(element, '_openOverlay')
-            .returns(Promise.resolve());
-        mockEvent = {preventDefault: sinon.stub()};
-      });
-
-      test('confirms discard of comments with message text', () => {
-        element._messageText = 'test';
-        element._handleDiscard(mockEvent);
-        assert.isTrue(overlayStub.calledWith(element.confirmDiscardOverlay));
-        assert.isFalse(discardStub.called);
-      });
-
-      test('no confirmation for comments without message text', () => {
-        element._messageText = '';
-        element._handleDiscard(mockEvent);
-        assert.isFalse(overlayStub.called);
-        assert.isTrue(discardStub.calledOnce);
-      });
-    });
-
-    test('ctrl+s saves comment', done => {
-      const stub = sinon.stub(element, 'save', () => {
-        assert.isTrue(stub.called);
-        stub.restore();
-        done();
-        return Promise.resolve();
-      });
-      element._messageText = 'is that the horse from horsing around??';
-      element.editing = true;
-      flushAsynchronousOperations();
-      MockInteractions.pressAndReleaseKeyOn(
-          element.textarea.$.textarea.textarea,
-          83, 'ctrl'); // 'ctrl + s'
-    });
-
-    test('draft saving/editing', done => {
-      const fireStub = sinon.stub(element, 'fire');
-      const cancelDebounce = sandbox.stub(element, 'cancelDebouncer');
-
-      element.draft = true;
-      MockInteractions.tap(element.$$('.edit'));
-      element._messageText = 'good news, everyone!';
-      element.flushDebouncer('fire-update');
-      element.flushDebouncer('store');
-      assert(fireStub.calledWith('comment-update'),
-          'comment-update should be sent');
-      assert.isTrue(fireStub.calledOnce);
-
-      element._messageText = 'good news, everyone!';
-      element.flushDebouncer('fire-update');
-      element.flushDebouncer('store');
-      assert.isTrue(fireStub.calledOnce,
-          'No events should fire for text editing');
-
-      MockInteractions.tap(element.$$('.save'));
-
-      assert.isTrue(element.disabled,
-          'Element should be disabled when creating draft.');
-
-      element._xhrPromise.then(draft => {
-        assert(fireStub.calledWith('comment-save'),
-            'comment-save should be sent');
-        assert(cancelDebounce.calledWith('store'));
-
-        assert.deepEqual(fireStub.lastCall.args[1], {
-          comment: {
-            __commentSide: 'right',
-            __draft: true,
-            __draftID: 'temp_draft_id',
-            id: 'baf0414d_40572e03',
-            line: 5,
-            message: 'saved!',
-            path: '/path/to/file',
-            updated: '2015-12-08 21:52:36.177000000',
-          },
-          patchNum: 1,
-        });
-        assert.isFalse(element.disabled,
-            'Element should be enabled when done creating draft.');
-        assert.equal(draft.message, 'saved!');
-        assert.isFalse(element.editing);
-      }).then(() => {
-        MockInteractions.tap(element.$$('.edit'));
-        element._messageText = 'You’ll be delivering a package to Chapek 9, ' +
-            'a world where humans are killed on sight.';
-        MockInteractions.tap(element.$$('.save'));
-        assert.isTrue(element.disabled,
-            'Element should be disabled when updating draft.');
-
-        element._xhrPromise.then(draft => {
-          assert.isFalse(element.disabled,
-              'Element should be enabled when done updating draft.');
-          assert.equal(draft.message, 'saved!');
-          assert.isFalse(element.editing);
-          fireStub.restore();
-          done();
-        });
-      });
-    });
-
-    test('draft prevent save when disabled', () => {
-      const saveStub = sandbox.stub(element, 'save').returns(Promise.resolve());
-      element.showActions = true;
-      element.draft = true;
-      MockInteractions.tap(element.$.header);
-      MockInteractions.tap(element.$$('.edit'));
-      element._messageText = 'good news, everyone!';
-      element.flushDebouncer('fire-update');
-      element.flushDebouncer('store');
-
-      element.disabled = true;
-      MockInteractions.tap(element.$$('.save'));
-      assert.isFalse(saveStub.called);
-
-      element.disabled = false;
-      MockInteractions.tap(element.$$('.save'));
-      assert.isTrue(saveStub.calledOnce);
-    });
-
-    test('clicking on date link does not trigger nav', () => {
-      const showStub = sinon.stub(page, 'show');
-      const dateEl = element.$$('.date');
-      assert.ok(dateEl);
-      MockInteractions.tap(dateEl);
-      const dest = window.location.pathname + '#5';
-      assert(showStub.lastCall.calledWithExactly(dest, null, false),
-          'Should navigate to ' + dest + ' without triggering nav');
-      showStub.restore();
-    });
-
-    test('proper event fires on resolve, comment is not saved', done => {
-      const save = sandbox.stub(element, 'save');
-      element.addEventListener('comment-update', e => {
-        assert.isTrue(e.detail.comment.unresolved);
-        assert.isFalse(save.called);
-        done();
-      });
-      MockInteractions.tap(element.$$('.resolve input'));
-    });
-
-    test('resolved comment state indicated by checkbox', () => {
-      sandbox.stub(element, 'save');
-      element.comment = {unresolved: false};
-      assert.isTrue(element.$$('.resolve input').checked);
-      element.comment = {unresolved: true};
-      assert.isFalse(element.$$('.resolve input').checked);
-    });
-
-    test('resolved checkbox saves with tap when !editing', () => {
-      element.editing = false;
-      const save = sandbox.stub(element, 'save');
-
-      element.comment = {unresolved: false};
-      assert.isTrue(element.$$('.resolve input').checked);
-      element.comment = {unresolved: true};
-      assert.isFalse(element.$$('.resolve input').checked);
-      assert.isFalse(save.called);
-      MockInteractions.tap(element.$.resolvedCheckbox);
-      assert.isTrue(element.$$('.resolve input').checked);
-      assert.isTrue(save.called);
-    });
-
-    suite('draft saving messages', () => {
-      test('_getSavingMessage', () => {
-        assert.equal(element._getSavingMessage(0), 'All changes saved');
-        assert.equal(element._getSavingMessage(1), 'Saving 1 draft...');
-        assert.equal(element._getSavingMessage(2), 'Saving 2 drafts...');
-        assert.equal(element._getSavingMessage(3), 'Saving 3 drafts...');
-      });
-
-      test('_show{Start,End}Request', () => {
-        const updateStub = sandbox.stub(element, '_updateRequestToast');
-        element._numPendingDraftRequests.number = 1;
-
-        element._showStartRequest();
-        assert.isTrue(updateStub.calledOnce);
-        assert.equal(updateStub.lastCall.args[0], 2);
-        assert.equal(element._numPendingDraftRequests.number, 2);
-
-        element._showEndRequest();
-        assert.isTrue(updateStub.calledTwice);
-        assert.equal(updateStub.lastCall.args[0], 1);
-        assert.equal(element._numPendingDraftRequests.number, 1);
-
-        element._showEndRequest();
-        assert.isTrue(updateStub.calledThrice);
-        assert.equal(updateStub.lastCall.args[0], 0);
-        assert.equal(element._numPendingDraftRequests.number, 0);
-      });
-    });
-
-    test('cancelling an unsaved draft discards, persists in storage', () => {
-      const discardSpy = sandbox.spy(element, '_fireDiscard');
-      const storeStub = sandbox.stub(element.$.storage, 'setDraftComment');
-      const eraseStub = sandbox.stub(element.$.storage, 'eraseDraftComment');
-      element._messageText = 'test text';
-      flushAsynchronousOperations();
-      element.flushDebouncer('store');
-
-      assert.isTrue(storeStub.called);
-      assert.equal(storeStub.lastCall.args[1], 'test text');
-      element._handleCancel({preventDefault: () => {}});
-      assert.isTrue(discardSpy.called);
-      assert.isFalse(eraseStub.called);
-    });
-
-    test('cancelling edit on a saved draft does not store', () => {
-      element.comment.id = 'foo';
-      const discardSpy = sandbox.spy(element, '_fireDiscard');
-      const storeStub = sandbox.stub(element.$.storage, 'setDraftComment');
-      element._messageText = 'test text';
-      flushAsynchronousOperations();
-      element.flushDebouncer('store');
-
-      assert.isFalse(storeStub.called);
-      element._handleCancel({preventDefault: () => {}});
-      assert.isTrue(discardSpy.called);
-    });
-
-    test('deleting text from saved draft and saving deletes the draft', () => {
-      element.comment = {id: 'foo', message: 'test'};
-      element._messageText = '';
-      const discardStub = sandbox.stub(element, '_discardDraft');
-
-      element.save();
-      assert.isTrue(discardStub.called);
-    });
-
-    test('_handleFix fires create-fix event', done => {
-      element.addEventListener('create-fix-comment', e => {
-        assert.deepEqual(e.detail, element._getEventPayload());
-        done();
-      });
-      element.isRobotComment = true;
-      flushAsynchronousOperations();
-
-      MockInteractions.tap(element.$$('.fix'));
-    });
-  });
-</script>
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 556a53a..1cfd5e7 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,6 +37,7 @@
 
   Polymer({
     is: 'gr-diff-cursor',
+    _legacyUndefinedCheck: true,
 
     properties: {
       /**
@@ -295,7 +296,7 @@
     },
 
     _rowHasThread(row) {
-      return row.querySelector('gr-diff-comment-thread');
+      return row.querySelector('.comment-thread');
     },
 
     /**
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 9668a54..f111378 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
@@ -60,7 +60,11 @@
 
       diffElement.loggedIn = false;
       diffElement.patchRange = {basePatchNum: 1, patchNum: 2};
-      diffElement.comments = {left: [], right: []};
+      diffElement.comments = {
+        left: [],
+        right: [],
+        meta: {patchRange: undefined},
+      };
       const setupDone = () => {
         cursorElement._updateStops();
         cursorElement.moveToFirstChunk();
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 cd5b53a..c820668 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,9 +19,14 @@
 
   Polymer({
     is: 'gr-diff-highlight',
+    _legacyUndefinedCheck: true,
 
     properties: {
-      comments: Object,
+      /** @type {!Array<!Gerrit.HoveredRange>} */
+      commentRanges: {
+        type: Array,
+        notify: true,
+      },
       loggedIn: Boolean,
       /**
        * querySelector can return null, so needs to be nullable.
@@ -29,19 +34,14 @@
        * @type {?HTMLElement}
        * */
       _cachedDiffBuilder: Object,
-      isAttached: Boolean,
     },
 
     listeners: {
-      'comment-mouse-out': '_handleCommentMouseOut',
-      'comment-mouse-over': '_handleCommentMouseOver',
-      'create-comment': '_createComment',
+      'comment-thread-mouseleave': '_handleCommentThreadMouseleave',
+      'comment-thread-mouseenter': '_handleCommentThreadMouseenter',
+      'create-range-comment': '_createRangeComment',
     },
 
-    observers: [
-      '_enableSelectionObserver(loggedIn, isAttached)',
-    ],
-
     get diffBuilder() {
       if (!this._cachedDiffBuilder) {
         this._cachedDiffBuilder =
@@ -50,56 +50,91 @@
       return this._cachedDiffBuilder;
     },
 
-    _enableSelectionObserver(loggedIn, isAttached) {
-      if (loggedIn && isAttached) {
-        this.listen(document, 'selectionchange', '_handleSelectionChange');
-      } else {
-        this.unlisten(document, 'selectionchange', '_handleSelectionChange');
-      }
-    },
 
     isRangeSelected() {
       return !!this.$$('gr-selection-action-box');
     },
 
-    _handleSelectionChange() {
-      // Can't use up or down events to handle selection started and/or ended in
-      // in comment threads or outside of diff.
-      // Debounce removeActionBox to give it a chance to react to click/tap.
-      this._removeActionBoxDebounced();
-      this.debounce('selectionChange', this._handleSelection, 200);
+    /**
+     * Determines side/line/range for a DOM selection and shows a tooltip.
+     *
+     * With native shadow DOM, gr-diff-highlight cannot access a selection that
+     * references the DOM elements making up the diff because they are in the
+     * shadow DOM the gr-diff element. For this reason, we listen to the
+     * selectionchange event and retrieve the selection in gr-diff, and then
+     * call this method to process the Selection.
+     *
+     * @param {Selection} selection A DOM Selection living in the shadow DOM of
+     *     the diff element.
+     * @param {boolean} isMouseUp If true, this is called due to a mouseup
+     *     event, in which case we might want to immediately create a comment,
+     *     because isMouseUp === true combined with an existing selection must
+     *     mean that this is the end of a double-click.
+     */
+    handleSelectionChange(selection, isMouseUp) {
+      // Debounce is not just nice for waiting until the selection has settled,
+      // it is also vital for being able to click on the action box before it is
+      // removed.
+      // If you wait longer than 50 ms, then you don't properly catch a very
+      // quick 'c' press after the selection change. If you wait less than 10
+      // ms, then you will have about 50 _handleSelection calls when doing a
+      // simple drag for select.
+      this.debounce(
+          'selectionChange', () => this._handleSelection(selection, isMouseUp),
+          10);
     },
 
-    _handleCommentMouseOver(e) {
-      const comment = e.detail.comment;
-      if (!comment.range) { return; }
-      const lineEl = this.diffBuilder.getLineElByChild(e.target);
-      const side = this.diffBuilder.getSideByLineEl(lineEl);
-      const index = this._indexOfComment(side, comment);
+    _getThreadEl(e) {
+      const path = Polymer.dom(e).path || [];
+      for (const pathEl of path) {
+        if (pathEl.classList.contains('comment-thread')) return pathEl;
+      }
+      return null;
+    },
+
+    _handleCommentThreadMouseenter(e) {
+      const threadEl = this._getThreadEl(e);
+      const index = this._indexForThreadEl(threadEl);
+
       if (index !== undefined) {
-        this.set(['comments', side, index, '__hovering'], true);
+        this.set(['commentRanges', index, 'hovering'], true);
       }
     },
 
-    _handleCommentMouseOut(e) {
-      const comment = e.detail.comment;
-      if (!comment.range) { return; }
-      const lineEl = this.diffBuilder.getLineElByChild(e.target);
-      const side = this.diffBuilder.getSideByLineEl(lineEl);
-      const index = this._indexOfComment(side, comment);
+    _handleCommentThreadMouseleave(e) {
+      const threadEl = this._getThreadEl(e);
+      const index = this._indexForThreadEl(threadEl);
+
       if (index !== undefined) {
-        this.set(['comments', side, index, '__hovering'], false);
+        this.set(['commentRanges', index, 'hovering'], false);
       }
     },
 
-    _indexOfComment(side, comment) {
-      const idProp = comment.id ? 'id' : '__draftID';
-      for (let i = 0; i < this.comments[side].length; i++) {
-        if (comment[idProp] &&
-            this.comments[side][i][idProp] === comment[idProp]) {
-          return i;
+    _indexForThreadEl(threadEl) {
+      const side = threadEl.getAttribute('comment-side');
+      const range = JSON.parse(threadEl.getAttribute('range'));
+
+      if (!range) return undefined;
+
+      return this._indexOfCommentRange(side, range);
+    },
+
+    _indexOfCommentRange(side, range) {
+      function rangesEqual(a, b) {
+        if (!a && !b) {
+          return true;
         }
+        if (!a || !b) {
+          return false;
+        }
+        return a.start_line === b.start_line &&
+            a.start_character === b.start_character &&
+            a.end_line === b.end_line &&
+            a.end_character === b.end_character;
       }
+
+      return this.commentRanges.findIndex(commentRange =>
+        commentRange.side === side && rangesEqual(commentRange.range, range));
     },
 
     /**
@@ -108,6 +143,7 @@
      * syntax highligh, convert native DOM Range objects to Gerrit concepts
      * (line, side, etc).
      *
+     * @param {Selection} selection
      * @return {({
      *   start: {
      *     node: Node,
@@ -123,8 +159,7 @@
      *   }
      * })|null|!Object}
      */
-    _getNormalizedRange() {
-      const selection = window.getSelection();
+    _getNormalizedRange(selection) {
       const rangeCount = selection.rangeCount;
       if (rangeCount === 0) {
         return null;
@@ -237,7 +272,7 @@
         node = contentText;
         column = 0;
       } else {
-        const thread = contentTd.querySelector('gr-diff-comment-thread');
+        const thread = contentTd.querySelector('.comment-thread');
         if (thread && thread.contains(node)) {
           column = this._getLength(contentText);
           node = contentText;
@@ -270,37 +305,72 @@
       actionBox.placeBelow(range);
     },
 
-    _handleSelection() {
-      const normalizedRange = this._getNormalizedRange();
-      if (!normalizedRange) {
-        return;
+    _isRangeValid(range) {
+      if (!range || !range.start || !range.end) {
+        return false;
       }
-      const domRange = window.getSelection().getRangeAt(0);
-      /** @type {?} */
-      const start = normalizedRange.start;
-      if (!start) {
-        return;
-      }
-      const end = normalizedRange.end;
-      if (!end) {
-        return;
-      }
+      const start = range.start;
+      const end = range.end;
       if (start.side !== end.side ||
           end.line < start.line ||
           (start.line === end.line && start.column === end.column)) {
+        return false;
+      }
+      return true;
+    },
+
+    _handleSelection(selection, isMouseUp) {
+      const normalizedRange = this._getNormalizedRange(selection);
+      if (!this._isRangeValid(normalizedRange)) {
+        this._removeActionBox();
         return;
       }
+      const domRange = selection.getRangeAt(0);
+      const start = normalizedRange.start;
+      const end = normalizedRange.end;
 
       // TODO (viktard): Drop empty first and last lines from selection.
 
-      const actionBox = document.createElement('gr-selection-action-box');
-      const root = Polymer.dom(this.root);
-      root.insertBefore(actionBox, root.firstElementChild);
+      // If the selection is from the end of one line to the start of the next
+      // line, then this must have been a double-click, or you have started
+      // dragging. Showing the action box is bad in the former case and not very
+      // useful in the latter, so never do that.
+      // If this was a mouse-up event, we create a comment immediately if
+      // the selection is from the end of a line to the start of the next line.
+      // In a perfect world we would only do this for double-click, but it is
+      // extremely rare that a user would drag from the end of one line to the
+      // start of the next and release the mouse, so we don't bother.
+      // TODO(brohlfs): This does not work, if the double-click is before a new
+      // diff chunk (start will be equal to end), and neither before an "expand
+      // the diff context" block (end line will match the first line of the new
+      // section and thus be greater than start line + 1).
+      if (start.line === end.line - 1 && end.column === 0) {
+        // Rather than trying to find the line contents (for comparing
+        // start.column with the content length), we just check if the selection
+        // is empty to see that it's at the end of a line.
+        const content = domRange.cloneContents().querySelector('.contentText');
+        if (isMouseUp && this._getLength(content) === 0) {
+          this.fire('create-range-comment', {side: start.side, range: {
+            start_line: start.line,
+            start_character: 0,
+            end_line: start.line,
+            end_character: start.column,
+          }});
+        }
+        return;
+      }
+
+      let actionBox = this.$$('gr-selection-action-box');
+      if (!actionBox) {
+        actionBox = document.createElement('gr-selection-action-box');
+        const root = Polymer.dom(this.root);
+        root.insertBefore(actionBox, root.firstElementChild);
+      }
       actionBox.range = {
-        startLine: start.line,
-        startChar: start.column,
-        endLine: end.line,
-        endChar: end.column,
+        start_line: start.line,
+        start_character: start.column,
+        end_line: end.line,
+        end_character: end.column,
       };
       actionBox.side = start.side;
       if (start.line === end.line) {
@@ -319,14 +389,10 @@
       }
     },
 
-    _createComment(e) {
+    _createRangeComment(e) {
       this._removeActionBox();
     },
 
-    _removeActionBoxDebounced() {
-      this.debounce('removeActionBox', this._removeActionBox, 10);
-    },
-
     _removeActionBox() {
       const actionBox = this.$$('gr-selection-action-box');
       if (actionBox) {
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 1c37380..2e50fdb 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
@@ -72,9 +72,9 @@
           <tr class="diff-row side-by-side" left-type="remove" right-type="add">
             <td class="left lineNum" data-value="140"></td>
             <!-- Next tag is formatted to eliminate zero-length text nodes. -->
-            <td class="content remove"><div class="contentText">na💢ti <hl class="foo">te, inquit</hl>, sumus <hl class="bar">aliquando</hl> otiosum, <hl>certe</hl> a <hl><span class="tab-indicator" style="tab-size:8;">	</span></hl>udiam, <hl>quid</hl> sit, <span class="tab-indicator" style="tab-size:8;">	</span>quod <hl>Epicurum</hl></div><gr-diff-comment-thread>
+            <td class="content remove"><div class="contentText">na💢ti <hl class="foo">te, inquit</hl>, sumus <hl class="bar">aliquando</hl> otiosum, <hl>certe</hl> a <hl><span class="tab-indicator" style="tab-size:8;">	</span></hl>udiam, <hl>quid</hl> sit, <span class="tab-indicator" style="tab-size:8;">	</span>quod <hl>Epicurum</hl></div><div class="comment-thread">
                 [Yet another random diff thread content here]
-              </gr-diff-comment-thread></td>
+            </div></td>
             <td class="right lineNum" data-value="120"></td>
             <!-- Next tag is formatted to eliminate zero-length text nodes. -->
             <td class="content add"><div class="contentText">nacti , <hl>,</hl> sumus <hl><span class="tab-indicator" style="tab-size:8;">	</span></hl> otiosum,  <span class="tab-indicator" style="tab-size:8;">	</span> audiam,  sit, quod</div></td>
@@ -158,33 +158,6 @@
       sandbox.restore();
     });
 
-    suite('selectionchange event handling', () => {
-      const emulateSelection = function() {
-        document.dispatchEvent(new CustomEvent('selectionchange'));
-        element.flushDebouncer('selectionChange');
-        element.flushDebouncer('removeActionBox');
-      };
-
-      setup(() => {
-        sandbox.stub(element, '_handleSelection');
-        sandbox.stub(element, '_removeActionBox');
-      });
-
-      test('enabled if logged in', () => {
-        element.loggedIn = true;
-        emulateSelection();
-        assert.isTrue(element._handleSelection.called);
-        assert.isTrue(element._removeActionBox.called);
-      });
-
-      test('ignored if logged out', () => {
-        element.loggedIn = false;
-        emulateSelection();
-        assert.isFalse(element._handleSelection.called);
-        assert.isFalse(element._removeActionBox.called);
-      });
-    });
-
     suite('comment events', () => {
       let builder;
 
@@ -197,27 +170,65 @@
         element._cachedDiffBuilder = builder;
       });
 
-      test('comment-mouse-over from line comments is ignored', () => {
+      test('comment-thread-mouseenter from line comments is ignored', () => {
+        const threadEl = document.createElement('div');
+        threadEl.className = 'comment-thread';
+        threadEl.setAttribute('comment-side', 'right');
+        threadEl.setAttribute('line-num', 3);
+        element.appendChild(threadEl);
+        element.commentRanges = [{side: 'right'}];
+
         sandbox.stub(element, 'set');
-        element.fire('comment-mouse-over', {comment: {}});
+        threadEl.dispatchEvent(
+            new CustomEvent('comment-thread-mouseenter', {bubbles: true}));
         assert.isFalse(element.set.called);
       });
 
-      test('comment-mouse-over from ranged comment causes set', () => {
+      test('comment-thread-mouseenter from ranged comment causes set', () => {
+        const threadEl = document.createElement('div');
+        threadEl.className = 'comment-thread';
+        threadEl.setAttribute('comment-side', 'right');
+        threadEl.setAttribute('line-num', 3);
+        threadEl.setAttribute('range', JSON.stringify({
+          start_line: 3,
+          start_character: 4,
+          end_line: 5,
+          end_character: 6,
+        }));
+        element.appendChild(threadEl);
+        element.commentRanges = [{side: 'right', range: {
+          start_line: 3,
+          start_character: 4,
+          end_line: 5,
+          end_character: 6,
+        }}];
+
         sandbox.stub(element, 'set');
-        sandbox.stub(element, '_indexOfComment').returns(0);
-        element.fire('comment-mouse-over', {comment: {range: {}}});
+        threadEl.dispatchEvent(
+            new CustomEvent('comment-thread-mouseenter', {bubbles: true}));
         assert.isTrue(element.set.called);
+        const args = element.set.lastCall.args;
+        assert.deepEqual(args[0], ['commentRanges', 0, 'hovering']);
+        assert.deepEqual(args[1], true);
       });
 
-      test('comment-mouse-out from line comments is ignored', () => {
-        element.fire('comment-mouse-over', {comment: {}});
-        assert.isFalse(builder.getContentsByLineRange.called);
+      test('comment-thread-mouseleave from line comments is ignored', () => {
+        const threadEl = document.createElement('div');
+        threadEl.className = 'comment-thread';
+        threadEl.setAttribute('comment-side', 'right');
+        threadEl.setAttribute('line-num', 3);
+        element.appendChild(threadEl);
+        element.commentRanges = [{side: 'right'}];
+
+        sandbox.stub(element, 'set');
+        threadEl.dispatchEvent(
+            new CustomEvent('comment-thread-mouseleave', {bubbles: true}));
+        assert.isFalse(element.set.called);
       });
 
-      test('on create-comment action box is removed', () => {
+      test('on create-range-comment action box is removed', () => {
         sandbox.stub(element, '_removeActionBox');
-        element.fire('create-comment', {
+        element.fire('create-range-comment', {
           comment: {
             range: {},
           },
@@ -255,7 +266,7 @@
         range.setStart(startNode, startOffset);
         range.setEnd(endNode, endOffset);
         selection.addRange(range);
-        element._handleSelection();
+        element._handleSelection(selection);
       };
 
       const getActionRange = () =>
@@ -318,10 +329,10 @@
         const actionBox = element.$$('gr-selection-action-box');
         assert.isTrue(element.isRangeSelected());
         assert.deepEqual(getActionRange(), {
-          startLine: 138,
-          startChar: 5,
-          endLine: 138,
-          endChar: 12,
+          start_line: 138,
+          start_character: 5,
+          end_line: 138,
+          end_character: 12,
         });
         assert.equal(getActionSide(), 'left');
         assert.notOk(actionBox.positionBelow);
@@ -337,10 +348,10 @@
         const actionBox = element.$$('gr-selection-action-box');
 
         assert.deepEqual(getActionRange(), {
-          startLine: 119,
-          startChar: 10,
-          endLine: 120,
-          endChar: 36,
+          start_line: 119,
+          start_character: 10,
+          end_line: 120,
+          end_character: 36,
         });
         assert.equal(getActionSide(), 'right');
         assert.notOk(actionBox.positionBelow);
@@ -362,18 +373,18 @@
         getRangeAtStub
             .onFirstCall().returns(startRange)
             .onSecondCall().returns(endRange);
-        sandbox.stub(window, 'getSelection').returns({
+        const selection = {
           rangeCount: 2,
           getRangeAt: getRangeAtStub,
           removeAllRanges: sandbox.stub(),
-        });
-        element._handleSelection();
+        };
+        element._handleSelection(selection);
         assert.isTrue(element.isRangeSelected());
         assert.deepEqual(getActionRange(), {
-          startLine: 119,
-          startChar: 10,
-          endLine: 120,
-          endChar: 36,
+          start_line: 119,
+          start_character: 10,
+          end_line: 120,
+          end_character: 36,
         });
       });
 
@@ -383,10 +394,10 @@
         emulateSelection(startContent.firstChild, 10, endContent.firstChild, 2);
         assert.isTrue(element.isRangeSelected());
         assert.deepEqual(getActionRange(), {
-          startLine: 119,
-          startChar: 10,
-          endLine: 120,
-          endChar: 2,
+          start_line: 119,
+          start_character: 10,
+          end_line: 120,
+          end_character: 2,
         });
         assert.equal(getActionSide(), 'right');
       });
@@ -404,10 +415,10 @@
         emulateSelection(hl.firstChild, 2, hl.nextSibling, 7);
         assert.isTrue(element.isRangeSelected());
         assert.deepEqual(getActionRange(), {
-          startLine: 140,
-          startChar: 8,
-          endLine: 140,
-          endChar: 23,
+          start_line: 140,
+          start_character: 8,
+          end_line: 140,
+          end_character: 23,
         });
         assert.equal(getActionSide(), 'left');
       });
@@ -418,10 +429,10 @@
         emulateSelection(hl.previousSibling, 2, hl.firstChild, 3);
         assert.isTrue(element.isRangeSelected());
         assert.deepEqual(getActionRange(), {
-          startLine: 140,
-          startChar: 18,
-          endLine: 140,
-          endChar: 27,
+          start_line: 140,
+          start_character: 18,
+          end_line: 140,
+          end_character: 27,
         });
       });
 
@@ -431,10 +442,10 @@
         emulateSelection(content.firstChild, 2, hl.firstChild, 2);
         assert.isTrue(element.isRangeSelected());
         assert.deepEqual(getActionRange(), {
-          startLine: 140,
-          startChar: 2,
-          endLine: 140,
-          endChar: 61,
+          start_line: 140,
+          start_character: 2,
+          end_line: 140,
+          end_character: 61,
         });
         assert.equal(getActionSide(), 'left');
       });
@@ -465,15 +476,15 @@
       test('starts in comment thread element', () => {
         const startContent = stubContent(140, 'left');
         const comment = startContent.parentElement.querySelector(
-            'gr-diff-comment-thread');
+            '.comment-thread');
         const endContent = stubContent(141, 'left');
         emulateSelection(comment.firstChild, 2, endContent.firstChild, 4);
         assert.isTrue(element.isRangeSelected());
         assert.deepEqual(getActionRange(), {
-          startLine: 140,
-          startChar: 83,
-          endLine: 141,
-          endChar: 4,
+          start_line: 140,
+          start_character: 83,
+          end_line: 141,
+          end_character: 4,
         });
         assert.equal(getActionSide(), 'left');
       });
@@ -481,14 +492,14 @@
       test('ends in comment thread element', () => {
         const content = stubContent(140, 'left');
         const comment = content.parentElement.querySelector(
-            'gr-diff-comment-thread');
+            '.comment-thread');
         emulateSelection(content.firstChild, 4, comment.firstChild, 1);
         assert.isTrue(element.isRangeSelected());
         assert.deepEqual(getActionRange(), {
-          startLine: 140,
-          startChar: 4,
-          endLine: 140,
-          endChar: 83,
+          start_line: 140,
+          start_character: 4,
+          end_line: 140,
+          end_character: 83,
         });
         assert.equal(getActionSide(), 'left');
       });
@@ -517,10 +528,10 @@
         emulateSelection(startContent.firstChild, 3, endContent.firstChild, 14);
         assert.isTrue(element.isRangeSelected());
         assert.deepEqual(getActionRange(), {
-          startLine: 130,
-          startChar: 3,
-          endLine: 146,
-          endChar: 14,
+          start_line: 130,
+          start_character: 3,
+          end_line: 146,
+          end_character: 14,
         });
         assert.equal(getActionSide(), 'right');
       });
@@ -531,10 +542,10 @@
             content.firstChild, 1, content.querySelector('span'), 0);
         assert.isTrue(element.isRangeSelected());
         assert.deepEqual(getActionRange(), {
-          startLine: 140,
-          startChar: 1,
-          endLine: 140,
-          endChar: 51,
+          start_line: 140,
+          start_character: 1,
+          end_line: 140,
+          end_character: 51,
         });
         assert.equal(getActionSide(), 'left');
       });
@@ -546,10 +557,10 @@
             content.querySelectorAll('span')[1].nextSibling, 1);
         assert.isTrue(element.isRangeSelected());
         assert.deepEqual(getActionRange(), {
-          startLine: 140,
-          startChar: 51,
-          endLine: 140,
-          endChar: 71,
+          start_line: 140,
+          start_character: 51,
+          end_line: 140,
+          end_character: 71,
         });
         assert.equal(getActionSide(), 'left');
       });
@@ -582,10 +593,10 @@
         emulateSelection(startContent.firstChild, 0, endContent.firstChild, 0);
         assert.isTrue(element.isRangeSelected());
         assert.deepEqual(getActionRange(), {
-          startLine: 119,
-          startChar: 0,
-          endLine: 119,
-          endChar: element._getLength(startContent),
+          start_line: 119,
+          start_character: 0,
+          end_line: 119,
+          end_character: element._getLength(startContent),
         });
         assert.equal(getActionSide(), 'right');
       });
@@ -597,10 +608,10 @@
             endContent.parentElement.previousElementSibling, 0);
         assert.isTrue(element.isRangeSelected());
         assert.deepEqual(getActionRange(), {
-          startLine: 146,
-          startChar: 0,
-          endLine: 146,
-          endChar: 84,
+          start_line: 146,
+          start_character: 0,
+          end_line: 146,
+          end_character: 84,
         });
         assert.equal(getActionSide(), 'right');
       });
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 305374b..63af2b0 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
@@ -16,9 +16,10 @@
 -->
 
 <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="../../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="../gr-diff/gr-diff.html">
 
 <dom-module id="gr-diff-host">
@@ -30,14 +31,12 @@
         patch-range="[[patchRange]]"
         path="[[path]]"
         prefs="[[prefs]]"
-        project-config="[[projectConfig]]"
         project-name="[[projectName]]"
         display-line="[[displayLine]]"
         is-image-diff="[[isImageDiff]]"
         commit-range="[[commitRange]]"
         hidden$="[[hidden]]"
         no-render-on-prefs-change="[[noRenderOnPrefsChange]]"
-        comments="[[comments]]"
         line-wrapping="[[lineWrapping]]"
         view-mode="[[viewMode]]"
         line-of-interest="[[lineOfInterest]]"
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 9b10847..a177fb1 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
@@ -28,6 +28,13 @@
     UNIFIED: 'UNIFIED_DIFF',
   };
 
+  /** @enum {string} */
+  const TimingLabel = {
+    TOTAL: 'Diff Total Render',
+    CONTENT: 'Diff Content Render',
+    SYNTAX: 'Diff Syntax Render',
+  };
+
   const WHITESPACE_IGNORE_NONE = 'IGNORE_NONE';
 
   /**
@@ -45,15 +52,21 @@
     return !!(diff.binary && (isA || isB));
   }
 
+  /** @enum {string} */
+  Gerrit.DiffSide = {
+    LEFT: 'left',
+    RIGHT: 'right',
+  };
+
   /**
    * Wrapper around gr-diff.
    *
    * Webcomponent fetching diffs and related data from restAPI and passing them
    * to the presentational gr-diff for rendering.
    */
-  // TODO(oler): Move all calls to restAPI from gr-diff here.
   Polymer({
     is: 'gr-diff-host',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the user selects a line.
@@ -85,9 +98,6 @@
       prefs: {
         type: Object,
       },
-      projectConfig: {
-        type: Object,
-      },
       projectName: String,
       displayLine: {
         type: Boolean,
@@ -112,7 +122,10 @@
         type: Boolean,
         value: false,
       },
-      comments: Object,
+      comments: {
+        type: Object,
+        observer: '_commentsChanged',
+      },
       lineWrapping: {
         type: Boolean,
         value: false,
@@ -181,10 +194,34 @@
       },
 
       _loadedWhitespaceLevel: String,
+
+      _parentIndex: {
+        type: Number,
+        computed: '_computeParentIndex(patchRange.*)',
+      },
     },
 
+    behaviors: [
+      Gerrit.PatchSetBehavior,
+    ],
+
     listeners: {
-      'draft-interaction': '_handleDraftInteraction',
+      // These are named inconsistently for a reason:
+      // The create-comment event is fired to indicate that we should
+      // create a comment.
+      // The comment-* events are just notifying that the comments did already
+      // change in some way, and that we should update any models we may want
+      // to keep in sync.
+      'create-comment': '_handleCreateComment',
+      'comment-discard': '_handleCommentDiscard',
+      'comment-update': '_handleCommentUpdate',
+      'comment-save': '_handleCommentSave',
+
+      'render-start': '_handleRenderStart',
+      'render-content': '_handleRenderContent',
+      'render-syntax': '_handleRenderSyntax',
+
+      'normalize-range': '_handleNormalizeRange',
     },
 
     observers: [
@@ -307,9 +344,15 @@
       this._blame = null;
     },
 
-    /** @return {!Array<!HTMLElement>} */
+    /**
+     * The thread elements in this diff, in no particular order.
+     *
+     * @return {!Array<!HTMLElement>}
+     */
     getThreadEls() {
-      return this.$.diff.getThreadEls();
+      // Polymer2: querySelectorAll returns NodeList instead of Array.
+      return Array.from(
+          Polymer.dom(this.$.diff).querySelectorAll('.comment-thread'));
     },
 
     /** @param {HTMLElement} el */
@@ -436,6 +479,69 @@
       return isImageDiff(diff);
     },
 
+    _commentsChanged(newComments) {
+      const allComments = [];
+      for (const side of [GrDiffBuilder.Side.LEFT, GrDiffBuilder.Side.RIGHT]) {
+        // This is needed by the threading.
+        for (const comment of newComments[side]) {
+          comment.__commentSide = side;
+        }
+        allComments.push(...newComments[side]);
+      }
+      // Currently, the only way this is ever changed here is when the initial
+      // comments are loaded, so it's okay performance wise to clear the threads
+      // and recreate them. If this changes in future, we might want to reuse
+      // some DOM nodes here.
+      this._clearThreads();
+      const threads = this._createThreads(allComments);
+      for (const thread of threads) {
+        const threadEl = this._createThreadElement(thread);
+        this._attachThreadElement(threadEl);
+      }
+    },
+
+    /**
+     * @param {!Array<!Object>} comments
+     * @return {!Array<!Object>} Threads for the given comments.
+     */
+    _createThreads(comments) {
+      const sortedComments = comments.slice(0).sort((a, b) => {
+        if (b.__draft && !a.__draft ) { return 0; }
+        if (a.__draft && !b.__draft ) { return 1; }
+        return util.parseDate(a.updated) - util.parseDate(b.updated);
+      });
+
+      const threads = [];
+      for (const comment of sortedComments) {
+        // If the comment is in reply to another comment, find that comment's
+        // thread and append to it.
+        if (comment.in_reply_to) {
+          const thread = threads.find(thread =>
+            thread.comments.some(c => c.id === comment.in_reply_to));
+          if (thread) {
+            thread.comments.push(comment);
+            continue;
+          }
+        }
+
+        // Otherwise, this comment starts its own thread.
+        const newThread = {
+          start_datetime: comment.updated,
+          comments: [comment],
+          commentSide: comment.__commentSide,
+          patchNum: comment.patch_set,
+          rootId: comment.id || comment.__draftID,
+          lineNum: comment.line,
+          isOnParent: comment.side === 'PARENT',
+        };
+        if (comment.range) {
+          newThread.range = Object.assign({}, comment.range);
+        }
+        threads.push(newThread);
+      }
+      return threads;
+    },
+
     /**
      * @param {Object} blame
      * @return {boolean}
@@ -453,11 +559,160 @@
           this.patchRange);
     },
 
-    _handleDraftInteraction() {
+    /** @param {CustomEvent} e */
+    _handleCreateComment(e) {
+      const {lineNum, side, patchNum, isOnParent, range} = e.detail;
+      const threadEl = this._getOrCreateThread(patchNum, lineNum, side, range,
+          isOnParent);
+      threadEl.addOrEditDraft(lineNum, range);
+
       this.$.reporting.recordDraftInteraction();
     },
 
     /**
+     * Gets or creates a comment thread at a given location.
+     * May provide a range, to get/create a range comment.
+     *
+     * @param {string} patchNum
+     * @param {?number} lineNum
+     * @param {string} commentSide
+     * @param {Gerrit.Range|undefined} range
+     * @param {boolean} isOnParent
+     * @return {!Object}
+     */
+    _getOrCreateThread(patchNum, lineNum, commentSide, range, isOnParent) {
+      let threadEl = this._getThreadEl(lineNum, commentSide, range);
+      if (!threadEl) {
+        threadEl = this._createThreadElement({
+          comments: [],
+          commentSide,
+          patchNum,
+          lineNum,
+          range,
+          isOnParent,
+        });
+        this._attachThreadElement(threadEl);
+      }
+      return threadEl;
+    },
+
+    _attachThreadElement(threadEl) {
+      Polymer.dom(this.$.diff).appendChild(threadEl);
+    },
+
+    _clearThreads() {
+      for (const threadEl of this.getThreadEls()) {
+        const parent = Polymer.dom(threadEl).parentNode;
+        Polymer.dom(parent).removeChild(threadEl);
+      }
+    },
+
+    _createThreadElement(thread) {
+      const threadEl = document.createElement('gr-comment-thread');
+      threadEl.className = 'comment-thread';
+      threadEl.setAttribute('slot', `${thread.commentSide}-${thread.lineNum}`);
+      threadEl.comments = thread.comments;
+      threadEl.commentSide = thread.commentSide;
+      threadEl.isOnParent = !!thread.isOnParent;
+      threadEl.parentIndex = this._parentIndex;
+      threadEl.changeNum = this.changeNum;
+      threadEl.patchNum = thread.patchNum;
+      threadEl.lineNum = thread.lineNum;
+      const rootIdChangedListener = changeEvent => {
+        thread.rootId = changeEvent.detail.value;
+      };
+      threadEl.addEventListener('root-id-changed', rootIdChangedListener);
+      threadEl.path = this.path;
+      threadEl.projectName = this.projectName;
+      threadEl.range = thread.range;
+      const threadDiscardListener = e => {
+        const threadEl = /** @type {!Node} */ (e.currentTarget);
+
+        const parent = Polymer.dom(threadEl).parentNode;
+        Polymer.dom(parent).removeChild(threadEl);
+
+        threadEl.removeEventListener('root-id-changed', rootIdChangedListener);
+        threadEl.removeEventListener('thread-discard', threadDiscardListener);
+      };
+      threadEl.addEventListener('thread-discard', threadDiscardListener);
+      return threadEl;
+    },
+
+    /**
+     * Gets a comment thread element at a given location.
+     * May provide a range, to get a range comment.
+     *
+     * @param {?number} lineNum
+     * @param {string} commentSide
+     * @param {!Gerrit.Range=} range
+     * @return {?Node}
+     */
+    _getThreadEl(lineNum, commentSide, range=undefined) {
+      let line;
+      if (commentSide === GrDiffBuilder.Side.LEFT) {
+        line = {beforeNumber: lineNum};
+      } else if (commentSide === GrDiffBuilder.Side.RIGHT) {
+        line = {afterNumber: lineNum};
+      } else {
+        throw new Error(`Unknown side: ${commentSide}`);
+      }
+      function matchesRange(threadEl) {
+        const threadRange = /** @type {!Gerrit.Range} */(
+          JSON.parse(threadEl.getAttribute('range')));
+        return Gerrit.rangesEqual(threadRange, range);
+      }
+
+      const filteredThreadEls = this._filterThreadElsForLocation(
+          this.getThreadEls(), line, commentSide).filter(matchesRange);
+      return filteredThreadEls.length ? filteredThreadEls[0] : null;
+    },
+
+    /**
+     * @param {!Array<!HTMLElement>} threadEls
+     * @param {!{beforeNumber: (number|string|undefined|null),
+     *           afterNumber: (number|string|undefined|null)}}
+     *     lineInfo
+     * @param {!Gerrit.DiffSide=} side The side (LEFT, RIGHT) for
+     *     which to return the threads.
+     * @return {!Array<!HTMLElement>} The thread elements matching the given
+     *     location.
+     */
+    _filterThreadElsForLocation(threadEls, lineInfo, side) {
+      function matchesLeftLine(threadEl) {
+        return threadEl.getAttribute('comment-side') ==
+            Gerrit.DiffSide.LEFT &&
+            threadEl.getAttribute('line-num') == lineInfo.beforeNumber;
+      }
+      function matchesRightLine(threadEl) {
+        return threadEl.getAttribute('comment-side') ==
+            Gerrit.DiffSide.RIGHT &&
+            threadEl.getAttribute('line-num') == lineInfo.afterNumber;
+      }
+      function matchesFileComment(threadEl) {
+        return threadEl.getAttribute('comment-side') == side &&
+              // line/range comments have 1-based line set, if line is falsy it's
+              // a file comment
+              !threadEl.getAttribute('line-num');
+      }
+
+      // Select the appropriate matchers for the desired side and line
+      // If side is BOTH, we want both the left and right matcher.
+      const matchers = [];
+      if (side !== Gerrit.DiffSide.RIGHT) {
+        matchers.push(matchesLeftLine);
+      }
+      if (side !== Gerrit.DiffSide.LEFT) {
+        matchers.push(matchesRightLine);
+      }
+      if (lineInfo.afterNumber === 'FILE' ||
+          lineInfo.beforeNumber === 'FILE') {
+        matchers.push(matchesFileComment);
+      }
+      return threadEls.filter(threadEl =>
+        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.
@@ -515,5 +770,107 @@
         this.reload();
       }
     },
+
+    /**
+     * @param {Object} patchRangeRecord
+     * @return {number|null}
+     */
+    _computeParentIndex(patchRangeRecord) {
+      return this.isMergeParent(patchRangeRecord.base.basePatchNum) ?
+        this.getParentIndex(patchRangeRecord.base.basePatchNum) : null;
+    },
+
+    _handleCommentSave(e) {
+      const comment = e.detail.comment;
+      const side = e.detail.comment.__commentSide;
+      const idx = this._findDraftIndex(comment, side);
+      this.set(['comments', side, idx], comment);
+      this._handleCommentSaveOrDiscard();
+    },
+
+    _handleCommentDiscard(e) {
+      const comment = e.detail.comment;
+      this._removeComment(comment);
+      this._handleCommentSaveOrDiscard();
+    },
+
+    /**
+     * Closure annotation for Polymer.prototype.push is off. Submitted PR:
+     * https://github.com/Polymer/polymer/pull/4776
+     * but for not supressing annotations.
+     *
+     * @suppress {checkTypes}
+     */
+    _handleCommentUpdate(e) {
+      const comment = e.detail.comment;
+      const side = e.detail.comment.__commentSide;
+      let idx = this._findCommentIndex(comment, side);
+      if (idx === -1) {
+        idx = this._findDraftIndex(comment, side);
+      }
+      if (idx !== -1) { // Update draft or comment.
+        this.set(['comments', side, idx], comment);
+      } else { // Create new draft.
+        this.push(['comments', side], comment);
+      }
+    },
+
+    _handleCommentSaveOrDiscard() {
+      this.dispatchEvent(new CustomEvent('diff-comments-modified',
+          {bubbles: true}));
+    },
+
+    _removeComment(comment) {
+      const side = comment.__commentSide;
+      this._removeCommentFromSide(comment, side);
+    },
+
+    _removeCommentFromSide(comment, side) {
+      let idx = this._findCommentIndex(comment, side);
+      if (idx === -1) {
+        idx = this._findDraftIndex(comment, side);
+      }
+      if (idx !== -1) {
+        this.splice('comments.' + side, idx, 1);
+      }
+    },
+
+    /** @return {number} */
+    _findCommentIndex(comment, side) {
+      if (!comment.id || !this.comments[side]) {
+        return -1;
+      }
+      return this.comments[side].findIndex(item => item.id === comment.id);
+    },
+
+    /** @return {number} */
+    _findDraftIndex(comment, side) {
+      if (!comment.__draftID || !this.comments[side]) {
+        return -1;
+      }
+      return this.comments[side].findIndex(
+          item => item.__draftID === comment.__draftID);
+    },
+
+    _handleRenderStart() {
+      this.$.reporting.time(TimingLabel.TOTAL);
+      this.$.reporting.time(TimingLabel.CONTENT);
+    },
+
+    _handleRenderContent() {
+      this.$.reporting.timeEnd(TimingLabel.CONTENT);
+      this.$.reporting.time(TimingLabel.SYNTAX);
+    },
+
+    _handleRenderSyntax() {
+      this.$.reporting.timeEnd(TimingLabel.SYNTAX);
+      this.$.reporting.timeEnd(TimingLabel.TOTAL);
+    },
+
+    _handleNormalizeRange(event) {
+      this.$.reporting.reportInteraction('normalize-range',
+          `Modified invalid comment range on l. ${event.detail.lineNum}` +
+          ` of the ${event.detail.side} side`);
+    },
   });
 })();
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 b553ae6..6e7c239 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
@@ -37,9 +37,19 @@
   suite('gr-diff-host tests', () => {
     let element;
     let sandbox;
+    let getLoggedIn;
 
     setup(() => {
       sandbox = sinon.sandbox.create();
+      getLoggedIn = false;
+      stub('gr-rest-api-interface', {
+        async getLoggedIn() { return getLoggedIn; },
+      });
+      stub('gr-reporting', {
+        time: sandbox.stub(),
+        timeEnd: sandbox.stub(),
+      });
+
       element = fixture('basic');
     });
 
@@ -47,6 +57,247 @@
       sandbox.restore();
     });
 
+    suite('handle comment-update', () => {
+      setup(() => {
+        sandbox.stub(element, '_commentsChanged');
+        element.comments = {
+          meta: {
+            changeNum: '42',
+            patchRange: {
+              basePatchNum: 'PARENT',
+              patchNum: 3,
+            },
+            path: '/path/to/foo',
+            projectConfig: {foo: 'bar'},
+          },
+          left: [
+            {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
+            {id: 'bc2', side: 'PARENT', __commentSide: 'left'},
+            {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
+            {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
+          ],
+          right: [
+            {id: 'c1', __commentSide: 'right'},
+            {id: 'c2', __commentSide: 'right'},
+            {id: 'd1', __draft: true, __commentSide: 'right'},
+            {id: 'd2', __draft: true, __commentSide: 'right'},
+          ],
+        };
+      });
+
+      test('creating a draft', () => {
+        const comment = {__draft: true, __draftID: 'tempID', side: 'PARENT',
+          __commentSide: 'left'};
+        element.fire('comment-update', {comment});
+        assert.include(element.comments.left, comment);
+      });
+
+      test('discarding a draft', () => {
+        const draftID = 'tempID';
+        const id = 'savedID';
+        const comment = {
+          __draft: true,
+          __draftID: draftID,
+          side: 'PARENT',
+          __commentSide: 'left',
+        };
+        const diffCommentsModifiedStub = sandbox.stub();
+        element.addEventListener('diff-comments-modified',
+            diffCommentsModifiedStub);
+        element.comments.left.push(comment);
+        comment.id = id;
+        element.fire('comment-discard', {comment});
+        const drafts = element.comments.left.filter(item => {
+          return item.__draftID === draftID;
+        });
+        assert.equal(drafts.length, 0);
+        assert.isTrue(diffCommentsModifiedStub.called);
+      });
+
+      test('saving a draft', () => {
+        const draftID = 'tempID';
+        const id = 'savedID';
+        const comment = {
+          __draft: true,
+          __draftID: draftID,
+          side: 'PARENT',
+          __commentSide: 'left',
+        };
+        const diffCommentsModifiedStub = sandbox.stub();
+        element.addEventListener('diff-comments-modified',
+            diffCommentsModifiedStub);
+        element.comments.left.push(comment);
+        comment.id = id;
+        element.fire('comment-save', {comment});
+        const drafts = element.comments.left.filter(item => {
+          return item.__draftID === draftID;
+        });
+        assert.equal(drafts.length, 1);
+        assert.equal(drafts[0].id, id);
+        assert.isTrue(diffCommentsModifiedStub.called);
+      });
+    });
+
+    test('remove comment', () => {
+      sandbox.stub(element, '_commentsChanged');
+      element.comments = {
+        meta: {
+          changeNum: '42',
+          patchRange: {
+            basePatchNum: 'PARENT',
+            patchNum: 3,
+          },
+          path: '/path/to/foo',
+          projectConfig: {foo: 'bar'},
+        },
+        left: [
+          {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
+          {id: 'bc2', side: 'PARENT', __commentSide: 'left'},
+          {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
+          {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
+        ],
+        right: [
+          {id: 'c1', __commentSide: 'right'},
+          {id: 'c2', __commentSide: 'right'},
+          {id: 'd1', __draft: true, __commentSide: 'right'},
+          {id: 'd2', __draft: true, __commentSide: 'right'},
+        ],
+      };
+
+      element._removeComment({});
+      // Using JSON.stringify because Safari 9.1 (11601.5.17.1) doesn’t seem
+      // to believe that one object deepEquals another even when they do :-/.
+      assert.equal(JSON.stringify(element.comments), JSON.stringify({
+        meta: {
+          changeNum: '42',
+          patchRange: {
+            basePatchNum: 'PARENT',
+            patchNum: 3,
+          },
+          path: '/path/to/foo',
+          projectConfig: {foo: 'bar'},
+        },
+        left: [
+          {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
+          {id: 'bc2', side: 'PARENT', __commentSide: 'left'},
+          {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
+          {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
+        ],
+        right: [
+          {id: 'c1', __commentSide: 'right'},
+          {id: 'c2', __commentSide: 'right'},
+          {id: 'd1', __draft: true, __commentSide: 'right'},
+          {id: 'd2', __draft: true, __commentSide: 'right'},
+        ],
+      }));
+
+      element._removeComment({id: 'bc2', side: 'PARENT',
+        __commentSide: 'left'});
+      assert.deepEqual(element.comments, {
+        meta: {
+          changeNum: '42',
+          patchRange: {
+            basePatchNum: 'PARENT',
+            patchNum: 3,
+          },
+          path: '/path/to/foo',
+          projectConfig: {foo: 'bar'},
+        },
+        left: [
+          {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
+          {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
+          {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
+        ],
+        right: [
+          {id: 'c1', __commentSide: 'right'},
+          {id: 'c2', __commentSide: 'right'},
+          {id: 'd1', __draft: true, __commentSide: 'right'},
+          {id: 'd2', __draft: true, __commentSide: 'right'},
+        ],
+      });
+
+      element._removeComment({id: 'd2', __commentSide: 'right'});
+      assert.deepEqual(element.comments, {
+        meta: {
+          changeNum: '42',
+          patchRange: {
+            basePatchNum: 'PARENT',
+            patchNum: 3,
+          },
+          path: '/path/to/foo',
+          projectConfig: {foo: 'bar'},
+        },
+        left: [
+          {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
+          {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
+          {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
+        ],
+        right: [
+          {id: 'c1', __commentSide: 'right'},
+          {id: 'c2', __commentSide: 'right'},
+          {id: 'd1', __draft: true, __commentSide: 'right'},
+        ],
+      });
+    });
+
+    test('thread-discard handling', () => {
+      const threads = [
+        {comments: [{id: 4711}]},
+        {comments: [{id: 42}]},
+      ];
+      element._parentIndex = 1;
+      element.changeNum = '2';
+      element.path = 'some/path';
+      element.projectName = 'Some project';
+      const threadEls = threads.map(
+          thread => element._createThreadElement(thread));
+      assert.equal(threadEls.length, 2);
+      assert.equal(threadEls[0].rootId, 4711);
+      assert.equal(threadEls[1].rootId, 42);
+      for (const threadEl of threadEls) {
+        Polymer.dom(element).appendChild(threadEl);
+      }
+
+      threadEls[0].dispatchEvent(
+          new CustomEvent('thread-discard', {detail: {rootId: 4711}}));
+      const attachedThreads = element.queryAllEffectiveChildren(
+          'gr-comment-thread');
+      assert.equal(attachedThreads.length, 1);
+      assert.equal(attachedThreads[0].rootId, 42);
+    });
+
+    suite('render reporting', () => {
+      test('starts total and content timer on render-start', done => {
+        element.dispatchEvent(
+            new CustomEvent('render-start', {bubbles: true}));
+        assert.isTrue(element.$.reporting.time.calledWithExactly(
+            'Diff Total Render'));
+        assert.isTrue(element.$.reporting.time.calledWithExactly(
+            'Diff Content Render'));
+        done();
+      });
+
+      test('ends content and starts syntax timer on render-content', done => {
+        element.dispatchEvent(
+            new CustomEvent('render-content', {bubbles: true}));
+        assert.isTrue(element.$.reporting.time.calledWithExactly(
+            'Diff Syntax Render'));
+        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('reload() cancels before network resolves', () => {
       const cancelStub = sandbox.stub(element.$.diff, 'cancel');
 
@@ -59,12 +310,8 @@
 
     suite('not logged in', () => {
       setup(() => {
-        const getLoggedInPromise = Promise.resolve(false);
-        stub('gr-rest-api-interface', {
-          getLoggedIn() { return getLoggedInPromise; },
-        });
+        getLoggedIn = false;
         element = fixture('basic');
-        return getLoggedInPromise;
       });
 
       test('reload() loads files weblinks', () => {
@@ -181,7 +428,11 @@
               });
 
           element.patchRange = {basePatchNum: 'PARENT', patchNum: 1};
-          element.comments = {left: [], right: []};
+          element.comments = {
+            left: [],
+            right: [],
+            meta: {patchRange: element.patchRange},
+          };
         });
 
         test('renders image diffs with same file name', done => {
@@ -555,13 +806,11 @@
       });
     });
 
-    test('delegates getThreadEls()', () => {
-      const returnValue = [document.createElement('b')];
-      const stub = sandbox.stub(element.$.diff, 'getThreadEls')
-          .returns(returnValue);
-      assert.equal(element.getThreadEls(), returnValue);
-      assert.isTrue(stub.calledOnce);
-      assert.equal(stub.lastCall.args.length, 0);
+    test('getThreadEls() returns .comment-threads', () => {
+      const threadEl = document.createElement('div');
+      threadEl.className = 'comment-thread';
+      Polymer.dom(element.$.diff).appendChild(threadEl);
+      assert.deepEqual(element.getThreadEls(), [threadEl]);
     });
 
     test('delegates addDraftAtLine(el)', () => {
@@ -617,12 +866,6 @@
       assert.equal(element.$.diff.prefs, value);
     });
 
-    test('passes in projectConfig', () => {
-      const value = {};
-      element.projectConfig = value;
-      assert.equal(element.$.diff.projectConfig, value);
-    });
-
     test('passes in changeNum', () => {
       const value = '12345';
       element.changeNum = value;
@@ -660,12 +903,6 @@
       assert.equal(element.$.diff.noRenderOnPrefsChange, value);
     });
 
-    test('passes in comments', () => {
-      const value = {left: [], right: []};
-      element.comments = value;
-      assert.equal(element.$.diff.comments, value);
-    });
-
     test('passes in lineWrapping', () => {
       const value = true;
       element.lineWrapping = value;
@@ -776,6 +1013,249 @@
       });
     });
 
+    test('_createThreads', () => {
+      const comments = [
+        {
+          id: 'sallys_confession',
+          message: 'i like you, jack',
+          updated: '2015-12-23 15:00:20.396000000',
+          line: 1,
+          __commentSide: 'left',
+        }, {
+          id: 'jacks_reply',
+          message: 'i like you, too',
+          updated: '2015-12-24 15:01:20.396000000',
+          __commentSide: 'left',
+          line: 1,
+          in_reply_to: 'sallys_confession',
+        },
+        {
+          id: 'new_draft',
+          message: 'i do not like either of you',
+          __commentSide: 'left',
+          __draft: true,
+          updated: '2015-12-20 15:01:20.396000000',
+        },
+      ];
+
+      const actualThreads = element._createThreads(comments);
+
+      assert.equal(actualThreads.length, 2);
+
+      assert.equal(
+          actualThreads[0].start_datetime, '2015-12-23 15:00:20.396000000');
+      assert.equal(actualThreads[0].commentSide, 'left');
+      assert.equal(actualThreads[0].comments.length, 2);
+      assert.deepEqual(actualThreads[0].comments[0], comments[0]);
+      assert.deepEqual(actualThreads[0].comments[1], comments[1]);
+      assert.equal(actualThreads[0].patchNum, undefined);
+      assert.equal(actualThreads[0].rootId, 'sallys_confession');
+      assert.equal(actualThreads[0].lineNum, 1);
+
+      assert.equal(
+          actualThreads[1].start_datetime, '2015-12-20 15:01:20.396000000');
+      assert.equal(actualThreads[1].commentSide, 'left');
+      assert.equal(actualThreads[1].comments.length, 1);
+      assert.deepEqual(actualThreads[1].comments[0], comments[2]);
+      assert.equal(actualThreads[1].patchNum, undefined);
+      assert.equal(actualThreads[1].rootId, 'new_draft');
+      assert.equal(actualThreads[1].lineNum, undefined);
+    });
+
+    test('_createThreads inherits patchNum and range', () => {
+      const comments = [{
+        id: 'betsys_confession',
+        message: 'i like you, jack',
+        updated: '2015-12-24 15:00:10.396000000',
+        range: {
+          start_line: 1,
+          start_character: 1,
+          end_line: 1,
+          end_character: 2,
+        },
+        patch_set: 5,
+        __commentSide: 'left',
+        line: 1,
+      }];
+
+      expectedThreads = [
+        {
+          start_datetime: '2015-12-24 15:00:10.396000000',
+          commentSide: 'left',
+          comments: [{
+            id: 'betsys_confession',
+            message: 'i like you, jack',
+            updated: '2015-12-24 15:00:10.396000000',
+            range: {
+              start_line: 1,
+              start_character: 1,
+              end_line: 1,
+              end_character: 2,
+            },
+            patch_set: 5,
+            __commentSide: 'left',
+            line: 1,
+          }],
+          patchNum: 5,
+          rootId: 'betsys_confession',
+          range: {
+            start_line: 1,
+            start_character: 1,
+            end_line: 1,
+            end_character: 2,
+          },
+          lineNum: 1,
+          isOnParent: false,
+        },
+      ];
+
+      assert.deepEqual(
+          element._createThreads(comments),
+          expectedThreads);
+    });
+
+    test('_createThreads does not thread unrelated comments at same location',
+        () => {
+          const comments = [
+            {
+              id: 'sallys_confession',
+              message: 'i like you, jack',
+              updated: '2015-12-23 15:00:20.396000000',
+              __commentSide: 'left',
+            }, {
+              id: 'jacks_reply',
+              message: 'i like you, too',
+              updated: '2015-12-24 15:01:20.396000000',
+              __commentSide: 'left',
+            },
+          ];
+          assert.equal(element._createThreads(comments).length, 2);
+        });
+
+    test('_createThreads derives isOnParent using  side from first comment',
+        () => {
+          const comments = [
+            {
+              id: 'sallys_confession',
+              message: 'i like you, jack',
+              updated: '2015-12-23 15:00:20.396000000',
+              // line: 1,
+              // __commentSide: 'left',
+            }, {
+              id: 'jacks_reply',
+              message: 'i like you, too',
+              updated: '2015-12-24 15:01:20.396000000',
+              // __commentSide: 'left',
+              // line: 1,
+              in_reply_to: 'sallys_confession',
+            },
+          ];
+
+          assert.equal(element._createThreads(comments)[0].isOnParent, false);
+
+          comments[0].side = 'REVISION';
+          assert.equal(element._createThreads(comments)[0].isOnParent, false);
+
+          comments[0].side = 'PARENT';
+          assert.equal(element._createThreads(comments)[0].isOnParent, true);
+        });
+
+    test('_getOrCreateThread', () => {
+      const commentSide = 'left';
+
+      assert.isOk(element._getOrCreateThread('2', 3,
+          commentSide, undefined, false));
+
+      let threads = Polymer.dom(element.$.diff)
+          .queryDistributedElements('gr-comment-thread');
+
+      assert.equal(threads.length, 1);
+      assert.equal(threads[0].commentSide, commentSide);
+      assert.equal(threads[0].range, undefined);
+      assert.equal(threads[0].isOnParent, false);
+      assert.equal(threads[0].patchNum, 2);
+
+
+      // Try to fetch a thread with a different range.
+      range = {
+        start_line: 1,
+        start_character: 1,
+        end_line: 1,
+        end_character: 3,
+      };
+
+      assert.isOk(element._getOrCreateThread(
+          '3', 1, commentSide, range, true));
+
+      threads = Polymer.dom(element.$.diff)
+          .queryDistributedElements('gr-comment-thread');
+
+      assert.equal(threads.length, 2);
+      assert.equal(threads[1].commentSide, commentSide);
+      assert.equal(threads[1].range, range);
+      assert.equal(threads[1].isOnParent, true);
+      assert.equal(threads[1].patchNum, 3);
+    });
+
+    test('_filterThreadElsForLocation with no threads', () => {
+      const line = {beforeNumber: 3, afterNumber: 5};
+
+      const threads = [];
+      assert.deepEqual(element._filterThreadElsForLocation(threads, line), []);
+      assert.deepEqual(element._filterThreadElsForLocation(threads, line,
+          Gerrit.DiffSide.LEFT), []);
+      assert.deepEqual(element._filterThreadElsForLocation(threads, line,
+          Gerrit.DiffSide.RIGHT), []);
+    });
+
+    test('_filterThreadElsForLocation for line comments', () => {
+      const line = {beforeNumber: 3, afterNumber: 5};
+
+      const l3 = document.createElement('div');
+      l3.setAttribute('line-num', 3);
+      l3.setAttribute('comment-side', 'left');
+
+      const l5 = document.createElement('div');
+      l5.setAttribute('line-num', 5);
+      l5.setAttribute('comment-side', 'left');
+
+      const r3 = document.createElement('div');
+      r3.setAttribute('line-num', 3);
+      r3.setAttribute('comment-side', 'right');
+
+      const r5 = document.createElement('div');
+      r5.setAttribute('line-num', 5);
+      r5.setAttribute('comment-side', 'right');
+
+      const threadEls = [l3, l5, r3, r5];
+      assert.deepEqual(element._filterThreadElsForLocation(threadEls, line),
+          [l3, r5]);
+      assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
+          Gerrit.DiffSide.LEFT), [l3]);
+      assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
+          Gerrit.DiffSide.RIGHT), [r5]);
+    });
+
+    test('_filterThreadElsForLocation for file comments', () => {
+      const line = {beforeNumber: 'FILE', afterNumber: 'FILE'};
+
+      const l = document.createElement('div');
+      l.setAttribute('comment-side', 'left');
+
+      const r = document.createElement('div');
+      r.setAttribute('comment-side', 'right');
+
+      const threadEls = [l, r];
+      assert.deepEqual(element._filterThreadElsForLocation(threadEls, line),
+          [l, r]);
+      assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
+          Gerrit.DiffSide.BOTH), [l, r]);
+      assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
+          Gerrit.DiffSide.LEFT), [l]);
+      assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
+          Gerrit.DiffSide.RIGHT), [r]);
+    });
+
     suite('_translateChunksToIgnore', () => {
       let content;
 
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 88dd91a..e2d6a28 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-diff-mode-selector',
+    _legacyUndefinedCheck: true,
 
     properties: {
       mode: {
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 b50ef69..bcc3c2c 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-diff-preferences-dialog',
+    _legacyUndefinedCheck: true,
 
     properties: {
       /** @type {?} */
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 baa8bba..663cf25 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
@@ -20,5 +20,7 @@
 <dom-module id="gr-diff-processor">
   <script src="../gr-diff/gr-diff-line.js"></script>
   <script src="../gr-diff/gr-diff-group.js"></script>
+
+  <script src="../../../scripts/util.js"></script>
   <script src="gr-diff-processor.js"></script>
 </dom-module>
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 b934d02..2cc69e6 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
@@ -45,8 +45,22 @@
    */
   const MAX_GROUP_SIZE = 120;
 
+  /**
+   * Converts the API's `DiffContent`s  to `GrDiffGroup`s for rendering.
+   *
+   * This includes a number of tasks:
+   *  - 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
+   *    context setting and do not have comments with a group representing the
+   *    "expand context" widget. This may require splitting a `DiffContent` 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: {
 
@@ -80,8 +94,19 @@
         value: 64,
       },
 
-      /** @type {number|undefined} */
+      /** @type {?number} */
       _nextStepHandle: Number,
+      /**
+       * The promise last returned from `process()` while the asynchronous
+       * processing is running - `null` otherwise. Provides a `cancel()`
+       * method that rejects it with `{isCancelled: true}`.
+       *
+       * @type {?Object}
+       */
+      _processPromise: {
+        type: Object,
+        value: null,
+      },
       _isScrolling: Boolean,
     },
 
@@ -109,6 +134,10 @@
      *     processed.
      */
     process(content, isBinary) {
+      // Cancel any still running process() calls, because they append to the
+      // same groups field.
+      this.cancel();
+
       this.groups = [];
       this.push('groups', this._makeFileComments());
 
@@ -116,57 +145,65 @@
       // so finish processing.
       if (isBinary) { return Promise.resolve(); }
 
-      return new Promise(resolve => {
-        const state = {
-          lineNums: {left: 0, right: 0},
-          sectionIndex: 0,
-        };
 
-        content = this._splitCommonGroupsWithComments(content);
+      this._processPromise = util.makeCancelable(
+          new Promise(resolve => {
+            const state = {
+              lineNums: {left: 0, right: 0},
+              sectionIndex: 0,
+            };
 
-        let currentBatch = 0;
-        const nextStep = () => {
-          if (this._isScrolling) {
-            this._nextStepHandle = this.async(nextStep, 100);
-            return;
-          }
-          // If we are done, resolve the promise.
-          if (state.sectionIndex >= content.length) {
-            resolve(this.groups);
-            this._nextStepHandle = undefined;
-            return;
-          }
+            content = this._splitLargeChunks(content);
+            content = this._splitUnchangedChunksWithComments(content);
 
-          // Process the next section and incorporate the result.
-          const result = this._processNext(state, content);
-          for (const group of result.groups) {
-            this.push('groups', group);
-            currentBatch += group.lines.length;
-          }
-          state.lineNums.left += result.lineDelta.left;
-          state.lineNums.right += result.lineDelta.right;
+            let currentBatch = 0;
+            const nextStep = () => {
+              if (this._isScrolling) {
+                this._nextStepHandle = this.async(nextStep, 100);
+                return;
+              }
+              // If we are done, resolve the promise.
+              if (state.sectionIndex >= content.length) {
+                resolve(this.groups);
+                this._nextStepHandle = null;
+                return;
+              }
 
-          // Increment the index and recurse.
-          state.sectionIndex++;
-          if (currentBatch >= this._asyncThreshold) {
-            currentBatch = 0;
-            this._nextStepHandle = this.async(nextStep, 1);
-          } else {
+              // Process the next section and incorporate the result.
+              const result = this._processNext(state, content);
+              for (const group of result.groups) {
+                this.push('groups', group);
+                currentBatch += group.lines.length;
+              }
+              state.lineNums.left += result.lineDelta.left;
+              state.lineNums.right += result.lineDelta.right;
+
+              // Increment the index and recurse.
+              state.sectionIndex++;
+              if (currentBatch >= this._asyncThreshold) {
+                currentBatch = 0;
+                this._nextStepHandle = this.async(nextStep, 1);
+              } else {
+                nextStep.call(this);
+              }
+            };
+
             nextStep.call(this);
-          }
-        };
-
-        nextStep.call(this);
-      });
+          }));
+      return this._processPromise
+          .finally(() => { this._processPromise = null; });
     },
 
     /**
      * Cancel any jobs that are running.
      */
     cancel() {
-      if (this._nextStepHandle !== undefined) {
+      if (this._nextStepHandle != null) {
         this.cancelAsync(this._nextStepHandle);
-        this._nextStepHandle = undefined;
+        this._nextStepHandle = null;
+      }
+      if (this._processPromise) {
+        this._processPromise.cancel();
       }
     },
 
@@ -352,61 +389,79 @@
       return new GrDiffGroup(GrDiffGroup.Type.BOTH, [line]);
     },
 
+
+    /**
+     * Split chunks into smaller chunks of the same kind.
+     *
+     * This is done to prevent doing too much work on the main thread in one
+     * uninterrupted rendering step, which would make the browser unresponsive.
+     *
+     * Note that in the case of unmodified chunks, we only split chunks if the
+     * context is set to file (because otherwise they are split up further down
+     * the processing into the visible and hidden context), and only split it
+     * 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.
+     */
+    _splitLargeChunks(chunks) {
+      const newChunks = [];
+
+      for (const chunk of chunks) {
+        if (!chunk.ab) {
+          for (const group of this._breakdownGroup(chunk)) {
+            newChunks.push(group);
+          }
+          continue;
+        }
+
+        // If the context is set to "whole file", then break down the shared
+        // chunks so they can be rendered incrementally. Note: this is not
+        // 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
+          // group size.
+          newChunks.push({ab: chunk.ab.slice(0, MAX_GROUP_SIZE)});
+          newChunks.push({ab: chunk.ab.slice(MAX_GROUP_SIZE)});
+        } else {
+          newChunks.push(chunk);
+        }
+      }
+      return newChunks;
+    },
+
     /**
      * 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.
      *
-     * @param {?} content The diff content object. (has to be iterable)
-     * @return {!Object} A new diff content object with regions split up.
+     * @param {!Array<!Object>} chunks DiffContents as returned from server.
+     * @return {!Array<!Object>} Finer grained DiffContents.
      */
-    _splitCommonGroupsWithComments(content) {
+    _splitUnchangedChunksWithComments(chunks) {
       const result = [];
       let leftLineNum = 0;
       let rightLineNum = 0;
 
-      // If the context is set to "whole file", then break down the shared
-      // chunks so they can be rendered incrementally. Note: this is not enabled
-      // for any other context preference because manipulating the chunks in
-      // this way violates assumptions by the context grouper logic.
-      if (this.context === -1) {
-        const newContent = [];
-        for (const group of content) {
-          if (group.ab && group.ab.length > MAX_GROUP_SIZE * 2) {
-            // Split large shared groups in two, where the first is the maximum
-            // group size.
-            newContent.push({ab: group.ab.slice(0, MAX_GROUP_SIZE)});
-            newContent.push({ab: group.ab.slice(MAX_GROUP_SIZE)});
-          } else {
-            newContent.push(group);
+      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.a) {
+            leftLineNum += chunk.a.length;
           }
-        }
-        content = newContent;
-      }
-
-      // For each section in the diff.
-      for (let i = 0; i < content.length; i++) {
-        // If it isn't a common group, append it as-is and update line numbers.
-        if (!content[i].ab) {
-          if (content[i].a) {
-            leftLineNum += content[i].a.length;
+          if (chunk.b) {
+            rightLineNum += chunk.b.length;
           }
-          if (content[i].b) {
-            rightLineNum += content[i].b.length;
-          }
-
-          for (const group of this._breakdownGroup(content[i])) {
-            result.push(group);
-          }
-
+          result.push(chunk);
           continue;
         }
 
-        const chunk = content[i].ab;
         let currentChunk = {ab: []};
 
         // For each line in the common group.
-        for (const subChunk of chunk) {
+        for (const line of chunk.ab) {
           leftLineNum++;
           rightLineNum++;
 
@@ -422,10 +477,10 @@
             }
 
             // Add the non-collapse line as its own chunk.
-            result.push({ab: [subChunk]});
+            result.push({ab: [line]});
           } else {
             // Append the current line to the current chunk.
-            currentChunk.ab.push(subChunk);
+            currentChunk.ab.push(line);
           }
         }
 
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 7ccd9f8..186a49e 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
@@ -280,7 +280,6 @@
           left: {1: true},
           right: {10: true},
         };
-        const lineNums = {left: 0, right: 0};
 
         const content = [
           {
@@ -304,7 +303,7 @@
           },
         ];
         const result =
-            element._splitCommonGroupsWithComments(content, lineNums);
+            element._splitUnchangedChunksWithComments(content);
         assert.deepEqual(result, [
           {
             ab: ['Copyright (C) 2015 The Android Open Source Project'],
@@ -337,28 +336,25 @@
         ]);
       });
 
-      test('breaks-down shared chunks w/ whole-file', () => {
+      test('breaks down shared chunks w/ whole-file', () => {
         const size = 120 * 2 + 5;
-        const lineNums = {left: 0, right: 0};
         const content = [{
           ab: _.times(size, () => { return `${Math.random()}`; }),
         }];
         element.context = -1;
-        const result =
-            element._splitCommonGroupsWithComments(content, lineNums);
+        const result = element._splitLargeChunks(content);
         assert.equal(result.length, 2);
         assert.deepEqual(result[0].ab, content[0].ab.slice(0, 120));
         assert.deepEqual(result[1].ab, content[0].ab.slice(120));
       });
 
       test('does not break-down shared chunks w/ context', () => {
-        const lineNums = {left: 0, right: 0};
         const content = [{
           ab: _.times(75, () => { return `${Math.random()}`; }),
         }];
         element.context = 4;
         const result =
-            element._splitCommonGroupsWithComments(content, lineNums);
+            element._splitUnchangedChunksWithComments(content);
         assert.deepEqual(result, content);
       });
 
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 7f25b3e..3484693 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,6 +32,7 @@
 
   Polymer({
     is: 'gr-diff-selection',
+    _legacyUndefinedCheck: true,
 
     properties: {
       diff: Object,
@@ -83,7 +84,7 @@
         targetClasses.push(SelectionClass.BLAME);
       } else {
         const commentSelected =
-            this._elementDescendedFromClass(e.target, 'gr-diff-comment');
+            this._elementDescendedFromClass(e.target, 'gr-comment');
         const side = this.diffBuilder.getSideByLineEl(lineEl);
 
         targetClasses.push(side === 'left' ?
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 f34429b8..469a894 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
@@ -36,7 +36,7 @@
           <td class="content">
             <div class="contentText" data-side="left">ba ba</div>
             <div data-side="left">
-              <div class="gr-diff-comment-thread">
+              <div class="comment-thread">
                 <div class="gr-formatted-text message">
                   <span id="output" class="gr-linked-text">This is a comment</span>
                 </div>
@@ -58,7 +58,7 @@
           <td class="content">
             <div class="contentText" data-side="right">more more more</div>
             <div data-side="right">
-              <div class="gr-diff-comment-thread">
+              <div class="comment-thread">
                 <div class="gr-formatted-text message">
                   <span id="output" class="gr-linked-text">This is a comment on the right</span>
                 </div>
@@ -72,7 +72,7 @@
           <td class="content">
             <div class="contentText" data-side="left">ga ga</div>
             <div data-side="left">
-              <div class="gr-diff-comment-thread">
+              <div class="comment-thread">
                 <div class="gr-formatted-text message">
                   <span id="output" class="gr-linked-text">This is <a>a</a> different comment 💩 unicode is fun</span>
                 </div>
@@ -87,7 +87,7 @@
           <td class="content">
             <div class="contentText" data-side="left">ga ga</div>
             <div data-side="left">
-              <div class="gr-diff-comment-thread">
+              <div class="comment-thread">
                 <textarea data-side="right">test for textarea copying</textarea>
               </div>
             </div>
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 ab1d668..104aaa8 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
@@ -289,11 +289,11 @@
             <span>Diff view:</span>
             <gr-diff-mode-selector
                 id="modeSelect"
-                save-on-change="[[_loggedIn]]"
+                save-on-change="[[!_diffPrefsDisabled]]"
                 mode="{{changeViewState.diffMode}}"></gr-diff-mode-selector>
           </div>
           <span id="diffPrefsContainer"
-              hidden$="[[_computePrefsButtonHidden(_prefs, _loggedIn)]]" hidden>
+              hidden$="[[_computePrefsButtonHidden(_prefs, _diffPrefsDisabled)]]" hidden>
             <span class="preferences desktop">
               <gr-button
                   link
@@ -336,10 +336,10 @@
         patch-range="[[_patchRange]]"
         path="[[_path]]"
         prefs="[[_prefs]]"
-        project-config="[[_projectConfig]]"
         project-name="[[_change.project]]"
         view-mode="[[_diffMode]]"
         is-blame-loaded="{{_isBlameLoaded}}"
+        on-comment-anchor-tap="_onLineSelected"
         on-line-selected="_onLineSelected">
     </gr-diff-host>
     <gr-diff-preferences-dialog
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 4cec917..7e6c381 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,6 +35,7 @@
 
   Polymer({
     is: 'gr-diff-view',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the title of the page should change.
@@ -69,6 +70,14 @@
         value() { return {}; },
         observer: '_changeViewStateChanged',
       },
+      disableDiffPrefs: {
+        type: Boolean,
+        value: false,
+      },
+      _diffPrefsDisabled: {
+        type: Boolean,
+        computed: '_computeDiffPrefsDisabled(disableDiffPrefs, _loggedIn)',
+      },
       /** @type {?} */
       _patchRange: Object,
       /** @type {?} */
@@ -217,7 +226,7 @@
         [this.Shortcut.EXPAND_ALL_DIFF_CONTEXT]: '_handleExpandAllDiffContext',
         [this.Shortcut.NEXT_UNREVIEWED_FILE]: '_handleNextUnreviewedFile',
 
-        // Final two are actually handled by gr-diff-comment-thread.
+        // Final two are actually handled by gr-comment-thread.
         [this.Shortcut.EXPAND_ALL_COMMENT_THREADS]: null,
         [this.Shortcut.COLLAPSE_ALL_COMMENT_THREADS]: null,
       };
@@ -461,6 +470,7 @@
     _handleCommaKey(e) {
       if (this.shouldSuppressKeyboardShortcut(e) ||
           this.modifierPressed(e)) { return; }
+      if (this._diffPrefsDisabled) { return; }
 
       e.preventDefault();
       this.$.diffPreferencesDialog.open();
@@ -807,8 +817,8 @@
           (unresolvedString ? `${unresolvedString}` : '');
     },
 
-    _computePrefsButtonHidden(prefs, loggedIn) {
-      return !loggedIn || !prefs;
+    _computePrefsButtonHidden(prefs, prefsDisabled) {
+      return prefsDisabled || !prefs;
     },
 
     _handleFileChange(e) {
@@ -1073,6 +1083,10 @@
       this.$.diffHost.expandAllContext();
     },
 
+    _computeDiffPrefsDisabled(disableDiffPrefs, loggedIn) {
+      return disableDiffPrefs || !loggedIn;
+    },
+
     _handleNextUnreviewedFile(e) {
       if (this.shouldSuppressKeyboardShortcut(e)) { return; }
       this._setReviewed(true);
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 b5aa579..b08aee6 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
@@ -136,6 +136,7 @@
       element._fileList = ['chell.go', 'glados.txt', 'wheatley.md'];
       element._path = 'glados.txt';
       element.changeViewState.selectedFileIndex = 1;
+      element._loggedIn = true;
 
       const diffNavStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
       const changeNavStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
@@ -178,6 +179,10 @@
       MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
       assert(showPrefsStub.calledOnce);
 
+      element.disableDiffPrefs = true;
+      MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
+      assert(showPrefsStub.calledOnce);
+
       let scrollStub = sandbox.stub(element.$.cursor, 'moveToNextChunk');
       MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
       assert(scrollStub.calledOnce);
@@ -344,23 +349,39 @@
           PARENT), 'Should navigate to /c/42/1');
     });
 
-    test('Diff preferences hidden when no prefs or logged out', () => {
-      element._loggedIn = false;
-      flushAsynchronousOperations();
-      assert.isTrue(element.$.diffPrefsContainer.hidden);
+    suite('diff prefs hidden', () => {
+      test('when no prefs or logged out', () => {
+        element.disableDiffPrefs = false;
+        element._loggedIn = false;
+        flushAsynchronousOperations();
+        assert.isTrue(element.$.diffPrefsContainer.hidden);
 
-      element._loggedIn = true;
-      flushAsynchronousOperations();
-      assert.isTrue(element.$.diffPrefsContainer.hidden);
+        element._loggedIn = true;
+        flushAsynchronousOperations();
+        assert.isTrue(element.$.diffPrefsContainer.hidden);
 
-      element._loggedIn = false;
-      element._prefs = {font_size: '12'};
-      flushAsynchronousOperations();
-      assert.isTrue(element.$.diffPrefsContainer.hidden);
+        element._loggedIn = false;
+        element._prefs = {font_size: '12'};
+        flushAsynchronousOperations();
+        assert.isTrue(element.$.diffPrefsContainer.hidden);
 
-      element._loggedIn = true;
-      flushAsynchronousOperations();
-      assert.isFalse(element.$.diffPrefsContainer.hidden);
+        element._loggedIn = true;
+        flushAsynchronousOperations();
+        assert.isFalse(element.$.diffPrefsContainer.hidden);
+      });
+
+      test('when disableDiffPrefs is set', () => {
+        element._loggedIn = true;
+        element._prefs = {font_size: '12'};
+        element.disableDiffPrefs = false;
+        flushAsynchronousOperations();
+
+        assert.isFalse(element.$.diffPrefsContainer.hidden);
+        element.disableDiffPrefs = true;
+        flushAsynchronousOperations();
+
+        assert.isTrue(element.$.diffPrefsContainer.hidden);
+      });
     });
 
     test('prefsButton opens gr-diff-preferences', () => {
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 88fcd0e..dd69724 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
@@ -20,11 +20,20 @@
   // Prevent redefinition.
   if (window.GrDiffGroup) { return; }
 
+  /**
+   * A chunk of the diff that should be rendered together.
+   */
   function GrDiffGroup(type, opt_lines) {
     this.type = type;
+
+    /** @type{!Array<!GrDiffLine>} */
     this.lines = [];
+    /** @type{!Array<!GrDiffLine>} */
     this.adds = [];
+    /** @type{!Array<!GrDiffLine>} */
     this.removes = [];
+
+    /** @type{boolean|undefined} */
     this.dueToRebase = undefined;
 
     this.lineRange = {
@@ -40,8 +49,13 @@
   GrDiffGroup.prototype.element = null;
 
   GrDiffGroup.Type = {
+    /** Unchanged context. */
     BOTH: 'both',
+
+    /** A widget used to show more context. */
     CONTEXT_CONTROL: 'contextControl',
+
+    /** Added, removed or modified chunk. */
     DELTA: 'delta',
   };
 
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 9613df8..8cf0e3f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -20,7 +20,6 @@
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../gr-diff-builder/gr-diff-builder.html">
-<link rel="import" href="../gr-diff-comment-thread/gr-diff-comment-thread.html">
 <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">
@@ -36,6 +35,11 @@
       :host(.no-left) .sideBySide ::content .right:not([data-value]) + td {
         display: none;
       }
+      .thread-group, ::slotted(*) .thread-group {
+        display: block;
+        max-width: var(--content-width, 80ch);
+        white-space: normal;
+      }
       .diffContainer {
         display: flex;
         font-family: var(--monospace-font-family);
@@ -282,10 +286,10 @@
         <gr-diff-highlight
             id="highlights"
             logged-in="[[loggedIn]]"
-            comments="{{comments}}">
+            comment-ranges="{{_commentRanges}}">
           <gr-diff-builder
               id="diffBuilder"
-              comments="[[comments]]"
+              comment-ranges="[[_commentRanges]]"
               project-name="[[projectName]]"
               diff="[[diff]]"
               diff-path="[[path]]"
@@ -295,10 +299,7 @@
               line-wrapping="[[lineWrapping]]"
               is-image-diff="[[isImageDiff]]"
               base-image="[[baseImage]]"
-              revision-image="[[revisionImage]]"
-              parent-index="[[_parentIndex]]"
-              create-comment-fn="[[_createThreadGroupFn]]"
-              line-of-interest="[[lineOfInterest]]">
+              revision-image="[[revisionImage]]">
             <table
                 id="diffTable"
                 class$="[[_diffTableClass]]"
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 7a670d3..88ba3ec 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -35,10 +35,63 @@
     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
+   * values.
+   *
+   * @param {Gerrit.Range=} a range 1
+   * @param {Gerrit.Range=} b range 2
+   * @return {boolean}
+   */
+  Gerrit.rangesEqual = function(a, b) {
+    if (!a && !b) { return true; }
+    if (!a || !b) { return false; }
+    return a.start_line === b.start_line &&
+        a.start_character === b.start_character &&
+        a.end_line === b.end_line &&
+        a.end_character === b.end_character;
+  };
+
+  function isThreadEl(node) {
+    return node.nodeType === Node.ELEMENT_NODE &&
+        node.classList.contains('comment-thread');
+  }
+
+  /**
+   * Turn a slot element into the corresponding content element.
+   * Slots are only fully supported in Polymer 2 - in Polymer 1, they are
+   * replaced with content elements during template parsing. This conversion is
+   * not applied for imperatively created slot elements, so this method
+   * implements the same behavior as the template parsing for imperative slots.
+   */
+  Gerrit.slotToContent = function(slot) {
+    const content = document.createElement('content');
+    content.name = slot.name;
+    content.setAttribute('select', `[slot='${slot.name}']`);
+    return content;
+  };
+
   const COMMIT_MSG_PATH = '/COMMIT_MSG';
   /**
    * 72 is the inofficial length standard for git commit messages.
@@ -49,8 +102,11 @@
    */
   const COMMIT_MSG_LINE_LENGTH = 72;
 
+  const RENDER_DIFF_TABLE_DEBOUNCE_NAME = 'renderDiffTable';
+
   Polymer({
     is: 'gr-diff',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the user selects a line.
@@ -65,15 +121,16 @@
      */
 
     /**
-     * Fired when a comment is saved or discarded
+     * Fired when a comment is created
      *
-     * @event diff-comments-modified
+     * @event create-comment
      */
 
     /**
-     * Fired when a draft is added or edited.
+     * Fired when rendering, including syntax highlighting, is done. Also fired
+     * when no rendering can be done because required preferences are not set.
      *
-     * @event draft-interaction
+     * @event render
      */
 
     properties: {
@@ -92,10 +149,6 @@
         type: Object,
         observer: '_prefsObserver',
       },
-      projectConfig: {
-        type: Object,
-        observer: '_projectConfigChanged',
-      },
       projectName: String,
       displayLine: {
         type: Boolean,
@@ -110,9 +163,10 @@
         reflectToAttribute: true,
       },
       noRenderOnPrefsChange: Boolean,
-      comments: {
-        type: Object,
-        value: {left: [], right: []},
+      /** @type {!Array<!Gerrit.HoveredRange>} */
+      _commentRanges: {
+        type: Array,
+        value: () => [],
       },
       lineWrapping: {
         type: Boolean,
@@ -125,14 +179,7 @@
         observer: '_viewModeObserver',
       },
 
-      /**
-       * Special line number which should not be collapsed into a shared region.
-       *
-       * @type {{
-       *  number: number,
-       *  leftSide: {boolean}
-       * }|null}
-       */
+      /** @type {?Defs.LineOfInterest} */
       lineOfInterest: Object,
 
       loading: {
@@ -191,27 +238,34 @@
         observer: '_blameChanged',
       },
 
-      _parentIndex: {
-        type: Number,
-        computed: '_computeParentIndex(patchRange.*)',
-      },
+      parentIndex: Number,
 
       _newlineWarning: {
         type: String,
         computed: '_computeNewlineWarning(diff)',
       },
 
-      /**
-       * @type {function(number, boolean, !string)}
-       */
-      _createThreadGroupFn: {
-        type: Function,
-        value() {
-          return this._createCommentThreadGroup.bind(this);
-        },
-      },
-
       _diffLength: Number,
+
+      /**
+       * Observes comment nodes added or removed after the initial render.
+       * Can be used to unregister when the entire diff is (re-)rendered or upon
+       * detachment.
+       *
+       * @type {?PolymerDomApi.ObserveHandle}
+       */
+      _incrementalNodeObserver: Object,
+
+      /**
+       * Observes comment nodes added or removed at any point.
+       * Can be used to unregister upon detachment.
+       *
+       * @type {?PolymerDomApi.ObserveHandle}
+       */
+      _nodeObserver: Object,
+
+      /** Set by Polymer. */
+      isAttached: Boolean,
     },
 
     behaviors: [
@@ -219,15 +273,135 @@
     ],
 
     listeners: {
-      'comment-discard': '_handleCommentDiscard',
-      'comment-update': '_handleCommentUpdate',
-      'comment-save': '_handleCommentSave',
-      'create-comment': '_handleCreateComment',
+      'create-range-comment': '_handleCreateRangeComment',
+      'render-content': '_handleRenderContent',
+    },
+
+    observers: [
+      '_enableSelectionObserver(loggedIn, isAttached)',
+    ],
+
+    attached() {
+      this._observeNodes();
+    },
+
+    detached() {
+      this._unobserveIncrementalNodes();
+      this._unobserveNodes();
+    },
+
+    _enableSelectionObserver(loggedIn, isAttached) {
+      if (loggedIn && isAttached) {
+        this.listen(document, 'selectionchange', '_handleSelectionChange');
+        this.listen(document, 'mouseup', '_handleMouseUp');
+      } else {
+        this.unlisten(document, 'selectionchange', '_handleSelectionChange');
+        this.unlisten(document, 'mouseup', '_handleMouseUp');
+      }
+    },
+
+    _handleSelectionChange() {
+      // Because of shadow DOM selections, we handle the selectionchange here,
+      // and pass the shadow DOM selection into gr-diff-highlight, where the
+      // corresponding range is determined and normalized.
+      const selection = this._getShadowOrDocumentSelection();
+      this.$.highlights.handleSelectionChange(selection, false);
+    },
+
+    _handleMouseUp(e) {
+      // To handle double-click outside of text creating comments, we check on
+      // mouse-up if there's a selection that just covers a line change. We
+      // can't do that on selection change since the user may still be dragging.
+      const selection = this._getShadowOrDocumentSelection();
+      this.$.highlights.handleSelectionChange(selection, true);
+    },
+
+    /** Gets the current selection, preferring the shadow DOM selection. */
+    _getShadowOrDocumentSelection() {
+      // When using native shadow DOM, the selection returned by
+      // document.getSelection() cannot reference the actual DOM elements making
+      // up the diff, because they are in the shadow DOM of the gr-diff element.
+      // This takes the shadow DOM selection if one exists.
+      return this.root.getSelection ?
+        this.root.getSelection() :
+        document.getSelection();
+    },
+
+    _observeNodes() {
+      this._nodeObserver = Polymer.dom(this).observeNodes(info => {
+        const addedThreadEls = info.addedNodes.filter(isThreadEl);
+        const removedThreadEls = info.removedNodes.filter(isThreadEl);
+        this._updateRanges(addedThreadEls, removedThreadEls);
+        this._redispatchHoverEvents(addedThreadEls);
+      });
+    },
+
+    _updateRanges(addedThreadEls, removedThreadEls) {
+      function commentRangeFromThreadEl(threadEl) {
+        const side = threadEl.getAttribute('comment-side');
+        const range = JSON.parse(threadEl.getAttribute('range'));
+        return {side, range, hovering: false};
+      }
+
+      const addedCommentRanges = addedThreadEls
+          .map(commentRangeFromThreadEl)
+          .filter(({range}) => range);
+      const removedCommentRanges = removedThreadEls
+          .map(commentRangeFromThreadEl)
+          .filter(({range}) => range);
+      for (const removedCommentRange of removedCommentRanges) {
+        const i = this._commentRanges.findIndex(commentRange => {
+          return commentRange.side === removedCommentRange.side &&
+              Gerrit.rangesEqual(commentRange.range, removedCommentRange.range);
+        });
+        this.splice('_commentRanges', i, 1);
+      }
+      this.push('_commentRanges', ...addedCommentRanges);
+    },
+
+    /**
+     * The key locations based on the comments and line of interests,
+     * where lines should not be collapsed.
+     *
+     * @return {{left: Object<(string|number), boolean>,
+     *     right: Object<(string|number), boolean>}}
+     */
+    _computeKeyLocations() {
+      const keyLocations = {left: {}, right: {}};
+      if (this.lineOfInterest) {
+        const side = this.lineOfInterest.leftSide ? 'left' : 'right';
+        keyLocations[side][this.lineOfInterest.number] = true;
+      }
+      const threadEls = Polymer.dom(this).getEffectiveChildNodes()
+          .filter(isThreadEl);
+
+      for (const threadEl of threadEls) {
+        const commentSide = threadEl.getAttribute('comment-side');
+        const lineNum = Number(threadEl.getAttribute('line-num')) ||
+            GrDiffLine.FILE;
+        keyLocations[commentSide][lineNum] = true;
+      }
+      return keyLocations;
+    },
+
+    // Dispatch events that are handled by the gr-diff-highlight.
+    _redispatchHoverEvents(addedThreadEls) {
+      for (const threadEl of addedThreadEls) {
+        threadEl.addEventListener('mouseenter', () => {
+          threadEl.dispatchEvent(
+              new CustomEvent('comment-thread-mouseenter', {bubbles: true}));
+        });
+        threadEl.addEventListener('mouseleave', () => {
+          threadEl.dispatchEvent(
+              new CustomEvent('comment-thread-mouseleave', {bubbles: true}));
+        });
+      }
     },
 
     /** Cancel any remaining diff builder rendering work. */
     cancel() {
       this.$.diffBuilder.cancel();
+      this.cancelDebouncer(RENDER_DIFF_TABLE_DEBOUNCE_NAME);
     },
 
     /** @return {!Array<!HTMLElement>} */
@@ -236,7 +410,9 @@
         return [];
       }
 
-      return Polymer.dom(this.root).querySelectorAll('.diff-row');
+      // Polymer2: querySelectorAll returns NodeList instead of Array.
+      return Array.from(
+          Polymer.dom(this.root).querySelectorAll('.diff-row'));
     },
 
     /** @return {boolean} */
@@ -257,22 +433,6 @@
       }
     },
 
-    _handleCommentSaveOrDiscard() {
-      this.dispatchEvent(new CustomEvent('diff-comments-modified',
-          {bubbles: true}));
-    },
-
-    /** @return {!Array<!HTMLElement>} */
-    getThreadEls() {
-      let threads = [];
-      const threadGroupEls = Polymer.dom(this.root)
-          .querySelectorAll('gr-diff-comment-thread-group');
-      for (const threadGroupEl of threadGroupEls) {
-        threads = threads.concat(threadGroupEl.threadEls);
-      }
-      return threads;
-    },
-
     /** @return {string} */
     _computeContainerClass(loggedIn, viewMode, displayLine) {
       const classes = ['diffContainer'];
@@ -337,10 +497,10 @@
       this._createComment(el, lineNum);
     },
 
-    _handleCreateComment(e) {
+    _handleCreateRangeComment(e) {
       const range = e.detail.range;
       const side = e.detail.side;
-      const lineNum = range.endLine;
+      const lineNum = range.end_line;
       const lineEl = this.$.diffBuilder.getLineElByNumber(lineNum, side);
 
       if (this._isValidElForComment(lineEl)) {
@@ -374,88 +534,52 @@
 
     /**
      * @param {!Object} lineEl
-     * @param {number=} opt_lineNum
-     * @param {string=} opt_side
-     * @param {!Object=} opt_range
+     * @param {number=} lineNum
+     * @param {string=} side
+     * @param {!Object=} range
      */
-    _createComment(lineEl, opt_lineNum, opt_side, opt_range) {
-      this.dispatchEvent(new CustomEvent('draft-interaction', {bubbles: true}));
+    _createComment(lineEl, lineNum=undefined, side=undefined, range=undefined) {
       const contentText = this.$.diffBuilder.getContentByLineEl(lineEl);
       const contentEl = contentText.parentElement;
-      const side = opt_side ||
+      side = side ||
           this._getCommentSideByLineAndContent(lineEl, contentEl);
-      const patchNum = this._getPatchNumByLineAndContent(lineEl, contentEl);
+      const patchForNewThreads = this._getPatchNumByLineAndContent(
+          lineEl, contentEl);
       const isOnParent =
-        this._getIsParentCommentByLineAndContent(lineEl, contentEl);
-      const threadEl = this._getOrCreateThread(contentEl, patchNum,
-          side, isOnParent, opt_range);
-      threadEl.addOrEditDraft(opt_lineNum, opt_range);
-    },
-
-    /**
-     * Fetch the thread group at the given range, or the range-less thread
-     * on the line if no range is provided.
-     *
-     * @param {!Object} threadGroupEl
-     * @param {string} commentSide
-     * @param {!Object=} opt_range
-     * @return {!Object}
-     */
-    _getThread(threadGroupEl, commentSide, opt_range) {
-      return threadGroupEl.getThread(commentSide, opt_range);
+          this._getIsParentCommentByLineAndContent(lineEl, contentEl);
+      this.dispatchEvent(new CustomEvent('create-comment', {
+        bubbles: true,
+        detail: {
+          lineNum,
+          side,
+          patchNum: patchForNewThreads,
+          isOnParent,
+          range,
+        },
+      }));
     },
 
     _getThreadGroupForLine(contentEl) {
-      return contentEl.querySelector('gr-diff-comment-thread-group');
+      return contentEl.querySelector('.thread-group');
     },
 
     /**
-     * Gets or creates a comment thread for a specific spot on a diff.
-     * May include a range, if the comment is a range comment.
+     * Gets or creates a comment thread group for a specific line and side on a
+     * diff.
      *
      * @param {!Object} contentEl
-     * @param {number} patchNum
-     * @param {string} commentSide
-     * @param {boolean} isOnParent
-     * @param {!Object=} opt_range
-     * @return {!Object}
+     * @param {!Gerrit.DiffSide} commentSide
+     * @return {!Node}
      */
-    _getOrCreateThread(contentEl, patchNum, commentSide,
-        isOnParent, opt_range) {
+    _getOrCreateThreadGroup(contentEl, commentSide) {
       // Check if thread group exists.
       let threadGroupEl = this._getThreadGroupForLine(contentEl);
       if (!threadGroupEl) {
-        threadGroupEl = this._createCommentThreadGroup(patchNum, isOnParent,
-            commentSide);
+        threadGroupEl = document.createElement('div');
+        threadGroupEl.className = 'thread-group';
+        threadGroupEl.setAttribute('data-side', commentSide);
         contentEl.appendChild(threadGroupEl);
       }
-
-      let threadEl = this._getThread(threadGroupEl, commentSide, opt_range);
-
-      if (!threadEl) {
-        threadGroupEl.addNewThread(commentSide, opt_range);
-        Polymer.dom.flush();
-        threadEl = this._getThread(threadGroupEl, commentSide, opt_range);
-      }
-      return threadEl;
-    },
-
-    /**
-     * @param {number} patchNum
-     * @param {boolean} isOnParent
-     * @param {!string} commentSide
-     * @return {!Object}
-     */
-    _createCommentThreadGroup(patchNum, isOnParent, commentSide) {
-      const threadGroupEl =
-          document.createElement('gr-diff-comment-thread-group');
-      threadGroupEl.changeNum = this.changeNum;
-      threadGroupEl.commentSide = commentSide;
-      threadGroupEl.patchForNewThreads = patchNum;
-      threadGroupEl.path = this.path;
-      threadGroupEl.isOnParent = isOnParent;
-      threadGroupEl.projectName = this.projectName;
-      threadGroupEl.parentIndex = this._parentIndex;
       return threadGroupEl;
     },
 
@@ -506,75 +630,6 @@
       return side;
     },
 
-    _handleCommentDiscard(e) {
-      const comment = e.detail.comment;
-      this._removeComment(comment);
-      this._handleCommentSaveOrDiscard();
-    },
-
-    _removeComment(comment) {
-      const side = comment.__commentSide;
-      this._removeCommentFromSide(comment, side);
-    },
-
-    _handleCommentSave(e) {
-      const comment = e.detail.comment;
-      const side = e.detail.comment.__commentSide;
-      const idx = this._findDraftIndex(comment, side);
-      this.set(['comments', side, idx], comment);
-      this._handleCommentSaveOrDiscard();
-    },
-
-    /**
-     * Closure annotation for Polymer.prototype.push is off. Submitted PR:
-     * https://github.com/Polymer/polymer/pull/4776
-     * but for not supressing annotations.
-     *
-     * @suppress {checkTypes} */
-    _handleCommentUpdate(e) {
-      const comment = e.detail.comment;
-      const side = e.detail.comment.__commentSide;
-      let idx = this._findCommentIndex(comment, side);
-      if (idx === -1) {
-        idx = this._findDraftIndex(comment, side);
-      }
-      if (idx !== -1) { // Update draft or comment.
-        this.set(['comments', side, idx], comment);
-      } else { // Create new draft.
-        this.push(['comments', side], comment);
-      }
-    },
-
-    _removeCommentFromSide(comment, side) {
-      let idx = this._findCommentIndex(comment, side);
-      if (idx === -1) {
-        idx = this._findDraftIndex(comment, side);
-      }
-      if (idx !== -1) {
-        this.splice('comments.' + side, idx, 1);
-      }
-    },
-
-    /** @return {number} */
-    _findCommentIndex(comment, side) {
-      if (!comment.id || !this.comments[side]) {
-        return -1;
-      }
-      return this.comments[side].findIndex(item => {
-        return item.id === comment.id;
-      });
-    },
-
-    /** @return {number} */
-    _findDraftIndex(comment, side) {
-      if (!comment.__draftID || !this.comments[side]) {
-        return -1;
-      }
-      return this.comments[side].findIndex(item => {
-        return item.__draftID === comment.__draftID;
-      });
-    },
-
     _prefsObserver(newPrefs, oldPrefs) {
       // Scan the preference objects one level deep to see if they differ.
       let differ = !oldPrefs;
@@ -641,19 +696,35 @@
 
       this.updateStyles(stylesToUpdate);
 
-      if (this.diff && this.comments && !this.noRenderOnPrefsChange) {
-        this._renderDiffTable();
+      if (this.diff && !this.noRenderOnPrefsChange) {
+        this._debounceRenderDiffTable();
       }
     },
 
     _diffChanged(newValue) {
       if (newValue) {
         this._diffLength = this.$.diffBuilder.getDiffLength();
-        this._renderDiffTable();
+        this._debounceRenderDiffTable();
       }
     },
 
+    /**
+     * When called multiple times from the same microtask, will call
+     * _renderDiffTable only once, in the next microtask, unless it is cancelled
+     * before that microtask runs.
+     *
+     * This should be used instead of calling _renderDiffTable directly to
+     * render the diff in response to an input change, because there may be
+     * multiple inputs changing in the same microtask, but we only want to
+     * render once.
+     */
+    _debounceRenderDiffTable() {
+      this.debounce(
+          RENDER_DIFF_TABLE_DEBOUNCE_NAME, () => this._renderDiffTable());
+    },
+
     _renderDiffTable() {
+      this._unobserveIncrementalNodes();
       if (!this.prefs) {
         this.dispatchEvent(new CustomEvent('render', {bubbles: true}));
         return;
@@ -667,7 +738,55 @@
       }
 
       this._showWarning = false;
-      this.$.diffBuilder.render(this.comments, this._getBypassPrefs());
+
+      const keyLocations = this._computeKeyLocations();
+      this.$.diffBuilder.render(keyLocations, this._getBypassPrefs())
+          .then(() => {
+            this.dispatchEvent(
+                new CustomEvent('render', {bubbles: true}));
+          });
+    },
+
+    _handleRenderContent() {
+      this._incrementalNodeObserver = Polymer.dom(this).observeNodes(info => {
+        const addedThreadEls = info.addedNodes.filter(isThreadEl);
+        // Removed nodes do not need to be handled because all this code does is
+        // adding a slot for the added thread elements, and the extra slots do
+        // 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.
+        for (const threadEl of addedThreadEls) {
+          const lineNumString = threadEl.getAttribute('line-num') || 'FILE';
+          const commentSide = threadEl.getAttribute('comment-side');
+          const lineEl = this.$.diffBuilder.getLineElByNumber(
+              lineNumString, commentSide);
+          const contentText = this.$.diffBuilder.getContentByLineEl(lineEl);
+          const contentEl = contentText.parentElement;
+          const threadGroupEl = this._getOrCreateThreadGroup(
+              contentEl, commentSide);
+          // Create a slot for the thread and attach it to the thread group.
+          // The Polyfill has some bugs and this only works if the slot is
+          // attached to the group after the group is attached to the DOM.
+          // The thread group may already have a slot with the right name, but
+          // that is okay because the first matching slot is used and the rest
+          // are ignored.
+          const slot = document.createElement('slot');
+          slot.name = threadEl.getAttribute('slot');
+          Polymer.dom(threadGroupEl).appendChild(Gerrit.slotToContent(slot));
+        }
+      });
+    },
+
+    _unobserveIncrementalNodes() {
+      if (this._incrementalNodeObserver) {
+        Polymer.dom(this).unobserveNodes(this._incrementalNodeObserver);
+      }
+    },
+
+    _unobserveNodes() {
+      if (this._nodeObserver) {
+        Polymer.dom(this).unobserveNodes(this._nodeObserver);
+      }
     },
 
     /**
@@ -681,16 +800,10 @@
     },
 
     clearDiffContent() {
+      this._unobserveIncrementalNodes();
       this.$.diffTable.innerHTML = null;
     },
 
-    _projectConfigChanged(projectConfig) {
-      const threadEls = this.getThreadEls();
-      for (let i = 0; i < threadEls.length; i++) {
-        threadEls[i].projectConfig = projectConfig;
-      }
-    },
-
     /** @return {!Array} */
     _computeDiffHeaderItems(diffInfoRecord) {
       const diffInfo = diffInfoRecord.base;
@@ -711,12 +824,12 @@
 
     _handleFullBypass() {
       this._safetyBypass = FULL_CONTEXT;
-      this._renderDiffTable();
+      this._debounceRenderDiffTable();
     },
 
     _handleLimitedBypass() {
       this._safetyBypass = LIMITED_CONTEXT;
-      this._renderDiffTable();
+      this._debounceRenderDiffTable();
     },
 
     /** @return {string} */
@@ -732,16 +845,6 @@
       return errorMessage ? 'showError' : '';
     },
 
-    /**
-     * @return {number|null}
-     */
-    _computeParentIndex(patchRangeRecord) {
-      if (!this.isMergeParent(patchRangeRecord.base.basePatchNum)) {
-        return null;
-      }
-      return this.getParentIndex(patchRangeRecord.base.basePatchNum);
-    },
-
     expandAllContext() {
       this._handleFullBypass();
     },
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 ec71563..42f098b 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
@@ -40,6 +40,8 @@
     let element;
     let sandbox;
 
+    const MINIMAL_PREFS = {tab_size: 2, line_length: 80};
+
     setup(() => {
       sandbox = sinon.sandbox.create();
     });
@@ -48,6 +50,30 @@
       sandbox.restore();
     });
 
+    suite('selectionchange event handling', () => {
+      const emulateSelection = function() {
+        document.dispatchEvent(new CustomEvent('selectionchange'));
+      };
+
+      setup(() => {
+        element = fixture('basic');
+        sandbox.stub(element.$.highlights, 'handleSelectionChange');
+      });
+
+      test('enabled if logged in', () => {
+        element.loggedIn = true;
+        emulateSelection();
+        assert.isTrue(element.$.highlights.handleSelectionChange.called);
+      });
+
+      test('ignored if logged out', () => {
+        element.loggedIn = false;
+        emulateSelection();
+        assert.isFalse(element.$.highlights.handleSelectionChange.called);
+      });
+    });
+
+
     test('cancel', () => {
       element = fixture('basic');
       const cancelStub = sandbox.stub(element.$.diffBuilder, 'cancel');
@@ -57,14 +83,14 @@
 
     test('line limit with line_wrapping', () => {
       element = fixture('basic');
-      element.prefs = {line_wrapping: true, line_length: 80, tab_size: 2};
+      element.prefs = Object.assign({}, MINIMAL_PREFS, {line_wrapping: true});
       flushAsynchronousOperations();
       assert.equal(element.customStyle['--line-limit'], '80ch');
     });
 
     test('line limit without line_wrapping', () => {
       element = fixture('basic');
-      element.prefs = {line_wrapping: false, line_length: 80, tab_size: 2};
+      element.prefs = Object.assign({}, MINIMAL_PREFS, {line_wrapping: false});
       flushAsynchronousOperations();
       assert.isNotOk(element.customStyle['--line-limit']);
     });
@@ -192,118 +218,8 @@
             element.$$('.diffContainer').classList.contains('displayLine'));
       });
 
-      test('remove comment', () => {
-        element.comments = {
-          meta: {
-            changeNum: '42',
-            patchRange: {
-              basePatchNum: 'PARENT',
-              patchNum: 3,
-            },
-            path: '/path/to/foo',
-            projectConfig: {foo: 'bar'},
-          },
-          left: [
-            {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
-            {id: 'bc2', side: 'PARENT', __commentSide: 'left'},
-            {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
-            {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
-          ],
-          right: [
-            {id: 'c1', __commentSide: 'right'},
-            {id: 'c2', __commentSide: 'right'},
-            {id: 'd1', __draft: true, __commentSide: 'right'},
-            {id: 'd2', __draft: true, __commentSide: 'right'},
-          ],
-        };
-
-        element._removeComment({});
-        // Using JSON.stringify because Safari 9.1 (11601.5.17.1) doesn’t seem
-        // to believe that one object deepEquals another even when they do :-/.
-        assert.equal(JSON.stringify(element.comments), JSON.stringify({
-          meta: {
-            changeNum: '42',
-            patchRange: {
-              basePatchNum: 'PARENT',
-              patchNum: 3,
-            },
-            path: '/path/to/foo',
-            projectConfig: {foo: 'bar'},
-          },
-          left: [
-            {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
-            {id: 'bc2', side: 'PARENT', __commentSide: 'left'},
-            {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
-            {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
-          ],
-          right: [
-            {id: 'c1', __commentSide: 'right'},
-            {id: 'c2', __commentSide: 'right'},
-            {id: 'd1', __draft: true, __commentSide: 'right'},
-            {id: 'd2', __draft: true, __commentSide: 'right'},
-          ],
-        }));
-
-        element._removeComment({id: 'bc2', side: 'PARENT',
-          __commentSide: 'left'});
-        assert.deepEqual(element.comments, {
-          meta: {
-            changeNum: '42',
-            patchRange: {
-              basePatchNum: 'PARENT',
-              patchNum: 3,
-            },
-            path: '/path/to/foo',
-            projectConfig: {foo: 'bar'},
-          },
-          left: [
-            {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
-            {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
-            {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
-          ],
-          right: [
-            {id: 'c1', __commentSide: 'right'},
-            {id: 'c2', __commentSide: 'right'},
-            {id: 'd1', __draft: true, __commentSide: 'right'},
-            {id: 'd2', __draft: true, __commentSide: 'right'},
-          ],
-        });
-
-        element._removeComment({id: 'd2', __commentSide: 'right'});
-        assert.deepEqual(element.comments, {
-          meta: {
-            changeNum: '42',
-            patchRange: {
-              basePatchNum: 'PARENT',
-              patchNum: 3,
-            },
-            path: '/path/to/foo',
-            projectConfig: {foo: 'bar'},
-          },
-          left: [
-            {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
-            {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
-            {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
-          ],
-          right: [
-            {id: 'c1', __commentSide: 'right'},
-            {id: 'c2', __commentSide: 'right'},
-            {id: 'd1', __draft: true, __commentSide: 'right'},
-          ],
-        });
-      });
-
       test('thread groups', () => {
         const contentEl = document.createElement('div');
-        const commentSide = 'left';
-        const patchNum = 1;
-        const side = 'PARENT';
-        let range = {
-          startLine: 1,
-          startChar: 1,
-          endLine: 1,
-          endChar: 2,
-        };
 
         element.changeNum = 123;
         element.patchRange = {basePatchNum: 1, patchNum: 2};
@@ -311,36 +227,19 @@
 
         const mock = document.createElement('mock-diff-response');
         element.$.diffBuilder._builder = element.$.diffBuilder._getDiffBuilder(
-            mock.diffResponse, {}, {tab_size: 2, line_length: 80});
+            mock.diffResponse, Object.assign({}, MINIMAL_PREFS));
 
         // No thread groups.
         assert.isNotOk(element._getThreadGroupForLine(contentEl));
 
         // A thread group gets created.
-        assert.isOk(element._getOrCreateThread(contentEl,
-            patchNum, commentSide, side));
+        const threadGroupEl = element._getOrCreateThreadGroup(contentEl);
+        assert.isOk(threadGroupEl);
 
-        // Try to fetch a thread with a different range.
-        range = {
-          startLine: 1,
-          startChar: 1,
-          endLine: 1,
-          endChar: 3,
-        };
-
-        assert.isOk(element._getOrCreateThread(
-            contentEl, patchNum, commentSide, side, range));
         // The new thread group can be fetched.
         assert.isOk(element._getThreadGroupForLine(contentEl));
 
-        assert.equal(contentEl.querySelectorAll(
-            'gr-diff-comment-thread-group').length, 1);
-
-        const threadGroup = contentEl.querySelector(
-            'gr-diff-comment-thread-group');
-        const threadLength = Polymer.dom(threadGroup.root).
-            querySelectorAll('gr-diff-comment-thread').length;
-        assert.equal(threadLength, 2);
+        assert.equal(contentEl.querySelectorAll('.thread-group').length, 1);
       });
 
       suite('image diffs', () => {
@@ -359,7 +258,6 @@
           };
 
           element.patchRange = {basePatchNum: 'PARENT', patchNum: 1};
-          element.comments = {left: [], right: []};
           element.isImageDiff = true;
           element.prefs = {
             auto_hide_diff_table_header: true,
@@ -552,7 +450,7 @@
             binary: true,
           };
 
-          element.addEventListener('render', () => {
+          function rendered() {
             // Recognizes that it should be an image diff.
             assert.isTrue(element.isImageDiff);
             assert.instanceOf(
@@ -564,7 +462,9 @@
             assert.isNotOk(leftImage);
             assert.isOk(rightImage);
             done();
-          });
+            element.removeEventListener('render', rendered);
+          }
+          element.addEventListener('render', rendered);
 
           element.revisionImage = mockFile2;
           element.diff = mockDiff;
@@ -587,7 +487,7 @@
             binary: true,
           };
 
-          element.addEventListener('render', () => {
+          function rendered() {
             // Recognizes that it should be an image diff.
             assert.isTrue(element.isImageDiff);
             assert.instanceOf(
@@ -599,7 +499,9 @@
             assert.isOk(leftImage);
             assert.isNotOk(rightImage);
             done();
-          });
+            element.removeEventListener('render', rendered);
+          }
+          element.addEventListener('render', rendered);
 
           element.baseImage = mockFile1;
           element.diff = mockDiff;
@@ -623,7 +525,7 @@
           };
           mockFile1.type = 'image/jpeg-evil';
 
-          element.addEventListener('render', () => {
+          function rendered() {
             // Recognizes that it should be an image diff.
             assert.isTrue(element.isImageDiff);
             assert.instanceOf(
@@ -631,7 +533,9 @@
             const leftImage = element.$.diffTable.querySelector('td.left img');
             assert.isNotOk(leftImage);
             done();
-          });
+            element.removeEventListener('render', rendered);
+          }
+          element.addEventListener('render', rendered);
 
           element.baseImage = mockFile1;
           element.diff = mockDiff;
@@ -685,10 +589,6 @@
         const setupDiff = function() {
           const mock = document.createElement('mock-diff-response');
           element.diff = mock.diffResponse;
-          element.comments = {
-            left: [],
-            right: [],
-          };
           element.prefs = {
             context: 10,
             tab_size: 8,
@@ -787,35 +687,14 @@
             change_type: 'MODIFIED',
             content: [{skip: 66}],
           };
-          element.comments = {
-            meta: {
-              changeNum: '42',
-              patchRange: {
-                basePatchNum: 'PARENT',
-                patchNum: 3,
-              },
-              path: '/path/to/foo',
-              projectConfig: {foo: 'bar'},
-            },
-            left: [
-              {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
-              {id: 'bc2', side: 'PARENT', __commentSide: 'left'},
-              {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
-              {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
-            ],
-            right: [
-              {id: 'c1', __commentSide: 'right'},
-              {id: 'c2', __commentSide: 'right'},
-              {id: 'd1', __draft: true, __commentSide: 'right'},
-              {id: 'd2', __draft: true, __commentSide: 'right'},
-            ],
-          };
+          element.flushDebouncer('renderDiffTable');
         });
 
         test('change in preferences re-renders diff', () => {
           sandbox.stub(element, '_renderDiffTable');
-          element.prefs = {};
-          element.prefs = {time_format: 'HHMM_12'};
+          element.prefs = Object.assign(
+              {}, MINIMAL_PREFS, {time_format: 'HHMM_12'});
+          element.flushDebouncer('renderDiffTable');
           assert.isTrue(element._renderDiffTable.called);
         });
 
@@ -823,91 +702,12 @@
             'noRenderOnPrefsChange', () => {
           sandbox.stub(element, '_renderDiffTable');
           element.noRenderOnPrefsChange = true;
-          element.prefs = {};
-          element.prefs = {time_format: 'HHMM_12'};
+          element.prefs = Object.assign(
+              {}, MINIMAL_PREFS, {time_format: 'HHMM_12'});
+          element.flushDebouncer('renderDiffTable');
           assert.isFalse(element._renderDiffTable.called);
         });
       });
-
-      suite('handle comment-update', () => {
-        setup(() => {
-          element.comments = {
-            meta: {
-              changeNum: '42',
-              patchRange: {
-                basePatchNum: 'PARENT',
-                patchNum: 3,
-              },
-              path: '/path/to/foo',
-              projectConfig: {foo: 'bar'},
-            },
-            left: [
-              {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
-              {id: 'bc2', side: 'PARENT', __commentSide: 'left'},
-              {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
-              {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
-            ],
-            right: [
-              {id: 'c1', __commentSide: 'right'},
-              {id: 'c2', __commentSide: 'right'},
-              {id: 'd1', __draft: true, __commentSide: 'right'},
-              {id: 'd2', __draft: true, __commentSide: 'right'},
-            ],
-          };
-        });
-
-        test('creating a draft', () => {
-          const comment = {__draft: true, __draftID: 'tempID', side: 'PARENT',
-            __commentSide: 'left'};
-          element.fire('comment-update', {comment});
-          assert.include(element.comments.left, comment);
-        });
-
-        test('discarding a draft', () => {
-          const draftID = 'tempID';
-          const id = 'savedID';
-          const comment = {
-            __draft: true,
-            __draftID: draftID,
-            side: 'PARENT',
-            __commentSide: 'left',
-          };
-          const diffCommentsModifiedStub = sandbox.stub();
-          element.addEventListener('diff-comments-modified',
-              diffCommentsModifiedStub);
-          element.comments.left.push(comment);
-          comment.id = id;
-          element.fire('comment-discard', {comment});
-          const drafts = element.comments.left.filter(item => {
-            return item.__draftID === draftID;
-          });
-          assert.equal(drafts.length, 0);
-          assert.isTrue(diffCommentsModifiedStub.called);
-        });
-
-        test('saving a draft', () => {
-          const draftID = 'tempID';
-          const id = 'savedID';
-          const comment = {
-            __draft: true,
-            __draftID: draftID,
-            side: 'PARENT',
-            __commentSide: 'left',
-          };
-          const diffCommentsModifiedStub = sandbox.stub();
-          element.addEventListener('diff-comments-modified',
-              diffCommentsModifiedStub);
-          element.comments.left.push(comment);
-          comment.id = id;
-          element.fire('comment-save', {comment});
-          const drafts = element.comments.left.filter(item => {
-            return item.__draftID === draftID;
-          });
-          assert.equal(drafts.length, 1);
-          assert.equal(drafts[0].id, id);
-          assert.isTrue(diffCommentsModifiedStub.called);
-        });
-      });
     });
 
     suite('diff header', () => {
@@ -967,38 +767,43 @@
         const mock = document.createElement('mock-diff-response');
         sandbox.stub(element.$.diffBuilder, 'getDiffLength').returns(10000);
         element.diff = mock.diffResponse;
-        element.comments = {left: [], right: []};
         element.noRenderOnPrefsChange = true;
       });
 
       test('large render w/ context = 10', done => {
-        element.prefs = {context: 10};
-        element.addEventListener('render', () => {
+        element.prefs = Object.assign({}, MINIMAL_PREFS, {context: 10});
+        function rendered() {
           assert.isTrue(renderStub.called);
           assert.isFalse(element._showWarning);
           done();
-        });
+          element.removeEventListener('render', rendered);
+        }
+        element.addEventListener('render', rendered);
         element._renderDiffTable();
       });
 
       test('large render w/ whole file and bypass', done => {
-        element.prefs = {context: -1};
+        element.prefs = Object.assign({}, MINIMAL_PREFS, {context: -1});
         element._safetyBypass = 10;
-        element.addEventListener('render', () => {
+        function rendered() {
           assert.isTrue(renderStub.called);
           assert.isFalse(element._showWarning);
           done();
-        });
+          element.removeEventListener('render', rendered);
+        }
+        element.addEventListener('render', rendered);
         element._renderDiffTable();
       });
 
       test('large render w/ whole file and no bypass', done => {
-        element.prefs = {context: -1};
-        element.addEventListener('render', () => {
+        element.prefs = Object.assign({}, MINIMAL_PREFS, {context: -1});
+        function rendered() {
           assert.isFalse(renderStub.called);
           assert.isTrue(element._showWarning);
           done();
-        });
+          element.removeEventListener('render', rendered);
+        }
+        element.addEventListener('render', rendered);
         element._renderDiffTable();
       });
     });
@@ -1148,6 +953,58 @@
         assert.equal(element._computeNewlineWarningClass('foo', false), shown);
       });
     });
+
+    suite('key locations', () => {
+      let renderStub;
+
+      setup(() => {
+        element = fixture('basic');
+        element.prefs = {};
+        renderStub = sandbox.stub(element.$.diffBuilder, 'render')
+            .returns(new Promise(() => {}));
+      });
+
+      test('lineOfInterest is a key location', () => {
+        element.lineOfInterest = {number: 789, leftSide: true};
+        element._renderDiffTable();
+        assert.isTrue(renderStub.called);
+        assert.deepEqual(renderStub.lastCall.args[0], {
+          left: {789: true},
+          right: {},
+        });
+      });
+
+      test('line comments are key locations', () => {
+        const threadEl = document.createElement('div');
+        threadEl.className = 'comment-thread';
+        threadEl.setAttribute('comment-side', 'right');
+        threadEl.setAttribute('line-num', 3);
+        Polymer.dom(element).appendChild(threadEl);
+        Polymer.dom.flush();
+
+        element._renderDiffTable();
+        assert.isTrue(renderStub.called);
+        assert.deepEqual(renderStub.lastCall.args[0], {
+          left: {},
+          right: {3: true},
+        });
+      });
+
+      test('file comments are key locations', () => {
+        const threadEl = document.createElement('div');
+        threadEl.className = 'comment-thread';
+        threadEl.setAttribute('comment-side', 'left');
+        Polymer.dom(element).appendChild(threadEl);
+        Polymer.dom.flush();
+
+        element._renderDiffTable();
+        assert.isTrue(renderStub.called);
+        assert.deepEqual(renderStub.lastCall.args[0], {
+          left: {FILE: true},
+          right: {},
+        });
+      });
+    });
   });
 
   a11ySuite('basic');
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 ad2e599..f913080 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,6 +31,7 @@
 
   Polymer({
     is: 'gr-patch-range-select',
+    _legacyUndefinedCheck: true,
 
     properties: {
       availablePatches: Array,
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 71b5bc3..c9e9f50 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
@@ -16,10 +16,8 @@
 -->
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
 <dom-module id="gr-ranged-comment-layer">
   <template>
-    <gr-reporting id="reporting" category="comments"></gr-reporting>
   </template>
   <script src="../gr-diff-highlight/gr-annotation.js"></script>
   <script src="gr-ranged-comment-layer.js"></script>
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 fb48b39..2183e7d 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,31 +17,42 @@
 (function() {
   'use strict';
 
-  const HOVER_PATH_PATTERN = /^comments\.(left|right)\.\#(\d+)\.__hovering$/;
-  const SPLICE_PATH_PATTERN = /^comments\.(left|right)\.splices$/;
+  const HOVER_PATH_PATTERN = /^commentRanges\.\#(\d+)\.hovering$/;
 
   const RANGE_HIGHLIGHT = 'range';
   const HOVER_HIGHLIGHT = 'rangeHighlight';
 
-  const NORMALIZE_RANGE_EVENT = 'normalize-range';
+  /** @typedef {{side: string, range: Gerrit.Range, hovering: boolean}} */
+  Gerrit.HoveredRange;
 
   Polymer({
     is: 'gr-ranged-comment-layer',
+    _legacyUndefinedCheck: true,
+
+    /**
+     * Fired when the range in a range comment was malformed and had to be
+     * normalized.
+     *
+     * It's `detail` has a `lineNum` and `side` parameter.
+     *
+     * @event normalize-range
+     */
 
     properties: {
-      comments: Object,
+      /** @type {!Array<!Gerrit.HoveredRange>} */
+      commentRanges: Array,
       _listeners: {
         type: Array,
         value() { return []; },
       },
-      _commentMap: {
+      _rangesMap: {
         type: Object,
-        value() { return {left: [], right: []}; },
+        value() { return {left: {}, right: {}}; },
       },
     },
 
     observers: [
-      '_handleCommentChange(comments.*)',
+      '_handleCommentRangesChange(commentRanges.*)',
     ],
 
     /**
@@ -49,9 +60,10 @@
      *
      * @param {!HTMLElement} el The DIV.contentText element to apply the
      *     annotation to.
+     * @param {!HTMLElement} lineNumberEl
      * @param {!Object} line The line object. (GrDiffLine)
      */
-    annotate(el, line) {
+    annotate(el, lineNumberEl, line) {
       let ranges = [];
       if (line.type === GrDiffLine.Type.REMOVE || (
         line.type === GrDiffLine.Type.BOTH &&
@@ -96,108 +108,92 @@
     },
 
     /**
-     * Handle change in the comments by updating the comment maps and by
+     * Handle change in the ranges by updating the ranges maps and by
      * emitting appropriate update notifications.
      *
      * @param {Object} record The change record.
      */
-    _handleCommentChange(record) {
-      if (!record.path) { return; }
+    _handleCommentRangesChange(record) {
+      if (!record) return;
 
       // If the entire set of comments was changed.
-      if (record.path === 'comments') {
-        this._commentMap.left = this._computeCommentMap(this.comments.left);
-        this._commentMap.right = this._computeCommentMap(this.comments.right);
-        return;
+      if (record.path === 'commentRanges') {
+        this._rangesMap = {left: {}, right: {}};
+        for (const {side, range, hovering} of record.value) {
+          this._updateRangesMap(
+              side, range, hovering, (forLine, start, end, hovering) => {
+                forLine.push({start, end, hovering});
+              });
+        }
       }
 
       // If the change only changed the `hovering` property of a comment.
-      let match = record.path.match(HOVER_PATH_PATTERN);
-      let side;
-
+      const match = record.path.match(HOVER_PATH_PATTERN);
       if (match) {
-        side = match[1];
-        const index = match[2];
-        const comment = this.comments[side][index];
-        if (comment && comment.range) {
-          this._commentMap[side] = this._computeCommentMap(this.comments[side]);
-          this._notifyUpdateRange(
-              comment.range.start_line, comment.range.end_line, side);
-        }
-        return;
+        const commentRangesIndex = match[1];
+        const {side, range, hovering} = this.commentRanges[commentRangesIndex];
+        this._updateRangesMap(
+            side, range, hovering, (forLine, start, end, hovering) => {
+              const index = forLine.findIndex(lineRange =>
+                lineRange.start === start && lineRange.end === end);
+              forLine[index].hovering = hovering;
+            });
       }
 
       // If comments were spliced in or out.
-      match = record.path.match(SPLICE_PATH_PATTERN);
-      if (match) {
-        side = match[1];
-        this._commentMap[side] = this._computeCommentMap(this.comments[side]);
-        this._handleCommentSplice(record.value, side);
+      if (record.path === 'commentRanges.splices') {
+        for (const indexSplice of record.value.indexSplices) {
+          const removed = indexSplice.removed;
+          for (const {side, range, hovering} of removed) {
+            this._updateRangesMap(
+                side, range, hovering, (forLine, start, end) => {
+                  const index = forLine.findIndex(lineRange =>
+                    lineRange.start === start && lineRange.end === end);
+                  forLine.splice(index, 1);
+                });
+          }
+          const added = indexSplice.object.slice(
+              indexSplice.index, indexSplice.index + indexSplice.addedCount);
+          for (const {side, range, hovering} of added) {
+            this._updateRangesMap(
+                side, range, hovering, (forLine, start, end, hovering) => {
+                  forLine.push({start, end, hovering});
+                });
+          }
+        }
       }
     },
 
-    /**
-     * Take a list of comments and return a sparse list mapping line numbers to
-     * partial ranges. Uses an end-character-index of -1 to indicate the end of
-     * the line.
-     *
-     * @param {?} commentList The list of comments.
-     *    Getting this param to match closure requirements caused problems.
-     * @return {!Object} The sparse list.
-     */
-    _computeCommentMap(commentList) {
-      const result = {};
-      for (const comment of commentList) {
-        if (!comment.range) { continue; }
-        const range = comment.range;
-        for (let line = range.start_line; line <= range.end_line; line++) {
-          if (!result[line]) { result[line] = []; }
-          result[line].push({
-            comment,
-            start: line === range.start_line ? range.start_character : 0,
-            end: line === range.end_line ? range.end_character : -1,
-          });
-        }
+    _updateRangesMap(side, range, hovering, operation) {
+      const forSide = this._rangesMap[side] || (this._rangesMap[side] = {});
+      for (let line = range.start_line; line <= range.end_line; line++) {
+        const forLine = forSide[line] || (forSide[line] = []);
+        const start = line === range.start_line ? range.start_character : 0;
+        const end = line === range.end_line ? range.end_character : -1;
+        operation(forLine, start, end, hovering);
       }
-      return result;
-    },
-
-    /**
-     * Translate a splice record into range update notifications.
-     */
-    _handleCommentSplice(record, side) {
-      if (!record || !record.indexSplices) { return; }
-
-      for (const splice of record.indexSplices) {
-        const ranges = splice.removed.length ?
-          splice.removed.map(c => { return c.range; }) :
-          [splice.object[splice.index].range];
-        for (const range of ranges) {
-          if (!range) { continue; }
-          this._notifyUpdateRange(range.start_line, range.end_line, side);
-        }
-      }
+      this._notifyUpdateRange(range.start_line, range.end_line, side);
     },
 
     _getRangesForLine(line, side) {
       const lineNum = side === 'left' ? line.beforeNumber : line.afterNumber;
-      const ranges = this.get(['_commentMap', side, lineNum]) || [];
+      const ranges = this.get(['_rangesMap', side, lineNum]) || [];
       return ranges
           .map(range => {
-            range = {
-              start: range.start,
-              end: range.end === -1 ? line.text.length : range.end,
-              hovering: !!range.comment.__hovering,
-            };
+            // Make a copy, so that the normalization below does not mess with
+            // our map.
+            range = Object.assign({}, range);
+            range.end = range.end === -1 ? line.text.length : range.end;
 
             // Normalize invalid ranges where the start is after the end but the
             // start still makes sense. Set the end to the end of the line.
             // @see Issue 5744
             if (range.start >= range.end && range.start < line.text.length) {
               range.end = line.text.length;
-              this.$.reporting.reportInteraction(NORMALIZE_RANGE_EVENT,
-                  'Modified invalid comment range on l.' + lineNum +
-                  ' of the ' + side + ' side');
+              this.dispatchEvent(new CustomEvent('normalize-range', {
+                bubbles: true,
+                detail: {lineNum, side},
+              }));
             }
 
             return range;
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 c541e26..682c026 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
@@ -40,62 +40,48 @@
     let sandbox;
 
     setup(() => {
-      const initialComments = {
-        left: [
-          {
-            id: '12345',
-            line: 39,
-            message: 'range comment',
-            range: {
-              end_character: 9,
-              end_line: 39,
-              start_character: 6,
-              start_line: 36,
-            },
-          }, {
-            id: '23456',
-            line: 100,
-            message: 'non range comment',
+      const initialCommentRanges = [
+        {
+          side: 'left',
+          range: {
+            end_character: 9,
+            end_line: 39,
+            start_character: 6,
+            start_line: 36,
           },
-        ],
-        right: [
-          {
-            id: '34567',
-            line: 10,
-            message: 'range comment',
-            range: {
-              end_character: 22,
-              end_line: 12,
-              start_character: 10,
-              start_line: 10,
-            },
-          }, {
-            id: '45678',
-            line: 100,
-            message: 'single line range comment',
-            range: {
-              end_character: 15,
-              end_line: 100,
-              start_character: 5,
-              start_line: 100,
-            },
-          }, {
-            id: '8675309',
-            line: 55,
-            message: 'nonsense range',
-            range: {
-              end_character: 2,
-              end_line: 55,
-              start_character: 32,
-              start_line: 55,
-            },
+        },
+        {
+          side: 'right',
+          range: {
+            end_character: 22,
+            end_line: 12,
+            start_character: 10,
+            start_line: 10,
           },
-        ],
-      };
+        },
+        {
+          side: 'right',
+          range: {
+            end_character: 15,
+            end_line: 100,
+            start_character: 5,
+            start_line: 100,
+          },
+        },
+        {
+          side: 'right',
+          range: {
+            end_character: 2,
+            end_line: 55,
+            start_character: 32,
+            start_line: 55,
+          },
+        },
+      ];
 
       sandbox = sinon.sandbox.create();
       element = fixture('basic');
-      element.comments = initialComments;
+      element.commentRanges = initialCommentRanges;
     });
 
     teardown(() => {
@@ -107,6 +93,7 @@
       let el;
       let line;
       let annotateElementStub;
+      const lineNumberEl = document.createElement('td');
 
       setup(() => {
         sandbox = sinon.sandbox.create();
@@ -125,7 +112,7 @@
         line.type = GrDiffLine.Type.REMOVE;
         line.beforeNumber = 40;
 
-        element.annotate(el, line);
+        element.annotate(el, lineNumberEl, line);
 
         assert.isFalse(annotateElementStub.called);
       });
@@ -136,7 +123,7 @@
         const expectedStart = 6;
         const expectedLength = line.text.length - expectedStart;
 
-        element.annotate(el, line);
+        element.annotate(el, lineNumberEl, line);
 
         assert.isTrue(annotateElementStub.called);
         const lastCall = annotateElementStub.lastCall;
@@ -149,12 +136,12 @@
       test('type=Remove has-comment hovering', () => {
         line.type = GrDiffLine.Type.REMOVE;
         line.beforeNumber = 36;
-        element.set(['comments', 'left', 0, '__hovering'], true);
+        element.set(['commentRanges', 0, 'hovering'], true);
 
         const expectedStart = 6;
         const expectedLength = line.text.length - expectedStart;
 
-        element.annotate(el, line);
+        element.annotate(el, lineNumberEl, line);
 
         assert.isTrue(annotateElementStub.called);
         const lastCall = annotateElementStub.lastCall;
@@ -171,7 +158,7 @@
         const expectedStart = 6;
         const expectedLength = line.text.length - expectedStart;
 
-        element.annotate(el, line);
+        element.annotate(el, lineNumberEl, line);
 
         assert.isTrue(annotateElementStub.called);
         const lastCall = annotateElementStub.lastCall;
@@ -186,7 +173,7 @@
         line.beforeNumber = 36;
         el.setAttribute('data-side', 'right');
 
-        element.annotate(el, line);
+        element.annotate(el, lineNumberEl, line);
 
         assert.isFalse(annotateElementStub.called);
       });
@@ -199,7 +186,7 @@
         const expectedStart = 0;
         const expectedLength = 22;
 
-        element.annotate(el, line);
+        element.annotate(el, lineNumberEl, line);
 
         assert.isTrue(annotateElementStub.called);
         const lastCall = annotateElementStub.lastCall;
@@ -210,29 +197,18 @@
       });
     });
 
-    test('_handleCommentChange overwrite', () => {
-      const handlerSpy = sandbox.spy(element, '_handleCommentChange');
-      const mapSpy = sandbox.spy(element, '_computeCommentMap');
+    test('_handleCommentRangesChange overwrite', () => {
+      element.set('commentRanges', []);
 
-      element.set('comments', {left: [], right: []});
-
-      assert.isTrue(handlerSpy.called);
-      assert.equal(mapSpy.callCount, 2);
-
-      assert.equal(Object.keys(element._commentMap.left).length, 0);
-      assert.equal(Object.keys(element._commentMap.right).length, 0);
+      assert.equal(Object.keys(element._rangesMap.left).length, 0);
+      assert.equal(Object.keys(element._rangesMap.right).length, 0);
     });
 
-    test('_handleCommentChange hovering', () => {
-      const handlerSpy = sandbox.spy(element, '_handleCommentChange');
-      const mapSpy = sandbox.spy(element, '_computeCommentMap');
+    test('_handleCommentRangesChange hovering', () => {
       const notifyStub = sinon.stub();
       element.addListener(notifyStub);
 
-      element.set(['comments', 'right', 0, '__hovering'], true);
-
-      assert.isTrue(handlerSpy.called);
-      assert.isTrue(mapSpy.called);
+      element.set(['commentRanges', 1, 'hovering'], true);
 
       assert.isTrue(notifyStub.called);
       const lastCall = notifyStub.lastCall;
@@ -241,16 +217,11 @@
       assert.equal(lastCall.args[2], 'right');
     });
 
-    test('_handleCommentChange splice out', () => {
-      const handlerSpy = sandbox.spy(element, '_handleCommentChange');
-      const mapSpy = sandbox.spy(element, '_computeCommentMap');
+    test('_handleCommentRangesChange splice out', () => {
       const notifyStub = sinon.stub();
       element.addListener(notifyStub);
 
-      element.splice('comments.right', 0, 1);
-
-      assert.isTrue(handlerSpy.called);
-      assert.isTrue(mapSpy.called);
+      element.splice('commentRanges', 1, 1);
 
       assert.isTrue(notifyStub.called);
       const lastCall = notifyStub.lastCall;
@@ -259,16 +230,12 @@
       assert.equal(lastCall.args[2], 'right');
     });
 
-    test('_handleCommentChange splice in', () => {
-      const handlerSpy = sandbox.spy(element, '_handleCommentChange');
-      const mapSpy = sandbox.spy(element, '_computeCommentMap');
+    test('_handleCommentRangesChange splice in', () => {
       const notifyStub = sinon.stub();
       element.addListener(notifyStub);
 
-      element.splice('comments.left', element.comments.left.length, 0, {
-        id: '56123',
-        line: 250,
-        message: 'new range comment',
+      element.splice('commentRanges', 1, 0, {
+        side: 'left',
         range: {
           end_character: 15,
           end_line: 275,
@@ -277,9 +244,6 @@
         },
       });
 
-      assert.isTrue(handlerSpy.called);
-      assert.isTrue(mapSpy.called);
-
       assert.isTrue(notifyStub.called);
       const lastCall = notifyStub.lastCall;
       assert.equal(lastCall.args[0], 250);
@@ -291,48 +255,48 @@
       // There is only one ranged comment on the left, but it spans ll.36-39.
       const leftKeys = [];
       for (let i = 36; i <= 39; i++) { leftKeys.push('' + i); }
-      assert.deepEqual(Object.keys(element._commentMap.left).sort(),
+      assert.deepEqual(Object.keys(element._rangesMap.left).sort(),
           leftKeys.sort());
 
-      assert.equal(element._commentMap.left[36].length, 1);
-      assert.equal(element._commentMap.left[36][0].start, 6);
-      assert.equal(element._commentMap.left[36][0].end, -1);
+      assert.equal(element._rangesMap.left[36].length, 1);
+      assert.equal(element._rangesMap.left[36][0].start, 6);
+      assert.equal(element._rangesMap.left[36][0].end, -1);
 
-      assert.equal(element._commentMap.left[37].length, 1);
-      assert.equal(element._commentMap.left[37][0].start, 0);
-      assert.equal(element._commentMap.left[37][0].end, -1);
+      assert.equal(element._rangesMap.left[37].length, 1);
+      assert.equal(element._rangesMap.left[37][0].start, 0);
+      assert.equal(element._rangesMap.left[37][0].end, -1);
 
-      assert.equal(element._commentMap.left[38].length, 1);
-      assert.equal(element._commentMap.left[38][0].start, 0);
-      assert.equal(element._commentMap.left[38][0].end, -1);
+      assert.equal(element._rangesMap.left[38].length, 1);
+      assert.equal(element._rangesMap.left[38][0].start, 0);
+      assert.equal(element._rangesMap.left[38][0].end, -1);
 
-      assert.equal(element._commentMap.left[39].length, 1);
-      assert.equal(element._commentMap.left[39][0].start, 0);
-      assert.equal(element._commentMap.left[39][0].end, 9);
+      assert.equal(element._rangesMap.left[39].length, 1);
+      assert.equal(element._rangesMap.left[39][0].start, 0);
+      assert.equal(element._rangesMap.left[39][0].end, 9);
 
       // The right has two ranged comments, one spanning ll.10-12 and the other
       // on line 100.
       const rightKeys = [];
       for (let i = 10; i <= 12; i++) { rightKeys.push('' + i); }
       rightKeys.push('55', '100');
-      assert.deepEqual(Object.keys(element._commentMap.right).sort(),
+      assert.deepEqual(Object.keys(element._rangesMap.right).sort(),
           rightKeys.sort());
 
-      assert.equal(element._commentMap.right[10].length, 1);
-      assert.equal(element._commentMap.right[10][0].start, 10);
-      assert.equal(element._commentMap.right[10][0].end, -1);
+      assert.equal(element._rangesMap.right[10].length, 1);
+      assert.equal(element._rangesMap.right[10][0].start, 10);
+      assert.equal(element._rangesMap.right[10][0].end, -1);
 
-      assert.equal(element._commentMap.right[11].length, 1);
-      assert.equal(element._commentMap.right[11][0].start, 0);
-      assert.equal(element._commentMap.right[11][0].end, -1);
+      assert.equal(element._rangesMap.right[11].length, 1);
+      assert.equal(element._rangesMap.right[11][0].start, 0);
+      assert.equal(element._rangesMap.right[11][0].end, -1);
 
-      assert.equal(element._commentMap.right[12].length, 1);
-      assert.equal(element._commentMap.right[12][0].start, 0);
-      assert.equal(element._commentMap.right[12][0].end, 22);
+      assert.equal(element._rangesMap.right[12].length, 1);
+      assert.equal(element._rangesMap.right[12][0].start, 0);
+      assert.equal(element._rangesMap.right[12][0].end, 22);
 
-      assert.equal(element._commentMap.right[100].length, 1);
-      assert.equal(element._commentMap.right[100][0].start, 5);
-      assert.equal(element._commentMap.right[100][0].end, 15);
+      assert.equal(element._rangesMap.right[100].length, 1);
+      assert.equal(element._rangesMap.right[100][0].start, 5);
+      assert.equal(element._rangesMap.right[100][0].end, 15);
     });
 
     test('_getRangesForLine normalizes invalid ranges', () => {
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 6349ab6..26bf738 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,11 +19,12 @@
 
   Polymer({
     is: 'gr-selection-action-box',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the comment creation action was taken (hotkey, click).
      *
-     * @event create-comment
+     * @event create-range-comment
      */
 
     properties: {
@@ -34,10 +35,10 @@
       range: {
         type: Object,
         value: {
-          startLine: NaN,
-          startChar: NaN,
-          endLine: NaN,
-          endChar: NaN,
+          start_line: NaN,
+          start_character: NaN,
+          end_line: NaN,
+          end_character: NaN,
         },
       },
       positionBelow: Boolean,
@@ -63,7 +64,7 @@
       Polymer.dom.flush();
       const rect = this._getTargetBoundingRect(el);
       const boxRect = this.$.tooltip.getBoundingClientRect();
-      const parentRect = this.parentElement.getBoundingClientRect();
+      const parentRect = this._getParentBoundingClientRect();
       this.style.top =
           rect.top - parentRect.top - boxRect.height - 6 + 'px';
       this.style.left =
@@ -74,11 +75,18 @@
       Polymer.dom.flush();
       const rect = this._getTargetBoundingRect(el);
       const boxRect = this.$.tooltip.getBoundingClientRect();
-      const parentRect = this.parentElement.getBoundingClientRect();
+      const parentRect = this._getParentBoundingClientRect();
       this.style.top =
-          rect.top - parentRect.top + boxRect.height - 6 + 'px';
+      rect.top - parentRect.top + boxRect.height - 6 + 'px';
       this.style.left =
-          rect.left - parentRect.left + (rect.width - boxRect.width) / 2 + 'px';
+      rect.left - parentRect.left + (rect.width - boxRect.width) / 2 + 'px';
+    },
+
+    _getParentBoundingClientRect() {
+      // With native shadow DOM, the parent is the shadow root, not the gr-diff
+      // element
+      const parent = this.parentElement || this.parentNode.host;
+      return parent.getBoundingClientRect();
     },
 
     _getTargetBoundingRect(el) {
@@ -110,7 +118,7 @@
     },
 
     _fireCreateComment() {
-      this.fire('create-comment', {side: this.side, range: this.range});
+      this.fire('create-range-comment', {side: this.side, range: this.range});
     },
   });
 })();
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 19155e4..dece366 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
@@ -89,16 +89,16 @@
     test('event fired contains playload', () => {
       const side = 'left';
       const range = {
-        startLine: 1,
-        startChar: 11,
-        endLine: 2,
-        endChar: 42,
+        start_line: 1,
+        start_character: 11,
+        end_line: 2,
+        end_character: 42,
       };
       element.side = 'left';
       element.range = range;
       MockInteractions.pressAndReleaseKeyOn(document.body, 67, null, 'c');
       assert(element.fire.calledWithExactly(
-          'create-comment',
+          'create-range-comment',
           {
             side,
             range,
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 017cd5d..67c32bb 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
@@ -21,6 +21,7 @@
   <template>
     <gr-lib-loader id="libLoader"></gr-lib-loader>
   </template>
+  <script src="../../../scripts/util.js"></script>
   <script src="../gr-diff/gr-diff-line.js"></script>
   <script src="../gr-diff-highlight/gr-annotation.js"></script>
   <script src="gr-syntax-layer.js"></script>
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 446805e..9d1a5da 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
@@ -85,6 +85,7 @@
     'text/x-swift': 'swift',
     'text/x-systemverilog': 'sv',
     'text/x-tcl': 'tcl',
+    'text/x-torque': 'torque',
     'text/x-twig': 'twig',
     'text/x-vb': 'vb',
     'text/x-verilog': 'v',
@@ -132,6 +133,7 @@
 
   Polymer({
     is: 'gr-syntax-layer',
+    _legacyUndefinedCheck: true,
 
     properties: {
       diff: {
@@ -158,6 +160,17 @@
       },
       /** @type {?number} */
       _processHandle: Number,
+      /**
+       * The promise last returned from `process()` while the asynchronous
+       * processing is running - `null` otherwise. Provides a `cancel()`
+       * method that rejects it with `{isCancelled: true}`.
+       *
+       * @type {?Object}
+       */
+      _processPromise: {
+        type: Object,
+        value: null,
+      },
       _hljs: Object,
     },
 
@@ -170,9 +183,10 @@
      * for the given line.
      *
      * @param {!HTMLElement} el
+     * @param {!HTMLElement} lineNumberEl
      * @param {!Object} line (GrDiffLine)
      */
-    annotate(el, line) {
+    annotate(el, lineNumberEl, line) {
       if (!this.enabled) { return; }
 
       // Determine the side.
@@ -202,6 +216,13 @@
       }
     },
 
+    _getLanguage(diffFileMetaInfo) {
+      // The Gerrit API provides only content-type, but for other users of
+      // gr-diff it may be more convenient to specify the language directly.
+      return diffFileMetaInfo.language ||
+          LANGUAGE_MAP[diffFileMetaInfo.content_type];
+    },
+
     /**
      * Start processing symtax for the loaded diff and notify layer listeners
      * as syntax info comes online.
@@ -209,6 +230,10 @@
      * @return {Promise}
      */
     process() {
+      // Cancel any still running process() calls, because they append to the
+      // same _baseRanges and _revisionRanges fields.
+      this.cancel();
+
       // Discard existing ranges.
       this._baseRanges = [];
       this._revisionRanges = [];
@@ -217,13 +242,11 @@
         return Promise.resolve();
       }
 
-      this.cancel();
-
       if (this.diff.meta_a) {
-        this._baseLanguage = LANGUAGE_MAP[this.diff.meta_a.content_type];
+        this._baseLanguage = this._getLanguage(this.diff.meta_a);
       }
       if (this.diff.meta_b) {
-        this._revisionLanguage = LANGUAGE_MAP[this.diff.meta_b.content_type];
+        this._revisionLanguage = this._getLanguage(this.diff.meta_b);
       }
       if (!this._baseLanguage && !this._revisionLanguage) {
         return Promise.resolve();
@@ -238,49 +261,55 @@
         lastNotify: {left: 1, right: 1},
       };
 
-      return this._loadHLJS().then(() => {
-        return new Promise(resolve => {
-          const nextStep = () => {
-            this._processHandle = null;
-            this._processNextLine(state);
+      this._processPromise = util.makeCancelable(this._loadHLJS()
+          .then(() => {
+            return new Promise(resolve => {
+              const nextStep = () => {
+                this._processHandle = null;
+                this._processNextLine(state);
 
-            // Move to the next line in the section.
-            state.lineIndex++;
+                // Move to the next line in the section.
+                state.lineIndex++;
 
-            // If the section has been exhausted, move to the next one.
-            if (this._isSectionDone(state)) {
-              state.lineIndex = 0;
-              state.sectionIndex++;
-            }
+                // If the section has been exhausted, move to the next one.
+                if (this._isSectionDone(state)) {
+                  state.lineIndex = 0;
+                  state.sectionIndex++;
+                }
 
-            // If all sections have been exhausted, finish.
-            if (state.sectionIndex >= this.diff.content.length) {
-              resolve();
-              this._notify(state);
-              return;
-            }
+                // If all sections have been exhausted, finish.
+                if (state.sectionIndex >= this.diff.content.length) {
+                  resolve();
+                  this._notify(state);
+                  return;
+                }
 
-            if (state.lineIndex % 100 === 0) {
-              this._notify(state);
-              this._processHandle = this.async(nextStep, ASYNC_DELAY);
-            } else {
-              nextStep.call(this);
-            }
-          };
+                if (state.lineIndex % 100 === 0) {
+                  this._notify(state);
+                  this._processHandle = this.async(nextStep, ASYNC_DELAY);
+                } else {
+                  nextStep.call(this);
+                }
+              };
 
-          this._processHandle = this.async(nextStep, 1);
-        });
-      });
+              this._processHandle = this.async(nextStep, 1);
+            });
+          }));
+      return this._processPromise
+          .finally(() => { this._processPromise = null; });
     },
 
     /**
      * Cancel any asynchronous syntax processing jobs.
      */
     cancel() {
-      if (this._processHandle) {
+      if (this._processHandle != null) {
         this.cancelAsync(this._processHandle);
         this._processHandle = null;
       }
+      if (this._processPromise) {
+        this._processPromise.cancel();
+      }
     },
 
     _diffChanged() {
@@ -356,7 +385,8 @@
       // To store the result of the syntax highlighter.
       let result;
 
-      if (this._baseLanguage && baseLine !== undefined) {
+      if (this._baseLanguage && baseLine !== undefined &&
+          this._hljs.getLanguage(this._baseLanguage)) {
         baseLine = this._workaround(this._baseLanguage, baseLine);
         result = this._hljs.highlight(this._baseLanguage, baseLine, true,
             state.baseContext);
@@ -364,7 +394,8 @@
         state.baseContext = result.top;
       }
 
-      if (this._revisionLanguage && revisionLine !== undefined) {
+      if (this._revisionLanguage && revisionLine !== undefined &&
+          this._hljs.getLanguage(this._revisionLanguage)) {
         revisionLine = this._workaround(this._revisionLanguage, revisionLine);
         result = this._hljs.highlight(this._revisionLanguage, revisionLine,
             true, state.revisionContext);
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 f2458fc..b63675a 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
@@ -38,6 +38,7 @@
     let sandbox;
     let diff;
     let element;
+    const lineNumberEl = document.createElement('td');
 
     function getMockHLJS() {
       const html = '<span class="gr-diff gr-syntax gr-syntax-string">' +
@@ -50,6 +51,11 @@
             top: state === undefined ? 1 : state + 1,
           };
         },
+        // Return something truthy because this method is used to check if the
+        // language is supported.
+        getLanguage(s) {
+          return {};
+        },
       };
     }
 
@@ -72,7 +78,7 @@
       const line = new GrDiffLine(GrDiffLine.Type.REMOVE);
       line.beforeNumber = 12;
 
-      element.annotate(el, line);
+      element.annotate(el, lineNumberEl, line);
 
       assert.isFalse(annotationSpy.called);
     });
@@ -94,7 +100,7 @@
         className,
       }];
 
-      element.annotate(el, line);
+      element.annotate(el, lineNumberEl, line);
 
       assert.isTrue(annotationSpy.called);
       assert.equal(annotationSpy.lastCall.args[0], el);
@@ -122,7 +128,7 @@
       }];
       element.enabled = false;
 
-      element.annotate(el, line);
+      element.annotate(el, lineNumberEl, line);
 
       assert.isFalse(annotationSpy.called);
     });
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 f850b9d..995326f 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-documentation-search',
+    _legacyUndefinedCheck: true,
 
     properties: {
       /**
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 168ded8..01cd9df 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-default-editor',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the content of the editor changes.
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 610e9ed..3567d06 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-edit-controls',
+    _legacyUndefinedCheck: true,
     properties: {
       change: Object,
       patchNum: String,
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 82010f5..9407f18 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-edit-file-controls',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when an action in the overflow menu is tapped.
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 51dc0f6..74c290e 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,6 +26,7 @@
 
   Polymer({
     is: 'gr-editor-view',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the title of the page should change.
diff --git a/polygerrit-ui/app/elements/gr-app-it_test.html b/polygerrit-ui/app/elements/gr-app-it_test.html
index 929156e..2601aeb 100644
--- a/polygerrit-ui/app/elements/gr-app-it_test.html
+++ b/polygerrit-ui/app/elements/gr-app-it_test.html
@@ -50,7 +50,6 @@
         getAccountCapabilities() { return Promise.resolve({}); },
         getConfig() {
           return Promise.resolve({
-            gerrit: {web_uis: ['GWT', 'POLYGERRIT']},
             plugin: {
               js_resource_paths: [],
               html_resource_paths: [
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index 3a9719d..8c50358 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -15,16 +15,18 @@
 limitations under the License.
 -->
 <script>
-  // 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.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,
+      };
+    }
   }
   // Needed for JSCompiler to understand it's global.
   // eslint-disable-next-line no-unused-vars, prefer-const
@@ -34,6 +36,7 @@
 
 <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="../behaviors/safe-types-behavior/safe-types-behavior.html">
 <script>
   security.polymer_resin.install({
@@ -42,6 +45,7 @@
     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">
@@ -58,6 +62,7 @@
 <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">
@@ -155,10 +160,16 @@
       <gr-main-header
           id="mainHeader"
           search-query="{{params.query}}"
-          class$="[[_computeShadowClass(_isShadowDom)]]">
+          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]]"
@@ -229,9 +240,6 @@
               rel="noopener"
               target="_blank">Send feedback</a> |
         </template>
-        <template is="dom-if" if="[[_computeShowGwtUiLink(_serverConfig)]]">
-          <a id="gwtLink" href$="[[computeGwtUrl(_path)]]" rel="external">Switch to Old UI</a> |
-        </template>
         Press &ldquo;?&rdquo; for keyboard shortcuts
         <gr-endpoint-decorator name="footer-right"></gr-endpoint-decorator>
       </div>
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index f48951b..b89d90e 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -20,12 +20,14 @@
   // Eagerly render Polymer components when backgrounded. (Skips
   // requestAnimationFrame.)
   // @see https://github.com/Polymer/polymer/issues/3851
-  // TODO: Reassess after Polymer 2.0 upgrade.
   // @see Issue 4699
-  Polymer.RenderStatus._makeReady();
+  if (!window.POLYMER2) {
+    Polymer.RenderStatus._makeReady();
+  }
 
   Polymer({
     is: 'gr-app',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the URL location changes.
@@ -87,6 +89,11 @@
       },
       _settingsUrl: String,
       _feedbackUrl: String,
+      // Used to allow searching on mobile
+      mobileSearch: {
+        type: Boolean,
+        value: false,
+      },
     },
 
     listeners: {
@@ -334,11 +341,6 @@
       this.$.header.unfloat();
     },
 
-    _computeShowGwtUiLink(config) {
-      return !window.DEPRECATE_GWT_UI &&
-          config.gerrit.web_uis && config.gerrit.web_uis.includes('GWT');
-    },
-
     _handlePageError(e) {
       const props = [
         '_showChangeListView',
@@ -466,5 +468,10 @@
       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 734d2fe..71ceab4 100644
--- a/polygerrit-ui/app/elements/gr-app_test.html
+++ b/polygerrit-ui/app/elements/gr-app_test.html
@@ -50,7 +50,6 @@
         getAccountCapabilities() { return Promise.resolve({}); },
         getConfig() {
           return Promise.resolve({
-            gerrit: {web_uis: ['GWT', 'POLYGERRIT']},
             plugin: {},
           });
         },
@@ -73,35 +72,6 @@
       assert.isTrue(element.$.reporting.appStarted.calledOnce);
     });
 
-    test('location change updates gwt footer', done => {
-      element._path = '/test/path';
-      flush(() => {
-        const gwtLink = element.$$('#gwtLink');
-        assert.equal(gwtLink.href, 'http://' + location.host +
-            element.getBaseUrl() + '/?polygerrit=0#/test/path');
-        done();
-      });
-    });
-
-    test('_handleLocationChange handles hashes', done => {
-      const curLocation = {
-        pathname: '/c/1/1/testfile.txt',
-        hash: '#2',
-        host: location.host,
-      };
-      element._handleLocationChange({detail: curLocation});
-
-      flush(() => {
-        const gwtLink = element.$$('#gwtLink');
-        assert.equal(
-            gwtLink.href,
-            'http://' + location.host + element.getBaseUrl() +
-            '/?polygerrit=0#/c/1/1/testfile.txt@2'
-        );
-        done();
-      });
-    });
-
     test('passes config to gr-plugin-host', () => {
       return element.$.restAPI.getConfig.lastCall.returnValue.then(config => {
         assert.deepEqual(element.$.plugins.config, config);
diff --git a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api.js b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api.js
index 3959186..d1f8e56 100644
--- a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api.js
+++ b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api.js
@@ -30,8 +30,8 @@
    * @param {string} text
    * @param {string} url
    */
-  GrAdminApi.prototype.addMenuLink = function(text, url) {
-    this._menuLinks.push({text, url});
+  GrAdminApi.prototype.addMenuLink = function(text, url, opt_capability) {
+    this._menuLinks.push({text, url, capability: opt_capability || null});
   };
 
   GrAdminApi.prototype.getMenuLinks = function() {
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 966efac..159f50a 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
@@ -54,7 +54,15 @@
       adminApi.addMenuLink('text', 'url');
       const links = adminApi.getMenuLinks();
       assert.equal(links.length, 1);
-      assert.deepEqual(links[0], {text: 'text', url: 'url'});
+      assert.deepEqual(links[0], {text: 'text', url: 'url', capability: null});
+    });
+
+    test('addMenuLinkWithCapability', () => {
+      adminApi.addMenuLink('text', 'url', 'capability');
+      const links = adminApi.getMenuLinks();
+      assert.equal(links.length, 1);
+      assert.deepEqual(links[0],
+          {text: 'text', url: 'url', capability: 'capability'});
     });
   });
 </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 cd59f7d..86238cf 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
@@ -30,6 +30,7 @@
   <script>
     Polymer({
       is: 'some-element',
+      _legacyUndefinedCheck: true,
       properties: {
         fooBar: {
           type: Object,
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 2d07382..028285b 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
@@ -54,6 +54,7 @@
   GrDomHook.prototype._createPlaceholder = function(hookName) {
     Polymer({
       is: hookName,
+      _legacyUndefinedCheck: true,
       properties: {
         plugin: Object,
         content: Object,
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 c0dd5d6..4ed7436 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,6 +21,7 @@
 
   Polymer({
     is: 'gr-endpoint-decorator',
+    _legacyUndefinedCheck: true,
 
     properties: {
       name: String,
@@ -72,7 +73,9 @@
     },
 
     _getEndpointParams() {
-      return Polymer.dom(this).querySelectorAll('gr-endpoint-param');
+      // 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-param/gr-endpoint-param.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js
index cbc3d6a..e21fc72 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-endpoint-param',
+    _legacyUndefinedCheck: true,
     properties: {
       name: String,
       value: {
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 709c042..47274f6 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
@@ -30,6 +30,7 @@
   <script>
     Polymer({
       is: 'some-element',
+      _legacyUndefinedCheck: true,
       properties: {
         fooBar: {
           type: Object,
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 0e8bb45..7924e27 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-external-style',
+    _legacyUndefinedCheck: true,
 
     properties: {
       name: String,
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 dcd9049..65f2207 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-plugin-host',
+    _legacyUndefinedCheck: true,
 
     properties: {
       config: {
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 dd37f84..3ef93e4 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
@@ -18,6 +18,7 @@
   'use strict';
   Polymer({
     is: 'gr-plugin-popup',
+    _legacyUndefinedCheck: true,
     get opened() {
       return this.$.overlay.opened;
     },
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 e47ba15..c9486ae 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
@@ -25,6 +25,7 @@
   <script>
     Polymer({
       is: 'gr-plugin-repo-command',
+      _legacyUndefinedCheck: true,
       properties: {
         title: String,
         repoName: String,
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 e6e8fa5..496d0e7 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
@@ -37,6 +37,7 @@
   <script>
     Polymer({
       is: 'gr-custom-plugin-header',
+      _legacyUndefinedCheck: true,
       properties: {
         logoUrl: String,
         title: String,
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 cc32690..51a22fc 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-account-info',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when account details are changed.
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 41595a98..fe36a86 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-agreements-list',
+    _legacyUndefinedCheck: true,
 
     properties: {
       _agreements: Array,
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 1ed91b8..d660ee5 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-change-table-editor',
+    _legacyUndefinedCheck: true,
 
     properties: {
       displayedColumns: {
@@ -42,8 +43,9 @@
      * @return {!Array<string>}
      */
     _getDisplayedColumns() {
-      return Polymer.dom(this.root)
-          .querySelectorAll('.checkboxContainer input:not([name=number])')
+      // Polymer2: querySelectorAll returns NodeList instead of Array.
+      return Array.from(Polymer.dom(this.root)
+          .querySelectorAll('.checkboxContainer input:not([name=number])'))
           .filter(checkbox => checkbox.checked)
           .map(checkbox => checkbox.name);
     },
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 ae53489..2a93155 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-cla-view',
+    _legacyUndefinedCheck: true,
 
     properties: {
       _groups: Object,
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 86350f9..37bce08 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-edit-preferences',
+    _legacyUndefinedCheck: true,
 
     properties: {
       hasUnsavedChanges: {
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 d08cc90..71d75cc 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-email-editor',
+    _legacyUndefinedCheck: true,
 
     properties: {
       hasUnsavedChanges: {
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 890061e..a50509c 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,6 +19,7 @@
 
   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 c29bfe2..aa53aca 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
@@ -169,7 +169,7 @@
       const newKeyString = 'not even close to valid';
 
       const addStub = sinon.stub(element.$.restAPI, 'addAccountGPGKey',
-          () => { return Promise.reject(); });
+          () => { return Promise.reject(new Error('error')); });
 
       element._newKey = newKeyString;
 
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 0f43563..4de24aa 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-group-list',
+    _legacyUndefinedCheck: true,
 
     properties: {
       _groups: Array,
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 003e471..99f4504 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-http-password',
+    _legacyUndefinedCheck: true,
 
     properties: {
       _username: String,
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 c927f1e..4560a2e 100644
--- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js
+++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js
@@ -24,6 +24,7 @@
 
   Polymer({
     is: 'gr-identities',
+    _legacyUndefinedCheck: true,
 
     properties: {
       _identities: Object,
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 aa83bf7..8587338 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-menu-editor',
+    _legacyUndefinedCheck: true,
 
     properties: {
       menuItems: Array,
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 c6cd578..6b4ee18 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-registration-dialog',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when account details are changed.
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 b61c7e0..30a3801 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
@@ -18,13 +18,13 @@
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 
 <dom-module id="gr-settings-item">
-  <style>
-    :host {
-      display: block;
-      margin-bottom: 2em;
-    }
-  </style>
   <template>
+    <style>
+      :host {
+        display: block;
+        margin-bottom: 2em;
+      }
+    </style>
     <h2 id="[[anchor]]">[[title]]</h2>
     <slot></slot>
   </template>
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 0ee1b28..dc1aa93 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,6 +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 3b47190..f64d898 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
@@ -19,9 +19,9 @@
 <link rel="import" href="../../../styles/gr-page-nav-styles.html">
 
 <dom-module id="gr-settings-menu-item">
-  <style include="shared-styles"></style>
-  <style include="gr-page-nav-styles"></style>
   <template>
+    <style include="shared-styles"></style>
+    <style include="gr-page-nav-styles"></style>
     <div class="navStyles">
       <li><a href$="[[href]]">[[title]]</a></li>
     </div>
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 38147cd..2a56b09 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,6 +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 db4d31a..f0c4d6e 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
@@ -55,6 +55,9 @@
       #email {
         margin-bottom: 1em;
       }
+      main section.darkToggle {
+        display: block;
+      }
       .filters p,
       .darkToggle p {
         margin-bottom: 1em;
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 23808ff..a66c38d 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,6 +47,7 @@
 
   Polymer({
     is: 'gr-settings-view',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the title of the page should change.
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 874173a..4c423e8 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,6 +19,7 @@
 
   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 1a884c9..8ed0730 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
@@ -155,7 +155,7 @@
       const newKeyString = 'not even close to valid';
 
       const addStub = sinon.stub(element.$.restAPI, 'addAccountSSHKey',
-          () => { return Promise.reject(); });
+          () => { return Promise.reject(new Error('error')); });
 
       element._newKey = newKeyString;
 
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 e751ea3..bd18456 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,6 +27,7 @@
 
   Polymer({
     is: 'gr-watched-projects-editor',
+    _legacyUndefinedCheck: true,
 
     properties: {
       hasUnsavedChanges: {
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 880cbc0..827e33b 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,6 +20,7 @@
 
   Polymer({
     is: 'gr-account-chip',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired to indicate a key was pressed while this chip was focused.
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 5b1b975..7983fad 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-account-label',
+    _legacyUndefinedCheck: true,
 
     properties: {
       /**
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 faaf9c3..03967f1 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-account-link',
+    _legacyUndefinedCheck: true,
 
     properties: {
       additionalText: String,
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 e7c8b2c..ec7b6eb 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-alert',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the action button is pressed.
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 2e55010..1af629d2 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-autocomplete-dropdown',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the dropdown is closed.
@@ -162,7 +163,9 @@
     _resetCursorStops() {
       if (this.suggestions.length > 0) {
         Polymer.dom.flush();
-        this._suggestionEls = this.$.suggestions.querySelectorAll('li');
+        // Polymer2: querySelectorAll returns NodeList instead of Array.
+        this._suggestionEls = Array.from(
+            this.$.suggestions.querySelectorAll('li'));
       } else {
         this._suggestionEls = [];
       }
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 4b447d1..a878174 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
@@ -29,7 +29,7 @@
         display: none;
       }
       .searchIcon.showSearchIcon {
-        display: initial;
+        display: inline-block;
       }
       iron-icon {
         margin: 0 .25em;
@@ -68,7 +68,6 @@
         no-label-float
         id="input"
         class$="[[_computeClass(borderless)]]"
-        is="iron-input"
         disabled$="[[disabled]]"
         value="{{text}}"
         placeholder="[[placeholder]]"
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 04815efd..9d16b9c 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
@@ -22,6 +22,7 @@
 
   Polymer({
     is: 'gr-autocomplete',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when a value is chosen.
@@ -210,7 +211,8 @@
     },
 
     get _inputElement() {
-      return this.$.input;
+      // Polymer2: this.$ can be undefined when this is first evaluated.
+      return this.$ && this.$.input;
     },
 
     /**
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 bf56382..2435e58 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-avatar',
+    _legacyUndefinedCheck: true,
 
     properties: {
       account: {
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 fe18180..cdf617f 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
@@ -94,7 +94,9 @@
       }
 
       /* Styles for the optional down arrow */
-      :host:not([down-arrow]) .downArrow {display: none; }
+      :host(:not([down-arrow])) .downArrow {
+        display: none;
+      }
       :host([down-arrow]) .downArrow {
         border-top: .36em solid #ccc;
         border-left: .36em solid transparent;
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 ca6705e..5988cde 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-button',
+    _legacyUndefinedCheck: true,
 
     properties: {
       tooltip: String,
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 3c46d1b..44f8c00 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-change-star',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when star state is toggled.
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 e256bf3..99ddff1 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
@@ -68,7 +68,7 @@
         background-color: transparent;
         padding: .1em;
       }
-      :host:not([flat]) .chip {
+      :host(:not([flat])) .chip {
         color: white;
       }
     </style>
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 8efd309..70c5d72 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
@@ -26,14 +26,15 @@
   };
 
   const WIP_TOOLTIP = 'This change isn\'t ready to be reviewed or submitted. ' +
-      'It will not appear in dashboards, and email notifications will be ' +
-      'silenced until the review is started.';
+      '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 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: {
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
new file mode 100644
index 0000000..8c80b37
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.html
@@ -0,0 +1,126 @@
+<!--
+@license
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.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">
+<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-storage/gr-storage.html">
+<link rel="import" href="../gr-comment/gr-comment.html">
+
+<dom-module id="gr-comment-thread">
+  <template>
+    <style include="shared-styles">
+      gr-button {
+        margin-left: .5em;
+      }
+      #actions {
+        margin-left: auto;
+        padding: .5em .7em;
+      }
+      #container {
+        background-color: var(--comment-background-color);
+        border: 1px solid var(--border-color);
+        color: var(--comment-text-color);
+        display: block;
+        margin-bottom: 1px;
+        white-space: normal;
+      }
+      #container.unresolved {
+        background-color: var(--unresolved-comment-background-color);
+      }
+      #commentInfoContainer {
+        border-top: 1px dotted var(--border-color);
+        display: flex;
+      }
+      #unresolvedLabel {
+        font-family: var(--font-family);
+        margin: auto 0;
+        padding: .5em .7em;
+      }
+      .pathInfo {
+        display: flex;
+        align-items: baseline;
+      }
+      .descriptionText {
+        margin-left: .5rem;
+        font-style: italic;
+      }
+    </style>
+    <template is="dom-if" if="[[showFilePath]]">
+      <div class="pathInfo">
+        <a href$="[[_getDiffUrlForComment(projectName, changeNum, path, patchNum)]]">[[_computeDisplayPath(path)]]</a>
+        <span class="descriptionText">Patchset [[patchNum]]</span>
+      </div>
+    </template>
+    <div id="container" class$="[[_computeHostClass(unresolved)]]">
+      <template id="commentList" is="dom-repeat" items="[[_orderedComments]]"
+          as="comment">
+        <gr-comment
+            comment="{{comment}}"
+            robot-button-disabled="[[_hideActions(_showActions, _lastComment)]]"
+            change-num="[[changeNum]]"
+            patch-num="[[patchNum]]"
+            draft="[[_isDraft(comment)]]"
+            show-actions="[[_showActions]]"
+            comment-side="[[comment.__commentSide]]"
+            side="[[comment.side]]"
+            root-id="[[rootId]]"
+            project-config="[[_projectConfig]]"
+            on-create-fix-comment="_handleCommentFix"
+            on-comment-discard="_handleCommentDiscard"
+            on-comment-save="_handleCommentSavedOrDiscarded"></gr-comment>
+      </template>
+      <div id="commentInfoContainer"
+          hidden$="[[_hideActions(_showActions, _lastComment)]]">
+        <span id="unresolvedLabel" hidden$="[[!unresolved]]">Unresolved</span>
+        <div id="actions">
+          <gr-button
+              id="replyBtn"
+              link
+              secondary
+              class="action reply"
+              on-tap="_handleCommentReply">Reply</gr-button>
+          <gr-button
+              id="quoteBtn"
+              link
+              secondary
+              class="action quote"
+              on-tap="_handleCommentQuote">Quote</gr-button>
+          <gr-button
+              id="ackBtn"
+              link
+              secondary
+              class="action ack"
+              on-tap="_handleCommentAck">Ack</gr-button>
+          <gr-button
+              id="doneBtn"
+              link
+              secondary
+              class="action done"
+              on-tap="_handleCommentDone">Done</gr-button>
+        </div>
+      </div>
+    </div>
+    <gr-reporting id="reporting"></gr-reporting>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+    <gr-storage id="storage"></gr-storage>
+  </template>
+  <script src="gr-comment-thread.js"></script>
+</dom-module>
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
new file mode 100644
index 0000000..0b4f806
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js
@@ -0,0 +1,495 @@
+/**
+ * @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 UNRESOLVED_EXPAND_COUNT = 5;
+  const NEWLINE_PATTERN = /\n/g;
+
+  Polymer({
+    is: 'gr-comment-thread',
+    _legacyUndefinedCheck: true,
+
+    /**
+     * Fired when the thread should be discarded.
+     *
+     * @event thread-discard
+     */
+
+    /**
+     * Fired when a comment in the thread is permanently modified.
+     *
+     * @event thread-changed
+     */
+
+    /**
+     * gr-comment-thread exposes the following attributes that allow a
+     * diff widget like gr-diff to show the thread in the right location:
+     *
+     * line-num:
+     *     1-based line number or undefined if it refers to the entire file.
+     *
+     * comment-side:
+     *     "left" or "right". These indicate which of the two diffed versions
+     *     the comment relates to. In the case of unified diff, the left
+     *     version is the one whose line number column is further to the left.
+     *
+     * range:
+     *     The range of text that the comment refers to (start_line,
+     *     start_character, end_line, end_character), serialized as JSON. If
+     *     set, range's end_line will have the same value as line-num. Line
+     *     numbers are 1-based, char numbers are 0-based. The start position
+     *     (start_line, start_character) is inclusive, and the end position
+     *     (end_line, end_character) is exclusive.
+     */
+    properties: {
+      changeNum: String,
+      comments: {
+        type: Array,
+        value() { return []; },
+      },
+      /**
+       * @type {?{start_line: number, start_character: number, end_line: number,
+       *          end_character: number}}
+       */
+      range: {
+        type: Object,
+        reflectToAttribute: true,
+      },
+      keyEventTarget: {
+        type: Object,
+        value() { return document.body; },
+      },
+      commentSide: {
+        type: String,
+        reflectToAttribute: true,
+      },
+      patchNum: String,
+      path: String,
+      projectName: {
+        type: String,
+        observer: '_projectNameChanged',
+      },
+      hasDraft: {
+        type: Boolean,
+        notify: true,
+        reflectToAttribute: true,
+      },
+      isOnParent: {
+        type: Boolean,
+        value: false,
+      },
+      parentIndex: {
+        type: Number,
+        value: null,
+      },
+      rootId: {
+        type: String,
+        notify: true,
+        computed: '_computeRootId(comments.*)',
+      },
+      /**
+       * If this is true, the comment thread also needs to have the change and
+       * line properties property set
+       */
+      showFilePath: {
+        type: Boolean,
+        value: false,
+      },
+      /** Necessary only if showFilePath is true or when used with gr-diff */
+      lineNum: {
+        type: Number,
+        reflectToAttribute: true,
+      },
+      unresolved: {
+        type: Boolean,
+        notify: true,
+        reflectToAttribute: true,
+      },
+      _showActions: Boolean,
+      _lastComment: Object,
+      _orderedComments: Array,
+      _projectConfig: Object,
+    },
+
+    behaviors: [
+      Gerrit.KeyboardShortcutBehavior,
+      Gerrit.PathListBehavior,
+    ],
+
+    listeners: {
+      'comment-update': '_handleCommentUpdate',
+    },
+
+    observers: [
+      '_commentsChanged(comments.*)',
+    ],
+
+    keyBindings: {
+      'e shift+e': '_handleEKey',
+    },
+
+    attached() {
+      this._getLoggedIn().then(loggedIn => {
+        this._showActions = loggedIn;
+      });
+      this._setInitialExpandedState();
+    },
+
+    addOrEditDraft(opt_lineNum, opt_range) {
+      const lastComment = this.comments[this.comments.length - 1] || {};
+      if (lastComment.__draft) {
+        const commentEl = this._commentElWithDraftID(
+            lastComment.id || lastComment.__draftID);
+        commentEl.editing = true;
+
+        // If the comment was collapsed, re-open it to make it clear which
+        // actions are available.
+        commentEl.collapsed = false;
+      } else {
+        const range = opt_range ? opt_range :
+          lastComment ? lastComment.range : undefined;
+        const unresolved = lastComment ? lastComment.unresolved : undefined;
+        this.addDraft(opt_lineNum, range, unresolved);
+      }
+    },
+
+    addDraft(opt_lineNum, opt_range, opt_unresolved) {
+      const draft = this._newDraft(opt_lineNum, opt_range);
+      draft.__editing = true;
+      draft.unresolved = opt_unresolved === false ? opt_unresolved : true;
+      this.push('comments', draft);
+    },
+
+    fireRemoveSelf() {
+      this.dispatchEvent(new CustomEvent('thread-discard',
+          {detail: {rootId: this.rootId}, bubbles: false}));
+    },
+
+    _getDiffUrlForComment(projectName, changeNum, path, patchNum) {
+      return Gerrit.Nav.getUrlForDiffById(changeNum,
+          projectName, path, patchNum,
+          null, this.lineNum);
+    },
+
+    _computeDisplayPath(path) {
+      const lineString = this.lineNum ? `#${this.lineNum}` : '';
+      return this.computeDisplayPath(path) + lineString;
+    },
+
+    _getLoggedIn() {
+      return this.$.restAPI.getLoggedIn();
+    },
+
+    _commentsChanged() {
+      this._orderedComments = this._sortedComments(this.comments);
+      this.updateThreadProperties();
+    },
+
+    updateThreadProperties() {
+      if (this._orderedComments.length) {
+        this._lastComment = this._getLastComment();
+        this.unresolved = this._lastComment.unresolved;
+        this.hasDraft = this._lastComment.__draft;
+      }
+    },
+
+    _hideActions(_showActions, _lastComment) {
+      return !_showActions || !_lastComment || !!_lastComment.__draft;
+    },
+
+    _getLastComment() {
+      return this._orderedComments[this._orderedComments.length - 1] || {};
+    },
+
+    _handleEKey(e) {
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+      // Don’t preventDefault in this case because it will render the event
+      // useless for other handlers (other gr-comment-thread elements).
+      if (e.detail.keyboardEvent.shiftKey) {
+        this._expandCollapseComments(true);
+      } else {
+        if (this.modifierPressed(e)) { return; }
+        this._expandCollapseComments(false);
+      }
+    },
+
+    _expandCollapseComments(actionIsCollapse) {
+      const comments =
+          Polymer.dom(this.root).querySelectorAll('gr-comment');
+      for (const comment of comments) {
+        comment.collapsed = actionIsCollapse;
+      }
+    },
+
+    /**
+     * Sets the initial state of the comment thread.
+     * Expands the thread if one of the following is true:
+     * - last {UNRESOLVED_EXPAND_COUNT} comments expanded by default if the
+     * thread is unresolved,
+     * - it's a robot comment.
+     */
+    _setInitialExpandedState() {
+      if (this._orderedComments) {
+        for (let i = 0; i < this._orderedComments.length; i++) {
+          const comment = this._orderedComments[i];
+          const isRobotComment = !!comment.robot_id;
+          // False if it's an unresolved comment under UNRESOLVED_EXPAND_COUNT.
+          const resolvedThread = !this.unresolved ||
+                this._orderedComments.length - i - 1 >= UNRESOLVED_EXPAND_COUNT;
+          comment.collapsed = !isRobotComment && resolvedThread;
+        }
+      }
+    },
+
+    _sortedComments(comments) {
+      return comments.slice().sort((c1, c2) => {
+        const c1Date = c1.__date || util.parseDate(c1.updated);
+        const c2Date = c2.__date || util.parseDate(c2.updated);
+        const dateCompare = c1Date - c2Date;
+        // Ensure drafts are at the end. There should only be one but in edge
+        // cases could be more. In the unlikely event two drafts are being
+        // compared, use the typical date compare.
+        if (c2.__draft && !c1.__draft ) { return -1; }
+        if (c1.__draft && !c2.__draft ) { return 1; }
+        if (dateCompare === 0 && (!c1.id || !c1.id.localeCompare)) { return 0; }
+        // If same date, fall back to sorting by id.
+        return dateCompare ? dateCompare : c1.id.localeCompare(c2.id);
+      });
+    },
+
+    _createReplyComment(parent, content, opt_isEditing,
+        opt_unresolved) {
+      this.$.reporting.recordDraftInteraction();
+      const reply = this._newReply(
+          this._orderedComments[this._orderedComments.length - 1].id,
+          parent.line,
+          content,
+          opt_unresolved,
+          parent.range);
+
+      // If there is currently a comment in an editing state, add an attribute
+      // so that the gr-comment knows not to populate the draft text.
+      for (let i = 0; i < this.comments.length; i++) {
+        if (this.comments[i].__editing) {
+          reply.__otherEditing = true;
+          break;
+        }
+      }
+
+      if (opt_isEditing) {
+        reply.__editing = true;
+      }
+
+      this.push('comments', reply);
+
+      if (!opt_isEditing) {
+        // Allow the reply to render in the dom-repeat.
+        this.async(() => {
+          const commentEl = this._commentElWithDraftID(reply.__draftID);
+          commentEl.save();
+        }, 1);
+      }
+    },
+
+    _isDraft(comment) {
+      return !!comment.__draft;
+    },
+
+    /**
+     * @param {boolean=} opt_quote
+     */
+    _processCommentReply(opt_quote) {
+      const comment = this._lastComment;
+      let quoteStr;
+      if (opt_quote) {
+        const msg = comment.message;
+        quoteStr = '> ' + msg.replace(NEWLINE_PATTERN, '\n> ') + '\n\n';
+      }
+      this._createReplyComment(comment, quoteStr, true, comment.unresolved);
+    },
+
+    _handleCommentReply(e) {
+      this._processCommentReply();
+    },
+
+    _handleCommentQuote(e) {
+      this._processCommentReply(true);
+    },
+
+    _handleCommentAck(e) {
+      const comment = this._lastComment;
+      this._createReplyComment(comment, 'Ack', false, false);
+    },
+
+    _handleCommentDone(e) {
+      const comment = this._lastComment;
+      this._createReplyComment(comment, 'Done', false, false);
+    },
+
+    _handleCommentFix(e) {
+      const comment = e.detail.comment;
+      const msg = comment.message;
+      const quoteStr = '> ' + msg.replace(NEWLINE_PATTERN, '\n> ') + '\n\n';
+      const response = quoteStr + 'Please Fix';
+      this._createReplyComment(comment, response, false, true);
+    },
+
+    _commentElWithDraftID(id) {
+      const els = Polymer.dom(this.root).querySelectorAll('gr-comment');
+      for (const el of els) {
+        if (el.comment.id === id || el.comment.__draftID === id) {
+          return el;
+        }
+      }
+      return null;
+    },
+
+    _newReply(inReplyTo, opt_lineNum, opt_message, opt_unresolved,
+        opt_range) {
+      const d = this._newDraft(opt_lineNum);
+      d.in_reply_to = inReplyTo;
+      d.range = opt_range;
+      if (opt_message != null) {
+        d.message = opt_message;
+      }
+      if (opt_unresolved !== undefined) {
+        d.unresolved = opt_unresolved;
+      }
+      return d;
+    },
+
+    /**
+     * @param {number=} opt_lineNum
+     * @param {!Object=} opt_range
+     */
+    _newDraft(opt_lineNum, opt_range) {
+      const d = {
+        __draft: true,
+        __draftID: Math.random().toString(36),
+        __date: new Date(),
+        path: this.path,
+        patchNum: this.patchNum,
+        side: this._getSide(this.isOnParent),
+        __commentSide: this.commentSide,
+      };
+      if (opt_lineNum) {
+        d.line = opt_lineNum;
+      }
+      if (opt_range) {
+        d.range = opt_range;
+      }
+      if (this.parentIndex) {
+        d.parent = this.parentIndex;
+      }
+      return d;
+    },
+
+    _getSide(isOnParent) {
+      if (isOnParent) { return 'PARENT'; }
+      return 'REVISION';
+    },
+
+    _computeRootId(comments) {
+      // Keep the root ID even if the comment was removed, so that notification
+      // to sync will know which thread to remove.
+      if (!comments.base.length) { return this.rootId; }
+      const rootComment = comments.base[0];
+      return rootComment.id || rootComment.__draftID;
+    },
+
+    _handleCommentDiscard(e) {
+      const diffCommentEl = Polymer.dom(e).rootTarget;
+      const comment = diffCommentEl.comment;
+      const idx = this._indexOf(comment, this.comments);
+      if (idx == -1) {
+        throw Error('Cannot find comment ' +
+            JSON.stringify(diffCommentEl.comment));
+      }
+      this.splice('comments', idx, 1);
+      if (this.comments.length === 0) {
+        this.fireRemoveSelf();
+      }
+      this._handleCommentSavedOrDiscarded(e);
+
+      // Check to see if there are any other open comments getting edited and
+      // set the local storage value to its message value.
+      for (const changeComment of this.comments) {
+        if (changeComment.__editing) {
+          const commentLocation = {
+            changeNum: this.changeNum,
+            patchNum: this.patchNum,
+            path: changeComment.path,
+            line: changeComment.line,
+          };
+          return this.$.storage.setDraftComment(commentLocation,
+              changeComment.message);
+        }
+      }
+    },
+
+    _handleCommentSavedOrDiscarded(e) {
+      this.dispatchEvent(new CustomEvent('thread-changed',
+          {detail: {rootId: this.rootId, path: this.path},
+            bubbles: false}));
+    },
+
+    _handleCommentUpdate(e) {
+      const comment = e.detail.comment;
+      const index = this._indexOf(comment, this.comments);
+      if (index === -1) {
+        // This should never happen: comment belongs to another thread.
+        console.warn('Comment update for another comment thread.');
+        return;
+      }
+      this.set(['comments', index], comment);
+      // Because of the way we pass these comment objects around by-ref, in
+      // combination with the fact that Polymer does dirty checking in
+      // observers, the this.set() call above will not cause a thread update in
+      // some situations.
+      this.updateThreadProperties();
+    },
+
+    _indexOf(comment, arr) {
+      for (let i = 0; i < arr.length; i++) {
+        const c = arr[i];
+        if ((c.__draftID != null && c.__draftID == comment.__draftID) ||
+            (c.id != null && c.id == comment.id)) {
+          return i;
+        }
+      }
+      return -1;
+    },
+
+    _computeHostClass(unresolved) {
+      return unresolved ? 'unresolved' : '';
+    },
+
+    /**
+     * Load the project config when a project name has been provided.
+     *
+     * @param {string} name The project name.
+     */
+    _projectNameChanged(name) {
+      if (!name) { return; }
+      this.$.restAPI.getProjectConfig(name).then(config => {
+        this._projectConfig = config;
+      });
+    },
+  });
+})();
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
new file mode 100644
index 0000000..74c6f5f
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html
@@ -0,0 +1,751 @@
+<!DOCTYPE html>
+<!--
+@license
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<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="../../../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-comment-thread.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-comment-thread></gr-comment-thread>
+  </template>
+</test-fixture>
+
+<test-fixture id="withComment">
+  <template>
+    <gr-comment-thread></gr-comment-thread>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-comment-thread tests', () => {
+    let element;
+    let sandbox;
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+      stub('gr-rest-api-interface', {
+        getLoggedIn() { return Promise.resolve(false); },
+      });
+      sandbox = sinon.sandbox.create();
+      element = fixture('basic');
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    test('comments are sorted correctly', () => {
+      const comments = [
+        {
+          message: 'i like you, too',
+          in_reply_to: 'sallys_confession',
+          __date: new Date('2015-12-25'),
+        }, {
+          id: 'sallys_confession',
+          message: 'i like you, jack',
+          updated: '2015-12-24 15:00:20.396000000',
+        }, {
+          id: 'sally_to_dr_finklestein',
+          message: 'i’m running away',
+          updated: '2015-10-31 09:00:20.396000000',
+        }, {
+          id: 'sallys_defiance',
+          in_reply_to: 'sally_to_dr_finklestein',
+          message: 'i will poison you so i can get away',
+          updated: '2015-10-31 15:00:20.396000000',
+        }, {
+          id: 'dr_finklesteins_response',
+          in_reply_to: 'sally_to_dr_finklestein',
+          message: 'no i will pull a thread and your arm will fall off',
+          updated: '2015-10-31 11:00:20.396000000',
+        }, {
+          id: 'sallys_mission',
+          message: 'i have to find santa',
+          updated: '2015-12-24 15:00:20.396000000',
+        },
+      ];
+      const results = element._sortedComments(comments);
+      assert.deepEqual(results, [
+        {
+          id: 'sally_to_dr_finklestein',
+          message: 'i’m running away',
+          updated: '2015-10-31 09:00:20.396000000',
+        }, {
+          id: 'dr_finklesteins_response',
+          in_reply_to: 'sally_to_dr_finklestein',
+          message: 'no i will pull a thread and your arm will fall off',
+          updated: '2015-10-31 11:00:20.396000000',
+        }, {
+          id: 'sallys_defiance',
+          in_reply_to: 'sally_to_dr_finklestein',
+          message: 'i will poison you so i can get away',
+          updated: '2015-10-31 15:00:20.396000000',
+        }, {
+          id: 'sallys_confession',
+          message: 'i like you, jack',
+          updated: '2015-12-24 15:00:20.396000000',
+        }, {
+          id: 'sallys_mission',
+          message: 'i have to find santa',
+          updated: '2015-12-24 15:00:20.396000000',
+        }, {
+          message: 'i like you, too',
+          in_reply_to: 'sallys_confession',
+          __date: new Date('2015-12-25'),
+        },
+      ]);
+    });
+
+    test('addOrEditDraft w/ edit draft', () => {
+      element.comments = [{
+        id: 'jacks_reply',
+        message: 'i like you, too',
+        in_reply_to: 'sallys_confession',
+        updated: '2015-12-25 15:00:20.396000000',
+        __draft: true,
+      }];
+      const commentElStub = sandbox.stub(element, '_commentElWithDraftID',
+          () => { return {}; });
+      const addDraftStub = sandbox.stub(element, 'addDraft');
+
+      element.addOrEditDraft(123);
+
+      assert.isTrue(commentElStub.called);
+      assert.isFalse(addDraftStub.called);
+    });
+
+    test('addOrEditDraft w/o edit draft', () => {
+      element.comments = [];
+      const commentElStub = sandbox.stub(element, '_commentElWithDraftID',
+          () => { return {}; });
+      const addDraftStub = sandbox.stub(element, 'addDraft');
+
+      element.addOrEditDraft(123);
+
+      assert.isFalse(commentElStub.called);
+      assert.isTrue(addDraftStub.called);
+    });
+
+    test('_hideActions', () => {
+      let showActions = true;
+      const lastComment = {};
+      assert.equal(element._hideActions(showActions, lastComment), false);
+      showActions = false;
+      assert.equal(element._hideActions(showActions, lastComment), true);
+      showActions = true;
+      lastComment.__draft = true;
+      assert.equal(element._hideActions(showActions, lastComment), true);
+    });
+
+    test('setting project name loads the project config', done => {
+      const projectName = 'foo/bar/baz';
+      const getProjectStub = sandbox.stub(element.$.restAPI, 'getProjectConfig')
+          .returns(Promise.resolve({}));
+      element.projectName = projectName;
+      flush(() => {
+        assert.isTrue(getProjectStub.calledWithExactly(projectName));
+        done();
+      });
+    });
+
+    test('optionally show file path', () => {
+      // Path info doesn't exist when showFilePath is false. Because it's in a
+      // dom-if it is not yet in the dom.
+      assert.isNotOk(element.$$('.pathInfo'));
+
+      sandbox.stub(Gerrit.Nav, 'getUrlForDiffById');
+      element.changeNum = 123;
+      element.projectName = 'test project';
+      element.path = 'path/to/file';
+      element.patchNum = 3;
+      element.lineNum = 5;
+      element.showFilePath = true;
+      flushAsynchronousOperations();
+      assert.isOk(element.$$('.pathInfo'));
+      assert.notEqual(getComputedStyle(element.$$('.pathInfo')).display,
+          'none');
+      assert.isTrue(Gerrit.Nav.getUrlForDiffById.lastCall.calledWithExactly(
+          element.changeNum, element.projectName, element.path,
+          element.patchNum, null, element.lineNum));
+    });
+
+    test('_computeDisplayPath', () => {
+      const path = 'path/to/file';
+      assert.equal(element._computeDisplayPath(path), 'path/to/file');
+
+      element.lineNum = 5;
+      assert.equal(element._computeDisplayPath(path), 'path/to/file#5');
+    });
+  });
+
+  suite('comment action tests', () => {
+    let element;
+    let sandbox;
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+      stub('gr-rest-api-interface', {
+        getLoggedIn() { return Promise.resolve(false); },
+        saveDiffDraft() {
+          return Promise.resolve({
+            ok: true,
+            text() {
+              return Promise.resolve(')]}\'\n' +
+                  JSON.stringify({
+                    id: '7afa4931_de3d65bd',
+                    path: '/path/to/file.txt',
+                    line: 5,
+                    in_reply_to: 'baf0414d_60047215',
+                    updated: '2015-12-21 02:01:10.850000000',
+                    message: 'Done',
+                  }));
+            },
+          });
+        },
+        deleteDiffDraft() { return Promise.resolve({ok: true}); },
+      });
+      element = fixture('withComment');
+      element.comments = [{
+        author: {
+          name: 'Mr. Peanutbutter',
+          email: 'tenn1sballchaser@aol.com',
+        },
+        id: 'baf0414d_60047215',
+        line: 5,
+        message: 'is this a crossover episode!?',
+        updated: '2015-12-08 19:48:33.843000000',
+        path: '/path/to/file.txt',
+      }];
+      flushAsynchronousOperations();
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    test('reply', () => {
+      const commentEl = element.$$('gr-comment');
+      const reportStub = sandbox.stub(element.$.reporting,
+          'recordDraftInteraction');
+      assert.ok(commentEl);
+
+      const replyBtn = element.$.replyBtn;
+      MockInteractions.tap(replyBtn);
+      flushAsynchronousOperations();
+
+      const drafts = element._orderedComments.filter(c => {
+        return c.__draft == true;
+      });
+      assert.equal(drafts.length, 1);
+      assert.notOk(drafts[0].message, 'message should be empty');
+      assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
+      assert.isTrue(reportStub.calledOnce);
+    });
+
+    test('quote reply', () => {
+      const commentEl = element.$$('gr-comment');
+      const reportStub = sandbox.stub(element.$.reporting,
+          'recordDraftInteraction');
+      assert.ok(commentEl);
+
+      const quoteBtn = element.$.quoteBtn;
+      MockInteractions.tap(quoteBtn);
+      flushAsynchronousOperations();
+
+      const drafts = element._orderedComments.filter(c => {
+        return c.__draft == true;
+      });
+      assert.equal(drafts.length, 1);
+      assert.equal(drafts[0].message, '> is this a crossover episode!?\n\n');
+      assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
+      assert.isTrue(reportStub.calledOnce);
+    });
+
+    test('quote reply multiline', () => {
+      const reportStub = sandbox.stub(element.$.reporting,
+          'recordDraftInteraction');
+      element.comments = [{
+        author: {
+          name: 'Mr. Peanutbutter',
+          email: 'tenn1sballchaser@aol.com',
+        },
+        id: 'baf0414d_60047215',
+        line: 5,
+        message: 'is this a crossover episode!?\nIt might be!',
+        updated: '2015-12-08 19:48:33.843000000',
+      }];
+      flushAsynchronousOperations();
+
+      const commentEl = element.$$('gr-comment');
+      assert.ok(commentEl);
+
+      const quoteBtn = element.$.quoteBtn;
+      MockInteractions.tap(quoteBtn);
+      flushAsynchronousOperations();
+
+      const drafts = element._orderedComments.filter(c => {
+        return c.__draft == true;
+      });
+      assert.equal(drafts.length, 1);
+      assert.equal(drafts[0].message,
+          '> is this a crossover episode!?\n> It might be!\n\n');
+      assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
+      assert.isTrue(reportStub.calledOnce);
+    });
+
+    test('ack', done => {
+      const reportStub = sandbox.stub(element.$.reporting,
+          'recordDraftInteraction');
+      element.changeNum = '42';
+      element.patchNum = '1';
+
+      const commentEl = element.$$('gr-comment');
+      assert.ok(commentEl);
+
+      const ackBtn = element.$.ackBtn;
+      MockInteractions.tap(ackBtn);
+      flush(() => {
+        const drafts = element.comments.filter(c => {
+          return c.__draft == true;
+        });
+        assert.equal(drafts.length, 1);
+        assert.equal(drafts[0].message, 'Ack');
+        assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
+        assert.equal(drafts[0].unresolved, false);
+        assert.isTrue(reportStub.calledOnce);
+        done();
+      });
+    });
+
+    test('done', done => {
+      const reportStub = sandbox.stub(element.$.reporting,
+          'recordDraftInteraction');
+      element.changeNum = '42';
+      element.patchNum = '1';
+      const commentEl = element.$$('gr-comment');
+      assert.ok(commentEl);
+
+      const doneBtn = element.$.doneBtn;
+      MockInteractions.tap(doneBtn);
+      flush(() => {
+        const drafts = element.comments.filter(c => {
+          return c.__draft == true;
+        });
+        assert.equal(drafts.length, 1);
+        assert.equal(drafts[0].message, 'Done');
+        assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
+        assert.isFalse(drafts[0].unresolved);
+        assert.isTrue(reportStub.calledOnce);
+        done();
+      });
+    });
+
+    test('save', done => {
+      element.changeNum = '42';
+      element.patchNum = '1';
+      element.path = '/path/to/file.txt';
+      const commentEl = element.$$('gr-comment');
+      assert.ok(commentEl);
+
+      const saveOrDiscardStub = sandbox.stub();
+      element.addEventListener('thread-changed', saveOrDiscardStub);
+      element.$$('gr-comment')._fireSave();
+
+      flush(() => {
+        assert.isTrue(saveOrDiscardStub.called);
+        assert.equal(saveOrDiscardStub.lastCall.args[0].detail.rootId,
+            'baf0414d_60047215');
+        assert.equal(element.rootId, 'baf0414d_60047215');
+        assert.equal(saveOrDiscardStub.lastCall.args[0].detail.path,
+            '/path/to/file.txt');
+        done();
+      });
+    });
+
+    test('please fix', done => {
+      element.changeNum = '42';
+      element.patchNum = '1';
+      const commentEl = element.$$('gr-comment');
+      assert.ok(commentEl);
+      commentEl.addEventListener('create-fix-comment', () => {
+        const drafts = element._orderedComments.filter(c => {
+          return c.__draft == true;
+        });
+        assert.equal(drafts.length, 1);
+        assert.equal(
+            drafts[0].message, '> is this a crossover episode!?\n\nPlease Fix');
+        assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
+        assert.isTrue(drafts[0].unresolved);
+        done();
+      });
+      commentEl.fire('create-fix-comment', {comment: commentEl.comment},
+          {bubbles: false});
+    });
+
+    test('discard', done => {
+      element.changeNum = '42';
+      element.patchNum = '1';
+      element.path = '/path/to/file.txt';
+      element.push('comments', element._newReply(
+          element.comments[0].id,
+          element.comments[0].line,
+          element.comments[0].path,
+          'it’s pronouced jiff, not giff'));
+      flushAsynchronousOperations();
+
+      const saveOrDiscardStub = sandbox.stub();
+      element.addEventListener('thread-changed', saveOrDiscardStub);
+      const draftEl =
+          Polymer.dom(element.root).querySelectorAll('gr-comment')[1];
+      assert.ok(draftEl);
+      draftEl.addEventListener('comment-discard', () => {
+        const drafts = element.comments.filter(c => {
+          return c.__draft == true;
+        });
+        assert.equal(drafts.length, 0);
+        assert.isTrue(saveOrDiscardStub.called);
+        assert.equal(saveOrDiscardStub.lastCall.args[0].detail.rootId,
+            element.rootId);
+        assert.equal(saveOrDiscardStub.lastCall.args[0].detail.path,
+            element.path);
+        done();
+      });
+      draftEl.fire('comment-discard', {comment: draftEl.comment},
+          {bubbles: false});
+    });
+
+    test('discard with a single comment still fires event with previous rootId',
+        done => {
+          element.changeNum = '42';
+          element.patchNum = '1';
+          element.path = '/path/to/file.txt';
+          element.comments = [];
+          element.addOrEditDraft('1');
+          flushAsynchronousOperations();
+          const rootId = element.rootId;
+          assert.isOk(rootId);
+
+          const saveOrDiscardStub = sandbox.stub();
+          element.addEventListener('thread-changed', saveOrDiscardStub);
+          const draftEl =
+          Polymer.dom(element.root).querySelectorAll('gr-comment')[0];
+          assert.ok(draftEl);
+          draftEl.addEventListener('comment-discard', () => {
+            assert.equal(element.comments.length, 0);
+            assert.isTrue(saveOrDiscardStub.called);
+            assert.equal(saveOrDiscardStub.lastCall.args[0].detail.rootId,
+                rootId);
+            assert.equal(saveOrDiscardStub.lastCall.args[0].detail.path,
+                element.path);
+            done();
+          });
+          draftEl.fire('comment-discard', {comment: draftEl.comment},
+              {bubbles: false});
+        });
+
+    test('first editing comment does not add __otherEditing attribute', () => {
+      element.comments = [{
+        author: {
+          name: 'Mr. Peanutbutter',
+          email: 'tenn1sballchaser@aol.com',
+        },
+        id: 'baf0414d_60047215',
+        line: 5,
+        message: 'is this a crossover episode!?',
+        updated: '2015-12-08 19:48:33.843000000',
+        __draft: true,
+      }];
+
+      const replyBtn = element.$.replyBtn;
+      MockInteractions.tap(replyBtn);
+      flushAsynchronousOperations();
+
+      const editing = element._orderedComments.filter(c => {
+        return c.__editing == true;
+      });
+      assert.equal(editing.length, 1);
+      assert.equal(!!editing[0].__otherEditing, false);
+    });
+
+    test('When not editing other comments, local storage not set' +
+        ' after discard', done => {
+      element.changeNum = '42';
+      element.patchNum = '1';
+      element.comments = [{
+        author: {
+          name: 'Mr. Peanutbutter',
+          email: 'tenn1sballchaser@aol.com',
+        },
+        id: 'baf0414d_60047215',
+        line: 5,
+        message: 'is this a crossover episode!?',
+        updated: '2015-12-08 19:48:31.843000000',
+      },
+      {
+        author: {
+          name: 'Mr. Peanutbutter',
+          email: 'tenn1sballchaser@aol.com',
+        },
+        __draftID: '1',
+        in_reply_to: 'baf0414d_60047215',
+        line: 5,
+        message: 'yes',
+        updated: '2015-12-08 19:48:32.843000000',
+        __draft: true,
+        __editing: true,
+      },
+      {
+        author: {
+          name: 'Mr. Peanutbutter',
+          email: 'tenn1sballchaser@aol.com',
+        },
+        __draftID: '2',
+        in_reply_to: 'baf0414d_60047215',
+        line: 5,
+        message: 'no',
+        updated: '2015-12-08 19:48:33.843000000',
+        __draft: true,
+      }];
+      const storageStub = sinon.stub(element.$.storage, 'setDraftComment');
+      flushAsynchronousOperations();
+
+      const draftEl =
+      Polymer.dom(element.root).querySelectorAll('gr-comment')[1];
+      assert.ok(draftEl);
+      draftEl.addEventListener('comment-discard', () => {
+        assert.isFalse(storageStub.called);
+        storageStub.restore();
+        done();
+      });
+      draftEl.fire('comment-discard', {comment: draftEl.comment},
+          {bubbles: false});
+    });
+
+    test('comment-update', () => {
+      const commentEl = element.$$('gr-comment');
+      const updatedComment = {
+        id: element.comments[0].id,
+        foo: 'bar',
+      };
+      commentEl.fire('comment-update', {comment: updatedComment});
+      assert.strictEqual(element.comments[0], updatedComment);
+    });
+
+    suite('jack and sally comment data test consolidation', () => {
+      setup(() => {
+        element.comments = [
+          {
+            id: 'jacks_reply',
+            message: 'i like you, too',
+            in_reply_to: 'sallys_confession',
+            updated: '2015-12-25 15:00:20.396000000',
+            unresolved: false,
+          }, {
+            id: 'sallys_confession',
+            in_reply_to: 'nonexistent_comment',
+            message: 'i like you, jack',
+            updated: '2015-12-24 15:00:20.396000000',
+          }, {
+            id: 'sally_to_dr_finklestein',
+            in_reply_to: 'nonexistent_comment',
+            message: 'i’m running away',
+            updated: '2015-10-31 09:00:20.396000000',
+          }, {
+            id: 'sallys_defiance',
+            message: 'i will poison you so i can get away',
+            updated: '2015-10-31 15:00:20.396000000',
+          }];
+      });
+
+      test('orphan replies', () => {
+        assert.equal(4, element._orderedComments.length);
+      });
+
+      test('keyboard shortcuts', () => {
+        const expandCollapseStub =
+            sinon.stub(element, '_expandCollapseComments');
+        MockInteractions.pressAndReleaseKeyOn(element, 69, null, 'e');
+        assert.isTrue(expandCollapseStub.lastCall.calledWith(false));
+
+        MockInteractions.pressAndReleaseKeyOn(element, 69, 'shift', 'e');
+        assert.isTrue(expandCollapseStub.lastCall.calledWith(true));
+      });
+
+      test('comment in_reply_to is either null or most recent comment', () => {
+        element._createReplyComment(element.comments[3], 'dummy', true);
+        flushAsynchronousOperations();
+        assert.equal(element._orderedComments.length, 5);
+        assert.equal(element._orderedComments[4].in_reply_to, 'jacks_reply');
+      });
+
+      test('resolvable comments', () => {
+        assert.isFalse(element.unresolved);
+        element._createReplyComment(element.comments[3], 'dummy', true, true);
+        flushAsynchronousOperations();
+        assert.isTrue(element.unresolved);
+      });
+
+      test('_setInitialExpandedState', () => {
+        element.unresolved = true;
+        element._setInitialExpandedState();
+        for (let i = 0; i < element.comments.length; i++) {
+          assert.isFalse(element.comments[i].collapsed);
+        }
+        element.unresolved = false;
+        element._setInitialExpandedState();
+        for (let i = 0; i < element.comments.length; i++) {
+          assert.isTrue(element.comments[i].collapsed);
+        }
+        for (let i = 0; i < element.comments.length; i++) {
+          element.comments[i].robot_id = 123;
+        }
+        element._setInitialExpandedState();
+        for (let i = 0; i < element.comments.length; i++) {
+          assert.isFalse(element.comments[i].collapsed);
+        }
+      });
+    });
+
+    test('_computeHostClass', () => {
+      assert.equal(element._computeHostClass(true), 'unresolved');
+      assert.equal(element._computeHostClass(false), '');
+    });
+
+    test('addDraft sets unresolved state correctly', () => {
+      let unresolved = true;
+      element.comments = [];
+      element.addDraft(null, null, unresolved);
+      assert.equal(element.comments[0].unresolved, true);
+
+      unresolved = false; // comment should get added as actually resolved.
+      element.comments = [];
+      element.addDraft(null, null, unresolved);
+      assert.equal(element.comments[0].unresolved, false);
+
+      element.comments = [];
+      element.addDraft();
+      assert.equal(element.comments[0].unresolved, true);
+    });
+
+    test('_newDraft', () => {
+      element.commentSide = 'left';
+      element.patchNum = 3;
+      const draft = element._newDraft();
+      assert.equal(draft.__commentSide, 'left');
+      assert.equal(draft.patchNum, 3);
+    });
+
+    test('new comment gets created', () => {
+      element.comments = [];
+      element.addOrEditDraft(1);
+      assert.equal(element.comments.length, 1);
+      // Mock a submitted comment.
+      element.comments[0].id = element.comments[0].__draftID;
+      element.comments[0].__draft = false;
+      element.addOrEditDraft(1);
+      assert.equal(element.comments.length, 2);
+    });
+
+    test('unresolved label', () => {
+      element.unresolved = false;
+      assert.isTrue(element.$.unresolvedLabel.hasAttribute('hidden'));
+      element.unresolved = true;
+      assert.isFalse(element.$.unresolvedLabel.hasAttribute('hidden'));
+    });
+
+    test('draft comments are at the end of orderedComments', () => {
+      element.comments = [{
+        author: {
+          name: 'Mr. Peanutbutter',
+          email: 'tenn1sballchaser@aol.com',
+        },
+        id: 2,
+        line: 5,
+        message: 'Earlier draft',
+        updated: '2015-12-08 19:48:33.843000000',
+        __draft: true,
+      },
+      {
+        author: {
+          name: 'Mr. Peanutbutter2',
+          email: 'tenn1sballchaser@aol.com',
+        },
+        id: 1,
+        line: 5,
+        message: 'This comment was left last but is not a draft',
+        updated: '2015-12-10 19:48:33.843000000',
+      },
+      {
+        author: {
+          name: 'Mr. Peanutbutter2',
+          email: 'tenn1sballchaser@aol.com',
+        },
+        id: 3,
+        line: 5,
+        message: 'Later draft',
+        updated: '2015-12-09 19:48:33.843000000',
+        __draft: true,
+      }];
+      assert.equal(element._orderedComments[0].id, '1');
+      assert.equal(element._orderedComments[1].id, '2');
+      assert.equal(element._orderedComments[2].id, '3');
+    });
+
+    test('reflects lineNum and commentSide to attributes', () => {
+      element.lineNum = 7;
+      element.commentSide = 'left';
+
+      assert.equal(element.getAttribute('line-num'), '7');
+      assert.equal(element.getAttribute('comment-side'), 'left');
+    });
+
+    test('reflects range to JSON serialized attribute if set', () => {
+      element.range = {
+        start_line: 4,
+        end_line: 5,
+        start_character: 6,
+        end_character: 7,
+      };
+
+      assert.deepEqual(
+          JSON.parse(element.getAttribute('range')),
+          {start_line: 4, end_line: 5, start_character: 6, end_character: 7});
+    });
+
+    test('removes range attribute if range is unset', () => {
+      element.range = {
+        start_line: 4,
+        end_line: 5,
+        start_character: 6,
+        end_character: 7,
+      };
+      element.range = undefined;
+
+      assert.notOk(element.hasAttribute('range'));
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html
new file mode 100644
index 0000000..a470285
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html
@@ -0,0 +1,388 @@
+<!--
+@license
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<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="../../../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">
+<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
+<link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-dialog/gr-dialog.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">
+<link rel="import" href="../../shared/gr-icons/gr-icons.html">
+<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="../../shared/gr-textarea/gr-textarea.html">
+<link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html">
+<link rel="import" href="../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html">
+<script src="../../../scripts/rootElement.js"></script>
+
+<dom-module id="gr-comment">
+  <template>
+    <style include="shared-styles">
+      :host {
+        display: block;
+        font-family: var(--font-family);
+        padding: .7em .7em;
+        --iron-autogrow-textarea: {
+          box-sizing: border-box;
+          padding: 2px;
+        };
+      }
+      :host([disabled]) {
+        pointer-events: none;
+      }
+      :host([disabled]) .actions,
+      :host([disabled]) .robotActions,
+      :host([disabled]) .date {
+        opacity: .5;
+      }
+      :host([discarding]) {
+        display: none;
+      }
+      .header {
+        align-items: baseline;
+        cursor: pointer;
+        display: flex;
+        font-family: 'Open Sans', sans-serif;
+        margin: -.7em -.7em 0 -.7em;
+        padding: .7em;
+      }
+      .container.collapsed .header {
+        margin-bottom: -.7em;
+      }
+      .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);
+        display: none;
+      }
+      .date {
+        justify-content: flex-end;
+        margin-left: 5px;
+        min-width: 4.5em;
+        text-align: right;
+        white-space: nowrap;
+      }
+      span.date {
+        color: var(--deemphasized-text-color);
+      }
+      span.date:hover {
+        text-decoration: underline;
+      }
+      .actions {
+        display: flex;
+        justify-content: flex-end;
+        padding-top: 0;
+      }
+      .action {
+        margin-left: 1em;
+      }
+      .robotActions {
+        display: flex;
+        justify-content: flex-start;
+        padding-top: 0;
+      }
+      .robotActions .action {
+        /* Keep button text lined up with output text */
+        margin-left: -.3rem;
+        margin-right: 1em;
+      }
+      .rightActions {
+        display: flex;
+        justify-content: flex-end;
+      }
+      .editMessage {
+        display: none;
+        margin: .5em 0;
+        width: 100%;
+      }
+      .container:not(.draft) .actions .hideOnPublished {
+        display: none;
+      }
+      .draft .reply,
+      .draft .quote,
+      .draft .ack,
+      .draft .done {
+        display: none;
+      }
+      .draft .draftLabel,
+      .draft .draftTooltip {
+        display: inline;
+      }
+      .draft:not(.editing) .save,
+      .draft:not(.editing) .cancel {
+        display: none;
+      }
+      .editing .message,
+      .editing .reply,
+      .editing .quote,
+      .editing .ack,
+      .editing .done,
+      .editing .edit,
+      .editing .discard,
+      .editing .unresolved {
+        display: none;
+      }
+      .editing .editMessage {
+        display: block;
+      }
+      .show-hide {
+        margin-left: .4em;
+      }
+      .robotId {
+        color: var(--deemphasized-text-color);
+        margin-bottom: .8em;
+        margin-top: -.4em;
+      }
+      .robotIcon {
+        margin-right: .2em;
+        /* 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;
+      }
+      .runIdInformation {
+        margin: .7em 0;
+      }
+      .robotRun {
+        margin-left: .5em;
+      }
+      .robotRunLink {
+        margin-left: .5em;
+      }
+      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;
+      }
+      #container .collapsedContent {
+        display: none;
+      }
+      #container.collapsed {
+        padding-bottom: 3px;
+      }
+      #container.collapsed .collapsedContent {
+        display: block;
+        overflow: hidden;
+        padding-left: 5px;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+      }
+      #container.collapsed .actions,
+      #container.collapsed gr-formatted-text,
+      #container.collapsed gr-textarea {
+        display: none;
+      }
+      .resolve,
+      .unresolved {
+        align-items: center;
+        display: flex;
+        flex: 1;
+        margin: 0;
+      }
+      .resolve label {
+        color: var(--comment-text-color);
+      }
+      gr-dialog .main {
+        display: flex;
+        flex-direction: column;
+        width: 100%;
+      }
+      #deleteBtn {
+        display: none;
+        --gr-button: {
+          color: var(--deemphasized-text-color);
+          padding: 0;
+        }
+      }
+      #deleteBtn.showDeleteButtons {
+        display: block;
+      }
+    </style>
+    <div id="container" class="container">
+      <div class="header" id="header" on-tap="_handleToggleCollapsed">
+        <div class="headerLeft">
+          <span class="authorName">[[comment.author.name]]</span>
+          <span class="draftLabel">DRAFT</span>
+          <gr-tooltip-content class="draftTooltip"
+              has-tooltip
+              title="This draft is only visible to you. To publish drafts, click the 'Reply' or 'Start review' button at the top of the change or press the 'A' key."
+              max-width="20em"
+              show-icon></gr-tooltip-content>
+        </div>
+        <div class="headerMiddle">
+          <span class="collapsedContent">[[comment.message]]</span>
+        </div>
+        <gr-button
+            id="deleteBtn"
+            link
+            secondary
+            class$="action delete [[_computeDeleteButtonClass(_isAdmin, draft)]]"
+            on-tap="_handleCommentDelete">
+          (Delete)
+        </gr-button>
+        <span class="date" on-tap="_handleAnchorTap">
+          <gr-date-formatter
+              has-tooltip
+              date-str="[[comment.updated]]"></gr-date-formatter>
+        </span>
+        <div class="show-hide">
+          <label class="show-hide">
+            <input type="checkbox" class="show-hide"
+               checked$="[[collapsed]]"
+               on-change="_handleToggleCollapsed">
+            [[_computeShowHideText(collapsed)]]
+          </label>
+        </div>
+      </div>
+      <div class="body">
+        <template is="dom-if" if="[[comment.robot_id]]">
+          <div class="robotId" hidden$="[[collapsed]]">
+            <iron-icon class="robotIcon" icon="gr-icons:robot"></iron-icon>
+            [[comment.robot_id]]
+          </div>
+        </template>
+        <template is="dom-if" if="[[editing]]">
+          <gr-textarea
+              id="editTextarea"
+              class="editMessage"
+              autocomplete="on"
+              monospace
+              disabled="{{disabled}}"
+              rows="4"
+              text="{{_messageText}}"></gr-textarea>
+        </template>
+        <!--The message class is needed to ensure selectability from
+        gr-diff-selection.-->
+        <gr-formatted-text class="message"
+            content="[[comment.message]]"
+            no-trailing-margin="[[!comment.__draft]]"
+            collapsed="[[collapsed]]"
+            config="[[projectConfig.commentlinks]]"></gr-formatted-text>
+        <div hidden$="[[!comment.robot_run_id]]" class="message">
+          <div class="runIdInformation" hidden$="[[collapsed]]">
+            Run ID:
+            <template is="dom-if" if="[[comment.url]]">
+              <a class="robotRunLink" href$="[[comment.url]]">
+                <span class="robotRun link">[[comment.robot_run_id]]</span>
+              </a>
+            </template>
+            <template is="dom-if" if="[[!comment.url]]">
+              <span class="robotRun text">[[comment.robot_run_id]]</span>
+            </template>
+          </div>
+        </div>
+        <div class="actions humanActions" hidden$="[[!_showHumanActions]]">
+          <div class="action resolve hideOnPublished">
+            <label>
+              <input type="checkbox"
+                  id="resolvedCheckbox"
+                  checked="[[resolved]]"
+                  on-change="_handleToggleResolved">
+              Resolved
+            </label>
+          </div>
+          <div class="rightActions">
+            <gr-button
+                link
+                secondary
+                class="action cancel hideOnPublished"
+                on-tap="_handleCancel">Cancel</gr-button>
+            <gr-button
+                link
+                secondary
+                class="action discard hideOnPublished"
+                on-tap="_handleDiscard">Discard</gr-button>
+            <gr-button
+                link
+                secondary
+                class="action edit hideOnPublished"
+                on-tap="_handleEdit">Edit</gr-button>
+            <gr-button
+                link
+                secondary
+                disabled$="[[_computeSaveDisabled(_messageText, comment, resolved)]]"
+                class="action save hideOnPublished"
+                on-tap="_handleSave">Save</gr-button>
+          </div>
+        </div>
+        <div class="robotActions" hidden$="[[!_showRobotActions]]">
+          <template is="dom-if" if="[[isRobotComment]]">
+            <gr-button
+                link
+                secondary
+                class="action fix"
+                on-tap="_handleFix"
+                disabled="[[robotButtonDisabled]]">
+              Please Fix
+            </gr-button>
+            <gr-endpoint-decorator name="robot-comment-controls">
+              <gr-endpoint-param name="comment" value="[[comment]]">
+              </gr-endpoint-param>
+            </gr-endpoint-decorator>
+          </template>
+        </div>
+      </div>
+    </div>
+    <template is="dom-if" if="[[_enableOverlay]]">
+      <gr-overlay id="confirmDeleteOverlay" with-backdrop>
+        <gr-confirm-delete-comment-dialog id="confirmDeleteComment"
+            on-confirm="_handleConfirmDeleteComment"
+            on-cancel="_handleCancelDeleteComment">
+        </gr-confirm-delete-comment-dialog>
+      </gr-overlay>
+      <gr-overlay id="confirmDiscardOverlay" with-backdrop>
+        <gr-dialog
+            id="confirmDiscardDialog"
+            confirm-label="Discard"
+            confirm-on-enter
+            on-confirm="_handleConfirmDiscard"
+            on-cancel="_closeConfirmDiscardOverlay">
+          <div class="header" slot="header">
+            Discard comment
+          </div>
+          <div class="main" slot="main">
+            Are you sure you want to discard this draft comment?
+          </div>
+        </gr-dialog>
+      </gr-overlay>
+    </template>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+    <gr-storage id="storage"></gr-storage>
+    <gr-reporting id="reporting"></gr-reporting>
+  </template>
+  <script src="gr-comment.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
new file mode 100644
index 0000000..b699b8b
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
@@ -0,0 +1,649 @@
+/**
+ * @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 STORAGE_DEBOUNCE_INTERVAL = 400;
+  const TOAST_DEBOUNCE_INTERVAL = 200;
+
+  const SAVING_MESSAGE = 'Saving';
+  const DRAFT_SINGULAR = 'draft...';
+  const DRAFT_PLURAL = 'drafts...';
+  const SAVED_MESSAGE = 'All changes saved';
+
+  const REPORT_CREATE_DRAFT = 'CreateDraftComment';
+  const REPORT_UPDATE_DRAFT = 'UpdateDraftComment';
+  const REPORT_DISCARD_DRAFT = 'DiscardDraftComment';
+
+  const FILE = 'FILE';
+
+  Polymer({
+    is: 'gr-comment',
+    _legacyUndefinedCheck: true,
+
+    /**
+     * Fired when the create fix comment action is triggered.
+     *
+     * @event create-fix-comment
+     */
+
+    /**
+     * Fired when this comment is discarded.
+     *
+     * @event comment-discard
+     */
+
+    /**
+     * Fired when this comment is saved.
+     *
+     * @event comment-save
+     */
+
+    /**
+     * Fired when this comment is updated.
+     *
+     * @event comment-update
+     */
+
+    /**
+     * Fired when the comment's timestamp is tapped.
+     *
+     * @event comment-anchor-tap
+     */
+
+    properties: {
+      changeNum: String,
+      /** @type {?} */
+      comment: {
+        type: Object,
+        notify: true,
+        observer: '_commentChanged',
+      },
+      isRobotComment: {
+        type: Boolean,
+        value: false,
+        reflectToAttribute: true,
+      },
+      disabled: {
+        type: Boolean,
+        value: false,
+        reflectToAttribute: true,
+      },
+      draft: {
+        type: Boolean,
+        value: false,
+        observer: '_draftChanged',
+      },
+      editing: {
+        type: Boolean,
+        value: false,
+        observer: '_editingChanged',
+      },
+      discarding: {
+        type: Boolean,
+        value: false,
+        reflectToAttribute: true,
+      },
+      hasChildren: Boolean,
+      patchNum: String,
+      showActions: Boolean,
+      _showHumanActions: Boolean,
+      _showRobotActions: Boolean,
+      collapsed: {
+        type: Boolean,
+        value: true,
+        observer: '_toggleCollapseClass',
+      },
+      /** @type {?} */
+      projectConfig: Object,
+      robotButtonDisabled: Boolean,
+      _isAdmin: {
+        type: Boolean,
+        value: false,
+      },
+
+      _xhrPromise: Object, // Used for testing.
+      _messageText: {
+        type: String,
+        value: '',
+        observer: '_messageTextChanged',
+      },
+      commentSide: String,
+
+      resolved: Boolean,
+
+      _numPendingDraftRequests: {
+        type: Object,
+        value: {number: 0}, // Intentional to share the object across instances.
+      },
+
+      _enableOverlay: {
+        type: Boolean,
+        value: false,
+      },
+
+      /**
+       * Property for storing references to overlay elements. When the overlays
+       * are moved to Gerrit.getRootElement() to be shown they are no-longer
+       * children, so they can't be queried along the tree, so they are stored
+       * here.
+       */
+      _overlays: {
+        type: Object,
+        value: () => ({}),
+      },
+    },
+
+    observers: [
+      '_commentMessageChanged(comment.message)',
+      '_loadLocalDraft(changeNum, patchNum, comment)',
+      '_isRobotComment(comment)',
+      '_calculateActionstoShow(showActions, isRobotComment)',
+    ],
+
+    behaviors: [
+      Gerrit.KeyboardShortcutBehavior,
+    ],
+
+    keyBindings: {
+      'ctrl+enter meta+enter ctrl+s meta+s': '_handleSaveKey',
+      'esc': '_handleEsc',
+    },
+
+    attached() {
+      if (this.editing) {
+        this.collapsed = false;
+      } else if (this.comment) {
+        this.collapsed = this.comment.collapsed;
+      }
+      this._getIsAdmin().then(isAdmin => {
+        this._isAdmin = isAdmin;
+      });
+    },
+
+    detached() {
+      this.cancelDebouncer('fire-update');
+      if (this.textarea) {
+        this.textarea.closeDropdown();
+      }
+    },
+
+    get textarea() {
+      return this.$$('#editTextarea');
+    },
+
+    get confirmDeleteOverlay() {
+      if (!this._overlays.confirmDelete) {
+        this._enableOverlay = true;
+        Polymer.dom.flush();
+        this._overlays.confirmDelete = this.$$('#confirmDeleteOverlay');
+      }
+      return this._overlays.confirmDelete;
+    },
+
+    get confirmDiscardOverlay() {
+      if (!this._overlays.confirmDiscard) {
+        this._enableOverlay = true;
+        Polymer.dom.flush();
+        this._overlays.confirmDiscard = this.$$('#confirmDiscardOverlay');
+      }
+      return this._overlays.confirmDiscard;
+    },
+
+    _computeShowHideText(collapsed) {
+      return collapsed ? '◀' : '▼';
+    },
+
+    _calculateActionstoShow(showActions, isRobotComment) {
+      this._showHumanActions = showActions && !isRobotComment;
+      this._showRobotActions = showActions && isRobotComment;
+    },
+
+    _isRobotComment(comment) {
+      this.isRobotComment = !!comment.robot_id;
+    },
+
+    isOnParent() {
+      return this.side === 'PARENT';
+    },
+
+    _getIsAdmin() {
+      return this.$.restAPI.getIsAdmin();
+    },
+
+    /**
+     * @param {*=} opt_comment
+     */
+    save(opt_comment) {
+      let comment = opt_comment;
+      if (!comment) { comment = this.comment; }
+
+      this.set('comment.message', this._messageText);
+      this.editing = false;
+      this.disabled = true;
+
+      if (!this._messageText) {
+        return this._discardDraft();
+      }
+
+      this._xhrPromise = this._saveDraft(comment).then(response => {
+        this.disabled = false;
+        if (!response.ok) { return response; }
+
+        this._eraseDraftComment();
+        return this.$.restAPI.getResponseObject(response).then(obj => {
+          const resComment = obj;
+          resComment.__draft = true;
+          // Maintain the ephemeral draft ID for identification by other
+          // elements.
+          if (this.comment.__draftID) {
+            resComment.__draftID = this.comment.__draftID;
+          }
+          resComment.__commentSide = this.commentSide;
+          this.comment = resComment;
+          this._fireSave();
+          return obj;
+        });
+      }).catch(err => {
+        this.disabled = false;
+        throw err;
+      });
+
+      return this._xhrPromise;
+    },
+
+    _eraseDraftComment() {
+      // Prevents a race condition in which removing the draft comment occurs
+      // prior to it being saved.
+      this.cancelDebouncer('store');
+
+      this.$.storage.eraseDraftComment({
+        changeNum: this.changeNum,
+        patchNum: this._getPatchNum(),
+        path: this.comment.path,
+        line: this.comment.line,
+        range: this.comment.range,
+      });
+    },
+
+    _commentChanged(comment) {
+      this.editing = !!comment.__editing;
+      this.resolved = !comment.unresolved;
+      if (this.editing) { // It's a new draft/reply, notify.
+        this._fireUpdate();
+      }
+    },
+
+    /**
+     * @param {!Object=} opt_mixin
+     *
+     * @return {!Object}
+     */
+    _getEventPayload(opt_mixin) {
+      return Object.assign({}, opt_mixin, {
+        comment: this.comment,
+        patchNum: this.patchNum,
+      });
+    },
+
+    _fireSave() {
+      this.fire('comment-save', this._getEventPayload());
+    },
+
+    _fireUpdate() {
+      this.debounce('fire-update', () => {
+        this.fire('comment-update', this._getEventPayload());
+      });
+    },
+
+    _draftChanged(draft) {
+      this.$.container.classList.toggle('draft', draft);
+    },
+
+    _editingChanged(editing, previousValue) {
+      this.$.container.classList.toggle('editing', editing);
+      if (this.comment && this.comment.id) {
+        this.$$('.cancel').hidden = !editing;
+      }
+      if (this.comment) {
+        this.comment.__editing = this.editing;
+      }
+      if (editing != !!previousValue) {
+        // To prevent event firing on comment creation.
+        this._fireUpdate();
+      }
+      if (editing) {
+        this.async(() => {
+          Polymer.dom.flush();
+          this.textarea.putCursorAtEnd();
+        }, 1);
+      }
+    },
+
+    _computeDeleteButtonClass(isAdmin, draft) {
+      return isAdmin && !draft ? 'showDeleteButtons' : '';
+    },
+
+    _computeSaveDisabled(draft, comment, resolved) {
+      // If resolved state has changed and a msg exists, save should be enabled.
+      if (comment.unresolved === resolved && draft) { return false; }
+      return !draft || draft.trim() === '';
+    },
+
+    _handleSaveKey(e) {
+      if (!this._computeSaveDisabled(this._messageText, this.comment,
+          this.resolved)) {
+        e.preventDefault();
+        this._handleSave(e);
+      }
+    },
+
+    _handleEsc(e) {
+      if (!this._messageText.length) {
+        e.preventDefault();
+        this._handleCancel(e);
+      }
+    },
+
+    _handleToggleCollapsed() {
+      this.collapsed = !this.collapsed;
+    },
+
+    _toggleCollapseClass(collapsed) {
+      if (collapsed) {
+        this.$.container.classList.add('collapsed');
+      } else {
+        this.$.container.classList.remove('collapsed');
+      }
+    },
+
+    _commentMessageChanged(message) {
+      this._messageText = message || '';
+    },
+
+    _messageTextChanged(newValue, oldValue) {
+      if (!this.comment || (this.comment && this.comment.id)) { return; }
+
+      this.debounce('store', () => {
+        const message = this._messageText;
+        const commentLocation = {
+          changeNum: this.changeNum,
+          patchNum: this._getPatchNum(),
+          path: this.comment.path,
+          line: this.comment.line,
+          range: this.comment.range,
+        };
+
+        if ((!this._messageText || !this._messageText.length) && oldValue) {
+          // If the draft has been modified to be empty, then erase the storage
+          // entry.
+          this.$.storage.eraseDraftComment(commentLocation);
+        } else {
+          this.$.storage.setDraftComment(commentLocation, message);
+        }
+      }, STORAGE_DEBOUNCE_INTERVAL);
+    },
+
+    _handleAnchorTap(e) {
+      e.preventDefault();
+      if (!this.comment.line) { return; }
+      this.dispatchEvent(new CustomEvent('comment-anchor-tap', {
+        bubbles: true,
+        detail: {
+          number: this.comment.line || FILE,
+          side: this.side,
+        },
+      }));
+    },
+
+    _handleEdit(e) {
+      e.preventDefault();
+      this._messageText = this.comment.message;
+      this.editing = true;
+      this.$.reporting.recordDraftInteraction();
+    },
+
+    _handleSave(e) {
+      e.preventDefault();
+
+      // Ignore saves started while already saving.
+      if (this.disabled) { return; }
+      const timingLabel = this.comment.id ?
+        REPORT_UPDATE_DRAFT : REPORT_CREATE_DRAFT;
+      const timer = this.$.reporting.getTimer(timingLabel);
+      this.set('comment.__editing', false);
+      return this.save().then(() => { timer.end(); });
+    },
+
+    _handleCancel(e) {
+      e.preventDefault();
+
+      if (!this.comment.message ||
+          this.comment.message.trim().length === 0 ||
+          !this.comment.id) {
+        this._fireDiscard();
+        return;
+      }
+      this._messageText = this.comment.message;
+      this.editing = false;
+    },
+
+    _fireDiscard() {
+      this.cancelDebouncer('fire-update');
+      this.fire('comment-discard', this._getEventPayload());
+    },
+
+    _handleFix() {
+      this.dispatchEvent(new CustomEvent('create-fix-comment', {
+        bubbles: true,
+        detail: this._getEventPayload(),
+      }));
+    },
+
+    _handleDiscard(e) {
+      e.preventDefault();
+      this.$.reporting.recordDraftInteraction();
+
+      if (!this._messageText) {
+        this._discardDraft();
+        return;
+      }
+
+      this._openOverlay(this.confirmDiscardOverlay).then(() => {
+        this.confirmDiscardOverlay.querySelector('#confirmDiscardDialog')
+            .resetFocus();
+      });
+    },
+
+    _handleConfirmDiscard(e) {
+      e.preventDefault();
+      const timer = this.$.reporting.getTimer(REPORT_DISCARD_DRAFT);
+      this._closeConfirmDiscardOverlay();
+      return this._discardDraft().then(() => { timer.end(); });
+    },
+
+    _discardDraft() {
+      if (!this.comment.__draft) {
+        throw Error('Cannot discard a non-draft comment.');
+      }
+      this.discarding = true;
+      this.editing = false;
+      this.disabled = true;
+      this._eraseDraftComment();
+
+      if (!this.comment.id) {
+        this.disabled = false;
+        this._fireDiscard();
+        return;
+      }
+
+      this._xhrPromise = this._deleteDraft(this.comment).then(response => {
+        this.disabled = false;
+        if (!response.ok) {
+          this.discarding = false;
+          return response;
+        }
+
+        this._fireDiscard();
+      }).catch(err => {
+        this.disabled = false;
+        throw err;
+      });
+
+      return this._xhrPromise;
+    },
+
+    _closeConfirmDiscardOverlay() {
+      this._closeOverlay(this.confirmDiscardOverlay);
+    },
+
+    _getSavingMessage(numPending) {
+      if (numPending === 0) { return SAVED_MESSAGE; }
+      return [
+        SAVING_MESSAGE,
+        numPending,
+        numPending === 1 ? DRAFT_SINGULAR : DRAFT_PLURAL,
+      ].join(' ');
+    },
+
+    _showStartRequest() {
+      const numPending = ++this._numPendingDraftRequests.number;
+      this._updateRequestToast(numPending);
+    },
+
+    _showEndRequest() {
+      const numPending = --this._numPendingDraftRequests.number;
+      this._updateRequestToast(numPending);
+    },
+
+    _handleFailedDraftRequest() {
+      this._numPendingDraftRequests.number--;
+
+      // Cancel the debouncer so that error toasts from the error-manager will
+      // not be overridden.
+      this.cancelDebouncer('draft-toast');
+    },
+
+    _updateRequestToast(numPending) {
+      const message = this._getSavingMessage(numPending);
+      this.debounce('draft-toast', () => {
+        // Note: the event is fired on the body rather than this element because
+        // this element may not be attached by the time this executes, in which
+        // case the event would not bubble.
+        document.body.dispatchEvent(new CustomEvent('show-alert',
+            {detail: {message}, bubbles: true}));
+      }, TOAST_DEBOUNCE_INTERVAL);
+    },
+
+    _saveDraft(draft) {
+      this._showStartRequest();
+      return this.$.restAPI.saveDiffDraft(this.changeNum, this.patchNum, draft)
+          .then(result => {
+            if (result.ok) {
+              this._showEndRequest();
+            } else {
+              this._handleFailedDraftRequest();
+            }
+            return result;
+          });
+    },
+
+    _deleteDraft(draft) {
+      this._showStartRequest();
+      return this.$.restAPI.deleteDiffDraft(this.changeNum, this.patchNum,
+          draft).then(result => {
+        if (result.ok) {
+          this._showEndRequest();
+        } else {
+          this._handleFailedDraftRequest();
+        }
+        return result;
+      });
+    },
+
+    _getPatchNum() {
+      return this.isOnParent() ? 'PARENT' : this.patchNum;
+    },
+
+    _loadLocalDraft(changeNum, patchNum, comment) {
+      // Only apply local drafts to comments that haven't been saved
+      // remotely, and haven't been given a default message already.
+      //
+      // Don't get local draft if there is another comment that is currently
+      // in an editing state.
+      if (!comment || comment.id || comment.message || comment.__otherEditing) {
+        delete comment.__otherEditing;
+        return;
+      }
+
+      const draft = this.$.storage.getDraftComment({
+        changeNum,
+        patchNum: this._getPatchNum(),
+        path: comment.path,
+        line: comment.line,
+        range: comment.range,
+      });
+
+      if (draft) {
+        this.set('comment.message', draft.message);
+      }
+    },
+
+    _handleToggleResolved() {
+      this.$.reporting.recordDraftInteraction();
+      this.resolved = !this.resolved;
+      // Modify payload instead of this.comment, as this.comment is passed from
+      // the parent by ref.
+      const payload = this._getEventPayload();
+      payload.comment.unresolved = !this.$.resolvedCheckbox.checked;
+      this.fire('comment-update', payload);
+      if (!this.editing) {
+        // Save the resolved state immediately.
+        this.save(payload.comment);
+      }
+    },
+
+    _handleCommentDelete() {
+      this._openOverlay(this.confirmDeleteOverlay);
+    },
+
+    _handleCancelDeleteComment() {
+      this._closeOverlay(this.confirmDeleteOverlay);
+    },
+
+    _openOverlay(overlay) {
+      Polymer.dom(Gerrit.getRootElement()).appendChild(overlay);
+      return overlay.open();
+    },
+
+    _closeOverlay(overlay) {
+      Polymer.dom(Gerrit.getRootElement()).removeChild(overlay);
+      overlay.close();
+    },
+
+    _handleConfirmDeleteComment() {
+      const dialog =
+          this.confirmDeleteOverlay.querySelector('#confirmDeleteComment');
+      this.$.restAPI.deleteComment(
+          this.changeNum, this.patchNum, this.comment.id, dialog.message)
+          .then(newComment => {
+            this._handleCancelDeleteComment();
+            this.comment = newComment;
+          });
+    },
+  });
+})();
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
new file mode 100644
index 0000000..7ca5242
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html
@@ -0,0 +1,847 @@
+<!DOCTYPE html>
+<!--
+@license
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-comment</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="../../../bower_components/page/page.js"></script>
+<script src="../../../scripts/util.js"></script>
+
+<link rel="import" href="gr-comment.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-comment></gr-comment>
+  </template>
+</test-fixture>
+
+<test-fixture id="draft">
+  <template>
+    <gr-comment draft="true"></gr-comment>
+  </template>
+</test-fixture>
+
+<script>
+
+  function isVisible(el) {
+    assert.ok(el);
+    return getComputedStyle(el).getPropertyValue('display') !== 'none';
+  }
+
+  suite('gr-comment tests', () => {
+    let element;
+    let sandbox;
+    setup(() => {
+      stub('gr-rest-api-interface', {
+        getAccount() { return Promise.resolve(null); },
+      });
+      element = fixture('basic');
+      element.comment = {
+        author: {
+          name: 'Mr. Peanutbutter',
+          email: 'tenn1sballchaser@aol.com',
+        },
+        id: 'baf0414d_60047215',
+        line: 5,
+        message: 'is this a crossover episode!?',
+        updated: '2015-12-08 19:48:33.843000000',
+      };
+      sandbox = sinon.sandbox.create();
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    test('collapsible comments', () => {
+      // When a comment (not draft) is loaded, it should be collapsed
+      assert.isTrue(element.collapsed);
+      assert.isFalse(isVisible(element.$$('gr-formatted-text')),
+          'gr-formatted-text is not visible');
+      assert.isFalse(isVisible(element.$$('.actions')),
+          'actions are not visible');
+      assert.isNotOk(element.textarea, 'textarea is not visible');
+
+      // The header middle content is only visible when comments are collapsed.
+      // It shows the message in a condensed way, and limits to a single line.
+      assert.isTrue(isVisible(element.$$('.collapsedContent')),
+          'header middle content is visible');
+
+      // When the header row is clicked, the comment should expand
+      MockInteractions.tap(element.$.header);
+      assert.isFalse(element.collapsed);
+      assert.isTrue(isVisible(element.$$('gr-formatted-text')),
+          'gr-formatted-text is visible');
+      assert.isTrue(isVisible(element.$$('.actions')),
+          'actions are visible');
+      assert.isNotOk(element.textarea, 'textarea is not visible');
+      assert.isFalse(isVisible(element.$$('.collapsedContent')),
+          'header middle content is not visible');
+    });
+
+    test('clicking on date link fires event', () => {
+      element.side = 'PARENT';
+      const stub = sinon.stub();
+      element.addEventListener('comment-anchor-tap', stub);
+      const dateEl = element.$$('.date');
+      assert.ok(dateEl);
+      MockInteractions.tap(dateEl);
+
+      assert.isTrue(stub.called);
+      assert.deepEqual(stub.lastCall.args[0].detail,
+          {side: element.side, number: element.comment.line});
+    });
+
+    test('message is not retrieved from storage when other edits', done => {
+      const storageStub = sandbox.stub(element.$.storage, 'getDraftComment');
+      const loadSpy = sandbox.spy(element, '_loadLocalDraft');
+
+      element.changeNum = 1;
+      element.patchNum = 1;
+      element.comment = {
+        author: {
+          name: 'Mr. Peanutbutter',
+          email: 'tenn1sballchaser@aol.com',
+        },
+        line: 5,
+        __otherEditing: true,
+      };
+      flush(() => {
+        assert.isTrue(loadSpy.called);
+        assert.isFalse(storageStub.called);
+        done();
+      });
+    });
+
+    test('message is retrieved from storage when no other edits', done => {
+      const storageStub = sandbox.stub(element.$.storage, 'getDraftComment');
+      const loadSpy = sandbox.spy(element, '_loadLocalDraft');
+
+      element.changeNum = 1;
+      element.patchNum = 1;
+      element.comment = {
+        author: {
+          name: 'Mr. Peanutbutter',
+          email: 'tenn1sballchaser@aol.com',
+        },
+        line: 5,
+      };
+      flush(() => {
+        assert.isTrue(loadSpy.called);
+        assert.isTrue(storageStub.called);
+        done();
+      });
+    });
+
+    test('_getPatchNum', () => {
+      element.side = 'PARENT';
+      element.patchNum = 1;
+      assert.equal(element._getPatchNum(), 'PARENT');
+      element.side = 'REVISION';
+      assert.equal(element._getPatchNum(), 1);
+    });
+
+    test('comment expand and collapse', () => {
+      element.collapsed = true;
+      assert.isFalse(isVisible(element.$$('gr-formatted-text')),
+          'gr-formatted-text is not visible');
+      assert.isFalse(isVisible(element.$$('.actions')),
+          'actions are not visible');
+      assert.isNotOk(element.textarea, 'textarea is not visible');
+      assert.isTrue(isVisible(element.$$('.collapsedContent')),
+          'header middle content is visible');
+
+      element.collapsed = false;
+      assert.isFalse(element.collapsed);
+      assert.isTrue(isVisible(element.$$('gr-formatted-text')),
+          'gr-formatted-text is visible');
+      assert.isTrue(isVisible(element.$$('.actions')),
+          'actions are visible');
+      assert.isNotOk(element.textarea, 'textarea is not visible');
+      assert.isFalse(isVisible(element.$$('.collapsedContent')),
+          'header middle content is is not visible');
+    });
+
+    suite('while editing', () => {
+      setup(() => {
+        element.editing = true;
+        element._messageText = 'test';
+        sandbox.stub(element, '_handleCancel');
+        sandbox.stub(element, '_handleSave');
+        flushAsynchronousOperations();
+      });
+
+      suite('when text is empty', () => {
+        setup(() => {
+          element._messageText = '';
+          element.comment = {};
+        });
+
+        test('esc closes comment when text is empty', () => {
+          MockInteractions.pressAndReleaseKeyOn(
+              element.textarea, 27); // esc
+          assert.isTrue(element._handleCancel.called);
+        });
+
+        test('ctrl+enter does not save', () => {
+          MockInteractions.pressAndReleaseKeyOn(
+              element.textarea, 13, 'ctrl'); // ctrl + enter
+          assert.isFalse(element._handleSave.called);
+        });
+
+        test('meta+enter does not save', () => {
+          MockInteractions.pressAndReleaseKeyOn(
+              element.textarea, 13, 'meta'); // meta + enter
+          assert.isFalse(element._handleSave.called);
+        });
+
+        test('ctrl+s does not save', () => {
+          MockInteractions.pressAndReleaseKeyOn(
+              element.textarea, 83, 'ctrl'); // ctrl + s
+          assert.isFalse(element._handleSave.called);
+        });
+      });
+
+      test('esc does not close comment that has content', () => {
+        MockInteractions.pressAndReleaseKeyOn(
+            element.textarea, 27); // esc
+        assert.isFalse(element._handleCancel.called);
+      });
+
+      test('ctrl+enter saves', () => {
+        MockInteractions.pressAndReleaseKeyOn(
+            element.textarea, 13, 'ctrl'); // ctrl + enter
+        assert.isTrue(element._handleSave.called);
+      });
+
+      test('meta+enter saves', () => {
+        MockInteractions.pressAndReleaseKeyOn(
+            element.textarea, 13, 'meta'); // meta + enter
+        assert.isTrue(element._handleSave.called);
+      });
+
+      test('ctrl+s saves', () => {
+        MockInteractions.pressAndReleaseKeyOn(
+            element.textarea, 83, 'ctrl'); // ctrl + s
+        assert.isTrue(element._handleSave.called);
+      });
+    });
+    test('delete comment button for non-admins is hidden', () => {
+      element._isAdmin = false;
+      assert.isFalse(element.$$('.action.delete')
+          .classList.contains('showDeleteButtons'));
+    });
+
+    test('delete comment button for admins with draft is hidden', () => {
+      element._isAdmin = false;
+      element.draft = true;
+      assert.isFalse(element.$$('.action.delete')
+          .classList.contains('showDeleteButtons'));
+    });
+
+    test('delete comment', done => {
+      sandbox.stub(
+          element.$.restAPI, 'deleteComment').returns(Promise.resolve({}));
+      sandbox.spy(element.confirmDeleteOverlay, 'open');
+      element.changeNum = 42;
+      element.patchNum = 0xDEADBEEF;
+      element._isAdmin = true;
+      assert.isTrue(element.$$('.action.delete')
+          .classList.contains('showDeleteButtons'));
+      MockInteractions.tap(element.$$('.action.delete'));
+      flush(() => {
+        element.confirmDeleteOverlay.open.lastCall.returnValue.then(() => {
+          const dialog =
+              this.confirmDeleteOverlay.querySelector('#confirmDeleteComment');
+          dialog.message = 'removal reason';
+          element._handleConfirmDeleteComment();
+          assert.isTrue(element.$.restAPI.deleteComment.calledWith(
+              42, 0xDEADBEEF, 'baf0414d_60047215', 'removal reason'));
+          done();
+        });
+      });
+    });
+
+    suite('draft update reporting', () => {
+      let endStub;
+      let getTimerStub;
+      let mockEvent;
+
+      setup(() => {
+        mockEvent = {preventDefault() {}};
+        sandbox.stub(element, 'save')
+            .returns(Promise.resolve({}));
+        sandbox.stub(element, '_discardDraft')
+            .returns(Promise.resolve({}));
+        endStub = sinon.stub();
+        getTimerStub = sandbox.stub(element.$.reporting, 'getTimer')
+            .returns({end: endStub});
+      });
+
+      test('create', () => {
+        element.comment = {};
+        return element._handleSave(mockEvent).then(() => {
+          assert.isTrue(endStub.calledOnce);
+          assert.isTrue(getTimerStub.calledOnce);
+          assert.equal(getTimerStub.lastCall.args[0], 'CreateDraftComment');
+        });
+      });
+
+      test('update', () => {
+        element.comment = {id: 'abc_123'};
+        return element._handleSave(mockEvent).then(() => {
+          assert.isTrue(endStub.calledOnce);
+          assert.isTrue(getTimerStub.calledOnce);
+          assert.equal(getTimerStub.lastCall.args[0], 'UpdateDraftComment');
+        });
+      });
+
+      test('discard', () => {
+        element.comment = {id: 'abc_123'};
+        sandbox.stub(element, '_closeConfirmDiscardOverlay');
+        return element._handleConfirmDiscard(mockEvent).then(() => {
+          assert.isTrue(endStub.calledOnce);
+          assert.isTrue(getTimerStub.calledOnce);
+          assert.equal(getTimerStub.lastCall.args[0], 'DiscardDraftComment');
+        });
+      });
+    });
+
+    test('edit reports interaction', () => {
+      const reportStub = sandbox.stub(element.$.reporting,
+          'recordDraftInteraction');
+      MockInteractions.tap(element.$$('.edit'));
+      assert.isTrue(reportStub.calledOnce);
+    });
+
+    test('discard reports interaction', () => {
+      const reportStub = sandbox.stub(element.$.reporting,
+          'recordDraftInteraction');
+      element.draft = true;
+      MockInteractions.tap(element.$$('.discard'));
+      assert.isTrue(reportStub.calledOnce);
+    });
+  });
+
+  suite('gr-comment draft tests', () => {
+    let element;
+    let sandbox;
+
+    setup(() => {
+      stub('gr-rest-api-interface', {
+        getAccount() { return Promise.resolve(null); },
+        saveDiffDraft() {
+          return Promise.resolve({
+            ok: true,
+            text() {
+              return Promise.resolve(
+                  ')]}\'\n{' +
+                  '"id": "baf0414d_40572e03",' +
+                  '"path": "/path/to/file",' +
+                  '"line": 5,' +
+                  '"updated": "2015-12-08 21:52:36.177000000",' +
+                  '"message": "saved!"' +
+                '}'
+              );
+            },
+          });
+        },
+        removeChangeReviewer() {
+          return Promise.resolve({ok: true});
+        },
+      });
+      stub('gr-storage', {
+        getDraftComment() { return null; },
+      });
+      element = fixture('draft');
+      element.changeNum = 42;
+      element.patchNum = 1;
+      element.editing = false;
+      element.comment = {
+        __commentSide: 'right',
+        __draft: true,
+        __draftID: 'temp_draft_id',
+        path: '/path/to/file',
+        line: 5,
+      };
+      element.commentSide = 'right';
+      sandbox = sinon.sandbox.create();
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    test('button visibility states', () => {
+      element.showActions = false;
+      assert.isTrue(element.$$('.humanActions').hasAttribute('hidden'));
+      assert.isTrue(element.$$('.robotActions').hasAttribute('hidden'));
+
+      element.showActions = true;
+      assert.isFalse(element.$$('.humanActions').hasAttribute('hidden'));
+      assert.isTrue(element.$$('.robotActions').hasAttribute('hidden'));
+
+      element.draft = true;
+      assert.isTrue(isVisible(element.$$('.edit')), 'edit is visible');
+      assert.isTrue(isVisible(element.$$('.discard')), 'discard is visible');
+      assert.isFalse(isVisible(element.$$('.save')), 'save is not visible');
+      assert.isFalse(isVisible(element.$$('.cancel')), 'cancel is not visible');
+      assert.isTrue(isVisible(element.$$('.resolve')), 'resolve is visible');
+      assert.isFalse(element.$$('.humanActions').hasAttribute('hidden'));
+      assert.isTrue(element.$$('.robotActions').hasAttribute('hidden'));
+
+      element.editing = true;
+      flushAsynchronousOperations();
+      assert.isFalse(isVisible(element.$$('.edit')), 'edit is not visible');
+      assert.isFalse(isVisible(element.$$('.discard')), 'discard not visible');
+      assert.isTrue(isVisible(element.$$('.save')), 'save is visible');
+      assert.isTrue(isVisible(element.$$('.cancel')), 'cancel is visible');
+      assert.isTrue(isVisible(element.$$('.resolve')), 'resolve is visible');
+      assert.isFalse(element.$$('.humanActions').hasAttribute('hidden'));
+      assert.isTrue(element.$$('.robotActions').hasAttribute('hidden'));
+
+      element.draft = false;
+      element.editing = false;
+      flushAsynchronousOperations();
+      assert.isFalse(isVisible(element.$$('.edit')), 'edit is not visible');
+      assert.isFalse(isVisible(element.$$('.discard')),
+          'discard is not visible');
+      assert.isFalse(isVisible(element.$$('.save')), 'save is not visible');
+      assert.isFalse(isVisible(element.$$('.cancel')), 'cancel is not visible');
+      assert.isFalse(element.$$('.humanActions').hasAttribute('hidden'));
+      assert.isTrue(element.$$('.robotActions').hasAttribute('hidden'));
+
+      element.comment.id = 'foo';
+      element.draft = true;
+      element.editing = true;
+      flushAsynchronousOperations();
+      assert.isTrue(isVisible(element.$$('.cancel')), 'cancel is visible');
+      assert.isFalse(element.$$('.humanActions').hasAttribute('hidden'));
+      assert.isTrue(element.$$('.robotActions').hasAttribute('hidden'));
+
+      element.isRobotComment = true;
+      element.draft = true;
+      assert.isTrue(element.$$('.humanActions').hasAttribute('hidden'));
+      assert.isFalse(element.$$('.robotActions').hasAttribute('hidden'));
+
+      // It is not expected to see Robot comment drafts, but if they appear,
+      // they will behave the same as non-drafts.
+      element.draft = false;
+      assert.isTrue(element.$$('.humanActions').hasAttribute('hidden'));
+      assert.isFalse(element.$$('.robotActions').hasAttribute('hidden'));
+
+      // A robot comment with run ID should display plain text.
+      element.set(['comment', 'robot_run_id'], 'text');
+      element.editing = false;
+      element.collapsed = false;
+      flushAsynchronousOperations();
+      assert.isNotOk(element.$$('.robotRun.link'));
+      assert.notEqual(getComputedStyle(element.$$('.robotRun.text')).display,
+          'none');
+
+      // A robot comment with run ID and url should display a link.
+      element.set(['comment', 'url'], '/path/to/run');
+      flushAsynchronousOperations();
+      assert.notEqual(getComputedStyle(element.$$('.robotRun.link')).display,
+          'none');
+      assert.equal(getComputedStyle(element.$$('.robotRun.text')).display,
+          'none');
+    });
+
+    test('collapsible drafts', () => {
+      assert.isTrue(element.collapsed);
+      assert.isFalse(isVisible(element.$$('gr-formatted-text')),
+          'gr-formatted-text is not visible');
+      assert.isFalse(isVisible(element.$$('.actions')),
+          'actions are not visible');
+      assert.isNotOk(element.textarea, 'textarea is not visible');
+      assert.isTrue(isVisible(element.$$('.collapsedContent')),
+          'header middle content is visible');
+
+      MockInteractions.tap(element.$.header);
+      assert.isFalse(element.collapsed);
+      assert.isTrue(isVisible(element.$$('gr-formatted-text')),
+          'gr-formatted-text is visible');
+      assert.isTrue(isVisible(element.$$('.actions')),
+          'actions are visible');
+      assert.isNotOk(element.textarea, 'textarea is not visible');
+      assert.isFalse(isVisible(element.$$('.collapsedContent')),
+          'header middle content is is not visible');
+
+      // When the edit button is pressed, should still see the actions
+      // and also textarea
+      MockInteractions.tap(element.$$('.edit'));
+      flushAsynchronousOperations();
+      assert.isFalse(element.collapsed);
+      assert.isFalse(isVisible(element.$$('gr-formatted-text')),
+          'gr-formatted-text is not visible');
+      assert.isTrue(isVisible(element.$$('.actions')),
+          'actions are visible');
+      assert.isTrue(isVisible(element.textarea), 'textarea is visible');
+      assert.isFalse(isVisible(element.$$('.collapsedContent')),
+          'header middle content is not visible');
+
+      // When toggle again, everything should be hidden except for textarea
+      // and header middle content should be visible
+      MockInteractions.tap(element.$.header);
+      assert.isTrue(element.collapsed);
+      assert.isFalse(isVisible(element.$$('gr-formatted-text')),
+          'gr-formatted-text is not visible');
+      assert.isFalse(isVisible(element.$$('.actions')),
+          'actions are not visible');
+      assert.isFalse(isVisible(element.$$('gr-textarea')),
+          'textarea is not visible');
+      assert.isTrue(isVisible(element.$$('.collapsedContent')),
+          'header middle content is visible');
+
+      // When toggle again, textarea should remain open in the state it was
+      // before
+      MockInteractions.tap(element.$.header);
+      assert.isFalse(isVisible(element.$$('gr-formatted-text')),
+          'gr-formatted-text is not visible');
+      assert.isTrue(isVisible(element.$$('.actions')),
+          'actions are visible');
+      assert.isTrue(isVisible(element.textarea), 'textarea is visible');
+      assert.isFalse(isVisible(element.$$('.collapsedContent')),
+          'header middle content is not visible');
+    });
+
+    test('draft creation/cancellation', done => {
+      assert.isFalse(element.editing);
+      MockInteractions.tap(element.$$('.edit'));
+      assert.isTrue(element.editing);
+
+      element._messageText = '';
+      const eraseMessageDraftSpy = sandbox.spy(element, '_eraseDraftComment');
+
+      // Save should be disabled on an empty message.
+      let disabled = element.$$('.save').hasAttribute('disabled');
+      assert.isTrue(disabled, 'save button should be disabled.');
+      element._messageText = '     ';
+      disabled = element.$$('.save').hasAttribute('disabled');
+      assert.isTrue(disabled, 'save button should be disabled.');
+
+      const updateStub = sinon.stub();
+      element.addEventListener('comment-update', updateStub);
+
+      let numDiscardEvents = 0;
+      element.addEventListener('comment-discard', e => {
+        numDiscardEvents++;
+        assert.isFalse(eraseMessageDraftSpy.called);
+        if (numDiscardEvents === 2) {
+          assert.isFalse(updateStub.called);
+          done();
+        }
+      });
+      MockInteractions.tap(element.$$('.cancel'));
+      element.flushDebouncer('fire-update');
+      element._messageText = '';
+      flushAsynchronousOperations();
+      MockInteractions.pressAndReleaseKeyOn(element.textarea, 27); // esc
+    });
+
+    test('draft discard removes message from storage', done => {
+      element._messageText = '';
+      const eraseMessageDraftSpy = sandbox.spy(element, '_eraseDraftComment');
+      sandbox.stub(element, '_closeConfirmDiscardOverlay');
+
+      element.addEventListener('comment-discard', e => {
+        assert.isTrue(eraseMessageDraftSpy.called);
+        done();
+      });
+      element._handleConfirmDiscard({preventDefault: sinon.stub()});
+    });
+
+    test('storage is cleared only after save success', () => {
+      element._messageText = 'test';
+      const eraseStub = sandbox.stub(element, '_eraseDraftComment');
+      sandbox.stub(element.$.restAPI, 'getResponseObject')
+          .returns(Promise.resolve({}));
+
+      sandbox.stub(element, '_saveDraft').returns(Promise.resolve({ok: false}));
+
+      const savePromise = element.save();
+      assert.isFalse(eraseStub.called);
+      return savePromise.then(() => {
+        assert.isFalse(eraseStub.called);
+
+        element._saveDraft.restore();
+        sandbox.stub(element, '_saveDraft')
+            .returns(Promise.resolve({ok: true}));
+        return element.save().then(() => {
+          assert.isTrue(eraseStub.called);
+        });
+      });
+    });
+
+    test('_computeSaveDisabled', () => {
+      const comment = {unresolved: true};
+      const msgComment = {message: 'test', unresolved: true};
+      assert.equal(element._computeSaveDisabled('', comment, false), true);
+      assert.equal(element._computeSaveDisabled('test', comment, false), false);
+      assert.equal(element._computeSaveDisabled('', msgComment, false), true);
+      assert.equal(
+          element._computeSaveDisabled('test', msgComment, false), false);
+      assert.equal(
+          element._computeSaveDisabled('test2', msgComment, false), false);
+      assert.equal(element._computeSaveDisabled('test', comment, true), false);
+      assert.equal(element._computeSaveDisabled('', comment, true), true);
+      assert.equal(element._computeSaveDisabled('', comment, false), true);
+    });
+
+    suite('confirm discard', () => {
+      let discardStub;
+      let overlayStub;
+      let mockEvent;
+
+      setup(() => {
+        discardStub = sandbox.stub(element, '_discardDraft');
+        overlayStub = sandbox.stub(element, '_openOverlay')
+            .returns(Promise.resolve());
+        mockEvent = {preventDefault: sinon.stub()};
+      });
+
+      test('confirms discard of comments with message text', () => {
+        element._messageText = 'test';
+        element._handleDiscard(mockEvent);
+        assert.isTrue(overlayStub.calledWith(element.confirmDiscardOverlay));
+        assert.isFalse(discardStub.called);
+      });
+
+      test('no confirmation for comments without message text', () => {
+        element._messageText = '';
+        element._handleDiscard(mockEvent);
+        assert.isFalse(overlayStub.called);
+        assert.isTrue(discardStub.calledOnce);
+      });
+    });
+
+    test('ctrl+s saves comment', done => {
+      const stub = sinon.stub(element, 'save', () => {
+        assert.isTrue(stub.called);
+        stub.restore();
+        done();
+        return Promise.resolve();
+      });
+      element._messageText = 'is that the horse from horsing around??';
+      element.editing = true;
+      flushAsynchronousOperations();
+      MockInteractions.pressAndReleaseKeyOn(
+          element.textarea.$.textarea.textarea,
+          83, 'ctrl'); // 'ctrl + s'
+    });
+
+    test('draft saving/editing', done => {
+      const fireStub = sinon.stub(element, 'fire');
+      const cancelDebounce = sandbox.stub(element, 'cancelDebouncer');
+
+      element.draft = true;
+      MockInteractions.tap(element.$$('.edit'));
+      element._messageText = 'good news, everyone!';
+      element.flushDebouncer('fire-update');
+      element.flushDebouncer('store');
+      assert(fireStub.calledWith('comment-update'),
+          'comment-update should be sent');
+      assert.isTrue(fireStub.calledOnce);
+
+      element._messageText = 'good news, everyone!';
+      element.flushDebouncer('fire-update');
+      element.flushDebouncer('store');
+      assert.isTrue(fireStub.calledOnce,
+          'No events should fire for text editing');
+
+      MockInteractions.tap(element.$$('.save'));
+
+      assert.isTrue(element.disabled,
+          'Element should be disabled when creating draft.');
+
+      element._xhrPromise.then(draft => {
+        assert(fireStub.calledWith('comment-save'),
+            'comment-save should be sent');
+        assert(cancelDebounce.calledWith('store'));
+
+        assert.deepEqual(fireStub.lastCall.args[1], {
+          comment: {
+            __commentSide: 'right',
+            __draft: true,
+            __draftID: 'temp_draft_id',
+            id: 'baf0414d_40572e03',
+            line: 5,
+            message: 'saved!',
+            path: '/path/to/file',
+            updated: '2015-12-08 21:52:36.177000000',
+          },
+          patchNum: 1,
+        });
+        assert.isFalse(element.disabled,
+            'Element should be enabled when done creating draft.');
+        assert.equal(draft.message, 'saved!');
+        assert.isFalse(element.editing);
+      }).then(() => {
+        MockInteractions.tap(element.$$('.edit'));
+        element._messageText = 'You’ll be delivering a package to Chapek 9, ' +
+            'a world where humans are killed on sight.';
+        MockInteractions.tap(element.$$('.save'));
+        assert.isTrue(element.disabled,
+            'Element should be disabled when updating draft.');
+
+        element._xhrPromise.then(draft => {
+          assert.isFalse(element.disabled,
+              'Element should be enabled when done updating draft.');
+          assert.equal(draft.message, 'saved!');
+          assert.isFalse(element.editing);
+          fireStub.restore();
+          done();
+        });
+      });
+    });
+
+    test('draft prevent save when disabled', () => {
+      const saveStub = sandbox.stub(element, 'save').returns(Promise.resolve());
+      element.showActions = true;
+      element.draft = true;
+      MockInteractions.tap(element.$.header);
+      MockInteractions.tap(element.$$('.edit'));
+      element._messageText = 'good news, everyone!';
+      element.flushDebouncer('fire-update');
+      element.flushDebouncer('store');
+
+      element.disabled = true;
+      MockInteractions.tap(element.$$('.save'));
+      assert.isFalse(saveStub.called);
+
+      element.disabled = false;
+      MockInteractions.tap(element.$$('.save'));
+      assert.isTrue(saveStub.calledOnce);
+    });
+
+    test('proper event fires on resolve, comment is not saved', done => {
+      const save = sandbox.stub(element, 'save');
+      element.addEventListener('comment-update', e => {
+        assert.isTrue(e.detail.comment.unresolved);
+        assert.isFalse(save.called);
+        done();
+      });
+      MockInteractions.tap(element.$$('.resolve input'));
+    });
+
+    test('resolved comment state indicated by checkbox', () => {
+      sandbox.stub(element, 'save');
+      element.comment = {unresolved: false};
+      assert.isTrue(element.$$('.resolve input').checked);
+      element.comment = {unresolved: true};
+      assert.isFalse(element.$$('.resolve input').checked);
+    });
+
+    test('resolved checkbox saves with tap when !editing', () => {
+      element.editing = false;
+      const save = sandbox.stub(element, 'save');
+
+      element.comment = {unresolved: false};
+      assert.isTrue(element.$$('.resolve input').checked);
+      element.comment = {unresolved: true};
+      assert.isFalse(element.$$('.resolve input').checked);
+      assert.isFalse(save.called);
+      MockInteractions.tap(element.$.resolvedCheckbox);
+      assert.isTrue(element.$$('.resolve input').checked);
+      assert.isTrue(save.called);
+    });
+
+    suite('draft saving messages', () => {
+      test('_getSavingMessage', () => {
+        assert.equal(element._getSavingMessage(0), 'All changes saved');
+        assert.equal(element._getSavingMessage(1), 'Saving 1 draft...');
+        assert.equal(element._getSavingMessage(2), 'Saving 2 drafts...');
+        assert.equal(element._getSavingMessage(3), 'Saving 3 drafts...');
+      });
+
+      test('_show{Start,End}Request', () => {
+        const updateStub = sandbox.stub(element, '_updateRequestToast');
+        element._numPendingDraftRequests.number = 1;
+
+        element._showStartRequest();
+        assert.isTrue(updateStub.calledOnce);
+        assert.equal(updateStub.lastCall.args[0], 2);
+        assert.equal(element._numPendingDraftRequests.number, 2);
+
+        element._showEndRequest();
+        assert.isTrue(updateStub.calledTwice);
+        assert.equal(updateStub.lastCall.args[0], 1);
+        assert.equal(element._numPendingDraftRequests.number, 1);
+
+        element._showEndRequest();
+        assert.isTrue(updateStub.calledThrice);
+        assert.equal(updateStub.lastCall.args[0], 0);
+        assert.equal(element._numPendingDraftRequests.number, 0);
+      });
+    });
+
+    test('cancelling an unsaved draft discards, persists in storage', () => {
+      const discardSpy = sandbox.spy(element, '_fireDiscard');
+      const storeStub = sandbox.stub(element.$.storage, 'setDraftComment');
+      const eraseStub = sandbox.stub(element.$.storage, 'eraseDraftComment');
+      element._messageText = 'test text';
+      flushAsynchronousOperations();
+      element.flushDebouncer('store');
+
+      assert.isTrue(storeStub.called);
+      assert.equal(storeStub.lastCall.args[1], 'test text');
+      element._handleCancel({preventDefault: () => {}});
+      assert.isTrue(discardSpy.called);
+      assert.isFalse(eraseStub.called);
+    });
+
+    test('cancelling edit on a saved draft does not store', () => {
+      element.comment.id = 'foo';
+      const discardSpy = sandbox.spy(element, '_fireDiscard');
+      const storeStub = sandbox.stub(element.$.storage, 'setDraftComment');
+      element._messageText = 'test text';
+      flushAsynchronousOperations();
+      element.flushDebouncer('store');
+
+      assert.isFalse(storeStub.called);
+      element._handleCancel({preventDefault: () => {}});
+      assert.isTrue(discardSpy.called);
+    });
+
+    test('deleting text from saved draft and saving deletes the draft', () => {
+      element.comment = {id: 'foo', message: 'test'};
+      element._messageText = '';
+      const discardStub = sandbox.stub(element, '_discardDraft');
+
+      element.save();
+      assert.isTrue(discardStub.called);
+    });
+
+    test('_handleFix fires create-fix event', done => {
+      element.addEventListener('create-fix-comment', e => {
+        assert.deepEqual(e.detail, element._getEventPayload());
+        done();
+      });
+      element.isRobotComment = true;
+      flushAsynchronousOperations();
+
+      MockInteractions.tap(element.$$('.fix'));
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/diff/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
similarity index 100%
rename from polygerrit-ui/app/elements/diff/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html
rename to polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html
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
new file mode 100644
index 0000000..4ac059d
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js
@@ -0,0 +1,54 @@
+/**
+ * @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.
+ */
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-confirm-delete-comment-dialog',
+    _legacyUndefinedCheck: true,
+
+    /**
+     * Fired when the confirm button is pressed.
+     *
+     * @event confirm
+     */
+
+    /**
+     * Fired when the cancel button is pressed.
+     *
+     * @event cancel
+     */
+
+    properties: {
+      message: String,
+    },
+
+    resetFocus() {
+      this.$.messageInput.textarea.focus();
+    },
+
+    _handleConfirmTap(e) {
+      e.preventDefault();
+      this.fire('confirm', {reason: this.message}, {bubbles: false});
+    },
+
+    _handleCancelTap(e) {
+      e.preventDefault();
+      this.fire('cancel', null, {bubbles: false});
+    },
+  });
+})();
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 cabee36..550f1df 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,6 +21,7 @@
 
   Polymer({
     is: 'gr-copy-clipboard',
+    _legacyUndefinedCheck: true,
 
     properties: {
       text: String,
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 5e1f587..766ac7f 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,6 +24,7 @@
 
   Polymer({
     is: 'gr-cursor-manager',
+    _legacyUndefinedCheck: true,
 
     properties: {
       stops: {
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 1090fea..481dd2f 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
@@ -20,7 +20,6 @@
 <link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
-<script src="../../../bower_components/moment/moment.js"></script>
 <script src="../../../scripts/util.js"></script>
 
 <dom-module id="gr-date-formatter">
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 43ec20e..a9ce4c9 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
@@ -33,6 +33,7 @@
 
   Polymer({
     is: 'gr-date-formatter',
+    _legacyUndefinedCheck: true,
 
     properties: {
       dateStr: {
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 6163b09..b8b2af4 100644
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-dialog',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the confirm button is pressed.
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 36fdf5b..89c3d74 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-diff-preferences',
+    _legacyUndefinedCheck: true,
 
     properties: {
       hasUnsavedChanges: {
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 ca77a30..ed7c2cc 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-download-commands',
+    _legacyUndefinedCheck: true,
     properties: {
       commands: Array,
       _loggedIn: {
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 2962dbe..6b3905f 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,6 +47,7 @@
 
   Polymer({
     is: 'gr-dropdown-list',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the selected value changes
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 7ee2f44..b4b7087 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
@@ -22,6 +22,7 @@
 
   Polymer({
     is: 'gr-dropdown',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when a non-link dropdown item with the given ID is tapped.
@@ -295,7 +296,9 @@
      */
     _resetCursorStops() {
       Polymer.dom.flush();
-      this._listElements = Polymer.dom(this.root).querySelectorAll('li');
+      // Polymer2: querySelectorAll returns NodeList instead of Array.
+      this._listElements = Array.from(
+          Polymer.dom(this.root).querySelectorAll('li'));
     },
 
     _computeHasTooltip(tooltip) {
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 482cacf..d9e228b 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,6 +22,7 @@
 
   Polymer({
     is: 'gr-editable-content',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the save button is pressed.
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 3514492..f23afea 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,6 +22,7 @@
 
   Polymer({
     is: 'gr-editable-label',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the value is changed.
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 2c32709..87cd4b4 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-fixed-panel',
+    _legacyUndefinedCheck: true,
 
     properties: {
       floatingDisabled: Boolean,
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 feae173..d84c38e 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,6 +22,7 @@
 
   Polymer({
     is: 'gr-formatted-text',
+    _legacyUndefinedCheck: true,
 
     properties: {
       content: {
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 71621fa..761ef5b 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js
@@ -27,6 +27,7 @@
 
   Polymer({
     is: 'gr-hovercard',
+    _legacyUndefinedCheck: true,
 
     properties: {
       /**
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 af137e5..2581ff9 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
@@ -20,15 +20,19 @@
   /**
    * Used to create a context for GrAnnotationActionsInterface.
    *
-   * @param {HTMLElement} el The DIV.contentText element to apply the
-   *     annotation to using annotateRange.
+   * @param {HTMLElement} contentEl The DIV.contentText element of the line
+   *     content to apply the annotation to using annotateRange.
+   * @param {HTMLElement} lineNumberEl The TD element of the line number to
+   *     apply the annotation to using annotateLineNumber.
    * @param {GrDiffLine} line The line object.
-   * @param {String} path The file path (eg: /COMMIT_MSG').
-   * @param {String} changeNum The Gerrit change number.
-   * @param {String} patchNum The Gerrit patch number.
+   * @param {string} path The file path (eg: /COMMIT_MSG').
+   * @param {string} changeNum The Gerrit change number.
+   * @param {string} patchNum The Gerrit patch number.
    */
-  function GrAnnotationActionsContext(el, line, path, changeNum, patchNum) {
-    this._el = el;
+  function GrAnnotationActionsContext(
+      contentEl, lineNumberEl, line, path, changeNum, patchNum) {
+    this._contentEl = contentEl;
+    this._lineNumberEl = lineNumberEl;
 
     this.line = line;
     this.path = path;
@@ -37,17 +41,30 @@
   }
 
   /**
-   * Method to add annotations to a line.
+   * Method to add annotations to a content line.
    *
-   * @param {Number} start The line number where the update starts.
-   * @param {Number} end The line number where the update ends.
-   * @param {String} cssClass The name of a CSS class created using Gerrit.css.
-   * @param {String} side The side of the update. ('left' or 'right')
+   * @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 {string} side The side of the update. ('left' or 'right')
    */
   GrAnnotationActionsContext.prototype.annotateRange = function(
-      start, end, cssClass, side) {
-    if (this._el.getAttribute('data-side') == side) {
-      GrAnnotation.annotateElement(this._el, start, end, cssClass);
+      offset, length, cssClass, side) {
+    if (this._contentEl && this._contentEl.getAttribute('data-side') == side) {
+      GrAnnotation.annotateElement(this._contentEl, offset, length, cssClass);
+    }
+  };
+
+  /**
+   * 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 {string} side The side of the update. ('left' or 'right')
+   */
+  GrAnnotationActionsContext.prototype.annotateLineNumber = function(
+      cssClass, side) {
+    if (this._lineNumberEl && this._lineNumberEl.classList.contains(side)) {
+      this._lineNumberEl.classList.add(cssClass);
     }
   };
 
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 cd86fa9..03c8c5e 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
@@ -39,6 +39,7 @@
     let instance;
     let sandbox;
     let el;
+    let lineNumberEl;
 
     setup(() => {
       sandbox = sinon.sandbox.create();
@@ -47,8 +48,10 @@
       el = document.createElement('div');
       el.textContent = str;
       el.setAttribute('data-side', 'right');
+      lineNumberEl = document.createElement('td');
+      lineNumberEl.classList.add('right');
       instance = new GrAnnotationActionsContext(
-          el, line, 'dummy/path', '123', '1');
+          el, lineNumberEl, line, 'dummy/path', '123', '1');
     });
 
     teardown(() => {
@@ -74,5 +77,17 @@
       assert.equal(args[2], end);
       assert.equal(args[3], cssClass);
     });
+
+    test('test annotateLineNumber', () => {
+      const cssClass = Gerrit.css('background-color: #000000');
+
+      // Assert that css class is *not* applied when side is different.
+      instance.annotateLineNumber(cssClass, 'left');
+      assert.isFalse(lineNumberEl.classList.contains(cssClass));
+
+      // Assert that css class is applied when side is the same.
+      instance.annotateLineNumber(cssClass, 'right');
+      assert.isTrue(lineNumberEl.classList.contains(cssClass));
+    });
   });
 </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 0be557f..09ff92a 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
@@ -32,8 +32,9 @@
 
   /**
    * Register a function to call to apply annotations. Plugins should use
-   * GrAnnotationActionsContext.annotateRange to apply a CSS class to a range
-   * within a line.
+   * GrAnnotationActionsContext.annotateRange and
+   * GrAnnotationActionsContext.annotateLineNumber to apply a CSS class to the
+   * line content or the line number.
    *
    * @param {function(GrAnnotationActionsContext)} addLayerFunc The function
    *     that will be called when the AnnotationLayer is ready to annotate.
@@ -165,13 +166,16 @@
   /**
    * Layer method to add annotations to a line.
    *
-   * @param {HTMLElement} el The DIV.contentText element to apply the
-   *     annotation to.
+   * @param {HTMLElement} contentEl The DIV.contentText element of the line
+   *     content to apply the annotation to using annotateRange.
+   * @param {HTMLElement} lineNumberEl The TD element of the line number to
+   *     apply the annotation to using annotateLineNumber.
    * @param {GrDiffLine} line The line object.
    */
-  AnnotationLayer.prototype.annotate = function(el, line) {
+  AnnotationLayer.prototype.annotate = function(contentEl, lineNumberEl, line) {
     const annotationActionsContext = new GrAnnotationActionsContext(
-        el, line, this._path, this._changeNum, this._patchNum);
+        contentEl, lineNumberEl, line, this._path, this._changeNum,
+        this._patchNum);
     this._addLayerFunc(annotationActionsContext);
   };
 
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 bfb8b47..bf7c2cb 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
@@ -71,7 +71,8 @@
       const annotationLayer = annotationActions.getLayer(
           '/dummy/path', changeNum, patchNum);
 
-      annotationLayer.annotate(el, line);
+      const lineNumberEl = document.createElement('td');
+      annotationLayer.annotate(el, lineNumberEl, line);
       assert.isTrue(testLayerFuncCalled);
     });
 
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 b5686bd..16fb7b7 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
@@ -28,6 +28,7 @@
     POST_REVERT: 'postrevert',
     ANNOTATE_DIFF: 'annotatediff',
     ADMIN_MENU_LINKS: 'admin-menu-links',
+    HIGHLIGHTJS_LOADED: 'highlightjs-loaded',
   };
 
   const Element = {
@@ -37,6 +38,7 @@
 
   Polymer({
     is: 'gr-js-api-interface',
+    _legacyUndefinedCheck: true,
 
     properties: {
       _elements: {
@@ -69,6 +71,9 @@
           case EventType.LABEL_CHANGE:
             this._handleLabelChange(detail);
             break;
+          case EventType.HIGHLIGHTJS_LOADED:
+            this._handleHighlightjsLoaded(detail);
+            break;
           default:
             console.warn('handleEvent called with unsupported event type:',
                 type);
@@ -188,6 +193,16 @@
       }
     },
 
+    _handleHighlightjsLoaded(detail) {
+      for (const cb of this._getEventCallbacks(EventType.HIGHLIGHTJS_LOADED)) {
+        try {
+          cb(detail.hljs);
+        } catch (err) {
+          console.error(err);
+        }
+      }
+    },
+
     modifyRevertMsg(change, revertMsg, origMsg) {
       for (const cb of this._getEventCallbacks(EventType.REVERT)) {
         try {
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 e0c7c37..f03aa09 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
@@ -300,6 +300,17 @@
       assert.isTrue(errorStub.calledTwice);
     });
 
+    test('highlightjs-loaded event', done => {
+      const testHljs = {_number: 42};
+      plugin.on(element.EventType.HIGHLIGHTJS_LOADED, throwErrFn);
+      plugin.on(element.EventType.HIGHLIGHTJS_LOADED, hljs => {
+        assert.deepEqual(hljs, testHljs);
+        assert.isTrue(errorStub.calledOnce);
+        done();
+      });
+      element.handleEvent(element.EventType.HIGHLIGHTJS_LOADED, {hljs: testHljs});
+    });
+
     test('versioning', () => {
       const callback = sandbox.spy();
       Gerrit.install(callback, '0.0pre-alpha');
@@ -384,19 +395,6 @@
       });
     });
 
-    test('installGwt calls _pluginInstalled', () => {
-      sandbox.stub(Gerrit, '_pluginInstalled');
-      Gerrit.installGwt('http://test.com/plugins/testplugin/static/test.js');
-      assert.isTrue(Gerrit._pluginInstalled.calledOnce);
-    });
-
-    test('installGwt returns a plugin', () => {
-      const plugin = Gerrit.installGwt(
-          'http://test.com/plugins/testplugin/static/test.js');
-      assert.isOk(plugin);
-      assert.isOk(plugin._loadedGwt);
-    });
-
     test('attributeHelper', () => {
       assert.isOk(plugin.attributeHelper());
     });
@@ -441,28 +439,32 @@
 
     test('installing preloaded plugin', () => {
       let plugin;
-      window.ASSETS_PATH = 'http://blips.com/chitz/';
+      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/plugins/foo/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 => { plugin = p; }, '0.1',
-            'http://test.com/r/plugins/testplugin/static/test.js');
+        Gerrit.install(p => { baseUrlPlugin = p; }, '0.1',
+            'http://test.com/r/plugins/baseurlplugin/static/test.js');
       });
 
       test('url', () => {
-        assert.notEqual(plugin.url(), 'http://test.com/plugins/testplugin/');
-        assert.equal(plugin.url(), 'http://test.com/r/plugins/testplugin/');
-        assert.equal(plugin.url('/static/test.js'),
-            'http://test.com/r/plugins/testplugin/static/test.js');
+        assert.notEqual(baseUrlPlugin.url(),
+            'http://test.com/plugins/baseurlplugin/');
+        assert.equal(baseUrlPlugin.url(),
+            'http://test.com/r/plugins/baseurlplugin/');
+        assert.equal(baseUrlPlugin.url('/static/test.js'),
+            '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 bf6a046..ca87956 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
@@ -135,7 +135,7 @@
         __key: 'key',
         __url: '/changes/1/revisions/2/foo~bar',
       };
-      const sendStub = sandbox.stub().returns(Promise.reject('boom'));
+      const sendStub = sandbox.stub().returns(Promise.reject(new Error('boom')));
       sandbox.stub(plugin, 'restApi').returns({
         send: sendStub,
       });
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.js
index 899a45e..5f5a74e 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.js
@@ -20,6 +20,7 @@
   function GrPluginEndpoints() {
     this._endpoints = {};
     this._callbacks = {};
+    this._dynamicPlugins = {};
   }
 
   GrPluginEndpoints.prototype.onNewEndpoint = function(endpoint, callback) {
@@ -51,8 +52,21 @@
     }
   };
 
+  /**
+   * Register a plugin to an endpoint.
+   *
+   * Dynamic plugins are registered to a specific prefix, such as
+   * 'change-list-header'. These plugins are then fetched by prefix to determine
+   * which endpoints to dynamically add to the page.
+   */
   GrPluginEndpoints.prototype.registerModule = function(plugin, endpoint, type,
-      moduleName, domHook) {
+      moduleName, domHook, dynamicEndpoint) {
+    if (dynamicEndpoint) {
+      if (!this._dynamicPlugins[dynamicEndpoint]) {
+        this._dynamicPlugins[dynamicEndpoint] = new Set();
+      }
+      this._dynamicPlugins[dynamicEndpoint].add(endpoint);
+    }
     if (!this._endpoints[endpoint]) {
       this._endpoints[endpoint] = [];
     }
@@ -63,6 +77,12 @@
     }
   };
 
+  GrPluginEndpoints.prototype.getDynamicEndpoints = function(dynamicEndpoint) {
+    const plugins = this._dynamicPlugins[dynamicEndpoint];
+    if (!plugins) return [];
+    return Array.from(plugins);
+  };
+
   /**
    * Get detailed information about modules registered with an extension
    * endpoint.
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 7e84a4b..ce619a0 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
@@ -57,9 +57,9 @@
    * @return {!Promise}
    */
   GrPluginRestApi.prototype.fetch = function(method, url, opt_payload,
-      opt_errFn) {
+      opt_errFn, opt_contentType) {
     return getRestApi().send(method, this.opt_prefix + url, opt_payload,
-        opt_errFn);
+        opt_errFn, opt_contentType);
   };
 
   /**
@@ -73,20 +73,21 @@
    * @return {!Promise} resolves on success, rejects on error.
    */
   GrPluginRestApi.prototype.send = function(method, url, opt_payload,
-      opt_errFn) {
-    return this.fetch(method, url, opt_payload, opt_errFn).then(response => {
-      if (response.status < 200 || response.status >= 300) {
-        return response.text().then(text => {
-          if (text) {
-            return Promise.reject(text);
+      opt_errFn, opt_contentType) {
+    return this.fetch(method, url, opt_payload, opt_errFn, opt_contentType)
+        .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 Promise.reject(response.status);
+            return getRestApi().getResponseObject(response);
           }
         });
-      } else {
-        return getRestApi().getResponseObject(response);
-      }
-    });
   };
 
   /**
@@ -101,16 +102,18 @@
    * @param {string} url URL without base path or plugin prefix
    * @return {!Promise} resolves on success, rejects on error.
    */
-  GrPluginRestApi.prototype.post = function(url, opt_payload) {
-    return this.send('POST', url, opt_payload);
+  GrPluginRestApi.prototype.post = function(url, opt_payload, opt_errFn,
+      opt_contentType) {
+    return this.send('POST', url, opt_payload, opt_errFn, opt_contentType);
   };
 
   /**
    * @param {string} url URL without base path or plugin prefix
    * @return {!Promise} resolves on success, rejects on error.
    */
-  GrPluginRestApi.prototype.put = function(url, opt_payload) {
-    return this.send('PUT', url, opt_payload);
+  GrPluginRestApi.prototype.put = function(url, opt_payload, opt_errFn,
+      opt_contentType) {
+    return this.send('PUT', url, opt_payload, opt_errFn, opt_contentType);
   };
 
   /**
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 580bf77..7376a12 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
@@ -99,10 +99,6 @@
     STYLE: 'style',
   };
 
-  // GWT JSNI uses $wnd to refer to window.
-  // http://www.gwtproject.org/doc/latest/DevGuideCodingBasicsJSNI.html
-  window.$wnd = window;
-
   function flushPreinstalls() {
     if (window.Gerrit.flushPreinstalls) {
       window.Gerrit.flushPreinstalls();
@@ -189,14 +185,36 @@
         this, endpointName, EndpointType.STYLE, moduleName);
   };
 
+  /**
+   * Registers an endpoint for the plugin.
+   */
   Plugin.prototype.registerCustomComponent = function(
       endpointName, opt_moduleName, opt_options) {
+    return this._registerCustomComponent(endpointName, opt_moduleName,
+        opt_options);
+  };
+
+  /**
+   * Registers a dynamic endpoint for the plugin.
+   *
+   * Dynamic plugins are registered by specific prefix, such as
+   * 'change-list-header'.
+   */
+  Plugin.prototype.registerDynamicCustomComponent = function(
+      endpointName, opt_moduleName, opt_options) {
+    const fullEndpointName = `${endpointName}-${this.getPluginName()}`;
+    return this._registerCustomComponent(fullEndpointName, opt_moduleName,
+        opt_options, endpointName);
+  };
+
+  Plugin.prototype._registerCustomComponent = function(
+      endpointName, opt_moduleName, opt_options, dynamicEndpoint) {
     const type = opt_options && opt_options.replace ?
       EndpointType.REPLACE : EndpointType.DECORATE;
     const hook = this._domHooks.getDomHook(endpointName, opt_moduleName);
     const moduleName = opt_moduleName || hook.getModuleName();
     Gerrit._endpoints.registerModule(
-        this, endpointName, type, moduleName, hook);
+        this, endpointName, type, moduleName, hook, dynamicEndpoint);
     return hook.getPublicAPI();
   };
 
@@ -217,9 +235,14 @@
   };
 
   Plugin.prototype.url = function(opt_path) {
-    const base = Gerrit.BaseUrlBehavior.getBaseUrl();
-    return this._url.origin + base + '/plugins/' +
-        this._name + (opt_path || '/');
+    const relPath = '/plugins/' + this._name + (opt_path || '/');
+    if (window.location.origin === this._url.origin) {
+      // Plugin loaded from the same origin as gr-app, getBaseUrl in effect.
+      return this._url.origin + Gerrit.BaseUrlBehavior.getBaseUrl() + relPath;
+    } else {
+      // Plugin loaded from assets bundle, expect assets placed along with it.
+      return this._url.href.split('/plugins/' + this._name)[0] + relPath;
+    }
   };
 
   Plugin.prototype.screenUrl = function(opt_screenName) {
@@ -566,24 +589,6 @@
     });
   };
 
-  /**
-   * Install "stepping stones" API for GWT-compiled plugins by default.
-   *
-   * @deprecated best effort support, will be removed with GWT UI.
-   */
-  Gerrit.installGwt = function(url) {
-    const name = getPluginNameFromUrl(url);
-    let plugin;
-    try {
-      plugin = _plugins[name] || new Plugin(url);
-      plugin.deprecated.install();
-      Gerrit._pluginInstalled(url);
-    } catch (e) {
-      Gerrit._pluginInstallError(`${e.name}: ${e.message}`);
-    }
-    return plugin;
-  };
-
   Gerrit.awaitPluginsLoaded = function() {
     if (!_allPluginsPromise) {
       if (Gerrit._arePluginsLoaded()) {
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 2020008..1050e42 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-label-info',
+    _legacyUndefinedCheck: true,
 
     properties: {
       labelInfo: Object,
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 0de0881..c437885 100644
--- a/polygerrit-ui/app/elements/shared/gr-label/gr-label.js
+++ b/polygerrit-ui/app/elements/shared/gr-label/gr-label.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-label',
+    _legacyUndefinedCheck: true,
 
     behaviors: [
       Gerrit.TooltipBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js
index 54b38eb..a892522 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-labeled-autocomplete',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when a value is chosen.
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 f70aff4..4137485 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
@@ -15,7 +15,11 @@
 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">
 
 <dom-module id="gr-lib-loader">
+  <template>
+    <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
+  </template>
   <script src="gr-lib-loader.js"></script>
 </dom-module>
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 63214af..ba86b66 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,6 +22,7 @@
 
   Polymer({
     is: 'gr-lib-loader',
+    _legacyUndefinedCheck: true,
 
     properties: {
       _hljsState: {
@@ -84,6 +85,9 @@
     _onHLJSLibLoaded() {
       const lib = this._getHighlightLib();
       this._hljsState.loading = false;
+      this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.HIGHLIGHTJS_LOADED, {
+        hljs: lib,
+      });
       for (const cb of this._hljsState.callbacks) {
         cb(lib);
       }
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 0dc3a7d..44a8791 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,6 +26,7 @@
 
   Polymer({
     is: 'gr-limited-text',
+    _legacyUndefinedCheck: true,
 
     properties: {
       /** The un-truncated text to display. */
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 f8f29b8..8388a07 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-linked-chip',
+    _legacyUndefinedCheck: true,
 
     properties: {
       href: String,
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 09f7bbf..85d80fa 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-linked-text',
+    _legacyUndefinedCheck: true,
 
     properties: {
       removeZeroWidthSpace: Boolean,
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 8b83eb3..53d05e1 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,6 +21,7 @@
 
   Polymer({
     is: 'gr-list-view',
+    _legacyUndefinedCheck: true,
 
     properties: {
       createNew: Boolean,
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 6df04a2..c167b3b 100644
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
@@ -23,6 +23,7 @@
 
   Polymer({
     is: 'gr-overlay',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when a fullscreen overlay is closed
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 2e05607..181c7bc 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-page-nav',
+    _legacyUndefinedCheck: true,
 
     properties: {
       _headerHeight: Number,
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 e2298c3..2fccc8d 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,6 +22,7 @@
 
   Polymer({
     is: 'gr-repo-branch-picker',
+    _legacyUndefinedCheck: true,
 
     properties: {
       repo: {
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 ad1fac0..546d004 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
@@ -185,6 +185,7 @@
 
   Polymer({
     is: 'gr-rest-api-interface',
+    _legacyUndefinedCheck: true,
 
     behaviors: [
       Gerrit.PathListBehavior,
@@ -359,7 +360,7 @@
 
     /**
      * @param {string} url
-     * @param {?Object=} opt_params URL params, key-value hash.
+     * @param {?Object|string=} opt_params URL params, key-value hash.
      * @return {string}
      */
     _urlWithParams(url, opt_params) {
@@ -468,10 +469,11 @@
     saveRepoConfig(repo, config, opt_errFn) {
       // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
       // supports it.
-      const encodeName = encodeURIComponent(repo);
+      const url = `/projects/${encodeURIComponent(repo)}/config`;
+      this._cache.delete(url);
       return this._send({
         method: 'PUT',
-        url: `/projects/${encodeName}/config`,
+        url,
         body: config,
         errFn: opt_errFn,
         anonymizedUrl: '/projects/*/config',
@@ -1328,6 +1330,8 @@
      * @param {function()=} opt_cancelCondition
      */
     getChangeDetail(changeNum, opt_errFn, opt_cancelCondition) {
+      // This list MUST be kept in sync with
+      // ChangeIT#changeDetailsDoesNotRequireIndex
       const options = [
         this.ListChangesOption.ALL_COMMITS,
         this.ListChangesOption.ALL_REVISIONS,
@@ -1357,30 +1361,32 @@
      * @param {function()=} opt_cancelCondition
      */
     getDiffChangeDetail(changeNum, opt_errFn, opt_cancelCondition) {
-      const params = this.listChangesOptionsToHex(
+      const optionsHex = this.listChangesOptionsToHex(
           this.ListChangesOption.ALL_COMMITS,
           this.ListChangesOption.ALL_REVISIONS,
           this.ListChangesOption.SKIP_MERGEABLE
       );
-      return this._getChangeDetail(changeNum, params, opt_errFn,
+      return this._getChangeDetail(changeNum, optionsHex, opt_errFn,
           opt_cancelCondition);
     },
 
     /**
      * @param {number|string} changeNum
+     * @param {string|undefined} optionsHex list changes options in hex
      * @param {function(?Response, string=)=} opt_errFn
      * @param {function()=} opt_cancelCondition
      */
-    _getChangeDetail(changeNum, params, opt_errFn, opt_cancelCondition) {
+    _getChangeDetail(changeNum, optionsHex, opt_errFn, opt_cancelCondition) {
       return this.getChangeActionURL(changeNum, null, '/detail').then(url => {
-        const urlWithParams = this._urlWithParams(url, params);
+        const urlWithParams = this._urlWithParams(url, optionsHex);
+        const params = {O: optionsHex};
         const req = {
           url,
           errFn: opt_errFn,
           cancelCondition: opt_cancelCondition,
-          params: {O: params},
+          params,
           fetchOptions: this._etags.getOptions(urlWithParams),
-          anonymizedUrl: '/changes/*~*/detail?O=' + params,
+          anonymizedUrl: '/changes/*~*/detail?O=' + optionsHex,
         };
         return this._fetchRawJSON(req).then(response => {
           if (response && response.status === 304) {
@@ -1528,7 +1534,9 @@
      * @param {function(?Response, string=)=} opt_errFn
      */
     getChangeSuggestedReviewers(changeNum, inputVal, opt_errFn) {
-      const params = {n: 10};
+      // More suggestions may obscure content underneath in the reply dialog,
+      // see issue 10793.
+      const params = {n: 6};
       if (inputVal) { params.q = inputVal; }
       return this._getChangeURLAndFetch({
         changeNum,
@@ -2526,7 +2534,9 @@
     _fetchB64File(url) {
       return this._fetch({url: this.getBaseUrl() + url})
           .then(response => {
-            if (!response.ok) { return Promise.reject(response.statusText); }
+            if (!response.ok) {
+              return Promise.reject(new Error(response.statusText));
+            }
             const type = response.headers.get('X-FYI-Content-Type');
             return response.text()
                 .then(text => {
@@ -2686,12 +2696,12 @@
       return this._send(req)
           .then(response => {
             if (response.status < 200 && response.status >= 300) {
-              return Promise.reject();
+              return Promise.reject(new Error('error'));
             }
             return this.getResponseObject(response);
           })
           .then(obj => {
-            if (!obj.valid) { return Promise.reject(); }
+            if (!obj.valid) { return Promise.reject(new Error('error')); }
             return obj;
           });
     },
@@ -2721,12 +2731,12 @@
       return this._send(req)
           .then(response => {
             if (response.status < 200 && response.status >= 300) {
-              return Promise.reject();
+              return Promise.reject(new Error('error'));
             }
             return this.getResponseObject(response);
           })
           .then(obj => {
-            if (!obj) { return Promise.reject(); }
+            if (!obj) { return Promise.reject(new Error('error')); }
             return obj;
           });
     },
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 5d4a3b0..ef4e401 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
@@ -88,10 +88,10 @@
     });
 
     test('cached promise', done => {
-      const promise = Promise.reject('foo');
+      const promise = Promise.reject(new Error('foo'));
       element._cache.set('/foo', promise);
       element._fetchSharedCacheURL({url: '/foo'}).catch(p => {
-        assert.equal(p, 'foo');
+        assert.equal(p.message, 'foo');
         done();
       });
     });
@@ -455,7 +455,7 @@
         status: 403,
       };
       window.fetch.onFirstCall().returns(
-          Promise.reject({message: 'Failed to fetch'}));
+          Promise.reject(new Error('Failed to fetch')));
       window.fetch.onSecondCall().returns(Promise.resolve(fakeAuthResponse));
       // Emulate logged in.
       element._cache.set('/accounts/self/detail', {});
@@ -507,7 +507,7 @@
       element._cache.set('/accounts/self/detail', true);
       sandbox.spy(element, 'checkCredentials');
       sandbox.stub(window, 'fetch', url => {
-        return Promise.reject({message: 'Failed to fetch'});
+        return Promise.reject(new Error('Failed to fetch'));
       });
       return element.getConfig(true)
           .catch(err => undefined)
@@ -1151,12 +1151,12 @@
       test('_getChangeDetail passes params to ETags decorator', () => {
         const changeNum = 4321;
         element._projectLookup[changeNum] = 'test';
-        const params = {foo: 'bar'};
         const expectedUrl =
-            window.CANONICAL_PATH + '/changes/test~4321/detail?foo=bar';
+            window.CANONICAL_PATH + '/changes/test~4321/detail?'+
+            '0=5&1=1&2=6&3=7&4=1&5=4';
         sandbox.stub(element._etags, 'getOptions');
         sandbox.stub(element._etags, 'collect');
-        return element._getChangeDetail(changeNum, params).then(() => {
+        return element._getChangeDetail(changeNum, '516714').then(() => {
           assert.isTrue(element._etags.getOptions.calledWithExactly(
               expectedUrl));
           assert.equal(element._etags.collect.lastCall.args[0], expectedUrl);
@@ -1169,7 +1169,7 @@
             .returns(Promise.resolve(''));
         sandbox.stub(element, '_fetchRawJSON')
             .returns(Promise.resolve({ok: false, status: 500}));
-        return element._getChangeDetail(123, {}, errFn).then(() => {
+        return element._getChangeDetail(123, '516714', errFn).then(() => {
           assert.isTrue(errFn.called);
         });
       });
@@ -1185,7 +1185,7 @@
           parsed: mockResponse,
           raw: JSON.stringify(mockResponse),
         }));
-        return element._getChangeDetail(1).then(() => {
+        return element._getChangeDetail(1, '516714').then(() => {
           assert.equal(Object.keys(element._projectLookup).length, 1);
           assert.equal(element._projectLookup[1], 'test');
         });
@@ -1216,7 +1216,7 @@
             ok: true,
           }));
 
-          return element._getChangeDetail(123, {}).then(detail => {
+          return element._getChangeDetail(123, '516714').then(detail => {
             assert.isFalse(getPayloadSpy.called);
             assert.isTrue(collectSpy.calledOnce);
             const cachedResponse = element._etags.getCachedPayload(requestUrl);
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 0a1b14e..05c2cee 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
@@ -153,6 +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.js b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
index b732fa5..85e1a61 100644
--- a/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
+++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-select',
+    _legacyUndefinedCheck: true,
     properties: {
       bindValue: {
         type: String,
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 2c546cc..901b8ce 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-shell-command',
+    _legacyUndefinedCheck: true,
 
     properties: {
       command: String,
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 6d82dad..83cd06c 100644
--- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
+++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
@@ -30,6 +30,7 @@
 
   Polymer({
     is: 'gr-storage',
+    _legacyUndefinedCheck: true,
 
     properties: {
       _lastCleanup: Number,
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 a3da7d8..7929fbe 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
@@ -54,6 +54,7 @@
 
   Polymer({
     is: 'gr-textarea',
+    _legacyUndefinedCheck: true,
 
     /**
      * @event bind-value-changed
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 c5de8f4..b46cafb 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,6 +19,7 @@
 
   Polymer({
     is: 'gr-tooltip-content',
+    _legacyUndefinedCheck: true,
 
     properties: {
       title: {
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 fb87b558..3e16beb 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-tooltip',
+    _legacyUndefinedCheck: true,
 
     properties: {
       text: String,
diff --git a/polygerrit-ui/app/embed/embed.html b/polygerrit-ui/app/embed/embed.html
index 948916f..1b2f20f 100644
--- a/polygerrit-ui/app/embed/embed.html
+++ b/polygerrit-ui/app/embed/embed.html
@@ -25,7 +25,6 @@
 <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">
 <link rel="import" href="../elements/change-list/gr-change-list-view/gr-change-list-view.html">
-<link rel="import" href="../elements/change-list/gr-change-list/gr-change-list.html">
-<link rel="import" href="../elements/change-list/gr-create-change-help/gr-create-change-help.html">
 <link rel="import" href="../elements/change-list/gr-dashboard-view/gr-dashboard-view.html">
+<link rel="import" href="../elements/change-list/gr-embed-dashboard/gr-embed-dashboard.html">
 <link rel="import" href="../styles/themes/app-theme.html">
diff --git a/polygerrit-ui/app/embed/gr-diff.html b/polygerrit-ui/app/embed/gr-diff.html
new file mode 100644
index 0000000..6aa9370
--- /dev/null
+++ b/polygerrit-ui/app/embed/gr-diff.html
@@ -0,0 +1,25 @@
+<!--
+@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>
+  // Needed for JSCompiler to understand it's global.
+  // eslint-disable-next-line no-unused-vars, prefer-const
+  let Gerrit = window.Gerrit || {};
+  window.Gerrit = 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/samples/bind-parameters.html b/polygerrit-ui/app/samples/bind-parameters.html
index dc7a87a..a7eb39a 100644
--- a/polygerrit-ui/app/samples/bind-parameters.html
+++ b/polygerrit-ui/app/samples/bind-parameters.html
@@ -15,6 +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 b298237..0d38c63 100644
--- a/polygerrit-ui/app/samples/coverage-plugin.html
+++ b/polygerrit-ui/app/samples/coverage-plugin.html
@@ -50,6 +50,7 @@
           const linesMissingCoverage = coverageData[path].linesMissingCoverage;
           if (linesMissingCoverage.includes(line.afterNumber)) {
             context.annotateRange(0, line.text.length, cssClass, 'right');
+            context.annotateLineNumber(cssClass, 'right');
           }
         }
       }).enableToggleCheckbox('Display Coverage', checkbox => {
diff --git a/polygerrit-ui/app/samples/repo-command.html b/polygerrit-ui/app/samples/repo-command.html
index 67e528a..37aca04 100644
--- a/polygerrit-ui/app/samples/repo-command.html
+++ b/polygerrit-ui/app/samples/repo-command.html
@@ -29,6 +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 de29315..527ebce 100644
--- a/polygerrit-ui/app/samples/some-screen.html
+++ b/polygerrit-ui/app/samples/some-screen.html
@@ -38,6 +38,7 @@
   <script>
     Polymer({
       is: 'some-screen-main',
+      _legacyUndefinedCheck: true,
       properties: {
         rootUrl: String,
       },
diff --git a/polygerrit-ui/app/scripts/util.js b/polygerrit-ui/app/scripts/util.js
index b4ab21a..624992b 100644
--- a/polygerrit-ui/app/scripts/util.js
+++ b/polygerrit-ui/app/scripts/util.js
@@ -41,5 +41,39 @@
     }
     return '';
   };
+
+  /**
+   * Make the promise cancelable.
+   *
+   * Returns a promise with a `cancel()` method wrapped around `promise`.
+   * Calling `cancel()` will reject the returned promise with
+   * {isCancelled: true} synchronously. If the inner promise for a cancelled
+   * promise resolves or rejects this is ignored.
+   */
+  util.makeCancelable = promise => {
+    // True if the promise is either resolved or reject (possibly cancelled)
+    let isDone = false;
+
+    let rejectPromise;
+
+    const wrappedPromise = new Promise((resolve, reject) => {
+      rejectPromise = reject;
+      promise.then(val => {
+        if (!isDone) resolve(val);
+        isDone = true;
+      }, error => {
+        if (!isDone) reject(error);
+        isDone = true;
+      });
+    });
+
+    wrappedPromise.cancel = () => {
+      if (isDone) return;
+      rejectPromise({isCanceled: true});
+      isDone = true;
+    };
+    return wrappedPromise;
+  };
+
   window.util = util;
 })(window);
diff --git a/polygerrit-ui/app/styles/gr-change-list-styles.html b/polygerrit-ui/app/styles/gr-change-list-styles.html
index aeca48a..8f72216 100644
--- a/polygerrit-ui/app/styles/gr-change-list-styles.html
+++ b/polygerrit-ui/app/styles/gr-change-list-styles.html
@@ -55,8 +55,8 @@
       .cell {
         vertical-align: middle;
       }
-      th:not(.label),
-      .cell:not(.label) {
+      th:not(.label):not(.endpoint),
+      .cell:not(.label):not(.endpoint) {
         padding-right: 8px;
       }
       th.label {
@@ -128,8 +128,10 @@
       .star {
         width: 30px;
       }
-      .label {
+      .label, .endpoint {
         border-left: 1px solid var(--border-color);
+      }
+      .label {
         text-align: center;
         width: 3rem;
       }
diff --git a/polygerrit-ui/app/styles/gr-form-styles.html b/polygerrit-ui/app/styles/gr-form-styles.html
index 59b633f..65c1ae3 100644
--- a/polygerrit-ui/app/styles/gr-form-styles.html
+++ b/polygerrit-ui/app/styles/gr-form-styles.html
@@ -29,11 +29,15 @@
       .gr-form-styles h2 {
         margin-bottom: .3em;
       }
+      .gr-form-styles h4 {
+        font-weight: var(--font-weight-bold);
+      }
       .gr-form-styles fieldset {
         border: none;
         margin-bottom: 2em;
       }
       .gr-form-styles section {
+        display: flex;
         margin: .25em 0;
         min-height: 2em;
       }
diff --git a/polygerrit-ui/app/styles/themes/app-theme.html b/polygerrit-ui/app/styles/themes/app-theme.html
index 374e81b..ec47c53 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.html
+++ b/polygerrit-ui/app/styles/themes/app-theme.html
@@ -14,8 +14,8 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<style is="custom-style">
-:root {
+<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. */
@@ -144,10 +144,11 @@
   --syntax-template-tag-color: #FA8602;
   --syntax-params-color: var(--primary-text-color);
   --syntax-doctag-weight: bold;
+  --reply-overlay-z-index: 1000;
 }
 @media screen and (max-width: 50em) {
-  :root {
+  html {
     --default-horizontal-margin: .7rem;
   }
 }
-</style>
+</style></custom-style>
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.html b/polygerrit-ui/app/styles/themes/dark-theme.html
index b985e88..718ac25 100644
--- a/polygerrit-ui/app/styles/themes/dark-theme.html
+++ b/polygerrit-ui/app/styles/themes/dark-theme.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 <dom-module id="dark-theme">
-  <style is="custom-style">
+  <custom-style><style is="custom-style">
     html {
       --primary-text-color: #e2e2e2;
       --view-background-color: #212121;
@@ -106,7 +106,9 @@
       --syntax-doctag-weight: bold;
       --syntax-params-color: var(--primary-text-color);
 
+      --reply-overlay-z-index: 1000;
+
       background-color: var(--view-background-color);
     }
-  </style>
+  </style></custom-style>
 </dom-module>
diff --git a/polygerrit-ui/app/test/common-test-setup.html b/polygerrit-ui/app/test/common-test-setup.html
index e0299da..a549dd4 100644
--- a/polygerrit-ui/app/test/common-test-setup.html
+++ b/polygerrit-ui/app/test/common-test-setup.html
@@ -62,3 +62,4 @@
 <link rel="import"
     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>
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 75ba705..cd3aaec 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -44,6 +44,7 @@
     'admin/gr-group-members/gr-group-members_test.html',
     'admin/gr-group/gr-group_test.html',
     'admin/gr-permission/gr-permission_test.html',
+    'admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.html',
     'admin/gr-plugin-list/gr-plugin-list_test.html',
     'admin/gr-repo-access/gr-repo-access_test.html',
     'admin/gr-repo-command/gr-repo-command_test.html',
@@ -51,6 +52,7 @@
     'admin/gr-repo-dashboards/gr-repo-dashboards_test.html',
     'admin/gr-repo-detail-list/gr-repo-detail-list_test.html',
     'admin/gr-repo-list/gr-repo-list_test.html',
+    'admin/gr-repo-plugin-config/gr-repo-plugin-config_test.html',
     'admin/gr-repo/gr-repo_test.html',
     'admin/gr-rule-editor/gr-rule-editor_test.html',
     'change-list/gr-change-list-item/gr-change-list-item_test.html',
@@ -104,9 +106,6 @@
     'core/gr-smart-search/gr-smart-search_test.html',
     'diff/gr-comment-api/gr-comment-api_test.html',
     'diff/gr-diff-builder/gr-diff-builder_test.html',
-    'diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group_test.html',
-    'diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html',
-    'diff/gr-diff-comment/gr-diff-comment_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',
@@ -159,6 +158,8 @@
     'shared/gr-button/gr-button_test.html',
     'shared/gr-change-star/gr-change-star_test.html',
     'shared/gr-change-status/gr-change-status_test.html',
+    '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-cursor-manager/gr-cursor-manager_test.html',
     'shared/gr-date-formatter/gr-date-formatter_test.html',
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go
index ba685184..2f5df90 100644
--- a/polygerrit-ui/server.go
+++ b/polygerrit-ui/server.go
@@ -17,6 +17,7 @@
 import (
 	"archive/zip"
 	"bufio"
+	"bytes"
 	"compress/gzip"
 	"encoding/json"
 	"errors"
@@ -32,20 +33,16 @@
 	"regexp"
 	"strings"
 
-	"github.com/robfig/soy"
-	"github.com/robfig/soy/soyhtml"
 	"golang.org/x/tools/godoc/vfs/httpfs"
 	"golang.org/x/tools/godoc/vfs/zipfs"
 )
 
 var (
-	plugins  = flag.String("plugins", "", "comma seperated plugin paths to serve")
-	port     = flag.String("port", ":8081", "Port to serve HTTP requests on")
-	prod     = flag.Bool("prod", false, "Serve production assets")
-	restHost = flag.String("host", "gerrit-review.googlesource.com", "Host to proxy requests to")
-	scheme   = flag.String("scheme", "https", "URL scheme")
-
-	tofu *soyhtml.Tofu
+	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.]*")
 )
 
 func main() {
@@ -61,55 +58,35 @@
 		log.Fatal(err)
 	}
 
-	tofu, err = resolveIndexTemplate()
-	if err != nil {
-		log.Fatal(err)
-	}
-
 	workspace := os.Getenv("BUILD_WORKSPACE_DIRECTORY")
 	if err := os.Chdir(filepath.Join(workspace, "polygerrit-ui")); err != nil {
 		log.Fatal(err)
 	}
 
-	http.HandleFunc("/index.html", handleIndex)
-
-	if *prod {
-		http.Handle("/", http.FileServer(http.Dir("dist")))
-	} else {
-		http.Handle("/", http.FileServer(http.Dir("app")))
-	}
-
+	http.Handle("/", http.FileServer(http.Dir("app")))
 	http.Handle("/bower_components/",
 		http.FileServer(httpfs.New(zipfs.New(componentsArchive, "bower_components"))))
 	http.Handle("/fonts/",
 		http.FileServer(httpfs.New(zipfs.New(fontsArchive, "fonts"))))
 
-	http.HandleFunc("/changes/", handleRESTProxy)
-	http.HandleFunc("/accounts/", handleRESTProxy)
-	http.HandleFunc("/config/", handleRESTProxy)
-	http.HandleFunc("/projects/", handleRESTProxy)
+	http.HandleFunc("/index.html", handleIndex)
+	http.HandleFunc("/changes/", handleProxy)
+	http.HandleFunc("/accounts/", handleProxy)
+	http.HandleFunc("/config/", handleProxy)
+	http.HandleFunc("/projects/", handleProxy)
 	http.HandleFunc("/accounts/self/detail", handleAccountDetail)
+
 	if len(*plugins) > 0 {
 		http.Handle("/plugins/", http.StripPrefix("/plugins/",
 			http.FileServer(http.Dir("../plugins"))))
 		log.Println("Local plugins from", "../plugins")
 	} else {
-		http.HandleFunc("/plugins/", handleRESTProxy)
+		http.HandleFunc("/plugins/", handleProxy)
 	}
 	log.Println("Serving on port", *port)
 	log.Fatal(http.ListenAndServe(*port, &server{}))
 }
 
-func resolveIndexTemplate() (*soyhtml.Tofu, error) {
-	basePath, err := resourceBasePath()
-	if err != nil {
-		return nil, err
-	}
-	return soy.NewBundle().
-		AddTemplateFile(basePath + ".runfiles/gerrit/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy").
-		CompileToTofu()
-}
-
 func openDataArchive(path string) (*zip.ReadCloser, error) {
 	absBinPath, err := resourceBasePath()
 	if err != nil {
@@ -122,40 +99,40 @@
 	return filepath.Abs(os.Args[0])
 }
 
-func handleIndex(w http.ResponseWriter, r *http.Request) {
-	var obj = map[string]interface{}{
-		"canonicalPath":      "",
-		"staticResourcePath": "",
+func handleIndex(writer http.ResponseWriter, originalRequest *http.Request) {
+	fakeRequest := &http.Request{
+		URL: &url.URL{
+			Path: "/",
+		},
 	}
-	w.Header().Set("Content-Type", "text/html")
-	tofu.Render(w, "com.google.gerrit.httpd.raw.Index", obj)
+	handleProxy(writer, fakeRequest)
 }
 
-func handleRESTProxy(w http.ResponseWriter, r *http.Request) {
-	req := &http.Request{
+func handleProxy(writer http.ResponseWriter, originalRequest *http.Request) {
+	patchedRequest := &http.Request{
 		Method: "GET",
 		URL: &url.URL{
 			Scheme:   *scheme,
-			Host:     *restHost,
-			Opaque:   r.URL.EscapedPath(),
-			RawQuery: r.URL.RawQuery,
+			Host:     *host,
+			Opaque:   originalRequest.URL.EscapedPath(),
+			RawQuery: originalRequest.URL.RawQuery,
 		},
 	}
-	res, err := http.DefaultClient.Do(req)
+	response, err := http.DefaultClient.Do(patchedRequest)
 	if err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
+		http.Error(writer, err.Error(), http.StatusInternalServerError)
 		return
 	}
-	defer res.Body.Close()
-	for name, values := range res.Header {
+	defer response.Body.Close()
+	for name, values := range response.Header {
 		for _, value := range values {
 			if name != "Content-Length" {
-				w.Header().Add(name, value)
+				writer.Header().Add(name, value)
 			}
 		}
 	}
-	w.WriteHeader(res.StatusCode)
-	if _, err := io.Copy(w, patchResponse(r, res)); err != nil {
+	writer.WriteHeader(response.StatusCode)
+	if _, err := io.Copy(writer, patchResponse(originalRequest, response)); err != nil {
 		log.Println("Error copying response to ResponseWriter:", err)
 		return
 	}
@@ -188,8 +165,10 @@
 	}
 }
 
-func patchResponse(r *http.Request, res *http.Response) io.Reader {
-	switch r.URL.EscapedPath() {
+func patchResponse(req *http.Request, res *http.Response) io.Reader {
+	switch req.URL.EscapedPath() {
+	case "/":
+		return replaceCdn(res.Body)
 	case "/config/server/info":
 		return injectLocalPlugins(res.Body)
 	default:
@@ -197,13 +176,23 @@
 	}
 }
 
-func injectLocalPlugins(r io.Reader) io.Reader {
+func replaceCdn(reader io.Reader) io.Reader {
+	buf := new(bytes.Buffer)
+	buf.ReadFrom(reader)
+	original := buf.String()
+
+	replaced := cdnPattern.ReplaceAllString(original, "")
+
+	return strings.NewReader(replaced)
+}
+
+func injectLocalPlugins(reader io.Reader) io.Reader {
 	if len(*plugins) == 0 {
-		return r
+		return reader
 	}
 	// Skip escape prefix
-	io.CopyN(ioutil.Discard, r, 5)
-	dec := json.NewDecoder(r)
+	io.CopyN(ioutil.Discard, reader, 5)
+	dec := json.NewDecoder(reader)
 
 	var response map[string]interface{}
 	err := dec.Decode(&response)
diff --git a/proto/BUILD b/proto/BUILD
index 26efdbc..57be265 100644
--- a/proto/BUILD
+++ b/proto/BUILD
@@ -12,23 +12,13 @@
     deps = [":cache_proto"],
 )
 
-genrule(
-    name = "gen_reviewdb_proto",
-    outs = ["reviewdb.proto"],
-    cmd = "$(location //java/com/google/gerrit/proto:ProtoGen) -o $@",
-    tools = ["//java/com/google/gerrit/proto:ProtoGen"],
-)
-
 proto_library(
-    name = "reviewdb_proto",
-    srcs = [":reviewdb.proto"],
+    name = "entities_proto",
+    srcs = ["entities.proto"],
 )
 
 java_proto_library(
-    name = "reviewdb_java_proto",
-    visibility = [
-        "//javatests/com/google/gerrit/proto:__pkg__",
-        "//tools/eclipse:__pkg__",
-    ],
-    deps = [":reviewdb_proto"],
+    name = "entities_java_proto",
+    visibility = ["//visibility:public"],
+    deps = [":entities_proto"],
 )
diff --git a/proto/cache.proto b/proto/cache.proto
index c2ac0d9..b34dbf3 100644
--- a/proto/cache.proto
+++ b/proto/cache.proto
@@ -61,10 +61,10 @@
 // were chosen ease of coding the initial implementation. In particular, where
 // there already exists another serialization mechanism in Gerrit for
 // serializing a particular field, we use that rather than defining a new proto
-// type. This includes ReviewDb types that can be serialized to proto using
-// ProtobufCodec as well as NoteDb and indexed types that are serialized using
-// JSON. We can always revisit this decision later, particularly when we
-// eliminate the ReviewDb types; it just requires bumping the cache version.
+// type. This includes types that can be serialized to proto using
+// ProtoConverters as well as NoteDb and indexed types that are serialized using
+// JSON. We can always revisit this decision later; it just requires bumping the
+// cache version.
 //
 // Note on nullability: there are a lot of nullable fields in ChangeNotesState
 // and its dependencies. It's likely we could make some of them non-nullable,
@@ -134,10 +134,10 @@
 
   repeated string hashtag = 5;
 
-  // Raw PatchSet proto as produced by ProtobufCodec.
+  // Raw PatchSet proto as produced by PatchSetProtoConverter.
   repeated bytes patch_set = 6;
 
-  // Raw PatchSetApproval proto as produced by ProtobufCodec.
+  // Raw PatchSetApproval proto as produced by PatchSetApprovalProtoConverter.
   repeated bytes approval = 7;
 
   // Next ID: 4
@@ -175,14 +175,14 @@
   // com.google.gerrit.server.index.change.ChangeField.StoredSubmitRecord.
   repeated string submit_record = 14;
 
-  // Raw ChangeMessage proto as produced by ProtobufCodec.
+  // Raw ChangeMessage proto as produced by ChangeMessageProtoConverter.
   repeated bytes change_message = 15;
 
   // JSON produced from com.google.gerrit.reviewdb.client.Comment.
   repeated string published_comment = 16;
 
-  int64 read_only_until = 17;
-  bool has_read_only_until = 18;
+  reserved 17;  // read_only_until
+  reserved 18;  // has_read_only_until
 }
 
 
@@ -234,3 +234,11 @@
   }
   repeated ExternalIdProto external_id = 1;
 }
+
+// Key for com.google.gerrit.server.git.PureRevertCache.
+// Next ID: 4
+message PureRevertKeyProto {
+  string project = 1;
+  bytes claimed_original = 2;
+  bytes claimed_revert = 3;
+}
diff --git a/proto/entities.proto b/proto/entities.proto
new file mode 100644
index 0000000..d2851d3
--- /dev/null
+++ b/proto/entities.proto
@@ -0,0 +1,158 @@
+// 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.
+
+syntax = "proto2";
+
+package devtools.gerritcodereview;
+
+option java_package = "com.google.gerrit.proto";
+
+// Serialized form of com.google.gerrit.reviewdb.client.Change.Id.
+// Next ID: 2
+message Change_Id {
+  required int32 id = 1;
+}
+
+// Serialized form of com.google.gerrit.reviewdb.client.Change.Key.
+// Next ID: 2
+message Change_Key {
+  optional string id = 1;
+}
+
+// Serialized form of com.google.gerrit.reviewdb.client.Change.
+// Next ID: 24
+message Change {
+  required Change_Id change_id = 1;
+  optional Change_Key change_key = 2;
+  optional int32 row_version = 3;
+  optional fixed64 created_on = 4;
+  optional fixed64 last_updated_on = 5;
+  optional Account_Id owner_account_id = 7;
+  optional Branch_NameKey dest = 8;
+  optional uint32 status = 10;
+  optional int32 current_patch_set_id = 12;
+  optional string subject = 13;
+  optional string topic = 14;
+  optional string original_subject = 17;
+  optional string submission_id = 18;
+  optional Account_Id assignee = 19;
+  optional bool is_private = 20;
+  optional bool work_in_progress = 21;
+  optional bool review_started = 22;
+  optional Change_Id revert_of = 23;
+
+  // Deleted fields, should not be reused:
+  reserved 6;    // sortkey
+  reserved 9;    // open
+  reserved 11;   // nbrPatchSets
+  reserved 15;   // lastSha1MergeTested
+  reserved 16;   // mergeable
+  reserved 101;  // note_db_state
+}
+
+// Serialized form of com.google.gerrit.reviewdb.client.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.
+// Next ID: 8
+message ChangeMessage {
+  required ChangeMessage_Key key = 1;
+  optional Account_Id author_id = 2;
+  optional fixed64 written_on = 3;
+  optional string message = 4;
+  optional PatchSet_Id patchset = 5;
+  optional string tag = 6;
+  optional Account_Id real_author = 7;
+}
+
+// Serialized form of com.google.gerrit.reviewdb.client.PatchSet.Id.
+// Next ID: 3
+message PatchSet_Id {
+  required Change_Id change_id = 1;
+  required int32 patch_set_id = 2;
+}
+
+// Serialized form of com.google.gerrit.reviewdb.client.PatchSet.
+// Next ID: 10
+message PatchSet {
+  required PatchSet_Id id = 1;
+  optional RevId revision = 2;
+  optional Account_Id uploader_account_id = 3;
+  optional fixed64 created_on = 4;
+  optional string groups = 6;
+  optional string push_certificate = 8;
+  optional string description = 9;
+
+  // Deleted fields, should not be reused:
+  reserved 5;  // draft
+  reserved 7;  // pushCertficate
+}
+
+// Serialized form of com.google.gerrit.reviewdb.client.Account.Id.
+// Next ID: 2
+message Account_Id {
+  required int32 id = 1;
+}
+
+// Serialized form of com.google.gerrit.reviewdb.client.LabelId.
+// Next ID: 2
+message LabelId {
+  required string id = 1;
+}
+
+// Serialized form of com.google.gerrit.reviewdb.client.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;
+}
+
+// Serialized form of com.google.gerrit.reviewdb.client.PatchSetApproval.
+// Next ID: 9
+message PatchSetApproval {
+  required PatchSetApproval_Key key = 1;
+  optional int32 value = 2;
+  optional fixed64 granted = 3;
+  optional string tag = 6;
+  optional Account_Id real_account_id = 7;
+  optional bool post_submit = 8;
+
+  // Deleted fields, should not be reused:
+  reserved 4;  // changeOpen
+  reserved 5;  // changeSortKey
+}
+
+// Serialized form of com.google.gerrit.reviewdb.client.Project.NameKey.
+// Next ID: 2
+message Project_NameKey {
+  optional string name = 1;
+}
+
+// Serialized form of com.google.gerrit.reviewdb.client.Branch.NameKey.
+// Next ID: 3
+message Branch_NameKey {
+  optional Project_NameKey project_name = 1;
+  optional string branch_name = 2;
+}
+
+// Serialized form of com.google.gerrit.reviewdb.client.RevId.
+// Next ID: 2
+message RevId {
+  optional string id = 1;
+}
diff --git a/proto/testing/BUILD b/proto/testing/BUILD
new file mode 100644
index 0000000..f32d745
--- /dev/null
+++ b/proto/testing/BUILD
@@ -0,0 +1,15 @@
+load("@rules_java//java:defs.bzl", "java_proto_library")
+load("@rules_proto//proto:defs.bzl", "proto_library")
+
+proto_library(
+    name = "test_proto",
+    testonly = 1,
+    srcs = ["test.proto"],
+)
+
+java_proto_library(
+    name = "test_java_proto",
+    testonly = 1,
+    visibility = ["//visibility:public"],
+    deps = [":test_proto"],
+)
diff --git a/proto/testing/test.proto b/proto/testing/test.proto
new file mode 100644
index 0000000..e28c9ff
--- /dev/null
+++ b/proto/testing/test.proto
@@ -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.
+
+syntax = "proto2";
+
+package devtools.gerritcodereview.testing;
+
+option java_package = "com.google.gerrit.proto.testing";
+
+// Test type for ProtobufSerializerTest
+// Next ID: 3
+message SerializableProto {
+  required int32 id = 1;
+  optional string text = 2;
+}
diff --git a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
index 9801b44..8f151a8 100644
--- a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
+++ b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
@@ -16,16 +16,14 @@
 
 {namespace com.google.gerrit.httpd.raw}
 
-/**
- * @param canonicalPath
- * @param staticResourcePath
- * @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? deprecateGwtUi
- */
 {template .Index}
+  {@param canonicalPath: ?}
+  {@param staticResourcePath: ?}
+  {@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: ?}
   <!DOCTYPE html>{\n}
   <html lang="en">{\n}
   <meta charset="utf-8">{\n}
@@ -40,10 +38,10 @@
   <script>
     window.CLOSURE_NO_DEPS = true;
     {if $canonicalPath != ''}window.CANONICAL_PATH = '{$canonicalPath}';{/if}
-    {if $deprecateGwtUi}window.DEPRECATE_GWT_UI = true;{/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}
   </script>{\n}
 
   {if $faviconPath}
@@ -73,7 +71,6 @@
     <link rel="import" href="{$assetsPath}/{$assetsBundle}">{\n}
   {/if}
 
-  <link rel="preload" href="{$staticResourcePath}/elements/gr-app.js" as="script" crossorigin="anonymous">{\n}
   <link rel="import" href="{$staticResourcePath}/elements/gr-app.html">{\n}
 
   <body unresolved>{\n}
diff --git a/resources/com/google/gerrit/pgm/init/libraries.config b/resources/com/google/gerrit/pgm/init/libraries.config
deleted file mode 100644
index 3d3545b..0000000
--- a/resources/com/google/gerrit/pgm/init/libraries.config
+++ /dev/null
@@ -1,50 +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.
-
-[library "mysqlDriver"]
-  name = MySQL Connector/J 5.1.43
-  url = https://repo1.maven.org/maven2/mysql/mysql-connector-java/5.1.43/mysql-connector-java-5.1.43.jar
-  sha1 = dee9103eec0d877f3a21c82d4d9e9f4fbd2d6e0a
-  remove = mysql-connector-java-.*[.]jar
-
-[library "mariadbDriver"]
-  name = MariaDB Connector/J 2.3.0
-  url = https://repo1.maven.org/maven2/org/mariadb/jdbc/mariadb-java-client/2.3.0/mariadb-java-client-2.3.0.jar
-  sha1 = c2b1a6002a169757d0649449288e9b3b776af76b
-  remove = mariadb-java-client-.*[.]jar
-
-[library "oracleDriver"]
-  name = Oracle JDBC driver 11g Release 2 (11.2.0)
-  url = file:///u01/app/oracle/product/11.2.0/xe/jdbc/lib/ojdbc6.jar
-  sha1 = 2f89cd9176772c3a6c261ce6a8e3d0d4425f5679
-  remove = ojdbc6.jar
-
-[library "db2Driver"]
-  name = DB2 Type 4 JDBC driver (10.5)
-  url = file:///opt/ibm/db2/V10.5/java/db2jcc4.jar
-  sha1 = 9344d4fd41d6511f2d1d1deb7759056495b3a39b
-  needs = db2DriverLicense
-  remove = db2jcc4.jar
-
-# Omit SHA-1 for license JAR as it's not stable and depends on the product
-# the customer has purchased.
-[library "db2DriverLicense"]
-  name = DB2 Type 4 JDBC driver license (10.5)
-  url = file:///opt/ibm/db2/V10.5/java/db2jcc_license_cu.jar
-  remove = db2jcc_license_cu.jar
-
-[library "hanaDriver"]
-  name = HANA JDBC driver
-  url = file:///usr/sap/hdbclient/ngdbc.jar
-  remove = ngdbc.jar
diff --git a/resources/com/google/gerrit/reviewdb/BUILD b/resources/com/google/gerrit/reviewdb/BUILD
deleted file mode 100644
index 8a1b457..0000000
--- a/resources/com/google/gerrit/reviewdb/BUILD
+++ /dev/null
@@ -1,8 +0,0 @@
-filegroup(
-    name = "reviewdb",
-    srcs = glob(
-        ["**/*"],
-        exclude = ["BUILD"],
-    ),
-    visibility = ["//visibility:public"],
-)
diff --git a/resources/com/google/gerrit/reviewdb/server/index_generic.sql b/resources/com/google/gerrit/reviewdb/server/index_generic.sql
deleted file mode 100644
index c58edb7..0000000
--- a/resources/com/google/gerrit/reviewdb/server/index_generic.sql
+++ /dev/null
@@ -1,40 +0,0 @@
--- Gerrit 2 : Generic
---
-
--- Indexes to support @Query
---
-
--- *********************************************************************
--- ApprovalCategoryAccess
---    too small to bother indexing
-
-
--- *********************************************************************
--- ApprovalCategoryValueAccess
---     @PrimaryKey covers: byCategory
-
-
--- *********************************************************************
--- BranchAccess
---    @PrimaryKey covers: byProject
-
-
--- *********************************************************************
--- ChangeMessageAccess
---    @PrimaryKey covers: byChange
-
---    covers:             byPatchSet
-CREATE INDEX change_messages_byPatchset
-ON change_messages (patchset_change_id, patchset_patch_set_id);
-
--- *********************************************************************
--- PatchLineCommentAccess
---    @PrimaryKey covers: published, draft
-CREATE INDEX patch_comment_drafts
-ON patch_comments (status, author_id);
-
-
--- *********************************************************************
--- PatchSetAccess
-CREATE INDEX patch_sets_byRevision
-ON patch_sets (revision);
diff --git a/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql b/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql
deleted file mode 100644
index 7f0f1bd..0000000
--- a/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql
+++ /dev/null
@@ -1,43 +0,0 @@
-delimiter #
--- Gerrit 2 : MaxDB
---
-
--- Indexes to support @Query
---
-
--- *********************************************************************
--- ApprovalCategoryAccess
---    too small to bother indexing
-
-
--- *********************************************************************
--- ApprovalCategoryValueAccess
---     @PrimaryKey covers: byCategory
-
-
--- *********************************************************************
--- BranchAccess
---    @PrimaryKey covers: byProject
-
-
--- *********************************************************************
--- ChangeMessageAccess
---    @PrimaryKey covers: byChange
-
---    covers:             byPatchSet
-CREATE INDEX change_messages_byPatchset
-ON change_messages (patchset_change_id, patchset_patch_set_id)
-#
-
--- *********************************************************************
--- PatchLineCommentAccess
---    @PrimaryKey covers: published, draft
-CREATE INDEX patch_comment_drafts
-ON patch_comments (status, author_id)
-#
-
--- *********************************************************************
--- PatchSetAccess
-CREATE INDEX patch_sets_byRevision
-ON patch_sets (revision)
-#
diff --git a/resources/com/google/gerrit/reviewdb/server/index_postgres.sql b/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
deleted file mode 100644
index 439fed7..0000000
--- a/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
+++ /dev/null
@@ -1,87 +0,0 @@
--- Gerrit 2 : PostgreSQL
---
-
--- Cluster hot tables by their primary method of access
---
-ALTER TABLE patch_sets CLUSTER ON patch_sets_pkey;
-ALTER TABLE change_messages CLUSTER ON change_messages_pkey;
-ALTER TABLE patch_comments CLUSTER ON patch_comments_pkey;
-ALTER TABLE patch_set_approvals CLUSTER ON patch_set_approvals_pkey;
-
-CLUSTER;
-
-
--- Define function for conditional installation of PL/pgSQL.
--- This is required, because starting with PostgreSQL 9.0, PL/pgSQL
--- language is installed by default and database returns error when
--- we try to install it again.
---
--- Source: http://wiki.postgresql.org/wiki/CREATE_OR_REPLACE_LANGUAGE
--- Author: David Fetter
---
-
-delimiter //
-
-CREATE OR REPLACE FUNCTION make_plpgsql()
-RETURNS VOID
-LANGUAGE SQL
-AS $$
-CREATE LANGUAGE plpgsql;
-$$;
-
-//
-
-delimiter ;
-
-SELECT
-    CASE
-    WHEN EXISTS(
-        SELECT 1
-        FROM pg_catalog.pg_language
-        WHERE lanname='plpgsql'
-    )
-    THEN NULL
-    ELSE make_plpgsql() END;
-
-DROP FUNCTION make_plpgsql();
-
-delimiter ;
-
--- Indexes to support @Query
---
-
--- *********************************************************************
--- ApprovalCategoryAccess
---    too small to bother indexing
-
-
--- *********************************************************************
--- ApprovalCategoryValueAccess
---     @PrimaryKey covers: byCategory
-
-
--- *********************************************************************
--- BranchAccess
---    @PrimaryKey covers: byProject
-
-
--- *********************************************************************
--- ChangeMessageAccess
---    @PrimaryKey covers: byChange
-
---    covers:             byPatchSet
-CREATE INDEX change_messages_byPatchset
-ON change_messages (patchset_change_id, patchset_patch_set_id);
-
--- *********************************************************************
--- PatchLineCommentAccess
---    @PrimaryKey covers: published, draft
-CREATE INDEX patch_comment_drafts
-ON patch_comments (author_id)
-WHERE status = 'd';
-
-
--- *********************************************************************
--- PatchSetAccess
-CREATE INDEX patch_sets_byRevision
-ON patch_sets (revision);
diff --git a/resources/com/google/gerrit/server/change/ChangeMessages.properties b/resources/com/google/gerrit/server/change/ChangeMessages.properties
index b2bcde3..ec20677 100644
--- a/resources/com/google/gerrit/server/change/ChangeMessages.properties
+++ b/resources/com/google/gerrit/server/change/ChangeMessages.properties
@@ -1,11 +1,7 @@
-# Changes to this file should also be made in
-# gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
 revertChangeDefaultMessage = Revert \"{0}\"\n\nThis reverts commit {1}.
 
 reviewerCantSeeChange = {0} does not have permission to see this change
-reviewerInactive = {0} identifies an inactive account
 reviewerInvalid = {0} is not a valid user identifier
-reviewerNotFoundUser = {0} does not identify a registered user
 reviewerNotFoundUserOrGroup = {0} does not identify a registered user or group
 
 groupIsNotAllowed = The group {0} cannot be added as reviewer.
diff --git a/resources/com/google/gerrit/server/mail/Abandoned.soy b/resources/com/google/gerrit/server/mail/Abandoned.soy
index 623cfe26..2785ffc 100644
--- a/resources/com/google/gerrit/server/mail/Abandoned.soy
+++ b/resources/com/google/gerrit/server/mail/Abandoned.soy
@@ -19,12 +19,12 @@
 /**
  * .Abandoned template will determine the contents of the email related to a
  * change being abandoned.
- * @param change
- * @param coverLetter
- * @param email
- * @param fromName
  */
 {template .Abandoned kind="text"}
+  {@param change: ?}
+  {@param coverLetter: ?}
+  {@param email: ?}
+  {@param fromName: ?}
   {$fromName} has abandoned this change.
   {if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n}
   {\n}
diff --git a/resources/com/google/gerrit/server/mail/AbandonedHtml.soy b/resources/com/google/gerrit/server/mail/AbandonedHtml.soy
index 75d940f..9ad996e 100644
--- a/resources/com/google/gerrit/server/mail/AbandonedHtml.soy
+++ b/resources/com/google/gerrit/server/mail/AbandonedHtml.soy
@@ -16,12 +16,10 @@
 
 {namespace com.google.gerrit.server.mail.template}
 
-/**
- * @param coverLetter
- * @param email
- * @param fromName
- */
 {template .AbandonedHtml}
+  {@param coverLetter: ?}
+  {@param email: ?}
+  {@param fromName: ?}
   <p>
     {$fromName} <strong>abandoned</strong> this change.
   </p>
diff --git a/resources/com/google/gerrit/server/mail/AddKey.soy b/resources/com/google/gerrit/server/mail/AddKey.soy
index be76aee..8b609cf 100644
--- a/resources/com/google/gerrit/server/mail/AddKey.soy
+++ b/resources/com/google/gerrit/server/mail/AddKey.soy
@@ -19,9 +19,9 @@
 /**
  * The .AddKey template will determine the contents of the email related to
  * adding a new SSH or GPG key to an account.
- * @param email
  */
 {template .AddKey kind="text"}
+  {@param email: ?}
   One or more new {$email.keyType} keys have been added to Gerrit Code Review at
   {sp}{$email.gerritHost}:
 
diff --git a/resources/com/google/gerrit/server/mail/AddKeyHtml.soy b/resources/com/google/gerrit/server/mail/AddKeyHtml.soy
index 04a0635..ed4f435 100644
--- a/resources/com/google/gerrit/server/mail/AddKeyHtml.soy
+++ b/resources/com/google/gerrit/server/mail/AddKeyHtml.soy
@@ -16,10 +16,8 @@
 
 {namespace com.google.gerrit.server.mail.template}
 
-/**
- * @param email
- */
 {template .AddKeyHtml}
+  {@param email: ?}
   <p>
     One or more new {$email.keyType} keys have been added to Gerrit Code Review
     at {$email.gerritHost}:
diff --git a/resources/com/google/gerrit/server/mail/ChangeFooter.soy b/resources/com/google/gerrit/server/mail/ChangeFooter.soy
index f1d201b..a8170ca 100644
--- a/resources/com/google/gerrit/server/mail/ChangeFooter.soy
+++ b/resources/com/google/gerrit/server/mail/ChangeFooter.soy
@@ -19,9 +19,9 @@
 /**
  * The .ChangeFooter template will determine the contents of the footer text
  * that will be appended to ALL emails related to changes.
- * @param email
  */
 {template .ChangeFooter kind="text"}
+  {@param email: ?}
   --{sp}
   {\n}
 
diff --git a/resources/com/google/gerrit/server/mail/ChangeFooterHtml.soy b/resources/com/google/gerrit/server/mail/ChangeFooterHtml.soy
index f802366..b619c53 100644
--- a/resources/com/google/gerrit/server/mail/ChangeFooterHtml.soy
+++ b/resources/com/google/gerrit/server/mail/ChangeFooterHtml.soy
@@ -16,11 +16,9 @@
 
 {namespace com.google.gerrit.server.mail.template}
 
-/**
- * @param change
- * @param email
- */
 {template .ChangeFooterHtml}
+  {@param change: ?}
+  {@param email: ?}
   {if $email.changeUrl or $email.settingsUrl}
     <p>
       {if $email.changeUrl}
@@ -38,7 +36,7 @@
   {if $email.changeUrl}
     <div itemscope itemtype="http://schema.org/EmailMessage">
       <div itemscope itemprop="action" itemtype="http://schema.org/ViewAction">
-        <link itemprop="url" href="{$email.changeUrl |blessStringAsTrustedResourceUrlForLegacy}"/>
+        <link itemprop="url" href="{$email.changeUrl}"/>
         <meta itemprop="name" content="View Change"/>
       </div>
     </div>
diff --git a/resources/com/google/gerrit/server/mail/ChangeSubject.soy b/resources/com/google/gerrit/server/mail/ChangeSubject.soy
index 48ec9a2..7fcd213 100644
--- a/resources/com/google/gerrit/server/mail/ChangeSubject.soy
+++ b/resources/com/google/gerrit/server/mail/ChangeSubject.soy
@@ -19,13 +19,13 @@
 /**
  * The .ChangeSubject template will determine the contents of the email subject
  * line for ALL emails related to changes.
- * @param branch
- * @param change
- * @param shortProjectName
- * @param instanceAndProjectName
- * @param addInstanceNameInSubject boolean
  */
 {template .ChangeSubject kind="text"}
+  {@param branch: ?}
+  {@param change: ?}
+  {@param shortProjectName: ?}
+  {@param instanceAndProjectName: ?}
+  {@param addInstanceNameInSubject: ?}  /** boolean */
   {if not $addInstanceNameInSubject}
     Change in {$shortProjectName}[{$branch.shortName}]: {$change.shortSubject}
   {else}
diff --git a/resources/com/google/gerrit/server/mail/Comment.soy b/resources/com/google/gerrit/server/mail/Comment.soy
index f9a11cd..1eb016b 100644
--- a/resources/com/google/gerrit/server/mail/Comment.soy
+++ b/resources/com/google/gerrit/server/mail/Comment.soy
@@ -19,13 +19,13 @@
 /**
  * The .Comment template will determine the contents of the email related to a
  * user submitting comments on changes.
- * @param change
- * @param coverLetter
- * @param email
- * @param fromName
- * @param commentFiles
  */
 {template .Comment kind="text"}
+  {@param change: ?}
+  {@param coverLetter: ?}
+  {@param email: ?}
+  {@param fromName: ?}
+  {@param commentFiles: ?}
   {$fromName} has posted comments on this change.
   {if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n}
   {\n}
diff --git a/resources/com/google/gerrit/server/mail/CommentHtml.soy b/resources/com/google/gerrit/server/mail/CommentHtml.soy
index d554258..534cbdb 100644
--- a/resources/com/google/gerrit/server/mail/CommentHtml.soy
+++ b/resources/com/google/gerrit/server/mail/CommentHtml.soy
@@ -16,15 +16,13 @@
 
 {namespace com.google.gerrit.server.mail.template}
 
-/**
- * @param commentFiles
- * @param commentCount
- * @param email
- * @param labels
- * @param patchSet
- * @param patchSetCommentBlocks
- */
 {template .CommentHtml}
+  {@param commentFiles: ?}
+  {@param commentCount: ?}
+  {@param email: ?}
+  {@param labels: ?}
+  {@param patchSet: ?}
+  {@param patchSetCommentBlocks: ?}
   {let $commentHeaderStyle kind="css"}
     margin-bottom: 4px;
   {/let}
diff --git a/resources/com/google/gerrit/server/mail/DeleteKey.soy b/resources/com/google/gerrit/server/mail/DeleteKey.soy
index 7a2cdf4..30548c8 100644
--- a/resources/com/google/gerrit/server/mail/DeleteKey.soy
+++ b/resources/com/google/gerrit/server/mail/DeleteKey.soy
@@ -19,9 +19,9 @@
 /**
  * The .DeleteKey template will determine the contents of the email related to
  * deleting a SSH or GPG key.
- * @param email
  */
 {template .DeleteKey kind="text"}
+  {@param email: ?}
   One or more {$email.keyType} keys have been deleted on Gerrit Code Review at
   {sp}{$email.gerritHost}:
 
diff --git a/resources/com/google/gerrit/server/mail/DeleteKeyHtml.soy b/resources/com/google/gerrit/server/mail/DeleteKeyHtml.soy
index 25c1bd8..1ab3955 100644
--- a/resources/com/google/gerrit/server/mail/DeleteKeyHtml.soy
+++ b/resources/com/google/gerrit/server/mail/DeleteKeyHtml.soy
@@ -16,10 +16,8 @@
 
 {namespace com.google.gerrit.server.mail.template}
 
-/**
- * @param email
- */
 {template .DeleteKeyHtml}
+  {@param email: ?}
   <p>
     One or more {$email.keyType} keys have been deleted on Gerrit Code Review
     at {$email.gerritHost}:
diff --git a/resources/com/google/gerrit/server/mail/DeleteReviewer.soy b/resources/com/google/gerrit/server/mail/DeleteReviewer.soy
index 065348a..3310249 100644
--- a/resources/com/google/gerrit/server/mail/DeleteReviewer.soy
+++ b/resources/com/google/gerrit/server/mail/DeleteReviewer.soy
@@ -19,12 +19,12 @@
 /**
  * The .DeleteReviewer template will determine the contents of the email related
  * to removal of a reviewer (and the reviewer's votes) from reviews.
- * @param change
- * @param coverLetter
- * @param email
- * @param fromName
  */
 {template .DeleteReviewer kind="text"}
+  {@param change: ?}
+  {@param coverLetter: ?}
+  {@param email: ?}
+  {@param fromName: ?}
   {$fromName} has removed{sp}
   {for $reviewerName in $email.reviewerNames}
     {if not isFirst($reviewerName)},{sp}{/if}
diff --git a/resources/com/google/gerrit/server/mail/DeleteReviewerHtml.soy b/resources/com/google/gerrit/server/mail/DeleteReviewerHtml.soy
index 0599b52..54720fe 100644
--- a/resources/com/google/gerrit/server/mail/DeleteReviewerHtml.soy
+++ b/resources/com/google/gerrit/server/mail/DeleteReviewerHtml.soy
@@ -16,11 +16,9 @@
 
 {namespace com.google.gerrit.server.mail.template}
 
-/**
- * @param email
- * @param fromName
- */
 {template .DeleteReviewerHtml}
+  {@param email: ?}
+  {@param fromName: ?}
   <p>
     {$fromName}{sp}
     <strong>
diff --git a/resources/com/google/gerrit/server/mail/DeleteVote.soy b/resources/com/google/gerrit/server/mail/DeleteVote.soy
index d493e09..0ee5454 100644
--- a/resources/com/google/gerrit/server/mail/DeleteVote.soy
+++ b/resources/com/google/gerrit/server/mail/DeleteVote.soy
@@ -19,12 +19,12 @@
 /**
  * The .DeleteVote template will determine the contents of the email related
  * to removing votes on changes.
- * @param change
- * @param coverLetter
- * @param email
- * @param fromName
  */
 {template .DeleteVote kind="text"}
+  {@param change: ?}
+  {@param coverLetter: ?}
+  {@param email: ?}
+  {@param fromName: ?}
   {$fromName} has removed a vote from this change.{if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n}{\n}
   {\n}
   Change subject: {$change.subject}{\n}
diff --git a/resources/com/google/gerrit/server/mail/DeleteVoteHtml.soy b/resources/com/google/gerrit/server/mail/DeleteVoteHtml.soy
index cb8162d..3a82927 100644
--- a/resources/com/google/gerrit/server/mail/DeleteVoteHtml.soy
+++ b/resources/com/google/gerrit/server/mail/DeleteVoteHtml.soy
@@ -16,12 +16,10 @@
 
 {namespace com.google.gerrit.server.mail.template}
 
-/**
- * @param coverLetter
- * @param email
- * @param fromName
- */
 {template .DeleteVoteHtml}
+  {@param coverLetter: ?}
+  {@param email: ?}
+  {@param fromName: ?}
   <p>
     {$fromName} <strong>removed a vote</strong> from this change.
   </p>
diff --git a/resources/com/google/gerrit/server/mail/Footer.soy b/resources/com/google/gerrit/server/mail/Footer.soy
index e1890a8..7483cd9 100644
--- a/resources/com/google/gerrit/server/mail/Footer.soy
+++ b/resources/com/google/gerrit/server/mail/Footer.soy
@@ -20,9 +20,9 @@
  * The .Footer template will determine the contents of the footer text
  * appended to the end of all outgoing emails after the ChangeFooter and
  * CommentFooter.
- * @param footers
  */
 {template .Footer kind="text"}
+  {@param footers: ?}
   {for $footer in $footers}
     {$footer}{\n}
   {/for}
diff --git a/resources/com/google/gerrit/server/mail/FooterHtml.soy b/resources/com/google/gerrit/server/mail/FooterHtml.soy
index 938655c..ce934d3 100644
--- a/resources/com/google/gerrit/server/mail/FooterHtml.soy
+++ b/resources/com/google/gerrit/server/mail/FooterHtml.soy
@@ -16,10 +16,8 @@
 
 {namespace com.google.gerrit.server.mail.template}
 
-/**
- * @param footers
- */
 {template .FooterHtml}
+  {@param footers: ?}
   {\n}
   {\n}
   {for $footer in $footers}
diff --git a/resources/com/google/gerrit/server/mail/HttpPasswordUpdate.soy b/resources/com/google/gerrit/server/mail/HttpPasswordUpdate.soy
index b40e5e3..38e679e 100644
--- a/resources/com/google/gerrit/server/mail/HttpPasswordUpdate.soy
+++ b/resources/com/google/gerrit/server/mail/HttpPasswordUpdate.soy
@@ -19,9 +19,9 @@
 /**
  * The .HttpPasswordUpdate template will determine the contents of the email related to
  * adding, changing or deleting the HTTP password.
- * @param email
  */
 {template .HttpPasswordUpdate kind="text"}
+  {@param email: ?}
   The HTTP password was {$email.operation} on Gerrit Code Review at
   {sp}{$email.gerritHost}.
 
diff --git a/resources/com/google/gerrit/server/mail/HttpPasswordUpdateHtml.soy b/resources/com/google/gerrit/server/mail/HttpPasswordUpdateHtml.soy
index b229f3a..3c4594c 100644
--- a/resources/com/google/gerrit/server/mail/HttpPasswordUpdateHtml.soy
+++ b/resources/com/google/gerrit/server/mail/HttpPasswordUpdateHtml.soy
@@ -16,10 +16,8 @@
 
 {namespace com.google.gerrit.server.mail.template}
 
-/**
- * @param email
- */
 {template .HttpPasswordUpdateHtml}
+  {@param email: ?}
   <p>
     The HTTP password was {$email.operation} on Gerrit Code Review
     at {$email.gerritHost}.
diff --git a/resources/com/google/gerrit/server/mail/Merged.soy b/resources/com/google/gerrit/server/mail/Merged.soy
index 47bfaf2..899d1c0 100644
--- a/resources/com/google/gerrit/server/mail/Merged.soy
+++ b/resources/com/google/gerrit/server/mail/Merged.soy
@@ -20,11 +20,11 @@
 /**
  * The .Merged template will determine the contents of the email related to
  * a change successfully merged to the head.
- * @param change
- * @param email
- * @param fromName
  */
 {template .Merged kind="text"}
+  {@param change: ?}
+  {@param email: ?}
+  {@param fromName: ?}
   {$fromName} has submitted this change.
   {if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n}
   {\n}
diff --git a/resources/com/google/gerrit/server/mail/MergedHtml.soy b/resources/com/google/gerrit/server/mail/MergedHtml.soy
index c51be96..f0a47c7 100644
--- a/resources/com/google/gerrit/server/mail/MergedHtml.soy
+++ b/resources/com/google/gerrit/server/mail/MergedHtml.soy
@@ -16,12 +16,10 @@
 
 {namespace com.google.gerrit.server.mail.template}
 
-/**
- * @param diffLines
- * @param email
- * @param fromName
- */
 {template .MergedHtml}
+  {@param diffLines: ?}
+  {@param email: ?}
+  {@param fromName: ?}
   <p>
     {$fromName} <strong>submitted</strong> this change.
   </p>
diff --git a/resources/com/google/gerrit/server/mail/NewChange.soy b/resources/com/google/gerrit/server/mail/NewChange.soy
index 8413894..fa447e9 100644
--- a/resources/com/google/gerrit/server/mail/NewChange.soy
+++ b/resources/com/google/gerrit/server/mail/NewChange.soy
@@ -19,13 +19,13 @@
 /**
  * The .NewChange template will determine the contents of the email related to a
  * user submitting a new change for review.
- * @param change
- * @param email
- * @param ownerName
- * @param patchSet
- * @param projectName
  */
 {template .NewChange kind="text"}
+  {@param change: ?}
+  {@param email: ?}
+  {@param ownerName: ?}
+  {@param patchSet: ?}
+  {@param projectName: ?}
   {if $email.reviewerNames}
     Hello{sp}
     {for $reviewerName in $email.reviewerNames}
diff --git a/resources/com/google/gerrit/server/mail/NewChangeHtml.soy b/resources/com/google/gerrit/server/mail/NewChangeHtml.soy
index 5bce806..9de8707 100644
--- a/resources/com/google/gerrit/server/mail/NewChangeHtml.soy
+++ b/resources/com/google/gerrit/server/mail/NewChangeHtml.soy
@@ -16,15 +16,13 @@
 
 {namespace com.google.gerrit.server.mail.template}
 
-/**
- * @param diffLines
- * @param email
- * @param fromName
- * @param ownerName
- * @param patchSet
- * @param projectName
- */
 {template .NewChangeHtml}
+  {@param diffLines: ?}
+  {@param email: ?}
+  {@param fromName: ?}
+  {@param ownerName: ?}
+  {@param patchSet: ?}
+  {@param projectName: ?}
   <p>
     {if $email.reviewerNames}
       {$fromName} would like{sp}
diff --git a/resources/com/google/gerrit/server/mail/Private.soy b/resources/com/google/gerrit/server/mail/Private.soy
index bb32a7e9..510f15e 100644
--- a/resources/com/google/gerrit/server/mail/Private.soy
+++ b/resources/com/google/gerrit/server/mail/Private.soy
@@ -22,17 +22,17 @@
 
 /**
  * Private template to generate "View Change" buttons.
- * @param email
  */
 {template .ViewChangeButton}
+  {@param email: ?}
   <a href="{$email.changeUrl}">View Change</a>
 {/template}
 
 /**
  * Private template to render PRE block with consistent font-sizing.
- * @param content
  */
 {template .Pre}
+  {@param content: ?}
   {let $preStyle kind="css"}
     font-family: monospace,monospace; // Use this to avoid browsers scaling down
                                       // monospace text.
@@ -53,10 +53,9 @@
  *
  * This mechanism encodes as little structure as possible in order to depend on
  * the Soy autoescape mechanism for all of the content.
- *
- * @param content
  */
 {template .WikiFormat}
+  {@param content: ?}
   {let $blockquoteStyle kind="css"}
     border-left: 1px solid #aaa;
     margin: 10px 0;
@@ -87,10 +86,8 @@
   {/for}
 {/template}
 
-/**
- * @param diffLines
- */
 {template .UnifiedDiff}
+  {@param diffLines: ?}
   {let $addStyle kind="css"}
     color: hsl(120, 100%, 40%);
   {/let}
diff --git a/resources/com/google/gerrit/server/mail/RegisterNewEmail.soy b/resources/com/google/gerrit/server/mail/RegisterNewEmail.soy
index 2886cc0..ee03de0 100644
--- a/resources/com/google/gerrit/server/mail/RegisterNewEmail.soy
+++ b/resources/com/google/gerrit/server/mail/RegisterNewEmail.soy
@@ -19,9 +19,9 @@
 /**
  * The .RegisterNewEmail template will determine the contents of the email
  * related to registering new email accounts.
- * @param email
  */
 {template .RegisterNewEmail kind="text"}
+  {@param email: ?}
   Welcome to Gerrit Code Review at {$email.gerritHost}.{\n}
 
   {\n}
diff --git a/resources/com/google/gerrit/server/mail/ReplacePatchSet.soy b/resources/com/google/gerrit/server/mail/ReplacePatchSet.soy
index 1cb0110..bb84cf1 100644
--- a/resources/com/google/gerrit/server/mail/ReplacePatchSet.soy
+++ b/resources/com/google/gerrit/server/mail/ReplacePatchSet.soy
@@ -19,14 +19,14 @@
 /**
  * The .ReplacePatchSet template will determine the contents of the email
  * related to a user submitting a new patchset for a change.
- * @param change
- * @param email
- * @param fromEmail
- * @param fromName
- * @param patchSet
- * @param projectName
  */
 {template .ReplacePatchSet kind="text"}
+  {@param change: ?}
+  {@param email: ?}
+  {@param fromEmail: ?}
+  {@param fromName: ?}
+  {@param patchSet: ?}
+  {@param projectName: ?}
   {if $email.reviewerNames and $fromEmail == $change.ownerEmail}
     Hello{sp}
     {for $reviewerName in $email.reviewerNames}
diff --git a/resources/com/google/gerrit/server/mail/ReplacePatchSetHtml.soy b/resources/com/google/gerrit/server/mail/ReplacePatchSetHtml.soy
index e618bef..96cba5f 100644
--- a/resources/com/google/gerrit/server/mail/ReplacePatchSetHtml.soy
+++ b/resources/com/google/gerrit/server/mail/ReplacePatchSetHtml.soy
@@ -16,15 +16,13 @@
 
 {namespace com.google.gerrit.server.mail.template}
 
-/**
- * @param change
- * @param email
- * @param fromName
- * @param fromEmail
- * @param patchSet
- * @param projectName
- */
 {template .ReplacePatchSetHtml}
+  {@param change: ?}
+  {@param email: ?}
+  {@param fromName: ?}
+  {@param fromEmail: ?}
+  {@param patchSet: ?}
+  {@param projectName: ?}
   <p>
     {$fromName} <strong>uploaded patch set #{$patchSet.patchSetId}</strong>{sp}
     to{sp}
diff --git a/resources/com/google/gerrit/server/mail/Restored.soy b/resources/com/google/gerrit/server/mail/Restored.soy
index 4fc6d8c..0ec65b30 100644
--- a/resources/com/google/gerrit/server/mail/Restored.soy
+++ b/resources/com/google/gerrit/server/mail/Restored.soy
@@ -19,12 +19,12 @@
 /**
  * The .Restored template will determine the contents of the email related to a
  * change being restored.
- * @param change
- * @param coverLetter
- * @param email
- * @param fromName
  */
 {template .Restored kind="text"}
+  {@param change: ?}
+  {@param coverLetter: ?}
+  {@param email: ?}
+  {@param fromName: ?}
   {$fromName} has restored this change.
   {if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n}
   {\n}
diff --git a/resources/com/google/gerrit/server/mail/RestoredHtml.soy b/resources/com/google/gerrit/server/mail/RestoredHtml.soy
index bb856ac..bcd358f 100644
--- a/resources/com/google/gerrit/server/mail/RestoredHtml.soy
+++ b/resources/com/google/gerrit/server/mail/RestoredHtml.soy
@@ -16,11 +16,9 @@
 
 {namespace com.google.gerrit.server.mail.template}
 
-/**
- * @param email
- * @param fromName
- */
 {template .RestoredHtml}
+  {@param email: ?}
+  {@param fromName: ?}
   <p>
     {$fromName} <strong>restored</strong> this change.
   </p>
diff --git a/resources/com/google/gerrit/server/mail/Reverted.soy b/resources/com/google/gerrit/server/mail/Reverted.soy
index fba8744..32a65c6 100644
--- a/resources/com/google/gerrit/server/mail/Reverted.soy
+++ b/resources/com/google/gerrit/server/mail/Reverted.soy
@@ -19,12 +19,12 @@
 /**
  * The .Reverted template will determine the contents of the email related
  * to a change being reverted.
- * @param change
- * @param coverLetter
- * @param email
- * @param fromName
  */
 {template .Reverted kind="text"}
+  {@param change: ?}
+  {@param coverLetter: ?}
+  {@param email: ?}
+  {@param fromName: ?}
   {$fromName} has created a revert of this change.
   {if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n}
   {\n}
diff --git a/resources/com/google/gerrit/server/mail/RevertedHtml.soy b/resources/com/google/gerrit/server/mail/RevertedHtml.soy
index b7b254e..69260ad 100644
--- a/resources/com/google/gerrit/server/mail/RevertedHtml.soy
+++ b/resources/com/google/gerrit/server/mail/RevertedHtml.soy
@@ -16,11 +16,9 @@
 
 {namespace com.google.gerrit.server.mail.template}
 
-/**
- * @param email
- * @param fromName
- */
 {template .RevertedHtml}
+  {@param email: ?}
+  {@param fromName: ?}
   <p>
     {$fromName} has <strong>created a revert</strong> of this change.
   </p>
diff --git a/resources/com/google/gerrit/server/mail/SetAssignee.soy b/resources/com/google/gerrit/server/mail/SetAssignee.soy
index 98290e9..1fdf690 100644
--- a/resources/com/google/gerrit/server/mail/SetAssignee.soy
+++ b/resources/com/google/gerrit/server/mail/SetAssignee.soy
@@ -19,13 +19,13 @@
 /**
  * The .SetAssignee template will determine the contents of the email related
  * to a user being assigned to a change.
- * @param change
- * @param email
- * @param fromName
- * @param patchSet
- * @param projectName
  */
 {template .SetAssignee kind="text"}
+  {@param change: ?}
+  {@param email: ?}
+  {@param fromName: ?}
+  {@param patchSet: ?}
+  {@param projectName: ?}
   Hello{sp}
   {$email.assigneeName},
 
diff --git a/resources/com/google/gerrit/server/mail/SetAssigneeHtml.soy b/resources/com/google/gerrit/server/mail/SetAssigneeHtml.soy
index dbd3fae..1826314 100644
--- a/resources/com/google/gerrit/server/mail/SetAssigneeHtml.soy
+++ b/resources/com/google/gerrit/server/mail/SetAssigneeHtml.soy
@@ -16,14 +16,12 @@
 
 {namespace com.google.gerrit.server.mail.template}
 
-/**
- * @param diffLines
- * @param email
- * @param fromName
- * @param patchSet
- * @param projectName
- */
 {template .SetAssigneeHtml}
+  {@param diffLines: ?}
+  {@param email: ?}
+  {@param fromName: ?}
+  {@param patchSet: ?}
+  {@param projectName: ?}
   <p>
     {$fromName} has <strong>assigned</strong> a change to{sp}
     {$email.assigneeName}.{sp}
diff --git a/tools/BUILD b/tools/BUILD
index b66a1a8..6876945 100644
--- a/tools/BUILD
+++ b/tools/BUILD
@@ -61,7 +61,7 @@
         "-Xep:LiteralClassName:WARN",
         "-Xep:MissingFail:ERROR",
         "-Xep:MissingOverride:WARN",
-        "-Xep:MutableConstantField:WARN",
+        "-Xep:MutableConstantField:ERROR",
         "-Xep:NarrowingCompoundAssignment:WARN",
         "-Xep:NonAtomicVolatileUpdate:WARN",
         "-Xep:NonOverridingEquals:WARN",
diff --git a/tools/bzl/gwt.bzl b/tools/bzl/gwt.bzl
deleted file mode 100644
index cf1c47e..0000000
--- a/tools/bzl/gwt.bzl
+++ /dev/null
@@ -1,311 +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.
-
-load("//tools/bzl:genrule2.bzl", "genrule2")
-load("//tools/bzl:java.bzl", "java_library2")
-
-jar_filetype = [".jar"]
-
-BROWSERS = [
-    "chrome",
-    "firefox",
-    "gecko1_8",
-    "safari",
-    "msie",
-    "ie8",
-    "ie9",
-    "ie10",
-    "edge",
-]
-
-ALIASES = {
-    "chrome": "safari",
-    "edge": "gecko1_8",
-    "firefox": "gecko1_8",
-    "msie": "ie10",
-}
-
-MODULE = "com.google.gerrit.GerritGwtUI"
-
-GWT_COMPILER = "com.google.gwt.dev.Compiler"
-
-GWT_JVM_ARGS = ["-Xmx512m"]
-
-GWT_COMPILER_ARGS = [
-    "-XdisableClassMetadata",
-]
-
-GWT_COMPILER_ARGS_RELEASE_MODE = GWT_COMPILER_ARGS + [
-    "-XdisableCastChecking",
-]
-
-GWT_PLUGIN_DEPS_NEVERLINK = [
-    "//gerrit-plugin-gwtui:gwtui-api-lib-neverlink",
-    "//lib/gwt:user-neverlink",
-]
-
-GWT_PLUGIN_DEPS = [
-    "//gerrit-plugin-gwtui:gwtui-api-lib",
-]
-
-GWT_TRANSITIVE_DEPS = [
-    "//lib:jsr305",
-    "//lib/gwt:ant",
-    "//lib/gwt:colt",
-    "//lib/gwt:javax-validation",
-    "//lib/gwt:javax-validation_src",
-    "//lib/gwt:jsinterop-annotations",
-    "//lib/gwt:jsinterop-annotations_src",
-    "//lib/gwt:tapestry",
-    "//lib/gwt:w3c-css-sac",
-    "//lib/ow2:ow2-asm",
-    "//lib/ow2:ow2-asm-analysis",
-    "//lib/ow2:ow2-asm-commons",
-    "//lib/ow2:ow2-asm-tree",
-    "//lib/ow2:ow2-asm-util",
-]
-
-DEPS = GWT_TRANSITIVE_DEPS + [
-    "//java/com/google/gwtexpui/css",
-    "//lib:gwtjsonrpc",
-    "//lib/gwt:dev",
-    "//lib/jgit/org.eclipse.jgit:jgit-source",
-]
-
-USER_AGENT_XML = """<module rename-to='gerrit_ui'>
-<inherits name='%s'/>
-<set-property name='user.agent' value='%s'/>
-<set-property name='locale' value='default'/>
-</module>
-"""
-
-def gwt_module(gwt_xml = None, resources = [], srcs = [], **kwargs):
-    if gwt_xml:
-        resources = resources + [gwt_xml]
-
-    java_library2(
-        srcs = srcs,
-        resources = resources,
-        **kwargs
-    )
-
-def _gwt_user_agent_module(ctx):
-    """Generate user agent specific GWT module."""
-    if not ctx.attr.user_agent:
-        return None
-
-    ua = ctx.attr.user_agent
-    impl = ua
-    if ua in ALIASES:
-        impl = ALIASES[ua]
-
-    # intermediate artifact: user agent speific GWT xml file
-    gwt_user_agent_xml = ctx.actions.declare_file(ctx.label.name + "_gwt.xml")
-    ctx.actions.write(
-        output = gwt_user_agent_xml,
-        content = USER_AGENT_XML % (MODULE, impl),
-    )
-
-    # intermediate artifact: user agent specific zip with GWT module
-    gwt_user_agent_zip = ctx.actions.declare_file(ctx.label.name + "_gwt.zip")
-    gwt = "%s_%s.gwt.xml" % (MODULE.replace(".", "/"), ua)
-    dir = gwt_user_agent_zip.path + ".dir"
-    cmd = " && ".join([
-        "p=$PWD",
-        "mkdir -p %s" % dir,
-        "cd %s" % dir,
-        "mkdir -p $(dirname %s)" % gwt,
-        "cp $p/%s %s" % (gwt_user_agent_xml.path, gwt),
-        "$p/%s cC $p/%s $(find . | sed 's|^./||')" % (ctx.executable._zip.path, gwt_user_agent_zip.path),
-    ])
-    ctx.actions.run_shell(
-        inputs = [gwt_user_agent_xml],
-        outputs = [gwt_user_agent_zip],
-        tools = ctx.files._zip,
-        command = cmd,
-        mnemonic = "GenerateUserAgentGWTModule",
-    )
-
-    return struct(
-        zip = gwt_user_agent_zip,
-        module = MODULE + "_" + ua,
-    )
-
-def _gwt_binary_impl(ctx):
-    module = ctx.attr.module[0]
-    output_zip = ctx.outputs.output
-    output_dir = output_zip.path + ".gwt_output"
-    deploy_dir = output_zip.path + ".gwt_deploy"
-
-    deps = _get_transitive_closure(ctx)
-
-    paths = [dep.path for dep in deps.to_list()]
-
-    gwt_user_agent_modules = []
-    ua = _gwt_user_agent_module(ctx)
-    if ua:
-        paths.append(ua.zip.path)
-        gwt_user_agent_modules.append(ua.zip)
-        module = ua.module
-
-    cmd = "%s %s -Dgwt.normalizeTimestamps=true -cp %s %s -war %s -deploy %s " % (
-        ctx.attr._jdk[java_common.JavaRuntimeInfo].java_executable_exec_path,
-        " ".join(ctx.attr.jvm_args),
-        ":".join(paths),
-        GWT_COMPILER,
-        output_dir,
-        deploy_dir,
-    )
-
-    # TODO(davido): clean up command concatenation
-    cmd += " ".join([
-        "-style %s" % ctx.attr.style,
-        "-optimize %s" % ctx.attr.optimize,
-        "-strict",
-        " ".join(ctx.attr.compiler_args),
-        module + "\n",
-        "rm -rf %s/gwt-unitCache\n" % output_dir,
-        "root=`pwd`\n",
-        "cd %s; $root/%s Cc ../%s $(find .)\n" % (
-            output_dir,
-            ctx.executable._zip.path,
-            output_zip.basename,
-        ),
-    ])
-
-    ctx.actions.run_shell(
-        inputs = depset(direct = gwt_user_agent_modules, transitive = [deps]),
-        outputs = [output_zip],
-        tools = ctx.files._jdk + ctx.files._zip,
-        mnemonic = "GwtBinary",
-        progress_message = "GWT compiling " + output_zip.short_path,
-        command = "set -e\n" + cmd,
-    )
-
-def _get_transitive_closure(ctx):
-    deps = []
-    for dep in ctx.attr.module_deps:
-        deps.append(dep[JavaInfo].transitive_runtime_deps)
-        deps.append(dep[JavaInfo].transitive_source_jars)
-    for dep in ctx.attr.deps:
-        if JavaInfo in dep:
-            deps.append(dep[JavaInfo].transitive_runtime_deps)
-        elif hasattr(dep, "files"):
-            deps.append(dep.files)
-
-    return depset(transitive = deps)
-
-gwt_binary = rule(
-    attrs = {
-        "compiler_args": attr.string_list(),
-        "jvm_args": attr.string_list(),
-        "module": attr.string_list(default = [MODULE]),
-        "module_deps": attr.label_list(allow_files = jar_filetype),
-        "optimize": attr.string(default = "9"),
-        "style": attr.string(default = "OBF"),
-        "user_agent": attr.string(),
-        "deps": attr.label_list(allow_files = jar_filetype),
-        "_jdk": attr.label(
-            default = Label("@bazel_tools//tools/jdk:current_java_runtime"),
-            cfg = "host",
-        ),
-        "_zip": attr.label(
-            default = Label("@bazel_tools//tools/zip:zipper"),
-            cfg = "host",
-            executable = True,
-            allow_single_file = True,
-        ),
-    },
-    outputs = {
-        "output": "%{name}.zip",
-    },
-    implementation = _gwt_binary_impl,
-)
-
-def gwt_genrule(suffix = ""):
-    dbg = "ui_dbg" + suffix
-    opt = "ui_opt" + suffix
-    module_dep = ":ui_module" + suffix
-    args = GWT_COMPILER_ARGS_RELEASE_MODE if suffix == "_r" else GWT_COMPILER_ARGS
-
-    genrule2(
-        name = "ui_optdbg" + suffix,
-        srcs = [
-            ":" + dbg,
-            ":" + opt,
-        ],
-        cmd = "cd $$TMP;" +
-              "unzip -q $$ROOT/$(location :%s);" % dbg +
-              "mv" +
-              " gerrit_ui/gerrit_ui.nocache.js" +
-              " gerrit_ui/dbg_gerrit_ui.nocache.js;" +
-              "unzip -qo $$ROOT/$(location :%s);" % opt +
-              "mkdir -p $$(dirname $@);" +
-              "zip -qrD $$ROOT/$@ .",
-        outs = ["ui_optdbg" + suffix + ".zip"],
-        visibility = ["//visibility:public"],
-    )
-
-    gwt_binary(
-        name = opt,
-        module = [MODULE],
-        module_deps = [module_dep],
-        deps = DEPS,
-        compiler_args = args,
-        jvm_args = GWT_JVM_ARGS,
-    )
-
-    gwt_binary(
-        name = dbg,
-        style = "PRETTY",
-        optimize = "0",
-        module_deps = [module_dep],
-        deps = DEPS,
-        compiler_args = GWT_COMPILER_ARGS,
-        jvm_args = GWT_JVM_ARGS,
-    )
-
-def gen_ui_module(name, suffix = ""):
-    gwt_module(
-        name = name + suffix,
-        srcs = native.glob(["src/main/java/**/*.java"]),
-        gwt_xml = "src/main/java/%s.gwt.xml" % MODULE.replace(".", "/"),
-        resources = native.glob(
-            ["src/main/java/**/*"],
-            exclude = ["src/main/java/**/*.java"] +
-                      ["src/main/java/%s.gwt.xml" % MODULE.replace(".", "/")],
-        ),
-        deps = [
-            "//gerrit-gwtui-common:diffy_logo",
-            "//gerrit-gwtui-common:client",
-            "//java/com/google/gwtexpui/css",
-            "//lib/codemirror:codemirror" + suffix,
-            "//lib/gwt:user",
-        ],
-        visibility = ["//visibility:public"],
-    )
-
-def gwt_user_agent_permutations():
-    for ua in BROWSERS:
-        gwt_binary(
-            name = "ui_%s" % ua,
-            user_agent = ua,
-            style = "PRETTY",
-            optimize = "0",
-            module = [MODULE],
-            module_deps = [":ui_module"],
-            deps = DEPS,
-            compiler_args = GWT_COMPILER_ARGS,
-            jvm_args = GWT_JVM_ARGS,
-        )
diff --git a/tools/bzl/js.bzl b/tools/bzl/js.bzl
index 8184d75..b8dd202 100644
--- a/tools/bzl/js.bzl
+++ b/tools/bzl/js.bzl
@@ -364,6 +364,7 @@
             "python",
             ctx.file._run_npm.path,
             ctx.file._crisper_archive.path,
+            "--script-in-head=false",
             "--always-write-script",
             "--source",
             bundled.path,
@@ -430,7 +431,7 @@
     """Combine html, js, css files and optionally split into js and html bundles."""
     _bundle_rule(pkg = native.package_name(), *args, **kwargs)
 
-def polygerrit_plugin(name, app, srcs = [], deps = [], assets = None, plugin_name = None, **kwargs):
+def polygerrit_plugin(name, app, srcs = [], deps = [], externs = [], assets = None, plugin_name = None, **kwargs):
     """Bundles plugin dependencies for deployment.
 
     This rule bundles all Polymer elements and JS dependencies into .html and .js files.
@@ -440,6 +441,7 @@
     Args:
       name: String, rule name.
       app: String, the main or root source file.
+      externs: Fileset, external definitions that should not be bundled.
       assets: Fileset, additional files to be used by plugin in runtime, exported to "plugins/${name}/static".
       plugin_name: String, plugin name. ${name} is used if not provided.
     """
@@ -465,7 +467,7 @@
 
     closure_js_library(
         name = name + "_closure_lib",
-        srcs = js_srcs,
+        srcs = js_srcs + externs,
         convention = "GOOGLE",
         no_closure_library = True,
         deps = [
diff --git a/tools/bzl/junit.bzl b/tools/bzl/junit.bzl
index 21d2cd8..1cf82ea 100644
--- a/tools/bzl/junit.bzl
+++ b/tools/bzl/junit.bzl
@@ -82,8 +82,8 @@
     )
     jvm_flags = kwargs.get("jvm_flags", [])
     jvm_flags = jvm_flags + select({
-        "//:java_next": POST_JDK8_OPTS,
         "//:java9": POST_JDK8_OPTS,
+        "//:java_next": POST_JDK8_OPTS,
         "//conditions:default": [],
     })
     java_test(
diff --git a/tools/bzl/license-map.py b/tools/bzl/license-map.py
index ebe57f2..2779130 100644
--- a/tools/bzl/license-map.py
+++ b/tools/bzl/license-map.py
@@ -54,6 +54,8 @@
     # We don't want any blank line before "= Gerrit Code Review - Licenses"
     print("""= Gerrit Code Review - Licenses
 
+// DO NOT EDIT - GENERATED AUTOMATICALLY.
+
 Gerrit open source software is licensed under the <<Apache2_0,Apache
 License 2.0>>.  Executable distributions also include other software
 components that are provided under additional licenses.
diff --git a/tools/bzl/license.bzl b/tools/bzl/license.bzl
index d059216..5a6bf7f 100644
--- a/tools/bzl/license.bzl
+++ b/tools/bzl/license.bzl
@@ -26,7 +26,7 @@
     native.genrule(
         name = "gen_license_txt_" + name,
         cmd = "python $(location //tools/bzl:license-map.py) %s %s > $@" % (" ".join(opts), " ".join(xmls)),
-        outs = [name + ".txt"],
+        outs = [name + ".gen.txt"],
         tools = tools,
         **kwargs
     )
diff --git a/tools/bzl/maven_jar.bzl b/tools/bzl/maven_jar.bzl
index 7730804..c8ba7ca 100644
--- a/tools/bzl/maven_jar.bzl
+++ b/tools/bzl/maven_jar.bzl
@@ -140,8 +140,6 @@
     args = [python, script, "-o", binjar_path, "-u", binurl]
     if ctx.attr.sha1:
         args.extend(["-v", sha1])
-    if ctx.attr.unsign:
-        args.append("--unsign")
     for x in ctx.attr.exclude:
         args.extend(["-x", x])
 
diff --git a/tools/bzl/pkg_war.bzl b/tools/bzl/pkg_war.bzl
index ee57e54..ab753bd 100644
--- a/tools/bzl/pkg_war.bzl
+++ b/tools/bzl/pkg_war.bzl
@@ -19,7 +19,6 @@
 LIBS = [
     "//java/com/google/gerrit/common:version",
     "//java/com/google/gerrit/httpd/init",
-    "//lib:postgresql",
     "//lib/bouncycastle:bcpkix",
     "//lib/bouncycastle:bcprov",
     "//lib/bouncycastle:bcpg",
@@ -137,14 +136,12 @@
     implementation = _war_impl,
 )
 
-def pkg_war(name, ui = "ui_optdbg", context = [], doc = False, **kwargs):
+def pkg_war(name, ui = "polygerrit", context = [], doc = False, **kwargs):
     doc_ctx = []
     doc_lib = []
     ui_deps = []
-    if ui == "polygerrit" or ui == "ui_optdbg" or ui == "ui_optdbg_r":
+    if ui == "polygerrit":
         ui_deps.append("//polygerrit-ui/app:polygerrit_ui")
-    if ui and ui != "polygerrit":
-        ui_deps.append("//gerrit-gwtui:%s" % ui)
     if doc:
         doc_ctx.append("//Documentation:html")
         doc_lib.append("//Documentation:index")
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
index ca3a6ab..ed64d1b 100644
--- a/tools/bzl/plugin.bzl
+++ b/tools/bzl/plugin.bzl
@@ -1,16 +1,5 @@
 load("@rules_java//java:defs.bzl", "java_binary", "java_library")
 load("//tools/bzl:genrule2.bzl", "genrule2")
-load(
-    "//tools/bzl:gwt.bzl",
-    "GWT_COMPILER_ARGS",
-    "GWT_JVM_ARGS",
-    "GWT_PLUGIN_DEPS_NEVERLINK",
-    "GWT_TRANSITIVE_DEPS",
-    "gwt_binary",
-    _gwt_plugin_deps = "GWT_PLUGIN_DEPS",
-)
-
-GWT_PLUGIN_DEPS = _gwt_plugin_deps
 
 PLUGIN_DEPS = ["//plugins:plugin-lib"]
 
@@ -28,7 +17,6 @@
         deps = [],
         provided_deps = [],
         srcs = [],
-        gwt_module = [],
         resources = [],
         manifest_entries = [],
         dir_name = None,
@@ -38,14 +26,12 @@
         name = name + "__plugin",
         srcs = srcs,
         resources = resources,
-        deps = provided_deps + deps + GWT_PLUGIN_DEPS_NEVERLINK + PLUGIN_DEPS_NEVERLINK,
+        deps = provided_deps + deps + PLUGIN_DEPS_NEVERLINK,
         visibility = ["//visibility:public"],
         **kwargs
     )
 
     static_jars = []
-    if gwt_module:
-        static_jars = [":%s-static" % name]
 
     if not dir_name:
         dir_name = name
@@ -61,34 +47,6 @@
         **kwargs
     )
 
-    if gwt_module:
-        java_library(
-            name = name + "__gwt_module",
-            resources = depset(srcs + resources).to_list(),
-            runtime_deps = deps + GWT_PLUGIN_DEPS,
-            visibility = ["//visibility:public"],
-            **kwargs
-        )
-        genrule2(
-            name = "%s-static" % name,
-            cmd = " && ".join([
-                "mkdir -p $$TMP/static",
-                "unzip -qd $$TMP/static $(location %s__gwt_application)" % name,
-                "cd $$TMP",
-                "zip -qr $$ROOT/$@ .",
-            ]),
-            tools = [":%s__gwt_application" % name],
-            outs = ["%s-static.jar" % name],
-        )
-        gwt_binary(
-            name = name + "__gwt_application",
-            module = [gwt_module],
-            deps = GWT_PLUGIN_DEPS + GWT_TRANSITIVE_DEPS + ["//lib/gwt:dev"],
-            module_deps = [":%s__gwt_module" % name],
-            compiler_args = GWT_COMPILER_ARGS,
-            jvm_args = GWT_JVM_ARGS,
-        )
-
     # TODO(davido): Remove manual merge of manifest file when this feature
     # request is implemented: https://github.com/bazelbuild/bazel/issues/2009
     genrule2(
diff --git a/tools/bzl/plugins.bzl b/tools/bzl/plugins.bzl
index 7fd7625..adde59e 100644
--- a/tools/bzl/plugins.bzl
+++ b/tools/bzl/plugins.bzl
@@ -1,11 +1,15 @@
 CORE_PLUGINS = [
     "codemirror-editor",
     "commit-message-length-validator",
+    "delete-project",
     "download-commands",
+    "gitiles",
     "hooks",
+    "plugin-manager",
     "replication",
     "reviewnotes",
     "singleusergroup",
+    "webhooks",
 ]
 
 CUSTOM_PLUGINS = [
diff --git a/tools/coverage.sh b/tools/coverage.sh
index 72e1d5b..11e50e6 100755
--- a/tools/coverage.sh
+++ b/tools/coverage.sh
@@ -22,15 +22,27 @@
 
 # coverage is expensive to run; use --jobs=2 to avoid overloading the
 # machine.
-bazel coverage -k --jobs=${COVERAGE_CPUS:-2} -- ... -//javatests/com/google/gerrit/common:auto_value_tests
+bazel coverage -k --jobs=${COVERAGE_CPUS:-2} -- ...
 
 # The coverage data contains filenames relative to the Java root, and
 # genhtml has no logic to search these elsewhere. Workaround this
 # limitation by running genhtml in a directory with the files in the
 # right place. Also -inexplicably- genhtml wants to have the source
 # files relative to the output directory.
-mkdir -p ${destdir}/
-cp -a */src/{main,test}/java/* ${destdir}/
+mkdir -p ${destdir}/java
+cp -r {java,javatests}/* ${destdir}/java
+
+mkdir -p ${destdir}/plugins
+for plugin in `find plugins/ -type d` -maxdepth 1
+do
+  mkdir -p ${destdir}/${plugin}/java
+  cp -r plugins/*/{java,javatests}/* ${destdir}/${plugin}/java
+
+  # for backwards compatibility support plugins with old file structure
+  mkdir -p ${destdir}/${plugin}/src/{main,test}/java
+  cp -r plugins/*/src/main/java/* ${destdir}/${plugin}/src/main/java
+  cp -r plugins/*/src/test/java/* ${destdir}/${plugin}/src/test/java
+done
 
 base=$(bazel info bazel-testlogs)
 for f in $(find ${base}  -name 'coverage.dat') ; do
diff --git a/tools/download_file.py b/tools/download_file.py
index 29398e6..d0fe96d 100755
--- a/tools/download_file.py
+++ b/tools/download_file.py
@@ -81,7 +81,6 @@
 opts.add_option('-v', help='expected content SHA-1')
 opts.add_option('-x', action='append', help='file to delete from ZIP')
 opts.add_option('--exclude_java_sources', action='store_true')
-opts.add_option('--unsign', action='store_true')
 args, _ = opts.parse_args()
 
 root_dir = args.o
@@ -140,18 +139,6 @@
         print('error opening %s: %s' % (cache_ent, err), file=stderr)
         exit(1)
 
-if args.unsign:
-    try:
-        with ZipFile(cache_ent, 'r') as zf:
-            for n in zf.namelist():
-                if (n.endswith('.RSA')
-                   or n.endswith('.SF')
-                   or n.endswith('.LIST')):
-                    exclude.append(n)
-    except (BadZipfile, LargeZipFile) as err:
-        print('error opening %s: %s' % (cache_ent, err), file=stderr)
-        exit(1)
-
 safe_mkdirs(path.dirname(args.o))
 if exclude:
     try:
diff --git a/tools/eclipse/BUILD b/tools/eclipse/BUILD
index ddb686a..e091fc1 100644
--- a/tools/eclipse/BUILD
+++ b/tools/eclipse/BUILD
@@ -9,53 +9,39 @@
 )
 
 TEST_DEPS = [
-    "//gerrit-gwtui:ui_tests",
     "//javatests/com/google/gerrit/elasticsearch:elasticsearch_test_utils",
     "//javatests/com/google/gerrit/server:server_tests",
 ]
 
+TEST_DEPS_GENERATED = [
+    "//proto/testing:test_java_proto",
+]
+
 DEPS = [
-    "//gerrit-gwtdebug:gwtdebug",
-    "//gerrit-gwtui:ui_module",
-    "//gerrit-plugin-gwtui:gwtui-api-lib",
     "//java/com/google/gerrit/acceptance:lib",
     "//java/com/google/gerrit/server",
     "//java/com/google/gerrit/asciidoctor:asciidoc_lib",
     "//java/com/google/gerrit/asciidoctor:doc_indexer_lib",
     "//lib/auto:auto-value",
-    "//lib/gwt:ant",
-    "//lib/gwt:colt",
-    "//lib/gwt:javax-validation",
-    "//lib/gwt:javax-validation_src",
-    "//lib/gwt:jsinterop-annotations",
-    "//lib/gwt:jsinterop-annotations_src",
-    "//lib/gwt:tapestry",
-    "//lib/gwt:w3c-css-sac",
-    "//lib/jetty:servlets",
     "//lib/prolog:compiler-lib",
-    "//proto:reviewdb_java_proto",
+    "//proto:entities_java_proto",
 ]
 
 java_library(
     name = "classpath",
     testonly = True,
-    runtime_deps = LIBS + PGMLIBS + DEPS,
+    runtime_deps = LIBS + PGMLIBS + DEPS + TEST_DEPS_GENERATED,
 )
 
 classpath_collector(
     name = "main_classpath_collect",
     testonly = True,
-    deps = LIBS + PGMLIBS + DEPS + TEST_DEPS +
+    deps = LIBS + PGMLIBS + DEPS + TEST_DEPS + TEST_DEPS_GENERATED +
            ["//plugins/%s:%s__plugin" % (n, n) for n in CORE_PLUGINS + CUSTOM_PLUGINS] +
            ["//plugins/%s:%s__plugin_test_deps" % (n, n) for n in CUSTOM_PLUGINS_TEST_DEPS],
 )
 
 classpath_collector(
-    name = "gwt_classpath_collect",
-    deps = ["//gerrit-gwtui:ui_module"],
-)
-
-classpath_collector(
     name = "autovalue_classpath_collect",
     deps = ["//lib/auto:auto-value"],
 )
diff --git a/tools/eclipse/gerrit_gwt_debug.launch b/tools/eclipse/gerrit_gwt_debug.launch
deleted file mode 100644
index 593837a..0000000
--- a/tools/eclipse/gerrit_gwt_debug.launch
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<launchConfiguration type="org.eclipse.jdt.launching.localJavaApplication">
-<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
-<listEntry value="/gerrit/gerrit-gwtdebug/src/main/java/com/google/gerrit/gwtdebug/GerritGwtDebugLauncher.java"/>
-</listAttribute>
-<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
-<listEntry value="1"/>
-</listAttribute>
-<listAttribute key="org.eclipse.debug.ui.favoriteGroups">
-<listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
-</listAttribute>
-<booleanAttribute key="org.eclipse.jdt.launching.ATTR_USE_START_ON_FIRST_THREAD" value="true"/>
-<listAttribute key="org.eclipse.jdt.launching.CLASSPATH">
-<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry containerPath=&quot;org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.7&quot; javaProject=&quot;gerrit&quot; path=&quot;1&quot; type=&quot;4&quot;/&gt;&#10;"/>
-<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry id=&quot;org.eclipse.jdt.launching.classpathentry.defaultClasspath&quot;&gt;&#10;&lt;memento exportedEntriesOnly=&quot;false&quot; project=&quot;gerrit&quot;/&gt;&#10;&lt;/runtimeClasspathEntry&gt;&#10;"/>
-</listAttribute>
-<booleanAttribute key="org.eclipse.jdt.launching.DEFAULT_CLASSPATH" value="false"/>
-<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="com.google.gerrit.gwtdebug.GerritGwtDebugLauncher"/>
-<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-strict -noprecompile -src ${resource_loc:/gerrit}/java -workDir ${resource_loc:/gerrit}/.gwt_work_dir com.google.gerrit.GerritGwtUI -src ${resource_loc:/gerrit}/gerrit-plugin-gwtui/src/main/java -- --console-log --show-stack-trace -d ${resource_loc:/gerrit}/../gerrit_testsite"/>
-<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="gerrit"/>
-<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Xmx1024M&#10;-XX:MaxPermSize=256M&#10;-Dgerrit.disable-gwtui-recompile=true"/>
-</launchConfiguration>
diff --git a/tools/eclipse/project.py b/tools/eclipse/project.py
index fe20331..649f7da 100755
--- a/tools/eclipse/project.py
+++ b/tools/eclipse/project.py
@@ -22,7 +22,6 @@
 import sys
 
 MAIN = '//tools/eclipse:classpath'
-GWT = '//gerrit-gwtui:ui_module'
 AUTO = '//lib/auto:auto-value'
 JRE = '/'.join([
     'org.eclipse.jdt.launching.JRE_CONTAINER',
@@ -32,7 +31,6 @@
 # Map of targets to corresponding classpath collector rules
 cp_targets = {
     AUTO: '//tools/eclipse:autovalue_classpath_collect',
-    GWT: '//tools/eclipse:gwt_classpath_collect',
     MAIN: '//tools/eclipse:main_classpath_collect',
 }
 
@@ -188,12 +186,25 @@
             e.setAttribute('output', out)
         if exported:
             e.setAttribute('exported', 'true')
+        atts = None
         if out and "test" in out:
             atts = doc.createElement('attributes')
             testAtt = doc.createElement('attribute')
             testAtt.setAttribute('name', 'test')
             testAtt.setAttribute('value', 'true')
             atts.appendChild(testAtt)
+        if "apt_generated" in path:
+            if not atts:
+                atts = doc.createElement('attributes')
+            ignoreOptionalProblems = doc.createElement('attribute')
+            ignoreOptionalProblems.setAttribute('name', 'ignore_optional_problems')
+            ignoreOptionalProblems.setAttribute('value', 'true')
+            atts.appendChild(ignoreOptionalProblems)
+            optional = doc.createElement('attribute')
+            optional.setAttribute('name', 'optional')
+            optional.setAttribute('value', 'true')
+            atts.appendChild(optional)
+        if atts:
             e.appendChild(atts)
         doc.documentElement.appendChild(e)
 
@@ -201,8 +212,6 @@
     src = set()
     lib = set()
     proto = set()
-    gwt_src = set()
-    gwt_lib = set()
     plugins = set()
 
     # Classpath entries are absolute for cross-cell support
@@ -210,10 +219,6 @@
     srcs = re.compile('(.*/external/[^/]+)/jar/(.*)[.]jar')
     for p in _query_classpath(MAIN):
         if p.endswith('-src.jar'):
-            # gwt_module() depends on -src.jar for Java to JavaScript compiles.
-            if p.startswith("external"):
-                p = os.path.join(ext, p)
-            gwt_lib.add(p)
             continue
 
         m = java_library.match(p)
@@ -241,11 +246,6 @@
                 p = os.path.join(ext, p)
             lib.add(p)
 
-    for p in _query_classpath(GWT):
-        m = java_library.match(p)
-        if m:
-            gwt_src.add(m.group(1))
-
     classpathentry('src', 'java')
     classpathentry('src', 'javatests', out='eclipse-out/test')
     classpathentry('src', 'resources')
@@ -262,7 +262,10 @@
 
         p = os.path.join(s, 'java')
         if os.path.exists(p):
-            classpathentry('src', p, out=out)
+            classpathentry('src', p, out=out + '/main')
+            p = os.path.join(s, 'javatests')
+            if os.path.exists(p):
+                classpathentry('src', p, out=out + '/test')
             continue
 
         for env in ['main', 'test']:
@@ -277,7 +280,7 @@
                 if os.path.exists(p):
                     classpathentry('src', p, out=o)
 
-    for libs in [lib, gwt_lib]:
+    for libs in [lib]:
         for j in sorted(libs):
             s = None
             m = srcs.match(j)
@@ -290,30 +293,18 @@
             if args.plugins:
                 classpathentry('lib', j, s, exported=True)
             else:
-                # Filter out the source JARs that we pull through transitive
-                # closure of GWT plugin API (we add source directories
-                # themselves).  Exception is libEdit-src.jar, that is needed
-                # for GWT SDM to work.
-                m = java_library.match(j)
-                if m:
-                    if m.group(1).startswith("gerrit-") and \
-                       j.endswith("-src.jar") and \
-                       not j.endswith("libEdit-src.jar"):
-                        continue
                 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 = s.replace('.jar', '-src.jar')
         classpathentry('lib', p, s)
 
-    for s in sorted(gwt_src):
-        p = os.path.join(ROOT, s, 'src', 'main', 'java')
-        if os.path.exists(p):
-            classpathentry('lib', p, out='eclipse-out/gwtsrc')
-
     classpathentry('con', JRE)
     classpathentry('output', 'eclipse-out/classes')
+    classpathentry('src', '.apt_generated')
+    classpathentry('src', '.apt_generated_tests', out="eclipse-out/test")
 
     p = os.path.join(ROOT, '.classpath')
     with open(p, 'w') as fd:
@@ -353,14 +344,8 @@
     gen_factorypath(ext_location)
     gen_bazel_path(ext_location)
 
-    # TODO(davido): Remove this when GWT gone
-    gwt_working_dir = ".gwt_work_dir"
-    if not os.path.isdir(gwt_working_dir):
-        os.makedirs(os.path.join(ROOT, gwt_working_dir))
-
     try:
-        subprocess.check_call(_build_bazel_cmd('build', MAIN, GWT,
-                              '//java/org/eclipse/jgit:libEdit-src.jar'))
+        subprocess.check_call(_build_bazel_cmd('build', MAIN))
     except subprocess.CalledProcessError:
         exit(1)
 except KeyboardInterrupt:
diff --git a/tools/js/bower2bazel.py b/tools/js/bower2bazel.py
index 7d00c0d..a03f764 100755
--- a/tools/js/bower2bazel.py
+++ b/tools/js/bower2bazel.py
@@ -204,11 +204,12 @@
     for d in data:
         if d["name"] in seeds:
             continue
-        out.write("""  bower_archive(
-    name = "%(name)s",
-    package = "%(normalized-name)s",
-    version = "%(version)s",
-    sha1 = "%(bazel-sha1)s")
+        out.write("""    bower_archive(
+        name = "%(name)s",
+        package = "%(normalized-name)s",
+        version = "%(version)s",
+        sha1 = "%(bazel-sha1)s",
+    )
 """ % d)
 
 
@@ -216,21 +217,21 @@
     out.write('load("//tools/bzl:js.bzl", "bower_component")\n\n')
     out.write('def define_bower_components():\n')
     for d in data:
-        out.write("  bower_component(\n")
-        out.write("    name = \"%s\",\n" % d["name"])
-        out.write("    license = \"//lib:LICENSE-%s\",\n" % d["bazel-license"])
+        out.write("    bower_component(\n")
+        out.write("        name = \"%s\",\n" % d["name"])
+        out.write("        license = \"//lib:LICENSE-%s\",\n" % d["bazel-license"])
         deps = sorted(d.get("dependencies", {}).keys())
         if deps:
             if len(deps) == 1:
-                out.write("    deps = [ \":%s\" ],\n" % deps[0])
+                out.write("        deps = [\":%s\"],\n" % deps[0])
             else:
-                out.write("    deps = [\n")
+                out.write("        deps = [\n")
                 for dep in deps:
-                    out.write("      \":%s\",\n" % dep)
-                out.write("    ],\n")
+                    out.write("            \":%s\",\n" % dep)
+                out.write("        ],\n")
         if d["name"] in seeds:
-            out.write("    seed = True,\n")
-        out.write("  )\n")
+            out.write("        seed = True,\n")
+        out.write("    )\n")
     # done
 
 
diff --git a/tools/maven/BUILD b/tools/maven/BUILD
index 10ed27d..6cbd219 100644
--- a/tools/maven/BUILD
+++ b/tools/maven/BUILD
@@ -10,19 +10,16 @@
         "gerrit-acceptance-framework": "//java/com/google/gerrit/acceptance:libframework-lib-src.jar",
         "gerrit-extension-api": "//java/com/google/gerrit/extensions:libapi-src.jar",
         "gerrit-plugin-api": "//plugins:plugin-api-sources_deploy.jar",
-        "gerrit-plugin-gwtui": "//gerrit-plugin-gwtui:gwtui-api-source_deploy.jar",
     },
     doc = {
         "gerrit-acceptance-framework": "//java/com/google/gerrit/acceptance:framework-javadoc",
         "gerrit-extension-api": "//java/com/google/gerrit/extensions:extension-api-javadoc",
         "gerrit-plugin-api": "//plugins:plugin-api-javadoc",
-        "gerrit-plugin-gwtui": "//gerrit-plugin-gwtui:gwtui-api-javadoc",
     },
     jar = {
         "gerrit-acceptance-framework": "//java/com/google/gerrit/acceptance:framework_deploy.jar",
         "gerrit-extension-api": "//java/com/google/gerrit/extensions:extension-api_deploy.jar",
         "gerrit-plugin-api": "//plugins:plugin-api_deploy.jar",
-        "gerrit-plugin-gwtui": "//gerrit-plugin-gwtui:gwtui-api_deploy.jar",
     },
     repository = MAVEN_REPOSITORY,
     url = URL,
diff --git a/tools/maven/gerrit-acceptance-framework_pom.xml b/tools/maven/gerrit-acceptance-framework_pom.xml
index d0607f1..9de10b1 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>2.16.16-SNAPSHOT</version>
+  <version>3.0.7-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Acceptance Test Framework</name>
   <description>Framework for Gerrit's acceptance tests</description>
diff --git a/tools/maven/gerrit-extension-api_pom.xml b/tools/maven/gerrit-extension-api_pom.xml
index 470aec2..a0970c8 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>2.16.16-SNAPSHOT</version>
+  <version>3.0.7-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Extension API</name>
   <description>API for Gerrit Extensions</description>
diff --git a/tools/maven/gerrit-plugin-api_pom.xml b/tools/maven/gerrit-plugin-api_pom.xml
index bf89865..fe5a7f6 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>2.16.16-SNAPSHOT</version>
+  <version>3.0.7-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Plugin API</name>
   <description>API for Gerrit Plugins</description>
diff --git a/tools/maven/gerrit-plugin-gwtui_pom.xml b/tools/maven/gerrit-plugin-gwtui_pom.xml
deleted file mode 100644
index 498ab82..0000000
--- a/tools/maven/gerrit-plugin-gwtui_pom.xml
+++ /dev/null
@@ -1,83 +0,0 @@
-<project>
-  <modelVersion>4.0.0</modelVersion>
-  <groupId>com.google.gerrit</groupId>
-  <artifactId>gerrit-plugin-gwtui</artifactId>
-  <version>2.16.16-SNAPSHOT</version>
-  <packaging>jar</packaging>
-  <name>Gerrit Code Review - Plugin GWT UI</name>
-  <description>Common Classes for Gerrit GWT UI Plugins</description>
-  <url>https://www.gerritcodereview.com/</url>
-
-  <licenses>
-    <license>
-      <name>The Apache Software License, Version 2.0</name>
-      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
-      <distribution>repo</distribution>
-    </license>
-  </licenses>
-
-  <scm>
-    <url>https://gerrit.googlesource.com/gerrit</url>
-    <connection>https://gerrit.googlesource.com/gerrit</connection>
-  </scm>
-
-  <developers>
-    <developer>
-      <name>Alice Kober-Sotzek</name>
-    </developer>
-    <developer>
-      <name>Ben Rohlfs</name>
-    </developer>
-    <developer>
-      <name>Dave Borowitz</name>
-    </developer>
-    <developer>
-      <name>David Ostrovsky</name>
-    </developer>
-    <developer>
-      <name>David Pursehouse</name>
-    </developer>
-    <developer>
-      <name>Edwin Kempin</name>
-    </developer>
-    <developer>
-      <name>Han-Wen Nienhuys</name>
-    </developer>
-    <developer>
-      <name>Hugo Arès</name>
-    </developer>
-    <developer>
-      <name>Luca Milanesio</name>
-    </developer>
-    <developer>
-      <name>Marco Miller</name>
-    </developer>
-    <developer>
-      <name>Martin Fick</name>
-    </developer>
-    <developer>
-      <name>Ole Rehmsen</name>
-    </developer>
-    <developer>
-      <name>Patrick Hiesel</name>
-    </developer>
-    <developer>
-      <name>Saša Živkov</name>
-    </developer>
-  </developers>
-
-  <mailingLists>
-    <mailingList>
-      <name>Repo and Gerrit Discussion</name>
-      <post>repo-discuss@googlegroups.com</post>
-      <subscribe>https://groups.google.com/forum/#!forum/repo-discuss</subscribe>
-      <unsubscribe>https://groups.google.com/forum/#!forum/repo-discuss</unsubscribe>
-      <archive>https://groups.google.com/forum/#!forum/repo-discuss</archive>
-    </mailingList>
-  </mailingLists>
-
-  <issueManagement>
-    <url>https://bugs.chromium.org/p/gerrit/issues/list</url>
-    <system>Gerrit Issue Tracker</system>
-  </issueManagement>
-</project>
diff --git a/tools/maven/gerrit-war_pom.xml b/tools/maven/gerrit-war_pom.xml
index f991f6e..d5bb97c 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>2.16.16-SNAPSHOT</version>
+  <version>3.0.7-SNAPSHOT</version>
   <packaging>war</packaging>
   <name>Gerrit Code Review - WAR</name>
   <description>Gerrit WAR</description>
diff --git a/tools/version.py b/tools/version.py
index 4aafcb0..bb3b560 100755
--- a/tools/version.py
+++ b/tools/version.py
@@ -46,8 +46,7 @@
 src_pattern = re.compile(r'^(\s*<version>)([-.\w]+)(</version>\s*)$',
                          re.MULTILINE)
 for project in ['gerrit-acceptance-framework', 'gerrit-extension-api',
-                'gerrit-plugin-api', 'gerrit-plugin-gwtui',
-                'gerrit-war']:
+                'gerrit-plugin-api', 'gerrit-war']:
     pom = os.path.join('tools', 'maven', '%s_pom.xml' % project)
     replace_in_file(pom, src_pattern)
 
diff --git a/version.bzl b/version.bzl
index 3a11a12..5fb580c 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 = "2.16.16-SNAPSHOT"
+GERRIT_VERSION = "3.0.7-SNAPSHOT"
diff --git a/webapp/WEB-INF/extra/jetty7/gerrit.xml b/webapp/WEB-INF/extra/jetty7/gerrit.xml
index cb0a256..4102f56 100644
--- a/webapp/WEB-INF/extra/jetty7/gerrit.xml
+++ b/webapp/WEB-INF/extra/jetty7/gerrit.xml
@@ -6,11 +6,7 @@
   so it answers to simple URLs like "/$changeid" and "/mine".
 
   * Copy this file to $JETTY_HOME/contexts/gerrit.xml
-  * Edit url, username, password as necessary below for database.
 
-  * Copy commons-dbcp-*.jar     to $JETTY_HOME/lib/ext/
-  * Copy commons-pool-*.jar     to $JETTY_HOME/lib/ext/
-  * Copy JDBC driver            to $JETTY_HOME/lib/ext/
   * Copy www/gerrit-*.war       to $JETTY_HOME/webapps/gerrit.war
 
   * Make sure you remove $JETTY_HOME/context/test.xml
@@ -33,36 +29,4 @@
       <Item>org.eclipse.jetty.webapp.JettyWebXmlConfiguration</Item>
     </Array>
   </Set>
-
-  <New id="ReviewDb" class="org.eclipse.jetty.plus.jndi.Resource">
-    <Arg></Arg>
-    <Arg>jdbc/ReviewDb</Arg>
-    <Arg>
-      <New class="org.apache.commons.dbcp.BasicDataSource">
-<!--  PostgreSQL
-        <Set name="driverClassName">org.postgresql.Driver</Set>
-        <Set name="url">jdbc:postgresql:reviewdb</Set>
-        <Set name="username">gerrit</Set>
-        <Set name="password">secretkey</Set>
--->
-<!--  MySQL
-        <Set name="driverClassName">com.mysql.jdbc.Driver</Set>
-        <Set name="url">jdbc:mysql://localhost/reviewdb?user=gerrit&amp;password=secretkey</Set>
--->
-<!--  MariaDB
-        <Set name="driverClassName">org.mariadb.jdbc.Driver</Set>
-        <Set name="url">jdbc:mariadb://localhost/reviewdb?user=gerrit&amp;password=secretkey</Set>
--->
-<!--  H2
-        <Set name="driverClassName">org.h2.Driver</Set>
-        <Set name="url">jdbc:h2:file:ReviewDb</Set>
--->
-        <Set name="initialSize">4</Set>
-        <Set name="maxActive">8</Set>
-        <Set name="minIdle">4</Set>
-        <Set name="maxIdle">4</Set>
-        <Set name="maxWait">30000</Set>
-      </New>
-    </Arg>
-  </New>
 </Configure>
diff --git a/webapp/WEB-INF/web.xml b/webapp/WEB-INF/web.xml
index e901357..540fe33 100644
--- a/webapp/WEB-INF/web.xml
+++ b/webapp/WEB-INF/web.xml
@@ -1,11 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <web-app>
-  <resource-ref>
-    <res-ref-name>jdbc/ReviewDb</res-ref-name>
-    <res-type>javax.sql.DataSource</res-type>
-    <res-auth>Container</res-auth>
-  </resource-ref>
-
   <filter>
     <filter-name>guiceFilter</filter-name>
     <filter-class>com.google.gerrit.httpd.init.WebAppInitializer</filter-class>